Skip to content

Collapse results and alerts by default in the reports list API#926

Merged
epompeii merged 1 commit into
develfrom
claude/collapse-report-list
Jul 18, 2026
Merged

Collapse results and alerts by default in the reports list API#926
epompeii merged 1 commit into
develfrom
claude/collapse-report-list

Conversation

@epompeii

@epompeii epompeii commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

BREAKING CHANGE: GET /v0/projects/{project}/reports now omits the results and alerts for each report by default. One console reports page could be large JSON, fully buffered by Dropshot before send, driving server memory spikes and a large amount of edge egress. Set the new expand query param to true (bencher report list --expand) to restore the previous full-report behavior; the console UI does not use it.

A new counts field is always present on every report response and mirrors the collapsed fields:

"counts": {
  "results": [
    { "benchmarks": 5, "measures": 2 },
    { "benchmarks": 5, "measures": 2 }
  ],
  "alerts": { "total": 3, "active": 1 }
}

Changes

  • JsonReport.results and .alerts are now Option with skip_serializing_if; new counts: JsonReportCounts field and expand query param
  • QueryReport::into_json takes a ReportMode { Full, Collapsed } enum so the compiler forces every call site to choose; only the list endpoint defaults to Collapsed (GET-one, POST, and bencher run stay Full)
  • Collapsed counts come from one GROUP BY aggregate over report_benchmark JOIN metric plus a status-only alert query; the full path computes counts in memory from the loaded data, and a test asserts both paths agree
  • Console: ReportRow renders from counts; the perf Reports tab lazily fetches GET-one when a report is expanded; the perf page default plot bootstrap fetches the first report via GET-one
  • CLI: new bencher report list --expand flag; bencher run reads its alert count from counts; bencher_comment handles the Option fields
  • Changelog entry with the breaking-change callout; regenerated openapi.json and bencher.ts

Compatibility

An older CLI's bencher report list cannot parse the new default list response (its generated type requires results). Everything else, including older bencher run, keeps working.

Testing

  • 4 new model unit tests (per-iteration counts, empty report, alert counts, full/collapsed agreement) and 4 new endpoint integration tests (collapsed by default, expand=true, GET-one and POST stay full)
  • Seed test asserts the collapsed list, --expand, and alert counts end to end
  • Browser smoke: public perf page default plot bootstrap, expandable report card, and branch page Recent Reports card all render correctly against a seeded server; collapsed list page was expanded even on tiny 5-benchmark seed reports

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

PR: #926
Base: devel
Head: claude/collapse-report-list
Commit: 97dd64b3582a5df5d0e05c020fdde2789109baf9


Cargo commands are blocked in this environment, so I'll base the review on the diff analysis. Here is my review.


Code Review: Collapse results and alerts by default in the reports list API

Summary

This PR makes the reports list endpoint (GET /projects/{project}/reports) omit the full results and alerts arrays by default, returning lightweight counts instead. Callers opt back into the full payload with ?expand=true. The single-report GET and the POST response remain full. The change is well-structured, thoroughly tested (unit + integration + seed-test + console), and the type generation (openapi.json, bencher.ts) was regenerated. Overall this is high quality and I found no correctness bugs.

Strengths

  • Excellent test coverage / TDD. New unit tests (get_report_counts_per_iteration, _empty_report, _alerts, into_json_full_and_collapsed_agree), integration tests covering collapse-by-default, expand=true, get-one-always-full, and POST-with-counts, plus seed-test assertions. The into_json_full_and_collapsed_agree test is the key one: it pins the invariant that the two count paths must agree.
  • Correctness of the two count paths. The collapsed aggregate query (report_benchmark ⋈ metric, count(distinct benchmark_id) / count(distinct measure_id)) correctly mirrors the full JSON builder, which inner-joins metrics and groups consecutive same-benchmark rows ordered by (iteration, benchmark.name, measure.name). Since benchmarks are unique per name, iteration.len() equals the distinct-benchmark count. The alerts join chain (alert → boundary → metric → report_benchmark) is 1:1 up the chain, so count_star per status can't double-count.
  • Clean refactor of bencher_comment. Introducing results() / alerts() accessors that funnel through as_deref().unwrap_or_default() is tidy and keeps the Option handling in one place.
  • Good adherence to CLAUDE.md: strong types throughout, ReportMode enum with doc comments, no emdashes, top-down module ordering, #[expect(...)] with reasons, QueryResult-style error mapping, and types regenerated.

Observations / Minor Points

  1. Breaking wire-format change (intended, but broad). JsonReport.results and .alerts change from required to optional, and the list endpoint stops returning them by default. Any third-party API consumer (not just the CLI/console) that reads results/alerts from the list endpoint will now silently get nothing until they add expand=true. This is the point of the PR and a changelog entry is included, but given the public v0 API it's worth making sure the changelog wording makes the default-behavior change explicit for external clients. Per CLAUDE.md, since the CLI changed (--expand), avoid auto-closing keywords in the PR description.

  2. Per-report query count in the list path. Collapsed mode runs two aggregate GROUP BY queries per report (results + alerts), so a page of N reports is ~2N queries. This is not a regression (the prior full path also ran per-report queries and loaded far larger payloads), and it's a net win, but the N+1 shape remains. Fine to leave; noting for awareness if list latency ever matters at large page sizes.

  3. Duplicated guard condition in PerfPanel.tsx. first_report_fetcher's condition (!clear() && branchesIsEmpty() && ... && tab() === DEFAULT_PERF_TAB) partially duplicates the guard in the following createEffect. If one is edited later the two could drift. Minor; consider extracting a shared memo. Also, the default plot now bootstraps only after the extra single-report fetch resolves (a small added latency / a fetch failure silently skips bootstrap), which is an acceptable degradation.

  4. run/mod.rs overflow guard. usize::try_from(json_report.counts.alerts.total).unwrap_or(usize::MAX) is fine and behavior-equivalent to the old alerts.len() (POST returns Full mode, where report_counts sets total = alerts.len()).

  5. Console measure-count semantics preserved. boundaryLimitsMap(iteration).size keyed distinct measures per iteration; the new counts.measures is also distinct measures per iteration, so the table display is unchanged.

Verdict

No blocking issues. The core invariant (collapsed counts == full counts) is the main risk, and it's directly locked down by tests. I was unable to run cargo build/clippy/nextest in this environment (cargo is sandbox-blocked), so please confirm CI green for: cargo clippy --all-features -Dwarnings, cargo check --no-default-features, cargo gen-types (no diff), and the console typecheck. Recommend approve once CI passes.


Model: claude-opus-4-8

@epompeii
epompeii force-pushed the claude/collapse-report-list branch from 978b032 to 210b67f Compare July 8, 2026 06:34
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchclaude/collapse-report-list
Testbedintel-v1
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
microseconds (µs)
(Result Δ%)
Upper Boundary
microseconds (µs)
(Limit %)
Adapter::Json📈 view plot
🚷 view threshold
4.74 µs
(+1.94%)Baseline: 4.65 µs
4.90 µs
(96.65%)
Adapter::Magic (JSON)📈 view plot
🚷 view threshold
4.59 µs
(+1.67%)Baseline: 4.52 µs
4.72 µs
(97.28%)
Adapter::Magic (Rust)📈 view plot
🚷 view threshold
25.40 µs
(-0.90%)Baseline: 25.63 µs
26.75 µs
(94.96%)
Adapter::Rust📈 view plot
🚷 view threshold
3.50 µs
(+0.26%)Baseline: 3.50 µs
3.60 µs
(97.26%)
Adapter::RustBench📈 view plot
🚷 view threshold
3.51 µs
(+0.48%)Baseline: 3.49 µs
3.60 µs
(97.50%)
🐰 View full continuous benchmarking report in Bencher

@epompeii
epompeii force-pushed the claude/collapse-report-list branch from 210b67f to 480fcc9 Compare July 8, 2026 06:39
@epompeii epompeii self-assigned this Jul 15, 2026
BREAKING CHANGE: GET /v0/projects/{project}/reports now omits the results
and alerts for each report by default. One console reports page could be
~150 MB of JSON, fully buffered by Dropshot before send, driving server
memory spikes and edge egress. Set the new expand query param to true
(bencher report list --expand) to restore the previous behavior.

A new counts field summarizes each report: the number of benchmarks and
distinct measures per iteration and the total and active alerts. It is
always present, computed from the loaded data on the full path and with
cheap aggregate queries on the collapsed path.

The console now renders report rows from counts, lazily fetches the full
report when one is expanded on the perf page, and bootstraps the default
perf plot with a single get-one request.
@epompeii
epompeii force-pushed the claude/collapse-report-list branch from 480fcc9 to 97dd64b Compare July 18, 2026 03:38
@epompeii
epompeii marked this pull request as ready for review July 18, 2026 03:45
@epompeii
epompeii merged commit ac764f2 into devel Jul 18, 2026
68 checks passed
@epompeii
epompeii deleted the claude/collapse-report-list branch July 18, 2026 04:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant