RUE-1045: ADR-0072 — runtime benchmarking anchored by a Rue static site generator - #2341
Conversation
Defines the Runtime performance benchmarking project around a first concrete workload: gazette, a static site generator written in Rue, building a frozen snapshot of the rue-lang.dev corpus and compared tool-vs-tool against pinned Zola and Hugo under a feature-parity subset. Reuses the ADR-0067/0071 measurement machinery, adds phase issues for the std prerequisites (read_dir, substring operations), the gazette libraries and program, the rue-bench runtime mode, the cross-tool comparison, and website publication.
The runtime workload builds the live rue-lang.dev content as the site build assembles it. Cross-tool comparison stays internally valid because all tools build the identical corpus within a run; drift is handled by recorded corpus identity per observation, annotated corpus-change events, and the pinned peers acting as a drift control (the gazette-to-peer ratio is the drift-immune longitudinal signal). Validation shifts from committed golden hashes to per-run determinism, cross-tool file-set equality, and spot goldens on stable pages. Freezing returns only as a prerequisite of a future regression ratchet.
Gazette runtime is measured on every trunk push as a new leg of the existing performance-collection workflow, sharing the compile the runtime harness performs anyway. Peer measurements are event-driven: they re-run only when corpus identity, peer versions, template ports or parity configs, or the runner epoch change, detected at fixture prep in the same job; the derive step joins gazette observations to the latest peer observation with matching corpus identity. Scale variants keep a scheduled-cadence safety valve if per-push cost demands.
Adversarial review — ADR-0072 + RUE-1045/1046/1047/1048/1049/1481–1485This is a strong, unusually self-aware ADR — the corpus-drift argument (Decision 2) and the event-driven peer cadence (Decision 9) are both better than the obvious alternatives, and the Negative section is honest about most of what it costs. RUE-1481 and RUE-1482 are correctly scoped and their premises check out against the tree. I read the ADR against the ten Linear issues, the four ADRs it builds on (0057/0067/0070/0071), the actual corpus and templates, 1. The ADR-0067 stall gate turns "intended dogfooding pressure" into a repo-wide hard stopADR-0072:210 and :389 say a page using constructs outside the equivalence subset "breaks the runtime leg of performance collection," framed as "intended dogfooding pressure." Decision 9 says regressions get "maintainer triage and no hard CI gate in v1." ADR-0067 has a second gate that isn't about regressions at all:
If the runtime series sits inside that scope, then one blog post containing a Markdown table stops the runtime series, and five merged commits later every PR in the repository is blocked — with no bypass, and with a remedy that isn't the one ADR-0067 anticipates. ADR-0067 offers "declaring the next epoch, which needs no baseline and so is a small manifest change"; here the fix is extending a Markdown parser written in Rue. "No hard CI gate in v1" and "inherits ADR-0067's machinery" can't both hold. Please state explicitly whether the runtime series is in or out of the stall gate, and if out, by what mechanism (separate suite? advisory record kind, like ADR-0068's incremental suite, which "intentionally does not append to ADR-0067's raw history"?). 2. "That compile doubles as a recorded fresh-process compile observation" is a free lunch the protocol forbidsDecision 7 (:280) puts gazette's compile-time measurement in Decision 9 (:307–312) then has the per-push runtime leg's single opportunistic compile also become "a recorded fresh-process compile observation." Those are incompatible. ADR-0067 §5 pins sampling policy in the epoch, and §"The product boundary" is categorical: "Validation refuses to append a run whose pinned components do not match its epoch." A one-sample, single-worker-setting compile from the per-push job satisfies no declared epoch in either Two related gaps in Decision 7: 3. No parallelism parity — the biggest single threat to the headline numberZola renders pages in parallel via rayon; Hugo is goroutine-parallel by design. Rue has no concurrency: there is no thread/sync/atomic module in So the default configuration measures single-threaded Rue against 4-way-parallel peers, and up to ~4× of whatever ratio gets published is thread count rather than codegen, language, or std quality. Decision 3, Decision 5, and RUE-1485 don't mention threads at all. For an order-of-magnitude claim this may well be acceptable — but it has to be a decision, not an omission, because it's the first thing a skeptical reader will raise. Concretely: pin the peers single-threaded ( 4. The fairness section covers disabled features but not the specialization asymmetry:383 frames the "unfair to Zola/Hugo" critique entirely as disabled features. The larger asymmetry is structural: gazette implements exactly the subset this corpus uses and by design "reject[s] constructs outside their documented subset" (:207), while Zola and Hugo run general engines and pay for generality even with features off. Concretely, from the corpus: of 1,225 shortcode invocations, 1,224 are The defensible headline claim is "a corpus-specialized Rue program vs. general-purpose tools," not "Rue vs. Rust vs. Go." I'd add that as a stated Negative and as a required caption on the published comparison table, alongside the disabled-feature list. 5. RUE-1483's template-engine scope is smaller than the templates it must coverRUE-1483 scopes the engine to "variable interpolation, conditionals, loops over pages/sections, template inheritance (base/child blocks), HTML escaping." What the 17 templates actually use (
Filters and globals are absent from the scope, and the very shortcode template the ADR names uses two of them (
6. "Live corpus" is content-only — templates and config are not live, and there are four sets, not three:175 introduces a benchmark Zola template/config set separate from the production Worth answering directly in the ADR: why port Zola templates at all, rather than run the site's own 17 templates minus the dashboard page? A port that's simpler than production flatters Zola or gazette depending on which way it simplifies, and it's a maintenance surface the ADR is otherwise trying to avoid. 7. Flagging on the raw runtime series will almost never have a clean trailing windowADR-0067 §5's flagging rule compares the current median against a trailing-window median, and corpus identity changes break that window's comparability. From GitHub history: 57 commits touched Decision 2 is right that the peer ratio is the drift-immune signal. But Decision 9 still promises "runtime regressions surface attached to the compiler change that caused them" against the raw series, and Open Question 4 leaves the threshold open without noting this. The ADR should say explicitly that flagging operates on corpus-identity-matched segments (or on the ratio), not on the raw median — otherwise the per-push regression story doesn't actually work. (The good news: since the peer leg fires on corpus change, each corpus-identity segment has a same-run peer denominator, so segment-scoped ratio flagging is well defined.) 8. Recording-instead-of-pinning the corpus is a genuine deviation from ADR-0067, not reuse:242: "corpus identity is recorded per observation rather than pinned." ADR-0067 §4 is the opposite rule:
and lists "each workload's own sources" among what stays pinned, specifically because they are not the product. The std precedent the ADR leans on doesn't transfer: std is unpinned because std is the product; the website corpus is an input. I think the decision is right — the RUE-1256 lesson (pinning an input that moves stops the series rather than describing it) applies cleanly here. But it introduces a category ADR-0067's validator has no concept of ("recorded input"), so the ADR's claim that it "reuses that machinery rather than inventing a parallel system" understates what's happening. Please say plainly that ADR-0067's identity rule is amended for the runtime suite, and what schema addition that implies. 9. Factual corrections
10. Two of RUE-1045's five open questions are dropped rather than answeredRUE-1045 asked the ADR to address five open questions. Regression detection, cadence, and peer pinning are all answered well. Two are not:
RUE-1045 also specifies sequencing as "harness first, then incremental corpus expansion, then peer translations," which this ADR inverts (harness is Phase 4). That's defensible but deserves a sentence, because of Finding 11. 11. Sequencing: the ADR's own "ships as soon as" promise is gated behind three phases of language work:299: "The longitudinal Rue-only view ships as soon as the harness produces data and does not wait for the cross-tool comparison." RUE-1049 makes the same promise. But under Implementation Phases the harness is Phase 4, behind Concrete alternative worth an explicit rejection if you don't take it: stand up Phase 4 first against a workload that exists today. This is the alternative I'd most want to see in an "Alternatives Considered" section (8 of 73 ADRs have one, so no convention is being broken — but this document has three live alternatives worth recording: harness-first-on-existing-example, frozen snapshot corpus, and translation-first per RUE-1048; the latter two are argued away well in prose already). 12. Scale-variant validity (for RUE-1484):178, "deterministic 10x and 100x scale variants, generated by path-prefixed duplication." At 100× that's ~9,600 pages / ~52 MB, hashed in full per sample per tool. Two wrinkles worth pre-empting:
13. Parity/determinism traps to pre-empt in RUE-1485
14. Verified-clean, for the record
What I'd want changed before acceptanceFindings 1, 2, 3, 5, 9, and 10 — each is either an internal contradiction, a claim the surrounding machinery won't honor, a number that's wrong, or a charter question dropped without a decision. 4, 7, 8, and 11 are substantive enough that I'd want at least a paragraph each, since they change how the results must be presented and how long the project takes to produce its first useful output. 12 and 13 are fine as follow-ups into RUE-1484/1485. None of that touches the core judgment, which I think is right: an SSG is a genuinely good first realistic workload, tool-vs-tool before translation is the right sequencing, and refusing to freeze a corpus that changes daily is the correct call for a comparative observatory. Generated by Claude Code |
Addresses the adversarial review on PR #2341: - The runtime series is its own record kind outside ADR-0067's required-CI gates, including the no-bypass stall gate; a stalled runtime series is a dashboard flag, never a repo-wide PR block (the ADR-0068 advisory posture). - Drop the claim that the runtime leg's compile doubles as a recorded compile observation; the scaling curve is the only recorded compile-time series for gazette, and joining it is a suite-revision event with the workflow time budget re-checked. - Thread parity: primary ratio pins peers single-threaded (RAYON_NUM_THREADS=1, GOMAXPROCS=1), mirroring ADR-0071's one-worker rationale; default-parallel peers published as a labeled secondary row; thread policy recorded in the epoch. - Fairness: gazette must use general engine paths (no corpus-specific fast paths for the 1,224 rule() shortcodes); published comparisons carry a corpus-specialized-vs-general-purpose caption. - Template subset expanded to the filters/helpers the ports use; pagination and the load_data-driven dashboard page are explicit carve-outs, and the ports are production templates minus those. - Recording-not-pinning the corpus stated as a deliberate amendment to ADR-0067's identity rule (new recorded-input category). - Flagging operates on corpus-identity-matched segments and the per-segment peer ratio, never across a corpus discontinuity. - Phases reordered harness-first: rue-bench runtime mode stands up on existing examples (wordfreq) before any std or gazette work. - Corpus size corrected to 513 KiB apparent (525,479 bytes); shortcode split stated (1,224 rule / 1 preview_feature); PMU counters marked unavailable on hosted runners; file-set allowlist and Hugo feed determinism traps recorded; Alternatives Considered added; RUE-487 relationship explicitly deferred.
|
All 14 findings addressed in b01f74b; the six blocking ones are all taken as written. I re-verified the load-bearing claims before editing (corpus byte count, shortcode split, the ADR-0067 stall-gate text, 1 — Stall gate. Taken. Decision 9 now states the runtime series is its own record kind sitting outside ADR-0067's required-CI gates, including the no-bypass stall gate, with the reasoning spelled out (that gate presumes an epoch-declaration remedy; this series' stall remedy is parser work). A stalled runtime series is a dashboard staleness flag and triage item, never a repo-wide block — explicitly the ADR-0068 advisory posture. 2 — Free-lunch compile observation. Taken; the claim is dropped rather than papered over. Decision 9's parenthetical now says the compile is shared cost only, and Decision 7 states the scaling curve is the only recorded compile-time series for gazette, that a one-sample compile matches no declared epoch and validation would rightly refuse it, that joining 3 — Thread parity. Taken, as a decision: the primary published ratio pins the peers single-threaded ( 4 — Specialization asymmetry. Taken, in both directions: a fairness rule on gazette (shortcodes/filters/templates must go through general engine paths; corpus-specific fast paths are out of bounds — the 1,224/1 5 — Template-engine scope. Taken. The in-scope subset now enumerates the filter/helper set the ports actually use ( 6 — Four sets, why port at all. Taken. The carve-outs answer the "why port" question and the ADR now says so: each port is the production template set minus the excluded pages and features, kept as close to production as the subset allows. The Negative now counts four template/config sets and asks website-template changes to include a port review. 7 — Flagging across discontinuities. Taken. Decision 9 now states raw medians are compared only within corpus-identity-matched segments, never across a change, and that the cross-segment signal is the per-segment peer ratio — well-defined because the peer leg fires on exactly the corpus-change runs. Open Question 4 is reworded to be about segment/ratio thresholds. 8 — Amendment, not reuse. Taken. Decision 2 now names the deviation: a third input category ("recorded input") that ADR-0067's compile-time suites don't have, named explicitly in the runtime record schema, with ADR-0067's rules unchanged for compile-time suites. 9 — Facts. All corrected: 513 KiB / 525,479 bytes in both ADR locations and RUE-1484; shortcode split stated (1,224 10 — Dropped charter questions. Taken. RUE-487 is in 11 — Harness-first. Adopted, not just recorded: phases are reordered so Phase 1 stands 12 — Scale-variant validity. Taken into both the ADR (Decision 2 now notes the curve is a page-count curve, not a site-shape curve, since 13 — Parity/determinism traps. Taken into Decision 4 (documented file-set allowlist for sitemap/feed-name/static differences; ports strip build-time-varying feed output, Hugo 14 — Verified-clean notes. The One judgment call to flag rather than bury: finding 11's adoption means RUE-1046 is now the first implementation issue and its scope statement ("first implementation step") is true again. If gazette-first was actually preferred despite the review's argument, that's a one-commit revert — but the harness-first case looks correct to me on the project's own goal of "a trusted system to iterate against while improving performance." Generated by Claude Code |
steveklabnik
left a comment
There was a problem hiding this comment.
I reviewed the revised ADR at b01f74b, including both discussion comments and the disposition of the earlier 14 findings. That rewrite materially improves the proposal, but I think five design gaps remain before acceptance:
-
Output validation can still publish wrong-but-fast results. Decision 4 validates determinism, filenames, non-empty pages, and a few spot goldens. A renderer that consistently drops body content from every un-goldened page passes all of those checks. The design needs an exhaustive semantic oracle for every page—e.g. normalized metadata, headings, text, links, shortcode results, section membership, and feed ordering—checked outside the timed window. Per-tool spot goldens are still useful for rendered-form coverage, but they are not a complete work-equivalence oracle.
-
The recorded corpus identity does not clearly cover the complete workload input. Decision 2 describes the identity as the Markdown content tree, while Decision 4 includes static passthrough in the file-set contract.
website/staticcurrently contains 35 files and roughly 1.2 MiB, more bytes than the Markdown corpus. A static change would therefore alter copied work/output without necessarily changing the stated identity or triggering peer remeasurement. Either the recorded identity and event detector must cover every live input—including static assets—or static copying should be excluded equally from every tool's measured boundary. -
The peer ratio is corpus-normalized, not drift-immune. Pinned peers still move with hosted-runner variation, not only corpus changes. With the event-only cadence, one noisy peer observation becomes the denominator for an entire corpus segment, so cross-segment ratio flagging can inherit that noise. I would either run at least one cheap, single-threaded 1× peer canary alongside every Gazette observation, or explicitly keep cross-segment ratios advisory and stop describing them as drift-immune. The latter may be enough for the stated order-of-magnitude objective.
-
The runtime platform and calibration contract is incomplete. Decision 5 says runtime measurement inherits the compiler suites' pinned GitHub Actions regime and calibration, while Decision 6 makes directory enumeration Linux-first. The current per-push compiler collector covers x86-64 Linux, AArch64 Linux, and AArch64 macOS; runtime sample counts also cannot inherit calibration from unrelated compiler workloads. Please name the exact v1 runtime platform matrix and require runtime-specific, per-platform calibration before using regression flags.
-
Harness-first now conflicts with “Gazette is the first runtime benchmark.” Decision 1 says Gazette is first, while Phase 1 begins a durable runtime series with “
wordfreqat minimum, optionallyjsonfmtorruelex.” That phase should declare an exact initial workload, a meaningful fixed fixture, arguments, correctness oracle, and whether the resulting series is permanent. Reusing the current tiny CLI fixture would mostly measure process startup and is not enough to establish a useful runtime trend.
The overall direction still looks good: an SSG is a strong realistic workload, harness-first is the right implementation order, single-threaded peers as the primary comparison is defensible, and the revised ADR is now honest about specialization and corpus discontinuities. CI is green; these findings are about completing the measurement contract rather than implementation failures.
…, matrix Addresses steveklabnik's five findings on PR #2341: - Decision 4 gains a semantic oracle run on every emitted page outside the timed window (normalized metadata, heading tree, text, links, shortcode results, section membership, feed ordering, compared across tools), closing the consistent-content-drop hole spot goldens leave. - Recorded identity now covers the complete fixture input: content tree, static passthrough assets (33 files, ~1.2 MiB), and the template-port/ parity-config revision; the peer leg triggers on any of it, and static copying is explicitly measured work under that identity. - The peer ratio is renamed corpus-normalized (not drift-immune); a single-threaded Zola 1x canary rides every gazette observation as the same-run denominator, the full peer matrix stays event-driven, and cross-segment ratio flags are advisory until calibration exists. - Decision 5 names the exact v1 platform matrix (x86_64-linux on the pinned regime; aarch64-linux after Phase 2 verifies there; macOS behind the @syscall gap) and requires runtime-specific per-platform calibration — none inherited from compiler workloads. - Phase 1 declares its exact initial workload: wordfreq over a seeded, tens-of-MiB deterministic text fixture (seed and generator pinned in runtime.toml, identity recorded), fixed args, byte-exact golden oracle, and a permanent series; gazette is reworded as the anchor workload rather than the first measured program.
|
All five gaps addressed in 1d935e7. Per-finding: 1 — Semantic oracle. Taken. Decision 4 gains a fourth validation layer: outside the timed window, a normalized extraction (front-matter metadata, heading tree, visible text, link targets, shortcode expansion results, section membership, feed entry ordering) is computed for every emitted page and compared across all three tools. The consistent-body-drop case you describe now fails by construction, and spot goldens are re-scoped to what the oracle deliberately ignores — rendered markup-level form. 2 — Complete input identity. Taken, on the include side rather than the exclude side: static passthrough is real SSG work all three tools perform, so it stays in the measured boundary, and the recorded identity is now a tree hash over everything the tools consume — content tree, static assets (verified at 33 files / ~1.2 MiB on this branch, more bytes than the Markdown, as you said), and the versioned template-port/parity-config revision. The peer-leg event detector triggers on that full fixture identity, so no input class can change the job invisibly. 3 — Ratio honesty. Both of your options, actually: the term "drift-immune" is gone everywhere in favor of corpus-normalized, with the denominator's runner noise stated explicitly; and a per-run canary — one single-threaded Zola build of the 1× corpus alongside every gazette observation — gives every observation a same-run denominator so no segment leans on a stale or singleton peer sample. The full peer matrix (Hugo, scale variants, default-parallel row) stays event-driven, and the canary sits under the same cost safety valve as the scale variants. Even with the canary, cross-segment ratio flags are explicitly advisory until finding 4's calibration exists. 4 — Platform matrix and calibration. Taken. Decision 5 now names the exact v1 matrix: 5 — Phase 1 workload declaration. Taken. Decision 1 is reworded — gazette is the anchor workload and first cross-tool benchmark, not the first measured program — and the phase plan now declares Phase 1's initial workload exactly: Linear is synced (RUE-1046 carries the full Phase 1 declaration; RUE-1484/1485 carry the oracle, identity, and canary changes). Validator passes; CI running on the push. Generated by Claude Code |
Addresses the adversarial review on PR rue-language#2341: - The runtime series is its own record kind outside ADR-0067's required-CI gates, including the no-bypass stall gate; a stalled runtime series is a dashboard flag, never a repo-wide PR block (the ADR-0068 advisory posture). - Drop the claim that the runtime leg's compile doubles as a recorded compile observation; the scaling curve is the only recorded compile-time series for gazette, and joining it is a suite-revision event with the workflow time budget re-checked. - Thread parity: primary ratio pins peers single-threaded (RAYON_NUM_THREADS=1, GOMAXPROCS=1), mirroring ADR-0071's one-worker rationale; default-parallel peers published as a labeled secondary row; thread policy recorded in the epoch. - Fairness: gazette must use general engine paths (no corpus-specific fast paths for the 1,224 rule() shortcodes); published comparisons carry a corpus-specialized-vs-general-purpose caption. - Template subset expanded to the filters/helpers the ports use; pagination and the load_data-driven dashboard page are explicit carve-outs, and the ports are production templates minus those. - Recording-not-pinning the corpus stated as a deliberate amendment to ADR-0067's identity rule (new recorded-input category). - Flagging operates on corpus-identity-matched segments and the per-segment peer ratio, never across a corpus discontinuity. - Phases reordered harness-first: rue-bench runtime mode stands up on existing examples (wordfreq) before any std or gazette work. - Corpus size corrected to 513 KiB apparent (525,479 bytes); shortcode split stated (1,224 rule / 1 preview_feature); PMU counters marked unavailable on hosted runners; file-set allowlist and Hugo feed determinism traps recorded; Alternatives Considered added; RUE-487 relationship explicitly deferred.
Adds ADR-0072 (status: proposal), the design document for the Runtime performance benchmarking project, with a first concrete workload: gazette, a static site generator written in Rue that builds the live rue-lang.dev corpus and is measured tool-vs-tool against pinned Zola and Hugo.
Key decisions:
website/build.shassembles it — no frozen snapshot. Cross-tool comparison is internally valid because all tools build the identical corpus within a run; every observation records corpus identity (tree hash, file count, bytes), corpus changes are annotated chart events, and the pinned peers act as a drift control — the gazette-to-peer ratio is the drift-immune longitudinal signal. Freezing returns only as a prerequisite of a future regression ratchet.performance/runtime.tomlmanifest on the ADR-0067 suite/epoch model; observations append to theperformance-data-v1orphan branch; the website derives comparison tables and time series at build, per the plan in RUE-1049.The remaining phase issues are RUE-1483 (gazette libraries), RUE-1484 (gazette + live-corpus fixture prep), RUE-1485 (cross-tool comparison), and RUE-1046 (harness mode). The ADR index in
docs/designs/README.mdis regenerated viascripts/validate-adrs.py --write.Fixes RUE-1045