Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dae17e1
Plan bench PR 3: a folded landing page
lwwmanning Oct 29, 2026
27702ac
Show a one-line headline on each folded bench group
lwwmanning Oct 29, 2026
5efb0ff
Mount a bench group's toolbar and charts only while it is open
lwwmanning Oct 29, 2026
260bafe
Keep bench's open groups in the URL fragment
lwwmanning Oct 29, 2026
af2e77a
Forget a bench group body's note when it closes
lwwmanning Oct 29, 2026
1ff5eef
Keep bench's open-groups bootstrap linear in the fragment
lwwmanning Oct 29, 2026
7855fa6
Restore bench's landing scroll by element on reload and Back
lwwmanning Oct 29, 2026
3a5e7d3
Restore bench's scroll only for a real reload or Back of the page
lwwmanning Oct 29, 2026
eea345b
Wait for a group's charts to mount in the parity check
lwwmanning Oct 29, 2026
de84be3
Describe how a bench chart island tears down in its group body
lwwmanning Oct 29, 2026
c1fcbd4
Document bench's folded landing page
lwwmanning Oct 29, 2026
da8f6db
Coalesce bench's open-group fragment writes
lwwmanning Oct 29, 2026
128d70e
Tighten bench's landing headline at its edges
lwwmanning Oct 29, 2026
1763ee8
Keep what a bench reader does before the landing page hydrates
lwwmanning Oct 29, 2026
11365d7
Request no bundle for a bench group with no charts
lwwmanning Oct 29, 2026
1979428
Correct what bench's landing docs and comments claim
lwwmanning Oct 29, 2026
9d27557
Keep bench's fresh-visit scroll still after input before hydration
lwwmanning Oct 29, 2026
8836d6b
Remove the folded landing page's working plans
lwwmanning Oct 29, 2026
4b42604
Keep bench's landing reads stable across a StrictMode replay
lwwmanning Oct 29, 2026
f762e63
Read the group headline's text through the DOM in its tests
lwwmanning Oct 29, 2026
511c316
Show each collapsed bench group's summary table
lwwmanning Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion bench/docs/architecture/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ gated by an `IntersectionObserver` with `LAZY_HYDRATION_ROOT_MARGIN = '300px
0px'` (a card begins hydrating just before it scrolls into view) and scheduled by
`priority = -index` so the top cards render first. Only the ~6 visible charts
hydrate on open; the rest hydrate on scroll. The initial latest-100 fetches are
capped per tab (`HYDRATION_CONCURRENCY = 4`).
capped per tab (`HYDRATION_CONCURRENCY = 4`). A collapsed group has no islands
at all: its cards mount only when it opens (see "Collapsed groups on the landing
page" below).

### Full history is opt-in

Expand Down Expand Up @@ -239,6 +241,46 @@ first. The cache is per-session by design: a close/reopen costs zero fetches, an
the small staleness window for an already-open tab after a server-side
revalidation is accepted (a refresh gets fresh data).

### Collapsed groups on the landing page

Every group starts collapsed: its header row, with the Vortex-vs-Parquet
headline, and its summary table, both server-rendered. Its toolbar and cards
([`GroupBody`](../../web/components/GroupBody.tsx)) mount only while it is open,
so a collapsed group hydrates no island. The open groups live in
the URL fragment (`/#compression,clickbench`), which the CDN never sees. An
inline script after the last group
([`OpenGroupsScript`](../../web/components/OpenGroupsScript.tsx)) opens them
while they are still in React's hidden streamed segment, so a reload or Back
never shows them collapsed first. It also leaves a marker for the client code: a
group the reader opens or closes before hydration stays that way, and a reader
who scrolls, touches, clicks or presses a key before hydration is not moved,
neither by the restore nor by the scroll to a link's first group. Scroll is
restored by element, not by pixel
([`lib/scroll-restore.ts`](../../web/lib/scroll-restore.ts)): the open bodies
mount only after hydration, so a saved `scrollY` would land against a shorter
page.

Measured against the previous build, the head of `claude/bench-cleanup`
(b4caf7c): a local restore of the production dump, desktop (1440×900), no
network throttling, CPU 4×, the median of 5 runs (Expand All: the median of 3),
with one-off scripts that are not in this repository.

- Landing HTML 84 → 27 KB gzipped, islands 791 → 0, DOM elements
21,237 → 1,585, script time 186 → 59 ms.
- The groups first paint at 413 → 182 ms. First contentful paint rises
124 → 184 ms, because the previous build first paints its "Loading benchmark
summaries…" card and shows it until its 2.3 MB page has parsed. The new page
is small enough that its first paint already shows the groups; the card shows
only while a slow network holds them back.
- Back and Reload with two groups open put the same card within 1 px of its
saved offset. The reader sees the top of the page for 1–2 frames, then that
spot. The Layout Instability API scores these loads at 1.4–1.8. It counts
content that moves above the restored card: the cards that mount during that
jump, and the empty cards that hide once the bundles arrive. The restore's
two scrolls cancel both, so the reader sees no shift after the jump.
- Expand All mounts all 791 islands in the toggle tasks that follow the click:
574 ms of blocking time against 500 ms.

### Visual downsampling (LTTB)

A chart card is only ~600–900 px wide and Chart.js draws ~2 px markers, so a
Expand Down
1 change: 1 addition & 0 deletions bench/scripts/parity/screenshots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ test('landing page with the first group open', async ({ page }) => {

const section = page.locator('section.group-details').first();
await section.locator('summary.group-summary').click();
await expect(section.locator('.chart-card').first()).toBeVisible();
// Charts draw when they scroll near the viewport, so visit every one before the screenshot.
const cards = section.locator('.chart-card');
for (let index = 0, count = await cards.count(); index < count; index++) {
Expand Down
59 changes: 27 additions & 32 deletions bench/web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ main {
}
.group-summary-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
padding: 1rem 2rem;
Expand Down Expand Up @@ -520,6 +521,19 @@ main {
line-height: 1;
text-transform: uppercase;
}
/*
* The one-line headline at the end of a group's summary row (GroupSection,
* lib/headline.ts): how Vortex compares with Parquet. Plain muted text; the
* words carry the verdict, so tone colors (data-tone on each part) are left to
* the visual redesign. It sits at the right on desktop and wraps to its own
* line on a phone.
*/
.group-headline {
margin-left: auto;
color: var(--muted);
font-size: 0.85rem;
font-variant-numeric: tabular-nums;
}
/*
* The copy-link button next to a group's chart count (GroupPermalink). A bare
* icon button with the info icon's muted-to-foreground hover treatment; the
Expand Down Expand Up @@ -664,7 +678,7 @@ main {
opacity: 1;
visibility: visible;
}
.group-details > .chart-grid {
.group-body > .chart-grid {
padding: 1.25rem;
/* Page-level surface behind the chart cards. v2's `.benchmark-graphs`
* uses `--bg-color` (`#101010` in dark mode); `--page-bg` is the v3
Expand All @@ -682,33 +696,13 @@ main {
margin: 0;
}

.group-disclosure:not([open]) ~ .chart-grid {
display: none;
}

/*
* During the post-load Chart.js prebuild, render closed groups' chart
* grids in a hidden offscreen position so each canvas has real layout
* dimensions for Chart.js to measure. Without this override, charts
* construct against a 0x0 parent (the closed-group rule above), and the
* eventual on-toggle resize leaves a brief flash of a blank, full-zoomed
* canvas before the next frame paints. The width matches the visible
* content area via `--bench-prebuild-width` (set by `chart-init.js` from
* the live `<main>` width) so the build dimensions are close enough to
* the actual on-screen dimensions that Chart.js's ResizeObserver-driven
* adjust on toggle is a no-op or a single sub-frame tick.
*/
body.bench-prebuilding .group-disclosure:not([open]) ~ .chart-grid {
display: grid !important;
position: fixed !important;
visibility: hidden !important;
pointer-events: none !important;
top: -10000px !important;
left: 0 !important;
width: var(--bench-prebuild-width, 100vw);
/* A collapsed group ends with its summary card: the section's own border
* closes it. */
.group-disclosure:not([open]) ~ .benchmark-scores-summary {
border-bottom: 0;
}

.group-details > .chart-grid .chart-card:last-child {
.group-body > .chart-grid .chart-card:last-child {
margin-bottom: 0;
}

Expand Down Expand Up @@ -1090,8 +1084,8 @@ body.bench-prebuilding .group-disclosure:not([open]) ~ .chart-grid {
* chart grid. Y-axis buttons on the left, a centered "Filter series"
* dropdown trigger, and a Reset button on the right. The dropdown panel
* holds engine/format macro chips and a series row populated by JS as
* charts in the group hydrate. Hidden alongside `.chart-grid` when the
* disclosure is closed via the same `:not([open]) ~` rule.
* charts in the group hydrate. It lives in `.group-body` inside the
* group's `<details>`, which mounts it only while the group is open.
* ---------------------------------------------------------------------- */
.group-toolbar {
display: flex;
Expand All @@ -1103,9 +1097,6 @@ body.bench-prebuilding .group-disclosure:not([open]) ~ .chart-grid {
background: var(--bg);
font-size: 0.78rem;
}
.group-disclosure:not([open]) ~ .group-toolbar {
display: none;
}
.group-toolbar-y {
flex: 0 0 auto;
gap: 0.25rem;
Expand Down Expand Up @@ -1720,7 +1711,11 @@ main > .chart-card:not(.chart-grid .chart-card) .chart-wrap {
flex: 0 0 auto;
font-size: 0.65rem;
}
.group-details > .chart-grid {
.group-headline {
flex-basis: 100%;
margin-left: 0;
}
.group-body > .chart-grid {
padding: 0.75rem;
}
.benchmark-scores-summary {
Expand Down
49 changes: 35 additions & 14 deletions bench/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { GroupNav } from '@/components/GroupNav';
import { GroupSection } from '@/components/GroupSection';
import { Header } from '@/components/Header';
import { HomeLoading } from '@/components/HomeLoading';
import { LandingState } from '@/components/LandingState';
import { OpenGroupsScript } from '@/components/OpenGroupsScript';
import { groupAnchors } from '@/lib/anchor';
import { parseFilterCsv, singleSearchParam } from '@/lib/chart-format';
import { cachedFilterUniverse, cachedGroups } from '@/lib/data-cache';
Expand All @@ -28,14 +30,32 @@ export const dynamic = 'force-dynamic';

/**
* The landing page: a server-rendered section per group in canonical
* `GROUP_ORDER`, each with its summary card, per-group toolbar, and a grid of
* chart-card client islands. `data-chart-index` is assigned page-wide so each
* chart's index is unique across every group (matching v3's `landing_body`
* counter).
* `GROUP_ORDER`, each collapsed to its header row (with the headline) and its
* summary card, and opening to its per-group toolbar and a grid of chart-card
* client islands (mounted only while the group is open). `data-chart-index` is assigned page-wide so
* each chart's index is unique across every group (matching v3's
* `landing_body` counter).
*
* `?engine=` / `?format=` are the global filter's URL allowlists (CSV). A
* A readable `?group=` anchor plus repeated `?hide=` / `?show=` values restores
* readable `?group=` anchor plus repeated `?hide=` / `?show=` values restores
* the target group's local series overrides.
*
* The URL fragment lists the open groups' anchors (`#tpc-h-nvme-sf-1,clickbench`,
* see `lib/open-groups.ts`). The server never sees it, so `/` stays one CDN
* cache entry. [`OpenGroupsScript`], right after the last group, opens the
* listed groups before they paint; [`LandingState`] opens them after a client
* navigation (after a full load it keeps what the reader opened or closed
* before hydration), writes the fragment after opens and closes (coalescing a
* burst of toggles into two writes), and owns the scroll: a reload or Back
* restores the element that was at the top of the viewport
* (`lib/scroll-restore.ts`), and a fresh visit scrolls to the first listed
* group. Neither moves a reader who scrolled, touched, clicked or pressed a
* key before hydration.
*
* The content (`HomeContent`) renders inside a Suspense boundary. A browser
* gets it streamed into a hidden segment that React reveals with an inline
* script, so with JavaScript off it never shows: a reader without JavaScript
* sees only the [`HomeLoading`] fallback. This predates the collapsed groups.
*/
export default async function Home({
searchParams,
Expand Down Expand Up @@ -75,13 +95,7 @@ async function HomeContent({
return (
<>
<Header universe={universe} initialEngines={initialEngines} initialFormats={initialFormats} />
<GroupNav
groups={groups.map((group, i) => ({
name: group.name,
slug: group.slug,
anchor: anchors[i]!,
}))}
/>
<GroupNav groups={groups.map((group) => ({ name: group.name, slug: group.slug }))} />
<main>
{groups.length === 0 ? (
<p className="empty">No data ingested yet.</p>
Expand Down Expand Up @@ -111,14 +125,21 @@ async function HomeContent({
/>
);
})}
{/* v3's landing_body early-returns on an empty database, so the
no-JS hint only renders alongside actual chart shells. */}
{/* After the last group's markup, in the same streamed chunk, so
it opens the fragment's groups before they paint. */}
<OpenGroupsScript />
{/* A browser never shows this hint. It streams inside the
Suspense boundary's hidden segment, which only React's inline
script reveals, so with JavaScript off the page shows the
loading fallback alone; with JavaScript on, `<noscript>`
shows nothing. */}
<noscript>
<p className="no-script">JavaScript is required to render the charts.</p>
</noscript>
</>
)}
</main>
<LandingState />
<Footer />
</>
);
Expand Down
6 changes: 4 additions & 2 deletions bench/web/components/Chart.empty-window.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ describe('empty-window charts hide by default and the toggle reveals them', () =
async function renderGroup(groupSlug: string): Promise<void> {
container.innerHTML =
'<section class="group-details">' +
'<details class="group-disclosure" open><summary class="group-summary">g</summary></details>' +
'<div class="chart-grid"><div id="m0"></div><div id="m1"></div></div>' +
'<details class="group-disclosure" open><summary class="group-summary">g</summary>' +
'<div class="group-body"><div class="chart-grid">' +
'<div id="m0"></div><div id="m1"></div></div></div>' +
'</details>' +
'</section>';
const roots: Root[] = [];
await act(async () => {
Expand Down
83 changes: 75 additions & 8 deletions bench/web/components/Chart.lazy-hydration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { Chart } from '@/components/Chart';
import { GroupBody } from '@/components/GroupBody';
import { FETCH_TIMEOUT_MS } from '@/lib/chart-format';
import { bundleQueue, hydrationQueue, resetPayloadCache } from '@/lib/chart-store';

Expand Down Expand Up @@ -114,8 +115,8 @@ describe('PR-5.0.95 landing-page lazy hydration', () => {
const mounts = Array.from({ length: n }, (_, i) => `<div id="m${i}"></div>`).join('');
container.innerHTML =
'<section class="group-details">' +
'<details class="group-disclosure" open><summary class="group-summary">g</summary></details>' +
`<div class="chart-grid">${mounts}</div>` +
'<details class="group-disclosure" open><summary class="group-summary">g</summary>' +
`<div class="group-body"><div class="chart-grid">${mounts}</div></div></details>` +
'</section>';
// Render each island into its OWN root so each gets its own mount effect.
const roots: Root[] = [];
Expand Down Expand Up @@ -388,8 +389,8 @@ describe('PR-5.0.97 group-bundle hydration', () => {
const mounts = Array.from({ length: n }, (_, i) => `<div id="m${i}"></div>`).join('');
container.innerHTML =
'<section class="group-details">' +
'<details class="group-disclosure" open><summary class="group-summary">g</summary></details>' +
`<div class="chart-grid">${mounts}</div>` +
'<details class="group-disclosure" open><summary class="group-summary">g</summary>' +
`<div class="group-body"><div class="chart-grid">${mounts}</div></div></details>` +
'</section>';
const roots: Root[] = [];
await act(async () => {
Expand Down Expand Up @@ -539,6 +540,72 @@ describe('PR-5.0.97 group-bundle hydration', () => {
expect(signals[0]!.aborted).toBe(true);
});

it('GroupBody mounts islands only while open; they share one bundle, and closing aborts it', async () => {
const signals: AbortSignal[] = [];
vi.stubGlobal('fetch', (url: string | URL, init?: { signal?: AbortSignal }) => {
fetchCalls.push(String(url));
if (init?.signal) {
signals.push(init.signal);
}
return new Promise<Response>((_res, reject) => {
init?.signal?.addEventListener('abort', () =>
reject(init.signal?.reason ?? new DOMException('Aborted', 'AbortError')),
);
});
});
// The landing page's shape: the body renders inside a CLOSED disclosure.
container.innerHTML =
'<section class="group-details">' +
'<details class="group-disclosure"><summary class="group-summary">g</summary>' +
'<div id="body"></div></details>' +
'</section>';
const bodyRoot = createRoot(container.querySelector('#body') as HTMLElement);
root = bodyRoot;
await act(async () => {
bodyRoot.render(
<GroupBody
groupSlug="g"
charts={[
{ name: 'q0', slug: 's0' },
{ name: 'q1', slug: 's1' },
{ name: 'q2', slug: 's2' },
]}
startIndex={0}
universe={{ engines: [], formats: [] }}
initialHiddenSeries={[]}
initialShownSeries={[]}
/>,
);
});
await act(async () => {
await Promise.resolve();
});
// Collapsed: no island exists, so nothing observes or fetches.
expect(container.querySelectorAll('.chart-card')).toHaveLength(0);
expect(MockIO.instances).toHaveLength(0);
expect(fetchCalls).toEqual([]);

const details = container.querySelector('details.group-disclosure') as HTMLDetailsElement;
await act(async () => {
details.open = true;
details.dispatchEvent(new Event('toggle'));
await Promise.resolve();
});
// Open: three islands, one observer each, and one shared bundle request.
expect(container.querySelectorAll('.chart-card')).toHaveLength(3);
expect(MockIO.instances).toHaveLength(3);
expect(fetchCalls).toEqual(['/api/group/g?n=100']);

await act(async () => {
details.open = false;
details.dispatchEvent(new Event('toggle'));
await Promise.resolve();
});
// Closed: the islands are gone and the bundle is aborted.
expect(container.querySelectorAll('.chart-card')).toHaveLength(0);
expect(signals[0]!.aborted).toBe(true);
});

it('reopen AFTER the bundle succeeded issues ZERO new fetches (cache hit)', async () => {
vi.stubGlobal('fetch', (url: string | URL) => {
fetchCalls.push(String(url));
Expand Down Expand Up @@ -750,12 +817,12 @@ describe('PR-5.0.97 group-bundle hydration', () => {
// so the top group (index 0) outranks the lower one (index 1).
container.innerHTML =
'<section class="group-details" id="ga">' +
'<details class="group-disclosure" open><summary class="group-summary">ga</summary></details>' +
'<div class="chart-grid"><div id="ma"></div></div>' +
'<details class="group-disclosure" open><summary class="group-summary">ga</summary>' +
'<div class="group-body"><div class="chart-grid"><div id="ma"></div></div></div></details>' +
'</section>' +
'<section class="group-details" id="gb">' +
'<details class="group-disclosure" open><summary class="group-summary">gb</summary></details>' +
'<div class="chart-grid"><div id="mb"></div></div>' +
'<details class="group-disclosure" open><summary class="group-summary">gb</summary>' +
'<div class="group-body"><div class="chart-grid"><div id="mb"></div></div></div></details>' +
'</section>';
const ra = createRoot(container.querySelector('#ma') as HTMLElement);
const rb = createRoot(container.querySelector('#mb') as HTMLElement);
Expand Down
Loading
Loading