a database layer insipred by caqti and ecto

test: add memory benchmarks and leak detection

Add comprehensive memory benchmarking suite using OCaml's Gc module:

bench/mem_bench.ml - Benchmark infrastructure:
- Allocation tracking (minor/major words, GC counts)
- Leak detection via repeated runs with heap monitoring
- Pretty-printed benchmark results

bench/bench_sqlite.ml - SQLite memory benchmarks
bench/bench_postgresql.ml - PostgreSQL memory benchmarks

Benchmarks cover:
- CRUD operations (insert, query, update, delete)
- Transactions
- Query DSL operations
- Complex queries (joins, grouping)

Leak detection runs 50 rounds of 100 operations each,
monitoring heap and live word growth between rounds.

Results (no leaks detected):
- SQLite: 0 word growth across all operations
- PostgreSQL: 0 word growth across all operations

Run with:
dune exec bench/bench_sqlite.exe
dune exec bench/bench_postgresql.exe

+272
bench/bench_postgresql.ml
··· 1 + open Mem_bench 2 + 3 + let conninfo = 4 + "host=localhost port=5432 dbname=repodb_test user=repodb password=repodb" 5 + 6 + let setup () = 7 + match Repodb_postgresql.connect conninfo with 8 + | Error e -> failwith (Repodb_postgresql.error_message e) 9 + | Ok conn -> 10 + let _ = 11 + Repodb_postgresql.exec conn "DROP TABLE IF EXISTS posts" ~params:[||] 12 + in 13 + let _ = 14 + Repodb_postgresql.exec conn "DROP TABLE IF EXISTS users" ~params:[||] 15 + in 16 + let _ = 17 + Repodb_postgresql.exec conn 18 + "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, \ 19 + email TEXT, age INTEGER, active BOOLEAN DEFAULT TRUE)" 20 + ~params:[||] 21 + in 22 + let _ = 23 + Repodb_postgresql.exec conn 24 + "CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INTEGER, title \ 25 + TEXT, body TEXT, created_at TIMESTAMP DEFAULT NOW())" 26 + ~params:[||] 27 + in 28 + let _ = 29 + Repodb_postgresql.exec conn 30 + "CREATE INDEX idx_posts_user_id ON posts(user_id)" ~params:[||] 31 + in 32 + conn 33 + 34 + let teardown conn = 35 + let _ = 36 + Repodb_postgresql.exec conn "DROP TABLE IF EXISTS posts" ~params:[||] 37 + in 38 + let _ = 39 + Repodb_postgresql.exec conn "DROP TABLE IF EXISTS users" ~params:[||] 40 + in 41 + Repodb_postgresql.close conn 42 + 43 + module Repo = Repodb.Repo.Make (Repodb_postgresql) 44 + 45 + let users_table = Repodb.Schema.table "users" 46 + 47 + let insert_user conn name email age = 48 + Repo.insert conn ~table:users_table ~columns:[ "name"; "email"; "age" ] 49 + ~values: 50 + [ 51 + Repodb.Driver.Value.text name; 52 + Repodb.Driver.Value.text email; 53 + Repodb.Driver.Value.int age; 54 + ] 55 + 56 + let query_all_users conn = 57 + Repodb_postgresql.query conn "SELECT * FROM users" ~params:[||] 58 + 59 + let query_user_by_id conn id = 60 + Repodb_postgresql.query_one conn "SELECT * FROM users WHERE id = $1" 61 + ~params:[| Repodb.Driver.Value.int id |] 62 + 63 + let update_user conn id new_age = 64 + Repo.update conn ~table:users_table ~columns:[ "age" ] 65 + ~values:[ Repodb.Driver.Value.int new_age ] 66 + ~where_column:"id" 67 + ~where_value:(Repodb.Driver.Value.int id) 68 + 69 + let delete_user conn id = 70 + Repo.delete conn ~table:users_table ~where_column:"id" 71 + ~where_value:(Repodb.Driver.Value.int id) 72 + 73 + let transaction_insert conn = 74 + Repo.transaction conn (fun conn -> 75 + let _ = insert_user conn "TxUser" "tx@example.com" 30 in 76 + Ok ()) 77 + 78 + let complex_query conn = 79 + Repodb_postgresql.query conn 80 + "SELECT u.*, COUNT(p.id) as post_count FROM users u LEFT JOIN posts p ON \ 81 + u.id = p.user_id GROUP BY u.id ORDER BY post_count DESC LIMIT 10" 82 + ~params:[||] 83 + 84 + let batch_insert conn n = 85 + for i = 1 to n do 86 + let _ = 87 + insert_user conn 88 + (Printf.sprintf "User%d" i) 89 + (Printf.sprintf "user%d@example.com" i) 90 + (20 + (i mod 50)) 91 + in 92 + () 93 + done 94 + 95 + let run_benchmarks () = 96 + Printf.printf "\n"; 97 + Printf.printf 98 + "╔═══════════════════════════════════════════════════════════════════════════════╗\n"; 99 + Printf.printf 100 + "║ REPODB POSTGRESQL MEMORY \ 101 + BENCHMARKS ║\n"; 102 + Printf.printf 103 + "╚═══════════════════════════════════════════════════════════════════════════════╝\n"; 104 + 105 + let conn = setup () in 106 + 107 + let _ = batch_insert conn 100 in 108 + 109 + let benchmarks = 110 + [ 111 + bench "insert (single row)" ~iterations:1000 (fun () -> 112 + insert_user conn "Test" "test@example.com" 25); 113 + bench "query_one (by id)" ~iterations:1000 (fun () -> 114 + query_user_by_id conn 1); 115 + bench "query_all (100+ rows)" ~iterations:100 (fun () -> 116 + query_all_users conn); 117 + bench "update (single row)" ~iterations:1000 (fun () -> 118 + update_user conn 1 26); 119 + bench "delete + insert" ~iterations:500 (fun () -> 120 + let _ = delete_user conn 1 in 121 + insert_user conn "Replaced" "replaced@example.com" 30); 122 + bench "transaction (insert)" ~iterations:500 (fun () -> 123 + transaction_insert conn); 124 + bench "complex query (join+group)" ~iterations:100 (fun () -> 125 + complex_query conn); 126 + ] 127 + in 128 + 129 + run_benchmarks "PostgreSQL Operations" benchmarks; 130 + 131 + let query_table = Repodb.Schema.table "users" in 132 + let query_benchmarks = 133 + [ 134 + bench "Query.from + all_query" ~iterations:500 (fun () -> 135 + let query = Repodb.Query.from query_table in 136 + Repo.all_query conn query ~decode:(fun row -> 137 + Repodb.Driver.row_int row 0)); 138 + bench "Query.where + one_query" ~iterations:500 (fun () -> 139 + let query = 140 + Repodb.Query.( 141 + from query_table |> where Repodb.Expr.(raw "id" = int 1)) 142 + in 143 + Repo.one_query conn query ~decode:(fun row -> 144 + Repodb.Driver.row_int row 0)); 145 + bench "Query.order+limit" ~iterations:500 (fun () -> 146 + let query = 147 + Repodb.Query.( 148 + from query_table |> desc (Repodb.Expr.raw "age") |> limit 10) 149 + in 150 + Repo.all_query conn query ~decode:(fun row -> 151 + Repodb.Driver.row_int row 0)); 152 + bench "Query.delete" ~iterations:100 (fun () -> 153 + let _ = insert_user conn "ToDelete" "delete@example.com" 99 in 154 + let query = 155 + Repodb.Query.( 156 + delete_from query_table 157 + |> where Repodb.Expr.(raw "email" = string "delete@example.com")) 158 + in 159 + Repo.delete_query conn query); 160 + ] 161 + in 162 + 163 + run_benchmarks "Query DSL Operations" query_benchmarks; 164 + 165 + teardown conn; 166 + 167 + let conn = setup () in 168 + Printf.printf "\n"; 169 + Printf.printf 170 + "╔═══════════════════════════════════════════════════════════════════════════════╗\n"; 171 + Printf.printf 172 + "║ MEMORY LEAK \ 173 + DETECTION ║\n"; 174 + Printf.printf 175 + "╚═══════════════════════════════════════════════════════════════════════════════╝\n"; 176 + 177 + let _ = 178 + leak_check "insert operations" ~rounds:50 ~ops_per_round:100 (fun () -> 179 + insert_user conn "LeakTest" "leak@example.com" 25) 180 + in 181 + 182 + let _ = batch_insert conn 1000 in 183 + 184 + let _ = 185 + leak_check "query operations" ~rounds:50 ~ops_per_round:100 (fun () -> 186 + query_all_users conn) 187 + in 188 + 189 + let _ = 190 + leak_check "transaction operations" ~rounds:50 ~ops_per_round:50 (fun () -> 191 + transaction_insert conn) 192 + in 193 + 194 + let _ = 195 + leak_check "Query DSL operations" ~rounds:50 ~ops_per_round:100 (fun () -> 196 + let query = 197 + Repodb.Query.( 198 + from query_table 199 + |> where Repodb.Expr.(raw "age" > int 25) 200 + |> limit 10) 201 + in 202 + Repo.all_query conn query ~decode:(fun row -> 203 + Repodb.Driver.row_int row 0)) 204 + in 205 + 206 + teardown conn; 207 + 208 + Printf.printf "\n"; 209 + Printf.printf 210 + "╔═══════════════════════════════════════════════════════════════════════════════╗\n"; 211 + Printf.printf 212 + "║ ALLOCATION \ 213 + ANALYSIS ║\n"; 214 + Printf.printf 215 + "╚═══════════════════════════════════════════════════════════════════════════════╝\n"; 216 + 217 + let conn = setup () in 218 + let _ = batch_insert conn 100 in 219 + 220 + Printf.printf "\nSingle operation allocation breakdown:\n\n"; 221 + 222 + Gc.full_major (); 223 + Gc.compact (); 224 + let before = get_stats () in 225 + let _ = insert_user conn "SingleTest" "single@example.com" 30 in 226 + let after = get_stats () in 227 + print_stats "Single INSERT" (diff_stats before after); 228 + 229 + Gc.full_major (); 230 + Gc.compact (); 231 + let before = get_stats () in 232 + let _ = query_all_users conn in 233 + let after = get_stats () in 234 + print_stats "Single SELECT (100+ rows)" (diff_stats before after); 235 + 236 + Gc.full_major (); 237 + Gc.compact (); 238 + let before = get_stats () in 239 + let _ = 240 + let query = 241 + Repodb.Query.( 242 + from query_table 243 + |> where Repodb.Expr.(raw "age" > int 25) 244 + |> desc (Repodb.Expr.raw "age") 245 + |> limit 10) 246 + in 247 + Repo.all_query conn query ~decode:(fun row -> Repodb.Driver.row_int row 0) 248 + in 249 + let after = get_stats () in 250 + print_stats "Query DSL (build + execute)" (diff_stats before after); 251 + 252 + Gc.full_major (); 253 + Gc.compact (); 254 + let before = get_stats () in 255 + let _ = 256 + Repo.transaction conn (fun conn -> 257 + insert_user conn "TxTest" "tx@example.com" 30) 258 + in 259 + let after = get_stats () in 260 + print_stats "Transaction (with INSERT)" (diff_stats before after); 261 + 262 + teardown conn; 263 + 264 + Printf.printf 265 + "╔═══════════════════════════════════════════════════════════════════════════════╗\n"; 266 + Printf.printf 267 + "║ BENCHMARK \ 268 + COMPLETE ║\n"; 269 + Printf.printf 270 + "╚═══════════════════════════════════════════════════════════════════════════════╝\n" 271 + 272 + let () = run_benchmarks ()
+261
bench/bench_sqlite.ml
··· 1 + open Mem_bench 2 + 3 + let db_path = "/tmp/repodb_bench.db" 4 + 5 + let setup () = 6 + (try Sys.remove db_path with Sys_error _ -> ()); 7 + match Repodb_sqlite.connect db_path with 8 + | Error e -> failwith (Repodb_sqlite.error_message e) 9 + | Ok conn -> 10 + let _ = 11 + Repodb_sqlite.exec conn 12 + "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, \ 13 + email TEXT, age INTEGER, active INTEGER DEFAULT 1)" 14 + ~params:[||] 15 + in 16 + let _ = 17 + Repodb_sqlite.exec conn 18 + "CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER, title \ 19 + TEXT, body TEXT, created_at TEXT)" 20 + ~params:[||] 21 + in 22 + let _ = 23 + Repodb_sqlite.exec conn 24 + "CREATE INDEX idx_posts_user_id ON posts(user_id)" ~params:[||] 25 + in 26 + conn 27 + 28 + let teardown conn = 29 + Repodb_sqlite.close conn; 30 + try Sys.remove db_path with Sys_error _ -> () 31 + 32 + module Repo = Repodb.Repo.Make (Repodb_sqlite) 33 + 34 + let users_table = Repodb.Schema.table "users" 35 + 36 + let insert_user conn name email age = 37 + Repo.insert conn ~table:users_table ~columns:[ "name"; "email"; "age" ] 38 + ~values: 39 + [ 40 + Repodb.Driver.Value.text name; 41 + Repodb.Driver.Value.text email; 42 + Repodb.Driver.Value.int age; 43 + ] 44 + 45 + let query_all_users conn = 46 + Repodb_sqlite.query conn "SELECT * FROM users" ~params:[||] 47 + 48 + let query_user_by_id conn id = 49 + Repodb_sqlite.query_one conn "SELECT * FROM users WHERE id = ?" 50 + ~params:[| Repodb.Driver.Value.int id |] 51 + 52 + let update_user conn id new_age = 53 + Repo.update conn ~table:users_table ~columns:[ "age" ] 54 + ~values:[ Repodb.Driver.Value.int new_age ] 55 + ~where_column:"id" 56 + ~where_value:(Repodb.Driver.Value.int id) 57 + 58 + let delete_user conn id = 59 + Repo.delete conn ~table:users_table ~where_column:"id" 60 + ~where_value:(Repodb.Driver.Value.int id) 61 + 62 + let transaction_insert conn = 63 + Repo.transaction conn (fun conn -> 64 + let _ = insert_user conn "TxUser" "tx@example.com" 30 in 65 + Ok ()) 66 + 67 + let complex_query conn = 68 + Repodb_sqlite.query conn 69 + "SELECT u.*, COUNT(p.id) as post_count FROM users u LEFT JOIN posts p ON \ 70 + u.id = p.user_id GROUP BY u.id ORDER BY post_count DESC LIMIT 10" 71 + ~params:[||] 72 + 73 + let batch_insert conn n = 74 + for i = 1 to n do 75 + let _ = 76 + insert_user conn 77 + (Printf.sprintf "User%d" i) 78 + (Printf.sprintf "user%d@example.com" i) 79 + (20 + (i mod 50)) 80 + in 81 + () 82 + done 83 + 84 + let run_benchmarks () = 85 + Printf.printf "\n"; 86 + Printf.printf 87 + "╔═══════════════════════════════════════════════════════════════════════════════╗\n"; 88 + Printf.printf 89 + "║ REPODB SQLITE MEMORY \ 90 + BENCHMARKS ║\n"; 91 + Printf.printf 92 + "╚═══════════════════════════════════════════════════════════════════════════════╝\n"; 93 + 94 + let conn = setup () in 95 + 96 + let _ = batch_insert conn 100 in 97 + 98 + let benchmarks = 99 + [ 100 + bench "insert (single row)" ~iterations:1000 (fun () -> 101 + insert_user conn "Test" "test@example.com" 25); 102 + bench "query_one (by id)" ~iterations:1000 (fun () -> 103 + query_user_by_id conn 1); 104 + bench "query_all (100 rows)" ~iterations:100 (fun () -> 105 + query_all_users conn); 106 + bench "update (single row)" ~iterations:1000 (fun () -> 107 + update_user conn 1 26); 108 + bench "delete + insert" ~iterations:500 (fun () -> 109 + let _ = delete_user conn 1 in 110 + insert_user conn "Replaced" "replaced@example.com" 30); 111 + bench "transaction (insert)" ~iterations:500 (fun () -> 112 + transaction_insert conn); 113 + bench "complex query (join+group)" ~iterations:100 (fun () -> 114 + complex_query conn); 115 + ] 116 + in 117 + 118 + run_benchmarks "SQLite Operations" benchmarks; 119 + 120 + let query_table = Repodb.Schema.table "users" in 121 + let query_benchmarks = 122 + [ 123 + bench "Query.from + all_query" ~iterations:500 (fun () -> 124 + let query = Repodb.Query.from query_table in 125 + Repo.all_query conn query ~decode:(fun row -> 126 + Repodb.Driver.row_int row 0)); 127 + bench "Query.where + one_query" ~iterations:500 (fun () -> 128 + let query = 129 + Repodb.Query.( 130 + from query_table |> where Repodb.Expr.(raw "id" = int 1)) 131 + in 132 + Repo.one_query conn query ~decode:(fun row -> 133 + Repodb.Driver.row_int row 0)); 134 + bench "Query.order+limit" ~iterations:500 (fun () -> 135 + let query = 136 + Repodb.Query.( 137 + from query_table |> desc (Repodb.Expr.raw "age") |> limit 10) 138 + in 139 + Repo.all_query conn query ~decode:(fun row -> 140 + Repodb.Driver.row_int row 0)); 141 + bench "Query.delete" ~iterations:100 (fun () -> 142 + let _ = insert_user conn "ToDelete" "delete@example.com" 99 in 143 + let query = 144 + Repodb.Query.( 145 + delete_from query_table 146 + |> where Repodb.Expr.(raw "email" = string "delete@example.com")) 147 + in 148 + Repo.delete_query conn query); 149 + ] 150 + in 151 + 152 + run_benchmarks "Query DSL Operations" query_benchmarks; 153 + 154 + teardown conn; 155 + 156 + let conn = setup () in 157 + Printf.printf "\n"; 158 + Printf.printf 159 + "╔═══════════════════════════════════════════════════════════════════════════════╗\n"; 160 + Printf.printf 161 + "║ MEMORY LEAK \ 162 + DETECTION ║\n"; 163 + Printf.printf 164 + "╚═══════════════════════════════════════════════════════════════════════════════╝\n"; 165 + 166 + let _ = 167 + leak_check "insert operations" ~rounds:50 ~ops_per_round:100 (fun () -> 168 + insert_user conn "LeakTest" "leak@example.com" 25) 169 + in 170 + 171 + let _ = batch_insert conn 1000 in 172 + 173 + let _ = 174 + leak_check "query operations" ~rounds:50 ~ops_per_round:100 (fun () -> 175 + query_all_users conn) 176 + in 177 + 178 + let _ = 179 + leak_check "transaction operations" ~rounds:50 ~ops_per_round:50 (fun () -> 180 + transaction_insert conn) 181 + in 182 + 183 + let _ = 184 + leak_check "Query DSL operations" ~rounds:50 ~ops_per_round:100 (fun () -> 185 + let query = 186 + Repodb.Query.( 187 + from query_table 188 + |> where Repodb.Expr.(raw "age" > int 25) 189 + |> limit 10) 190 + in 191 + Repo.all_query conn query ~decode:(fun row -> 192 + Repodb.Driver.row_int row 0)) 193 + in 194 + 195 + teardown conn; 196 + 197 + Printf.printf "\n"; 198 + Printf.printf 199 + "╔═══════════════════════════════════════════════════════════════════════════════╗\n"; 200 + Printf.printf 201 + "║ ALLOCATION \ 202 + ANALYSIS ║\n"; 203 + Printf.printf 204 + "╚═══════════════════════════════════════════════════════════════════════════════╝\n"; 205 + 206 + let conn = setup () in 207 + let _ = batch_insert conn 100 in 208 + 209 + Printf.printf "\nSingle operation allocation breakdown:\n\n"; 210 + 211 + Gc.full_major (); 212 + Gc.compact (); 213 + let before = get_stats () in 214 + let _ = insert_user conn "SingleTest" "single@example.com" 30 in 215 + let after = get_stats () in 216 + print_stats "Single INSERT" (diff_stats before after); 217 + 218 + Gc.full_major (); 219 + Gc.compact (); 220 + let before = get_stats () in 221 + let _ = query_all_users conn in 222 + let after = get_stats () in 223 + print_stats "Single SELECT (100 rows)" (diff_stats before after); 224 + 225 + Gc.full_major (); 226 + Gc.compact (); 227 + let before = get_stats () in 228 + let _ = 229 + let query = 230 + Repodb.Query.( 231 + from query_table 232 + |> where Repodb.Expr.(raw "age" > int 25) 233 + |> desc (Repodb.Expr.raw "age") 234 + |> limit 10) 235 + in 236 + Repo.all_query conn query ~decode:(fun row -> Repodb.Driver.row_int row 0) 237 + in 238 + let after = get_stats () in 239 + print_stats "Query DSL (build + execute)" (diff_stats before after); 240 + 241 + Gc.full_major (); 242 + Gc.compact (); 243 + let before = get_stats () in 244 + let _ = 245 + Repo.transaction conn (fun conn -> 246 + insert_user conn "TxTest" "tx@example.com" 30) 247 + in 248 + let after = get_stats () in 249 + print_stats "Transaction (with INSERT)" (diff_stats before after); 250 + 251 + teardown conn; 252 + 253 + Printf.printf 254 + "╔═══════════════════════════════════════════════════════════════════════════════╗\n"; 255 + Printf.printf 256 + "║ BENCHMARK \ 257 + COMPLETE ║\n"; 258 + Printf.printf 259 + "╚═══════════════════════════════════════════════════════════════════════════════╝\n" 260 + 261 + let () = run_benchmarks ()
+14
bench/dune
··· 1 + (library 2 + (name mem_bench) 3 + (modules mem_bench) 4 + (libraries unix)) 5 + 6 + (executable 7 + (name bench_sqlite) 8 + (libraries mem_bench repodb repodb_sqlite unix) 9 + (modules bench_sqlite)) 10 + 11 + (executable 12 + (name bench_postgresql) 13 + (libraries mem_bench repodb repodb_postgresql unix) 14 + (modules bench_postgresql))
+128
bench/mem_bench.ml
··· 1 + type alloc_stats = { 2 + minor_words : float; 3 + major_words : float; 4 + minor_collections : int; 5 + major_collections : int; 6 + heap_words : int; 7 + live_words : int; 8 + } 9 + 10 + let get_stats () = 11 + Gc.full_major (); 12 + let stat = Gc.stat () in 13 + { 14 + minor_words = stat.minor_words; 15 + major_words = stat.major_words; 16 + minor_collections = stat.minor_collections; 17 + major_collections = stat.major_collections; 18 + heap_words = stat.heap_words; 19 + live_words = stat.live_words; 20 + } 21 + 22 + let diff_stats before after = 23 + { 24 + minor_words = after.minor_words -. before.minor_words; 25 + major_words = after.major_words -. before.major_words; 26 + minor_collections = after.minor_collections - before.minor_collections; 27 + major_collections = after.major_collections - before.major_collections; 28 + heap_words = after.heap_words - before.heap_words; 29 + live_words = after.live_words - before.live_words; 30 + } 31 + 32 + let print_stats label stats = 33 + Printf.printf "=== %s ===\n" label; 34 + Printf.printf " Minor words: %.0f (%.2f KB)\n" stats.minor_words 35 + (stats.minor_words *. 8.0 /. 1024.0); 36 + Printf.printf " Major words: %.0f (%.2f KB)\n" stats.major_words 37 + (stats.major_words *. 8.0 /. 1024.0); 38 + Printf.printf " Minor GCs: %d\n" stats.minor_collections; 39 + Printf.printf " Major GCs: %d\n" stats.major_collections; 40 + Printf.printf " Heap words: %d (%.2f KB)\n" stats.heap_words 41 + (float_of_int stats.heap_words *. 8.0 /. 1024.0); 42 + Printf.printf " Live words: %d (%.2f KB)\n" stats.live_words 43 + (float_of_int stats.live_words *. 8.0 /. 1024.0); 44 + Printf.printf "\n" 45 + 46 + type bench_result = { 47 + name : string; 48 + iterations : int; 49 + total_time_ms : float; 50 + per_op_time_us : float; 51 + allocs : alloc_stats; 52 + allocs_per_op : float; 53 + } 54 + 55 + let print_bench_result r = 56 + Printf.printf "%-30s | %6d iters | %8.2f ms | %8.2f µs/op | %10.0f words/op\n" 57 + r.name r.iterations r.total_time_ms r.per_op_time_us r.allocs_per_op 58 + 59 + let bench name ~iterations f = 60 + Gc.full_major (); 61 + Gc.compact (); 62 + let before = get_stats () in 63 + let t0 = Unix.gettimeofday () in 64 + for _ = 1 to iterations do 65 + ignore (f ()) 66 + done; 67 + let t1 = Unix.gettimeofday () in 68 + let after = get_stats () in 69 + let allocs = diff_stats before after in 70 + let total_time_ms = (t1 -. t0) *. 1000.0 in 71 + let per_op_time_us = total_time_ms *. 1000.0 /. float_of_int iterations in 72 + let allocs_per_op = allocs.minor_words /. float_of_int iterations in 73 + { name; iterations; total_time_ms; per_op_time_us; allocs; allocs_per_op } 74 + 75 + let leak_check name ~rounds ~ops_per_round f = 76 + Printf.printf "\n=== Leak Check: %s ===\n" name; 77 + Printf.printf "Running %d rounds of %d operations each...\n\n" rounds 78 + ops_per_round; 79 + let heap_sizes = Array.make rounds 0 in 80 + let live_sizes = Array.make rounds 0 in 81 + for round = 0 to rounds - 1 do 82 + for _ = 1 to ops_per_round do 83 + ignore (f ()) 84 + done; 85 + Gc.full_major (); 86 + Gc.compact (); 87 + let stat = Gc.stat () in 88 + heap_sizes.(round) <- stat.heap_words; 89 + live_sizes.(round) <- stat.live_words; 90 + if round mod 10 = 0 || round = rounds - 1 then 91 + Printf.printf " Round %3d: heap=%d words, live=%d words\n" (round + 1) 92 + stat.heap_words stat.live_words 93 + done; 94 + let first_heap = heap_sizes.(0) in 95 + let last_heap = heap_sizes.(rounds - 1) in 96 + let first_live = live_sizes.(0) in 97 + let last_live = live_sizes.(rounds - 1) in 98 + let heap_growth = last_heap - first_heap in 99 + let live_growth = last_live - first_live in 100 + Printf.printf "\nResults:\n"; 101 + Printf.printf " Heap growth: %d words (%.2f KB)\n" heap_growth 102 + (float_of_int heap_growth *. 8.0 /. 1024.0); 103 + Printf.printf " Live growth: %d words (%.2f KB)\n" live_growth 104 + (float_of_int live_growth *. 8.0 /. 1024.0); 105 + if live_growth > 1000 then 106 + Printf.printf " ⚠️ POTENTIAL LEAK: Live memory grew by %d words\n" 107 + live_growth 108 + else Printf.printf " ✓ No significant memory leak detected\n"; 109 + (heap_growth, live_growth) 110 + 111 + let run_benchmarks name benchmarks = 112 + Printf.printf "\n"; 113 + Printf.printf 114 + "╔═══════════════════════════════════════════════════════════════════════════════╗\n"; 115 + Printf.printf "║ %-77s ║\n" (Printf.sprintf "BENCHMARK: %s" name); 116 + Printf.printf 117 + "╠═══════════════════════════════════════════════════════════════════════════════╣\n"; 118 + Printf.printf "║ %-30s | %6s | %8s | %11s | %14s ║\n" "Operation" "Iters" 119 + "Total" "Per Op" "Allocs/Op"; 120 + Printf.printf 121 + "╠═══════════════════════════════════════════════════════════════════════════════╣\n"; 122 + List.iter 123 + (fun r -> 124 + Printf.printf "║ %-30s | %6d | %6.1f ms | %8.2f µs | %11.0f w ║\n" r.name 125 + r.iterations r.total_time_ms r.per_op_time_us r.allocs_per_op) 126 + benchmarks; 127 + Printf.printf 128 + "╚═══════════════════════════════════════════════════════════════════════════════╝\n"