Skip to content

docs(bench): plot the results, and fold the tables under them - #451

Merged
FBumann merged 5 commits into
mainfrom
docs/benchmark-charts
Aug 1, 2026
Merged

FBumann merged 5 commits into
mainfrom
docs/benchmark-charts

Conversation

@FBumann

@FBumann FBumann commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

The benchmarks page was 110 table rows in twelve tables, all weighted the
same, with no headline and no shape — and the one chart page we had
(benchmarks-scaling.html) was a separate file, absent from the nav, linked
from the middle of a paragraph. The numbers were published; the finding was
not.

Judge it here: the Read the Docs preview build on this PR renders the real
page in both schemes — that is the surface to look at, not the diff.

Four figures, from the same jsonl as the tables

wall time to a loaded solver, faceted by sink, log-log
peak the same builds' memory — its own chart, never a second axis on the first
cases all six models as small multiples: same two lines, six shapes
phases where the time goes at l — the chart the tables cannot be read for, since they publish one total per row

phases is the one I would keep if only one survived: our claim lives in
build, and after #438 emit is at parity. The tables mix those together into
a single ratio.

How they are drawn

bench/svg.py, no plotting library. These pages are read on GitHub as often as
on the site, so a chart has to be a committed file rather than something a
renderer produces at view time — and a dependency that draws axes is a poor
trade for a docs build that installs in seconds.

Two files per figure. mkdocs-material's palette toggle stamps the host
page, which an <img>-referenced SVG cannot see, so the page carries both and
Material's #only-light / #only-dark suffixes choose. GitHub takes the first
of the pair. Dark is drawn first because dark is the site's default scheme.

The palette is validated, not chosen. Slots 1 and 2 of a reference
categorical palette, run through six checks against both surfaces — lightness
band, chroma floor, CVD separation (worst adjacent ΔE 24.7 light / 26.8 dark),
normal-vision floor, contrast. Every series is direct-labelled as well as
coloured, so identity never rests on hue alone.

What else changed

  • The twelve tables stay, one <details> per case. <details> rather than
    mkdocs' ??? because it folds on GitHub too.
  • Their headings became bold text: a heading inside a fold still lands in
    the table of contents, and twelve rail entries for the appendix was most of
    what made the page feel like a wall.
  • docs/bench.svg deleted — drawn for Backend router, vtype, shift(), and piecewise: blocks #19 in the duckdb era, hardcoded
    light-mode colours, referenced from nowhere since.
  • tests/test_docs_site.py checks figures in both directions: every
    reference resolves, every file is referenced, every dark has a light twin. The
    orphan above is exactly what the second half prevents.

Notes

Stacked on #438 (it touches bench/report.py too) — GitHub will retarget this
to main when that merges.

The figures plot the existing committed results, which carry lp and
highs. The gurobi facet appears on your next ladder run; bench.plot drops a
sink that has no measurements rather than drawing an empty panel, since a facet
with no line reads as a measurement of zero.

🤖 Generated with Claude Code

@FBumann FBumann added the docs Documentation pages, guides, reference and README label Jul 31, 2026
@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@FBumann, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dab238f7-09f0-41af-99dd-58410e36450d

📥 Commits

Reviewing files that changed from the base of the PR and between c14b180 and 35aefe2.

⛔ Files ignored due to path filters (9)
  • docs/bench.svg is excluded by !**/*.svg
  • docs/charts/cases-dark.svg is excluded by !**/*.svg
  • docs/charts/cases-light.svg is excluded by !**/*.svg
  • docs/charts/peak-dark.svg is excluded by !**/*.svg
  • docs/charts/peak-light.svg is excluded by !**/*.svg
  • docs/charts/sinks-dark.svg is excluded by !**/*.svg
  • docs/charts/sinks-light.svg is excluded by !**/*.svg
  • docs/charts/wall-dark.svg is excluded by !**/*.svg
  • docs/charts/wall-light.svg is excluded by !**/*.svg
📒 Files selected for processing (12)
  • bench/conftest.py
  • bench/plot.py
  • bench/report.py
  • bench/results.py
  • bench/results/latest.json
  • bench/results/latest.jsonl
  • bench/svg.py
  • docs/benchmarks-scaling.html
  • docs/benchmarks.md
  • docs/stylesheets/extra.css
  • mkdocs.yml
  • tests/test_docs_site.py
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch docs/benchmark-charts
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/benchmark-charts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@read-the-docs-community

read-the-docs-community Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 lpspec | 🛠️ Build #33864443 | 📁 Comparing 35aefe2 against latest (ce0fb3d)

  🔍 Preview build  

28 files changed · ± 28 modified

± Modified

@FBumann
FBumann force-pushed the docs/benchmark-charts branch from 38c9c55 to f319cd1 Compare July 31, 2026 20:49
@FBumann
FBumann changed the base branch from bench/gurobi-arm to main July 31, 2026 20:49
@FBumann

FBumann commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main, which moved a long way under this branch — and that turned up two things worth flagging separately from the charts.

bench.plot was broken on main. #448 moved the readers to pytest-benchmark's .json, but bench/results/ still holds the .jsonl the previous runner wrote, so refreshing the chart page or the figures died on a missing file. results.records now accepts either — the old format is one record per line and already in the shape both readers want.

The figures had been drawn from the wrong file. They came from a latest.jsonl at lpspec 0.0.0a27 that still carried duckdb and duckdb@1GB arms — from before that engine was removed. They now come from what the repo ships today (lpspec 0.0.1a55), so figures, tables and chart page all read the same run. The interactive page's own DATA was already in step and did not move.

Also: the reproduce block still named python -m bench.run, which #448 deleted. It is the pytest commands now.

Rebased past the merged #438, so this targets main directly.

@codspeed

codspeed Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 12.53%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
✅ 10 untouched benchmarks
⏩ 10 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ test_emit[transport-s-lpspec-lp] 24.6 MB 21.6 MB +13.71%
⚡ test_emit[profiled-s-lpspec-lp] 34.3 MB 30.8 MB +11.37%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing docs/benchmark-charts (35aefe2) with main (c14b180)

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@FBumann

FBumann commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

@codspeedbot explain why this is faster

@codspeed

codspeed Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

@codspeedbot explain why this is faster

Short answer: it isn't faster, and it isn't this PR. The single reported change is a peak-heap measurement (memory mode, not time), and the code it measures is byte-identical between main@e160e32 and f10c3ee.

The measured tree did not change

git diff main HEAD on this branch touches no src/, no bench/test_ladder.py, no bench/cases.py. What it does touch cannot reach a measured allocation:

  • bench/plot.py, bench/svg.py, bench/report.py, bench/results.py, the figures and docs/ — all reporting-side, none imported by the benchmark.
  • TRACKED in bench/conftest.py — read only inside pytest_benchmark_update_machine_info, a pytest-benchmark hook, via importlib.metadata.version. The CodSpeed job installs --no-default-groups --group codspeed, so pytest-benchmark isn't even present and that hook never fires.
  • pytest-benchmem>=0.5 — in the bench group, which that same job does not install.

The allocation profile says "same program, different machine"

BASE (e160e32) HEAD (f10c3ee)
peak heap 7,254,141 B 5,677,912 B (−21.7%)
alloc calls 49,411 49,124 (−0.6%)
total allocated 61.4 MB 55.5 MB (−9.7%)
avg alloc size 1,243 B 1,129 B (−9.2%)

The same number of allocations, each slightly smaller. A real optimisation normally removes allocations; a buffer-sizing change removes bytes while the call count stays put. That is what machine-dependent sizing looks like — and the runner CPU did change under this comparison (EPYC 7763 → EPYC 9V74, 64 → 80 cores), which CodSpeed flagged for exactly these twelve memory benchmarks. polars sizes its thread pool and per-thread buffers from the visible core count, which moves total bytes without moving the call count.

The rest of the matrix moved both ways

In the same pair of runs: dispatch-s-lpspec-lp 35.6 → 37.7 MB (worse by 5.9%), transport-s-lpspec-highs 22.2 → 22.8 MB, nodal-s-lpspec-lp −0.7%, sector-s-lpspec-highs −1.4%. And this benchmark on main alone, with nothing on its path changing: 6.80 / 6.92 / 7.41 MiB across 6e4e3a3, e160e32, c14b180.

sector at the s rung is the smallest peak in the matrix (~5–7 MiB), so a ~1.5 MB absolute shift reads as 28% where the same shift on fleet (38 MB) would not clear the threshold. Only one of twelve crossed it, and one moved the other way by 5.9% — the signature of a threshold catching the smallest benchmark, not of a change in emit.

Verdict: unattributable to the diff; do not claim it in the changelog. It should revert to ~6.9 MB the next time this branch is measured on a 7763-class runner.

One thing worth fixing, if these numbers are to be compared over time

uv.lock is gitignored, so uv sync --no-default-groups --group codspeed resolves fresh on every CodSpeed run. Two runs an hour apart almost certainly got the same polars/numpy/pyarrow, but nothing guarantees it, and across weeks it definitely won't — a polars release would land in the report as an lpspec regression with no lpspec commit behind it. Given TRACKED exists precisely because "a number measured against a different polars is a different number", pinning the CodSpeed job's resolution (a committed lock for CI, a constraints file, or --exclude-newer) would close the same hole on the instrument side. Happy to open that as its own PR if you want it.

No changes committed here — there was nothing to fix in the branch.

Summary
  • Warming up the performance wizard...
  • Investigating the reported memory improvement — Traced the 27.76% peak-heap change to a runner environment difference, not a code change. No changes needed.
  • Handling comment — Comment handling finished successfully

Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.

FBumann and others added 5 commits August 1, 2026 09:28
The benchmarks page was 110 table rows in twelve tables, all weighted the
same, with no shape and no headline — and the one chart page we had was a
separate HTML file linked from a mid-paragraph, absent from the nav. So
the numbers were published and the *finding* was not.

Four figures now lead the Results section, written by `bench.plot` from
the same jsonl the tables come from:

  wall    time to a loaded solver, faceted by sink, log-log
  peak    the same builds' resident memory, its own chart, never a
          second axis on the first
  cases   all six models as small multiples
  phases  where the time goes at `l` — the chart the tables cannot be
          read for at all, since they publish one total per row

`bench/svg.py` draws them: no plotting library, because these pages are
read on GitHub as often as on the site and a chart has to be a committed
file rather than something a renderer produces at view time. Two files
per figure, light and dark, because mkdocs-material's toggle stamps the
host page and an `<img>`-referenced SVG cannot see it; the `#only-light`
/ `#only-dark` suffixes choose, and GitHub takes the first of the pair.

The palette is validated rather than chosen — slots 1 and 2 of a
reference categorical palette, run through the six checks against both
surfaces (lightness band, chroma floor, CVD separation, normal-vision
floor, contrast). Every series is direct-labelled as well as coloured, so
identity survives a reader who sees neither hue.

The twelve tables stay, one `<details>` per case, and their headings
became bold text: a heading inside a fold still lands in the table of
contents, which is twelve rail entries for the appendix.

`docs/bench.svg` deleted — drawn for #19, hardcoded light-mode colours,
referenced from nowhere since. `tests/test_docs_site.py` now checks both
directions, so the next one fails instead of rotting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things, both about reading rather than drawing.

**The charts stopped shrinking below the tablet breakpoint.** They are
`<img>`-referenced SVG, so they scaled with the column like any image —
right on a wide screen and wrong on a narrow one, where a uniform
downscale takes 11px axis labels to about 5px and leaves a chart whose
axes cannot be read. Now the figure scrolls sideways instead and the
chart keeps its size. Done in CSS, selected on the `src` path, so the
markdown stays exactly what GitHub renders — which is the whole reason
these are files rather than inline SVG.

**The interactive page is in the nav.** It has always had the cursor the
static figures cannot: a pointer readout over the same run, ratio bands
across all six models, and its own light/dark. It was reachable only
through one link in the middle of a paragraph. Hover was never missing
software — it was a missing nav entry.

The Results section says so once, under the figures, rather than four
times.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things, and the first is why the other two matter.

**`bench.plot` could not run.** #448 moved the readers to
pytest-benchmark's `.json` while `bench/results/` still holds the `.jsonl`
the previous runner wrote, so refreshing the chart page or the figures
died on a missing file. `results.records` now takes either — the old
format is one record per line and already in the shape both readers want
— and the paths resolve to whichever is committed.

**The figures were drawn from the wrong measurements.** They came from a
`latest.jsonl` at lpspec 0.0.0a27 that still carried `duckdb` and
`duckdb@1GB` arms — a file from before that engine was removed.
Regenerated against what the repo ships today, lpspec 0.0.1a55, so the
figures, the tables and the chart page all read the same run. The chart
page's own numbers were already in step and did not move.

**The reproduce block named a module that no longer exists.** `bench.run`
went with #448; the commands are the pytest ones now, which is what
bench/README.md has said since.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A full run at f319cd1: six models, four rungs, both arms, and for the
first time all three sinks — 192 measurements, 35 minutes, 192 passed.

**Under pytest-benchmem 0.5**, whose isolated pass no longer warms up, so
a `peak` here is the cold first call in a fresh process. That is the
thing this page describes and the thing a caller building one model
pays; a 0.4 peak is a different quantity wearing the same name, so the
floor moved to `>=0.5` rather than letting the two mix. The release's
headline fix — Linux `ru_maxrss` reading the parent's high-water mark —
does not touch these numbers; macOS was never affected.

**Peak reproduced; wall did not.** Against the previous run this one
agrees on every peak cell to within 0.02x — the same 1.46x, 1.10x,
0.33x — and disagrees on wall by up to 40% relative. Peak is a property
of what the code allocates; wall on a shared laptop is a property of
what else was running. The page now says so, and says to read the wall
column for its sign and order of magnitude rather than its second digit.

**Gurobi is the slowest destination for both arms**, and the one where
the lanes converge: gurobipy's per-row and per-column ingestion (#434)
dominates the build in front of it.

The fourth figure had to change with the harness. #448 made `test_emit`
one number for build-and-hand-over and left the only other measurement a
warm in-process rebuild, so there is no honest way to stack a phase bar
any more; it is now the `l` rung through every sink, which is the
question the third sink makes worth asking.

`bench/conftest.py` tracks gurobipy and scipy from here on — a ratio
through a solver has to say which solver. This file predates that by
minutes, so the two versions are named in the page instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A second full run — nine rounds per measurement instead of five, on a
machine at load 1.5 rather than 4.5, clean tree at f10c3ee — and a
cell-by-cell comparison against the first.

    peak   median difference 0.7%   worst cell  7.7%
    wall   median difference 5.3%   worst cell   65%

So the two columns are not the same kind of number. Peak is a property
of what the code allocates and it reproduces: 1.46x is 1.46x both times.
Wall on a shared laptop is partly a property of what else was running —
`dispatch` through `lp` came out 0.46x and then 0.75x. The page now
publishes that comparison, says to read a wall cell for its sign and
order of magnitude rather than its second digit, and makes only the
claims that survived both runs.

Which changes one of them: through `gurobi` we lead on all six models,
because gurobipy's ingestion is a constant added to both arms and what
is left showing is the build in front of it. `profiled` is still the
loss through `highs` and `lp`.

This run needed no prose to patch its provenance: clean tree, and
gurobipy and scipy recorded in the file by the tracking added last
commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FBumann
FBumann force-pushed the docs/benchmark-charts branch from 82caa01 to 35aefe2 Compare August 1, 2026 07:30
@FBumann

FBumann commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Re-ran the ladder a second time to check reproducibility — nine rounds per measurement instead of five, on a machine at load 1.5 rather than 4.5, clean tree. Compared cell by cell against the first run:

median difference worst cell
peak 0.7% 7.7%
wall 5.3% 65%

Peak reproduces (1.46x is 1.46x both times); wall does not, per cell — dispatch through lp came out 0.46x then 0.75x. The page now publishes that comparison and makes only the claims that survived both runs, which changed one of them: through gurobi we lead on all six models, because gurobipy's ingestion is a constant added to both arms and what is left showing is the build in front of it.

Rebased onto #455, which reached the pytest-benchmem 0.5 conclusion independently and added the instrument to the fingerprint — the right call, and stronger than the note I had written. TRACKED is now the union: their pytest-benchmem plus gurobipy/scipy from here.

@FBumann
FBumann merged commit 042c2ad into main Aug 1, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation pages, guides, reference and README

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant