aboutsummaryrefslogblamecommitdiffstats
path: root/bin/bench.ml
blob: 8f496f216b93f725614c4fb4a89e5e9e2c8482fa (plain) (tree)









































































































































































































                                                                                          
open Relational_engine

(*  helpers  *)

let time label f =
  let t0 = Unix.gettimeofday () in
  f ();
  let elapsed = Unix.gettimeofday () -. t0 in
  Printf.printf "  %-60s %.4f s\n%!" label elapsed

let mk_int (n : int) : Conventions.AbstractValue.t = Obj.magic n

let make_tuple relation pairs =
  let attributes =
    List.fold_left
      (fun acc (k, v) ->
        Tuple.AttributeMap.add k { Attribute.value = mk_int v } acc)
      Tuple.AttributeMap.empty
      pairs
  in
  { Tuple.relation; attributes }

let unwrap label = function
  | Ok v    -> v
  | Error _ -> failwith label

let get_rel db name =
  match Manipulation.Memory.get_relation db ~name with
  | Some r -> r
  | None   -> failwith ("relation not found: " ^ name)

(*  database setup  *)

(*
  Schema:
    Department { dept_id }
    Employee   { emp_id, dept_id }

  Constraint on Employee:
    Exists d in Department, MemberOf Department (dept_id = Var "dept_id")

  This is a classic FK: every employee must reference an existing department.
  On the sublanguages branch, deleting a referenced Department triggers a
  cascade re-check of affected Employee tuples and rejects the delete.
  On master (no cascade), the delete succeeds unconditionally.
*)
let setup storage ~n_depts ~employees_per_dept =
  let db =
    unwrap "create_database"
      (Manipulation.Memory.create_database storage ~name:"bench")
  in
  let db =
    fst (unwrap "create Department"
      (Manipulation.Memory.create_relation storage db ~name:"Department"
         ~schema:(Schema.empty |> Schema.add "dept_id" "natural")))
  in
  let db =
    fst (unwrap "create Employee"
      (Manipulation.Memory.create_relation storage db ~name:"Employee"
         ~schema:(Schema.empty
                  |> Schema.add "emp_id"  "natural"
                  |> Schema.add "dept_id" "natural")))
  in
  let fk_body =
    Constraint.Exists {
      variable   = "d";
      quantifier = "Department";
      body = Constraint.MemberOf {
        target  = "Department";
        binding = Constraint.BindingMap.singleton "dept_id" (Constraint.Var "dept_id");
      };
    }
  in
  let db =
    unwrap "register_constraint"
      (Manipulation.Memory.register_constraint storage db
         ~constraint_name:"fk_dept"
         ~relation_name:"Employee"
         ~body:fk_body)
  in
  let dept_tuples =
    List.init n_depts (fun i ->
      make_tuple "Department" [("dept_id", i + 1)])
  in
  let db =
    let (db, _, _) = unwrap "insert departments"
      (Manipulation.Memory.create_tuples storage db
         (get_rel db "Department") dept_tuples)
    in db
  in
  let emp_tuples =
    List.init (n_depts * employees_per_dept) (fun i ->
      make_tuple "Employee" [
        ("emp_id",  i + 1);
        ("dept_id", (i mod n_depts) + 1);
      ])
  in
  let db =
    let (db, _, _) = unwrap "insert employees"
      (Manipulation.Memory.create_tuples storage db
         (get_rel db "Employee") emp_tuples)
    in db
  in
  db

(*  benchmark cases  *)

(*
  Attempt to delete each department that has employees referencing it.

  sublanguages: retract_tuple runs the cascade check — uses polarity analysis
  to identify Employee as the constrained relation, then focused_filter to
  narrow the scan to only employees with dept_id = deleted dept_id. Finds a
  match → returns ConstraintViolation. Delete is rejected.

  master: no cascade logic; retract_tuple succeeds unconditionally and db
  becomes inconsistent.

  db is not threaded through since on sublanguages every delete is rejected
  (db state is unchanged after each Error), and on master we deliberately
  want to measure per-delete cost against a consistent employee set.
*)
let bench_retract_referenced storage db ~n_depts =
  let rel = get_rel db "Department" in
  for i = 1 to n_depts do
    let hash = Hashing.hash_tuple (make_tuple "Department" [("dept_id", i)]) in
    ignore (Manipulation.Memory.retract_tuple storage db rel ~tuple_hash:hash)
  done

(*
  Delete departments that have NO referencing employees. Neither branch
  needs to scan Employee tuples (focused_filter finds no matches), so this
  measures raw retract_tuple cost: Merkle tree update + storage write.

  We thread db through the loop so each delete builds on the previous state
  and only one live tree version accumulates in memory at a time.

  Orphan dept ids start at offset to avoid colliding with the employee-owned
  depts (1..n_depts).
*)
let bench_retract_unreferenced storage db ~n_depts ~offset =
  let orphan_tuples =
    List.init n_depts (fun i ->
      make_tuple "Department" [("dept_id", offset + i + 1)])
  in
  let db =
    let (db, _, _) = unwrap "insert orphan depts"
      (Manipulation.Memory.create_tuples storage db
         (get_rel db "Department") orphan_tuples)
    in db
  in
  let _db =
    List.fold_left (fun db i ->
      let rel  = get_rel db "Department" in
      let hash = Hashing.hash_tuple (make_tuple "Department" [("dept_id", offset + i)]) in
      match Manipulation.Memory.retract_tuple storage db rel ~tuple_hash:hash with
      | Ok (db, _) -> db
      | Error _    -> db)
      db
      (List.init n_depts (fun i -> i + 1))
  in
  ()

(*  entry point  *)

let () =
  let n_depts            = 100 in
  let employees_per_dept = 200 in
  let total_employees    = n_depts * employees_per_dept in

  Printf.printf "\nSakura constraint benchmark\n";
  Printf.printf "  Departments    : %d\n"   n_depts;
  Printf.printf "  Employees/dept : %d  (%d total)\n\n"
    employees_per_dept total_employees;

  let make_storage () =
    match Management.Physical.Memory.create () with
    | Ok s    -> s
    | Error _ -> failwith "create storage"
  in

  (* Warmup build — not measured *)
  let storage = make_storage () in
  ignore (setup storage ~n_depts ~employees_per_dept);

  (* Fresh storage for measured runs *)
  let storage = make_storage () in
  let db = setup storage ~n_depts ~employees_per_dept in

  Printf.printf "Scenarios:\n";

  time (Printf.sprintf
          "retract %d referenced depts (cascade check, violations expected)"
          n_depts)
    (fun () -> bench_retract_referenced storage db ~n_depts);

  time (Printf.sprintf
          "retract %d unreferenced depts (no matching employees)"
          n_depts)
    (fun () -> bench_retract_unreferenced storage db ~n_depts ~offset:1_000_000);

  Printf.printf "\n"