From ce8d9dfff7d6e6f88b2db7ae826e3934a09eec81 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Fri, 16 Oct 2026 05:17:03 -0700 Subject: [PATCH 1/9] feat(scaling): ceiling cards and guardrails Co-authored-by: Toshi --- app/scaling/components/CeilingCards.tsx | 46 +++++++++++++++++ app/scaling/components/Guardrails.tsx | 57 +++++++++++++++++++++ app/scaling/components/Ladder.tsx | 4 +- app/scaling/components/Tag.tsx | 11 ++++ app/scaling/components/firstScreen.test.tsx | 50 ++++++++++++++++++ app/scaling/page.tsx | 4 ++ 6 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 app/scaling/components/CeilingCards.tsx create mode 100644 app/scaling/components/Guardrails.tsx create mode 100644 app/scaling/components/Tag.tsx create mode 100644 app/scaling/components/firstScreen.test.tsx diff --git a/app/scaling/components/CeilingCards.tsx b/app/scaling/components/CeilingCards.tsx new file mode 100644 index 0000000..8188204 --- /dev/null +++ b/app/scaling/components/CeilingCards.tsx @@ -0,0 +1,46 @@ +import Link from 'next/link'; + +import { Card } from '../../components/ui/Card'; +import { Text } from '../../components/ui/Text'; +import { deriveBinding, type Ceiling, type CeilingInput } from '../snapshot.mjs'; +import { Info } from './Info'; +import { Tag } from './Tag'; + +const AREAS: [Ceiling['area'], string][] = [['da', 'DA'], ['execution', 'Execution'], ['state', 'State'], ['networking', 'Networking']]; + +function input({ name, value, unit, status, asOf }: CeilingInput): string { + const shown = value === null ? status : `${value} ${unit}`.trim(); + return [name, shown, value === null ? null : status, asOf && new Date(asOf).toISOString().slice(0, 10)].filter(Boolean).join(' · '); +} + +export function CeilingCards({ ceilings }: { ceilings: Ceiling[] }) { + const binding = deriveBinding(ceilings); + return ( +
+ Ceilings by bottleneck · Mgas/s +
+ {AREAS.map(([area, name]) => { + const c = ceilings.find((x) => x.area === area)!; + return ( + + + {name} + {c.help} + + + {c.value === null ? '—' : c.value.toLocaleString('en-US', { maximumFractionDigits: 2 })} + + {c === binding ? Binding : null} + + {c.formula} + {c.reason ?

{c.reason}

: null} +
    + {c.inputs.map((i) =>
  • {input(i)}
  • )} +
+
+ ); + })} +
+
+ ); +} diff --git a/app/scaling/components/Guardrails.tsx b/app/scaling/components/Guardrails.tsx new file mode 100644 index 0000000..0a31143 --- /dev/null +++ b/app/scaling/components/Guardrails.tsx @@ -0,0 +1,57 @@ +import { cn } from '../../components/ui/cn'; +import { Card } from '../../components/ui/Card'; +import { Text } from '../../components/ui/Text'; +import type { Guardrail } from '../snapshot.mjs'; +import { Info } from './Info'; +import { Tag } from './Tag'; + +const TH = 'px-4 py-2 text-left text-[12px] font-[500] text-bds-gray-60'; +const TD = 'px-4 py-3 align-top'; +const DOT: Record = { ok: 'bg-bds-green-50', breached: 'bg-bds-red-50', unknown: 'bg-bds-gray-30' }; +const STATUS: Record = { ok: 'OK', breached: 'Breached', unknown: 'Unknown' }; + +const fmt = (n: number, unit: string) => `${n.toLocaleString('en-US', { maximumFractionDigits: 2 })} ${unit}`.trim(); + +export function Guardrails({ guardrails }: { guardrails: Guardrail[] }) { + return ( +
+ Guardrails + + + + + + + + + + + + {guardrails.map((g) => ( + + + + + + + ))} + +
GuardrailValueThresholdStatus
+ {g.label} {g.help} + {g.value === null ? '—' : fmt(g.value, g.unit)} + + {g.threshold === null ? '—' : `${g.comparator === '<=' ? '≤' : '≥'} ${fmt(g.threshold, g.unit)}`} + {g.thresholdTag === 'set' ? null : } + {g.owner ? {g.owner} : null} + + + + +
+
+ Breached SLOs are not attributed to saturation until triaged. +
+ ); +} diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx index b436a87..3c0df49 100644 --- a/app/scaling/components/Ladder.tsx +++ b/app/scaling/components/Ladder.tsx @@ -6,13 +6,13 @@ import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; import { deriveBinding, type Rung, type Snapshot } from '../snapshot.mjs'; import { Info } from './Info'; +import { Tag } from './Tag'; import { layoutRungs, type Anchor } from './ladderLayout'; const LABEL_PX = 150; const ROW = 44; const AXIS = 12; const AREA_NAME = { da: 'DA', execution: 'execution', state: 'state', networking: 'networking' }; -const TAG_LABEL: Record = { 'not-measured': 'Not measured', 'not-modeled': 'Not modeled', 'not-established': 'Not established' }; const ALIGN: Record = { start: 'items-start text-left', middle: 'items-center text-center', end: 'items-end text-right' }; const SHIFT: Record = { start: 'none', middle: 'translateX(-50%)', end: 'translateX(-100%)' }; @@ -95,7 +95,7 @@ export function Ladder(props: Props) { {all.filter((item) => !placed.has(item.id)).map((item) => (
  • {item.label} - {TAG_LABEL[item.tag] ?? item.tag} + {item.help}
  • ))} diff --git a/app/scaling/components/Tag.tsx b/app/scaling/components/Tag.tsx new file mode 100644 index 0000000..5fe623d --- /dev/null +++ b/app/scaling/components/Tag.tsx @@ -0,0 +1,11 @@ +const LABEL: Record = { + measured: 'Measured', modeled: 'Modeled', estimated: 'Estimated', + 'not-measured': 'Not measured', 'not-modeled': 'Not modeled', 'not-established': 'Not established', + provisional: 'Provisional', unset: 'Unset', +}; + +export function Tag({ tag }: { tag: string }) { + return ( + {LABEL[tag] ?? tag} + ); +} diff --git a/app/scaling/components/firstScreen.test.tsx b/app/scaling/components/firstScreen.test.tsx new file mode 100644 index 0000000..b7419e8 --- /dev/null +++ b/app/scaling/components/firstScreen.test.tsx @@ -0,0 +1,50 @@ +import { readFileSync } from 'node:fs'; + +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; + +import type { Snapshot } from '../snapshot.mjs'; +import { CeilingCards } from './CeilingCards'; +import { Guardrails } from './Guardrails'; + +// Synthetic data only: real snapshots are internal and never committed. +function fixture(): Snapshot { + return JSON.parse(readFileSync(new URL('../fixtures/synthetic.v2.json', import.meta.url), 'utf8')); +} + +const cards = (html: string) => html.split(' { + it('renders every area in a fixed order with its tag, and badges the binding card', () => { + const html = cards(renderToStaticMarkup()); + expect(html.map((c) => c.match(/href="\/scaling\/(\w+)"/)![1])).toEqual(['da', 'execution', 'state', 'networking']); + expect(html.map((c) => c.match(/border-dashed[^>]*>([^<]+) c.includes('>Binding<'))).toEqual([true, false, false, false]); + }); + + it('moves the binding badge to a lower measured ceiling, and shows a dash, reason and pending inputs when there is no value', () => { + const { ceilings } = fixture(); + ceilings[1] = { ...ceilings[1], value: 90, tag: 'measured' }; + const html = cards(renderToStaticMarkup()); + expect(html.map((c) => c.includes('>Binding<'))).toEqual([false, true, false, false]); + expect(html[2]).toContain('>—<'); + expect(html[2]).toContain('live-state growth not measured'); + expect(html[2]).toContain('state_bytes_per_gas · pending'); + expect(html[2]).toContain('runway_years · 4 years · provisional · 2026-01-01'); + }); +}); + +describe('Guardrails', () => { + it('shows comparator and unit with the threshold, tags provisional and unset thresholds, and colors only known statuses', () => { + const rows = renderToStaticMarkup().split('Provisional<'); + expect(rows[2]).toContain('Synthetic owner'); + expect(rows[3]).toContain('>Unset<'); + expect(rows[3]).toContain('bg-bds-gray-30'); + expect(rows[3]).not.toMatch(/bg-bds-(green|red)/); + }); +}); diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index cf7600c..cb7caf3 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -6,6 +6,8 @@ import { Card } from '../components/ui/Card'; import { EmptyState } from '../components/ui/EmptyState'; import { Text } from '../components/ui/Text'; +import { CeilingCards } from './components/CeilingCards'; +import { Guardrails } from './components/Guardrails'; import { Ladder } from './components/Ladder'; import { isStale, parseSnapshot, type Metric, type Snapshot, type WorkItem } from './snapshot.mjs'; @@ -68,6 +70,8 @@ export default async function ScalingPage() { + + {snapshot.sections.map((section) => (
    From 3ffeea6329b0a33626b68948b4923a4abd0b7fe6 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Fri, 16 Oct 2026 05:23:41 -0700 Subject: [PATCH 2/9] feat(scaling): progress from Linear and collapsed detail sections Co-authored-by: Toshi --- app/scaling/components/Progress.tsx | 62 +++++++++++++++++++++ app/scaling/components/Tag.tsx | 1 + app/scaling/components/firstScreen.test.tsx | 20 +++++++ app/scaling/page.test.tsx | 14 +++++ app/scaling/page.tsx | 55 +++++------------- 5 files changed, 110 insertions(+), 42 deletions(-) create mode 100644 app/scaling/components/Progress.tsx create mode 100644 app/scaling/page.test.tsx diff --git a/app/scaling/components/Progress.tsx b/app/scaling/components/Progress.tsx new file mode 100644 index 0000000..15451ae --- /dev/null +++ b/app/scaling/components/Progress.tsx @@ -0,0 +1,62 @@ +import { Card } from '../../components/ui/Card'; +import { EmptyState } from '../../components/ui/EmptyState'; +import { Text } from '../../components/ui/Text'; +import type { WorkItem } from '../snapshot.mjs'; +import { Info } from './Info'; +import { Tag } from './Tag'; + +export function Progress({ work }: { work: WorkItem[] }) { + return ( +
    + Progress + {work.length === 0 ? ( + + ) : ( + + {work.map((w) => )} + + )} + Expected effects are not summed. +
    + ); +} + +function Row({ item: w }: { item: WorkItem }) { + const l = w.linear; + return ( +
    + ); +} diff --git a/app/scaling/components/Tag.tsx b/app/scaling/components/Tag.tsx index 5fe623d..cb83d08 100644 --- a/app/scaling/components/Tag.tsx +++ b/app/scaling/components/Tag.tsx @@ -2,6 +2,7 @@ const LABEL: Record = { measured: 'Measured', modeled: 'Modeled', estimated: 'Estimated', 'not-measured': 'Not measured', 'not-modeled': 'Not modeled', 'not-established': 'Not established', provisional: 'Provisional', unset: 'Unset', + da: 'DA', execution: 'Execution', state: 'State', networking: 'Networking', }; export function Tag({ tag }: { tag: string }) { diff --git a/app/scaling/components/firstScreen.test.tsx b/app/scaling/components/firstScreen.test.tsx index b7419e8..5f95aa2 100644 --- a/app/scaling/components/firstScreen.test.tsx +++ b/app/scaling/components/firstScreen.test.tsx @@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest'; import type { Snapshot } from '../snapshot.mjs'; import { CeilingCards } from './CeilingCards'; import { Guardrails } from './Guardrails'; +import { Progress } from './Progress'; // Synthetic data only: real snapshots are internal and never committed. function fixture(): Snapshot { @@ -48,3 +49,22 @@ describe('Guardrails', () => { expect(rows[3]).not.toMatch(/bg-bds-(green|red)/); }); }); + +describe('Progress', () => { + it('shows Linear state and a labelled bar when linked, and proposed with no bar otherwise', () => { + const [linked, proposed] = renderToStaticMarkup().split('px-4 py-3').slice(1); + expect(linked).toContain('>started<'); + expect(linked).toContain('aria-label="4 of 10 issues done"'); + expect(linked).toContain('width:40%'); + expect(linked).toContain('Realized:'); + expect(linked).toContain('href="https://example.com/synthetic/linear/project"'); + expect(proposed).toContain('>proposed<'); + expect(proposed).not.toContain('progressbar'); + expect(proposed).not.toContain('Realized:'); + }); + + it('hides realized without evidence', () => { + const [item] = fixture().work; + expect(renderToStaticMarkup()).not.toContain('Realized:'); + }); +}); diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx new file mode 100644 index 0000000..75068a7 --- /dev/null +++ b/app/scaling/page.test.tsx @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url'; + +import { renderToStaticMarkup } from 'react-dom/server'; +import { expect, it, vi } from 'vitest'; + +import ScalingPage from './page'; + +it('collapses each detail section by default with its title and row count', async () => { + vi.stubEnv('SCALING_SNAPSHOT_PATH', fileURLToPath(new URL('./fixtures/synthetic.v2.json', import.meta.url))); + const html = renderToStaticMarkup(await ScalingPage()); + expect(html).toMatch(/
    ]*>]*>Data availability<\/h2>]*> · 2 rows<\/span>/); + expect(html).not.toContain('
    + + {snapshot.sections.map((section) => ( -
    -
    - {section.title} - {section.summary} -
    - +
    + + {section.title} + · {section.metrics.length} {section.metrics.length === 1 ? 'row' : 'rows'} + {section.summary} + + {section.metrics.map((m) => (
    @@ -91,27 +95,16 @@ export default async function ScalingPage() { ))} {section.gaps.length > 0 ? ( -
    +
    Missing evidence
      {section.gaps.map((gap) =>
    • {gap}
    • )}
    ) : null} -
    +
    ))} -
    - What we're working on - {snapshot.work.length === 0 ? ( - - ) : ( -
    - {snapshot.work.map((w) => )} -
    - )} -
    - Experiment results: Benchmarks ·{' '} Load tests @@ -160,28 +153,6 @@ function MetricDetails({ metric, snapshot }: { metric: Metric; snapshot: Snapsho ); } -function WorkCard({ item }: { item: WorkItem }) { - const rows = [ - ['Hypothesis', item.hypothesis], - ['Change', item.change], - ['Expected impact', item.expected ?? 'Not estimated'], - ['Realized impact', item.realized ?? 'Not measured'], - ]; - return ( - - {item.title} - {item.status} · {item.owner ?? 'No owner assigned'} - {rows.map(([term, text]) => ( -

    {term}: {text}

    - ))} -
    - {item.evidence ? : null} - {item.links.map((l) => )} -
    -
    - ); -} - function ExternalLink({ link }: { link: { label: string; url: string } }) { return ( From b7f14cc45d8fbb65624d889595e15e21aae2eaee Mon Sep 17 00:00:00 2001 From: Francis Li Date: Fri, 16 Oct 2026 05:35:12 -0700 Subject: [PATCH 3/9] feat(scaling): shared snapshot header and guardrails page Co-authored-by: Toshi --- .github/workflows/ci.yml | 2 +- app/scaling/components/Guardrails.tsx | 10 ++-- app/scaling/components/SectionTable.tsx | 56 +++++++++++++++++++ app/scaling/fixtures/synthetic.v2.json | 9 ++++ app/scaling/guardrails/page.tsx | 20 +++++++ app/scaling/loadSnapshot.tsx | 71 +++++++++++++++++++++++++ app/scaling/page.test.tsx | 24 +++++++-- app/scaling/page.tsx | 62 ++------------------- app/scaling/snapshot.mjs | 7 ++- app/scaling/snapshot.test.ts | 6 +++ 10 files changed, 201 insertions(+), 66 deletions(-) create mode 100644 app/scaling/components/SectionTable.tsx create mode 100644 app/scaling/guardrails/page.tsx create mode 100644 app/scaling/loadSnapshot.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eed6966..08553f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,7 +165,7 @@ jobs: /internal-explorer/bundles/0x1 /api/internal-explorer/blocks \ /tips /tips/block/0x1 /tips/bundles/0x1 /api/tips/blocks \ /benchmark /benchmark/run/latest /benchmark/run-comparison/1 \ - /benchmark/load-tests/sepolia /scaling; do + /benchmark/load-tests/sepolia /scaling /scaling/guardrails; do code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:3000${route}") if [ "${code}" != "404" ]; then echo "FAIL: ${route} returned ${code}, expected 404" diff --git a/app/scaling/components/Guardrails.tsx b/app/scaling/components/Guardrails.tsx index 0a31143..278218f 100644 --- a/app/scaling/components/Guardrails.tsx +++ b/app/scaling/components/Guardrails.tsx @@ -1,3 +1,5 @@ +import Link from 'next/link'; + import { cn } from '../../components/ui/cn'; import { Card } from '../../components/ui/Card'; import { Text } from '../../components/ui/Text'; @@ -12,10 +14,10 @@ const STATUS: Record = { ok: 'OK', breached: 'Breac const fmt = (n: number, unit: string) => `${n.toLocaleString('en-US', { maximumFractionDigits: 2 })} ${unit}`.trim(); -export function Guardrails({ guardrails }: { guardrails: Guardrail[] }) { +export function Guardrails({ guardrails, detailed = false }: { guardrails: Guardrail[]; detailed?: boolean }) { return (
    - Guardrails + {detailed ? null : Guardrails →} @@ -30,7 +32,9 @@ export function Guardrails({ guardrails }: { guardrails: Guardrail[] }) { {guardrails.map((g) => ( + + + + + + + + + + + + ); +} + +function Chip({ on, onClick, children }: { on: boolean; onClick: () => void; children: ReactNode }) { + const tone = on ? 'border-base-blue text-base-blue' : 'border-bds-gray-20 text-bds-gray-60'; + return ; +} From b415fdf23032fb99ba4545f20509b17afca95a3a Mon Sep 17 00:00:00 2001 From: Francis Li Date: Fri, 16 Oct 2026 05:57:23 -0700 Subject: [PATCH 8/9] Move formatTransactions to the run data layer Co-authored-by: Toshi --- app/benchmark/components/ConfigCard.test.ts | 3 ++- app/benchmark/components/ConfigCard.tsx | 26 +-------------------- app/scaling/runs/lib/formatters.tsx | 26 ++++++++++++++++++++- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/app/benchmark/components/ConfigCard.test.ts b/app/benchmark/components/ConfigCard.test.ts index 5c7c7a5..b654569 100644 --- a/app/benchmark/components/ConfigCard.test.ts +++ b/app/benchmark/components/ConfigCard.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; -import { buildRows, formatTransactions } from "./ConfigCard"; +import { buildRows } from "./ConfigCard"; +import { formatTransactions } from "../../scaling/runs/lib/formatters"; import { LoadTestConfig } from "../../scaling/runs/lib/types"; describe("formatTransactions", () => { diff --git a/app/benchmark/components/ConfigCard.tsx b/app/benchmark/components/ConfigCard.tsx index 93ccb73..3ee72eb 100644 --- a/app/benchmark/components/ConfigCard.tsx +++ b/app/benchmark/components/ConfigCard.tsx @@ -1,5 +1,5 @@ import { LoadTestConfig } from "../../scaling/runs/lib/types"; -import { formatEthFromWeiString } from "../../scaling/runs/lib/formatters"; +import { formatEthFromWeiString, formatTransactions } from "../../scaling/runs/lib/formatters"; import StatCard from "./StatCard"; interface ConfigCardProps { @@ -11,30 +11,6 @@ interface Row { value: string; } -const percent = (value: number): string => `${Math.round(value * 100)}%`; - -export const formatTransactions = ( - config: Pick, -): string => { - const txs = config.transactions; - if (!txs || txs.length === 0) return "—"; - const total = txs.reduce((acc, t) => acc + t.weight, 0); - if (total === 0) return txs.map((t) => t.type).join(" · "); - - const freshRecipientRatio = config.fresh_recipient_ratio ?? 0; - return txs - .map((t) => { - if (t.type === "transfer" && freshRecipientRatio >= 1) { - return `account-create (${percent(t.weight / total)})`; - } - if (t.type === "transfer" && freshRecipientRatio > 0) { - return `${t.type} (${percent(t.weight / total)}, ${percent(freshRecipientRatio)} account-create)`; - } - return `${t.type} (${percent(t.weight / total)})`; - }) - .join(" · "); -}; - const formatTargetGps = (gps: number): string => { if (gps >= 1e9) return `${(gps / 1e9).toFixed(1)}B gas/s`; if (gps >= 1e6) return `${(gps / 1e6).toFixed(0)}M gas/s`; diff --git a/app/scaling/runs/lib/formatters.tsx b/app/scaling/runs/lib/formatters.tsx index a69a18e..73b8f4a 100644 --- a/app/scaling/runs/lib/formatters.tsx +++ b/app/scaling/runs/lib/formatters.tsx @@ -1,4 +1,4 @@ -import { ChartConfig, RustDuration } from "./types"; +import { ChartConfig, LoadTestConfig, RustDuration } from "./types"; const PREFIXES = { "": 1, @@ -227,3 +227,27 @@ export const camelToTitleCase = (str: string) => { .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); }; + +const percent = (value: number): string => `${Math.round(value * 100)}%`; + +export const formatTransactions = ( + config: Pick, +): string => { + const txs = config.transactions; + if (!txs || txs.length === 0) return "—"; + const total = txs.reduce((acc, t) => acc + t.weight, 0); + if (total === 0) return txs.map((t) => t.type).join(" · "); + + const freshRecipientRatio = config.fresh_recipient_ratio ?? 0; + return txs + .map((t) => { + if (t.type === "transfer" && freshRecipientRatio >= 1) { + return `account-create (${percent(t.weight / total)})`; + } + if (t.type === "transfer" && freshRecipientRatio > 0) { + return `${t.type} (${percent(t.weight / total)}, ${percent(freshRecipientRatio)} account-create)`; + } + return `${t.type} (${percent(t.weight / total)})`; + }) + .join(" · "); +}; From 26ba767b9539661002e41a476a61c620aa0c2cb0 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Fri, 16 Oct 2026 05:57:23 -0700 Subject: [PATCH 9/9] Show load-test mixes as normalized shares Co-authored-by: Toshi --- app/scaling/runs/fixtures/synthetic.json | 2 +- app/scaling/runs/lib/unified.test.ts | 2 +- app/scaling/runs/lib/unified.ts | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/scaling/runs/fixtures/synthetic.json b/app/scaling/runs/fixtures/synthetic.json index 5456a58..2312e37 100644 --- a/app/scaling/runs/fixtures/synthetic.json +++ b/app/scaling/runs/fixtures/synthetic.json @@ -44,7 +44,7 @@ "config": { "funding_amount": "1000000000000000000", "sender_count": 10, "sender_offset": 0, "in_flight_per_sender": 4, "duration": "30s", "target_gps": 15000000, "seed": 1, "chain_id": null, - "transactions": [{ "type": "transfer", "weight": 70 }, { "type": "swap", "weight": 30 }], + "transactions": [{ "type": "transfer", "weight": 3 }, { "type": "swap", "weight": 1 }], "looper_contract": null, "swap_token_amount": "0" } } diff --git a/app/scaling/runs/lib/unified.test.ts b/app/scaling/runs/lib/unified.test.ts index 08ea38f..04b4d20 100644 --- a/app/scaling/runs/lib/unified.test.ts +++ b/app/scaling/runs/lib/unified.test.ts @@ -37,7 +37,7 @@ it('maps a load test as a burst with its mix, observed gas/s and p99 latencies', timestamp: Date.UTC(2026, 0, 8, 0, 0, 1), duration: 30, profile: 'burst', - workload: '70% transfer · 30% swap · target 15 Mgas/s', + workload: 'transfer (75%) · swap (25%) · target 15 Mgas/s', gasPerSecond: 12.5e6, latency: [{ label: 'block p99', seconds: 3 }, { label: 'flashblocks p99', seconds: 0.8 }], status: 'success', diff --git a/app/scaling/runs/lib/unified.ts b/app/scaling/runs/lib/unified.ts index 05daa6c..5d71fea 100644 --- a/app/scaling/runs/lib/unified.ts +++ b/app/scaling/runs/lib/unified.ts @@ -1,4 +1,4 @@ -import { durationToMs, parseLoadTestTimestamp } from './formatters'; +import { durationToMs, formatTransactions, parseLoadTestTimestamp } from './formatters'; import type { BenchmarkRunWithStatus, LoadTestEntry, LoadTestResult, RunStatus, RustDuration } from './types'; export type LoadRun = LoadTestEntry & { result?: LoadTestResult }; @@ -51,7 +51,6 @@ function fromBench(run: BenchmarkRunWithStatus): UnifiedRun { function fromLoad({ network, timestamp, result }: LoadRun): UnifiedRun { const config = result?.config; - const mix = config?.transactions.map((t) => `${t.weight}% ${t.type}`).join(' · '); const target = config ? `target ${config.target_gps / 1e6} Mgas/s` : undefined; return withQualifies({ kind: 'load', @@ -62,7 +61,7 @@ function fromLoad({ network, timestamp, result }: LoadRun): UnifiedRun { commit: null, machine: null, duration: result?.throughput ? secs(result.throughput.duration) : null, - workload: [mix, target].filter(Boolean).join(' · ') || '—', + workload: [config && formatTransactions(config), target].filter(Boolean).join(' · ') || '—', gasPerSecond: result?.throughput?.gps ?? null, latency: result?.block_latency ? [{ label: 'block p99', seconds: secs(result.block_latency.p99) }, { label: 'flashblocks p99', seconds: secs(result.flashblocks_latency.p99) }] : [], status: result ? (result.error ? 'fatal' : 'success') : null,
    - {g.label} {g.help} + {detailed ? <>{g.label}

    {g.help}

    : ( + {g.label} {g.help} + )}
    {g.value === null ? '—' : fmt(g.value, g.unit)} diff --git a/app/scaling/components/SectionTable.tsx b/app/scaling/components/SectionTable.tsx new file mode 100644 index 0000000..48c49f3 --- /dev/null +++ b/app/scaling/components/SectionTable.tsx @@ -0,0 +1,56 @@ +import { cn } from '../../components/ui/cn'; +import { Card } from '../../components/ui/Card'; +import { Text } from '../../components/ui/Text'; +import { formatTime } from '../loadSnapshot'; +import type { Section, Snapshot } from '../snapshot.mjs'; + +const TH = 'px-4 py-2 text-left text-[12px] font-[500] text-bds-gray-60'; +const TD = 'px-4 py-3 align-top'; +const STATUS: Record = { + observed: 'Observed', partial: 'Partial', unavailable: 'Unavailable', 'not-measured': 'Not measured', +}; + +export function SectionTable({ section, windows }: { section: Section; windows: Snapshot['windows'] }) { + return ( +
    + {section.title} + {section.summary} + + + + + {['Metric', 'Value', 'Status', 'Window', 'Query', 'Source'].map((h) => )} + + + + {section.metrics.map((m) => { + const window = m.window ? windows[m.window] : null; + return ( + + + + + + + + + ); + })} + +
    {h}
    {m.label}

    {m.note}

    + {m.value === null ? '—' : `${m.value.toLocaleString('en-US', { maximumFractionDigits: 2 })} ${m.unit}`.trim()} + {STATUS[m.status]}{window ? <>{window.label}

    {formatTime(window.start)} → {formatTime(window.end)}

    : '—'}
    {m.query ? {m.query} : '—'} + {m.source ? {m.source.label} : '—'} +
    +
    + {section.gaps.length > 0 ? ( +
    + Missing evidence +
      + {section.gaps.map((gap) =>
    • {gap}
    • )} +
    +
    + ) : null} +
    + ); +} diff --git a/app/scaling/fixtures/synthetic.v2.json b/app/scaling/fixtures/synthetic.v2.json index 7cae772..d38a9e9 100644 --- a/app/scaling/fixtures/synthetic.v2.json +++ b/app/scaling/fixtures/synthetic.v2.json @@ -179,6 +179,15 @@ { "label": "Confirmed drain", "status": "not-measured", "value": null, "unit": "KB/s", "window": null, "note": "No query yet." } ], "gaps": ["Confirmed drain"] + }, + { + "id": "outcomes", + "title": "User outcomes", + "summary": "Synthetic user-facing results.", + "metrics": [ + { "label": "Inclusion latency p50", "status": "observed", "value": 1.8, "unit": "s", "window": "day", "note": "Invented.", "query": "synthetic.inclusion_latency" } + ], + "gaps": [] } ], "work": [ diff --git a/app/scaling/guardrails/page.tsx b/app/scaling/guardrails/page.tsx new file mode 100644 index 0000000..79e8154 --- /dev/null +++ b/app/scaling/guardrails/page.tsx @@ -0,0 +1,20 @@ +import { Guardrails } from '../components/Guardrails'; +import { SectionTable } from '../components/SectionTable'; +import { Header, loadSnapshot, Unavailable } from '../loadSnapshot'; + +export const dynamic = 'force-dynamic'; + +export default async function ScalingGuardrailsPage() { + const loaded = await loadSnapshot(); + if (loaded.kind !== 'ok') return ; + + const { snapshot } = loaded; + const outcomes = snapshot.sections.find((s) => s.id === 'outcomes'); + return ( +
    +
    + + {outcomes ? : null} +
    + ); +} diff --git a/app/scaling/loadSnapshot.tsx b/app/scaling/loadSnapshot.tsx new file mode 100644 index 0000000..a0cf43b --- /dev/null +++ b/app/scaling/loadSnapshot.tsx @@ -0,0 +1,71 @@ +import { readFile } from 'node:fs/promises'; +import Link from 'next/link'; +import type { ReactNode } from 'react'; + +import { Banner } from '../components/ui/Banner'; +import { EmptyState } from '../components/ui/EmptyState'; +import { Text } from '../components/ui/Text'; + +import { isStale, parseSnapshot, type Snapshot } from './snapshot.mjs'; + +const STALE_AFTER_DAYS = 10; +const DAY_MS = 86_400_000; + +export type Loaded = + | { kind: 'unconfigured' } + | { kind: 'invalid'; errors: string[] } + | { kind: 'ok'; snapshot: Snapshot; ageDays: number; stale: boolean }; + +export async function loadSnapshot(): Promise { + const path = process.env.SCALING_SNAPSHOT_PATH; + if (!path) return { kind: 'unconfigured' }; + try { + const result = parseSnapshot(JSON.parse(await readFile(path, 'utf8'))); + if (!result.ok) return { kind: 'invalid', errors: result.errors }; + const { collectedAt } = result.snapshot; + const ageDays = Math.floor((Date.now() - Date.parse(collectedAt)) / DAY_MS); + return { kind: 'ok', snapshot: result.snapshot, ageDays, stale: isStale(collectedAt, new Date(), STALE_AFTER_DAYS) }; + } catch (error) { + return { kind: 'invalid', errors: [error instanceof Error ? error.message : String(error)] }; + } +} + +export function Unavailable({ loaded }: { loaded: Exclude }) { + if (loaded.kind === 'unconfigured') { + return ( + + ); + } + return ( + {loaded.errors.join('\n')}} + /> + ); +} + +export function Header({ loaded, title, crumb }: { loaded: Extract; title: ReactNode; crumb?: string }) { + const { snapshot, ageDays, stale } = loaded; + return ( +
    + {crumb ? ( + Scaling / {crumb} + ) : null} + {title} + + As of {formatTime(snapshot.collectedAt)} ({ageDays === 0 ? 'today' : `${ageDays}d old`}) · manually refreshed + snapshot, not live data. {snapshot.context} + + {stale ? ( + Stale: this snapshot is older than {STALE_AFTER_DAYS} days. Refresh it before using these numbers. + ) : null} +
    + ); +} + +export function formatTime(iso: string): string { + return `${new Date(iso).toISOString().slice(0, 16).replace('T', ' ')} UTC`; +} diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 75068a7..60cf2c9 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -1,14 +1,32 @@ import { fileURLToPath } from 'node:url'; import { renderToStaticMarkup } from 'react-dom/server'; -import { expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, expect, it, vi } from 'vitest'; +import fixture from './fixtures/synthetic.v2.json'; +import ScalingGuardrailsPage from './guardrails/page'; import ScalingPage from './page'; +beforeEach(() => vi.stubEnv('SCALING_SNAPSHOT_PATH', fileURLToPath(new URL('./fixtures/synthetic.v2.json', import.meta.url)))); +afterEach(() => vi.unstubAllEnvs()); + it('collapses each detail section by default with its title and row count', async () => { - vi.stubEnv('SCALING_SNAPSHOT_PATH', fileURLToPath(new URL('./fixtures/synthetic.v2.json', import.meta.url))); const html = renderToStaticMarkup(await ScalingPage()); expect(html).toMatch(/
    ]*>]*>Data availability<\/h2>]*> · 2 rows<\/span>/); expect(html).not.toContain('
    { + expect(renderToStaticMarkup(await ScalingPage())).toContain('Guardrails →'); +}); + +it('shows guardrail help inline and the outcomes table on the guardrails page', async () => { + const html = renderToStaticMarkup(await ScalingGuardrailsPage()); + expect(html).toContain('href="/scaling">Scaling / Guardrails'); + expect(html).toContain('As of 2026-01-02 00:00 UTC'); + for (const g of fixture.guardrails) expect(html).toContain(g.help); + expect(html).not.toContain('Guardrails →'); + expect(html).toContain('Inclusion latency p50'); + expect(html).toContain('synthetic.inclusion_latency'); + expect(html).toContain('2026-01-01 00:00 UTC → 2026-01-02 00:00 UTC'); }); diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index e12034c..be99bb0 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -1,74 +1,26 @@ -import { readFile } from 'node:fs/promises'; import Link from 'next/link'; -import { Banner } from '../components/ui/Banner'; import { Card } from '../components/ui/Card'; -import { EmptyState } from '../components/ui/EmptyState'; import { Text } from '../components/ui/Text'; import { CeilingCards } from './components/CeilingCards'; import { Guardrails } from './components/Guardrails'; import { Ladder } from './components/Ladder'; import { Progress } from './components/Progress'; -import { isStale, parseSnapshot, type Metric, type Snapshot } from './snapshot.mjs'; +import { formatTime, Header, loadSnapshot, Unavailable } from './loadSnapshot'; +import type { Metric, Snapshot } from './snapshot.mjs'; // The snapshot is a runtime file, so render per request rather than at build. export const dynamic = 'force-dynamic'; -const STALE_AFTER_DAYS = 10; -const DAY_MS = 86_400_000; - -type Loaded = - | { kind: 'unconfigured' } - | { kind: 'invalid'; errors: string[] } - | { kind: 'ok'; snapshot: Snapshot; ageDays: number; stale: boolean }; - -async function loadSnapshot(): Promise { - const path = process.env.SCALING_SNAPSHOT_PATH; - if (!path) return { kind: 'unconfigured' }; - try { - const result = parseSnapshot(JSON.parse(await readFile(path, 'utf8'))); - if (!result.ok) return { kind: 'invalid', errors: result.errors }; - const { collectedAt } = result.snapshot; - const ageDays = Math.floor((Date.now() - Date.parse(collectedAt)) / DAY_MS); - return { kind: 'ok', snapshot: result.snapshot, ageDays, stale: isStale(collectedAt, new Date(), STALE_AFTER_DAYS) }; - } catch (error) { - return { kind: 'invalid', errors: [error instanceof Error ? error.message : String(error)] }; - } -} - export default async function ScalingPage() { const loaded = await loadSnapshot(); - if (loaded.kind === 'unconfigured') { - return ( - - ); - } - if (loaded.kind === 'invalid') { - return ( - {loaded.errors.join('\n')}} - /> - ); - } + if (loaded.kind !== 'ok') return ; - const { snapshot, ageDays, stale } = loaded; + const { snapshot } = loaded; return (
    -
    - Scaling scorecard · {snapshot.network} - - As of {formatTime(snapshot.collectedAt)} ({ageDays === 0 ? 'today' : `${ageDays}d old`}) · manually refreshed - snapshot, not live data. {snapshot.context} - - {stale ? ( - Stale: this snapshot is older than {STALE_AFTER_DAYS} days. Refresh it before using these numbers. - ) : null} -
    +
    @@ -160,7 +112,3 @@ function ExternalLink({ link }: { link: { label: string; url: string } }) { ); } - -function formatTime(iso: string): string { - return `${new Date(iso).toISOString().slice(0, 16).replace('T', ' ')} UTC`; -} diff --git a/app/scaling/snapshot.mjs b/app/scaling/snapshot.mjs index 465e9e3..24c515f 100644 --- a/app/scaling/snapshot.mjs +++ b/app/scaling/snapshot.mjs @@ -15,7 +15,7 @@ * label: string, status: MetricStatus, value: number | null, unit: string, * window: string | null, note: string, query?: string, source?: Link * }} Metric - * @typedef {{ id: string, title: string, summary: string, metrics: Metric[], gaps: string[] }} Section + * @typedef {{ id: Area | 'outcomes', title: string, summary: string, metrics: Metric[], gaps: string[] }} Section * @typedef {{ value: number | null, unit: 'Mgas/s', tag: Tag, window: string | null, source?: Link, help: string }} Rung * @typedef {{ observed: { p50: Rung, p99: Rung }, operatingPoint: Rung, configuredLimit: Rung, qualified: Rung }} Ladder * @typedef {{ @@ -53,6 +53,8 @@ const STATUSES = ['observed', 'partial', 'unavailable', 'not-measured']; const TAGS = ['measured', 'modeled', 'estimated', 'not-measured', 'not-modeled', 'not-established']; const VALUED_TAGS = ['measured', 'modeled', 'estimated']; const AREAS = ['da', 'execution', 'state', 'networking']; +// Each section renders on its area page, or on the guardrails page for outcomes. +const SECTION_IDS = [...AREAS, 'outcomes']; const INPUT_STATUSES = ['observed', 'configured', 'provisional', 'pending']; const DAY_MS = 86_400_000; const V1_MESSAGE = @@ -216,7 +218,8 @@ export function parseSnapshot(data) { list(data, 'sections', 'snapshot').forEach((s, i) => { const path = `sections[${i}]`; if (!isObj(s)) return fail(path, 'expected object'); - for (const key of ['id', 'title', 'summary']) str(s, key, path); + for (const key of ['title', 'summary']) str(s, key, path); + oneOf(s, 'id', path, SECTION_IDS); if (ids.has(s.id)) fail(`${path}.id`, 'duplicate'); ids.add(s.id); list(s, 'metrics', path).forEach((m, j) => metric(m, `${path}.metrics[${j}]`)); diff --git a/app/scaling/snapshot.test.ts b/app/scaling/snapshot.test.ts index 86d344e..24cdd43 100644 --- a/app/scaling/snapshot.test.ts +++ b/app/scaling/snapshot.test.ts @@ -67,6 +67,12 @@ describe('parseSnapshot', () => { expect(errorsOf(data)).toEqual(['snapshot.ceilings: missing networking', 'guardrails[1].id: duplicate']); }); + it('rejects a section id without a page', () => { + const data = fixture(); + data.sections[1].id = 'networking-extra'; + expect(errorsOf(data)).toEqual(['sections[1].id: expected one of da, execution, state, networking, outcomes']); + }); + it('rejects a guardrail status that disagrees with its value and threshold', () => { const data = fixture(); data.guardrails[0].status = 'breached'; From 04ba0710ae9d5c5cf6454b33642cde618978ba0b Mon Sep 17 00:00:00 2001 From: Francis Li Date: Fri, 16 Oct 2026 05:40:56 -0700 Subject: [PATCH 4/9] feat(scaling): per-area detail pages Co-authored-by: Toshi --- .github/workflows/ci.yml | 2 +- app/scaling/[area]/page.tsx | 31 ++++++++++ app/scaling/components/CeilingCards.tsx | 48 ++++++++------- app/scaling/fixtures/synthetic.v2.json | 26 ++++++++ app/scaling/page.test.tsx | 36 ++++++++++- app/scaling/page.tsx | 81 +------------------------ 6 files changed, 117 insertions(+), 107 deletions(-) create mode 100644 app/scaling/[area]/page.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08553f3..9d52e8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,7 +165,7 @@ jobs: /internal-explorer/bundles/0x1 /api/internal-explorer/blocks \ /tips /tips/block/0x1 /tips/bundles/0x1 /api/tips/blocks \ /benchmark /benchmark/run/latest /benchmark/run-comparison/1 \ - /benchmark/load-tests/sepolia /scaling /scaling/guardrails; do + /benchmark/load-tests/sepolia /scaling /scaling/da /scaling/guardrails; do code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:3000${route}") if [ "${code}" != "404" ]; then echo "FAIL: ${route} returned ${code}, expected 404" diff --git a/app/scaling/[area]/page.tsx b/app/scaling/[area]/page.tsx new file mode 100644 index 0000000..16548dc --- /dev/null +++ b/app/scaling/[area]/page.tsx @@ -0,0 +1,31 @@ +import { notFound } from 'next/navigation'; + +import { EmptyState } from '../../components/ui/EmptyState'; +import { AREAS, CeilingCard } from '../components/CeilingCards'; +import { SectionTable } from '../components/SectionTable'; +import { Header, loadSnapshot, Unavailable } from '../loadSnapshot'; + +export const dynamic = 'force-dynamic'; + +export default async function ScalingAreaPage({ params }: { params: Promise<{ area: string }> }) { + const { area } = await params; + const entry = AREAS.find(([a]) => a === area); + if (!entry) notFound(); + const loaded = await loadSnapshot(); + if (loaded.kind !== 'ok') return ; + + const { snapshot } = loaded; + const section = snapshot.sections.find((s) => s.id === area); + return ( +
    +
    + + {section ? ( + + ) : ( + + )} + +
    + ); +} diff --git a/app/scaling/components/CeilingCards.tsx b/app/scaling/components/CeilingCards.tsx index 8188204..15c1cdc 100644 --- a/app/scaling/components/CeilingCards.tsx +++ b/app/scaling/components/CeilingCards.tsx @@ -6,7 +6,7 @@ import { deriveBinding, type Ceiling, type CeilingInput } from '../snapshot.mjs' import { Info } from './Info'; import { Tag } from './Tag'; -const AREAS: [Ceiling['area'], string][] = [['da', 'DA'], ['execution', 'Execution'], ['state', 'State'], ['networking', 'Networking']]; +export const AREAS: [Ceiling['area'], string][] = [['da', 'DA'], ['execution', 'Execution'], ['state', 'State'], ['networking', 'Networking']]; function input({ name, value, unit, status, asOf }: CeilingInput): string { const shown = value === null ? status : `${value} ${unit}`.trim(); @@ -14,33 +14,35 @@ function input({ name, value, unit, status, asOf }: CeilingInput): string { } export function CeilingCards({ ceilings }: { ceilings: Ceiling[] }) { - const binding = deriveBinding(ceilings); return (
    Ceilings by bottleneck · Mgas/s
    - {AREAS.map(([area, name]) => { - const c = ceilings.find((x) => x.area === area)!; - return ( - - - {name} - {c.help} - - - {c.value === null ? '—' : c.value.toLocaleString('en-US', { maximumFractionDigits: 2 })} - - {c === binding ? Binding : null} - - {c.formula} - {c.reason ?

    {c.reason}

    : null} -
      - {c.inputs.map((i) =>
    • {input(i)}
    • )} -
    -
    - ); - })} + {AREAS.map(([area]) => )}
    ); } + +export function CeilingCard({ area, ceilings }: { area: Ceiling['area']; ceilings: Ceiling[] }) { + const name = AREAS.find(([a]) => a === area)![1]; + const c = ceilings.find((x) => x.area === area)!; + return ( + + + {name} + {c.help} + + + {c.value === null ? '—' : c.value.toLocaleString('en-US', { maximumFractionDigits: 2 })} + + {c === deriveBinding(ceilings) ? Binding : null} + + {c.formula} + {c.reason ?

    {c.reason}

    : null} +
      + {c.inputs.map((i) =>
    • {input(i)}
    • )} +
    +
    + ); +} diff --git a/app/scaling/fixtures/synthetic.v2.json b/app/scaling/fixtures/synthetic.v2.json index d38a9e9..2229a46 100644 --- a/app/scaling/fixtures/synthetic.v2.json +++ b/app/scaling/fixtures/synthetic.v2.json @@ -180,6 +180,32 @@ ], "gaps": ["Confirmed drain"] }, + { + "id": "execution", + "title": "Execution", + "summary": "Synthetic block-building observations.", + "metrics": [ + { + "label": "Block build time p90", + "status": "partial", + "value": 310, + "unit": "ms", + "window": "week", + "note": "Invented; sampled from one synthetic sequencer.", + "source": { "label": "Synthetic dashboard", "url": "https://example.com/synthetic/build-time" } + } + ], + "gaps": [] + }, + { + "id": "state", + "title": "State", + "summary": "Synthetic state-size observations.", + "metrics": [ + { "label": "Live state growth", "status": "unavailable", "value": null, "unit": "GB/day", "window": "week", "note": "Synthetic query timed out." } + ], + "gaps": ["Live state growth"] + }, { "id": "outcomes", "title": "User outcomes", diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 60cf2c9..c235cc3 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url'; import { renderToStaticMarkup } from 'react-dom/server'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; +import ScalingAreaPage from './[area]/page'; import fixture from './fixtures/synthetic.v2.json'; import ScalingGuardrailsPage from './guardrails/page'; import ScalingPage from './page'; @@ -10,10 +11,39 @@ import ScalingPage from './page'; beforeEach(() => vi.stubEnv('SCALING_SNAPSHOT_PATH', fileURLToPath(new URL('./fixtures/synthetic.v2.json', import.meta.url)))); afterEach(() => vi.unstubAllEnvs()); -it('collapses each detail section by default with its title and row count', async () => { +const area = async (id: string) => renderToStaticMarkup(await ScalingAreaPage({ params: Promise.resolve({ area: id }) })); + +it('links the first screen to five detail pages instead of listing metric rows', async () => { const html = renderToStaticMarkup(await ScalingPage()); - expect(html).toMatch(/
    ]*>]*>Data availability<\/h2>]*> · 2 rows<\/span>/); - expect(html).not.toContain('
    { + for (const section of fixture.sections) { + const html = section.id === 'outcomes' ? renderToStaticMarkup(await ScalingGuardrailsPage()) : await area(section.id); + for (const m of section.metrics) expect(html).toContain(m.label); + } +}); + +it('renders an area with breadcrumb, as-of header, ceiling, metric details, gaps and a runs slot', async () => { + const html = await area('da'); + expect(html).toContain('href="/scaling">Scaling / DA'); + expect(html).toContain('As of 2026-01-02 00:00 UTC'); + expect(html).toContain('da_bytes_per_gas · 0.0055 bytes/gas · observed · 2026-01-02'); + expect(html).toContain('synthetic.blob_utilization'); + expect(html).toContain('
  • Confirmed drain
  • '); + expect(html).toContain('Related runs'); +}); + +it('shows the ceiling and an empty state for an area without metrics', async () => { + const html = await area('networking'); + expect(html).toContain('gossip and peer limits unknown'); + expect(html).toContain('No detailed metrics yet'); +}); + +it('404s an unknown area', async () => { + await expect(area('bogus')).rejects.toMatchObject({ digest: 'NEXT_HTTP_ERROR_FALLBACK;404' }); }); it('links the first-screen guardrail strip to the guardrails page', async () => { diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index be99bb0..9889be5 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -1,14 +1,12 @@ import Link from 'next/link'; -import { Card } from '../components/ui/Card'; import { Text } from '../components/ui/Text'; import { CeilingCards } from './components/CeilingCards'; import { Guardrails } from './components/Guardrails'; import { Ladder } from './components/Ladder'; import { Progress } from './components/Progress'; -import { formatTime, Header, loadSnapshot, Unavailable } from './loadSnapshot'; -import type { Metric, Snapshot } from './snapshot.mjs'; +import { Header, loadSnapshot, Unavailable } from './loadSnapshot'; // The snapshot is a runtime file, so render per request rather than at build. export const dynamic = 'force-dynamic'; @@ -28,35 +26,6 @@ export default async function ScalingPage() { - {snapshot.sections.map((section) => ( -
    - - {section.title} - · {section.metrics.length} {section.metrics.length === 1 ? 'row' : 'rows'} - {section.summary} - - - {section.metrics.map((m) => ( -
    -
    - {m.label} - -
    - -
    - ))} -
    - {section.gaps.length > 0 ? ( -
    - Missing evidence -
      - {section.gaps.map((gap) =>
    • {gap}
    • )} -
    -
    - ) : null} -
    - ))} - Experiment results: Benchmarks ·{' '} Load tests @@ -64,51 +33,3 @@ export default async function ScalingPage() {
    ); } - -const STATUS_LABEL: Record = { - observed: '', partial: 'Partial', unavailable: 'Unavailable', 'not-measured': 'Not measured', -}; - -function Value({ metric }: { metric: Metric }) { - if (metric.value === null) { - return ( - - {STATUS_LABEL[metric.status]} - - ); - } - return ( - - - {metric.value.toLocaleString('en-US', { maximumFractionDigits: 2 })} - {metric.unit ? {metric.unit} : null} - - {metric.status === 'partial' ? Partial : null} - - ); -} - -function MetricDetails({ metric, snapshot }: { metric: Metric; snapshot: Snapshot }) { - const window = metric.window ? snapshot.windows[metric.window] : null; - return ( -
    - - {window ? `${window.label} · details` : metric.value === null ? 'No measurement yet · details' : 'Details'} - -
    - {window ?

    {formatTime(window.start)} → {formatTime(window.end)}

    : null} -

    {metric.note}

    - {metric.query ? {metric.query} : null} - {metric.source ? : null} -
    -
    - ); -} - -function ExternalLink({ link }: { link: { label: string; url: string } }) { - return ( - - {link.label} - - ); -} From 481938165bb7179104717977bcfbd4dc573db8db Mon Sep 17 00:00:00 2001 From: Francis Li Date: Fri, 16 Oct 2026 05:44:37 -0700 Subject: [PATCH 5/9] Label the detail-page ceiling in Mgas/s Co-authored-by: Toshi --- app/scaling/[area]/page.tsx | 6 +++++- app/scaling/page.test.tsx | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/scaling/[area]/page.tsx b/app/scaling/[area]/page.tsx index 16548dc..1e7afbc 100644 --- a/app/scaling/[area]/page.tsx +++ b/app/scaling/[area]/page.tsx @@ -1,6 +1,7 @@ import { notFound } from 'next/navigation'; import { EmptyState } from '../../components/ui/EmptyState'; +import { Text } from '../../components/ui/Text'; import { AREAS, CeilingCard } from '../components/CeilingCards'; import { SectionTable } from '../components/SectionTable'; import { Header, loadSnapshot, Unavailable } from '../loadSnapshot'; @@ -19,7 +20,10 @@ export default async function ScalingAreaPage({ params }: { params: Promise<{ ar return (
    - +
    + Ceiling · Mgas/s + +
    {section ? ( ) : ( diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index c235cc3..2fd3346 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -30,6 +30,7 @@ it('renders an area with breadcrumb, as-of header, ceiling, metric details, gaps const html = await area('da'); expect(html).toContain('href="/scaling">Scaling / DA'); expect(html).toContain('As of 2026-01-02 00:00 UTC'); + expect(html).toMatch(/>Ceiling ]*>· Mgas\/s<\/span>/); expect(html).toContain('da_bytes_per_gas · 0.0055 bytes/gas · observed · 2026-01-02'); expect(html).toContain('synthetic.blob_utilization'); expect(html).toContain('
  • Confirmed drain
  • '); From a1e6b58aea6f7e33491a2a5fa7bd90055f06f2d5 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Fri, 16 Oct 2026 05:49:28 -0700 Subject: [PATCH 6/9] refactor(benchmark): move run data layer to scaling/runs Co-authored-by: Toshi --- app/benchmark/components/BaseChart.tsx | 2 +- app/benchmark/components/BenchmarkToolbar.tsx | 2 +- app/benchmark/components/ChartGrid.tsx | 4 ++-- app/benchmark/components/ChartSelector.tsx | 4 ++-- app/benchmark/components/ConfigCard.test.ts | 2 +- app/benchmark/components/ConfigCard.tsx | 4 ++-- app/benchmark/components/ConfigurationTags.tsx | 4 ++-- app/benchmark/components/LineChart.tsx | 4 ++-- app/benchmark/components/MachineInfo.tsx | 2 +- app/benchmark/components/RunList.tsx | 4 ++-- app/benchmark/components/RunListFilter.tsx | 4 ++-- app/benchmark/components/ThroughputChart.tsx | 4 ++-- app/benchmark/filter.test.ts | 2 +- app/benchmark/hooks/useBenchmarkFilters.ts | 2 +- app/benchmark/utils/downloadUtils.ts | 2 +- app/benchmark/utils/useDataSeries.ts | 4 ++-- app/benchmark/views/LoadTestAllRuns.tsx | 2 +- app/benchmark/views/LoadTestDetail.tsx | 4 ++-- app/benchmark/views/RunComparison.tsx | 2 +- app/benchmark/views/RunIndex.tsx | 2 +- app/{benchmark/services => scaling/runs/lib}/dataService.ts | 2 +- app/{benchmark/utils => scaling/runs/lib}/formatters.test.ts | 0 app/{benchmark/utils => scaling/runs/lib}/formatters.tsx | 2 +- app/{benchmark => scaling/runs/lib}/metricDefinitions.ts | 0 app/{benchmark => scaling/runs/lib}/types.ts | 0 25 files changed, 32 insertions(+), 32 deletions(-) rename app/{benchmark/services => scaling/runs/lib}/dataService.ts (99%) rename app/{benchmark/utils => scaling/runs/lib}/formatters.test.ts (100%) rename app/{benchmark/utils => scaling/runs/lib}/formatters.tsx (99%) rename app/{benchmark => scaling/runs/lib}/metricDefinitions.ts (100%) rename app/{benchmark => scaling/runs/lib}/types.ts (100%) diff --git a/app/benchmark/components/BaseChart.tsx b/app/benchmark/components/BaseChart.tsx index 934b68f..15b1c52 100644 --- a/app/benchmark/components/BaseChart.tsx +++ b/app/benchmark/components/BaseChart.tsx @@ -1,6 +1,6 @@ import React, { useRef, useEffect, useState } from "react"; import * as d3 from "d3"; -import { MetricData } from "../types"; +import { MetricData } from "../../scaling/runs/lib/types"; interface BaseChartProps { data: MetricData[]; diff --git a/app/benchmark/components/BenchmarkToolbar.tsx b/app/benchmark/components/BenchmarkToolbar.tsx index 6ca7b67..0f2c79b 100644 --- a/app/benchmark/components/BenchmarkToolbar.tsx +++ b/app/benchmark/components/BenchmarkToolbar.tsx @@ -5,7 +5,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useLoadTestList, useTestMetadata } from "../utils/useDataSeries"; import { uniqBy } from "../utils/collections"; -import { formatLoadTestTimestamp } from "../utils/formatters"; +import { formatLoadTestTimestamp } from "../../scaling/runs/lib/formatters"; import { loadTestHref } from "../routes"; import Select from "./Select"; diff --git a/app/benchmark/components/ChartGrid.tsx b/app/benchmark/components/ChartGrid.tsx index fe4596e..0a0042f 100644 --- a/app/benchmark/components/ChartGrid.tsx +++ b/app/benchmark/components/ChartGrid.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { SORTED_CHART_CONFIG } from "../metricDefinitions"; -import { DataSeries } from "../types"; +import { SORTED_CHART_CONFIG } from "../../scaling/runs/lib/metricDefinitions"; +import { DataSeries } from "../../scaling/runs/lib/types"; import LineChart from "./LineChart"; interface ProvidedProps { diff --git a/app/benchmark/components/ChartSelector.tsx b/app/benchmark/components/ChartSelector.tsx index 585e88b..cc465fe 100644 --- a/app/benchmark/components/ChartSelector.tsx +++ b/app/benchmark/components/ChartSelector.tsx @@ -1,13 +1,13 @@ "use client"; import { useEffect, useMemo, useRef } from "react"; -import { BenchmarkRuns } from "../types"; +import { BenchmarkRuns } from "../../scaling/runs/lib/types"; import { deepEqual } from "../utils/collections"; import { camelToTitleCase, formatValue, formatLabel, -} from "../utils/formatters"; +} from "../../scaling/runs/lib/formatters"; import { interpolateWarm } from "d3"; import { useBenchmarkFilters } from "../hooks/useBenchmarkFilters"; import Link from "next/link"; diff --git a/app/benchmark/components/ConfigCard.test.ts b/app/benchmark/components/ConfigCard.test.ts index 38a3eac..5c7c7a5 100644 --- a/app/benchmark/components/ConfigCard.test.ts +++ b/app/benchmark/components/ConfigCard.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { buildRows, formatTransactions } from "./ConfigCard"; -import { LoadTestConfig } from "../types"; +import { LoadTestConfig } from "../../scaling/runs/lib/types"; describe("formatTransactions", () => { it("formats a weighted transaction mix", () => { diff --git a/app/benchmark/components/ConfigCard.tsx b/app/benchmark/components/ConfigCard.tsx index 8055b92..93ccb73 100644 --- a/app/benchmark/components/ConfigCard.tsx +++ b/app/benchmark/components/ConfigCard.tsx @@ -1,5 +1,5 @@ -import { LoadTestConfig } from "../types"; -import { formatEthFromWeiString } from "../utils/formatters"; +import { LoadTestConfig } from "../../scaling/runs/lib/types"; +import { formatEthFromWeiString } from "../../scaling/runs/lib/formatters"; import StatCard from "./StatCard"; interface ConfigCardProps { diff --git a/app/benchmark/components/ConfigurationTags.tsx b/app/benchmark/components/ConfigurationTags.tsx index 5568120..72479ab 100644 --- a/app/benchmark/components/ConfigurationTags.tsx +++ b/app/benchmark/components/ConfigurationTags.tsx @@ -1,5 +1,5 @@ -import { formatValue, formatLabel } from "../utils/formatters"; -import { camelToTitleCase } from "../utils/formatters"; +import { formatValue, formatLabel } from "../../scaling/runs/lib/formatters"; +import { camelToTitleCase } from "../../scaling/runs/lib/formatters"; interface ConfigurationTagsProps { testConfig: Record; diff --git a/app/benchmark/components/LineChart.tsx b/app/benchmark/components/LineChart.tsx index 9c00325..2c8ddc8 100644 --- a/app/benchmark/components/LineChart.tsx +++ b/app/benchmark/components/LineChart.tsx @@ -7,9 +7,9 @@ import React, { useMemo, } from "react"; import * as d3 from "d3"; -import { DataSeries, MetricData, ChartConfig } from "../types"; +import { DataSeries, MetricData, ChartConfig } from "../../scaling/runs/lib/types"; import BaseChart from "./BaseChart"; -import { formatValue, formatLabel } from "../utils/formatters"; +import { formatValue, formatLabel } from "../../scaling/runs/lib/formatters"; import calculateTooltipLayout from "../hooks/useTooltipLayout"; import useChartHoverSync, { ChartHoverEvent } from "../hooks/useChartHoverSync"; import { deepEqual } from "../utils/collections"; diff --git a/app/benchmark/components/MachineInfo.tsx b/app/benchmark/components/MachineInfo.tsx index e36723f..cfacfb9 100644 --- a/app/benchmark/components/MachineInfo.tsx +++ b/app/benchmark/components/MachineInfo.tsx @@ -1,4 +1,4 @@ -import { MachineInfo as MachineInfoType } from "../types"; +import { MachineInfo as MachineInfoType } from "../../scaling/runs/lib/types"; interface MachineInfoProps { machineInfo?: MachineInfoType; diff --git a/app/benchmark/components/RunList.tsx b/app/benchmark/components/RunList.tsx index d69cda8..9864dc3 100644 --- a/app/benchmark/components/RunList.tsx +++ b/app/benchmark/components/RunList.tsx @@ -1,8 +1,8 @@ import { groupBy } from "../utils/collections"; import { useState } from "react"; -import { formatValue, MetricValue } from "../utils/formatters"; +import { formatValue, MetricValue } from "../../scaling/runs/lib/formatters"; import ThresholdDisplay from "../views/ThresholdDisplay"; -import { BenchmarkRunWithStatus } from "../types"; +import { BenchmarkRunWithStatus } from "../../scaling/runs/lib/types"; import StatusBadge from "./StatusBadge"; import StatusSummary from "./StatusSummary"; import ConfigurationTags from "./ConfigurationTags"; diff --git a/app/benchmark/components/RunListFilter.tsx b/app/benchmark/components/RunListFilter.tsx index 9f7a3c3..6ef5204 100644 --- a/app/benchmark/components/RunListFilter.tsx +++ b/app/benchmark/components/RunListFilter.tsx @@ -2,9 +2,9 @@ import Link from "next/link"; import { useState } from "react"; -import { camelToTitleCase, formatLabel } from "../utils/formatters"; +import { camelToTitleCase, formatLabel } from "../../scaling/runs/lib/formatters"; import { FilterValue } from "../filter"; -import { BenchmarkRunWithStatus } from "../types"; +import { BenchmarkRunWithStatus } from "../../scaling/runs/lib/types"; import { downloadAllRuns, downloadAllRunsCSV } from "../utils/downloadUtils"; import Select from "./Select"; import FloatingDropdown from "./FloatingDropdown"; diff --git a/app/benchmark/components/ThroughputChart.tsx b/app/benchmark/components/ThroughputChart.tsx index 1c36ef3..5c1a062 100644 --- a/app/benchmark/components/ThroughputChart.tsx +++ b/app/benchmark/components/ThroughputChart.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import * as d3 from "d3"; -import { ThroughputSample } from "../types"; -import { formatTps, formatGps } from "../utils/formatters"; +import { ThroughputSample } from "../../scaling/runs/lib/types"; +import { formatTps, formatGps } from "../../scaling/runs/lib/formatters"; interface ThroughputChartProps { samples: ThroughputSample[]; diff --git a/app/benchmark/filter.test.ts b/app/benchmark/filter.test.ts index 4457708..a72023f 100644 --- a/app/benchmark/filter.test.ts +++ b/app/benchmark/filter.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { getBenchmarkVariables } from "./filter"; -import { BenchmarkRun } from "./types"; +import { BenchmarkRun } from "../scaling/runs/lib/types"; // Sample data for testing - This data now aligns with the ActualBenchmarkRun type const sampleRuns: BenchmarkRun[] = [ diff --git a/app/benchmark/hooks/useBenchmarkFilters.ts b/app/benchmark/hooks/useBenchmarkFilters.ts index e6db59f..004c609 100644 --- a/app/benchmark/hooks/useBenchmarkFilters.ts +++ b/app/benchmark/hooks/useBenchmarkFilters.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo } from "react"; import { deepEqual } from "../utils/collections"; -import { type BenchmarkRun } from "../types"; +import { type BenchmarkRun } from "../../scaling/runs/lib/types"; import { useSearchParamsState } from "../utils/useSearchParamsState"; import { getBenchmarkVariables } from "../filter"; type FilterValue = string | number | boolean; diff --git a/app/benchmark/utils/downloadUtils.ts b/app/benchmark/utils/downloadUtils.ts index 7bd693f..29ecbfe 100644 --- a/app/benchmark/utils/downloadUtils.ts +++ b/app/benchmark/utils/downloadUtils.ts @@ -1,4 +1,4 @@ -import { BenchmarkRunWithStatus } from "../types"; +import { BenchmarkRunWithStatus } from "../../scaling/runs/lib/types"; /** * Downloads run information as a JSON file diff --git a/app/benchmark/utils/useDataSeries.ts b/app/benchmark/utils/useDataSeries.ts index 24f08ec..b69157c 100644 --- a/app/benchmark/utils/useDataSeries.ts +++ b/app/benchmark/utils/useDataSeries.ts @@ -4,9 +4,9 @@ import { LoadTestEntry, LoadTestResult, MetricData, -} from "../types"; +} from "../../scaling/runs/lib/types"; import { useCallback } from "react"; -import { getDataService } from "../services/dataService"; +import { getDataService } from "../../scaling/runs/lib/dataService"; // Fetch metrics data using the configured data service export const fetchMetrics = async ( diff --git a/app/benchmark/views/LoadTestAllRuns.tsx b/app/benchmark/views/LoadTestAllRuns.tsx index 0747ad1..51e6642 100644 --- a/app/benchmark/views/LoadTestAllRuns.tsx +++ b/app/benchmark/views/LoadTestAllRuns.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useLoadTestList } from "../utils/useDataSeries"; -import { formatLoadTestTimestamp } from "../utils/formatters"; +import { formatLoadTestTimestamp } from "../../scaling/runs/lib/formatters"; import { loadTestHref, loadTestsHref } from "../routes"; interface ProvidedProps { diff --git a/app/benchmark/views/LoadTestDetail.tsx b/app/benchmark/views/LoadTestDetail.tsx index 754e772..ab38af4 100644 --- a/app/benchmark/views/LoadTestDetail.tsx +++ b/app/benchmark/views/LoadTestDetail.tsx @@ -15,13 +15,13 @@ import { formatGpsVerbose, formatLoadTestTimestamp, formatTps, -} from "../utils/formatters"; +} from "../../scaling/runs/lib/formatters"; import { BlockRange, FlashblocksLatencyStats, LatencyStats, LoadTestResult, -} from "../types"; +} from "../../scaling/runs/lib/types"; import { LoadTestRunSelect } from "../components/BenchmarkToolbar"; import { loadTestAllHref } from "../routes"; diff --git a/app/benchmark/views/RunComparison.tsx b/app/benchmark/views/RunComparison.tsx index 5d41695..7e77467 100644 --- a/app/benchmark/views/RunComparison.tsx +++ b/app/benchmark/views/RunComparison.tsx @@ -7,7 +7,7 @@ import ChartSelector, { } from "../components/ChartSelector"; import ChartGrid from "../components/ChartGrid"; import { useTestMetadata, useMultipleDataSeries } from "../utils/useDataSeries"; -import { DataSeries } from "../types"; +import { DataSeries } from "../../scaling/runs/lib/types"; import { BenchmarkRunSelect } from "../components/BenchmarkToolbar"; import DataSourceError from "../components/DataSourceError"; import { runComparisonHref } from "../routes"; diff --git a/app/benchmark/views/RunIndex.tsx b/app/benchmark/views/RunIndex.tsx index 6c5d522..4bcf3c8 100644 --- a/app/benchmark/views/RunIndex.tsx +++ b/app/benchmark/views/RunIndex.tsx @@ -4,7 +4,7 @@ import { useTestMetadata } from "../utils/useDataSeries"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { getBenchmarkVariables } from "../filter"; import RunList from "../components/RunList"; -import { BenchmarkRuns, getTestRunsWithStatus } from "../types"; +import { BenchmarkRuns, getTestRunsWithStatus } from "../../scaling/runs/lib/types"; import { groupBy } from "../utils/collections"; import RunListFilter from "../components/RunListFilter"; import { BenchmarkRunSelect } from "../components/BenchmarkToolbar"; diff --git a/app/benchmark/services/dataService.ts b/app/scaling/runs/lib/dataService.ts similarity index 99% rename from app/benchmark/services/dataService.ts rename to app/scaling/runs/lib/dataService.ts index 5f3f51c..605bb0e 100644 --- a/app/benchmark/services/dataService.ts +++ b/app/scaling/runs/lib/dataService.ts @@ -5,7 +5,7 @@ import { LoadTestEntry, LoadTestResult, MetricData, -} from "../types"; +} from "./types"; // Load-test endpoints live under the versioned API prefix, unlike benchmark // data which uses the legacy unversioned `output/` paths. Centralized here so diff --git a/app/benchmark/utils/formatters.test.ts b/app/scaling/runs/lib/formatters.test.ts similarity index 100% rename from app/benchmark/utils/formatters.test.ts rename to app/scaling/runs/lib/formatters.test.ts diff --git a/app/benchmark/utils/formatters.tsx b/app/scaling/runs/lib/formatters.tsx similarity index 99% rename from app/benchmark/utils/formatters.tsx rename to app/scaling/runs/lib/formatters.tsx index 0770c6d..a69a18e 100644 --- a/app/benchmark/utils/formatters.tsx +++ b/app/scaling/runs/lib/formatters.tsx @@ -1,4 +1,4 @@ -import { ChartConfig, RustDuration } from "../types"; +import { ChartConfig, RustDuration } from "./types"; const PREFIXES = { "": 1, diff --git a/app/benchmark/metricDefinitions.ts b/app/scaling/runs/lib/metricDefinitions.ts similarity index 100% rename from app/benchmark/metricDefinitions.ts rename to app/scaling/runs/lib/metricDefinitions.ts diff --git a/app/benchmark/types.ts b/app/scaling/runs/lib/types.ts similarity index 100% rename from app/benchmark/types.ts rename to app/scaling/runs/lib/types.ts From 1e25bdac78cbebc9a987f50e84000eb40e940491 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Fri, 16 Oct 2026 05:55:12 -0700 Subject: [PATCH 7/9] feat(scaling): unified run index Co-authored-by: Toshi --- .github/workflows/ci.yml | 2 +- app/scaling/loadSnapshot.tsx | 3 +- app/scaling/page.test.tsx | 1 + app/scaling/page.tsx | 2 +- app/scaling/runs/fixtures/synthetic.json | 70 ++++++++++++++ app/scaling/runs/lib/unified.test.ts | 58 ++++++++++++ app/scaling/runs/lib/unified.ts | 84 +++++++++++++++++ app/scaling/runs/page.test.tsx | 34 +++++++ app/scaling/runs/page.tsx | 111 +++++++++++++++++++++++ 9 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 app/scaling/runs/fixtures/synthetic.json create mode 100644 app/scaling/runs/lib/unified.test.ts create mode 100644 app/scaling/runs/lib/unified.ts create mode 100644 app/scaling/runs/page.test.tsx create mode 100644 app/scaling/runs/page.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d52e8f..1d419d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,7 +165,7 @@ jobs: /internal-explorer/bundles/0x1 /api/internal-explorer/blocks \ /tips /tips/block/0x1 /tips/bundles/0x1 /api/tips/blocks \ /benchmark /benchmark/run/latest /benchmark/run-comparison/1 \ - /benchmark/load-tests/sepolia /scaling /scaling/da /scaling/guardrails; do + /benchmark/load-tests/sepolia /scaling /scaling/da /scaling/guardrails /scaling/runs; do code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:3000${route}") if [ "${code}" != "404" ]; then echo "FAIL: ${route} returned ${code}, expected 404" diff --git a/app/scaling/loadSnapshot.tsx b/app/scaling/loadSnapshot.tsx index a0cf43b..c645c85 100644 --- a/app/scaling/loadSnapshot.tsx +++ b/app/scaling/loadSnapshot.tsx @@ -47,7 +47,7 @@ export function Unavailable({ loaded }: { loaded: Exclude; title: ReactNode; crumb?: string }) { +export function Header({ loaded, title, crumb, action }: { loaded: Extract; title: ReactNode; crumb?: string; action?: ReactNode }) { const { snapshot, ageDays, stale } = loaded; return (
    @@ -55,6 +55,7 @@ export function Header({ loaded, title, crumb }: { loaded: ExtractScaling / {crumb} ) : null} {title} + {action} As of {formatTime(snapshot.collectedAt)} ({ageDays === 0 ? 'today' : `${ageDays}d old`}) · manually refreshed snapshot, not live data. {snapshot.context} diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 2fd3346..d442d71 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -17,6 +17,7 @@ it('links the first screen to five detail pages instead of listing metric rows', const html = renderToStaticMarkup(await ScalingPage()); for (const page of ['da', 'execution', 'state', 'networking', 'guardrails']) expect(html).toContain(`href="/scaling/${page}"`); expect(html).not.toContain('Blob utilization'); + expect(html).toContain('href="/scaling/runs"'); }); it('shows every metric row on its detail page', async () => { diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index 9889be5..a158d88 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -18,7 +18,7 @@ export default async function ScalingPage() { const { snapshot } = loaded; return (
    -
    +
    Runs →} /> diff --git a/app/scaling/runs/fixtures/synthetic.json b/app/scaling/runs/fixtures/synthetic.json new file mode 100644 index 0000000..5456a58 --- /dev/null +++ b/app/scaling/runs/fixtures/synthetic.json @@ -0,0 +1,70 @@ +{ + "metadata": { + "runs": [ + { + "id": "test-synthetic-1", + "sourceFile": "synthetic.yml", + "testName": "Synthetic bench run", + "testDescription": "Invented values for tests.", + "outputDir": "synthetic-1", + "createdAt": "2026-01-07T12:00:00Z", + "testConfig": { "BenchmarkRun": "test-synthetic-1", "BlockTimeMilliseconds": 1000, "GasLimit": 100000000, "TransactionPayload": "synthetic-payload" }, + "machineInfo": { "type": "synthetic.large", "provider": "", "region": "", "fileSystem": "ext4" }, + "thresholds": { "warning": { "latency/get_payload": 50000000 } }, + "result": { + "success": true, + "complete": true, + "sequencerMetrics": { "gasPerSecond": 42000000, "forkChoiceUpdated": 0.001, "getPayload": 0.02, "sendTxs": 0.01 }, + "validatorMetrics": { "gasPerSecond": 420000000, "newPayload": 0.03 } + } + }, + { + "id": "test-synthetic-2", + "sourceFile": "synthetic.yml", + "testName": "Synthetic incomplete run", + "testDescription": "Invented values for tests.", + "outputDir": "synthetic-2", + "createdAt": "2025-12-20T12:00:00Z", + "testConfig": { "BenchmarkRun": "test-synthetic-2" }, + "result": null + } + ] + }, + "loadTests": [ + { + "network": "sepolia", + "timestamp": "2026-01-08T00-00-01", + "result": { + "block_latency": { "min": { "secs": 1, "nanos": 0 }, "max": { "secs": 3, "nanos": 0 }, "mean": { "secs": 2, "nanos": 0 }, "p50": { "secs": 2, "nanos": 0 }, "p95": { "secs": 2, "nanos": 500000000 }, "p99": { "secs": 3, "nanos": 0 } }, + "flashblocks_latency": { "count": 10, "min": { "secs": 0, "nanos": 100000000 }, "max": { "secs": 0, "nanos": 900000000 }, "mean": { "secs": 0, "nanos": 300000000 }, "p50": { "secs": 0, "nanos": 250000000 }, "p90": { "secs": 0, "nanos": 600000000 }, "p95": { "secs": 0, "nanos": 700000000 }, "p99": { "secs": 0, "nanos": 800000000 } }, + "throughput": { "total_submitted": 1000, "total_confirmed": 990, "total_failed": 10, "total_reverted": 0, "tps": 33, "gps": 12500000, "duration": { "secs": 30, "nanos": 0 } }, + "throughput_percentiles": { "tps_p50": 30, "tps_p90": 35, "tps_p99": 36, "tps_max": 36, "gps_p50": 12000000, "gps_p90": 13000000, "gps_p99": 13500000, "gps_max": 13500000 }, + "gas": { "total_gas": 375000000, "avg_gas": 378787, "total_cost_wei": 1000000000000, "avg_gas_price": 1000 }, + "top_failure_reasons": [], + "config": { + "funding_amount": "1000000000000000000", "sender_count": 10, "sender_offset": 0, "in_flight_per_sender": 4, + "duration": "30s", "target_gps": 15000000, "seed": 1, "chain_id": null, + "transactions": [{ "type": "transfer", "weight": 70 }, { "type": "swap", "weight": 30 }], + "looper_contract": null, "swap_token_amount": "0" + } + } + }, + { + "network": "sepolia", + "timestamp": "2026-01-01-00-00-02", + "result": { + "block_latency": { "min": { "secs": 1, "nanos": 0 }, "max": { "secs": 2, "nanos": 0 }, "mean": { "secs": 1, "nanos": 500000000 }, "p50": { "secs": 1, "nanos": 500000000 }, "p95": { "secs": 1, "nanos": 900000000 }, "p99": { "secs": 2, "nanos": 0 } }, + "flashblocks_latency": { "count": 5, "min": { "secs": 0, "nanos": 100000000 }, "max": { "secs": 0, "nanos": 500000000 }, "mean": { "secs": 0, "nanos": 200000000 }, "p50": { "secs": 0, "nanos": 200000000 }, "p90": { "secs": 0, "nanos": 400000000 }, "p95": { "secs": 0, "nanos": 450000000 }, "p99": { "secs": 0, "nanos": 500000000 } }, + "throughput": { "total_submitted": 500, "total_confirmed": 500, "total_failed": 0, "total_reverted": 0, "tps": 17, "gps": 6000000, "duration": { "secs": 30, "nanos": 0 } }, + "throughput_percentiles": { "tps_p50": 16, "tps_p90": 18, "tps_p99": 18, "tps_max": 18, "gps_p50": 6000000, "gps_p90": 6500000, "gps_p99": 6500000, "gps_max": 6500000 }, + "gas": { "total_gas": 180000000, "avg_gas": 360000, "total_cost_wei": 1000000000000, "avg_gas_price": 1000 }, + "top_failure_reasons": [] + } + }, + { + "network": "sepolia", + "timestamp": "2025-12-31T00-00-03", + "result": { "error": "synthetic fatal error" } + } + ] +} diff --git a/app/scaling/runs/lib/unified.test.ts b/app/scaling/runs/lib/unified.test.ts new file mode 100644 index 0000000..08ea38f --- /dev/null +++ b/app/scaling/runs/lib/unified.test.ts @@ -0,0 +1,58 @@ +import { expect, it } from 'vitest'; + +import fixture from '../fixtures/synthetic.json'; +import { getTestRunsWithStatus, type BenchmarkRuns } from './types'; +import { toUnifiedRun, weekOf, type LoadRun } from './unified'; + +const [bench, incomplete] = getTestRunsWithStatus(fixture.metadata as BenchmarkRuns).map(toUnifiedRun); +const [load, legacy, fatal] = (fixture.loadTests as unknown as LoadRun[]).map(toUnifiedRun); + +it('maps a bench run without inventing its duration', () => { + expect(bench).toEqual({ + kind: 'bench', + href: '/scaling/runs/bench/test-synthetic-1', + env: 'bench', + network: null, + timestamp: Date.parse('2026-01-07T12:00:00Z'), + commit: null, + machine: 'synthetic.large', + state: 'unknown', + duration: null, + profile: null, + workload: 'synthetic-payload', + gasPerSecond: 42e6, + latency: [{ label: 'get-payload', seconds: 0.02 }, { label: 'new-payload', seconds: 0.03 }], + status: 'success', + areas: ['execution'], + qualifies: { ok: false, reason: 'duration not recorded; state size not recorded' }, + }); + expect(incomplete).toMatchObject({ status: 'incomplete', machine: null, gasPerSecond: null, latency: [], workload: 'Synthetic incomplete run' }); +}); + +it('maps a load test as a burst with its mix, observed gas/s and p99 latencies', () => { + expect(load).toMatchObject({ + kind: 'load', + href: '/scaling/runs/load/sepolia/2026-01-08T00-00-01', + env: 'sepolia', + timestamp: Date.UTC(2026, 0, 8, 0, 0, 1), + duration: 30, + profile: 'burst', + workload: '70% transfer · 30% swap · target 15 Mgas/s', + gasPerSecond: 12.5e6, + latency: [{ label: 'block p99', seconds: 3 }, { label: 'flashblocks p99', seconds: 0.8 }], + status: 'success', + areas: ['execution', 'da', 'networking'], + qualifies: { ok: false, reason: '30 s burst, needs ≥ 1 h; state size not recorded' }, + }); + expect(legacy).toMatchObject({ timestamp: Date.UTC(2026, 0, 1, 0, 0, 2), workload: '—', profile: 'burst' }); + expect(fatal).toMatchObject({ status: 'fatal', duration: null, profile: null, gasPerSecond: null, latency: [] }); +}); + +it('leaves status unknown when a load result could not be fetched', () => { + expect(toUnifiedRun({ network: 'sepolia', timestamp: '2026-01-08T00-00-01' })).toMatchObject({ status: null, duration: null }); +}); + +it('groups by the Monday that starts the UTC week', () => { + expect(weekOf(Date.UTC(2026, 0, 11, 23, 59))).toBe('2026-01-05'); + expect(weekOf(Date.UTC(2026, 0, 12))).toBe('2026-01-12'); +}); diff --git a/app/scaling/runs/lib/unified.ts b/app/scaling/runs/lib/unified.ts new file mode 100644 index 0000000..05daa6c --- /dev/null +++ b/app/scaling/runs/lib/unified.ts @@ -0,0 +1,84 @@ +import { durationToMs, parseLoadTestTimestamp } from './formatters'; +import type { BenchmarkRunWithStatus, LoadTestEntry, LoadTestResult, RunStatus, RustDuration } from './types'; + +export type LoadRun = LoadTestEntry & { result?: LoadTestResult }; + +export type UnifiedRun = { + kind: 'bench' | 'load'; + href: string; + env: string; + network: string | null; + timestamp: number; + commit: string | null; + machine: string | null; + state: 'unknown'; + duration: number | null; + profile: 'sustained' | 'burst' | null; + workload: string; + gasPerSecond: number | null; + latency: { label: string; seconds: number }[]; + status: RunStatus | null; + areas: ('execution' | 'da' | 'networking')[]; + qualifies: { ok: false; reason: string }; +}; + +const SUSTAINED_SECONDS = 3600; +const secs = (d: RustDuration) => durationToMs(d) / 1000; + +export function toUnifiedRun(run: BenchmarkRunWithStatus | LoadRun): UnifiedRun { + return 'testConfig' in run ? fromBench(run) : fromLoad(run); +} + +function fromBench(run: BenchmarkRunWithStatus): UnifiedRun { + const r = run.result; + // metadata.json records no block count, so a bench run's duration is unknown. + return withQualifies({ + kind: 'bench', + href: `/scaling/runs/bench/${run.id}`, + env: 'bench', + network: null, + timestamp: Date.parse(run.createdAt), + commit: r?.clientVersion ?? null, + machine: [run.machineInfo?.type, run.machineInfo?.provider, run.machineInfo?.region].filter(Boolean).join(' · ') || null, + duration: null, + workload: String(run.testConfig.TransactionPayload ?? run.testName), + gasPerSecond: r?.sequencerMetrics?.gasPerSecond ?? null, + latency: [r?.sequencerMetrics && { label: 'get-payload', seconds: r.sequencerMetrics.getPayload }, r?.validatorMetrics && { label: 'new-payload', seconds: r.validatorMetrics.newPayload }].filter((l) => !!l), + status: run.status, + areas: ['execution'], + }); +} + +function fromLoad({ network, timestamp, result }: LoadRun): UnifiedRun { + const config = result?.config; + const mix = config?.transactions.map((t) => `${t.weight}% ${t.type}`).join(' · '); + const target = config ? `target ${config.target_gps / 1e6} Mgas/s` : undefined; + return withQualifies({ + kind: 'load', + href: `/scaling/runs/load/${network}/${timestamp}`, + env: network, + network, + timestamp: parseLoadTestTimestamp(timestamp)?.getTime() ?? 0, + commit: null, + machine: null, + duration: result?.throughput ? secs(result.throughput.duration) : null, + workload: [mix, target].filter(Boolean).join(' · ') || '—', + gasPerSecond: result?.throughput?.gps ?? null, + latency: result?.block_latency ? [{ label: 'block p99', seconds: secs(result.block_latency.p99) }, { label: 'flashblocks p99', seconds: secs(result.flashblocks_latency.p99) }] : [], + status: result ? (result.error ? 'fatal' : 'success') : null, + areas: ['execution', 'da', 'networking'], + }); +} + +function withQualifies(run: Omit): UnifiedRun { + const { duration } = run; + const profile = duration === null ? null : duration >= SUSTAINED_SECONDS ? 'sustained' : 'burst'; + const reasons = [duration === null ? 'duration not recorded' : profile === 'burst' ? `${duration} s burst, needs ≥ 1 h` : null, 'state size not recorded']; + return { ...run, state: 'unknown', profile, qualifies: { ok: false, reason: reasons.filter(Boolean).join('; ') } }; +} + +export function weekOf(ts: number): string { + const d = new Date(ts); + d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7)); + return d.toISOString().slice(0, 10); +} diff --git a/app/scaling/runs/page.test.tsx b/app/scaling/runs/page.test.tsx new file mode 100644 index 0000000..60d4985 --- /dev/null +++ b/app/scaling/runs/page.test.tsx @@ -0,0 +1,34 @@ +import { renderToStaticMarkup } from 'react-dom/server'; +import { expect, it, vi } from 'vitest'; + +import fixture from './fixtures/synthetic.json'; +import { getTestRunsWithStatus, type BenchmarkRuns } from './lib/types'; +import { toUnifiedRun, type LoadRun } from './lib/unified'; +import RunsPage from './page'; + +const swr = vi.hoisted(() => ({ value: {} as { data?: unknown; error?: unknown } })); +vi.mock('swr', () => ({ default: () => swr.value })); + +const runs = [ + ...getTestRunsWithStatus(fixture.metadata as BenchmarkRuns).map(toUnifiedRun), + ...(fixture.loadTests as unknown as LoadRun[]).map(toUnifiedRun), +].sort((a, b) => b.timestamp - a.timestamp); + +it('lists both families newest first, grouped by week, linking to their detail pages', () => { + swr.value = { data: { runs, loadTotal: 3 } }; + const html = renderToStaticMarkup(); + const order = ['Week of 2026-01-05', '/scaling/runs/load/sepolia/2026-01-08T00-00-01', '/scaling/runs/bench/test-synthetic-1', + 'Week of 2025-12-29', '/scaling/runs/load/sepolia/2026-01-01-00-00-02', '/scaling/runs/load/sepolia/2025-12-31T00-00-03', + 'Week of 2025-12-15', '/scaling/runs/bench/test-synthetic-2']; + const at = order.map((s) => html.indexOf(s)); + expect(at.every((i) => i >= 0)).toBe(true); + expect(at).toEqual([...at].sort((a, b) => a - b)); + expect(html).toContain('12.5'); + expect(html).toContain('flashblocks p99 800.0 ms'); + expect(html).toContain('the latest 3 of 3 load tests'); +}); + +it('shows the data-source error', () => { + swr.value = { error: new Error('Benchmark data source is not configured.') }; + expect(renderToStaticMarkup()).toContain('Benchmark data source is not configured.'); +}); diff --git a/app/scaling/runs/page.tsx b/app/scaling/runs/page.tsx new file mode 100644 index 0000000..d406a48 --- /dev/null +++ b/app/scaling/runs/page.tsx @@ -0,0 +1,111 @@ +'use client'; + +import Link from 'next/link'; +import { useState, type ReactNode } from 'react'; +import useSWR from 'swr'; + +import { Card } from '../../components/ui/Card'; +import { cn } from '../../components/ui/cn'; +import { EmptyState } from '../../components/ui/EmptyState'; +import { Text } from '../../components/ui/Text'; +import { Info } from '../components/Info'; +import { Tag } from '../components/Tag'; +import { getDataService } from './lib/dataService'; +import { formatValue } from './lib/formatters'; +import { getTestRunsWithStatus, type RunStatus } from './lib/types'; +import { toUnifiedRun, weekOf, type UnifiedRun } from './lib/unified'; + +// report-api has no endpoint listing load-test networks. +const NETWORK = 'sepolia'; +// One request per load-test result; nightly runs grow without bound. +const LOAD_LIMIT = 30; + +const TH = 'px-3 py-2 text-left text-[12px] font-[500] text-bds-gray-60'; +const TD = 'px-3 py-2 align-top'; +const DOT: Record = { success: 'bg-bds-green-50', warning: 'bg-bds-yellow-50', error: 'bg-bds-red-50', fatal: 'bg-bds-red-50', incomplete: 'bg-bds-gray-30' }; + +async function loadRuns() { + const api = getDataService(); + const [metadata, entries] = await Promise.all([api.getMetadata(), api.getLoadTestList(NETWORK)]); + const recent = entries.slice(0, LOAD_LIMIT); + // A missing result leaves that row's metrics unknown instead of failing the whole index. + const results = await Promise.all(recent.map((e) => api.getLoadTestResult(e.network, e.timestamp).catch(() => undefined))); + const runs = [...getTestRunsWithStatus(metadata), ...recent.map((e, i) => ({ ...e, result: results[i] }))].map(toUnifiedRun); + runs.sort((a, b) => b.timestamp - a.timestamp); + return { runs, loadTotal: entries.length }; +} + +export default function RunsPage() { + const { data, error } = useSWR('scaling-runs', loadRuns); + const [kind, setKind] = useState(null); + const [env, setEnv] = useState(null); + const [qualifying, setQualifying] = useState(false); + + const runs = (data?.runs ?? []).filter((r) => (!kind || r.kind === kind) && (!env || r.env === env) && (!qualifying || r.qualifies.ok)); + const weeks = Map.groupBy(runs, (r) => weekOf(r.timestamp)); + const envs = [...new Set(data?.runs.map((r) => r.env))]; + + return ( +
    +
    + Scaling / Runs + Runs + Base-bench runs and {data ? `the latest ${Math.min(LOAD_LIMIT, data.loadTotal)} of ${data.loadTotal}` : 'the latest'} load tests, newest first. +
    +
    + {([null, 'bench', 'load'] as const).map((k) => setKind(k)}>{k ?? 'all kinds'})} + {[null, ...envs].map((e) => setEnv(e)}>{e ?? 'all envs'})} + setQualifying(!qualifying)}>qualifying only +
    + {error ? ( + + ) : !data ? ( + Loading runs… + ) : runs.length === 0 ? ( + + ) : ( + + + + + {['When', 'Kind', 'Env', 'Profile', 'Workload', 'Mgas/s', 'Latency', 'Status', 'Duration', 'Machine'].map((h) => )} + + + {[...weeks].map(([week, rows]) => ( + + + {rows.map((r) => )} + + ))} +
    {h}
    Week of {week}
    +
    + )} +
    + ); +} + +function Row({ run: r }: { run: UnifiedRun }) { + return ( +
    {new Date(r.timestamp).toISOString().slice(0, 16).replace('T', ' ')}{r.env}{r.profile ? : '—'}{r.qualifies.reason}{r.workload}{r.gasPerSecond === null ? '—' : (r.gasPerSecond / 1e6).toFixed(1)} + {r.latency.length ? r.latency.map((l) =>
    {l.label} {formatValue(l.seconds, 's')}
    ) : '—'} +
    + + {r.duration === null ? '—' : formatValue(r.duration, 's')}{r.machine ?? '—'}