Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions apps/website/__tests__/components/ComparisonTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ it("renders trail markers for each adapter", () => {
screen.getByRole("img", { name: /recommended path/i }),
).toBeInTheDocument();
expect(
screen.getByRole("img", { name: /slower scroll.*row-height drift/i }),
screen.getByRole("img", { name: /slower scroll.*slower interaction/i }),
).toBeInTheDocument();
expect(
screen.getByRole("img", { name: /headless.*selection and nav/i }),
screen.getByRole("img", { name: /headless.*slower interaction/i }),
).toBeInTheDocument();
expect(
screen.getByRole("img", { name: /parity at scroll p95/i }),
screen.getByRole("img", { name: /scroll-p95 parity/i }),
).toBeInTheDocument();
});
175 changes: 168 additions & 7 deletions apps/website/app/bench/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,97 @@ function loadH1HighRepeatCorrection(): H1HighRepeatCorrectionFile {
return JSON.parse(raw) as H1HighRepeatCorrectionFile;
}

interface InteractionAdapterRow {
scriptName: "sort" | "filter-metadata" | "filter-text";
interactionLatencyMs: number | null;
settleDurationMs: number | null;
sampleCount: number;
}

interface InteractionAdapterSummary {
adapterId: string;
rows: InteractionAdapterRow[];
}

interface InteractionSummaryFile {
runsetId: string;
generatedAt: string;
scenarioId: string;
scale: string;
browserName: string;
scripts: string[];
adapters: InteractionAdapterSummary[];
}

interface InteractionRow {
adapter: (typeof ADAPTER_ORDER)[number];
label: string;
sortMs: number;
filterMetadataMs: number;
filterTextMs: number;
}

function loadInteractionSummary(): {
rows: InteractionRow[];
filename: string;
runsetId: string;
} {
const filename = "status/milestones/2026-05-10-b2-sort-filter-summary.json";
const raw = readFileSync(repoRootRelative(filename), "utf8");
const data = JSON.parse(raw) as InteractionSummaryFile;
const rows = ADAPTER_ORDER.flatMap<InteractionRow>((adapter) => {
const entry = data.adapters.find((a) => a.adapterId === adapter);
if (!entry) return [];
const sortRow = entry.rows.find((r) => r.scriptName === "sort");
const fmRow = entry.rows.find((r) => r.scriptName === "filter-metadata");
const ftRow = entry.rows.find((r) => r.scriptName === "filter-text");
if (
sortRow?.interactionLatencyMs == null ||
fmRow?.interactionLatencyMs == null ||
ftRow?.interactionLatencyMs == null
) {
return [];
}
return [
{
adapter,
label: ADAPTER_LABEL[adapter],
sortMs: sortRow.interactionLatencyMs,
filterMetadataMs: fmRow.interactionLatencyMs,
filterTextMs: ftRow.interactionLatencyMs,
},
];
});
return { rows, filename, runsetId: data.runsetId };
}

function interactionVerdictFor(
row: InteractionRow,
fastest: InteractionRow,
): string {
if (row.adapter === fastest.adapter) {
return "fastest tied; full quality pass";
}
const ratios = [
row.sortMs / fastest.sortMs,
row.filterMetadataMs / fastest.filterMetadataMs,
row.filterTextMs / fastest.filterTextMs,
];
const minR = Math.min(...ratios);
const maxR = Math.max(...ratios);
const tieScripts: string[] = [];
if (row.filterMetadataMs / fastest.filterMetadataMs < 1.05) {
tieScripts.push("filter-metadata");
}
const range =
Math.round(minR * 10) === Math.round(maxR * 10)
? `${minR.toFixed(1)}× slower`
: `${minR.toFixed(1)}–${maxR.toFixed(1)}× slower`;
return tieScripts.length > 0
? `${range} (${tieScripts.join(", ")} ties pretable)`
: range;
}

function verdictFor(
row: ScrollRow,
fastest: ScrollRow,
Expand Down Expand Up @@ -189,6 +280,13 @@ export default function BenchPage() {
const pretableHighRepeat = highRepeat.perAdapter.pretable;
const muiHighRepeat = highRepeat.perAdapter.mui;

const { rows: interactionRows } = loadInteractionSummary();
const interactionFastest = interactionRows.reduce((min, r) => {
const minSum = min.sortMs + min.filterMetadataMs + min.filterTextMs;
const rSum = r.sortMs + r.filterMetadataMs + r.filterTextMs;
return rSum < minSum ? r : min;
});

return (
<article className="prose">
<h1 className="font-display text-[44px] leading-[1.05] tracking-[-0.025em] text-text-primary">
Expand Down Expand Up @@ -308,15 +406,78 @@ export default function BenchPage() {
</p>

<h2 className="mt-12 font-display text-[28px] tracking-[-0.02em] text-text-primary">
Interaction (sort, filter)
Interactions (sort, filter)
</h2>
<p className="mt-3 text-[15px] leading-[1.6] text-text-secondary">
Scenario <code>S2</code> (3,000 rows, wrapped multilingual messages).
Sort applies a column-state change; filter-metadata applies an equals
filter on a metadata column; filter-text applies a contains filter on
the wrapped-text primary column. Latency measured from trigger to first
changed frame; lower is better.
</p>

<table className="mt-6 w-full table-fixed border-collapse text-left text-[14px]">
<thead>
<tr className="border-b border-rule text-text-muted">
<th className="py-3 font-mono text-[11px] uppercase tracking-[0.14em]">
Adapter
</th>
<th className="py-3 font-mono text-[11px] uppercase tracking-[0.14em]">
sort p95 (ms)
</th>
<th className="py-3 font-mono text-[11px] uppercase tracking-[0.14em]">
filter-metadata p95 (ms)
</th>
<th className="py-3 font-mono text-[11px] uppercase tracking-[0.14em]">
filter-text p95 (ms)
</th>
<th className="py-3 font-mono text-[11px] uppercase tracking-[0.14em]">
Verdict
</th>
</tr>
</thead>
<tbody>
{interactionRows.map((r) => (
<tr
className="border-b border-rule-soft text-text-primary"
key={r.adapter}
>
<td className="py-3 font-mono text-[13px] font-semibold">
{r.label}
</td>
<td className="py-3 font-mono text-[13px] tabular-nums">
{r.sortMs.toFixed(1)}
</td>
<td className="py-3 font-mono text-[13px] tabular-nums">
{r.filterMetadataMs.toFixed(1)}
</td>
<td className="py-3 font-mono text-[13px] tabular-nums">
{r.filterTextMs.toFixed(1)}
</td>
<td className="py-3 text-[13px] text-text-secondary">
{interactionVerdictFor(r, interactionFastest)}
</td>
</tr>
))}
</tbody>
</table>

<p className="mt-6 max-w-[60ch] text-[15px] leading-[1.6] text-text-secondary">
Pretable sorts and filters 3,000 wrapped-text rows in 16–18 ms across
all three scripts — clear of the single 60Hz frame budget on{" "}
<code>filter-metadata</code> and <code>sort</code>, fractionally over on{" "}
<code>filter-text</code>. AG Grid Community runs sort and filter 3–3.5×
slower despite being a full feature-surface grid; MUI X DataGrid
Community lands at roughly 2× across all three scripts. TanStack Table
v8 + TanStack Virtual is the only comparator that ties pretable on a
single metric — <code>filter-metadata</code> at 15.7 ms vs 16.0 ms,
within run noise — but is 2.1× slower on sort and 2.3× slower on{" "}
<code>filter-text</code>.
</p>
<p className="mt-3 max-w-[60ch] text-[15px] leading-[1.6] text-text-secondary">
Sort, metadata-filter, and wrapped-text-filter scripts (H6, H7, H8) all
stay within pretable&rsquo;s single-frame interaction budget on the same
dataset and remain satisfied on this runset. The comparator grids each
carry their own sort/filter pipelines, but our matrix gates interaction
scripts to pretable for now — comparative interaction evidence is on the
roadmap.
Like the scroll story, the H6/H7/H8 evaluators check pretable&rsquo;s
absolute thresholds (<code>≤ 32 ms</code> interaction p95) rather than
gating on comparator parity. All three hypotheses stay satisfied at n=3.
</p>

<h2 className="mt-12 font-display text-[28px] tracking-[-0.02em] text-text-primary">
Expand Down
39 changes: 35 additions & 4 deletions apps/website/app/components/ComparisonTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const NA_MARKER = "n/a";
// Same slice for pretable + mui at 20 repeats. Confirms parity (mean
// diff −0.065 ms, well inside the 2σ noise floor of 0.40 ms).
//
// status/milestones/2026-05-10-b2-sort-filter-summary.json
// S2/hypothesis/Chromium × 3 repeats × 4 adapters × 3 interaction
// scripts. Pretable beats AG Grid 3-3.5× and MUI 2× across sort,
// filter-metadata, filter-text; TanStack at parity on filter-metadata
// only.
//
// Re-derive with `pnpm bench:matrix --adapters=pretable,ag-grid,tanstack,mui
// --scenarios=S2 --scripts=scroll --scale=hypothesis --repeats=10`.
//
Expand Down Expand Up @@ -64,6 +70,30 @@ const ROWS: readonly Row[] = [
mui: "0",
budget: "≤ 16",
},
{
metric: "sort latency p95 (ms) — interaction",
pretable: "16.5",
agGrid: "58.3",
tanstack: "34.4",
mui: "35.0",
budget: "≤ 16",
},
{
metric: "filter-metadata latency p95 (ms)",
pretable: "16.0",
agGrid: "49.9",
tanstack: "15.7",
mui: "33.4",
budget: "≤ 16",
},
{
metric: "filter-text latency p95 (ms)",
pretable: "17.7",
agGrid: "50.0",
tanstack: "40.2",
mui: "33.3",
budget: "≤ 16",
},
{
metric: "headless engine + React surface",
pretable: "yes",
Expand Down Expand Up @@ -98,7 +128,8 @@ export function ComparisonTable() {
of AG Grid Community and TanStack on raw frame p95 — and is the only
adapter here that clears every quality threshold (zero blank gaps,
zero anchor shift, ≤1 px row-height drift) at full-grid feature
weight.{" "}
weight. Interactive sort and filter run 2–3.5× faster than every
measured comparator on the same dataset.{" "}
<a
href="https://github.com/cacheplane/pretable/tree/main/status/milestones"
className="text-accent-deep underline-offset-2 hover:underline"
Expand All @@ -124,7 +155,7 @@ export function ComparisonTable() {
<span className="inline-flex items-center gap-2">
<TrailMarker
variant="blue"
label="Slower scroll; row-height drift"
label="1.7× slower scroll, 3× slower interaction; row-height drift"
/>
AG Grid
</span>
Expand All @@ -133,7 +164,7 @@ export function ComparisonTable() {
<span className="inline-flex items-center gap-2">
<TrailMarker
variant="black"
label="Headless; you wire selection and nav"
label="Headless; ~2× slower interaction (filter-metadata ties pretable)"
/>
TanStack
</span>
Expand All @@ -142,7 +173,7 @@ export function ComparisonTable() {
<span className="inline-flex items-center gap-2">
<TrailMarker
variant="green"
label="Parity at scroll p95; full-grid feature surface"
label="Scroll-p95 parity; 2× slower interaction"
/>
MUI X
</span>
Expand Down
29 changes: 29 additions & 0 deletions docs/research/repo-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,32 @@ Hypothesis status (H6/H7/H8 evaluators remain pretable-only — comparator data
- Homepage narrative refresh to reflect the 2–3.5× wedge on interaction scripts (separate editorial follow-up; this PR is harness wiring + evidence only).

This closes the structural part of B2 follow-up #5; the supportedScripts gate is no longer pretable-only for any non-selection script.

## 2026-05-11

### B2 follow-up: homepage interaction wedge refresh

Editorial PR landing the PR #131 sort + filter comparator wedge on the homepage. Three editorial surfaces touched; no source/package changes.

- **`apps/website/app/components/ComparisonTable.tsx`** — three new rows added between `scroll anchor shift (px)` and `headless engine + React surface`: `sort latency p95`, `filter-metadata latency p95`, `filter-text latency p95`. Each row carries per-adapter numbers from the PR #131 runset (n=3 medians). The section subhead gets one new sentence: "Interactive sort and filter run 2–3.5× faster than every measured comparator on the same dataset." Header docblock cites the new milestone source.
- **Trail-marker labels** rewritten on three comparators (pretable's "Recommended path" unchanged):
- **AG Grid** — `"Slower scroll; row-height drift"` → `"1.7× slower scroll, 3× slower interaction; row-height drift"`.
- **TanStack** — `"Headless; you wire selection and nav"` → `"Headless; ~2× slower interaction (filter-metadata ties pretable)"`.
- **MUI X** — `"Parity at scroll p95; full-grid feature surface"` → `"Scroll-p95 parity; 2× slower interaction"`. This is the most consequential change — the prior "parity at scroll p95" framing read positively about MUI; the new label keeps that half of the story while adding the interaction caveat.
- **`apps/website/app/bench/page.tsx`** — the existing placeholder Interactions section ("comparative interaction evidence is on the roadmap") is replaced with a real section mirroring the H1 scroll layout: a four-adapter × three-script table plus two prose paragraphs. New `loadInteractionSummary()` + `interactionVerdictFor()` helpers parallel the existing scroll-side patterns. The verdict helper computes per-script ratio ranges and annotates the TanStack `filter-metadata` tie inline.
- **Aggregator + summary file:** `scripts/extract-interaction-summary.mjs` is a one-shot Node script that reads the PR #131 per-run JSONs (`status/chromium-<adapter>-default-s2-hypothesis-{sort,filter-metadata,filter-text}-2026-05-10*.summary.json`), picks medians, and writes `status/milestones/2026-05-10-b2-sort-filter-summary.json`. Future matrix runs can regenerate the summary via the same script.

**Tests:** `apps/website/__tests__/components/ComparisonTable.test.tsx` regression-guards the new trail-marker label phrasings via regex; pretable assertion unchanged. No new test for the `/bench` page section beyond the existing render-check.

### Out of scope (deliberate)

- **`ReceiptsBand.tsx`** — PR #129 (streaming reframe) is still open and modifies this file. Leaving it untouched here to avoid a merge conflict over an unresolved editorial decision.
- **`FeatureGrid.tsx` Stream-aware card** — already capability-anchored after PR #126; doesn't need re-touching.
- **High-repeat (n=20) follow-up for borderline cases:** pretable's `filter-text` at 17.7 ms and TanStack's `filter-metadata` at 15.7 ms both sit within ±2 ms of the 16 ms frame budget. The page prose acknowledges this; an n=20 rerun would tighten the verdict but isn't a v1 blocker.
- **Comparator-aware H6/H7/H8 evaluators** — the page reads from the aggregated summary file directly (mirroring the scroll-summary pattern); evaluator-array extension is future work if needed.

### Open from B2

- **PR #129 streaming reframe** — still awaiting user prose review. Touches `ComparisonTable.tsx` (streaming-row rename) and `ReceiptsBand.tsx`. File-level conflict with this PR is limited to the docblock at the top of `ComparisonTable.tsx`; resolvable.
- **High-repeat protocol for interaction borderlines** — logged here.
- **Pretable `scroll-with-render` 16.4 ms anomaly** — logged in the 2026-05-10 entry above; still pending investigation.
Loading