Files
2026-07-13 13:05:14 +08:00

248 lines
7.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Plot dashboard stress test.
Usage:
-----
```
pixi run py-plot-dashboard --help
```
Example:
-------
```
pixi run py-plot-dashboard --num-plots 10 --num-series-per-plot 5 --num-points-per-series 5000 --freq 1000
```
"""
from __future__ import annotations
import argparse
import math
import time
from typing import Any, cast
import numpy as np
import rerun as rr # pip install rerun-sdk
import rerun.blueprint as rrb
parser = argparse.ArgumentParser(description="Plot dashboard stress test")
rr.script_add_args(parser)
parser.add_argument("--num-plots", type=int, default=1, help="How many different plots?")
parser.add_argument(
"--num-series-per-plot",
type=int,
default=1,
help="How many series in each single plot?",
)
parser.add_argument(
"--num-points-per-series",
type=int,
default=100000,
help="How many points in each single series?",
)
parser.add_argument(
"--freq",
type=float,
default=1000,
help="Frequency of logging (applies to all series)",
)
parser.add_argument(
"--temporal-batch-size",
type=int,
default=None,
help="Number of rows to include in each log call",
)
parser.add_argument(
"--blueprint",
action="store_true",
help="Setup a blueprint for a 5s window",
)
order = [
"forwards",
"backwards",
"random",
]
parser.add_argument(
"--order",
type=str,
default=order[0],
help="What order to log the data in (applies to all series)",
choices=order,
)
series_type = [
"gaussian-random-walk",
"sin-uniform",
]
parser.add_argument(
"--series-type",
type=str,
default=series_type[0],
choices=series_type,
help="The method used to generate time series",
)
# TODO(cmc): could have flags to add attributes (color, radius...) to put some more stress
# on the line fragmenter.
args = parser.parse_args()
def main() -> None:
rr.script_setup(args, "rerun_example_plot_dashboard_stress")
plot_paths = [f"plot_{i}" for i in range(args.num_plots)]
series_paths = [f"series_{i}" for i in range(args.num_series_per_plot)]
if args.blueprint:
print("logging blueprint!")
rr.send_blueprint(
rrb.Blueprint(
rrb.Grid(*[
rrb.TimeSeriesView(
name=p,
origin=f"/{p}",
time_ranges=rrb.VisibleTimeRanges(
timeline="sim_time",
start=rrb.TimeRangeBoundary.cursor_relative(offset=rr.TimeInt(seconds=-2.5)),
end=rrb.TimeRangeBoundary.cursor_relative(offset=rr.TimeInt(seconds=2.5)),
),
)
for p in plot_paths
]),
rrb.BlueprintPanel(state="collapsed"),
rrb.SelectionPanel(state="collapsed"),
),
)
time_per_sim_step = 1.0 / args.freq
stop_time = args.num_points_per_series * time_per_sim_step
if args.order == "forwards":
sim_times = np.arange(0, stop_time, time_per_sim_step)
elif args.order == "backwards":
sim_times = np.arange(0, stop_time, time_per_sim_step)[::-1]
else:
sim_times = np.random.randint(0, args.num_points_per_series)
num_series = len(plot_paths) * len(series_paths)
time_per_tick = time_per_sim_step
scalars_per_tick = num_series
if args.temporal_batch_size is not None:
time_per_tick *= args.temporal_batch_size
scalars_per_tick *= args.temporal_batch_size
expected_total_freq = args.freq * num_series
values_shape = (
len(sim_times),
len(plot_paths),
len(series_paths),
)
if args.series_type == "gaussian-random-walk":
values = np.cumsum(np.random.normal(size=values_shape), axis=0)
elif args.series_type == "sin-uniform":
values = np.sin(np.random.uniform(0, math.pi, size=values_shape))
else:
# Just generate random numbers rather than crash
values = np.random.normal(size=values_shape)
if args.temporal_batch_size is None:
ticks: Any = enumerate(sim_times)
else:
offsets = range(0, len(sim_times), args.temporal_batch_size)
ticks = zip(
offsets,
(sim_times[offset : offset + args.temporal_batch_size] for offset in offsets),
strict=False,
)
time_column = None
total_start_time = time.time()
total_num_scalars = 0
tick_start_time = time.time()
max_load = 0.0
for index, sim_time in ticks:
if args.temporal_batch_size is None:
rr.set_time("sim_time", duration=sim_time)
else:
time_column = rr.TimeColumn("sim_time", duration=sim_time)
# Log
for plot_idx, plot_path in enumerate(plot_paths):
for series_idx, series_path in enumerate(series_paths):
if args.temporal_batch_size is None:
value = values[index, plot_idx, series_idx]
rr.log(f"{plot_path}/{series_path}", rr.Scalars(value))
else:
value_index = slice(index, index + args.temporal_batch_size)
rr.send_columns(
f"{plot_path}/{series_path}",
indexes=[cast("rr.TimeColumn", time_column)],
columns=rr.Scalars.columns(scalars=values[value_index, plot_idx, series_idx]),
)
# Measure how long this took and how high the load was.
elapsed = time.time() - tick_start_time
max_load = max(max_load, elapsed / time_per_tick)
# Throttle
sleep_duration = time_per_tick - elapsed
if sleep_duration > 0.0:
sleep_start_time = time.time()
time.sleep(sleep_duration)
sleep_elapsed = time.time() - sleep_start_time
# We will very likely be put to sleep for more than we asked for, and therefore need
# to pay off that debt in order to meet our frequency goal.
sleep_debt = sleep_elapsed - sleep_duration
tick_start_time = time.time() - sleep_debt
else:
tick_start_time = time.time()
# Progress report
#
# Must come after throttle since we report every wall-clock second:
# If ticks are large & fast, then after each send we run into throttle.
# So if this was before throttle, we'd not report the first tick no matter how large it was.
total_num_scalars += scalars_per_tick
total_elapsed = time.time() - total_start_time
if total_elapsed >= 1.0:
print(
f"logged {total_num_scalars} scalars over {round(total_elapsed, 3)}s \
(freq={round(total_num_scalars / total_elapsed, 3)}Hz, expected={round(expected_total_freq, 3)}Hz, \
load={round(max_load * 100.0, 3)}%)",
)
elapsed_debt = total_elapsed % 1 # just keep the fractional part
total_start_time = time.time() - elapsed_debt
total_num_scalars = 0
max_load = 0.0
if total_num_scalars > 0:
total_elapsed = time.time() - total_start_time
print(
f"logged {total_num_scalars} scalars over {round(total_elapsed, 3)}s \
(freq={round(total_num_scalars / total_elapsed, 3)}Hz, expected={round(expected_total_freq, 3)}Hz, \
load={round(max_load * 100.0, 3)}%)",
)
rr.script_teardown(args)
if __name__ == "__main__":
main()