Skip to content

Commit 7645ba2

Browse files
Minipadaclaude
andcommitted
feat(tools/e2e): add limits harness one-command capacity baseline (#386)
Adds scripts/run_limits_capacity_baseline.sh, the final piece of #323's limits-harness epic: sequences the four axis scripts (Shipper fan-in, single-robot ceiling, Uploader concurrency, drain rate) and combines their curve_reporter.py reports into one stable/diffable artifact via the new scripts/limits_baseline.py, so a contributor can check the effect of a change with one command and a release can record a capacity baseline. curve_reporter.py gains from_json() (the inverse of to_json(), to read a per-axis report back in) and a generic closing_sentence() so every axis gets #323's PRD "single defensible sentence" — composition and measured/extrapolated conditions stated — without a bespoke implementation per axis; run_limits_shipper_fanin.py keeps its own existing wording for the PRD's literal flagship example. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsH1x5McUTKyuXScdaf7ZN Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
1 parent c176d08 commit 7645ba2

6 files changed

Lines changed: 526 additions & 0 deletions

File tree

tools/e2e/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,47 @@ testing decision.
569569

570570
Not wired into `ci.yaml`, same as the scenarios above.
571571

572+
## One-command capacity baseline (#386)
573+
574+
The final piece of #323's epic: a single entry point that runs all four axes above and
575+
combines their reports into one artifact, so a contributor can check the effect of a
576+
change with one command instead of assembling four scenarios by hand, and a release can
577+
record a capacity baseline instead of repeating the previous release's claim.
578+
579+
```sh
580+
./tools/e2e/scripts/run_limits_capacity_baseline.sh
581+
```
582+
583+
Runs `run_limits_shipper_fanin.sh`, `run_limits_single_robot_ceiling.sh`,
584+
`run_limits_upload_concurrency.sh`, and `run_limits_drain_rate.sh` in sequence — each is
585+
its own full podman bring-up/teardown, and none of the four are meant to run
586+
concurrently, same as every other pair of sibling scenario scripts in this harness —
587+
then hands the four already-written `curve_reporter.py` report JSON files (only the
588+
Shipper fan-in axis's **unconstrained** phase; its constrained phase is that axis's own
589+
internal instrument proof, not a fifth axis here) to `scripts/limits_baseline.py`.
590+
`merge_reports()` combines them into one `CurveReport` sorted by axis name — the same
591+
stability guarantee `curve_reporter.build_report()` gives within one report, extended
592+
across reports, so the combined artifact is diffable between runs of an unchanged
593+
system exactly as each axis's own report already is. `render_baseline()` then renders
594+
`curve_reporter.render_summary()`'s per-axis detail followed by every axis's one-sentence
595+
capacity claim from the new `curve_reporter.closing_sentence()` — a generic form of
596+
`run_limits_shipper_fanin.py`'s own bespoke "single defensible sentence" (which keeps its
597+
particular wording, since it's the PRD's literal flagship example), built entirely from
598+
an already-computed `AxisReport` with no axis-specific knowledge, so every axis gets one
599+
without a fifth bespoke implementation. Both `merge_reports()` and `render_baseline()`
600+
are pure functions over already-written reports, unit-tested with synthetic data
601+
(`test_limits_baseline.py`), matching this epic's testing philosophy: this script and the
602+
four axis scripts it sequences are proven by running them, not by a unit test.
603+
604+
Because `set -euo pipefail` combines with each axis script's own hard-failing gates, any
605+
axis failing aborts the whole baseline immediately — a combined artifact is only ever
606+
written from four axes that actually passed. Every env var an individual axis script
607+
accepts (`DC_E2E_FANIN_*`, `DC_E2E_CEILING_*`, `DC_E2E_UPLOAD_*`, `DC_E2E_DRAIN_*`,
608+
`DC_E2E_IMAGE`/`DC_WORKSPACE_IMAGE`, `DC_E2E_KEEP`) still applies unchanged, since this
609+
script does no parameter translation of its own.
610+
611+
Not wired into `ci.yaml`, same as the scenarios above.
612+
572613
## Layout
573614

574615
- `Containerfile` — builds the full DC workspace (every `dc_*` package, all C++ since
@@ -703,6 +744,13 @@ Not wired into `ci.yaml`, same as the scenarios above.
703744
`find_drain_rate_curve()` are pure and unit-tested with fakes
704745
(`test_drain_rate_axis.py`); the podman/load_driver.py orchestration is exercised by
705746
running the scenario script.
747+
- `scripts/limits_baseline.py` / `scripts/run_limits_capacity_baseline.sh` — the
748+
one-command capacity baseline (#386, the final piece of #323's epic) described above:
749+
sequences the four axis scripts, then combines their `curve_reporter.py` reports into
750+
one stable artifact with every axis's closing sentence. `merge_reports()`/
751+
`render_baseline()` are pure and unit-tested with synthetic reports
752+
(`test_limits_baseline.py`); the four axes it sequences are each exercised by running
753+
their own scenario script, not re-tested here.
706754

707755
## `.dockerignore`
708756

tools/e2e/scripts/curve_reporter.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,49 @@ def to_json(report: CurveReport) -> str:
231231
return json.dumps(dataclasses.asdict(report), indent=2, sort_keys=True)
232232

233233

234+
def _resource_sample_from_dict(d: dict | None) -> ResourceSample | None:
235+
return None if d is None else ResourceSample(**d)
236+
237+
238+
def _binding_constraint_from_dict(d: dict) -> BindingConstraint:
239+
return BindingConstraint(
240+
resource=None if d["resource"] is None else ResourceKind(d["resource"]),
241+
percent=d["percent"],
242+
reason=d["reason"],
243+
)
244+
245+
246+
def _level_result_from_dict(d: dict) -> LevelResult:
247+
return LevelResult(
248+
level=d["level"],
249+
saturated=d["saturated"],
250+
kind=PointKind(d["kind"]),
251+
composition=RunComposition(**d["composition"]),
252+
resources=_resource_sample_from_dict(d["resources"]),
253+
)
254+
255+
256+
def axis_report_from_dict(d: dict) -> AxisReport:
257+
"""The inverse of `dataclasses.asdict()` for one axis, used to read back a
258+
per-axis report a scenario script already wrote via `to_json()` — the shape #386's
259+
one-command entry point combines across axes."""
260+
return AxisReport(
261+
axis=d["axis"],
262+
unit=d["unit"],
263+
outcome=RunOutcome(d["outcome"]),
264+
highest_clear_level=d["highest_clear_level"],
265+
tripped_level=d["tripped_level"],
266+
binding_constraint=_binding_constraint_from_dict(d["binding_constraint"]),
267+
points=tuple(_level_result_from_dict(p) for p in d["points"]),
268+
)
269+
270+
271+
def from_json(text: str) -> CurveReport:
272+
"""The inverse of `to_json()`."""
273+
data = json.loads(text)
274+
return CurveReport(axes=tuple(axis_report_from_dict(a) for a in data["axes"]))
275+
276+
234277
def render_summary(report: CurveReport) -> str:
235278
"""A human-readable summary naming, per axis, the outcome and the binding
236279
constraint at saturation, with every point labelled measured or extrapolated and
@@ -271,3 +314,60 @@ def render_summary(report: CurveReport) -> str:
271314
lines.append("")
272315

273316
return "\n".join(lines).rstrip() + "\n"
317+
318+
319+
def _point_at(axis: AxisReport, level: float) -> LevelResult:
320+
for point in axis.points:
321+
if point.level == level:
322+
return point
323+
raise ValueError(f"axis {axis.axis!r} has no point at level {level}")
324+
325+
326+
def closing_sentence(axis: AxisReport) -> str:
327+
"""#323's PRD's single defensible sentence, generalised across axes: 'sustains N
328+
<unit> before applying backpressure, with its composition and measured/extrapolated
329+
conditions stated' — never a fabricated ceiling. `run_limits_shipper_fanin.py`
330+
carries its own bespoke wording for the PRD's literal flagship example; this is the
331+
generic form #386's one-command entry point uses for every axis, built entirely from
332+
the already-computed `AxisReport`, with no axis-specific knowledge."""
333+
unit = axis.unit
334+
if axis.outcome == RunOutcome.BOUND_NOT_FOUND:
335+
highest = axis.highest_clear_level
336+
if highest is None:
337+
return (
338+
f"{axis.axis}: bound not found — no level was tested without saturating, "
339+
f"so no ceiling is reported (never fabricated)."
340+
)
341+
point = _point_at(axis, highest)
342+
comp = point.composition
343+
return (
344+
f"{axis.axis} sustains at least {highest} {unit} ({comp.real_stacks} real + "
345+
f"{comp.synthetic_senders} synthetic, {point.kind.value}) without applying "
346+
f"backpressure — the ramp never saturated within the tested range, so no "
347+
f"ceiling is reported beyond {highest} {unit} (bound not found; never "
348+
f"fabricated)."
349+
)
350+
351+
tripped = axis.tripped_level
352+
assert tripped is not None # KNEE_FOUND always carries a tripped_level
353+
tripped_point = _point_at(axis, tripped)
354+
extrapolated_note = (
355+
"not extrapolated — the knee itself was measured, not projected"
356+
if tripped_point.kind == PointKind.MEASURED
357+
else "extrapolated beyond what was directly measured"
358+
)
359+
highest = axis.highest_clear_level
360+
if highest is None:
361+
return (
362+
f"{axis.axis} saturated at the first level tested, {tripped} {unit} "
363+
f"({tripped_point.composition.real_stacks} real + "
364+
f"{tripped_point.composition.synthetic_senders} synthetic); no lower level "
365+
f"was sustained ({extrapolated_note})."
366+
)
367+
sustain_point = _point_at(axis, highest)
368+
comp = sustain_point.composition
369+
return (
370+
f"{axis.axis} sustains {highest} {unit} ({comp.real_stacks} real + "
371+
f"{comp.synthetic_senders} synthetic, {sustain_point.kind.value}) before applying "
372+
f"backpressure; saturation observed at {tripped} {unit} ({extrapolated_note})."
373+
)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# SPDX-FileCopyrightText: 2022-2026 David Bensoussan
2+
# SPDX-License-Identifier: MPL-2.0
3+
4+
"""Limits harness: one-command capacity baseline (#386, part of #323's epic).
5+
6+
Combines the four axes' already-written `curve_reporter.CurveReport` JSON files
7+
(Shipper fan-in, single-robot ceiling, Uploader concurrency, drain rate — each written
8+
by its own axis script via `curve_reporter.to_json()`) into one combined, diffable
9+
report plus a human-readable summary carrying every axis's closing sentence
10+
(`curve_reporter.closing_sentence()`). `merge_reports()`/`render_baseline()` are pure
11+
functions of the per-axis reports already on disk — no containers, no ramping, no
12+
axis-specific knowledge — so they're unit-tested directly against synthetic
13+
`CurveReport`s, matching this epic's other reporting code
14+
(`curve_reporter.build_report()`'s own testing precedent). Reading the four report files
15+
and writing the combined ones is the only I/O here; `run_limits_capacity_baseline.sh`
16+
is what actually runs the four axes first.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import argparse
22+
import json
23+
import sys
24+
from collections.abc import Sequence
25+
from pathlib import Path
26+
27+
from curve_reporter import CurveReport, closing_sentence, from_json, render_summary, to_json
28+
29+
30+
def merge_reports(reports: Sequence[CurveReport]) -> CurveReport:
31+
"""One combined report from N single-axis (or multi-axis) reports. Axis order is
32+
resolved by name alone, independent of the order the reports were handed in or of
33+
each axis's own internal ordering — the same stability guarantee
34+
`curve_reporter.build_report()` gives within one report, extended across reports."""
35+
axes = tuple(sorted((axis for report in reports for axis in report.axes), key=lambda a: a.axis))
36+
names = [axis.axis for axis in axes]
37+
if len(names) != len(set(names)):
38+
duplicates = sorted({name for name in names if names.count(name) > 1})
39+
raise ValueError(f"duplicate axis name(s) across the reports being combined: {duplicates}")
40+
return CurveReport(axes=axes)
41+
42+
43+
def render_baseline(report: CurveReport) -> str:
44+
"""The combined artifact's human-readable form: `curve_reporter.render_summary()`'s
45+
per-axis detail, followed by every axis's one-sentence capacity claim in one place —
46+
#386's acceptance criterion that each axis's closing sentence, with its composition
47+
and measured/extrapolated conditions, is present in the combined output."""
48+
lines = [render_summary(report).rstrip(), "", "Closing statements:", ""]
49+
for axis in report.axes:
50+
lines.append(f"- {closing_sentence(axis)}")
51+
return "\n".join(lines).rstrip() + "\n"
52+
53+
54+
def _parse_args(argv: list[str]) -> argparse.Namespace:
55+
parser = argparse.ArgumentParser(description=__doc__)
56+
parser.add_argument(
57+
"--report",
58+
dest="reports",
59+
action="append",
60+
required=True,
61+
metavar="PATH",
62+
help="a per-axis curve_reporter report.json; repeat once per axis",
63+
)
64+
parser.add_argument("--output-json", required=True)
65+
parser.add_argument("--output-txt", required=True)
66+
return parser.parse_args(argv)
67+
68+
69+
def main(argv: list[str] | None = None) -> int:
70+
args = _parse_args(argv if argv is not None else sys.argv[1:])
71+
72+
reports = [from_json(Path(p).read_text()) for p in args.reports]
73+
combined = merge_reports(reports)
74+
75+
Path(args.output_json).write_text(to_json(combined))
76+
summary = render_baseline(combined)
77+
Path(args.output_txt).write_text(summary)
78+
print(summary)
79+
80+
print(json.dumps({"axes": [axis.axis for axis in combined.axes]}))
81+
return 0
82+
83+
84+
if __name__ == "__main__":
85+
sys.exit(main())
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/bin/bash
2+
# SPDX-FileCopyrightText: 2022-2026 David Bensoussan
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
# Limits harness: one-command capacity baseline (#386, the final piece of #323's epic).
6+
# From the repo root:
7+
#
8+
# ./tools/e2e/scripts/run_limits_capacity_baseline.sh
9+
#
10+
# Runs all four limits-harness axes — Shipper fan-in (#382), single-robot ceiling
11+
# (#383), Uploader concurrency (#384), drain rate (#385) — one after another (each is
12+
# its own full podman bring-up/teardown, and no two of them are meant to run
13+
# concurrently, same as every other pair of sibling scenario scripts in this harness),
14+
# then combines the four already-written curve_reporter.py reports into one artifact via
15+
# scripts/limits_baseline.py: a single combined_report.json (stable/diffable — a sort by
16+
# axis name on top of each axis's own stable report, nothing more) and a
17+
# combined_summary.txt carrying every axis's closing sentence
18+
# (curve_reporter.closing_sentence()) alongside its per-axis detail. That's the artifact
19+
# a contributor diffs to check the effect of a change, and the one a release records as
20+
# its capacity baseline instead of repeating the previous release's claim.
21+
#
22+
# Only the Shipper fan-in axis's UNCONSTRAINED phase is folded into the combined
23+
# report — its own deliberately-constrained second phase is that axis's internal
24+
# instrument proof (#382's own gate, run and asserted here as part of running that
25+
# axis), not a fifth axis of this baseline.
26+
#
27+
# Each axis script already fails loudly and aborts on its own gate (a hard-fail process
28+
# exit, not a skip) — combined with this script's own `set -euo pipefail`, any axis
29+
# failing aborts the whole baseline immediately: a combined artifact is only ever
30+
# written from four axes that actually passed.
31+
#
32+
# Every env var each individual axis script accepts (DC_E2E_FANIN_*, DC_E2E_CEILING_*,
33+
# DC_E2E_UPLOAD_*, DC_E2E_DRAIN_*, DC_E2E_IMAGE / DC_WORKSPACE_IMAGE, DC_E2E_KEEP) still
34+
# applies here unchanged — this script does no parameter translation of its own, it only
35+
# sequences the four scripts and combines what they already produce. See each axis
36+
# script's own header for its env vars.
37+
set -euo pipefail
38+
39+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
40+
E2E_DIR="$(dirname "$SCRIPT_DIR")"
41+
RUN_DIR="$E2E_DIR/.run/limits_capacity_baseline"
42+
43+
mkdir -p "$RUN_DIR"
44+
cd "$E2E_DIR"
45+
46+
log() { echo "[e2e-baseline $(date -u +%H:%M:%S)] $*"; }
47+
48+
log "=== axis 1/4: Shipper fan-in (#382) ==="
49+
"$SCRIPT_DIR/run_limits_shipper_fanin.sh"
50+
51+
log "=== axis 2/4: single-robot ceiling (#383) ==="
52+
"$SCRIPT_DIR/run_limits_single_robot_ceiling.sh"
53+
54+
log "=== axis 3/4: Uploader concurrency (#384) ==="
55+
"$SCRIPT_DIR/run_limits_upload_concurrency.sh"
56+
57+
log "=== axis 4/4: drain rate (#385) ==="
58+
"$SCRIPT_DIR/run_limits_drain_rate.sh"
59+
60+
log "combining the four axes' reports into one capacity baseline"
61+
uv run --frozen python3 "$SCRIPT_DIR/limits_baseline.py" \
62+
--report "$E2E_DIR/.run/limits_shipper_fanin/unconstrained_report.json" \
63+
--report "$E2E_DIR/.run/limits_single_robot_ceiling/curve_report.json" \
64+
--report "$E2E_DIR/.run/upload_concurrency/curve_report.json" \
65+
--report "$E2E_DIR/.run/limits_drain_rate/curve_report.json" \
66+
--output-json "$RUN_DIR/combined_report.json" \
67+
--output-txt "$RUN_DIR/combined_summary.txt"
68+
69+
echo
70+
echo "=== #323's capacity baseline (#386) ==="
71+
cat "$RUN_DIR/combined_summary.txt"
72+
73+
log "PASS: limits harness one-command capacity baseline (#386) — $RUN_DIR/combined_report.json"

0 commit comments

Comments
 (0)