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