Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,39 @@ if(BUILD_TESTS)
"1,write,300000,primary"
)

# Same workload as pi_basic_blocking, driven by locust rather than piccolo,
# so that the number of concurrent clients can be varied.
add_e2e_test(
NAME pi_basic_blocking_locust
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/basicperf_locust.py
LABEL perf
CONFIGURATIONS perf
ADDITIONAL_ARGS
--package
"samples/apps/basic/basic"
--perf-label
"Basic Blocking Locust"
# add_piccolo_test passes this for every piccolo perf test, but
# add_e2e_test does not, and the default is 10. Without it this
# benchmark snapshots every 10 transactions and measures the disk.
--snapshot-tx-interval
10000
--users
320
--spawn-rate
320
--locust-processes
10
--measure-time-s
20
# Blocking writes return on commit, and commit cannot outpace the
# signature interval, so this sweeps from latency-bound to capacity-bound.
--sig-ms-intervals
2
100
1000
)

if(WORKER_THREADS)
add_piccolo_test(
NAME pi_basic_mt
Expand Down
83 changes: 75 additions & 8 deletions scripts/perf_compare_radar.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,49 @@ def axis_label_color(percent: float, higher_is_better: bool, within_noise: bool)
return LABEL_GOOD if improved else LABEL_BAD


# U+2026 HORIZONTAL ELLIPSIS. One character wide, so eliding the middle of a
# word with it costs a single column rather than the three of "...".
ELLIPSIS = "\u2026"

# Eliding a shorter word saves nothing, since "abc" and "a" + ELLIPSIS + "c"
# are both three characters.
MIN_ELIDABLE_WORD_LENGTH = 4


def elide_word(word: str) -> str:
"""Replace the middle of a word with a single ellipsis character."""
return f"{word[0]}{ELLIPSIS}{word[-1]}"


def shorten_label(label: str, max_length: int) -> str:
"""Shorten a label to fit by eliding the middles of its words.

Words are elided from left to right, each keeping its first and last letter,
until the label fits. Benchmarks measured at several settings differ only in
their last word, for example the interval in "Basic Blocking Locust 100ms",
so eliding from the left keeps the part which tells them apart readable for
as long as possible.
"""
if len(label) <= max_length:
return label

words = label.split(" ")
for index, word in enumerate(words):
if len(word) < MIN_ELIDABLE_WORD_LENGTH:
continue
words[index] = elide_word(word)
elided = " ".join(words)
if len(elided) <= max_length:
return elided

# Every word is elided and it still does not fit. Keep the end, which is
# what distinguishes one setting of a benchmark from another.
elided = " ".join(words)
if max_length <= 1:
return elided[:max_length]
return ELLIPSIS + elided[len(elided) - (max_length - 1) :]


def axis_label(
benchmark: str, value: float, percent: float, unit: str, within_noise: bool
) -> str:
Expand All @@ -257,10 +300,7 @@ def axis_label(
f": {metric_label_value(value, unit)} "
f"{format_delta_percent(percent, within_noise)}"
)
max_label_length = MAX_AXIS_LABEL_LENGTH - len(suffix)
if len(label) <= max_label_length:
return f"{label}{suffix}"
return f"{label[:max_label_length - 3]}...{suffix}"
return f"{shorten_label(label, MAX_AXIS_LABEL_LENGTH - len(suffix))}{suffix}"


def normalized_percent(value: float, baseline: float) -> float:
Expand Down Expand Up @@ -319,14 +359,33 @@ def render_mermaid_radar_chart(
for data in trend
if (value := metric_value(data, benchmark, metric)) is not None
]
if not main_values:
continue

baseline = ewma(main_values)
# A benchmark added by this branch has no main runs to build a baseline
# from. Rather than drop it, which would make a new benchmark invisible
# on the very pull request which adds it, use this branch's own earliest
# run as the reference, so the axis is normalised and plotted exactly
# like every other one.
if main_values:
baseline = ewma(main_values)
sigma = statistics.pstdev(main_values) if len(main_values) > 1 else 0.0
else:
branch_values = [
value
for data in branch_runs
if (value := metric_value(data, benchmark, metric)) is not None
]
if not branch_values:
continue
baseline = branch_values[0]
# Spread is measured the same way as for a benchmark with main
# history, from the runs available, so that such an axis carries a
# band and a noise threshold like every other one rather than
# collapsing to a point at the baseline.
sigma = statistics.pstdev(branch_values) if len(branch_values) > 1 else 0.0

if baseline <= 0:
continue

sigma = statistics.pstdev(main_values) if len(main_values) > 1 else 0.0
branch_percent = normalized_percent(branch_value, baseline)
sigma_percent = normalized_percent(sigma, baseline)
within_noise = within_noise_band(branch_percent, sigma_percent)
Expand Down Expand Up @@ -507,6 +566,14 @@ def render_comparison(
"Higher is better for throughput and rate, lower for latency and memory._"
),
"",
(
"_A benchmark which does not exist on `main` yet has no baseline of its "
"own, so this branch's earliest run is used as its reference and its band "
"is measured across this branch's runs. Its axis is normalized, scaled and "
"coloured like any other, but the comparison is against this branch rather "
"than against `main`._"
),
"",
"</details>",
"",
]
Expand Down
Loading