TCP/TLS connection pooling for Eio

html output

+642 -118
+1 -11
test/dune
··· 1 1 (executable 2 2 (name stress_test) 3 - (modules stress_test trace) 3 + (modules stress_test) 4 4 (libraries conpool eio eio_main unix)) 5 5 6 - (executable 7 - (name visualize) 8 - (modules visualize) 9 - (libraries str)) 10 - 11 6 (rule 12 7 (alias runtest) 13 8 (deps stress_test.exe) 14 9 (action (run ./stress_test.exe --all -o stress_test_results.json))) 15 - 16 - (rule 17 - (alias runtest) 18 - (deps visualize.exe stress_test_results.json) 19 - (action (run ./visualize.exe -i stress_test_results.json -o stress_test_results.html)))
+641 -107
test/stress_test.ml
··· 106 106 mutable total : float; 107 107 mutable min : float; 108 108 mutable max : float; 109 + mutable latencies : (float * float) list; (* (timestamp, latency) pairs *) 109 110 } 110 111 111 112 let create_latency_stats () = { ··· 113 114 total = 0.0; 114 115 min = Float.infinity; 115 116 max = 0.0; 117 + latencies = []; 116 118 } 117 119 118 - let update_latency stats latency = 120 + let update_latency stats latency timestamp = 119 121 stats.count <- stats.count + 1; 120 122 stats.total <- stats.total +. latency; 121 123 stats.min <- min stats.min latency; 122 - stats.max <- max stats.max latency 124 + stats.max <- max stats.max latency; 125 + stats.latencies <- (timestamp, latency) :: stats.latencies 123 126 124 127 (** Generate a random message of given size *) 125 128 let generate_message size = ··· 165 168 port 166 169 167 170 (** Client test: connect via pool, send message, verify echo *) 168 - let run_client_test ~clock ~collector pool endpoint endpoint_id message client_id latency_stats errors = 171 + let run_client_test ~clock ~test_start_time pool endpoint message latency_stats errors = 169 172 let msg_len = String.length message in 170 173 let start_time = Eio.Time.now clock in 171 - 172 - (* Get or create connection ID for tracking *) 173 - let conn_id = Trace.next_connection_id collector in 174 174 175 175 try 176 176 Conpool.with_connection pool endpoint (fun flow -> 177 - (* Record acquire event *) 178 - Trace.record collector ~clock ~event_type:Trace.Connection_acquired 179 - ~endpoint_id ~connection_id:conn_id ~client_id (); 180 - 181 177 (* Send message *) 182 178 Eio.Flow.copy_string message flow; 183 179 Eio.Flow.copy_string "\n" flow; 184 - Trace.record collector ~clock ~event_type:Trace.Message_sent 185 - ~endpoint_id ~connection_id:conn_id ~client_id (); 186 180 187 181 (* Read echo response *) 188 182 let response = Eio.Buf_read.of_flow flow ~max_size:(msg_len + 1) in 189 183 let echoed = Eio.Buf_read.line response in 190 - Trace.record collector ~clock ~event_type:Trace.Message_received 191 - ~endpoint_id ~connection_id:conn_id ~client_id (); 192 184 193 185 let end_time = Eio.Time.now clock in 194 186 let latency = (end_time -. start_time) *. 1000.0 in (* Convert to ms *) 187 + let relative_time = (end_time -. test_start_time) *. 1000.0 in (* ms since test start *) 195 188 196 189 if String.equal echoed message then begin 197 - update_latency latency_stats latency; 198 - Trace.record collector ~clock ~event_type:Trace.Message_verified 199 - ~endpoint_id ~connection_id:conn_id ~client_id () 190 + update_latency latency_stats latency relative_time 200 191 end else begin 201 - incr errors; 202 - Trace.record collector ~clock ~event_type:(Trace.Connection_error "echo_mismatch") 203 - ~endpoint_id ~connection_id:conn_id ~client_id () 204 - end; 205 - 206 - (* Record release event *) 207 - Trace.record collector ~clock ~event_type:Trace.Connection_released 208 - ~endpoint_id ~connection_id:conn_id ~client_id () 192 + incr errors 193 + end 209 194 ) 210 - with ex -> 211 - incr errors; 212 - Trace.record collector ~clock ~event_type:(Trace.Connection_error (Printexc.to_string ex)) 213 - ~endpoint_id ~connection_id:conn_id ~client_id () 195 + with _ex -> 196 + incr errors 214 197 215 198 (** Run a single client that sends multiple messages *) 216 - let run_client ~clock ~collector pool endpoints config latency_stats errors client_id = 217 - for _ = 1 to config.messages_per_client do 199 + let run_client ~clock ~test_start_time pool endpoints (cfg : config) latency_stats errors client_id = 200 + for _ = 1 to cfg.messages_per_client do 218 201 let endpoint_idx = Random.int (Array.length endpoints) in 219 202 let endpoint = endpoints.(endpoint_idx) in 220 - let message = Printf.sprintf "c%d-%s" client_id (generate_message config.message_size) in 221 - run_client_test ~clock ~collector pool endpoint endpoint_idx message client_id latency_stats errors 203 + let message = Printf.sprintf "c%d-%s" client_id (generate_message cfg.message_size) in 204 + run_client_test ~clock ~test_start_time pool endpoint message latency_stats errors 222 205 done 223 206 224 - (** Main stress test runner - returns a test trace *) 225 - let run_stress_test ~env config : Trace.test_trace = 207 + (** Pool statistics aggregated from all endpoints *) 208 + type pool_stats = { 209 + total_created : int; 210 + total_reused : int; 211 + total_closed : int; 212 + active : int; 213 + idle : int; 214 + pool_errors : int; 215 + } 216 + 217 + (** Test result type *) 218 + type test_result = { 219 + test_name : string; 220 + num_servers : int; 221 + num_clients : int; 222 + messages_per_client : int; 223 + pool_size : int; 224 + duration : float; 225 + total_messages : int; 226 + total_errors : int; 227 + throughput : float; 228 + avg_latency : float; 229 + min_latency : float; 230 + max_latency : float; 231 + latency_data : (float * float) list; (* (timestamp, latency) pairs for visualization *) 232 + pool_stats : pool_stats; 233 + } 234 + 235 + (** Main stress test runner - returns a test result *) 236 + let run_stress_test ~env (cfg : config) : test_result = 226 237 let net = Eio.Stdenv.net env in 227 238 let clock = Eio.Stdenv.clock env in 228 239 229 - let collector = Trace.create_collector () in 230 240 let latency_stats = create_latency_stats () in 231 241 let errors = ref 0 in 232 242 let ports = ref [||] in 233 243 234 - let trace_config : Trace.test_config = { 235 - num_servers = config.num_servers; 236 - num_clients = config.num_clients; 237 - messages_per_client = config.messages_per_client; 238 - max_parallel_clients = config.max_parallel_clients; 239 - message_size = config.message_size; 240 - pool_size = config.pool_size; 241 - } in 242 - 243 - let start_unix_time = Unix.gettimeofday () in 244 - 245 - let result = ref None in 244 + let result : test_result option ref = ref None in 246 245 247 246 begin 248 247 try 249 248 Eio.Switch.run @@ fun sw -> 250 249 (* Start echo servers *) 251 - ports := Array.init config.num_servers (fun _ -> 250 + ports := Array.init cfg.num_servers (fun _ -> 252 251 start_echo_server ~sw net 253 252 ); 254 253 ··· 258 257 Conpool.Endpoint.make ~host:"127.0.0.1" ~port 259 258 ) !ports in 260 259 261 - (* Create connection pool with hooks to track events *) 260 + (* Create connection pool *) 262 261 let pool_config = Conpool.Config.make 263 - ~max_connections_per_endpoint:config.pool_size 262 + ~max_connections_per_endpoint:cfg.pool_size 264 263 ~max_idle_time:30.0 265 264 ~max_connection_lifetime:120.0 266 265 ~connect_timeout:5.0 267 266 ~connect_retry_count:3 268 - ~on_connection_created:(fun ep -> 269 - let port = Conpool.Endpoint.port ep in 270 - let endpoint_id = Array.to_list !ports 271 - |> List.mapi (fun i p -> (i, p)) 272 - |> List.find (fun (_, p) -> p = port) 273 - |> fst in 274 - let conn_id = Trace.next_connection_id collector in 275 - Trace.record collector ~clock ~event_type:Trace.Connection_created 276 - ~endpoint_id ~connection_id:conn_id () 277 - ) 278 - ~on_connection_reused:(fun ep -> 279 - let port = Conpool.Endpoint.port ep in 280 - let endpoint_id = Array.to_list !ports 281 - |> List.mapi (fun i p -> (i, p)) 282 - |> List.find (fun (_, p) -> p = port) 283 - |> fst in 284 - let conn_id = Trace.next_connection_id collector in 285 - Trace.record collector ~clock ~event_type:Trace.Connection_reused 286 - ~endpoint_id ~connection_id:conn_id () 287 - ) 288 - ~on_connection_closed:(fun ep -> 289 - let port = Conpool.Endpoint.port ep in 290 - let endpoint_id = Array.to_list !ports 291 - |> List.mapi (fun i p -> (i, p)) 292 - |> List.find (fun (_, p) -> p = port) 293 - |> fst in 294 - let conn_id = Trace.next_connection_id collector in 295 - Trace.record collector ~clock ~event_type:Trace.Connection_closed 296 - ~endpoint_id ~connection_id:conn_id () 297 - ) 298 267 () 299 268 in 300 269 ··· 302 271 303 272 (* Record start time *) 304 273 let start_time = Eio.Time.now clock in 305 - Trace.set_start_time collector start_time; 306 274 307 275 (* Run clients in parallel *) 308 - let total_clients = config.num_servers * config.num_clients in 276 + let total_clients = cfg.num_servers * cfg.num_clients in 309 277 let client_ids = List.init total_clients (fun i -> i) in 310 - Eio.Fiber.List.iter ~max_fibers:config.max_parallel_clients 278 + Eio.Fiber.List.iter ~max_fibers:cfg.max_parallel_clients 311 279 (fun client_id -> 312 - run_client ~clock ~collector pool endpoints config latency_stats errors client_id) 280 + run_client ~clock ~test_start_time:start_time pool endpoints cfg latency_stats errors client_id) 313 281 client_ids; 314 282 315 283 let end_time = Eio.Time.now clock in 316 284 let duration = end_time -. start_time in 317 285 286 + (* Collect pool statistics from all endpoints *) 287 + let all_stats = Conpool.all_stats pool in 288 + let pool_stats = List.fold_left (fun acc (_, stats) -> 289 + { 290 + total_created = acc.total_created + Conpool.Stats.total_created stats; 291 + total_reused = acc.total_reused + Conpool.Stats.total_reused stats; 292 + total_closed = acc.total_closed + Conpool.Stats.total_closed stats; 293 + active = acc.active + Conpool.Stats.active stats; 294 + idle = acc.idle + Conpool.Stats.idle stats; 295 + pool_errors = acc.pool_errors + Conpool.Stats.errors stats; 296 + } 297 + ) { total_created = 0; total_reused = 0; total_closed = 0; active = 0; idle = 0; pool_errors = 0 } all_stats in 298 + 318 299 (* Build result *) 319 - let events = Trace.get_events collector in 320 - let endpoint_summaries = Trace.compute_endpoint_summaries events config.num_servers !ports in 321 - 322 - result := Some { 323 - Trace.test_name = config.name; 324 - config = trace_config; 325 - start_time = start_unix_time; 300 + let r : test_result = { 301 + test_name = cfg.name; 302 + num_servers = cfg.num_servers; 303 + num_clients = cfg.num_clients; 304 + messages_per_client = cfg.messages_per_client; 305 + pool_size = cfg.pool_size; 326 306 duration; 327 - events; 328 - endpoint_summaries; 329 307 total_messages = latency_stats.count; 330 308 total_errors = !errors; 331 309 throughput = float_of_int latency_stats.count /. duration; ··· 334 312 else 0.0; 335 313 min_latency = if latency_stats.count > 0 then latency_stats.min else 0.0; 336 314 max_latency = latency_stats.max; 337 - }; 315 + latency_data = List.rev latency_stats.latencies; 316 + pool_stats; 317 + } in 318 + result := Some r; 338 319 339 320 Eio.Switch.fail sw Exit 340 321 with Exit -> () ··· 344 325 | Some r -> r 345 326 | None -> failwith "Test failed to produce result" 346 327 347 - (** Run all preset tests and return traces *) 328 + (** Convert result to JSON string *) 329 + let result_to_json result = 330 + Printf.sprintf {|{ 331 + "test_name": "%s", 332 + "num_servers": %d, 333 + "num_clients": %d, 334 + "messages_per_client": %d, 335 + "duration": %.3f, 336 + "total_messages": %d, 337 + "total_errors": %d, 338 + "throughput": %.2f, 339 + "avg_latency": %.2f, 340 + "min_latency": %.2f, 341 + "max_latency": %.2f 342 + }|} 343 + result.test_name 344 + result.num_servers 345 + result.num_clients 346 + result.messages_per_client 347 + result.duration 348 + result.total_messages 349 + result.total_errors 350 + result.throughput 351 + result.avg_latency 352 + result.min_latency 353 + result.max_latency 354 + 355 + (** Escape strings for JavaScript *) 356 + let js_escape s = 357 + let buf = Buffer.create (String.length s) in 358 + String.iter (fun c -> 359 + match c with 360 + | '\\' -> Buffer.add_string buf "\\\\" 361 + | '"' -> Buffer.add_string buf "\\\"" 362 + | '\n' -> Buffer.add_string buf "\\n" 363 + | '\r' -> Buffer.add_string buf "\\r" 364 + | '\t' -> Buffer.add_string buf "\\t" 365 + | _ -> Buffer.add_char buf c 366 + ) s; 367 + Buffer.contents buf 368 + 369 + (** Calculate histogram buckets for latency data *) 370 + let calculate_histogram latencies num_buckets = 371 + if List.length latencies = 0 then ([], []) else 372 + let latency_values = List.map snd latencies in 373 + let min_lat = List.fold_left min Float.infinity latency_values in 374 + let max_lat = List.fold_left max 0.0 latency_values in 375 + let bucket_width = (max_lat -. min_lat) /. float_of_int num_buckets in 376 + 377 + let buckets = Array.make num_buckets 0 in 378 + List.iter (fun lat -> 379 + let bucket_idx = min (num_buckets - 1) (int_of_float ((lat -. min_lat) /. bucket_width)) in 380 + buckets.(bucket_idx) <- buckets.(bucket_idx) + 1 381 + ) latency_values; 382 + 383 + let bucket_labels = List.init num_buckets (fun i -> 384 + let start = min_lat +. (float_of_int i *. bucket_width) in 385 + Printf.sprintf "%.2f" start 386 + ) in 387 + let bucket_counts = Array.to_list buckets in 388 + (bucket_labels, bucket_counts) 389 + 390 + (** Generate HTML report from test results *) 391 + let generate_html_report results = 392 + let timestamp = Unix.time () |> Unix.gmtime in 393 + let date_str = Printf.sprintf "%04d-%02d-%02d %02d:%02d:%02d UTC" 394 + (timestamp.Unix.tm_year + 1900) 395 + (timestamp.Unix.tm_mon + 1) 396 + timestamp.Unix.tm_mday 397 + timestamp.Unix.tm_hour 398 + timestamp.Unix.tm_min 399 + timestamp.Unix.tm_sec 400 + in 401 + 402 + (* Calculate summary statistics *) 403 + let total_messages = List.fold_left (fun acc r -> acc + r.total_messages) 0 results in 404 + let total_errors = List.fold_left (fun acc r -> acc + r.total_errors) 0 results in 405 + let total_duration = List.fold_left (fun acc r -> acc +. r.duration) 0.0 results in 406 + 407 + (* Generate JavaScript arrays for comparison charts *) 408 + let test_names = String.concat ", " (List.map (fun r -> Printf.sprintf "\"%s\"" (js_escape r.test_name)) results) in 409 + let throughputs = String.concat ", " (List.map (fun r -> Printf.sprintf "%.2f" r.throughput) results) in 410 + let avg_latencies = String.concat ", " (List.map (fun r -> Printf.sprintf "%.2f" r.avg_latency) results) in 411 + let error_rates = String.concat ", " (List.map (fun r -> 412 + if r.total_messages > 0 then 413 + Printf.sprintf "%.2f" (float_of_int r.total_errors /. float_of_int r.total_messages *. 100.0) 414 + else "0.0" 415 + ) results) in 416 + 417 + (* Generate per-test detailed sections with histograms and timelines *) 418 + let test_details = String.concat "\n" (List.mapi (fun idx r -> 419 + let (hist_labels, hist_counts) = calculate_histogram r.latency_data 20 in 420 + let hist_labels_str = String.concat ", " (List.map (fun s -> Printf.sprintf "\"%s\"" s) hist_labels) in 421 + let hist_counts_str = String.concat ", " (List.map string_of_int hist_counts) in 422 + 423 + (* Sample data points for timeline (take every Nth point if too many) *) 424 + let max_points = 500 in 425 + let sample_rate = max 1 ((List.length r.latency_data) / max_points) in 426 + let sampled_data = List.filteri (fun i _ -> i mod sample_rate = 0) r.latency_data in 427 + let timeline_data = String.concat ", " (List.map (fun (t, l) -> 428 + Printf.sprintf "{x: %.2f, y: %.3f}" t l 429 + ) sampled_data) in 430 + 431 + Printf.sprintf {| 432 + <div class="test-detail"> 433 + <h3>%s</h3> 434 + <div class="compact-grid"> 435 + <div class="compact-metric"><span class="label">Servers:</span> <span class="value">%d</span></div> 436 + <div class="compact-metric"><span class="label">Clients:</span> <span class="value">%d</span></div> 437 + <div class="compact-metric"><span class="label">Msgs/Client:</span> <span class="value">%d</span></div> 438 + <div class="compact-metric"><span class="label">Pool Size:</span> <span class="value">%d</span></div> 439 + <div class="compact-metric"><span class="label">Total Msgs:</span> <span class="value">%d</span></div> 440 + <div class="compact-metric"><span class="label">Duration:</span> <span class="value">%.2fs</span></div> 441 + <div class="compact-metric highlight"><span class="label">Throughput:</span> <span class="value">%.0f/s</span></div> 442 + <div class="compact-metric highlight"><span class="label">Avg Lat:</span> <span class="value">%.2fms</span></div> 443 + <div class="compact-metric"><span class="label">Min Lat:</span> <span class="value">%.2fms</span></div> 444 + <div class="compact-metric"><span class="label">Max Lat:</span> <span class="value">%.2fms</span></div> 445 + <div class="compact-metric %s"><span class="label">Errors:</span> <span class="value">%d</span></div> 446 + </div> 447 + <div class="compact-grid" style="margin-top: 0.5rem;"> 448 + <div class="compact-metric"><span class="label">Conns Created:</span> <span class="value">%d</span></div> 449 + <div class="compact-metric"><span class="label">Conns Reused:</span> <span class="value">%d</span></div> 450 + <div class="compact-metric"><span class="label">Conns Closed:</span> <span class="value">%d</span></div> 451 + <div class="compact-metric"><span class="label">Active:</span> <span class="value">%d</span></div> 452 + <div class="compact-metric"><span class="label">Idle:</span> <span class="value">%d</span></div> 453 + <div class="compact-metric"><span class="label">Reuse Rate:</span> <span class="value">%.1f%%%%</span></div> 454 + </div> 455 + <div class="chart-row"> 456 + <div class="chart-half"> 457 + <h4>Latency Distribution</h4> 458 + <canvas id="hist_%d"></canvas> 459 + </div> 460 + <div class="chart-half"> 461 + <h4>Latency Timeline</h4> 462 + <canvas id="timeline_%d"></canvas> 463 + </div> 464 + </div> 465 + </div> 466 + <script> 467 + new Chart(document.getElementById('hist_%d'), { 468 + type: 'bar', 469 + data: { 470 + labels: [%s], 471 + datasets: [{ 472 + label: 'Count', 473 + data: [%s], 474 + backgroundColor: 'rgba(102, 126, 234, 0.6)', 475 + borderColor: 'rgba(102, 126, 234, 1)', 476 + borderWidth: 1 477 + }] 478 + }, 479 + options: { 480 + responsive: true, 481 + maintainAspectRatio: false, 482 + plugins: { legend: { display: false } }, 483 + scales: { 484 + x: { title: { display: true, text: 'Latency (ms)' } }, 485 + y: { beginAtZero: true, title: { display: true, text: 'Count' } } 486 + } 487 + } 488 + }); 489 + 490 + new Chart(document.getElementById('timeline_%d'), { 491 + type: 'scatter', 492 + data: { 493 + datasets: [{ 494 + label: 'Latency', 495 + data: [%s], 496 + backgroundColor: 'rgba(118, 75, 162, 0.5)', 497 + borderColor: 'rgba(118, 75, 162, 0.8)', 498 + pointRadius: 2 499 + }] 500 + }, 501 + options: { 502 + responsive: true, 503 + maintainAspectRatio: false, 504 + plugins: { legend: { display: false } }, 505 + scales: { 506 + x: { title: { display: true, text: 'Time (ms)' } }, 507 + y: { beginAtZero: true, title: { display: true, text: 'Latency (ms)' } } 508 + } 509 + } 510 + }); 511 + </script>|} 512 + (js_escape r.test_name) 513 + r.num_servers 514 + r.num_clients 515 + r.messages_per_client 516 + r.pool_size 517 + r.total_messages 518 + r.duration 519 + r.throughput 520 + r.avg_latency 521 + r.min_latency 522 + r.max_latency 523 + (if r.total_errors > 0 then "error" else "") 524 + r.total_errors 525 + r.pool_stats.total_created 526 + r.pool_stats.total_reused 527 + r.pool_stats.total_closed 528 + r.pool_stats.active 529 + r.pool_stats.idle 530 + (if r.pool_stats.total_created > 0 then 531 + (float_of_int r.pool_stats.total_reused /. float_of_int r.pool_stats.total_created *. 100.0) 532 + else 0.0) 533 + idx idx idx 534 + hist_labels_str 535 + hist_counts_str 536 + idx 537 + timeline_data 538 + ) results) in 539 + 540 + Printf.sprintf {|<!DOCTYPE html> 541 + <html lang="en"> 542 + <head> 543 + <meta charset="UTF-8"> 544 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 545 + <title>Connection Pool Stress Test Results</title> 546 + <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> 547 + <style> 548 + * { margin: 0; padding: 0; box-sizing: border-box; } 549 + body { 550 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 551 + background: #f5f5f5; 552 + padding: 1rem; 553 + color: #333; 554 + font-size: 14px; 555 + } 556 + .container { max-width: 1600px; margin: 0 auto; } 557 + h1 { 558 + color: #667eea; 559 + text-align: center; 560 + margin-bottom: 0.3rem; 561 + font-size: 1.8rem; 562 + } 563 + .subtitle { 564 + text-align: center; 565 + margin-bottom: 1rem; 566 + font-size: 0.9rem; 567 + color: #666; 568 + } 569 + .summary { 570 + background: white; 571 + border-radius: 6px; 572 + padding: 1rem; 573 + margin-bottom: 1rem; 574 + box-shadow: 0 2px 4px rgba(0,0,0,0.1); 575 + } 576 + .summary h2 { 577 + color: #667eea; 578 + margin-bottom: 0.8rem; 579 + font-size: 1.2rem; 580 + } 581 + .summary-grid { 582 + display: grid; 583 + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); 584 + gap: 0.8rem; 585 + } 586 + .summary-metric { 587 + text-align: center; 588 + padding: 0.8rem; 589 + background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%); 590 + border-radius: 4px; 591 + color: white; 592 + } 593 + .summary-metric-label { 594 + font-size: 0.75rem; 595 + opacity: 0.9; 596 + margin-bottom: 0.3rem; 597 + } 598 + .summary-metric-value { 599 + font-size: 1.4rem; 600 + font-weight: bold; 601 + } 602 + .comparison { 603 + background: white; 604 + border-radius: 6px; 605 + padding: 1rem; 606 + margin-bottom: 1rem; 607 + box-shadow: 0 2px 4px rgba(0,0,0,0.1); 608 + } 609 + .comparison h2 { 610 + color: #667eea; 611 + margin-bottom: 0.8rem; 612 + font-size: 1.2rem; 613 + } 614 + .comparison-charts { 615 + display: grid; 616 + grid-template-columns: repeat(3, 1fr); 617 + gap: 1rem; 618 + } 619 + .comparison-chart { 620 + height: 200px; 621 + position: relative; 622 + } 623 + .test-detail { 624 + background: white; 625 + border-radius: 6px; 626 + padding: 1rem; 627 + margin-bottom: 1rem; 628 + box-shadow: 0 2px 4px rgba(0,0,0,0.1); 629 + border-left: 3px solid #667eea; 630 + } 631 + .test-detail h3 { 632 + color: #764ba2; 633 + margin-bottom: 0.6rem; 634 + font-size: 1.1rem; 635 + } 636 + .test-detail h4 { 637 + color: #666; 638 + margin-bottom: 0.4rem; 639 + font-size: 0.9rem; 640 + font-weight: 500; 641 + } 642 + .compact-grid { 643 + display: grid; 644 + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); 645 + gap: 0.4rem; 646 + margin-bottom: 0.8rem; 647 + font-size: 0.85rem; 648 + } 649 + .compact-metric { 650 + background: #f8f9fa; 651 + padding: 0.4rem 0.6rem; 652 + border-radius: 3px; 653 + display: flex; 654 + justify-content: space-between; 655 + align-items: center; 656 + } 657 + .compact-metric .label { 658 + color: #666; 659 + font-weight: 500; 660 + } 661 + .compact-metric .value { 662 + color: #333; 663 + font-weight: 600; 664 + } 665 + .compact-metric.highlight { 666 + background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%); 667 + color: white; 668 + } 669 + .compact-metric.highlight .label, 670 + .compact-metric.highlight .value { 671 + color: white; 672 + } 673 + .compact-metric.error { 674 + background: #fee; 675 + border: 1px solid #fcc; 676 + } 677 + .chart-row { 678 + display: grid; 679 + grid-template-columns: 1fr 1fr; 680 + gap: 1rem; 681 + } 682 + .chart-half { 683 + position: relative; 684 + height: 220px; 685 + } 686 + @media (max-width: 1200px) { 687 + .comparison-charts { grid-template-columns: 1fr; } 688 + .chart-row { grid-template-columns: 1fr; } 689 + } 690 + @media (max-width: 768px) { 691 + .compact-grid { grid-template-columns: repeat(2, 1fr); } 692 + } 693 + </style> 694 + </head> 695 + <body> 696 + <div class="container"> 697 + <h1>Connection Pool Stress Test Results</h1> 698 + <div class="subtitle">%s</div> 699 + 700 + <div class="summary"> 701 + <h2>Summary</h2> 702 + <div class="summary-grid"> 703 + <div class="summary-metric"> 704 + <div class="summary-metric-label">Tests</div> 705 + <div class="summary-metric-value">%d</div> 706 + </div> 707 + <div class="summary-metric"> 708 + <div class="summary-metric-label">Messages</div> 709 + <div class="summary-metric-value">%s</div> 710 + </div> 711 + <div class="summary-metric"> 712 + <div class="summary-metric-label">Errors</div> 713 + <div class="summary-metric-value">%d</div> 714 + </div> 715 + <div class="summary-metric"> 716 + <div class="summary-metric-label">Duration</div> 717 + <div class="summary-metric-value">%.1fs</div> 718 + </div> 719 + </div> 720 + </div> 721 + 722 + <div class="comparison"> 723 + <h2>Comparison</h2> 724 + <div class="comparison-charts"> 725 + <div class="comparison-chart"><canvas id="cmpThroughput"></canvas></div> 726 + <div class="comparison-chart"><canvas id="cmpLatency"></canvas></div> 727 + <div class="comparison-chart"><canvas id="cmpErrors"></canvas></div> 728 + </div> 729 + </div> 730 + 731 + %s 732 + </div> 733 + 734 + <script> 735 + const testNames = [%s]; 736 + const throughputs = [%s]; 737 + const avgLatencies = [%s]; 738 + const errorRates = [%s]; 739 + 740 + const cc = { 741 + primary: 'rgba(102, 126, 234, 0.8)', 742 + secondary: 'rgba(118, 75, 162, 0.8)', 743 + danger: 'rgba(220, 53, 69, 0.8)', 744 + }; 745 + 746 + new Chart(document.getElementById('cmpThroughput'), { 747 + type: 'bar', 748 + data: { 749 + labels: testNames, 750 + datasets: [{ 751 + label: 'msg/s', 752 + data: throughputs, 753 + backgroundColor: cc.primary, 754 + borderColor: cc.primary, 755 + borderWidth: 1 756 + }] 757 + }, 758 + options: { 759 + responsive: true, 760 + maintainAspectRatio: false, 761 + plugins: { 762 + legend: { display: false }, 763 + title: { display: true, text: 'Throughput (msg/s)' } 764 + }, 765 + scales: { y: { beginAtZero: true } } 766 + } 767 + }); 768 + 769 + new Chart(document.getElementById('cmpLatency'), { 770 + type: 'bar', 771 + data: { 772 + labels: testNames, 773 + datasets: [{ 774 + label: 'ms', 775 + data: avgLatencies, 776 + backgroundColor: cc.secondary, 777 + borderColor: cc.secondary, 778 + borderWidth: 1 779 + }] 780 + }, 781 + options: { 782 + responsive: true, 783 + maintainAspectRatio: false, 784 + plugins: { 785 + legend: { display: false }, 786 + title: { display: true, text: 'Avg Latency (ms)' } 787 + }, 788 + scales: { y: { beginAtZero: true } } 789 + } 790 + }); 791 + 792 + new Chart(document.getElementById('cmpErrors'), { 793 + type: 'bar', 794 + data: { 795 + labels: testNames, 796 + datasets: [{ 797 + label: '%%', 798 + data: errorRates, 799 + backgroundColor: cc.danger, 800 + borderColor: cc.danger, 801 + borderWidth: 1 802 + }] 803 + }, 804 + options: { 805 + responsive: true, 806 + maintainAspectRatio: false, 807 + plugins: { 808 + legend: { display: false }, 809 + title: { display: true, text: 'Error Rate (%%)' } 810 + }, 811 + scales: { y: { beginAtZero: true } } 812 + } 813 + }); 814 + </script> 815 + </body> 816 + </html>|} 817 + date_str 818 + (List.length results) 819 + (if total_messages >= 1000 then 820 + Printf.sprintf "%d,%03d" (total_messages / 1000) (total_messages mod 1000) 821 + else 822 + string_of_int total_messages) 823 + total_errors 824 + total_duration 825 + test_details 826 + test_names 827 + throughputs 828 + avg_latencies 829 + error_rates 830 + 831 + (** Run all preset tests and return results *) 348 832 let run_all_presets ~env = 349 833 List.map (fun config -> 350 834 Printf.eprintf "Running test: %s\n%!" config.name; ··· 421 905 | Single config -> 422 906 let config = if config.name = "default" then custom_config else config in 423 907 Eio_main.run @@ fun env -> 424 - let trace = run_stress_test ~env config in 425 - let json = Printf.sprintf "[%s]" (Trace.trace_to_json trace) in 908 + let result = run_stress_test ~env config in 909 + let results = [result] in 910 + 911 + (* Write JSON *) 912 + let json = Printf.sprintf "[%s]" (result_to_json result) in 426 913 let oc = open_out output_file in 427 914 output_string oc json; 428 915 close_out oc; 429 916 Printf.printf "Results written to %s\n" output_file; 917 + 918 + (* Write HTML *) 919 + let html_file = 920 + if Filename.check_suffix output_file ".json" then 921 + Filename.chop_suffix output_file ".json" ^ ".html" 922 + else 923 + output_file ^ ".html" 924 + in 925 + let html = generate_html_report results in 926 + let oc_html = open_out html_file in 927 + output_string oc_html html; 928 + close_out oc_html; 929 + Printf.printf "HTML report written to %s\n" html_file; 930 + 430 931 Printf.printf "Test: %s - %d messages, %.2f msg/s, %.2fms avg latency, %d errors\n" 431 - trace.test_name trace.total_messages trace.throughput trace.avg_latency trace.total_errors 932 + result.test_name result.total_messages result.throughput result.avg_latency result.total_errors 432 933 433 934 | AllPresets -> 434 935 Eio_main.run @@ fun env -> 435 - let traces = run_all_presets ~env in 436 - let json = "[" ^ String.concat ",\n" (List.map Trace.trace_to_json traces) ^ "]" in 936 + let results = run_all_presets ~env in 937 + 938 + (* Write JSON *) 939 + let json = "[" ^ String.concat ",\n" (List.map result_to_json results) ^ "]" in 437 940 let oc = open_out output_file in 438 941 output_string oc json; 439 942 close_out oc; 440 943 Printf.printf "Results written to %s\n" output_file; 441 - List.iter (fun t -> 944 + 945 + (* Write HTML *) 946 + let html_file = 947 + if Filename.check_suffix output_file ".json" then 948 + Filename.chop_suffix output_file ".json" ^ ".html" 949 + else 950 + output_file ^ ".html" 951 + in 952 + let html = generate_html_report results in 953 + let oc_html = open_out html_file in 954 + output_string oc_html html; 955 + close_out oc_html; 956 + Printf.printf "HTML report written to %s\n" html_file; 957 + 958 + List.iter (fun r -> 442 959 Printf.printf " %s: %d messages, %.2f msg/s, %.2fms avg latency, %d errors\n" 443 - t.Trace.test_name t.total_messages t.throughput t.avg_latency t.total_errors 444 - ) traces 960 + r.test_name r.total_messages r.throughput r.avg_latency r.total_errors 961 + ) results 445 962 446 963 | Extended -> 447 964 Printf.printf "Running extended stress test: %d servers, %d clients/server, %d msgs/client\n" ··· 449 966 Printf.printf "Total messages: %d\n%!" 450 967 (extended_preset.num_servers * extended_preset.num_clients * extended_preset.messages_per_client); 451 968 Eio_main.run @@ fun env -> 452 - let trace = run_stress_test ~env extended_preset in 453 - let json = Printf.sprintf "[%s]" (Trace.trace_to_json trace) in 969 + let result = run_stress_test ~env extended_preset in 970 + let results = [result] in 971 + 972 + (* Write JSON *) 973 + let json = Printf.sprintf "[%s]" (result_to_json result) in 454 974 let oc = open_out output_file in 455 975 output_string oc json; 456 976 close_out oc; 457 977 Printf.printf "Results written to %s\n" output_file; 978 + 979 + (* Write HTML *) 980 + let html_file = 981 + if Filename.check_suffix output_file ".json" then 982 + Filename.chop_suffix output_file ".json" ^ ".html" 983 + else 984 + output_file ^ ".html" 985 + in 986 + let html = generate_html_report results in 987 + let oc_html = open_out html_file in 988 + output_string oc_html html; 989 + close_out oc_html; 990 + Printf.printf "HTML report written to %s\n" html_file; 991 + 458 992 Printf.printf "Test: %s - %d messages, %.2f msg/s, %.2fms avg latency, %d errors\n" 459 - trace.test_name trace.total_messages trace.throughput trace.avg_latency trace.total_errors 993 + result.test_name result.total_messages result.throughput result.avg_latency result.total_errors