Skip to content

benchmark groups start collapsed, load charts when a group opens - #130

Draft
lwwmanning wants to merge 59 commits into
mainfrom
claude/bench-landing
Draft

lwwmanning wants to merge 59 commits into
mainfrom
claude/bench-landing

Conversation

@lwwmanning

@lwwmanning lwwmanning commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Benchmark groups on the landing page start collapsed. Each group shows a one-line result from its summary, for example "DuckDB 2.5× faster". If Vortex is slower, the text says "N× slower". A group loads its charts only when it opens. The URL fragment lists the open groups, and a link with that fragment opens them. Reload and Back open the same groups and go back to the same scroll position.

These values are from a copy of the production database, with the CPU 4× slower:

  • Landing page HTML: 83.6 → 27.1 KB (gzip)
  • Charts that start at page load: 791 → 0
  • Total Blocking Time: 22 → 0 ms
  • Time until the groups show: 413 → 182 ms
  • First Contentful Paint: 124 → 184 ms, because the old page first showed a loading message
  • Expand All: 574 ms of blocking time (500 ms before)

The APIs do not change. The landing page screenshots change, as expected. With JavaScript off, the page shows only a loading message, as before.

🤖 Generated with Claude Code

lwwmanning and others added 30 commits October 28, 2026 21:32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
bench/scripts/parity/run.sh serves a base git ref and the working tree
side by side with `next start`, against one Postgres restored from a
production dump, and compares them. compare-api.ts requests every group
and chart route from both servers and compares status, content-type,
cache-control and body, printing the route and first differing JSON path
with both values. screenshots.spec.ts compares the landing page and five
chart pages pixel by pixel at desktop and phone widths, and Playwright
keeps the diff images.

The database runs from a local Postgres 16 data directory, since the
integration suite's container is not required. Group-cache fills run one
at a time because they share a database-wide advisory lock, and each
server starts with an empty Data Cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Every URL the API check requests comes from the base's own /api/groups,
so a 404, a redirect or an empty body on either side now fails with
"nothing was compared for this route", even when both servers agree on
it. Slugs are percent-encoded, and the summary line tallies the statuses
and body bytes that were compared.

run.sh deletes the whole .next/cache of each side before it starts a
server, so a Next.js version that keeps Data Cache payloads somewhere
other than fetch-cache cannot serve payloads made by older code.

The README and config comment now say what the screenshot comparison
does: Playwright's comparator ignores pixels it classes as anti-aliasing,
and any other differing pixel, or a layout shift of 1px or more, fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
GitHub's dependency graph does not read bun.lock, so dependency-review
sees none of the site's transitive packages. scripts/dependency-snapshot.ts
turns the lock into a snapshot for the Dependency Submission API, with
one manifest per workspace. A package is runtime when a workspace's
dependencies or optionalDependencies reach it, and development
otherwise. Optional peers add no edge: Next.js lists Playwright as one,
and following it would mark Playwright runtime. Every package in the
current lock is in the snapshot, and every open high advisory in
`bun audit` is development scope.

The dependency-snapshot workflow runs it on pushes to main and on pull
requests from this repository, with contents: write on that job only
and no bun install. dependency-review now retries for up to 300 seconds
while the snapshot arrives. fail-on-scopes stays at the action's default,
runtime. CLAUDE.md layer 5 says so.

The tests use node:test, so the root tsc needs no bun types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
The submission script now waits 5 seconds and tries once more after a
network error or a 5xx answer. Any other status, and a second failure,
still exits 1. The wait can be set with SNAPSHOT_RETRY_DELAY_MS, which
the tests use. The token is never logged.

The workflow groups a push to main by commit, so a burst of pushes
cannot cancel or replace a pending run and leave a commit without a
snapshot. Pull requests keep one group per ref and cancel the older run.

New tests run the script against a local mock of the API: 503 then 201,
503 twice, 422 with no retry, and a refused connection. Another test
covers a workspace whose path (bench/web) differs from its package name
(benchmarks-web), with a workspace-keyed copy of a package, a hoisted
copy, and a decoy keyed by path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
bench/web joins the root Bun workspace. One bun.lock replaces
bench/web/pnpm-lock.yaml, and bench/web/pnpm-workspace.yaml goes:
- The zod override (^4.6.7) moves to the root package.json. It also
  moves Lighthouse CI's chromium-bidi to zod 4, as decided, and clears
  the site's accepted zod advisory. The sharp override was already
  there. The postcss override is not carried over: Next.js 16.4.1
  depends on postcss 8.5.23, which is patched, so until bench moves to
  Next.js 16 its next 15.5.28 brings postcss 8.4.31. CLAUDE.md lists
  those advisories under "bench advisories".
- undici 7.30.1 is still inside the 14-day window, so bunfig.toml
  exempts undici until 2026-10-29 14:18 UTC, with the reason from the
  pnpm file. Every other version pnpm had locked is past the window.
- The lockfile keeps pnpm's versions for every package only bench
  uses. The rest are the site's copies, which Bun dedupes onto. The
  site's resolution changes only in zod 3.25.76 -> 4.6.7.

bunfig.toml sets linker = "isolated", Bun's default for a workspace.
A hoisted tree hands bench's typescript-eslint the site's TypeScript 7,
which it rejects. publicHoistPattern puts ESLint and Prettier packages
in the root node_modules, where bench's ESLint config finds its
plugins, as pnpm's default did.

next.config.js traces from the repository root, where the packages
now live. Every route traces the same packages at the same versions as
the pnpm build on main.

The bench-checks action, bench-ci.yml and bench-deploy.yml install with
`bun install --frozen-lockfile` at the root and run bench's scripts with
`bun run --cwd bench/web`. bench/web/vercel.json sets the same install
command for `vercel build`, so the deploy does not depend on the
project's settings. bench-deploy.yml also runs on changes to the root
package.json, bun.lock and bunfig.toml. changed-paths counts the
workspace's root files for both apps. CLAUDE.md and bench's docs use
the Bun commands.

trustedDependencies stays ["esbuild", "sharp"]. pnpm also let
unrs-resolver build; its postinstall only checks for the native binding,
which Bun installs as an optional dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
bench moves to next 16.4.1 and react/react-dom ^19.3.1, the site's
versions, so both apps now share one copy. Next.js 16.4.1 depends on
postcss 8.5.23, which clears the four postcss advisories that next
15.5.28's postcss 8.4.31 carried; no postcss override is needed.

- POST /api/revalidate calls revalidateTag(BENCH_DATA_TAG, { expire: 0 }).
  That is the documented equivalent of the old one-argument call; 'max'
  would serve stale data once after an ingest. A new test pins it.
- Other bumps, each the newest version at least 14 days old: pg and
  @types/pg 8.23.1, jsdom 30.1.2, vitest 5.0.4 (clears the two
  @vitest/mocker advisories), eslint-config-next 16.4.1, @types/node
  ^24.19.2, @types/react(-dom) ^19.3.0, @vercel/functions ^3.10.1
  (already locked). chart.js stays at 4.5.1.
- jsdom 30 uses undici 8. The newest 8.x past the window, 8.12.1, has
  four high advisories fixed in 8.12.2, so the existing undici exemption
  now covers 8.12.2 as well; the lock pins 8.12.2, and the exemption
  list's removal time moves to 2026-10-29 15:06 UTC.
- The lock keeps @types/pg on the shared @types/node 24.19.2, as the
  workspace move did.
- eslint-config-next 16 ships flat config, so eslint.config.mjs imports
  it directly instead of through FlatCompat. Two new React Compiler rules
  from eslint-plugin-react-hooks 7 are off, so the upgrade needs no code
  change.
- next build (Turbopack, now the default) set tsconfig's jsx to
  react-jsx and added .next/dev/types; committed so builds stay clean.
- Docs: bench/web/README.md, the architecture pages that describe
  revalidateTag, the postcss.config.mjs and vitest.config.ts comments,
  bunfig.toml, and CLAUDE.md (toolchain, audit counts 17 -> 11, the
  bench advisories list).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
These two files were outside Prettier's glob and ran past 100 columns.
Biome's check now covers them, so this is the one-time mechanical
reformat, kept apart from the Biome switch. No logic changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
The root biome.json now covers bench. A bench/** override keeps its
Prettier style (single quotes, trailing commas, 100 columns) and
turns useExhaustiveDependencies on, in place of ESLint's react-hooks
rule. It turns off five rules that fire on deliberate markup and CSS,
and import sorting. bench/AGENTS.md gives the reasons, because
biome.json cannot hold comments.

Lint fixes keep behavior: a block around a switch case, an explicit
type on one let, braces on void forEach callbacks, an optional chain,
a template string, an import type. Two ESLint disable comments for
exhaustive-deps become biome-ignore, and a third effect gets one.
Stale ESLint disable comments are gone.

Remove eslint, eslint-config-next, @eslint/eslintrc and prettier with
their config files. Their plugins were the only reason for
publicHoistPattern in bunfig.toml, so it goes too; a clean install,
the suite and both builds pass without it. Fix the stale Next.js,
TypeScript and ESLint statements in bunfig.toml and CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
bench's format and lint steps covered only bench/web, and ci.yml skips
the root check:ci on bench-only changes, so bench/scripts went
unchecked until the next site PR. One step, bun run biome ci bench,
replaces both: it uses the workspace's pinned Biome with the root
config and covers web, scripts and the parity harness. The job name
does not change.

Fix two notes in bench/AGENTS.md: organizeImports would rewrite 27
files (25 in bench/web, 2 in bench/scripts), and the site config also
turns off noExplicitAny, so bench's old no-explicit-any check is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Move bench/web to typescript ^7.0.2, as at the root, so both apps run the
Go-based compiler. The tsconfig options the two apps shared now live in a
root tsconfig.base.json (strict, noEmit, esModuleInterop, module,
moduleResolution, resolveJsonModule, isolatedModules, skipLibCheck,
allowJs, incremental, noUnusedLocals, noUnusedParameters, lib, jsx and the
next plugin). Each tsconfig.json extends it and keeps its own target,
paths and extra flags: the site its noUncheckedIndexedAccess and
exactOptionalPropertyTypes, bench its noFallthroughCasesInSwitch and
noUncheckedSideEffectImports. `tsc --showConfig` gives the same effective
options for both apps as before. TypeScript 7 reports no errors in bench
under the existing flags, so no source changes.

bench-deploy.yml now also runs when tsconfig.base.json changes, since
bench's build extends it. bunfig.toml and CLAUDE.md no longer say bench
keeps its own TypeScript 5.9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
The bench override in biome.json turned four a11y rules off for all of
bench. New UI would then skip them unseen. Turn them back on and put a
biome-ignore comment with its reason at each of the seven existing
places. The markup does not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
…heck

The parity check compared only the read API and light-theme screenshots.
It now also requests the landing page and the five chart pages the
screenshots use, and compares their status, content-type, cache-control
and title. It probes an unknown chart slug (404) and a malformed slug
(400 from the API, 404 from the page), and accepts only those expected
statuses. Every other URL must still answer 200. The screenshots run in
the dark theme too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
bunfig.toml gave a reason for the isolated linker that no longer holds,
now that both apps share TypeScript and ESLint is gone. State the reason
that does hold. vitest.config.ts said Next.js owns tsconfig's jsx
setting; it now comes from tsconfig.base.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
bench-checks lost its dependency cache when it moved from pnpm to Bun.
Restore it with the same cache step as ci.yml. Run the parity check's
own tests there too, by explicit path, so its planning and comparison
logic is checked on every bench change. Correct a comment that said
Biome checks bench's imports; import sorting is off for bench.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Dependency review passed without bun.lock's packages when the snapshot
job failed, was skipped for a fork, or finished after the action's retry
window. A guard now runs before the action. For a pull request from this
repository, it waits up to 10 minutes for the snapshot job on the head
commit and fails unless that job succeeded. For a fork, which gets no
snapshot, it fails when the pull request changes a bun.lock. The job
gains checks: read, and its checkout no longer keeps the token.

The snapshot job now checks out the pull request's head commit, so the
graph it files under that commit holds the head's own bun.lock.

CLAUDE.md describes the guard, says that development-scope advisories
(bench's included) no longer block merge and show in bun audit, and
gives the real fallback for the zod override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Give the publish times behind the statement that next 16.4.1 and
react 19.3.1 were past the cooldown when bench moved to them, and say
how the "13 days old" for the site differs. Rewrite the vite note,
which still described pnpm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
The database suites needed Docker, so they skipped on a Mac without it.
Set BENCH_TEST_DATABASE_URL to a Postgres server and each suite creates
a throwaway bench_test_* database there, applies the migrations, and
drops the database when it ends. A Postgres advisory lock serializes the
first applies, because the migrations create cluster-wide roles.

Without the URL, the suites use testcontainers as before. When CI=true
and neither Docker nor the URL is available, the suites now throw
instead of skipping, so a broken runner cannot pass with them off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Production signs in with a static BENCH_DB_PASSWORD for the read-only
bench_read role, which has no rds_iam grant (migration 005). The IAM
path in lib/db.ts never ran there.

getPool() now requires BENCH_DB_PASSWORD through requireEnv, so a missing
or empty value fails at first use with a clear message. passwordProvider,
the region setting and the rds-signer import are gone, and the pool gets
the password as a string. DbConfig stops being exported.

Remove @aws-sdk/rds-signer. bun.lock loses only the signer and its
transitive packages; nothing else is re-resolved. The README and the
deploy runbook now list BENCH_DB_PASSWORD as required and drop
BENCH_DB_REGION.

BENCH_DB_REGION on the benchmarks-website Vercel project is now unused.
Deleting it is a settings change for Will, so this commit leaves it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
collectGroupCharts called chartPayload once per chart, two queries each:
about 200 round trips for a 99-chart TPC-DS group. groupChartPayloads
builds the same payloads from one statement for the last n commits and
one for every chart's rows, a LATERAL join per chart. A chart's x-axis is
the last min(n, total) commits of that shared window, so the JSON is
byte-identical. chartPayload is unchanged and is the tests' oracle.

On one connection the brief's SQL lost to the old 7-wide fan-out, so:
- The commit count is an aggregate lateral plus a width_bucket binary
  search. A scalar subquery was flattened and ran once per output row.
- A query chart's first row is a skip scan of
  idx_query_measurements_summary. MIN read ~27K heap rows per chart.
- The compression families split charts by NULL variant, so each pin is
  sargable. IS NOT DISTINCT FROM planned a semi join per fact row.
- Rows come in slices of 25 charts, one after another. One result for
  TPC-DS at ?n=all peaked at 1.6x the old memory.

On the 2026-10-28 dump every group and window matches the old path. At
n=100, warm: TPC-DS (NVMe) SF=1 1587 -> 321 ms, Clickbench 865 -> 143
ms, Compression 70 -> 120 ms. At ?n=all, TPC-DS takes 5.3 s against
4.9 s, with peak memory at most 1.34x the old.

QUERY_CONCURRENCY now bounds only the summaries in collectGroups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
groupChartPayloads ran the commit window and the first rows slice on two
pooled connections, and later slices after them, each in its own READ
COMMITTED snapshot. An ingest landing between them could give payloads
the per-chart path never could: a commit counted in total_commits but
missing from commits[], or a short chart losing its own first commit.

Now the window and every rows slice run one after another on one client,
in a REPEATABLE READ, READ ONLY transaction, rolled back on error, with
the client always released. The clamp that patched one direction of the
skew is gone. A group still runs two statements for up to 25 charts; a
TPC-DS group (99 charts) runs five. The group fill now holds one more
pool connection, after the summaries finish.

New window-edge tests pin byte-equality with chartPayload for a
random-access group, a vector-search group with a 0.1 threshold, and a
compression-size group with a non-NULL variant block, with per-chart
windows. The statement-count tests now read the pooled client.

performance.md states the ?n=all memory as measured: under 1.5x, 1.2 to
1.8 GB against 1.3 GB. On the 2026-10-28 dump every group still matches
the old path at n=100 and n=50.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Move lib/chart-store.ts into lib/chart-store/: queues, bundles,
global-filter and group-store, with index.ts re-exporting each module
so '@/lib/chart-store' keeps its export surface. The code moves
verbatim. group-store reads the global filter through
getGlobalFilterSnapshot() instead of the module variable.

Add a pin test for the chart island before it is split: under
StrictMode, one wheel event pans once, and unmount removes the listener.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Move the ChartController class and its helpers out of
components/Chart.tsx into components/chart/: types, datasets, tooltip,
config, card-dom, pan-zoom and controller. Chart.tsx keeps the 'use
client' boundary, the architecture notes, ChartIslandProps and the
Chart component.

The code moves verbatim. The moved private methods (the slider, badge
and window-chip renderers, wheel pan and the range strip) take the
state, elements and abort signal as parameters, and the controller keeps
a delegator with the same name for each. maybeConstruct gets the
Chart.js config from lineChartConfig().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
PALETTE, colorFor, assignStableColors and labelForCommit in
lib/chart-format.ts, and commitWindowUrlValue in lib/window.ts, have no
importer outside their own tests. Delete them and their six tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Move noUncheckedIndexedAccess and exactOptionalPropertyTypes from the
site's tsconfig.json into tsconfig.base.json, so bench inherits them.
The site's effective config does not change.

TypeScript 7 reported 162 errors in bench: 41 in app code, 121 in
tests. The fixes change types only:

- Optional properties that receive undefined get `| undefined`.
- An index access gets `!` only where the index is in range by
  construction. commitDateLabel gets an explicit undefined check that
  returns '' as before.
- Chart.js's types do not let a line chart or its datasets widen to the
  generic types under exactOptionalPropertyTypes, so three casts at
  that boundary bridge them.
- Tests use `!`, or `?.` where Biome forbids `!` after `?.`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
lib/chart-store.ts is now lib/chart-store/, and the Chart.js wiring left
components/Chart.tsx for components/chart/. Fix the links and file
references in AGENTS.md, performance.md (two links were broken) and
design-decisions.md, and the chart-format.ts header. Two moved comments
named the wrong place: config.ts now names pan-zoom.ts for the wheel
listener, and card-dom.ts names renderDownsampleBadge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
payloads[i]! in collectGroupCharts and values[vi]! in
normalizeChartPayload removed null from the type as well as undefined.
A chart with no rows is null, so the null check that follows looked dead
to the compiler. Cast to the element type instead; this removes only
undefined.

Also state the parallel-length precondition of lttbIndices, pin the
undefined-month and undefined-day paths of commitDateLabel with a test,
and correct CLAUDE.md: the two strict flags now live in
tsconfig.base.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
lwwmanning and others added 13 commits October 29, 2026 05:20
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
The islands now mount into an open group and unmount on close, so
teardown runs through effect cleanup and destroy(), not the toggle event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Safari throws on a document's 101st history.replaceState call in 10
seconds. Each group toggle made two calls: ours, then Next.js's own
re-sync, which is not caught. About 50 fast toggles replaced the
landing page with Next.js's error page.

A toggle after 300 ms of quiet still writes at once. Inside that time
the write waits until the toggles stop, so a burst is two writes and
writes stay at least 300 ms apart. A hashchange drops a pending write
before it reads the new fragment, and unmount drops it too. A chart
link click makes a pending write before it saves the scroll. pagehide
saves the scroll under the current URL, which a reload loads, then
makes the write and saves again under the new fragment for Back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
main squash-merged the bench toolchain PR (#128). Its tree equals that
PR's last commit, f92f33a, which this branch already contains, so the
merge keeps this branch's tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
A query or random-access headline now says "(partial)" when it leaves
out an engine, or a hot or cold side, that ran Parquet but not Vortex.
Before, that result was dropped without a word. An engine that ran
only Vortex is still left out silently.

A size reads as a multiple outside half to twice Parquet's: "2.0×
larger" once the printed percentage would reach 100 (from 1.995), and
"2.0× smaller" from a ratio of 0.5 down. No size reads "100% larger"
or "100% smaller" any more.

Tests now also cover random access with no Parquet run, an unknown
engine's label and order, a vector-search tie and NaN score, and the
9.95× and 20× display boundaries. None of the 23 groups on the local
production dump changes its wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
The design and plan documents for the toolchain move and the cleanup
were working notes for those changes. The code, the architecture docs
and CLAUDE.md now record what they decided.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Before hydration, a reader can already open or close groups, and scroll.
LandingState overrode both:

- On mount it reopened every group the fragment lists, so a group the
  reader closed before hydration opened again. A group opened before
  hydration reached the fragment only with the next toggle.
- A Reload or Back restore moved a reader who had already scrolled.

The pre-paint script now leaves a marker on its document, after it opens
the groups. One-shot passive capture listeners (wheel, touchstart,
keydown, pointerdown) set its input flag. A scroll does not set it, so
the browser's jump to a single-anchor fragment is not input.
LandingState takes the marker on its first mount, once per document. It
then opens nothing, writes the fragment once from what is open, and
skips a restore when the flag is set, as input after mount would. A
client navigation to / finds no marker and opens the groups from the
fragment, as before.

Also:

- Save the scroll position when the tab goes hidden too, through the
  pagehide path. A mobile browser can discard a hidden tab without a
  pagehide.
- A scroll that no click asked for (a fresh visit, a restore's fallback,
  a new fragment) now jumps. Only the group nav scrolls smoothly.
- Space fragment writes on performance.now(), not Date.now(). A wall
  clock that jumps forward let two writes come closer than 300 ms.
- Drop a pending fragment write when the path changed since it was
  scheduled, as when the fragment changed.
- Remove the wall-clock limit from the bootstrap's linear-work test. The
  DOM-call counts are the bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
A group with no charts mounts no chart island, so it has no bundle to
wait for. Its body now requests nothing on open, and the scroll
restorer's wait for it resolves at once.

Rename a GroupBody test that claimed to pin the once-per-page-load seed
of the URL's series overrides. The body stays mounted across a close, so
the test passes without that guard; lib/group-bodies.test.ts pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
With JavaScript off, / shows only the loading card: the groups stream
inside the Suspense boundary's hidden segment, which only React's inline
script reveals. This predates the folded page. Comments in GroupSection,
SummaryCard and GroupPermalink claimed behavior "with or without
JavaScript". They now say what holds: before hydration, with JavaScript
on. page.tsx says why its <noscript> hint never shows in a browser. The
hint stays where it is: outside the boundary it would need a wrapper and
would sit outside the page layout, above the header or below the footer.

performance.md's folded-page section now:

- names OpenGroupsScript and the pre-hydration marker;
- says why first contentful paint rose 124 -> 184 ms: the previous build
  first paints its loading card, which the new page no longer needs;
- says what the reader sees on Reload and Back (1-2 frames of the page
  top) and where the 1.4-1.8 layout-instability score comes from;
- gives the conditions: local dump, no network throttling, CPU 4x,
  median of 5 runs (Expand All: 3), one-off scripts, and the previous
  build (the claude/bench-cleanup head, b4caf7c);
- says Expand All mounts its islands in the toggle tasks after the
  click.

Chart.tsx's header adds GroupBody's mount at hydration for a group that
is already open. Header.tsx no longer says Expand All writes the
fragment once; writes are scheduled and coalesced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Input before hydration means the reader has taken over the scroll. The
pre-paint script's input flag stopped only the Reload and Back restore.
On a fresh visit to a link that lists groups, the page still jumped to
the first listed group after hydration, away from where the reader had
scrolled.

planScroll now returns "none" whenever the flag is set: no restore, no
fallback, and no fragment scroll. A scroll alone is still not input, so
the browser's jump to a single-anchor fragment changes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
Brings in main's merge of the toolchain PR and the removal of the
working plans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
The design and plan documents for the folded landing page were working
notes. The code, bench/docs/architecture/performance.md and the PR record
what they decided.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
@vercel

vercel Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
vortex Ignored Ignored Preview Oct 29, 2026 7:32am UTC

Request Review

Base automatically changed from claude/bench-cleanup to main October 29, 2026 07:20
@lwwmanning lwwmanning changed the title Fold bench's landing page and load charts when a group opens benchmark groups start collapsed, load charts when a group opens Oct 29, 2026
lwwmanning and others added 2 commits October 29, 2026 08:22
The first run of LandingState's effect may rewrite the fragment, which
keys the saved position and orders the listed groups. Keep that run's
reads in a ref so the replay in development restores the same position.
Name a click as input before hydration in the comments that list it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
main squash-merged the bench cleanup PR (#129). Its tree equals that
PR's last commit, 4365dee, which this branch already contains, so the
merge keeps this branch's tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
@lwwmanning
lwwmanning marked this pull request as ready for review October 29, 2026 07:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T07:26:27.339130Z e149958 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment thread bench/web/components/GroupSection.test.tsx Fixed
Comment thread bench/web/components/GroupSection.test.tsx Fixed
CodeQL flags the tag-stripping regex as incomplete HTML sanitization.
The tests now take the summary row's textContent instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Bench preview for 7ee508ea31bface6b59e0bb52000badc021385d8: https://benchmarks-website-oaerm1tzt-vortex-data.vercel.app

A collapsed group showed only its header row. The summary card now sits
after the group's <details>, inside its section, so it shows whether the
group is open or not. The toolbar and charts still mount only when the
group opens. The card was already in the HTML, so the page ships the same
bytes. Comments and docs now say "collapsed", not "folded".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Will Manning <will@willmanning.io>

This branch was successfully deployed

2 active (1 outdated) deployments
bench-preview — 7ee508ea Deployed Oct 29, 2026 by lwwmanning via Build & deploy to Vercel #68
Preview — 8a5d2063 Deployed Oct 29, 2026 by vercel[bot]
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