Skip to content

Commit bc960b6

Browse files
authored
Improve perf infra display for multiple clients (#7770)
1 parent 6e24a87 commit bc960b6

1 file changed

Lines changed: 188 additions & 57 deletions

File tree

tests/infra/basicperf.py

Lines changed: 188 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)