Skip to content

Chart performance per release, and show it in the README - #89

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/magical-knuth-idzj5r
Sep 16, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/magical-knuth-idzj5r

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Adds a BenchmarkDotNet suite, a workflow that measures a fixed subset of it once per release, and a chart in the README built from the accumulated numbers. Seeded with six releases so it says something the day it merges rather than after the next one.

the chart as it stands

What is measured, and how it is made comparable

Allocation and time are read differently, and the chart says so.

Allocation is exact. The same code allocates the same bytes on any machine, so a step in the top row is always a real change.

Time is not, because these run on shared CI hosts and the difference between an x86-64-v3 and a v4 runner is larger than most releases are. BaselineBenchmarks.ReferenceWork is a fixed integer loop measured beside the benchmarks in the same job; the chart divides every time by it. That cancels most of the host difference and leaves something indicative. Its body must never change — editing it rescales the whole history.

A backfill measures every version in one job for the same reason: within a job the points are comparable as they stand.

Measuring a release

Backfills measure the published packages, through BenchmarkAgainstVersion. No tag before this commit carries a benchmark project, so there is no older source to check out — and measuring packages is the better comparison anyway, since every version is then timed by identical benchmark code rather than by whatever each tag shipped.

That variable has to reach the build through the environment, not -p:. BenchmarkDotNet generates and builds a project of its own per run, which a property on the outer command line never reaches; it would build the benchmark assembly against the version asked for and the harness against the one pinned centrally, which does not compile when the type changed shape between them. MSBuild reads environment variables as properties in every project, so the environment form reaches both.

Releases from here on are measured from their own tag, through a worktree. That keeps the release path off the race against the NuGet push happening beside it, which the package route would otherwise lose about as often as it won.

What the older packages cannot be asked

The history starts at 1.3.0, and one panel of it starts at 1.4.20.

Every operand is built by parsing text, which is the only construction route all the published versions share. 1.2.2 and 1.2.7 throw NotSupportedException from Parse, so they compile against these benchmarks, run, and report a table of NA. That is not a build failure, so it got as far as being recorded as a release with nothing under it — a point on the axis reading zero, which is the opposite of the truth. ingest now refuses a run with no measurement in it, the backfill treats that as a skip, and the two versions are off the default list.

Significance reduction is spelled three ways across the versions: 1.2.x has no precision overload of ToSignificantNumber at all, and 1.3 and 1.4.0 hang it off the PreciseNumber base, where the receiver's type argument has to be written out. Carrying three spellings would measure the spellings, so the file is left out of builds against anything older than 1.4.20 and the chart draws that one panel with a gap. Everything else in those versions is still measured — the alternative was losing six panels to a compile error.

The result

The 2.0 step is the type becoming a readonly record struct over PreciseNumber instead of deriving from it:

30-digit Add 1.4.40 2.0.1
allocated 3,160 B 40 B
time 24.9× reference 0.274× reference

CompareTo reaches 0 B.

Verification

  • dotnet build -c Release — clean, 0 warnings. The benchmark project was missing from the solution in the first commit, so the root build was not compiling it at all; that is fixed here.
  • dotnet test -c Release — 95 passed, 0 failed.
  • Both spellings of the seeding run were exercised end to end locally, including the empty-measurement refusal against the 1.2.2 reports on disk.

🤖 Generated with Claude Code

https://claude.ai/code/session_017jrnV7N94UGL8fDRRE8Xt8


Generated by Claude Code

Adds a BenchmarkDotNet suite covering arithmetic, comparison, significance
reduction, parsing and conversion, and a workflow that measures a fixed subset
of it once per release, appends the numbers to a committed history file, and
redraws the chart the README shows.

Every point is measured in one job, because separate CI runs land on different
hosts and that difference is larger than most releases are. Across jobs,
BaselineBenchmarks ties them together: a fixed reference loop measured beside
the benchmarks, which the chart divides the times by. Allocation needs none of
that -- the same code allocates the same bytes anywhere -- so the chart reads
allocation as exact and time as indicative.

Backfills measure published packages through BenchmarkAgainstVersion, since no
tag before this commit carries a benchmark project. It has to reach the build
through the environment rather than a -p: switch: BenchmarkDotNet generates a
project of its own per run, which a property on the outer command line never
reaches. Releases from here on are measured from their own tag instead, which
also keeps that path off the race against the NuGet push beside it.

Two versions cannot be measured by this suite at all. 1.2.2 and 1.2.7 throw
NotSupportedException from Parse, which is how every operand here is built, so
they compile, run, and report a table of NA. Ingest refuses a run with no
measurement in it rather than recording a release with nothing under it, and
the backfill treats that as a skip. Significance reduction is spelled three
ways across the published versions, so its file is left out of builds against
anything older than 1.4.20 and the chart draws that panel with a gap, rather
than losing six panels to a compile error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017jrnV7N94UGL8fDRRE8Xt8
Six releases, 1.3.0 through 2.0.1, all measured in one pass against a single
reference reading so that the times are comparable as they stand. Without this
the README would show an empty chart until the next release, and a one-point
chart after it.

Also adds the benchmark project to the solution, which the first commit left
out: `dotnet build` at the root built the library and the tests and never
noticed the benchmarks were not compiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017jrnV7N94UGL8fDRRE8Xt8
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit fed7b49 into main Sep 16, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/magical-knuth-idzj5r branch September 16, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants