@@ -324,13 +324,25 @@ def run(args):
324324 primary_has_stopped = False
325325 while True :
326326 stop_waiting = True
327+ running = []
328+ done_list = []
327329 for i , remote_client in enumerate (clients ):
328330 done = remote_client .check_done ()
329- # all the clients need to be done
330- LOG . info (
331- f"Client { i } has { 'completed' if done else 'not completed' } running ( { time . time () - start_time :>{ format_width }.2f } s / { args . client_timeout_s } s)"
332- )
331+ if done :
332+ done_list . append ( i )
333+ else :
334+ running . append ( i )
333335 stop_waiting = stop_waiting and done
336+ elapsed = f"{ time .time () - start_time :>{format_width }.2f} s / { args .client_timeout_s } s"
337+ if len (clients ) <= 3 :
338+ for i , remote_client in enumerate (clients ):
339+ status = "completed" if i in done_list else "not completed"
340+ LOG .info (f"Client { i } has { status } running ({ elapsed } )" )
341+ else :
342+ parts = [f"{ len (running )} running ({ elapsed } )" ]
343+ if done_list :
344+ parts .append (f"{ len (done_list )} complete" )
345+ LOG .info (f"Clients: { ', ' .join (parts )} " )
334346 if stop_waiting :
335347 break
336348 if time .time () > start_time + args .client_timeout_s :
@@ -444,9 +456,14 @@ def table():
444456 f"{ remote_client .name } : First send at { first_send } , last receive at { last_recv } "
445457 )
446458 duration = (last_recv - first_send ).total_seconds ()
447- print (
448- f"{ remote_client .name } : { len (overall )} requests in { duration } s => { len (overall )// duration } tx/s"
449- )
459+ if duration > 0 :
460+ print (
461+ f"{ remote_client .name } : { len (overall )} requests in { duration } s => { len (overall )// duration } tx/s"
462+ )
463+ else :
464+ print (
465+ f"{ remote_client .name } : { len (overall )} requests in { duration } s"
466+ )
450467 agg .append (overall )
451468
452469 table ()
@@ -471,6 +488,12 @@ def table():
471488 start_send = agg ["sendTime" ].min ()
472489 end_recv = agg ["receiveTime" ].max ()
473490 duration_s = (end_recv - start_send ).total_seconds ()
491+ if duration_s < 1 :
492+ LOG .error (
493+ f"Duration is { duration_s } s (first send: { start_send } , last recv: { end_recv } ). "
494+ "Clamping to 1s; results may be inaccurate."
495+ )
496+ duration_s = 1
474497 throughput = len (agg ) / duration_s
475498 statistics ["average_throughput_tx/s" ] = throughput
476499 print (f"Average throughput: { throughput :.2f} tx/s" )
@@ -489,6 +512,12 @@ def table():
489512 client ["receiveTime" ].max () for client in each_client
490513 )
491514 all_active_duration_s = (earliest_end - latest_start ).total_seconds ()
515+ if all_active_duration_s <= 0 :
516+ LOG .error (
517+ f"All-clients-active duration is { all_active_duration_s } s. "
518+ "Clamping to 1s; results may be inaccurate."
519+ )
520+ all_active_duration_s = 1
492521 statistics ["all_clients_active_from" ] = latest_start .isoformat ()
493522 statistics ["all_clients_active_to" ] = earliest_end .isoformat ()
494523 statistics ["all_clients_active_duration_s" ] = all_active_duration_s
@@ -509,81 +538,183 @@ def table():
509538 agg_all_active = agg .filter (pl .col ("sendTime" ) > latest_start ).filter (
510539 pl .col ("receiveTime" ) < earliest_end
511540 )
512- all_active_duration_s = (earliest_end - latest_start ).total_seconds ()
513- all_active_throughput = len (agg_all_active ) / all_active_duration_s
514- statistics ["all_clients_active_average_throughput_tx/s" ] = (
515- all_active_throughput
516- )
541+ n_all_active = max (len (agg_all_active ), 1 )
542+ all_active_throughput = n_all_active / all_active_duration_s
517543 writes = len (
518544 agg_all_active .filter (pl .col ("request" ).bin .starts_with (b"PUT " ))
519545 )
520- statistics ["all_clients_active_write_fraction" ] = writes / len (
521- agg_all_active
546+ write_fraction = writes / n_all_active
547+ statistics ["all_clients_active_average_throughput_tx/s" ] = (
548+ all_active_throughput
522549 )
550+ statistics ["all_clients_active_write_fraction" ] = write_fraction
523551
524552 statistics_path = os .path .join (network .common_dir , "statistics.json" )
525553 with open (statistics_path , "w" ) as f :
526554 json .dump (statistics , f , indent = 2 )
527555 print (f"Aggregated statistics written to { statistics_path } " )
528556
529- sent_per_sec = (
530- agg .with_columns (
531- (
532- (pl .col ("sendTime" ).alias ("second" ) - start_send ) / 1000000
533- ).cast (pl .Int64 )
557+ # Build per-client, per-second breakdown for stacked charts
558+ chart_width = 100
559+ max_builtin_legend_clients = 3
560+
561+ client_names = sorted (
562+ agg ["client" ].unique ().to_list (),
563+ key = lambda c : int (c .split ("_" )[- 1 ]),
564+ )
565+ n_clients = len (client_names )
566+ all_seconds = set ()
567+
568+ # Color gradient helpers: interpolate n RGB colors from start to end.
569+ # client_0 gets the darkest shade (matching plotext defaults), higher clients get lighter.
570+ def _shades (n , start , end ):
571+ return [
572+ tuple (
573+ s + i * (e - s ) // max (n - 1 , 1 ) for s , e in zip (start , end )
574+ )
575+ for i in range (n )
576+ ]
577+
578+ def blue_shades (n ):
579+ return _shades (n , (59 , 142 , 234 ), (100 , 100 , 255 ))
580+
581+ def green_shades (n ):
582+ return _shades (n , (13 , 188 , 121 ), (100 , 255 , 100 ))
583+
584+ def red_shades (n ):
585+ return _shades (n , (205 , 49 , 49 ), (255 , 100 , 100 ))
586+
587+ def format_title (title , width ):
588+ """Centered title bar in the style of plotext's simple_bar charts."""
589+ if title is None :
590+ return ""
591+ visible_len = len (plt .uncolorize (title ))
592+ w1 = (width - 2 - visible_len ) // 2
593+ w2 = width - visible_len - 2 - w1
594+ return (
595+ plt .colorize (
596+ "─" * w1 + " " + title + " " + "─" * w2 , "gray+" , "bold"
597+ )
598+ + "\n "
534599 )
535- .group_by ("second" )
600+
601+ def color_legend (names , * color_groups ):
602+ """Print a legend bar with color swatches for first and last client."""
603+ marker = "▇" * 3
604+ parts = []
605+ for color_list , suffix in color_groups :
606+ first = plt .colorize (marker , color_list [0 ])
607+ last = plt .colorize (marker , color_list [- 1 ])
608+ parts .append (
609+ f"{ names [0 ]} { suffix } { first } .. { last } { names [- 1 ]} { suffix } "
610+ )
611+ print (format_title (" " .join (parts ), chart_width ), end = "" )
612+
613+ # Generate per-client color palettes
614+ sent_colors = blue_shades (n_clients )
615+ rcvd_colors = green_shades (n_clients )
616+ error_colors = red_shades (n_clients )
617+
618+ # Compute per-client, per-second counts in a single pass
619+ agg_with_seconds = agg .with_columns (
620+ ((pl .col ("sendTime" ) - start_send ) / 1000000 )
621+ .cast (pl .Int64 )
622+ .alias ("send_second" ),
623+ ((pl .col ("receiveTime" ) - start_send ) / 1000000 )
624+ .cast (pl .Int64 )
625+ .alias ("recv_second" ),
626+ (pl .col ("responseStatus" ) >= 500 ).alias ("is_error" ),
627+ )
628+
629+ sent_counts = (
630+ agg_with_seconds .group_by ("client" , "send_second" )
536631 .len ()
537- .rename ({"len" : "sent " })
632+ .rename ({"send_second" : "second" , " len" : "count " })
538633 )
539- recv_per_sec = (
540- agg .with_columns (
541- (
542- (pl .col ("receiveTime" ).alias ("second" ) - start_send )
543- / 1000000
544- ).cast (pl .Int64 )
545- )
546- .group_by ("second" )
634+ rcvd_counts = (
635+ agg_with_seconds .filter (~ pl .col ("is_error" ))
636+ .group_by ("client" , "recv_second" )
547637 .len ()
548- .rename ({"len" : "rcvd " })
638+ .rename ({"recv_second" : "second" , " len" : "count " })
549639 )
550- errors_per_sec = (
551- agg .with_columns (
552- (
553- (pl .col ("receiveTime" ).alias ("second" ) - start_send )
554- / 1000000
555- ).cast (pl .Int64 )
556- )
557- .filter (pl .col ("responseStatus" ) >= 500 )
558- .group_by ("second" )
640+ error_counts = (
641+ agg_with_seconds .filter (pl .col ("is_error" ))
642+ .group_by ("client" , "recv_second" )
559643 .len ()
560- .rename ({"len" : "errors " })
644+ .rename ({"recv_second" : "second" , " len" : "count " })
561645 )
562646
563- per_sec = (
564- sent_per_sec .join (recv_per_sec , on = "second" )
565- .join (errors_per_sec , on = "second" , how = "full" )
566- .sort ("second" )
567- .fill_null (0 )
568- )
647+ client_sent = {}
648+ client_rcvd = {}
649+ client_errors = {}
650+ for cn in client_names :
651+ cs = sent_counts .filter (pl .col ("client" ) == cn )
652+ client_sent [cn ] = dict (
653+ zip (cs ["second" ].to_list (), cs ["count" ].to_list ())
654+ )
655+ all_seconds .update (cs ["second" ].to_list ())
656+
657+ cr = rcvd_counts .filter (pl .col ("client" ) == cn )
658+ client_rcvd [cn ] = dict (
659+ zip (cr ["second" ].to_list (), cr ["count" ].to_list ())
660+ )
661+ all_seconds .update (cr ["second" ].to_list ())
569662
570- plt .simple_bar (
571- list (per_sec ["second" ]),
572- list (per_sec ["sent" ]),
573- width = 100 ,
574- title = "Sent requests per second" ,
663+ ce = error_counts .filter (pl .col ("client" ) == cn )
664+ client_errors [cn ] = dict (
665+ zip (ce ["second" ].to_list (), ce ["count" ].to_list ())
666+ )
667+ all_seconds .update (ce ["second" ].to_list ())
668+
669+ seconds = sorted (all_seconds )
670+
671+ use_builtin_legend = n_clients <= max_builtin_legend_clients
672+
673+ # Stacked bar: sent per second, blue shades per client
674+ sent_stacks = [
675+ [client_sent [cn ].get (s , 0 ) for s in seconds ] for cn in client_names
676+ ]
677+ sent_kwargs = {"colors" : sent_colors }
678+ if use_builtin_legend :
679+ sent_kwargs ["labels" ] = client_names
680+ plt .simple_stacked_bar (
681+ seconds ,
682+ sent_stacks ,
683+ width = chart_width ,
684+ title = "Sent requests per second (by client)" ,
685+ ** sent_kwargs ,
575686 )
576687 plt .show ()
688+ if not use_builtin_legend :
689+ color_legend (client_names , (sent_colors , "" ))
577690
691+ # Stacked bar: received per second, green shades per client + red for errors
692+ rcvd_stacks = [
693+ [client_rcvd [cn ].get (s , 0 ) for s in seconds ] for cn in client_names
694+ ]
695+ error_stacks = [
696+ [client_errors [cn ].get (s , 0 ) for s in seconds ]
697+ for cn in client_names
698+ ]
699+ rcvd_kwargs = {"colors" : rcvd_colors + error_colors }
700+ if use_builtin_legend :
701+ rcvd_kwargs ["labels" ] = client_names + [
702+ f"{ cn } errors" for cn in client_names
703+ ]
578704 plt .simple_stacked_bar (
579- list (per_sec ["second" ]),
580- [list (per_sec ["rcvd" ]), list (per_sec ["errors" ])],
581- width = 100 ,
582- labels = ["rcvd" , "errors" ],
583- colors = ["green" , "red" ],
584- title = "Received requests per second" ,
705+ seconds ,
706+ rcvd_stacks + error_stacks ,
707+ width = chart_width ,
708+ title = "Received requests per second (by client)" ,
709+ ** rcvd_kwargs ,
585710 )
586711 plt .show ()
712+ if not use_builtin_legend :
713+ color_legend (
714+ client_names ,
715+ (rcvd_colors , "" ),
716+ (error_colors , " errors" ),
717+ )
587718
588719 if number_of_errors and not args .stop_primary_after_s :
589720 raise RuntimeError (
0 commit comments