From 171343736745cee15184b172c19739de8995c570 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 08:45:06 -0700 Subject: [PATCH 01/33] feat(scaling): build the scorecard from benchmark pipeline results --- app/scaling/[area]/page.tsx | 4 +- app/scaling/components/CeilingCards.tsx | 9 +- app/scaling/components/DaWorkloads.tsx | 33 +++-- app/scaling/components/Guardrails.tsx | 47 +++++++- app/scaling/components/Ladder.tsx | 19 ++- app/scaling/components/ProvenCapacity.tsx | 20 ++- .../components/RunBackedCeilingCard.tsx | 12 +- app/scaling/components/firstScreen.test.tsx | 46 ++++++- .../components/provenCapacity.test.tsx | 48 ++++++-- app/scaling/components/runBacked.test.tsx | 31 +++++ app/scaling/da/workloads.test.ts | 34 +++++- app/scaling/da/workloads.ts | 36 +++++- app/scaling/guardrails/page.tsx | 2 +- app/scaling/page.test.tsx | 12 +- app/scaling/page.tsx | 7 +- app/scaling/proven/capacity.test.ts | 114 +++++++++++++++--- app/scaling/proven/capacity.ts | 101 ++++++++++++++-- 17 files changed, 497 insertions(+), 78 deletions(-) diff --git a/app/scaling/[area]/page.tsx b/app/scaling/[area]/page.tsx index fe39e7b..67feb01 100644 --- a/app/scaling/[area]/page.tsx +++ b/app/scaling/[area]/page.tsx @@ -27,9 +27,9 @@ export default async function ScalingAreaPage({ params }: { params: Promise<{ ar
Ceiling · Mgas/s - +
- {area === 'da' ? : null} + {area === 'da' ? : null} {section ? ( ) : ( diff --git a/app/scaling/components/CeilingCards.tsx b/app/scaling/components/CeilingCards.tsx index 96bbc21..b1bb936 100644 --- a/app/scaling/components/CeilingCards.tsx +++ b/app/scaling/components/CeilingCards.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; import { Card } from '../../components/ui/Card'; import { Text } from '../../components/ui/Text'; +import type { DaWorkload } from '../da/workloads'; import type { UnifiedRun } from '../runs/lib/unified'; import { deriveBinding, type Ceiling, type CeilingInput } from '../snapshot.mjs'; import { DaWorkloadRows } from './DaWorkloads'; @@ -17,18 +18,18 @@ function input({ name, value, unit, status, asOf }: CeilingInput): string { return [name, shown, value === null ? null : status, asOf && new Date(asOf).toISOString().slice(0, 10)].filter(Boolean).join(' · '); } -export function CeilingCards({ ceilings }: { ceilings: Ceiling[] }) { +export function CeilingCards({ ceilings, benchUrl }: { ceilings: Ceiling[]; benchUrl?: string }) { return (
Ceilings by bottleneck · Mgas/s
- {AREAS.map(([area]) => )} + {AREAS.map(([area]) => )}
); } -export function CeilingCard({ area, ceilings, run }: { area: Ceiling['area']; ceilings: Ceiling[]; run?: UnifiedRun }) { +export function CeilingCard({ area, ceilings, workloads, run }: { area: Ceiling['area']; ceilings: Ceiling[]; workloads?: DaWorkload[]; run?: UnifiedRun }) { const name = AREAS.find(([a]) => a === area)![1]; const c = ceilings.find((x) => x.area === area)!; return ( @@ -45,7 +46,7 @@ export function CeilingCard({ area, ceilings, run }: { area: Ceiling['area']; ce {area === 'da' ? ( <>

{c.reason}

- + ) : ( <> diff --git a/app/scaling/components/DaWorkloads.tsx b/app/scaling/components/DaWorkloads.tsx index f14cd36..54cab58 100644 --- a/app/scaling/components/DaWorkloads.tsx +++ b/app/scaling/components/DaWorkloads.tsx @@ -1,20 +1,23 @@ +'use client'; + import { cn } from '../../components/ui/cn'; import { Card } from '../../components/ui/Card'; import { Text } from '../../components/ui/Text'; -import { BLOB_MAX, BLOB_TARGET, bindingWorkload, daCapacity, DA_WORKLOADS, fmt } from '../da/workloads'; +import { BLOB_MAX, BLOB_TARGET, bindingWorkload, daCapacity, daWorkloads, DA_WORKLOADS, fmt, type DaWorkload } from '../da/workloads'; +import { useProvenCapacity } from './ProvenCapacity'; import { Tag } from './Tag'; const TH = 'px-4 py-2 text-right text-[12px] font-[500] text-bds-gray-60 first:text-left'; const TD = 'whitespace-nowrap px-4 py-3 text-right font-mono first:text-left first:font-sans'; // The DA ceiling card's body: one aligned row per workload instead of the generic input list. -export function DaWorkloadRows() { - const binding = bindingWorkload(); +export function DaWorkloadRows({ workloads = DA_WORKLOADS }: { workloads?: DaWorkload[] }) { + const binding = bindingWorkload(workloads); const row = 'grid grid-cols-[1fr_2.75rem_3rem] items-baseline gap-x-2 py-1 text-right'; return (
  • Mgas/stx/s
  • - {DA_WORKLOADS.map((w) => { + {workloads.map((w) => { const { mgasPerSecond, txPerSecond } = daCapacity(w, BLOB_TARGET); return (
  • @@ -28,9 +31,22 @@ export function DaWorkloadRows() { ); } -export function DaWorkloads({ observedP50 }: { observedP50: number | null }) { - const binding = bindingWorkload(); - const rows = DA_WORKLOADS.map((w) => ({ w, target: daCapacity(w, BLOB_TARGET), max: daCapacity(w, BLOB_MAX) })); +function BlobsPerSlot({ bench }: { bench: NonNullable }) { + const { da, run } = bench; + const verdict = da.withinTarget === null ? null : da.withinTarget ? 'within' : 'over'; + return ( + + {da.result.blobs_per_l1_slot.toFixed(1)} / {da.target ?? BLOB_TARGET} + {verdict ? {verdict} : null} + + ); +} + +export function DaWorkloads({ observedP50, benchUrl }: { observedP50: number | null; benchUrl?: string }) { + const bench = useProvenCapacity(benchUrl); + const workloads = daWorkloads(bench.error ? undefined : bench.data?.da); + const binding = bindingWorkload(workloads); + const rows = workloads.map((w) => ({ w, target: daCapacity(w, BLOB_TARGET), max: daCapacity(w, BLOB_MAX) })); const scale = 1.1 * Math.max(...rows.map((r) => r.max.mgasPerSecond), observedP50 ?? 0); const pct = (v: number) => `${(v / scale) * 100}%`; return ( @@ -65,7 +81,7 @@ export function DaWorkloads({ observedP50 }: { observedP50: number | null }) { - {['Workload', 'Bytes / tx', 'Gas / tx', 'Bytes / gas', 'tx/s', 'Mgas/s', 'Source'].map((h) => )} + {['Workload', 'Bytes / tx', 'Gas / tx', 'Bytes / gas', 'tx/s', 'Mgas/s', 'Bench blobs / slot', 'Source'].map((h) => )} @@ -79,6 +95,7 @@ export function DaWorkloads({ observedP50 }: { observedP50: number | null }) { + - {guardrails.toSorted((a, b) => rank(a) - rank(b)).map((g) => ( + {scorecardGuardrails(guardrails, run).toSorted((a, b) => rank(a) - rank(b)).map((g) => (
    {h}{h}
    {(w.bytesPerTx / w.gasPerTx).toFixed(5)} {fmt(target.txPerSecond)} {fmt(target.mgasPerSecond)}{w.bench ? : '—'} {' '} {w.source ? ( diff --git a/app/scaling/components/Guardrails.tsx b/app/scaling/components/Guardrails.tsx index e0980f3..eefcb4e 100644 --- a/app/scaling/components/Guardrails.tsx +++ b/app/scaling/components/Guardrails.tsx @@ -1,10 +1,14 @@ +'use client'; + import Link from 'next/link'; import { cn } from '../../components/ui/cn'; import { Card } from '../../components/ui/Card'; import { Text } from '../../components/ui/Text'; +import type { Run } from '../proven/capacity'; import type { Guardrail } from '../snapshot.mjs'; import { Info } from './Info'; +import { useProvenCapacity } from './ProvenCapacity'; import { Tag } from './Tag'; const TH = 'px-4 py-2 text-left text-[12px] font-[500] text-bds-gray-60'; @@ -15,7 +19,46 @@ const rank = (g: Guardrail) => (g.status === 'not-monitored' ? 2 : g.value === n const fmt = (n: number, unit: string) => `${n.toLocaleString('en-US', { maximumFractionDigits: 2 })} ${unit}`.trim(); -export function Guardrails({ guardrails, detailed = false }: { guardrails: Guardrail[]; detailed?: boolean }) { +const judge = (g: Omit): Guardrail => ({ + ...g, + status: g.value === null || g.threshold === null ? 'unknown' : (g.comparator === '<=' ? g.value <= g.threshold : g.value >= g.threshold) ? 'ok' : 'breached', +}); + +// Production validators may trail the tip by at most 3 blocks. `run` is the qualifying mainnet-mix +// bench run: undefined without a bench host, null when none is loaded. +export function scorecardGuardrails(snapshot: Guardrail[], run: Run | null | undefined): Guardrail[] { + const keepUp = (g?: Guardrail) => judge({ + id: 'validator-keep-up', label: 'Validator keep-up', value: g?.value ?? null, unit: 'blocks', threshold: 3, comparator: '<=', + thresholdTag: 'set', owner: g?.owner ?? null, asOf: g?.asOf ?? null, + help: 'How far production validators trail the tip, from the snapshot. They must stay within 3 blocks.', + }); + const rows = snapshot.some((g) => g.id === 'validator-keep-up') + ? snapshot.map((g) => (g.id === 'validator-keep-up' ? keepUp(g) : g)) + : [...snapshot, keepUp()]; + if (run === undefined) return rows; + const source = run ? ` From bench run ${run.outputDir}.` : ' No qualifying mainnet-mix bench run is loaded.'; + const bench = { comparator: '<=' as const, threshold: null, thresholdTag: 'unset' as const, owner: null, asOf: run ? new Date(run.createdAt).toISOString() : null }; + return [ + ...rows, + judge({ + ...bench, id: 'proof-node-head-lag', label: 'Proof-node head lag p99', value: run?.proofNode?.proofs?.headLagBlocks.p99 ?? null, unit: 'blocks', + help: `How far proof history trails the inserted head while the proof node replays the bench blocks.${source} Threshold pending.`, + }), + judge({ + ...bench, id: 'proof-node-execute-payload', label: 'debug_executePayload p99', value: run?.proofNode?.proofs?.methods.debug_executePayload?.latency.p99Ms ?? null, unit: 'ms', + help: `Latency of the witness call the prover makes for each block, on the proof node.${source} Threshold pending.`, + }), + judge({ + ...bench, id: 'validator-replay-headroom', label: 'Validator replay headroom', value: run?.replay?.blocksPerSecond ?? null, unit: 'blocks/s', + comparator: '>=', threshold: 5, thresholdTag: 'set', + help: `Blocks per second a validator replays the bench blocks at. Keeping up needs 5, one block per 200 ms.${source}`, + }), + ]; +} + +export function Guardrails({ guardrails, detailed = false, benchUrl }: { guardrails: Guardrail[]; detailed?: boolean; benchUrl?: string }) { + const bench = useProvenCapacity(benchUrl); + const run = benchUrl ? (bench.error ? null : bench.data?.current.mix?.run ?? null) : undefined; return (
    {detailed ? null : Guardrails →} @@ -30,7 +73,7 @@ export function Guardrails({ guardrails, detailed = false }: { guardrails: Guard
    {detailed ? <>{g.label}

    {g.help}

    : ( diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx index c69eeed..d46d436 100644 --- a/app/scaling/components/Ladder.tsx +++ b/app/scaling/components/Ladder.tsx @@ -4,7 +4,8 @@ import { useLayoutEffect, useRef, useState } from 'react'; import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; -import type { Proven } from '../proven/capacity'; +import { daWorkloads, withDaWorkloads } from '../da/workloads'; +import { runHref, type Proven, type ProvenCapacity as Capacity } from '../proven/capacity'; import { deriveBinding, type Rung, type Snapshot } from '../snapshot.mjs'; import { Info } from './Info'; import { day, ProvenCapacity, useProvenCapacity } from './ProvenCapacity'; @@ -23,7 +24,7 @@ type Item = { id: string; label: string; value: number | null; tag: string; help function benchItem({ mgas, atTop, run }: Proven) { const top = atTop ? ` ≥ because ${mgas} Mgas/s is the highest rate the bench runs.` : ''; - const help = `Bench-proven: the highest rate whose latest main run in the last 14 days was comparable and delivered ≥ 95% of target. Run ${run.outputDir}, commit ${run.commit.slice(0, 7)}, ${day(run.createdAt)}.${top} DA and proof guardrails are not checked.`; + const help = `Bench-proven: the highest rate whose latest main run in the last 14 days was comparable and delivered ≥ 95% of target. Run ${run.outputDir}, commit ${run.commit.slice(0, 7)}, ${day(run.createdAt)}.${top} Comparable requires every enabled pipeline step to pass; the DA blob target and proof-node lag and latency are recorded, not gated.`; return { value: mgas, tag: 'measured' as const, help, atLeast: atTop, source: 'bench' }; } @@ -51,6 +52,13 @@ function translation({ gasPerPayment, gasPerSwap }: Snapshot['translations']): s return `1 Mgas/s ≈ ${per(gasPerPayment, 'payments')} ≈ ${per(gasPerSwap, 'swaps')}`; } +// Delivered tx/s of each workload's proven run; the rate can only be higher. +function BenchTranslation({ current }: { current: Capacity['current'] }) { + const rate = (p: Proven | null, noun: string, what: string) => + p ? ≥ {Math.floor(p.run.tps).toLocaleString('en-US')} {noun} ({what}) : `${noun} (${what}) pending`; + return <>{rate(current.payment, 'payments/s', 'B20 transfers')} · {rate(current.swap, 'swaps/s', 'Uniswap v3')}; +} + export function Ladder(props: Props) { const axisRef = useRef(null); const [width, setWidth] = useState(960); @@ -61,8 +69,9 @@ export function Ladder(props: Props) { }, []); const bench = useProvenCapacity(props.benchUrl); - const proven = bench.error ? null : bench.data?.current; - const all = items(props, proven); + const loaded = bench.error ? undefined : bench.data; + const proven = loaded?.current.mix; + const all = items(loaded ? { ...props, ceilings: withDaWorkloads(props.ceilings, daWorkloads(loaded.da)) } : props, proven); const valued = all.flatMap((i) => (i.value === null ? [] : [{ id: i.id, value: i.value }])); const max = props.ladder.configuredLimit.value ?? Math.max(0, ...valued.map((v) => v.value)); const placed = new Map(max > 0 ? layoutRungs(valued, max, LABEL_PX / width).map((p) => [p.id, p]) : []); @@ -116,7 +125,7 @@ export function Ladder(props: Props) { Qualified sustained by {props.ladder.qualified.evidence.map((l) => {l.label})} ) : null} - {translation(props.translations)} + {loaded ? : translation(props.translations)} {props.benchUrl ? : null} {props.dashboardUrl ? Mainnet load, gas limit, DA ceiling → : null} diff --git a/app/scaling/components/ProvenCapacity.tsx b/app/scaling/components/ProvenCapacity.tsx index 1c84d5c..3ab452d 100644 --- a/app/scaling/components/ProvenCapacity.tsx +++ b/app/scaling/components/ProvenCapacity.tsx @@ -2,14 +2,16 @@ import useSWR from 'swr'; +import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; -import { loadProvenCapacity, type ProvenCapacity as Capacity } from '../proven/capacity'; +import { loadProvenCapacity, runHref, type PipelineStep, type ProvenCapacity as Capacity } from '../proven/capacity'; import { LineChart } from '../runs/components/LineChart'; export type Bench = Capacity & { now: number }; export const day = (t: number) => new Date(t).toISOString().slice(0, 10); const mgas = (m: number | null) => (m === null ? 'none' : `${m} Mgas/s`); +const STEP_DOT: Record = { passed: 'bg-bds-green-50', failed: 'bg-bds-red-50', skipped: 'bg-bds-gray-30', 'not-implemented': 'bg-bds-gray-30' }; export function useProvenCapacity(benchUrl?: string) { return useSWR(benchUrl ? ['scaling-proven-capacity', benchUrl] : null, async (): Promise => { @@ -27,18 +29,26 @@ export function ProvenCapacity({ data, error }: { data?: Bench; error?: unknown function Result({ capacity: { current, history, unknown, now } }: { capacity: Bench }) { // A run whose files failed to load is skipped, so an older result at its rate may count instead. const partial = unknown.length ? {unknown.length} run(s) could not be loaded, so this result may be wrong. : null; - if (!current) return partial ?? No main run passed in the last 14 days.; - const { run } = current; + if (!current.mix) return partial ?? No main run passed in the last 14 days.; + const { run } = current.mix; const y = (m: number | null) => (m ?? 0) * 1e6; const points = history.flatMap((s, i): [number, number][] => (i ? [[s.at, y(history[i - 1].mgas)], [s.at, y(s.mgas)]] : [[s.at, y(s.mgas)]])); - points.push([now, y(current.mgas)]); + points.push([now, y(current.mix.mgas)]); return ( <> {partial} - Qualified by {run.outputDir}: {run.mgas} Mgas/s run delivered {(run.gps / 1e6).toFixed(1)} Mgas/s · commit{' '} + Qualified by {run.outputDir}: {run.mgas} Mgas/s run delivered {(run.gps / 1e6).toFixed(1)} Mgas/s · commit{' '} {run.commit.slice(0, 7)} · {day(run.createdAt)} +
      + {run.steps.map((s) => ( +
    • +
    • + ))} +
      diff --git a/app/scaling/components/RunBackedCeilingCard.tsx b/app/scaling/components/RunBackedCeilingCard.tsx index 0881047..d3d35cf 100644 --- a/app/scaling/components/RunBackedCeilingCard.tsx +++ b/app/scaling/components/RunBackedCeilingCard.tsx @@ -2,11 +2,13 @@ import useSWR from 'swr'; +import { daWorkloads, withDaWorkloads } from '../da/workloads'; import { getDataService } from '../runs/lib/dataService'; import { getTestRunsWithStatus, type BenchmarkRuns, type BenchmarkRunWithStatus } from '../runs/lib/types'; import { toUnifiedRun } from '../runs/lib/unified'; import type { Ceiling } from '../snapshot.mjs'; import { CeilingCard } from './CeilingCards'; +import { useProvenCapacity } from './ProvenCapacity'; const gps = (r: BenchmarkRunWithStatus) => r.result?.sequencerMetrics?.gasPerSecond ?? 0; @@ -30,12 +32,16 @@ const fromRun = (c: Ceiling, run: BenchmarkRunWithStatus): Ceiling => ({ reason: 'state size not recorded', }); -export function RunBackedCeilingCard({ area, ceilings }: { area: Ceiling['area']; ceilings: Ceiling[] }) { +export function RunBackedCeilingCard({ area, ceilings: snapshotCeilings, benchUrl }: { area: Ceiling['area']; ceilings: Ceiling[]; benchUrl?: string }) { const { data } = useSWR('scaling-bench-metadata', () => getDataService().getMetadata()); + const bench = useProvenCapacity(benchUrl); + const loaded = bench.error ? undefined : bench.data; + const workloads = daWorkloads(loaded?.da); + const ceilings = loaded ? withDaWorkloads(snapshotCeilings, workloads) : snapshotCeilings; const run = data && newestPassing(data); const execution = ceilings.find((c) => c.area === 'execution')!; // A measured or modeled snapshot value outranks a run-backed estimate; the snapshot is also the fallback without the API. - if (!run || execution.tag === 'measured' || execution.tag === 'modeled') return ; + if (!run || execution.tag === 'measured' || execution.tag === 'modeled') return ; const live = ceilings.map((c) => (c === execution ? fromRun(c, run) : c)); - return ; + return ; } diff --git a/app/scaling/components/firstScreen.test.tsx b/app/scaling/components/firstScreen.test.tsx index 44a8d72..7770a94 100644 --- a/app/scaling/components/firstScreen.test.tsx +++ b/app/scaling/components/firstScreen.test.tsx @@ -3,9 +3,10 @@ import { readFileSync } from 'node:fs'; import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it } from 'vitest'; +import type { Run } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; import { CeilingCards } from './CeilingCards'; -import { Guardrails } from './Guardrails'; +import { Guardrails, scorecardGuardrails } from './Guardrails'; import { Progress } from './Progress'; // Synthetic data only: real snapshots are internal and never committed. @@ -42,8 +43,10 @@ describe('Guardrails', () => { expect(rows[0]).toContain('bg-bds-green-50'); expect(rows[1]).toContain('≥ 99.9 %'); expect(rows[1]).toContain('bg-bds-red-50'); - expect(rows[2]).toContain('>Provisional<'); + expect(rows[2]).toContain('≤ 3 blocks'); + expect(rows[2]).not.toContain('>Provisional<'); expect(rows[2]).toContain('Synthetic owner'); + expect(rows[2]).toContain('bg-bds-green-50'); expect(rows[3]).toContain('>Unset<'); expect(rows[3]).toContain('bg-bds-gray-30'); expect(rows[3]).not.toMatch(/bg-bds-(green|red)/); @@ -62,6 +65,45 @@ describe('Guardrails', () => { }); }); +describe('scorecardGuardrails', () => { + const LATENCY = { p50Ms: 2, p99Ms: 9, maxMs: 12 }; + const run = (blocksPerSecond: number): Run => ({ + id: 'synthetic', outputDir: 'synthetic-400', workload: 'mix', mgas: 400, gps: 4e8, tps: 6000, passed: true, commit: 'c0ffee', + createdAt: Date.parse('2026-01-05T00:00:00Z'), steps: [], da: null, + replay: { blocksPerSecond, newPayload: LATENCY, match: true }, + proofNode: { + blocksPerSecond: 20, newPayload: LATENCY, match: true, + proofs: { headLagBlocks: { samples: 10, p50: 1, p99: 4, max: 9 }, failedProbes: 0, methods: { debug_executePayload: { errors: 0, latency: LATENCY } }, witnessBytes: { samples: 0, p50: 0, p99: 0, max: 0 } }, + }, + }); + const byId = (rows: ReturnType) => Object.fromEntries(rows.map((g) => [g.id, g])); + + it('judges validator keep-up against 3 blocks, unknown without a snapshot value', () => { + const guardrails = fixture().guardrails; + expect(byId(scorecardGuardrails(guardrails, undefined))['validator-keep-up']).toMatchObject({ value: 3, threshold: 3, thresholdTag: 'set', status: 'ok' }); + const breached = guardrails.map((g) => (g.id === 'validator-keep-up' ? { ...g, value: 4 } : g)); + expect(byId(scorecardGuardrails(breached, undefined))['validator-keep-up'].status).toBe('breached'); + const rows = scorecardGuardrails(guardrails.filter((g) => g.id !== 'validator-keep-up'), undefined); + expect(rows.at(-1)).toMatchObject({ id: 'validator-keep-up', value: null, threshold: 3, status: 'unknown' }); + }); + + it('adds bench rows only with a bench host: proof-node lag and latency without a threshold, and replay headroom against 5 blocks/s', () => { + expect(scorecardGuardrails(fixture().guardrails, undefined)).toHaveLength(6); + const rows = byId(scorecardGuardrails(fixture().guardrails, run(36.4))); + expect(rows['proof-node-head-lag']).toMatchObject({ value: 4, unit: 'blocks', threshold: null, thresholdTag: 'unset', status: 'unknown', asOf: '2026-01-05T00:00:00.000Z' }); + expect(rows['proof-node-head-lag'].help).toContain('From bench run synthetic-400. Threshold pending.'); + expect(rows['proof-node-execute-payload']).toMatchObject({ value: 9, unit: 'ms', status: 'unknown' }); + expect(rows['validator-replay-headroom']).toMatchObject({ value: 36.4, threshold: 5, comparator: '>=', status: 'ok' }); + expect(byId(scorecardGuardrails(fixture().guardrails, run(4)))['validator-replay-headroom'].status).toBe('breached'); + }); + + it('shows bench rows as unknown when no qualifying run is loaded', () => { + const rows = scorecardGuardrails(fixture().guardrails, null).slice(-3); + expect(rows.map((g) => [g.value, g.status])).toEqual([[null, 'unknown'], [null, 'unknown'], [null, 'unknown']]); + expect(rows[0].help).toContain('No qualifying mainnet-mix bench run is loaded.'); + }); +}); + 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); diff --git a/app/scaling/components/provenCapacity.test.tsx b/app/scaling/components/provenCapacity.test.tsx index 37e9ed5..a5bd86c 100644 --- a/app/scaling/components/provenCapacity.test.tsx +++ b/app/scaling/components/provenCapacity.test.tsx @@ -17,15 +17,26 @@ vi.mock('./Info', () => ({ Info: ({ children }: { children: ReactNode }) => ({ outputDir: `synthetic-${mgas}`, mgas, gps: (mgas - 5) * 1e6, passed: true, commit: 'bbbbbbbbbbbbbbbb', createdAt: T0 + 5 * DAY }); -const bench = (mgas: number, atTop: boolean): Bench => ({ - current: { mgas, atTop, run: run(mgas) }, +const STEPS: Run['steps'] = [ + { name: 'sequencer', enabled: true, status: 'passed' }, + { name: 'validator_replay', enabled: true, status: 'passed' }, + { name: 'tee_proof', enabled: false, status: 'not-implemented', reason: 'needs a synthetic L1' }, +]; +const run = (mgas: number, workload: Run['workload'] = 'mix', da: Run['da'] = null): Run => ({ + id: `synthetic-${workload}-${mgas}`, outputDir: `synthetic-${mgas}`, workload, mgas, gps: (mgas - 5) * 1e6, tps: mgas * 10.5, passed: true, + commit: 'bbbbbbbbbbbbbbbb', createdAt: T0 + 5 * DAY, steps: STEPS, replay: null, proofNode: null, da, +}); +const none = { mix: null, payment: null, swap: null }; +const bench = (mgas: number, atTop: boolean, more: Partial = {}): Bench => ({ + current: { ...none, mix: { mgas, atTop, run: run(mgas) } }, + da: none, history: [ { at: T0, mgas: 40, commit: 'aaaaaaaaaaaaaaaa', previousCommit: null, compareUrl: null }, { at: T0 + 5 * DAY, mgas, commit: 'bbbbbbbbbbbbbbbb', previousCommit: 'aaaaaaaaaaaaaaaa', compareUrl: 'https://github.com/base/base/compare/aaaa...bbbb' }, ], unknown: [], now: T0 + 7 * DAY, + ...more, }); const snapshot: Snapshot = JSON.parse(readFileSync(new URL('../fixtures/synthetic.v2.json', import.meta.url), 'utf8')); const render = (props: { benchUrl?: string; dashboardUrl?: string } = { benchUrl: BENCH }) => @@ -62,13 +73,16 @@ describe('Ladder bench capacity', () => { swr.result = { data: bench(400, true) }; const html = render(); expect(rung(html, 'Qualified sustained')).toMatch(/^≥ 400]*>measured · bench$/); - for (const text of ['latest main run in the last 14 days was comparable and delivered ≥ 95% of target', 'Run synthetic-400, commit bbbbbbb, 2026-01-06.', '400 Mgas/s is the highest rate the bench runs', 'DA and proof guardrails are not checked']) { + for (const text of ['latest main run in the last 14 days was comparable and delivered ≥ 95% of target', 'Run synthetic-400, commit bbbbbbb, 2026-01-06.', '400 Mgas/s is the highest rate the bench runs', 'Comparable requires every enabled pipeline step to pass']) { expect(html).toContain(text); } - expect(html).toContain('Qualified by synthetic-400: 400 Mgas/s run delivered 395.0 Mgas/s'); + expect(html).toContain('Qualified by synthetic-400: 400 Mgas/s run delivered 395.0 Mgas/s'); + expect(html).toMatch(/bg-bds-green-50"><\/span>validator replay passed { @@ -81,13 +95,31 @@ describe('Ladder bench capacity', () => { }); it('shows an empty state when no run counts, and warns when run files failed to load', () => { - swr.result = { data: { current: null, history: [], unknown: [], now: T0 } }; + swr.result = { data: { current: none, da: none, history: [], unknown: [], now: T0 } }; expect(render()).toContain('No main run passed in the last 14 days.'); - swr.result = { data: { current: null, history: [], unknown: ['r1', 'r2'], now: T0 } }; + swr.result = { data: { current: none, da: none, history: [], unknown: ['r1', 'r2'], now: T0 } }; expect(render()).toContain('2 run(s) could not be loaded'); expect(render()).not.toContain('No main run passed'); }); + it('translates to bench payments/s and swaps/s, pending without a qualifying run, and the snapshot constants without bench data', () => { + expect(render({})).toContain('1 Mgas/s ≈ 19 payments/s ≈ swaps/s pending'); + swr.result = { data: bench(400, true, { current: { mix: null, swap: null, payment: { mgas: 200, atTop: false, run: run(200, 'payment') } } }) }; + expect(render()).toContain('≥ 2,100 payments/s (B20 transfers) · swaps/s (Uniswap v3) pending'); + }); + + it('binds a measured DA ceiling once payments and swaps have bench DA runs', () => { + const da = (frame_bytes: number): Run['da'] => ({ + result: { seconds: 600, gas_used: 4e10, transactions: 1e6, transaction_bytes: 0, frame_bytes, gas_per_second: 0, bytes_per_gas: 0, compression_ratio: 0, blobs_per_l1_slot: 15, blob_fill: 1, blob_target_gas_per_second: 0 }, + target: 14, + withinTarget: false, + }); + swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', da(8e7)), swap: run(200, 'swap', da(4e7)) } }) }; + const html = render(); + expect(ceilingLabel(html)).toBe('DA'); + expect(rung(html, 'Ceiling · DA')).toMatch(/^76]*>modeled$/); + }); + it('links the dashboard only when set', () => { expect(render({ dashboardUrl: 'https://dashboards.example.com/scaling' })).toContain('href="https://dashboards.example.com/scaling">Mainnet load, gas limit, DA ceiling →'); expect(render()).not.toContain('Mainnet load'); diff --git a/app/scaling/components/runBacked.test.tsx b/app/scaling/components/runBacked.test.tsx index 445a16e..56bff69 100644 --- a/app/scaling/components/runBacked.test.tsx +++ b/app/scaling/components/runBacked.test.tsx @@ -6,8 +6,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import runFixture from '../runs/fixtures/synthetic.json'; import { getTestRunsWithStatus, type BenchmarkRuns } from '../runs/lib/types'; import { toUnifiedRun, type LoadRun } from '../runs/lib/unified'; +import type { Run } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; import { CeilingCards } from './CeilingCards'; +import { DaWorkloads } from './DaWorkloads'; import { Ladder } from './Ladder'; import { Progress } from './Progress'; import { RelatedRuns } from './RelatedRuns'; @@ -85,3 +87,32 @@ it('shows gas/s and status for evidence that points at a run page', () => { expect(at('https://example.com/scaling/runs/load/sepolia/2026-01-08T00-00-01')).toMatch(/12\.5 Mgas\/s.*?success/); expect(at('https://example.com/synthetic/doc')).not.toContain('Mgas/s'); }); + +describe('DA from bench runs', () => { + const da = (frame_bytes: number, withinTarget: boolean | null): Run['da'] => ({ + result: { seconds: 600, gas_used: 4e10, transactions: 1e6, transaction_bytes: 0, frame_bytes, gas_per_second: 1.9e8, bytes_per_gas: 0, compression_ratio: 0, blobs_per_l1_slot: 15.5, blob_fill: 1, blob_target_gas_per_second: 0 }, + target: 14, + withinTarget, + }); + const run = (workload: Run['workload'], runDa: Run['da']): Run => ({ + id: `synthetic-${workload}`, outputDir: `synthetic-${workload}`, workload, mgas: 200, gps: 2e8, tps: 4000, passed: true, + commit: 'c0ffee', createdAt: Date.parse('2026-01-05T00:00:00Z'), steps: [], replay: null, proofNode: null, da: runDa, + }); + const rows = (html: string) => html.split(' { + const html = rows(renderToStaticMarkup()); + expect(html[0]).toMatch(/>~100<.*>Estimated<.*b20-transfer.*, pending/); + expect(html[0]).toContain('>—<'); + }); + + it('switches a payload to measured with its run link, and shows blobs per slot against the target', () => { + swr.byKey['scaling-proven-capacity'] = { data: { da: { payment: run('payment', da(8e7, false)), swap: null, mix: run('mix', da(4e7, null)) } } }; + const html = rows(renderToStaticMarkup()); + expect(html[0]).toMatch(/>80<.*>40,000<.*>Measured<.*href="\/scaling\/runs\/bench\/synthetic-payment"[^>]*>bench 200 Mgas\/s run]*>overEstimated<'); + expect(html[2]).toContain('>155<'); + expect(html[2]).toMatch(/15\.5 \/ 14<\/span>/); + }); +}); diff --git a/app/scaling/da/workloads.test.ts b/app/scaling/da/workloads.test.ts index a6a3c6b..bfe0d63 100644 --- a/app/scaling/da/workloads.test.ts +++ b/app/scaling/da/workloads.test.ts @@ -1,13 +1,14 @@ import { expect, it } from 'vitest'; import type { Ceiling } from '../snapshot.mjs'; -import { BLOB_MAX, BLOB_TARGET, daCapacity, withDaWorkloads, type DaWorkload } from './workloads'; +import type { Da, Run } from '../proven/capacity'; +import { BLOB_MAX, BLOB_TARGET, daCapacity, daWorkloads, DA_WORKLOADS, withDaWorkloads, type DaWorkload } from './workloads'; const ceiling = (area: Ceiling['area'], value: number): Ceiling => ({ area, value, unit: 'Mgas/s', tag: 'modeled', formula: 'snapshot', inputs: [], help: 'snapshot', }); const workload = (label: string, bytesPerTx: number, gasPerTx: number, tag: DaWorkload['tag']): DaWorkload => ({ - label, benchWorkload: label, bytesPerTx, gasPerTx, tag, asOf: null, + label, payload: 'mix', benchWorkload: label, bytesPerTx, gasPerTx, tag, asOf: null, }); it('turns bytes per transaction into tx/s and gas/s at a blob count', () => { @@ -32,3 +33,32 @@ it('is modeled once every workload is measured', () => { const [da] = withDaWorkloads([ceiling('da', 820)], [workload('mix', 155, 193_000, 'measured')]); expect(da.tag).toBe('modeled'); }); + +const run = (workload: Run['workload'], da: Run['da']): Run => ({ + id: `synthetic-${workload}`, outputDir: `synthetic-${workload}`, workload, mgas: 200, gps: 2e8, tps: 4000, passed: true, + commit: 'c0ffee', createdAt: Date.parse('2026-01-05T00:00:00Z'), steps: [], replay: null, proofNode: null, da, +}); +const da = (frame_bytes: number, gas_used: number, transactions: number): Da => ({ + result: { seconds: 600, gas_used, transactions, transaction_bytes: 0, frame_bytes, gas_per_second: 0, bytes_per_gas: frame_bytes / gas_used, compression_ratio: 0, blobs_per_l1_slot: 15.5, blob_fill: 1, blob_target_gas_per_second: 0 }, + target: 14, + withinTarget: false, +}); + +it('measures bytes/tx and gas/tx from a bench DA run, keeping the mainnet mix and estimates without one', () => { + const payment = run('payment', da(8e7, 4e10, 1e6)); + const [payments, swaps, mix] = daWorkloads({ payment, swap: run('swap', null), mix: run('mix', da(1, 1, 1)) }); + expect(payments).toMatchObject({ + bytesPerTx: 80, gasPerTx: 40_000, tag: 'measured', asOf: '2026-01-05T00:00:00.000Z', + source: { label: 'bench 200 Mgas/s run', url: '/scaling/runs/bench/synthetic-payment' }, bench: { run: payment }, + }); + // bytes/tx ÷ gas/tx is the run's frame bytes per gas, so the 14-blob formula matches the run's own. + expect(daCapacity(payments, BLOB_TARGET).mgasPerSecond).toBeCloseTo((14 * 130_044) / 12 / payment.da!.result.bytes_per_gas / 1e6); + expect(swaps).toBe(DA_WORKLOADS[1]); + expect(mix).toMatchObject({ bytesPerTx: DA_WORKLOADS[2].bytesPerTx, tag: 'measured', bench: { da: { withinTarget: false } } }); + expect(daWorkloads(undefined)).toEqual(DA_WORKLOADS); +}); + +it('is modeled once payments and swaps are measured', () => { + const [da2] = withDaWorkloads([ceiling('da', 820)], daWorkloads({ payment: run('payment', da(8e7, 4e10, 1e6)), swap: run('swap', da(2e8, 1.4e11, 1e6)), mix: null })); + expect(da2.tag).toBe('modeled'); +}); diff --git a/app/scaling/da/workloads.ts b/app/scaling/da/workloads.ts index ba621c6..63a3e17 100644 --- a/app/scaling/da/workloads.ts +++ b/app/scaling/da/workloads.ts @@ -1,3 +1,4 @@ +import { runHref, type Da, type ProvenCapacity, type Run, type Workload } from '../proven/capacity'; import type { Ceiling, Link } from '../snapshot.mjs'; // Ethereum's blob target and max per L1 block since BPO2 (2026-01-07), and the data one blob carries. @@ -8,21 +9,26 @@ const L1_SLOT_SECONDS = 12; export type DaWorkload = { label: string; + payload: Workload; benchWorkload: string; gasPerTx: number; bytesPerTx: number; tag: 'measured' | 'estimated'; asOf: string | null; source?: Link; + // The latest bench run's DA step, shown beside the row whether or not it set bytes/tx. + bench?: { run: Run; da: Da }; }; -// Bytes per transaction come from `base-bench da`; gas per transaction for bench workloads from the -// load-test calibration (etc/scripts/local/mainnet-mix.py in base/base). +// Bytes per transaction are compressed batch frame bytes per non-deposit transaction from +// `base-bench da`, the bytes that fill a blob; gas per transaction for bench workloads comes from +// the load-test calibration (etc/scripts/local/mainnet-mix.py in base/base). export const DA_WORKLOADS: DaWorkload[] = [ - { label: 'Payments (B20)', benchWorkload: 'b20-transfer', gasPerTx: 37_743, bytesPerTx: 100, tag: 'estimated', asOf: null }, - { label: 'Swaps', benchWorkload: 'uniswap-swap', gasPerTx: 143_987, bytesPerTx: 190, tag: 'estimated', asOf: null }, + { label: 'Payments (B20)', payload: 'payment', benchWorkload: 'b20-transfer', gasPerTx: 37_743, bytesPerTx: 100, tag: 'estimated', asOf: null }, + { label: 'Swaps', payload: 'swap', benchWorkload: 'uniswap-swap', gasPerTx: 143_987, bytesPerTx: 190, tag: 'estimated', asOf: null }, { label: 'Mainnet mix', + payload: 'mix', benchWorkload: 'mainnet blocks 53,091,727–53,095,326', gasPerTx: 193_349, bytesPerTx: 154.7, @@ -32,6 +38,28 @@ export const DA_WORKLOADS: DaWorkload[] = [ }, ]; +// A bench DA run replaces an estimate with frame_bytes ÷ transactions and gas_used ÷ transactions, so +// bytes/tx ÷ gas/tx is the run's bytes per gas. The mainnet mix keeps its measurement from real +// mainnet blocks, which the synthetic bench mix only approximates. +export function daWorkloads(da: ProvenCapacity['da'] | undefined, workloads: DaWorkload[] = DA_WORKLOADS): DaWorkload[] { + return workloads.map((w) => { + const run = da?.[w.payload]; + if (!run?.da) return w; + const bench = { run, da: run.da }; + if (w.tag === 'measured') return { ...w, bench }; + const { frame_bytes, gas_used, transactions } = run.da.result; + return { + ...w, + gasPerTx: gas_used / transactions, + bytesPerTx: frame_bytes / transactions, + tag: 'measured', + asOf: new Date(run.createdAt).toISOString(), + source: { label: `bench ${run.mgas} Mgas/s run`, url: runHref(run) }, + bench, + }; + }); +} + export function daCapacity(w: DaWorkload, blobs: number): { txPerSecond: number; mgasPerSecond: number } { const txPerSecond = (blobs * BLOB_DATA_BYTES) / L1_SLOT_SECONDS / w.bytesPerTx; return { txPerSecond, mgasPerSecond: (txPerSecond * w.gasPerTx) / 1e6 }; diff --git a/app/scaling/guardrails/page.tsx b/app/scaling/guardrails/page.tsx index 79e8154..15477b3 100644 --- a/app/scaling/guardrails/page.tsx +++ b/app/scaling/guardrails/page.tsx @@ -13,7 +13,7 @@ export default async function ScalingGuardrailsPage() { return (
      - + {outcomes ? : null}
      ); diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 725ee05..0ab985d 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -66,6 +66,14 @@ it('passes the runtime bench and dashboard URLs to the ladder', async () => { expect(html).toContain('href="https://dashboards.example.com/scaling"'); }); +it('adds bench guardrail rows when a bench host is set, unknown until its results load', async () => { + vi.stubEnv('SCALING_BENCH_BASE_URL', 'https://bench.example.com'); + for (const html of [renderToStaticMarkup(await ScalingGuardrailsPage()), renderToStaticMarkup(await ScalingPage())]) { + for (const label of ['Proof-node head lag p99', 'debug_executePayload p99', 'Validator replay headroom']) expect(html).toContain(label); + } + expect(renderToStaticMarkup(await ScalingGuardrailsPage())).toContain('No qualifying mainnet-mix bench run is loaded.'); +}); + it('links the first-screen guardrail strip to the guardrails page', async () => { expect(renderToStaticMarkup(await ScalingPage())).toContain('Guardrails →'); }); @@ -74,7 +82,9 @@ it('shows guardrail help inline and the outcomes table on the guardrails page', 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); + for (const g of fixture.guardrails) if (g.id !== 'validator-keep-up') expect(html).toContain(g.help); + expect(html).toContain('They must stay within 3 blocks.'); + expect(html).not.toContain('Proof-node head lag p99'); expect(html).not.toContain('Guardrails →'); expect(html).toContain('Inclusion latency p50'); expect(html).toContain('synthetic.inclusion_latency'); diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index b9d928a..b5e19b9 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -18,13 +18,14 @@ export default async function ScalingPage() { const { snapshot } = loaded; const ceilings = withDaWorkloads(snapshot.ceilings); + const benchUrl = process.env.SCALING_BENCH_BASE_URL; return (
      Runs →} /> - - - + + + diff --git a/app/scaling/proven/capacity.test.ts b/app/scaling/proven/capacity.test.ts index 6fa8279..5c81e05 100644 --- a/app/scaling/proven/capacity.test.ts +++ b/app/scaling/proven/capacity.test.ts @@ -1,22 +1,44 @@ import { expect, it } from 'vitest'; -import { loadProvenCapacity } from './capacity'; +import { loadProvenCapacity, PAYLOADS, type Workload } from './capacity'; const NOW = Date.parse('2026-02-01T00:00:00Z'); const DAY = 86_400_000; +const HTML = ''; type Spec = { dir: string; mgas: number; daysAgo: number; + workload?: Workload; ref?: string | null; comparable?: boolean; delivered?: number; scenario?: string; benchmarkRun?: string; fail?: string; + // false: written before the step pipeline, so no pipeline-result.json. + pipeline?: boolean; + missing?: string[]; + withinTarget?: boolean | null; }; +const LATENCY = { meanMs: 3, p50Ms: 2, p99Ms: 9, maxMs: 12 }; +const STEPS = (s: Spec) => [ + { name: 'sequencer', enabled: true, status: 'passed', reason: null, durationSeconds: 900, outputs: {} }, + { name: 'validator_replay', enabled: true, status: s.comparable === false ? 'failed' : 'passed', reason: s.comparable === false ? 'replay mismatch' : null, durationSeconds: 130, outputs: {} }, + { name: 'da', enabled: true, status: 'passed', reason: null, durationSeconds: 20, outputs: { target: 14, blobsPerL1Slot: 15.5, blobTargetGasPerSecond: 1.9e8, withinTarget: s.withinTarget === undefined ? false : s.withinTarget } }, + { name: 'tee_proof', enabled: false, status: 'not-implemented', reason: 'needs a synthetic L1' }, +]; +const REPLAY = { blocksPerSecond: 36.4, newPayload: LATENCY, forkchoice: LATENCY, match: true, blocks: [{ number: 1 }] }; +const PROOFS = { + headLagBlocks: { samples: 3000, p50: 1, p99: 4, max: 9 }, + failedProbes: 0, + methods: { debug_executePayload: { errors: 0, latency: LATENCY } }, + witnessBytes: { samples: 3000, p50: 4096, p99: 8192, max: 9000 }, +}; +const DA = { seconds: 600, gas_used: 6e10, transactions: 1e6, transaction_bytes: 1.5e8, frame_bytes: 6e7, gas_per_second: 1e8, bytes_per_gas: 0.001, compression_ratio: 2.5, blobs_per_l1_slot: 15.5, blob_fill: 0.98, blob_target_gas_per_second: 1.5e8 }; + // Run outputs are cached by outputDir across calls, so every test uses its own dirs. function load(specs: Spec[]) { const metadata = { @@ -24,10 +46,10 @@ function load(specs: Spec[]) { outputDir: s.dir, createdAt: new Date(NOW - s.daysAgo * DAY).toISOString(), testConfig: { - BenchmarkRun: s.benchmarkRun ?? 'mainnet-snapshot', + BenchmarkRun: s.benchmarkRun ?? `run-${s.dir}`, ChainId: 8453, Scenario: s.scenario ?? `sustained-${s.mgas}mgas`, - TransactionPayload: 'mainnet-mix', + TransactionPayload: PAYLOADS[s.workload ?? 'mix'], }, result: { success: true }, })), @@ -35,31 +57,87 @@ function load(specs: Spec[]) { const fetchImpl = async (url: string) => { const path = url.replace('https://bench.example/output/', ''); if (path === 'metadata.json') return Response.json(metadata); - const [dir, file] = path.split('/'); + const [dir, ...rest] = path.split('/'); + const file = rest.join('/'); const s = specs.find((x) => x.dir === dir)!; if (s.fail === file) return new Response('error', { status: 500 }); - if (file === 'validation.json') return Response.json({ comparable: s.comparable ?? true, reasons: [] }); - if (file === 'load-test-result.json') return Response.json({ throughput: { gps: (s.delivered ?? 1) * s.mgas * 1e6 } }); - if (s.ref === null) return new Response(''); + if (s.missing?.includes(file) || (file === 'pipeline-result.json' && s.pipeline === false)) return new Response(HTML); + const delivered = (s.delivered ?? 1) * s.mgas; + switch (file) { + case 'validation.json': return Response.json({ comparable: s.comparable ?? true, reasons: [] }); + case 'load-test-result.json': return Response.json({ throughput: { gps: delivered * 1e6, tps: delivered * 10 } }); + case 'pipeline-result.json': return Response.json({ suite: 'synthetic', comparable: s.comparable ?? true, steps: STEPS(s) }); + case 'replay-result.json': return Response.json(REPLAY); + case 'proof-node/replay-result.json': return Response.json({ ...REPLAY, blocksPerSecond: 20, proofs: PROOFS }); + case 'da-result.json': return Response.json(DA); + } + if (s.ref === null) return new Response(HTML); return Response.json({ source: { repository: 'base/base', ref: s.ref ?? 'main', revision: `rev-${dir}` } }); }; return loadProvenCapacity('https://bench.example', fetchImpl as typeof fetch, NOW); } -it('counts only runs built from main', async () => { +it('counts only pipeline runs built from main, with their steps, replays and DA result', async () => { const { current, unknown } = await load([ { dir: 'main-100', mgas: 100, daysAgo: 3 }, { dir: 'branch-html-200', mgas: 200, daysAgo: 2, ref: null }, { dir: 'branch-ref-300', mgas: 300, daysAgo: 1, ref: 'feature' }, + { dir: 'pre-pipeline-400', mgas: 400, daysAgo: 1, pipeline: false }, ]); - expect(current).toEqual({ + expect(current.mix).toEqual({ mgas: 100, atTop: true, - run: { outputDir: 'main-100', mgas: 100, gps: 100e6, passed: true, commit: 'rev-main-100', createdAt: NOW - 3 * DAY }, + run: { + id: 'run-main-100', + outputDir: 'main-100', + workload: 'mix', + mgas: 100, + gps: 100e6, + tps: 1000, + passed: true, + commit: 'rev-main-100', + createdAt: NOW - 3 * DAY, + steps: STEPS({ dir: '', mgas: 0, daysAgo: 0 }), + replay: { blocksPerSecond: 36.4, newPayload: LATENCY, match: true }, + proofNode: { blocksPerSecond: 20, newPayload: LATENCY, match: true, proofs: PROOFS }, + da: { result: DA, target: 14, withinTarget: false }, + }, }); expect(unknown).toEqual([]); }); +it('keeps each payload separate, with the latest DA run per payload', async () => { + const { current, da, history } = await load([ + { dir: 'pay-mix-400', mgas: 400, daysAgo: 3 }, + { dir: 'pay-b20-200', mgas: 200, daysAgo: 3, workload: 'payment' }, + { dir: 'pay-b20-300', mgas: 300, daysAgo: 2, workload: 'payment', delivered: 0.5 }, + { dir: 'pay-b20-old', mgas: 100, daysAgo: 20, workload: 'payment' }, + { dir: 'pay-swap-100', mgas: 100, daysAgo: 1, workload: 'swap', missing: ['da-result.json'] }, + ]); + expect(current.mix).toMatchObject({ mgas: 400, run: { outputDir: 'pay-mix-400' } }); + expect(current.payment).toMatchObject({ mgas: 200, atTop: false, run: { tps: 2000 } }); + expect(current.swap).toMatchObject({ mgas: 100, run: { da: null } }); + expect(da.mix?.outputDir).toBe('pay-mix-400'); + expect(da.payment?.outputDir).toBe('pay-b20-300'); + expect(da.swap).toBeNull(); + expect(history.map((s) => s.mgas)).toEqual([400]); +}); + +it('fails a run whose pipeline step failed', async () => { + const { current } = await load([ + { dir: 'step-100', mgas: 100, daysAgo: 3 }, + { dir: 'step-200', mgas: 200, daysAgo: 2, comparable: false }, + ]); + expect(current.mix).toMatchObject({ mgas: 100, atTop: false }); +}); + +it('reads missing replay, proof-node and DA files as null, and an unjudged blob target as null', async () => { + const { current } = await load([ + { dir: 'partial-100', mgas: 100, daysAgo: 1, missing: ['replay-result.json', 'proof-node/replay-result.json'], withinTarget: null }, + ]); + expect(current.mix?.run).toMatchObject({ replay: null, proofNode: null, da: { target: 14, withinTarget: null } }); +}); + it('ignores alias copies and non-sustained scenarios', async () => { const { current, unknown } = await load([ { dir: 'cand-100', mgas: 100, daysAgo: 3 }, @@ -67,7 +145,7 @@ it('ignores alias copies and non-sustained scenarios', async () => { { dir: 'cand-glam-200', mgas: 200, daysAgo: 2, scenario: 'glamsterdam-sustained-200mgas' }, { dir: 'cand-scrape-400', mgas: 400, daysAgo: 2, scenario: 'sustained-400mgas-scrape200ms' }, ]); - expect(current).toMatchObject({ mgas: 100, atTop: true }); + expect(current.mix).toMatchObject({ mgas: 100, atTop: true }); expect(unknown).toEqual([]); }); @@ -78,7 +156,7 @@ it('fails runs that are not comparable or deliver under 95% of target', async () { dir: 'fail-150', mgas: 150, daysAgo: 3, delivered: 0.94 }, { dir: 'fail-200', mgas: 200, daysAgo: 3, comparable: false }, ]); - expect(current).toMatchObject({ mgas: 120, atTop: false, run: { gps: 114e6 } }); + expect(current.mix).toMatchObject({ mgas: 120, atTop: false, run: { gps: 114e6 } }); }); it('ignores runs older than 14 days', async () => { @@ -86,7 +164,7 @@ it('ignores runs older than 14 days', async () => { { dir: 'window-100', mgas: 100, daysAgo: 13 }, { dir: 'window-200', mgas: 200, daysAgo: 14 }, ]); - expect(current).toMatchObject({ mgas: 100, atTop: true }); + expect(current.mix).toMatchObject({ mgas: 100, atTop: true }); }); it('uses the latest run at each rate over an older pass', async () => { @@ -95,7 +173,7 @@ it('uses the latest run at each rate over an older pass', async () => { { dir: 'latest-200-old', mgas: 200, daysAgo: 4 }, { dir: 'latest-200-new', mgas: 200, daysAgo: 1, comparable: false }, ]); - expect(current).toMatchObject({ mgas: 100, atTop: false }); + expect(current.mix).toMatchObject({ mgas: 100, atTop: false }); }); it('steps history when proven capacity changes, with compare links', async () => { @@ -134,13 +212,15 @@ it('dates a drop when the higher pass ages out of the window', async () => { ]); }); -it('marks a run unknown when one of its files fails, keeping the rest', async () => { +it('marks a run unknown when one of its files fails or its results are missing, keeping the rest', async () => { const { current, unknown } = await load([ { dir: 'iso-100', mgas: 100, daysAgo: 3 }, { dir: 'iso-200', mgas: 200, daysAgo: 2, fail: 'load-test-result.json' }, + { dir: 'iso-300', mgas: 300, daysAgo: 2, fail: 'da-result.json' }, + { dir: 'iso-400', mgas: 400, daysAgo: 2, missing: ['validation.json'] }, ]); - expect(current).toMatchObject({ mgas: 100, atTop: true }); - expect(unknown).toEqual(['iso-200']); + expect(current.mix).toMatchObject({ mgas: 100, atTop: true }); + expect(unknown.toSorted()).toEqual(['iso-200', 'iso-300', 'iso-400']); }); it('revalidates the run index instead of trusting a cached copy', async () => { diff --git a/app/scaling/proven/capacity.ts b/app/scaling/proven/capacity.ts index 7c20db1..d10a024 100644 --- a/app/scaling/proven/capacity.ts +++ b/app/scaling/proven/capacity.ts @@ -1,12 +1,56 @@ import type { BenchmarkRun, BenchmarkRuns } from '../runs/lib/types'; +export const PAYLOADS = { mix: 'mainnet-mix', payment: 'b20-transfer-existing', swap: 'uniswap-v3-swap' } as const; +export type Workload = keyof typeof PAYLOADS; + +export type PipelineStep = { + name: string; + enabled: boolean; + status: 'passed' | 'failed' | 'skipped' | 'not-implemented'; + reason?: string | null; + durationSeconds?: number; + outputs?: Record; +}; +type Latency = { p50Ms: number; p99Ms: number; maxMs: number }; +type Counts = { samples: number; p50: number; p99: number; max: number }; +export type Replay = { blocksPerSecond: number; newPayload: Latency; match: boolean }; +export type Proofs = { + headLagBlocks: Counts; + failedProbes: number; + methods: Record; + witnessBytes: Counts; +}; +// `base-bench da` writes snake_case. +export type DaResult = { + seconds: number; + gas_used: number; + transactions: number; + transaction_bytes: number; + frame_bytes: number; + gas_per_second: number; + bytes_per_gas: number; + compression_ratio: number; + blobs_per_l1_slot: number; + blob_fill: number; + blob_target_gas_per_second: number; +}; +// `target` and `withinTarget` come from the pipeline's DA step; the blob target is recorded, not gating. +export type Da = { result: DaResult; target: number | null; withinTarget: boolean | null }; + export type Run = { + id: string; outputDir: string; + workload: Workload; mgas: number; gps: number; + tps: number; passed: boolean; commit: string; createdAt: number; + steps: PipelineStep[]; + replay: Replay | null; + proofNode: (Replay & { proofs: Proofs | null }) | null; + da: Da | null; }; export type Proven = { mgas: number; atTop: boolean; run: Run }; @@ -19,16 +63,26 @@ export type Step = { compareUrl: string | null; }; -export type ProvenCapacity = { current: Proven | null; history: Step[]; unknown: string[] }; +// `history` is the mainnet mix's; `da` is the latest counting run per workload with a DA result. +export type ProvenCapacity = { + current: Record; + da: Record; + history: Step[]; + unknown: string[]; +}; const WINDOW_MS = 14 * 24 * 60 * 60 * 1000; const PASS_RATIO = 0.95; const CONCURRENCY = 6; const SCENARIO = /^sustained-(\d+)mgas$/; +const WORKLOADS = Object.keys(PAYLOADS) as Workload[]; -// Run outputs never change once written. `null` marks a run not built from main. +// Run outputs never change once written. `null` marks a run that does not count: not built from +// main, or written before the step pipeline. const runCache = new Map(); +export const runHref = (run: Run) => `/scaling/runs/bench/${encodeURIComponent(run.id)}`; + export async function loadProvenCapacity( baseUrl: string, fetchImpl: typeof fetch = fetch, @@ -49,16 +103,25 @@ export async function loadProvenCapacity( unknown.push(meta.outputDir); } }); - return { current: provenAt(runs, now), history: history(runs, now), unknown }; + const of = (w: Workload) => runs.filter((r) => r.workload === w); + const inWindow = (r: Run) => r.createdAt > now - WINDOW_MS && r.createdAt <= now; + const latestDa = (w: Workload) => of(w).filter((r) => r.da && inWindow(r)).sort((a, b) => b.createdAt - a.createdAt)[0] ?? null; + return { + current: Object.fromEntries(WORKLOADS.map((w) => [w, provenAt(of(w), now)])) as ProvenCapacity['current'], + da: Object.fromEntries(WORKLOADS.map((w) => [w, latestDa(w)])) as ProvenCapacity['da'], + history: history(of('mix'), now), + unknown, + }; } function candidates({ runs }: BenchmarkRuns): BenchmarkRun[] { + const payloads: string[] = Object.values(PAYLOADS); const byDir = new Map(); for (const run of runs) { const c = run.testConfig; if ( c.ChainId === 8453 && - c.TransactionPayload === 'mainnet-mix' && + payloads.includes(String(c.TransactionPayload)) && SCENARIO.test(String(c.Scenario)) && !String(c.BenchmarkRun).endsWith('--latest') ) { @@ -69,30 +132,46 @@ function candidates({ runs }: BenchmarkRuns): BenchmarkRun[] { } async function loadRun(root: string, meta: BenchmarkRun, fetchImpl: typeof fetch): Promise { + // A missing file comes back as the host's HTML fallback with a 200, so non-JSON means missing. const get = async (file: string) => { const res = await fetchImpl(`${root}${meta.outputDir}/${file}`); if (!res.ok) throw new Error(`Failed to fetch ${file}: ${res.status}`); - return res; + return res.json().catch(() => null); }; - const [validation, load, provenance] = await Promise.all([ - get('validation.json').then((r) => r.json()), - get('load-test-result.json').then((r) => r.json()), - // Only main-tracking runs write provenance.json; for others the host serves its HTML fallback with a 200. - get('provenance.json').then((r) => r.json().catch(() => null)), + const [pipeline, provenance] = await Promise.all([get('pipeline-result.json'), get('provenance.json')]); + if (!pipeline || provenance?.source?.ref !== 'main') return null; + const [validation, load, replay, proofNode, da] = await Promise.all([ + get('validation.json'), + get('load-test-result.json'), + get('replay-result.json'), + get('proof-node/replay-result.json'), + get('da-result.json'), ]); - if (provenance?.source?.ref !== 'main') return null; + if (!validation || !load) throw new Error(`Missing results for ${meta.outputDir}`); const mgas = Number(SCENARIO.exec(String(meta.testConfig.Scenario))![1]); const gps: number = load.throughput.gps; + const steps: PipelineStep[] = pipeline.steps; + const daOutputs = steps.find((s) => s.name === 'da')?.outputs; return { + id: String(meta.testConfig.BenchmarkRun), outputDir: meta.outputDir, + workload: WORKLOADS.find((w) => PAYLOADS[w] === meta.testConfig.TransactionPayload)!, mgas, gps, + tps: load.throughput.tps, passed: validation.comparable === true && gps >= PASS_RATIO * mgas * 1e6, commit: provenance.source.revision, createdAt: Date.parse(meta.createdAt), + steps, + replay: replay && pickReplay(replay), + proofNode: proofNode && { ...pickReplay(proofNode), proofs: proofNode.proofs ?? null }, + da: da && { result: da, target: (daOutputs?.target as number) ?? null, withinTarget: (daOutputs?.withinTarget as boolean) ?? null }, }; } +// replay-result.json also lists every block's timings; keep only the summary. +const pickReplay = ({ blocksPerSecond, newPayload, match }: Replay): Replay => ({ blocksPerSecond, newPayload, match }); + function provenAt(runs: Run[], t: number): Proven | null { const latest = new Map(); for (const run of runs) { From c6f418a1aeffafcdd0673a333e6ba473bb3b3cbe Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 08:52:28 -0700 Subject: [PATCH 02/33] perf(scaling): load replay and DA files only for the runs the page shows --- app/scaling/proven/capacity.test.ts | 29 ++++++++++++- app/scaling/proven/capacity.ts | 66 +++++++++++++++++++---------- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/app/scaling/proven/capacity.test.ts b/app/scaling/proven/capacity.test.ts index 5c81e05..3933458 100644 --- a/app/scaling/proven/capacity.test.ts +++ b/app/scaling/proven/capacity.test.ts @@ -219,10 +219,37 @@ it('marks a run unknown when one of its files fails or its results are missing, { dir: 'iso-300', mgas: 300, daysAgo: 2, fail: 'da-result.json' }, { dir: 'iso-400', mgas: 400, daysAgo: 2, missing: ['validation.json'] }, ]); - expect(current.mix).toMatchObject({ mgas: 100, atTop: true }); + // A failed detail file leaves the run counted, without its details. + expect(current.mix).toMatchObject({ mgas: 300, atTop: true, run: { replay: null, da: null } }); expect(unknown.toSorted()).toEqual(['iso-200', 'iso-300', 'iso-400']); }); +it('fetches replay, proof-node and DA files only for proven and latest-DA runs', async () => { + const fetched: string[] = []; + const metadata = { runs: ['sel-100', 'sel-200', 'sel-300'].map((dir, i) => ({ + outputDir: dir, createdAt: new Date(NOW - (3 - i) * DAY).toISOString(), + testConfig: { BenchmarkRun: dir, ChainId: 8453, Scenario: `sustained-${(i + 1) * 100}mgas`, TransactionPayload: 'mainnet-mix' }, + })) }; + const fetchImpl = async (url: string) => { + const path = url.replace('https://bench.example/output/', ''); + if (path === 'metadata.json') return Response.json(metadata); + fetched.push(path); + const [dir, ...rest] = path.split('/'); + const s: Spec = { dir, mgas: Number(dir.slice(4)), daysAgo: 0, comparable: dir !== 'sel-300' }; + const file = rest.join('/'); + if (file === 'validation.json') return Response.json({ comparable: s.comparable }); + if (file === 'load-test-result.json') return Response.json({ throughput: { gps: s.mgas * 1e6, tps: 1 } }); + if (file === 'pipeline-result.json') return Response.json({ steps: STEPS(s) }); + if (file === 'provenance.json') return Response.json({ source: { ref: 'main', revision: 'r' } }); + return Response.json(file === 'da-result.json' ? DA : REPLAY); + }; + const { current, da } = await loadProvenCapacity('https://bench.example', fetchImpl as typeof fetch, NOW); + expect(current.mix?.run.outputDir).toBe('sel-200'); + expect(da.mix?.outputDir).toBe('sel-300'); + const details = fetched.filter((p) => /replay-result|da-result/.test(p)).map((p) => p.split('/')[0]); + expect([...new Set(details)].toSorted()).toEqual(['sel-200', 'sel-300']); +}); + it('revalidates the run index instead of trusting a cached copy', async () => { const inits = new Map(); const fetchImpl = async (url: string, init?: RequestInit) => { diff --git a/app/scaling/proven/capacity.ts b/app/scaling/proven/capacity.ts index d10a024..48c231a 100644 --- a/app/scaling/proven/capacity.ts +++ b/app/scaling/proven/capacity.ts @@ -63,7 +63,9 @@ export type Step = { compareUrl: string | null; }; -// `history` is the mainnet mix's; `da` is the latest counting run per workload with a DA result. +// `history` is the mainnet mix's; `da` is the latest counting run per workload in the window whose DA +// step passed. Replay, proof-node and DA results are loaded only for the runs in `current` and `da`: +// replay files list every block, and every other run would download them for nothing. export type ProvenCapacity = { current: Record; da: Record; @@ -80,6 +82,7 @@ const WORKLOADS = Object.keys(PAYLOADS) as Workload[]; // Run outputs never change once written. `null` marks a run that does not count: not built from // main, or written before the step pipeline. const runCache = new Map(); +const detailCache = new Map>(); export const runHref = (run: Run) => `/scaling/runs/bench/${encodeURIComponent(run.id)}`; @@ -103,13 +106,27 @@ export async function loadProvenCapacity( unknown.push(meta.outputDir); } }); - const of = (w: Workload) => runs.filter((r) => r.workload === w); const inWindow = (r: Run) => r.createdAt > now - WINDOW_MS && r.createdAt <= now; - const latestDa = (w: Workload) => of(w).filter((r) => r.da && inWindow(r)).sort((a, b) => b.createdAt - a.createdAt)[0] ?? null; + const daPassed = (r: Run) => r.steps.some((s) => s.name === 'da' && s.status === 'passed'); + const latestDa = (rs: Run[]) => rs.filter((r) => inWindow(r) && daPassed(r)).sort((a, b) => b.createdAt - a.createdAt)[0]; + const of = (rs: Run[], w: Workload) => rs.filter((r) => r.workload === w); + const selected = new Set(WORKLOADS.flatMap((w) => [provenAt(of(runs, w), now)?.run, latestDa(of(runs, w))]).filter((r) => r !== undefined)); + await forEachLimit([...selected], CONCURRENCY, async (run) => { + try { + if (!detailCache.has(run.outputDir)) detailCache.set(run.outputDir, await loadDetails(root, run, fetchImpl)); + } catch { + unknown.push(run.outputDir); + } + }); + const full = runs.map((r) => ({ ...r, ...detailCache.get(r.outputDir) })); + const daRun = (w: Workload) => { + const run = latestDa(of(full, w)); + return run?.da ? run : null; + }; return { - current: Object.fromEntries(WORKLOADS.map((w) => [w, provenAt(of(w), now)])) as ProvenCapacity['current'], - da: Object.fromEntries(WORKLOADS.map((w) => [w, latestDa(w)])) as ProvenCapacity['da'], - history: history(of('mix'), now), + current: Object.fromEntries(WORKLOADS.map((w) => [w, provenAt(of(full, w), now)])) as ProvenCapacity['current'], + da: Object.fromEntries(WORKLOADS.map((w) => [w, daRun(w)])) as ProvenCapacity['da'], + history: history(of(full, 'mix'), now), unknown, }; } @@ -131,27 +148,21 @@ function candidates({ runs }: BenchmarkRuns): BenchmarkRun[] { return [...byDir.values()]; } +// A missing file comes back as the host's HTML fallback with a 200, so non-JSON means missing. +async function getJson(fetchImpl: typeof fetch, url: string) { + const res = await fetchImpl(url); + if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`); + return res.json().catch(() => null); +} + async function loadRun(root: string, meta: BenchmarkRun, fetchImpl: typeof fetch): Promise { - // A missing file comes back as the host's HTML fallback with a 200, so non-JSON means missing. - const get = async (file: string) => { - const res = await fetchImpl(`${root}${meta.outputDir}/${file}`); - if (!res.ok) throw new Error(`Failed to fetch ${file}: ${res.status}`); - return res.json().catch(() => null); - }; + const get = (file: string) => getJson(fetchImpl, `${root}${meta.outputDir}/${file}`); const [pipeline, provenance] = await Promise.all([get('pipeline-result.json'), get('provenance.json')]); if (!pipeline || provenance?.source?.ref !== 'main') return null; - const [validation, load, replay, proofNode, da] = await Promise.all([ - get('validation.json'), - get('load-test-result.json'), - get('replay-result.json'), - get('proof-node/replay-result.json'), - get('da-result.json'), - ]); + const [validation, load] = await Promise.all([get('validation.json'), get('load-test-result.json')]); if (!validation || !load) throw new Error(`Missing results for ${meta.outputDir}`); const mgas = Number(SCENARIO.exec(String(meta.testConfig.Scenario))![1]); const gps: number = load.throughput.gps; - const steps: PipelineStep[] = pipeline.steps; - const daOutputs = steps.find((s) => s.name === 'da')?.outputs; return { id: String(meta.testConfig.BenchmarkRun), outputDir: meta.outputDir, @@ -162,7 +173,18 @@ async function loadRun(root: string, meta: BenchmarkRun, fetchImpl: typeof fetch passed: validation.comparable === true && gps >= PASS_RATIO * mgas * 1e6, commit: provenance.source.revision, createdAt: Date.parse(meta.createdAt), - steps, + steps: pipeline.steps, + replay: null, + proofNode: null, + da: null, + }; +} + +async function loadDetails(root: string, run: Run, fetchImpl: typeof fetch): Promise> { + const get = (file: string) => getJson(fetchImpl, `${root}${run.outputDir}/${file}`); + const [replay, proofNode, da] = await Promise.all([get('replay-result.json'), get('proof-node/replay-result.json'), get('da-result.json')]); + const daOutputs = run.steps.find((s) => s.name === 'da')?.outputs; + return { replay: replay && pickReplay(replay), proofNode: proofNode && { ...pickReplay(proofNode), proofs: proofNode.proofs ?? null }, da: da && { result: da, target: (daOutputs?.target as number) ?? null, withinTarget: (daOutputs?.withinTarget as boolean) ?? null }, From 1bbb9d53b9556eaa3ce03fd50718b91412234cbb Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 09:11:42 -0700 Subject: [PATCH 03/33] feat(scaling): show only the latest proven-capacity change instead of a chart The ladder keeps current state; one line notes the last increase with its compare link. Full history stays on /scaling/runs. --- app/scaling/components/ProvenCapacity.tsx | 26 +++++++------------ .../components/provenCapacity.test.tsx | 6 ++--- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/app/scaling/components/ProvenCapacity.tsx b/app/scaling/components/ProvenCapacity.tsx index 3ab452d..149d374 100644 --- a/app/scaling/components/ProvenCapacity.tsx +++ b/app/scaling/components/ProvenCapacity.tsx @@ -5,7 +5,6 @@ import useSWR from 'swr'; import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; import { loadProvenCapacity, runHref, type PipelineStep, type ProvenCapacity as Capacity } from '../proven/capacity'; -import { LineChart } from '../runs/components/LineChart'; export type Bench = Capacity & { now: number }; @@ -26,14 +25,14 @@ export function ProvenCapacity({ data, error }: { data?: Bench; error?: unknown : ; } -function Result({ capacity: { current, history, unknown, now } }: { capacity: Bench }) { +function Result({ capacity: { current, history, unknown } }: { capacity: Bench }) { // A run whose files failed to load is skipped, so an older result at its rate may count instead. const partial = unknown.length ? {unknown.length} run(s) could not be loaded, so this result may be wrong. : null; if (!current.mix) return partial ?? No main run passed in the last 14 days.; const { run } = current.mix; - const y = (m: number | null) => (m ?? 0) * 1e6; - const points = history.flatMap((s, i): [number, number][] => (i ? [[s.at, y(history[i - 1].mgas)], [s.at, y(s.mgas)]] : [[s.at, y(s.mgas)]])); - points.push([now, y(current.mix.mgas)]); + // Only the latest change is shown; the full history lives on /scaling/runs. + const last = history.length > 1 ? history[history.length - 1] : null; + const previous = last ? history[history.length - 2] : null; return ( <> {partial} @@ -49,17 +48,12 @@ function Result({ capacity: { current, history, unknown, now } }: { capacity: Be ))}
    - -
      - {history.map((s, i) => ( -
    • - {day(s.at)} - {i ? `${mgas(history[i - 1].mgas)} → ` : ''}{mgas(s.mgas)} - {s.compareUrl ? compare : null} -
    • - ))} -
    + {last && previous ? ( + + {last.mgas !== null && previous.mgas !== null && last.mgas < previous.mgas ? '↓' : '↑'} from {mgas(previous.mgas)} on {day(last.at)} + {last.compareUrl ? <> · compare : null} + + ) : null} ); } diff --git a/app/scaling/components/provenCapacity.test.tsx b/app/scaling/components/provenCapacity.test.tsx index a5bd86c..5b90e56 100644 --- a/app/scaling/components/provenCapacity.test.tsx +++ b/app/scaling/components/provenCapacity.test.tsx @@ -69,7 +69,7 @@ describe('Ladder bench capacity', () => { expect(html).not.toContain('bench'); }); - it('shows ≥ at the top rate with bench provenance, the rule, run, commit, caveat, evidence and chart', () => { + it('shows ≥ at the top rate with bench provenance, the rule, run, commit, caveat, evidence and the last change', () => { swr.result = { data: bench(400, true) }; const html = render(); expect(rung(html, 'Qualified sustained')).toMatch(/^≥ 400]*>measured · bench$/); @@ -79,8 +79,8 @@ describe('Ladder bench capacity', () => { expect(html).toContain('Qualified by synthetic-400: 400 Mgas/s run delivered 395.0 Mgas/s'); expect(html).toMatch(/bg-bds-green-50"><\/span>validator replay passedcompare'); // The page passes #214's estimated DA ceiling, which never binds. expect(ceilingLabel(html)).toBe('execution'); }); From d2646f727a8af80d6dffbb68401b86b4eb960017 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 09:35:15 -0700 Subject: [PATCH 04/33] feat(scaling): lay out the scorecard as headline, ladder, bottlenecks and subsystem table --- app/scaling/components/Bottlenecks.tsx | 73 +++++++ app/scaling/components/CeilingCards.tsx | 12 -- app/scaling/components/Guardrails.tsx | 11 +- app/scaling/components/Headline.tsx | 112 ++++++++++ app/scaling/components/Ladder.tsx | 155 ++++++-------- app/scaling/components/ProvenCapacity.tsx | 52 +---- app/scaling/components/Scorecard.tsx | 38 ++++ app/scaling/components/Subsystems.tsx | 133 ++++++++++++ app/scaling/components/firstScreen.test.tsx | 9 +- .../components/provenCapacity.test.tsx | 127 ------------ app/scaling/components/runBacked.test.tsx | 17 +- app/scaling/components/scorecard.test.tsx | 191 ++++++++++++++++++ app/scaling/page.test.tsx | 7 + app/scaling/page.tsx | 8 +- 14 files changed, 633 insertions(+), 312 deletions(-) create mode 100644 app/scaling/components/Bottlenecks.tsx create mode 100644 app/scaling/components/Headline.tsx create mode 100644 app/scaling/components/Scorecard.tsx create mode 100644 app/scaling/components/Subsystems.tsx delete mode 100644 app/scaling/components/provenCapacity.test.tsx create mode 100644 app/scaling/components/scorecard.test.tsx diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx new file mode 100644 index 0000000..fbebe1c --- /dev/null +++ b/app/scaling/components/Bottlenecks.tsx @@ -0,0 +1,73 @@ +import { cn } from '../../components/ui/cn'; +import { Text } from '../../components/ui/Text'; +import { BLOB_TARGET, daCapacity, fmt, type DaWorkload } from '../da/workloads'; +import type { Proven } from '../proven/capacity'; +import { REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; +import { Tag } from './Tag'; + +export type Bar = { id: string; label: string; value: number | null; text: string; tag?: DaWorkload['tag'] }; + +const x = (n: number) => `${n < 10 ? n.toFixed(1) : Math.round(n)}×`; + +// Validators and the proof node kept up at the proven rate, so their limit is at least that rate. +export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): Bar[] { + const da = workloads.map((w): Bar => { + const value = daCapacity(w, BLOB_TARGET).mgasPerSecond; + return { id: `da-${w.payload}`, label: `DA · ${w.label}`, value, text: `${w.tag === 'estimated' ? '~' : ''}${fmt(value)}`, tag: w.tag }; + }); + const run = proven?.run; + const m = proven?.mgas ?? null; + const proofs = run?.proofNode?.proofs; + return [ + ...da, + { id: 'execution', label: 'Execution (sequencer)', value: m, text: !proven ? 'no proven run' : proven.atTop ? `≥ ${m} · top rate tested` : `${m}` }, + run?.replay + ? { id: 'validators', label: 'Validators', value: m, text: `≥ ${m} · ${x(run.replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND)} replay headroom` } + : { id: 'validators', label: 'Validators', value: null, text: 'pending' }, + proofs + ? { id: 'proof-node', label: 'Proof node', value: m, text: `≥ ${m} · lag p99 ${proofs.headLagBlocks.p99} blocks` } + : { id: 'proof-node', label: 'Proof node', value: null, text: 'pending' }, + ]; +} + +export function bindingBar(bars: Bar[]): Bar | null { + return bars.reduce((low, b) => (b.value !== null && (low === null || b.value < low.value!) ? b : low), null); +} + +type Line = { value: number | null; label: string; className: string }; + +export function Bottlenecks({ bars, max, p50, limit }: { bars: Bar[]; max: number; p50: number | null; limit: number | null }) { + const binding = bindingBar(bars); + const pct = (v: number) => `${Math.min(100, (v / max) * 100)}%`; + const lines: Line[] = [ + { value: p50, label: `today p50 ${p50}`, className: 'border-bds-gray-100 dark:border-white' }, + { value: limit, label: `gas limit ${limit}`, className: 'border-bds-orange-60 text-bds-orange-60' }, + ]; + return ( +
    + What breaks first? · Mgas/s +
      + {bars.map((b) => ( +
    • + {b.label} +
      + {b.value === null ? null : ( +
      + )} + {lines.map((l) => (l.value === null ? null :
      ))} +
      + + {b.text} + {b.tag ? : null} + {b === binding ? · binding : null} + +
    • + ))} +
    +
    + {lines.map((l) => (l.value === null ? null : {l.label}))} +
    + Same scale as the ladder. State joins once a runway is chosen. +
    + ); +} diff --git a/app/scaling/components/CeilingCards.tsx b/app/scaling/components/CeilingCards.tsx index b1bb936..fa34cfc 100644 --- a/app/scaling/components/CeilingCards.tsx +++ b/app/scaling/components/CeilingCards.tsx @@ -7,7 +7,6 @@ import type { UnifiedRun } from '../runs/lib/unified'; import { deriveBinding, type Ceiling, type CeilingInput } from '../snapshot.mjs'; import { DaWorkloadRows } from './DaWorkloads'; import { Info } from './Info'; -import { RunBackedCeilingCard } from './RunBackedCeilingCard'; import { Tag } from './Tag'; // Networking stays a work area but has no ceiling page: nothing measures its limit. @@ -18,17 +17,6 @@ function input({ name, value, unit, status, asOf }: CeilingInput): string { return [name, shown, value === null ? null : status, asOf && new Date(asOf).toISOString().slice(0, 10)].filter(Boolean).join(' · '); } -export function CeilingCards({ ceilings, benchUrl }: { ceilings: Ceiling[]; benchUrl?: string }) { - return ( -
    - Ceilings by bottleneck · Mgas/s -
    - {AREAS.map(([area]) => )} -
    -
    - ); -} - export function CeilingCard({ area, ceilings, workloads, run }: { area: Ceiling['area']; ceilings: Ceiling[]; workloads?: DaWorkload[]; run?: UnifiedRun }) { const name = AREAS.find(([a]) => a === area)![1]; const c = ceilings.find((x) => x.area === area)!; diff --git a/app/scaling/components/Guardrails.tsx b/app/scaling/components/Guardrails.tsx index eefcb4e..dd4c701 100644 --- a/app/scaling/components/Guardrails.tsx +++ b/app/scaling/components/Guardrails.tsx @@ -19,6 +19,9 @@ const rank = (g: Guardrail) => (g.status === 'not-monitored' ? 2 : g.value === n const fmt = (n: number, unit: string) => `${n.toLocaleString('en-US', { maximumFractionDigits: 2 })} ${unit}`.trim(); +export const KEEP_UP_BLOCKS = 3; +export const REPLAY_BLOCKS_PER_SECOND = 5; + const judge = (g: Omit): Guardrail => ({ ...g, status: g.value === null || g.threshold === null ? 'unknown' : (g.comparator === '<=' ? g.value <= g.threshold : g.value >= g.threshold) ? 'ok' : 'breached', @@ -28,9 +31,9 @@ const judge = (g: Omit): Guardrail => ({ // bench run: undefined without a bench host, null when none is loaded. export function scorecardGuardrails(snapshot: Guardrail[], run: Run | null | undefined): Guardrail[] { const keepUp = (g?: Guardrail) => judge({ - id: 'validator-keep-up', label: 'Validator keep-up', value: g?.value ?? null, unit: 'blocks', threshold: 3, comparator: '<=', + id: 'validator-keep-up', label: 'Validator keep-up', value: g?.value ?? null, unit: 'blocks', threshold: KEEP_UP_BLOCKS, comparator: '<=', thresholdTag: 'set', owner: g?.owner ?? null, asOf: g?.asOf ?? null, - help: 'How far production validators trail the tip, from the snapshot. They must stay within 3 blocks.', + help: `How far production validators trail the tip, from the snapshot. They must stay within ${KEEP_UP_BLOCKS} blocks.`, }); const rows = snapshot.some((g) => g.id === 'validator-keep-up') ? snapshot.map((g) => (g.id === 'validator-keep-up' ? keepUp(g) : g)) @@ -50,8 +53,8 @@ export function scorecardGuardrails(snapshot: Guardrail[], run: Run | null | und }), judge({ ...bench, id: 'validator-replay-headroom', label: 'Validator replay headroom', value: run?.replay?.blocksPerSecond ?? null, unit: 'blocks/s', - comparator: '>=', threshold: 5, thresholdTag: 'set', - help: `Blocks per second a validator replays the bench blocks at. Keeping up needs 5, one block per 200 ms.${source}`, + comparator: '>=', threshold: REPLAY_BLOCKS_PER_SECOND, thresholdTag: 'set', + help: `Blocks per second a validator replays the bench blocks at. Keeping up needs ${REPLAY_BLOCKS_PER_SECOND}, one block per ${1000 / REPLAY_BLOCKS_PER_SECOND} ms.${source}`, }), ]; } diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx new file mode 100644 index 0000000..c4dde59 --- /dev/null +++ b/app/scaling/components/Headline.tsx @@ -0,0 +1,112 @@ +import type { ReactNode } from 'react'; + +import { Text } from '../../components/ui/Text'; +import { runHref, type Proven, type ProvenCapacity, type Workload } from '../proven/capacity'; +import type { Snapshot } from '../snapshot.mjs'; +import type { Bar } from './Bottlenecks'; +import type { ProvenMark } from './Ladder'; +import { day } from './ProvenCapacity'; +import { Tag } from './Tag'; + +export type BenchState = { configured: boolean; data?: ProvenCapacity; error?: unknown }; + +const STEP_NAME: Record = { + sequencer: 'sequencer', validator_replay: 'validators', proof_node: 'proof node', da: 'DA encoding', tee_proof: 'TEE proving', rpc_sync: 'RPC sync', +}; +const TRAFFIC: Record = { payment: 'payments', swap: 'swaps', mix: 'mainnet-like traffic' }; +const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 2 }); +const list = (xs: string[]) => (xs.length < 2 ? xs.join('') : `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}`); + +type Props = Pick & { + bench: BenchState; + proven: ProvenMark; + bars: Bar[]; + binding: Bar | null; +}; + +function provenCaption({ configured, data, error }: BenchState, proven: ProvenMark, qualified: Snapshot['ladder']['qualified']): ReactNode { + if (!configured) { + if (!proven) return ; + return <>Mgas/s · from the snapshot{qualified.evidence?.map((l) => · {l.label})}; + } + if (error) return 'Could not load bench results. The bench host is reachable only on VPN and must allow this origin via CORS.'; + if (!data) return 'Loading bench results…'; + // A run whose files failed to load is skipped, so an older result at its rate may count instead. + const partial = data.unknown.length ? ` · ${data.unknown.length} run(s) could not be loaded, so this may be wrong` : ''; + const mix = data.current.mix; + if (!mix) return `No main run passed in the last 14 days${partial}.`; + const passed = mix.run.steps.filter((s) => s.status === 'passed').map((s) => STEP_NAME[s.name] ?? s.name); + return `Mgas/s · ${list(passed)} ${passed.length > 1 ? 'all pass' : 'pass'} on mainnet state${partial}`; +} + +function Stat({ label, value, caption }: { label: string; value: string; caption: ReactNode }) { + return ( +
    + {label} + {value} + {caption} +
    + ); +} + +function FirstLimit({ bars, binding, proven }: { bars: Bar[]; binding: Bar; proven: ProvenMark }) { + const below = proven && binding.value! < proven.value ? `, below the proven ${n(proven.value)}` : ''; + if (binding.id.startsWith('da-')) { + const part = (b: Bar) => <>{b.text} Mgas/s of {TRAFFIC[b.id.slice(3) as Workload]} {b.tag ? : null}; + const mix = bars.find((b) => b.id === 'da-mix'); + return ( + <>First limit: DA. Ethereum's blob target fills at {part(binding)}{mix && mix !== binding ? <> or {part(mix)} : null}{below}. + ); + } + return proven?.atLeast + ? <>No limit found up to {n(proven.value)} Mgas/s, the top rate tested. + : <>First limit: above {n(binding.value!)} Mgas/s, where the next rate tested did not pass.; +} + +function LastChange({ history }: { history: ProvenCapacity['history'] }) { + if (history.length < 2) return null; + const [previous, last] = history.slice(-2); + const down = last.mgas !== null && previous.mgas !== null && last.mgas < previous.mgas; + return ( + + {' '}{down ? '↓' : '↑'} from {previous.mgas === null ? 'none' : `${previous.mgas} Mgas/s`} on {day(last.at)} + {last.compareUrl ? <> · compare : null} + + ); +} + +function translation({ gasPerPayment, gasPerSwap }: Snapshot['translations']): string { + if (!gasPerPayment && !gasPerSwap) return 'Translation to payments/s and swaps/s: constants pending'; + const per = (c: typeof gasPerPayment, noun: string) => (c ? `${Math.round(1e6 / c.value)} ${noun}/s` : `${noun}/s pending`); + return `1 Mgas/s ≈ ${per(gasPerPayment, 'payments')} ≈ ${per(gasPerSwap, 'swaps')}`; +} + +// Delivered tx/s of each workload's proven run; the rate can only be higher. +function BenchTranslation({ current }: { current: ProvenCapacity['current'] }) { + const rate = (p: Proven | null, noun: string, what: string) => + p ? ≥ {Math.floor(p.run.tps).toLocaleString('en-US')} {noun} ({what}) : `${noun} (${what}) pending`; + return <>{rate(current.payment, 'payments/s', 'B20 transfers')} · {rate(current.swap, 'swaps/s', 'Uniswap v3')}; +} + +export function Headline({ ladder, windows, translations, bench, proven, bars, binding }: Props) { + const { p50, p99 } = ladder.observed; + const window = p50.window ? ` · ${windows[p50.window].label}` : ''; + const headroom = proven && p50.value ? proven.value / p50.value : null; + return ( +
    + How much can Base handle, and how much do we use? +
    + + + +
    + {binding ? ( +

    + + {bench.data ? : null} +

    + ) : null} + {bench.data ? : translation(translations)} +
    + ); +} diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx index d46d436..05072b8 100644 --- a/app/scaling/components/Ladder.tsx +++ b/app/scaling/components/Ladder.tsx @@ -4,130 +4,91 @@ import { useLayoutEffect, useRef, useState } from 'react'; import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; -import { daWorkloads, withDaWorkloads } from '../da/workloads'; -import { runHref, type Proven, type ProvenCapacity as Capacity } from '../proven/capacity'; -import { deriveBinding, type Rung, type Snapshot } from '../snapshot.mjs'; +import type { Rung, Snapshot } from '../snapshot.mjs'; import { Info } from './Info'; -import { day, ProvenCapacity, useProvenCapacity } from './ProvenCapacity'; -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 LABEL_PX = 130; +const ROW = 36; +const BAR = 22; 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%)' }; +const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 2 }); -type Props = Pick & { benchUrl?: string; dashboardUrl?: string }; -type Item = { id: string; label: string; value: number | null; tag: string; help: string; limit?: boolean; atLeast?: boolean; source?: string }; +type Marker = { id: string; label: string; value: number; help: string; tick: string; text: string }; +export type ProvenMark = { value: number; atLeast: boolean; help: string } | null; -function benchItem({ mgas, atTop, run }: Proven) { - const top = atTop ? ` ≥ because ${mgas} Mgas/s is the highest rate the bench runs.` : ''; - const help = `Bench-proven: the highest rate whose latest main run in the last 14 days was comparable and delivered ≥ 95% of target. Run ${run.outputDir}, commit ${run.commit.slice(0, 7)}, ${day(run.createdAt)}.${top} Comparable requires every enabled pipeline step to pass; the DA blob target and proof-node lag and latency are recorded, not gated.`; - return { value: mgas, tag: 'measured' as const, help, atLeast: atTop, source: 'bench' }; -} - -function items({ ladder, ceilings, windows }: Props, proven: Proven | null | undefined): Item[] { - const rung = (id: string, label: string, r: Rung): Item => - ({ id, label, value: r.value, tag: r.tag, help: r.window ? `${r.help} Window: ${windows[r.window].label}.` : r.help }); - const bench = proven ? benchItem(proven) : null; - const binding = deriveBinding(bench ? ceilings.map((c) => (c.area === 'execution' ? { ...c, value: bench.value, tag: bench.tag } : c)) : ceilings); - const ceiling: Item = binding - ? { id: 'ceiling', label: `Ceiling · ${AREA_NAME[binding.area]}`, value: binding.value, tag: binding.tag, help: binding.help, ...(bench && binding.area === 'execution' ? bench : {}) } - : { id: 'ceiling', label: 'Ceiling', value: null, tag: 'not-modeled', help: `No area ceiling is measured or modeled yet: ${ceilings.map((c) => `${AREA_NAME[c.area]} ${c.tag}`).join(', ')}.` }; - return [ - rung('p50', 'Observed p50', ladder.observed.p50), - rung('p99', 'Observed p99', ladder.observed.p99), - rung('operating', 'Operating point', ladder.operatingPoint), - bench ? { id: 'qualified', label: 'Qualified sustained', ...bench } : rung('qualified', 'Qualified sustained', ladder.qualified), - ceiling, - { ...rung('limit', 'Configured limit', ladder.configuredLimit), limit: true }, - ]; -} - -function translation({ gasPerPayment, gasPerSwap }: Snapshot['translations']): string { - if (!gasPerPayment && !gasPerSwap) return 'Translation to payments/s and swaps/s: constants pending'; - const per = (c: typeof gasPerPayment, noun: string) => (c ? `${Math.round(1e6 / c.value)} ${noun}/s` : `${noun}/s pending`); - return `1 Mgas/s ≈ ${per(gasPerPayment, 'payments')} ≈ ${per(gasPerSwap, 'swaps')}`; -} +type Props = Pick & { proven: ProvenMark; max: number; dashboardUrl?: string }; -// Delivered tx/s of each workload's proven run; the rate can only be higher. -function BenchTranslation({ current }: { current: Capacity['current'] }) { - const rate = (p: Proven | null, noun: string, what: string) => - p ? ≥ {Math.floor(p.run.tps).toLocaleString('en-US')} {noun} ({what}) : `${noun} (${what}) pending`; - return <>{rate(current.payment, 'payments/s', 'B20 transfers')} · {rate(current.swap, 'swaps/s', 'Uniswap v3')}; -} - -export function Ladder(props: Props) { - const axisRef = useRef(null); +export function Ladder({ ladder, windows, proven, max, dashboardUrl }: Props) { + const ref = useRef(null); const [width, setWidth] = useState(960); useLayoutEffect(() => { const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width)); - observer.observe(axisRef.current!); + observer.observe(ref.current!); return () => observer.disconnect(); }, []); - const bench = useProvenCapacity(props.benchUrl); - const loaded = bench.error ? undefined : bench.data; - const proven = loaded?.current.mix; - const all = items(loaded ? { ...props, ceilings: withDaWorkloads(props.ceilings, daWorkloads(loaded.da)) } : props, proven); - const valued = all.flatMap((i) => (i.value === null ? [] : [{ id: i.id, value: i.value }])); - const max = props.ladder.configuredLimit.value ?? Math.max(0, ...valued.map((v) => v.value)); - const placed = new Map(max > 0 ? layoutRungs(valued, max, LABEL_PX / width).map((p) => [p.id, p]) : []); - const depth = (side: string) => 1 + Math.max(-1, ...[...placed.values()].filter((p) => p.side === side).map((p) => p.tier)); + const help = (r: Rung) => (r.window ? `${r.help} Window: ${windows[r.window].label}.` : r.help); + const mark = (id: string, name: string, r: Rung, tick: string, text = ''): Marker[] => + r.value === null ? [] : [{ id, label: `${name} ${n(r.value)}`, value: r.value, help: help(r), tick, text }]; + const { p50, p99 } = ladder.observed; + const limit = ladder.configuredLimit.value; + const markers: Marker[] = [ + ...mark('p50', 'p50', p50, 'border-l-2 border-white'), + ...mark('p99', 'p99', p99, 'border-l border-dashed border-bds-gray-100 dark:border-white'), + ...mark('operating', 'fee target', ladder.operatingPoint, 'border-l border-dashed border-bds-gray-100 dark:border-white'), + ...mark('limit', 'gas limit', ladder.configuredLimit, 'border-l-2 border-bds-orange-60', 'text-bds-orange-60'), + ...(proven + ? [{ id: 'proven', label: `proven ${proven.atLeast ? '≥ ' : ''}${n(proven.value)}`, value: proven.value, help: proven.help, tick: 'border-l-[3px] border-base-blue', text: 'font-[600] text-base-blue' }] + : []), + ]; + const placed = max > 0 ? layoutRungs(markers, max, LABEL_PX / width) : []; + const depth = (side: string) => 1 + Math.max(-1, ...placed.filter((p) => p.side === side).map((p) => p.tier)); const above = depth('above'); - const axisY = above * ROW + AXIS / 2; + const barTop = above * ROW + 6; + const pct = (v: number) => `${Math.min(100, (v / max) * 100)}%`; + const used = p99.value ?? p50.value; + const bands = [ + used !== null && { label: 'used', className: 'bg-base-blue' }, + limit !== null && { label: 'allowed by policy', className: 'bg-bds-blue-10 dark:bg-bds-blue-80' }, + proven && limit !== null && proven.value > limit && { label: 'proven, not yet allowed', className: 'bg-bds-gray-10 dark:bg-white/10' }, + ].filter((b) => b !== false && b !== null); return (
    - Capacity ladder · Mgas/s -
    -
    - {all.map((item) => { - const p = placed.get(item.id); - if (!p) return null; - const top = p.side === 'above' ? (above - 1 - p.tier) * ROW : above * ROW + AXIS + p.tier * ROW; - const left = `${p.x * 100}%`; + How close are we, and to what? · Mgas/s +
    +
    + {limit === null ? null :
    } + {used === null ? null :
    } + {placed.map((p) => { + const m = markers.find((x) => x.id === p.id)!; + const top = p.side === 'above' ? (above - 1 - p.tier) * ROW : barTop + BAR + 6 + p.tier * ROW; return ( -
    +
    +
    -
    - - {item.label} {item.help} - - - {item.atLeast ? '≥ ' : ''}{item.value!.toLocaleString('en-US', { maximumFractionDigits: 2 })} - {item.limit ? null : {item.tag}{item.source ? ` · ${item.source}` : ''}} + + {m.label} {m.help}
    ); })}
    -
      - {all.filter((item) => !placed.has(item.id)).map((item) => ( -
    • - {item.label} - - {item.help} -
    • - ))} -
    - {!proven && props.ladder.qualified.evidence?.length ? ( - - Qualified sustained by {props.ladder.qualified.evidence.map((l) => {l.label})} - - ) : null} - {loaded ? : translation(props.translations)} - {props.benchUrl ? : null} - {props.dashboardUrl ? Mainnet load, gas limit, DA ceiling → : null} +
    + + {bands.map((b) => ( + + ))} + + 0 – {n(max)} Mgas/s +
    + {dashboardUrl ? Mainnet load, gas limit, DA ceiling → : null}
    ); } diff --git a/app/scaling/components/ProvenCapacity.tsx b/app/scaling/components/ProvenCapacity.tsx index 149d374..30d2372 100644 --- a/app/scaling/components/ProvenCapacity.tsx +++ b/app/scaling/components/ProvenCapacity.tsx @@ -2,58 +2,10 @@ import useSWR from 'swr'; -import { cn } from '../../components/ui/cn'; -import { Text } from '../../components/ui/Text'; -import { loadProvenCapacity, runHref, type PipelineStep, type ProvenCapacity as Capacity } from '../proven/capacity'; - -export type Bench = Capacity & { now: number }; +import { loadProvenCapacity } from '../proven/capacity'; export const day = (t: number) => new Date(t).toISOString().slice(0, 10); -const mgas = (m: number | null) => (m === null ? 'none' : `${m} Mgas/s`); -const STEP_DOT: Record = { passed: 'bg-bds-green-50', failed: 'bg-bds-red-50', skipped: 'bg-bds-gray-30', 'not-implemented': 'bg-bds-gray-30' }; export function useProvenCapacity(benchUrl?: string) { - return useSWR(benchUrl ? ['scaling-proven-capacity', benchUrl] : null, async (): Promise => { - const now = Date.now(); - return { ...(await loadProvenCapacity(benchUrl!, fetch, now)), now }; - }, { revalidateOnFocus: false }); -} - -export function ProvenCapacity({ data, error }: { data?: Bench; error?: unknown }) { - return error ? Could not load bench results. The bench host is reachable only on VPN and must allow this origin via CORS. - : !data ? Loading bench results… - : ; -} - -function Result({ capacity: { current, history, unknown } }: { capacity: Bench }) { - // A run whose files failed to load is skipped, so an older result at its rate may count instead. - const partial = unknown.length ? {unknown.length} run(s) could not be loaded, so this result may be wrong. : null; - if (!current.mix) return partial ?? No main run passed in the last 14 days.; - const { run } = current.mix; - // Only the latest change is shown; the full history lives on /scaling/runs. - const last = history.length > 1 ? history[history.length - 1] : null; - const previous = last ? history[history.length - 2] : null; - return ( - <> - {partial} - - Qualified by {run.outputDir}: {run.mgas} Mgas/s run delivered {(run.gps / 1e6).toFixed(1)} Mgas/s · commit{' '} - {run.commit.slice(0, 7)} · {day(run.createdAt)} - -
      - {run.steps.map((s) => ( -
    • -
    • - ))} -
    - {last && previous ? ( - - {last.mgas !== null && previous.mgas !== null && last.mgas < previous.mgas ? '↓' : '↑'} from {mgas(previous.mgas)} on {day(last.at)} - {last.compareUrl ? <> · compare : null} - - ) : null} - - ); + return useSWR(benchUrl ? ['scaling-proven-capacity', benchUrl] : null, () => loadProvenCapacity(benchUrl!), { revalidateOnFocus: false }); } diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx new file mode 100644 index 0000000..db01c31 --- /dev/null +++ b/app/scaling/components/Scorecard.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { daWorkloads } from '../da/workloads'; +import type { Proven } from '../proven/capacity'; +import type { Snapshot } from '../snapshot.mjs'; +import { bindingBar, bottleneckBars, Bottlenecks } from './Bottlenecks'; +import { Headline } from './Headline'; +import { Ladder, type ProvenMark } from './Ladder'; +import { day, useProvenCapacity } from './ProvenCapacity'; +import { Subsystems } from './Subsystems'; + +function benchMark({ mgas, atTop, run }: Proven): ProvenMark { + const top = atTop ? ` ≥ because ${mgas} Mgas/s is the highest rate the bench runs.` : ''; + const help = `Bench-proven: the highest rate whose latest main run in the last 14 days was comparable and delivered ≥ 95% of target. Run ${run.outputDir}, commit ${run.commit.slice(0, 7)}, ${day(run.createdAt)}.${top} Comparable requires every enabled pipeline step to pass; the DA blob target and proof-node lag and latency are recorded, not gated.`; + return { value: mgas, atLeast: atTop, help }; +} + +// Sections A–D share one bench fetch and one scale. Without a bench host, the proven rate falls back to +// the snapshot's qualified rung. +export function Scorecard({ snapshot, benchUrl, dashboardUrl }: { snapshot: Snapshot; benchUrl?: string; dashboardUrl?: string }) { + const bench = useProvenCapacity(benchUrl); + const data = bench.error ? undefined : bench.data; + const { ladder } = snapshot; + const proven = data?.current.mix ?? null; + const qualified = ladder.qualified; + const mark = proven ? benchMark(proven) : !benchUrl && qualified.value !== null ? { value: qualified.value, atLeast: false, help: qualified.help } : null; + const bars = bottleneckBars(daWorkloads(data?.da), proven); + const values = [ladder.observed.p50, ladder.observed.p99, ladder.operatingPoint, ladder.configuredLimit].map((r) => r.value ?? 0); + const max = Math.max(mark?.value ?? 0, ladder.configuredLimit.value ?? 0) || Math.max(0, ...values); + return ( + <> + + + + + + ); +} diff --git a/app/scaling/components/Subsystems.tsx b/app/scaling/components/Subsystems.tsx new file mode 100644 index 0000000..b53754e --- /dev/null +++ b/app/scaling/components/Subsystems.tsx @@ -0,0 +1,133 @@ +import Link from 'next/link'; +import type { ReactNode } from 'react'; + +import { Card } from '../../components/ui/Card'; +import { cn } from '../../components/ui/cn'; +import { Text } from '../../components/ui/Text'; +import { BLOB_TARGET } from '../da/workloads'; +import { runHref, type PipelineStep, type Proven } from '../proven/capacity'; +import type { Snapshot } from '../snapshot.mjs'; +import { KEEP_UP_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; +import { day } from './ProvenCapacity'; +import { Tag } from './Tag'; + +type Tone = 'ok' | 'bad' | 'muted'; +type Row = { name: string; href?: string; measured: ReactNode; budget: ReactNode; headroom: string; status: [string, Tone] }; + +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 TONE: Record = { ok: 'text-bds-green-60', bad: 'text-bds-red-60', muted: 'text-bds-gray-60' }; +const STEP_DOT: Record = { passed: 'bg-bds-green-50', failed: 'bg-bds-red-50', skipped: 'bg-bds-gray-30', 'not-implemented': 'bg-bds-gray-30' }; +const SECONDS_PER_YEAR = 365.25 * 86_400; +const PENDING: [string, Tone] = ['pending', 'muted']; +const x = (v: number) => `${v < 10 ? v.toFixed(1) : Math.round(v)}×`; + +function stepStatus(proven: Proven | null, name: string, passed: [string, Tone]): [string, Tone] { + const status = proven?.run.steps.find((s) => s.name === name)?.status; + return status === 'passed' ? passed : status === 'failed' ? ['fail', 'bad'] : status ? [status, 'muted'] : PENDING; +} + +function rows(proven: Proven | null, { guardrails, ceilings }: Pick): Row[] { + const run = proven?.run; + const { replay, da } = run ?? {}; + const proofs = run?.proofNode?.proofs; + const executePayload = proofs?.methods.debug_executePayload; + const keepUp = guardrails.find((g) => g.id === 'validator-keep-up')?.value ?? null; + const target = da?.target ?? BLOB_TARGET; + const input = (name: string) => ceilings.find((c) => c.area === 'state')?.inputs.find((i) => i.name === name); + const maxSize = input('single_node_max'); + const bytesPerGas = input('state_bytes_per_gas')?.value; + const tbPerYear = proven && typeof bytesPerGas === 'number' ? (bytesPerGas * proven.mgas * 1e6 * SECONDS_PER_YEAR) / 1e12 : null; + return [ + { + name: 'Sequencer', href: '/scaling/execution', + measured: run ? `${(run.gps / 1e6).toFixed(1)} Mgas/s delivered` : 'pending', + budget: '≥ 95% of target', headroom: '—', + status: stepStatus(proven, 'sequencer', ['pass', 'ok']), + }, + { + name: 'Validator replay', + measured: [ + replay && `${replay.blocksPerSecond.toFixed(1)} blocks/s, newPayload p99 ${replay.newPayload.p99Ms} ms`, + keepUp !== null && `production lag ${keepUp} blocks`, + ].filter(Boolean).join('; ') || 'pending', + budget: `≥ ${REPLAY_BLOCKS_PER_SECOND} blocks/s; production lag ≤ ${KEEP_UP_BLOCKS} blocks`, + headroom: replay ? x(replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND) : '—', + status: !replay ? PENDING : !replay.match ? ['mismatch', 'bad'] : replay.blocksPerSecond >= REPLAY_BLOCKS_PER_SECOND ? ['pass', 'ok'] : ['below budget', 'bad'], + }, + { + name: 'Proof node', + measured: proofs ? [`lag p99 ${proofs.headLagBlocks.p99} blocks`, executePayload && `executePayload p99 ${executePayload.latency.p99Ms} ms`].filter(Boolean).join('; ') : 'pending', + budget: pending, headroom: '—', + status: stepStatus(proven, 'proof_node', ['budget pending', 'muted']), + }, + { + name: 'DA (bench mix)', href: '/scaling/da', + measured: da ? `${da.result.blobs_per_l1_slot.toFixed(1)} blobs per L1 slot` : 'pending', + budget: `${target} blobs`, + headroom: da ? x(target / da.result.blobs_per_l1_slot) : '—', + status: !da ? PENDING : da.withinTarget === null ? ['not judged', 'muted'] : da.withinTarget ? ['within target', 'ok'] : ['over target', 'bad'], + }, + { + name: 'State (full node)', href: '/scaling/state', + measured: tbPerYear === null ? 'pending' : <>~{tbPerYear.toFixed(1)} TB/yr at {proven!.mgas} , + budget: maxSize?.value == null ? 'pending' : `${maxSize.value} ${maxSize.unit}`.trim(), + headroom: tbPerYear && maxSize?.unit === 'TB' && typeof maxSize.value === 'number' ? `~${(maxSize.value / tbPerYear).toFixed(1)} yr` : '—', + status: ['runway pending', 'muted'], + }, + ]; +} + +function Evidence({ proven }: { proven: Proven }) { + const { run } = proven; + return ( +
    + + Qualified by {run.outputDir}: {run.mgas} Mgas/s run delivered {(run.gps / 1e6).toFixed(1)} Mgas/s · commit{' '} + {run.commit.slice(0, 7)} · {day(run.createdAt)} + +
      + {run.steps.map((s) => ( +
    • +
    • + ))} +
    +
    + ); +} + +export function Subsystems({ proven, guardrails, ceilings }: Pick & { proven: Proven | null }) { + return ( +
    + Subsystem detail + + + + + {['Subsystem', `Measured at ${proven ? `${proven.mgas} Mgas/s` : 'the proven rate'}`, 'Budget', 'Headroom', 'Status'].map((h) => )} + + + + {rows(proven, { guardrails, ceilings }).map((r) => ( + + + + + + + + ))} + + + + + + +
    {h}
    {r.href ? {r.name} : r.name}{r.measured}{r.budget}{r.headroom}{r.status[0]}
    TEE proving, RPC under syncnot measured yet—
    +
    + {proven ? : null} +
    + ); +} diff --git a/app/scaling/components/firstScreen.test.tsx b/app/scaling/components/firstScreen.test.tsx index 7770a94..6a42754 100644 --- a/app/scaling/components/firstScreen.test.tsx +++ b/app/scaling/components/firstScreen.test.tsx @@ -5,7 +5,8 @@ import { describe, expect, it } from 'vitest'; import type { Run } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; -import { CeilingCards } from './CeilingCards'; +import { AREAS } from './CeilingCards'; +import { RunBackedCeilingCard } from './RunBackedCeilingCard'; import { Guardrails, scorecardGuardrails } from './Guardrails'; import { Progress } from './Progress'; @@ -16,9 +17,9 @@ function fixture(): Snapshot { const cards = (html: string) => html.split(' { +describe('CeilingCard', () => { it('renders every area but networking in a fixed order with its tag, and badges the binding card', () => { - const html = cards(renderToStaticMarkup()); + const html = cards(renderToStaticMarkup(<>{AREAS.map(([area]) => )})); expect(html.map((c) => c.match(/href="\/scaling\/(\w+)"/)![1])).toEqual(['da', 'execution', 'state']); expect(html.map((c) => c.match(/border-dashed[^>]*>([^<]+) c.includes('>Binding<'))).toEqual([true, false, false]); @@ -27,7 +28,7 @@ describe('CeilingCards', () => { 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()); + const html = cards(renderToStaticMarkup(<>{AREAS.map(([area]) => )})); expect(html.map((c) => c.includes('>Binding<'))).toEqual([false, true, false]); expect(html[2]).toContain('>—<'); expect(html[2]).toContain('live-state growth not measured'); diff --git a/app/scaling/components/provenCapacity.test.tsx b/app/scaling/components/provenCapacity.test.tsx deleted file mode 100644 index 5b90e56..0000000 --- a/app/scaling/components/provenCapacity.test.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { readFileSync } from 'node:fs'; - -import type { ReactNode } from 'react'; -import { renderToStaticMarkup } from 'react-dom/server'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import type { Bench } from './ProvenCapacity'; -import type { Run } from '../proven/capacity'; -import type { Snapshot } from '../snapshot.mjs'; -import { Ladder } from './Ladder'; - -// Synthetic data only: real bench results and snapshots are internal and never committed. -const swr = vi.hoisted(() => ({ result: {} as object, key: undefined as unknown })); -vi.mock('swr', () => ({ default: (key: unknown) => ((swr.key = key), key === null ? {} : swr.result) })); -vi.mock('./Info', () => ({ Info: ({ children }: { children: ReactNode }) => {children} })); - -const BENCH = 'https://bench.example.com'; -const DAY = 86_400_000; -const T0 = Date.parse('2026-01-01T00:00:00Z'); -const STEPS: Run['steps'] = [ - { name: 'sequencer', enabled: true, status: 'passed' }, - { name: 'validator_replay', enabled: true, status: 'passed' }, - { name: 'tee_proof', enabled: false, status: 'not-implemented', reason: 'needs a synthetic L1' }, -]; -const run = (mgas: number, workload: Run['workload'] = 'mix', da: Run['da'] = null): Run => ({ - id: `synthetic-${workload}-${mgas}`, outputDir: `synthetic-${mgas}`, workload, mgas, gps: (mgas - 5) * 1e6, tps: mgas * 10.5, passed: true, - commit: 'bbbbbbbbbbbbbbbb', createdAt: T0 + 5 * DAY, steps: STEPS, replay: null, proofNode: null, da, -}); -const none = { mix: null, payment: null, swap: null }; -const bench = (mgas: number, atTop: boolean, more: Partial = {}): Bench => ({ - current: { ...none, mix: { mgas, atTop, run: run(mgas) } }, - da: none, - history: [ - { at: T0, mgas: 40, commit: 'aaaaaaaaaaaaaaaa', previousCommit: null, compareUrl: null }, - { at: T0 + 5 * DAY, mgas, commit: 'bbbbbbbbbbbbbbbb', previousCommit: 'aaaaaaaaaaaaaaaa', compareUrl: 'https://github.com/base/base/compare/aaaa...bbbb' }, - ], - unknown: [], - now: T0 + 7 * DAY, - ...more, -}); -const snapshot: Snapshot = JSON.parse(readFileSync(new URL('../fixtures/synthetic.v2.json', import.meta.url), 'utf8')); -const render = (props: { benchUrl?: string; dashboardUrl?: string } = { benchUrl: BENCH }) => - renderToStaticMarkup(); -// A placed rung renders its label, help and value; an unplaced one is a list item with its tag. -const rung = (html: string, label: string) => html.match(new RegExp(`${label}(?:(?!).)*?(.*?)`))?.[1]; -const ceilingLabel = (html: string) => html.match(/Ceiling · (\w+)/)?.[1]; - -beforeEach(() => { - swr.result = {}; - swr.key = undefined; -}); - -describe('Ladder bench capacity', () => { - it('keeps the snapshot rungs and fetches nothing without a URL', () => { - const html = render({}); - expect(swr.key).toBeNull(); - expect(html).toMatch(/Qualified sustained<\/span>.*Not established/); - expect(ceilingLabel(html)).toBe('DA'); - expect(html).not.toContain('bench results'); - }); - - it('keeps the snapshot rungs while loading and after a failed fetch', () => { - expect(render()).toContain('Loading bench results'); - expect(swr.key).toEqual(['scaling-proven-capacity', BENCH]); - swr.result = { error: new TypeError('Failed to fetch'), data: bench(400, true) }; - const html = render(); - expect(html).toContain('VPN'); - expect(html).toMatch(/Qualified sustained<\/span>.*Not established/); - expect(html).not.toContain('bench'); - }); - - it('shows ≥ at the top rate with bench provenance, the rule, run, commit, caveat, evidence and the last change', () => { - swr.result = { data: bench(400, true) }; - const html = render(); - expect(rung(html, 'Qualified sustained')).toMatch(/^≥ 400]*>measured · bench$/); - for (const text of ['latest main run in the last 14 days was comparable and delivered ≥ 95% of target', 'Run synthetic-400, commit bbbbbbb, 2026-01-06.', '400 Mgas/s is the highest rate the bench runs', 'Comparable requires every enabled pipeline step to pass']) { - expect(html).toContain(text); - } - expect(html).toContain('Qualified by synthetic-400: 400 Mgas/s run delivered 395.0 Mgas/s'); - expect(html).toMatch(/bg-bds-green-50"><\/span>validator replay passedcompare'); - // The page passes #214's estimated DA ceiling, which never binds. - expect(ceilingLabel(html)).toBe('execution'); - }); - - it('drops ≥ below the top rate, and the bench execution ceiling binds when it is the lowest', () => { - swr.result = { data: bench(80, false) }; - const html = render(); - expect(rung(html, 'Qualified sustained')).toMatch(/^80]*>measured · bench$/); - expect(ceilingLabel(html)).toBe('execution'); - expect(rung(html, 'Ceiling · execution')).toMatch(/^80]*>measured · bench$/); - expect(html).not.toContain('highest rate the bench runs'); - }); - - it('shows an empty state when no run counts, and warns when run files failed to load', () => { - swr.result = { data: { current: none, da: none, history: [], unknown: [], now: T0 } }; - expect(render()).toContain('No main run passed in the last 14 days.'); - swr.result = { data: { current: none, da: none, history: [], unknown: ['r1', 'r2'], now: T0 } }; - expect(render()).toContain('2 run(s) could not be loaded'); - expect(render()).not.toContain('No main run passed'); - }); - - it('translates to bench payments/s and swaps/s, pending without a qualifying run, and the snapshot constants without bench data', () => { - expect(render({})).toContain('1 Mgas/s ≈ 19 payments/s ≈ swaps/s pending'); - swr.result = { data: bench(400, true, { current: { mix: null, swap: null, payment: { mgas: 200, atTop: false, run: run(200, 'payment') } } }) }; - expect(render()).toContain('≥ 2,100 payments/s (B20 transfers) · swaps/s (Uniswap v3) pending'); - }); - - it('binds a measured DA ceiling once payments and swaps have bench DA runs', () => { - const da = (frame_bytes: number): Run['da'] => ({ - result: { seconds: 600, gas_used: 4e10, transactions: 1e6, transaction_bytes: 0, frame_bytes, gas_per_second: 0, bytes_per_gas: 0, compression_ratio: 0, blobs_per_l1_slot: 15, blob_fill: 1, blob_target_gas_per_second: 0 }, - target: 14, - withinTarget: false, - }); - swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', da(8e7)), swap: run(200, 'swap', da(4e7)) } }) }; - const html = render(); - expect(ceilingLabel(html)).toBe('DA'); - expect(rung(html, 'Ceiling · DA')).toMatch(/^76]*>modeled$/); - }); - - it('links the dashboard only when set', () => { - expect(render({ dashboardUrl: 'https://dashboards.example.com/scaling' })).toContain('href="https://dashboards.example.com/scaling">Mainnet load, gas limit, DA ceiling →'); - expect(render()).not.toContain('Mainnet load'); - }); -}); diff --git a/app/scaling/components/runBacked.test.tsx b/app/scaling/components/runBacked.test.tsx index 56bff69..91da4db 100644 --- a/app/scaling/components/runBacked.test.tsx +++ b/app/scaling/components/runBacked.test.tsx @@ -8,9 +8,9 @@ import { getTestRunsWithStatus, type BenchmarkRuns } from '../runs/lib/types'; import { toUnifiedRun, type LoadRun } from '../runs/lib/unified'; import type { Run } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; -import { CeilingCards } from './CeilingCards'; +import { AREAS } from './CeilingCards'; +import { RunBackedCeilingCard } from './RunBackedCeilingCard'; import { DaWorkloads } from './DaWorkloads'; -import { Ladder } from './Ladder'; import { Progress } from './Progress'; import { RelatedRuns } from './RelatedRuns'; @@ -34,7 +34,7 @@ describe('execution ceiling', () => { swr.byKey['scaling-bench-metadata'] = { data: { runs: [older, bench, fasterConfig, metadata.runs[1]] } }; const { ceilings } = snapshot(); ceilings[0].value = 70; // DA modeled above the run's 48 Mgas/s. - const [da, execution] = cards(renderToStaticMarkup()); + const [da, execution] = cards(renderToStaticMarkup(<>{AREAS.map(([area]) => )})); expect(execution).toMatch(/>48<\/span>.*?>Estimated { it('falls back to the snapshot when the run API is unavailable', () => { swr.byKey['scaling-bench-metadata'] = { error: new Error('not configured') }; - const [, execution] = cards(renderToStaticMarkup()); + const [, execution] = cards(renderToStaticMarkup(<>{AREAS.map(([area]) => )})); expect(execution).toMatch(/>150<\/span>.*?>Estimated { swr.byKey['scaling-bench-metadata'] = { data: metadata }; const { ceilings } = snapshot(); Object.assign(ceilings[1], { value: 60, tag: 'measured' }); - const [, execution] = cards(renderToStaticMarkup()); + const [, execution] = cards(renderToStaticMarkup(<>{AREAS.map(([area]) => )})); expect(execution).toMatch(/>60<\/span>.*?>Measured<.*?>Binding { - const { ladder, ceilings, translations, windows } = snapshot(); - ladder.qualified = { ...ladder.qualified, value: 30, tag: 'measured', evidence: [{ label: 'Synthetic run', url: 'https://example.com/scaling/runs/bench/synthetic-1' }] }; - const html = renderToStaticMarkup(); - expect(html).toContain('Qualified sustained by Synthetic run'); -}); - it('lists the newest runs of each kind covering an area', () => { const nightly = Array.from({ length: 6 }, (_, i) => ({ network: 'sepolia', timestamp: `2026-02-0${i + 1}T00-00-00` })); const runs = [...getTestRunsWithStatus(metadata), ...(runFixture.loadTests as unknown as LoadRun[]), ...nightly].map(toUnifiedRun).sort((a, b) => b.timestamp - a.timestamp); diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx new file mode 100644 index 0000000..6ad7ab0 --- /dev/null +++ b/app/scaling/components/scorecard.test.tsx @@ -0,0 +1,191 @@ +import { readFileSync } from 'node:fs'; + +import type { ReactNode } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { ProvenCapacity, Run } from '../proven/capacity'; +import type { Snapshot } from '../snapshot.mjs'; +import { Scorecard } from './Scorecard'; + +// Synthetic data only: real bench results and snapshots are internal and never committed. +const swr = vi.hoisted(() => ({ result: {} as object, key: undefined as unknown })); +vi.mock('swr', () => ({ default: (key: unknown) => ((swr.key = key), key === null ? {} : swr.result) })); +vi.mock('./Info', () => ({ Info: ({ children }: { children: ReactNode }) => {children} })); + +const BENCH = 'https://bench.example.com'; +const DAY = 86_400_000; +const T0 = Date.parse('2026-01-01T00:00:00Z'); +const LATENCY = { p50Ms: 20, p99Ms: 60, maxMs: 90 }; +const STEPS: Run['steps'] = [ + { name: 'sequencer', enabled: true, status: 'passed' }, + { name: 'validator_replay', enabled: true, status: 'passed' }, + { name: 'proof_node', enabled: true, status: 'passed' }, + { name: 'tee_proof', enabled: false, status: 'not-implemented', reason: 'needs a synthetic L1' }, +]; +const daResult = (frame_bytes: number, blobs: number, withinTarget: boolean | null): Run['da'] => ({ + result: { seconds: 600, gas_used: 4e10, transactions: 1e6, transaction_bytes: 0, frame_bytes, gas_per_second: 0, bytes_per_gas: 0, compression_ratio: 0, blobs_per_l1_slot: blobs, blob_fill: 1, blob_target_gas_per_second: 0 }, + target: 14, + withinTarget, +}); +const run = (mgas: number, workload: Run['workload'] = 'mix', da: Run['da'] = null): Run => ({ + id: `synthetic-${workload}-${mgas}`, outputDir: `synthetic-${mgas}`, workload, mgas, gps: (mgas - 5) * 1e6, tps: mgas * 10.5, passed: true, + commit: 'bbbbbbbbbbbbbbbb', createdAt: T0 + 5 * DAY, steps: STEPS, da, + replay: { blocksPerSecond: 36.4, newPayload: LATENCY, match: true }, + proofNode: { + blocksPerSecond: 20, newPayload: LATENCY, match: true, + proofs: { headLagBlocks: { samples: 10, p50: 1, p99: 4, max: 9 }, failedProbes: 0, methods: { debug_executePayload: { errors: 0, latency: { p50Ms: 200, p99Ms: 733, maxMs: 900 } } }, witnessBytes: { samples: 0, p50: 0, p99: 0, max: 0 } }, + }, +}); +const none = { mix: null, payment: null, swap: null }; +const bench = (mgas: number, atTop: boolean, more: Partial = {}): ProvenCapacity => ({ + current: { ...none, mix: { mgas, atTop, run: run(mgas, 'mix', daResult(1, 15.5, false)) } }, + da: none, + history: [ + { at: T0, mgas: 40, commit: 'aaaaaaaaaaaaaaaa', previousCommit: null, compareUrl: null }, + { at: T0 + 5 * DAY, mgas, commit: 'bbbbbbbbbbbbbbbb', previousCommit: 'aaaaaaaaaaaaaaaa', compareUrl: 'https://github.com/base/base/compare/aaaa...bbbb' }, + ], + unknown: [], + ...more, +}); +const fixture = (): Snapshot => JSON.parse(readFileSync(new URL('../fixtures/synthetic.v2.json', import.meta.url), 'utf8')); +// Sections in page order: A headline, C ladder, B bottleneck bars, D subsystem table. +const render = (props: { benchUrl?: string; dashboardUrl?: string } = { benchUrl: BENCH }, snapshot = fixture()) => { + const html = renderToStaticMarkup(); + const [, a, c, b, d] = html.split(' html.replace(/<[^>]+>/g, '').replaceAll(''', "'"); +const bar = (b: string, label: string) => text(` li.includes(`>${label}<`)) ?? '>'}`); +const row = (d: string, name: string) => text(` tr.includes(`>${name}<`)) ?? '>'}`); + +beforeEach(() => { + swr.result = {}; + swr.key = undefined; +}); + +describe('without bench data', () => { + it('fetches nothing without a URL and falls back to the snapshot and DA estimates', () => { + const { a, c, b, d } = render({}); + expect(swr.key).toBeNull(); + expect(text(a)).toContain('Proven capacity—Not established'); + expect(text(a)).toContain('Used today12.5Mgas/s median · p99 not measured · 24 h'); + expect(text(a)).toContain('Headroom—'); + expect(text(a)).toContain("First limit: DA. Ethereum's blob target fills at ~57 Mgas/s of payments Estimated or 190 Mgas/s of mainnet-like traffic Measured."); + expect(text(a)).toContain('1 Mgas/s ≈ 19 payments/s ≈ swaps/s pending'); + expect(text(c)).toContain('0 – 200 Mgas/s'); + expect(text(c)).not.toContain('proven'); + expect(bar(b, 'DA · Payments (B20)')).toContain('~57Estimated· binding'); + expect(bar(b, 'Execution (sequencer)')).toContain('no proven run'); + expect(bar(b, 'Validators')).toContain('pending'); + expect(text(d)).toContain('Measured at the proven rate'); + expect(row(d, 'Validator replay')).toContain('production lag 3 blocks'); + expect(d).not.toContain('Qualified by'); + }); + + it('uses a qualified snapshot rung and links the runs behind it', () => { + const snapshot = fixture(); + snapshot.ladder.qualified = { ...snapshot.ladder.qualified, value: 30, tag: 'measured', evidence: [{ label: 'Synthetic run', url: 'https://example.com/scaling/runs/bench/synthetic-1' }] }; + const { a, c } = render({}, snapshot); + expect(text(a)).toContain('Proven capacity30Mgas/s · from the snapshot · Synthetic run'); + expect(a).toContain('href="https://example.com/scaling/runs/bench/synthetic-1"'); + expect(text(a)).toContain('Headroom2.4×'); + expect(text(c)).toContain('proven 30'); + }); + + it('says so while loading and after a failed fetch, ignoring stale data', () => { + expect(text(render().a)).toContain('Loading bench results…'); + expect(swr.key).toEqual(['scaling-proven-capacity', BENCH]); + swr.result = { error: new TypeError('Failed to fetch'), data: bench(400, true) }; + const { a, c } = render(); + expect(text(a)).toContain('VPN'); + expect(text(a)).toContain('Proven capacity—'); + expect(text(c)).not.toContain('proven'); + }); + + it('shows when no run counts, and warns when run files failed to load', () => { + swr.result = { data: { current: none, da: none, history: [], unknown: [] } }; + expect(text(render().a)).toContain('No main run passed in the last 14 days.'); + swr.result = { data: { current: none, da: none, history: [], unknown: ['r1', 'r2'] } }; + expect(text(render().a)).toContain('2 run(s) could not be loaded'); + }); +}); + +describe('with a proven run', () => { + it('heads with proven, used and headroom, names the first limit and the last change', () => { + swr.result = { data: bench(400, true) }; + const { a } = render(); + expect(text(a)).toContain('Proven capacity≥ 400Mgas/s · sequencer, validators and proof node all pass on mainnet state'); + expect(text(a)).toContain('Headroom32×'); + expect(text(a)).toContain('of mainnet-like traffic Measured, below the proven 400.'); + expect(text(a)).toContain('↑ from 40 Mgas/s on 2026-01-06 · compare'); + expect(a).toContain('href="https://github.com/base/base/compare/aaaa...bbbb"'); + }); + + it('draws the ladder from 0 to the proven rate with three bands and five markers, keeping help text', () => { + swr.result = { data: bench(400, true) }; + const { c } = render({ benchUrl: BENCH, dashboardUrl: 'https://dashboards.example.com/scaling' }); + expect(text(c)).toContain('0 – 400 Mgas/s'); + for (const label of ['used', 'allowed by policy', 'proven, not yet allowed', 'p50 12.5', 'fee target 40', 'gas limit 200', 'proven ≥ 400']) expect(text(c)).toContain(label); + expect(text(c)).not.toMatch(/DA ·|Ceiling|payments/); + expect(c).toContain('400 Mgas/s is the highest rate the bench runs'); + expect(c).toContain('href="https://dashboards.example.com/scaling"'); + }); + + it('bars each subsystem on the same scale and highlights the lowest', () => { + swr.result = { data: bench(400, true) }; + const { b } = render(); + expect(bar(b, 'Execution (sequencer)')).toContain('≥ 400 · top rate tested'); + expect(bar(b, 'Validators')).toContain('≥ 400 · 7.3× replay headroom'); + expect(bar(b, 'Proof node')).toContain('≥ 400 · lag p99 4 blocks'); + expect(b.match(/bg-bds-red-60/g)).toHaveLength(1); + expect(bar(b, 'DA · Payments (B20)')).toContain('binding'); + expect(text(b)).toContain('today p50 12.5'); + expect(text(b)).toContain('gas limit 200'); + expect(text(b)).toContain('State joins once a runway is chosen.'); + }); + + it('binds below the top rate when the proven rate is under every DA ceiling', () => { + swr.result = { data: bench(40, false) }; + const { a, b } = render(); + expect(text(a)).toContain('Proven capacity40'); + expect(text(a)).toContain('First limit: above 40 Mgas/s, where the next rate tested did not pass.'); + expect(bar(b, 'Execution (sequencer)')).toMatch(/40· binding$/); + }); + + it('lists measured values, budgets, headroom and status per subsystem, with the run evidence', () => { + swr.result = { data: bench(400, true) }; + const { d } = render(); + expect(text(d)).toContain('Measured at 400 Mgas/s'); + expect(row(d, 'Sequencer')).toBe('Sequencer395.0 Mgas/s delivered≥ 95% of target—pass'); + expect(row(d, 'Validator replay')).toBe('Validator replay36.4 blocks/s, newPayload p99 60 ms; production lag 3 blocks≥ 5 blocks/s; production lag ≤ 3 blocks7.3×pass'); + expect(row(d, 'Proof node')).toBe('Proof nodelag p99 4 blocks; executePayload p99 733 mspending—budget pending'); + expect(row(d, 'DA (bench mix)')).toBe('DA (bench mix)15.5 blobs per L1 slot14 blobs0.9×over target'); + expect(row(d, 'State (full node)')).toBe('State (full node)pending45 TB—runway pending'); + expect(text(d)).toContain('TEE proving, RPC under syncnot measured yet'); + for (const href of ['/scaling/execution', '/scaling/da', '/scaling/state']) expect(d).toContain(`href="${href}"`); + expect(text(d)).toContain('Qualified by synthetic-400: 400 Mgas/s run delivered 395.0 Mgas/s'); + expect(d).toMatch(/bg-bds-green-50"><\/span>validator replay passed { + swr.result = { data: bench(400, true) }; + const snapshot = fixture(); + const state = snapshot.ceilings.find((c) => c.area === 'state')!; + state.inputs = state.inputs.map((i) => (i.name === 'state_bytes_per_gas' ? { ...i, value: 0.001, status: 'observed' } : i)); + expect(row(render({ benchUrl: BENCH }, snapshot).d, 'State (full node)')).toBe('State (full node)~12.6 TB/yr at 400 Estimated45 TB~3.6 yrrunway pending'); + }); + + it('switches DA bars to measured once payments and swaps have bench DA runs', () => { + swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', daResult(8e7, 20, false)), swap: run(200, 'swap', daResult(4e7, 9, true)) } }) }; + const { a, b } = render(); + expect(bar(b, 'DA · Payments (B20)')).toContain('76Measured· binding'); + expect(bar(b, 'DA · Swaps')).toContain('152Measured'); + expect(text(a)).toContain('fills at 76 Mgas/s of payments Measured'); + }); + + it('translates to bench payments/s and swaps/s, pending without a qualifying run', () => { + swr.result = { data: bench(400, true, { current: { mix: null, swap: null, payment: { mgas: 200, atTop: false, run: run(200, 'payment') } } }) }; + expect(render().a).toContain('≥ 2,100 payments/s (B20 transfers) · swaps/s (Uniswap v3) pending'); + }); +}); diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 0ab985d..713d65c 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -24,6 +24,13 @@ it('links the first screen to four detail pages instead of listing metric rows', expect(html).toContain('href="/scaling/runs"'); }); +it('orders the first screen headline, ladder, bottleneck bars, subsystem table, guardrails, progress', async () => { + const html = renderToStaticMarkup(await ScalingPage()); + const order = ['How much can Base handle', 'How close are we, and to what?', 'What breaks first?', 'Subsystem detail', 'Guardrails →', 'Progress'].map((h) => html.indexOf(h)); + expect(order.every((i, k) => i > (order[k - 1] ?? -1))).toBe(true); + expect(html).not.toContain('Ceilings by bottleneck'); +}); + it('shows every metric row on its detail page', async () => { for (const section of fixture.sections) { const html = section.id === 'outcomes' ? renderToStaticMarkup(await ScalingGuardrailsPage()) : await area(section.id); diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index b5e19b9..2f6f125 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -2,11 +2,9 @@ import Link from 'next/link'; 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 { withDaWorkloads } from './da/workloads'; +import { Scorecard } from './components/Scorecard'; import { Header, loadSnapshot, Unavailable } from './loadSnapshot'; // The snapshot is a runtime file, so render per request rather than at build. @@ -17,14 +15,12 @@ export default async function ScalingPage() { if (loaded.kind !== 'ok') return ; const { snapshot } = loaded; - const ceilings = withDaWorkloads(snapshot.ceilings); const benchUrl = process.env.SCALING_BENCH_BASE_URL; return (
    Runs →} /> - - + From 276580b63a3da2cad3efc400b1029f01cbb209bb Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 09:50:54 -0700 Subject: [PATCH 05/33] feat(scaling): judge proof-node budgets and label usage dates instead of a stale banner --- app/scaling/components/Bottlenecks.tsx | 22 ++++++++------ app/scaling/components/Guardrails.tsx | 15 ++++++---- app/scaling/components/Headline.tsx | 8 +++--- app/scaling/components/Ladder.tsx | 7 +++-- app/scaling/components/ProvenCapacity.tsx | 1 + app/scaling/components/Scorecard.tsx | 4 +-- app/scaling/components/Subsystems.tsx | 16 +++++++---- app/scaling/components/firstScreen.test.tsx | 20 +++++++------ app/scaling/components/scorecard.test.tsx | 32 +++++++++++++++++---- app/scaling/guardrails/page.tsx | 2 +- app/scaling/loadSnapshot.tsx | 16 ++++------- app/scaling/page.test.tsx | 7 +++++ app/scaling/page.tsx | 2 +- app/scaling/snapshot.mjs | 10 ------- app/scaling/snapshot.test.ts | 12 +------- 15 files changed, 100 insertions(+), 74 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index fbebe1c..56e4a67 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -2,14 +2,15 @@ import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; import { BLOB_TARGET, daCapacity, fmt, type DaWorkload } from '../da/workloads'; import type { Proven } from '../proven/capacity'; -import { REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; +import { EXECUTE_PAYLOAD_P99_MS, PROOF_LAG_P99_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; import { Tag } from './Tag'; export type Bar = { id: string; label: string; value: number | null; text: string; tag?: DaWorkload['tag'] }; const x = (n: number) => `${n < 10 ? n.toFixed(1) : Math.round(n)}×`; -// Validators and the proof node kept up at the proven rate, so their limit is at least that rate. +// Validators and the proof node within budget at the proven rate have a limit of at least that rate; +// over budget, their limit is somewhere below it. export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): Bar[] { const da = workloads.map((w): Bar => { const value = daCapacity(w, BLOB_TARGET).mgasPerSecond; @@ -18,15 +19,20 @@ export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): const run = proven?.run; const m = proven?.mgas ?? null; const proofs = run?.proofNode?.proofs; + const replay = run?.replay; + const executeP99 = proofs?.methods.debug_executePayload?.latency.p99Ms; + const proofOk = proofs && executeP99 !== undefined && proofs.headLagBlocks.p99 <= PROOF_LAG_P99_BLOCKS && executeP99 <= EXECUTE_PAYLOAD_P99_MS; return [ ...da, { id: 'execution', label: 'Execution (sequencer)', value: m, text: !proven ? 'no proven run' : proven.atTop ? `≥ ${m} · top rate tested` : `${m}` }, - run?.replay - ? { id: 'validators', label: 'Validators', value: m, text: `≥ ${m} · ${x(run.replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND)} replay headroom` } - : { id: 'validators', label: 'Validators', value: null, text: 'pending' }, - proofs - ? { id: 'proof-node', label: 'Proof node', value: m, text: `≥ ${m} · lag p99 ${proofs.headLagBlocks.p99} blocks` } - : { id: 'proof-node', label: 'Proof node', value: null, text: 'pending' }, + !replay + ? { id: 'validators', label: 'Validators', value: null, text: 'pending' } + : replay.match && replay.blocksPerSecond >= REPLAY_BLOCKS_PER_SECOND + ? { id: 'validators', label: 'Validators', value: m, text: `≥ ${m} · ${x(replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND)} replay headroom` } + : { id: 'validators', label: 'Validators', value: null, text: `below budget at ${m}` }, + !proofs + ? { id: 'proof-node', label: 'Proof node', value: null, text: 'pending' } + : { id: 'proof-node', label: 'Proof node', value: proofOk ? m : null, text: `${proofOk ? `≥ ${m}` : `over budget at ${m}`} · lag p99 ${proofs.headLagBlocks.p99} blocks` }, ]; } diff --git a/app/scaling/components/Guardrails.tsx b/app/scaling/components/Guardrails.tsx index dd4c701..349d291 100644 --- a/app/scaling/components/Guardrails.tsx +++ b/app/scaling/components/Guardrails.tsx @@ -8,7 +8,7 @@ import { Text } from '../../components/ui/Text'; import type { Run } from '../proven/capacity'; import type { Guardrail } from '../snapshot.mjs'; import { Info } from './Info'; -import { useProvenCapacity } from './ProvenCapacity'; +import { useProvenCapacity, usageAsOf } from './ProvenCapacity'; import { Tag } from './Tag'; const TH = 'px-4 py-2 text-left text-[12px] font-[500] text-bds-gray-60'; @@ -21,6 +21,9 @@ const fmt = (n: number, unit: string) => `${n.toLocaleString('en-US', { maximumF export const KEEP_UP_BLOCKS = 3; export const REPLAY_BLOCKS_PER_SECOND = 5; +// Proof-node budgets at the tested rate, owner-approved 2026-10-27. +export const PROOF_LAG_P99_BLOCKS = 10; +export const EXECUTE_PAYLOAD_P99_MS = 1000; const judge = (g: Omit): Guardrail => ({ ...g, @@ -45,11 +48,13 @@ export function scorecardGuardrails(snapshot: Guardrail[], run: Run | null | und ...rows, judge({ ...bench, id: 'proof-node-head-lag', label: 'Proof-node head lag p99', value: run?.proofNode?.proofs?.headLagBlocks.p99 ?? null, unit: 'blocks', - help: `How far proof history trails the inserted head while the proof node replays the bench blocks.${source} Threshold pending.`, + threshold: PROOF_LAG_P99_BLOCKS, thresholdTag: 'set', + help: `How far proof history trails the inserted head while the proof node replays the bench blocks.${source}`, }), judge({ ...bench, id: 'proof-node-execute-payload', label: 'debug_executePayload p99', value: run?.proofNode?.proofs?.methods.debug_executePayload?.latency.p99Ms ?? null, unit: 'ms', - help: `Latency of the witness call the prover makes for each block, on the proof node.${source} Threshold pending.`, + threshold: EXECUTE_PAYLOAD_P99_MS, thresholdTag: 'set', + help: `Latency of the witness call the prover makes for each block, on the proof node.${source}`, }), judge({ ...bench, id: 'validator-replay-headroom', label: 'Validator replay headroom', value: run?.replay?.blocksPerSecond ?? null, unit: 'blocks/s', @@ -59,7 +64,7 @@ export function scorecardGuardrails(snapshot: Guardrail[], run: Run | null | und ]; } -export function Guardrails({ guardrails, detailed = false, benchUrl }: { guardrails: Guardrail[]; detailed?: boolean; benchUrl?: string }) { +export function Guardrails({ guardrails, collectedAt, detailed = false, benchUrl }: { guardrails: Guardrail[]; collectedAt: string; detailed?: boolean; benchUrl?: string }) { const bench = useProvenCapacity(benchUrl); const run = benchUrl ? (bench.error ? null : bench.data?.current.mix?.run ?? null) : undefined; return ( @@ -102,7 +107,7 @@ export function Guardrails({ guardrails, detailed = false, benchUrl }: { guardra
    - Breached SLOs are not attributed to saturation until triaged. + {usageAsOf(collectedAt)}; bench rows show their run. Breached SLOs are not attributed to saturation until triaged. ); } diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx index c4dde59..074fcb0 100644 --- a/app/scaling/components/Headline.tsx +++ b/app/scaling/components/Headline.tsx @@ -5,7 +5,7 @@ import { runHref, type Proven, type ProvenCapacity, type Workload } from '../pro import type { Snapshot } from '../snapshot.mjs'; import type { Bar } from './Bottlenecks'; import type { ProvenMark } from './Ladder'; -import { day } from './ProvenCapacity'; +import { day, usageAsOf } from './ProvenCapacity'; import { Tag } from './Tag'; export type BenchState = { configured: boolean; data?: ProvenCapacity; error?: unknown }; @@ -17,7 +17,7 @@ const TRAFFIC: Record = { payment: 'payments', swap: 'swaps', const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 2 }); const list = (xs: string[]) => (xs.length < 2 ? xs.join('') : `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}`); -type Props = Pick & { +type Props = Pick & { bench: BenchState; proven: ProvenMark; bars: Bar[]; @@ -88,7 +88,7 @@ function BenchTranslation({ current }: { current: ProvenCapacity['current'] }) { return <>{rate(current.payment, 'payments/s', 'B20 transfers')} · {rate(current.swap, 'swaps/s', 'Uniswap v3')}; } -export function Headline({ ladder, windows, translations, bench, proven, bars, binding }: Props) { +export function Headline({ ladder, windows, translations, collectedAt, bench, proven, bars, binding }: Props) { const { p50, p99 } = ladder.observed; const window = p50.window ? ` · ${windows[p50.window].label}` : ''; const headroom = proven && p50.value ? proven.value / p50.value : null; @@ -97,7 +97,7 @@ export function Headline({ ladder, windows, translations, bench, proven, bars, b How much can Base handle, and how much do we use?
    - + Mgas/s median · {p99.value === null ? 'p99 not measured' : `${n(p99.value)} p99`}{window}
    {usageAsOf(collectedAt)}} />
    {binding ? ( diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx index 05072b8..6a76574 100644 --- a/app/scaling/components/Ladder.tsx +++ b/app/scaling/components/Ladder.tsx @@ -6,6 +6,7 @@ import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; import type { Rung, Snapshot } from '../snapshot.mjs'; import { Info } from './Info'; +import { usageAsOf } from './ProvenCapacity'; import { layoutRungs, type Anchor } from './ladderLayout'; const LABEL_PX = 130; @@ -18,9 +19,9 @@ const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 2 }) type Marker = { id: string; label: string; value: number; help: string; tick: string; text: string }; export type ProvenMark = { value: number; atLeast: boolean; help: string } | null; -type Props = Pick & { proven: ProvenMark; max: number; dashboardUrl?: string }; +type Props = Pick & { proven: ProvenMark; max: number; dashboardUrl?: string }; -export function Ladder({ ladder, windows, proven, max, dashboardUrl }: Props) { +export function Ladder({ ladder, windows, collectedAt, proven, max, dashboardUrl }: Props) { const ref = useRef(null); const [width, setWidth] = useState(960); useLayoutEffect(() => { @@ -86,7 +87,7 @@ export function Ladder({ ladder, windows, proven, max, dashboardUrl }: Props) { ))} - 0 – {n(max)} Mgas/s + {usageAsOf(collectedAt)} · 0 – {n(max)} Mgas/s {dashboardUrl ? Mainnet load, gas limit, DA ceiling → : null} diff --git a/app/scaling/components/ProvenCapacity.tsx b/app/scaling/components/ProvenCapacity.tsx index 30d2372..76744c2 100644 --- a/app/scaling/components/ProvenCapacity.tsx +++ b/app/scaling/components/ProvenCapacity.tsx @@ -5,6 +5,7 @@ import useSWR from 'swr'; import { loadProvenCapacity } from '../proven/capacity'; export const day = (t: number) => new Date(t).toISOString().slice(0, 10); +export const usageAsOf = (collectedAt: string) => `Usage data as of ${day(Date.parse(collectedAt))}`; export function useProvenCapacity(benchUrl?: string) { return useSWR(benchUrl ? ['scaling-proven-capacity', benchUrl] : null, () => loadProvenCapacity(benchUrl!), { revalidateOnFocus: false }); diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index db01c31..979f63b 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -29,8 +29,8 @@ export function Scorecard({ snapshot, benchUrl, dashboardUrl }: { snapshot: Snap const max = Math.max(mark?.value ?? 0, ladder.configuredLimit.value ?? 0) || Math.max(0, ...values); return ( <> - - + + diff --git a/app/scaling/components/Subsystems.tsx b/app/scaling/components/Subsystems.tsx index b53754e..536c151 100644 --- a/app/scaling/components/Subsystems.tsx +++ b/app/scaling/components/Subsystems.tsx @@ -7,7 +7,7 @@ import { Text } from '../../components/ui/Text'; import { BLOB_TARGET } from '../da/workloads'; import { runHref, type PipelineStep, type Proven } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; -import { KEEP_UP_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; +import { EXECUTE_PAYLOAD_P99_MS, KEEP_UP_BLOCKS, PROOF_LAG_P99_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; import { day } from './ProvenCapacity'; import { Tag } from './Tag'; @@ -20,7 +20,7 @@ const TONE: Record = { ok: 'text-bds-green-60', bad: 'text-bds-red const STEP_DOT: Record = { passed: 'bg-bds-green-50', failed: 'bg-bds-red-50', skipped: 'bg-bds-gray-30', 'not-implemented': 'bg-bds-gray-30' }; const SECONDS_PER_YEAR = 365.25 * 86_400; const PENDING: [string, Tone] = ['pending', 'muted']; -const x = (v: number) => `${v < 10 ? v.toFixed(1) : Math.round(v)}×`; +const x = (v: number) => (!Number.isFinite(v) ? '—' : `${v < 10 ? v.toFixed(1) : Math.round(v)}×`); function stepStatus(proven: Proven | null, name: string, passed: [string, Tone]): [string, Tone] { const status = proven?.run.steps.find((s) => s.name === name)?.status; @@ -53,13 +53,19 @@ function rows(proven: Proven | null, { guardrails, ceilings }: Pick= REPLAY_BLOCKS_PER_SECOND ? ['pass', 'ok'] : ['below budget', 'bad'], + status: !replay ? PENDING + : !replay.match ? ['mismatch', 'bad'] + : replay.blocksPerSecond < REPLAY_BLOCKS_PER_SECOND ? ['below budget', 'bad'] + : keepUp !== null && keepUp > KEEP_UP_BLOCKS ? ['production lag over budget', 'bad'] + : ['pass', 'ok'], }, { name: 'Proof node', measured: proofs ? [`lag p99 ${proofs.headLagBlocks.p99} blocks`, executePayload && `executePayload p99 ${executePayload.latency.p99Ms} ms`].filter(Boolean).join('; ') : 'pending', - budget: pending, headroom: '—', - status: stepStatus(proven, 'proof_node', ['budget pending', 'muted']), + budget: `≤ ${PROOF_LAG_P99_BLOCKS} blocks; ≤ ${EXECUTE_PAYLOAD_P99_MS} ms`, + headroom: proofs && executePayload ? `${x(PROOF_LAG_P99_BLOCKS / proofs.headLagBlocks.p99)} / ${x(EXECUTE_PAYLOAD_P99_MS / executePayload.latency.p99Ms)}` : '—', + status: stepStatus(proven, 'proof_node', !proofs || !executePayload ? PENDING + : proofs.headLagBlocks.p99 <= PROOF_LAG_P99_BLOCKS && executePayload.latency.p99Ms <= EXECUTE_PAYLOAD_P99_MS ? ['pass', 'ok'] : ['over budget', 'bad']), }, { name: 'DA (bench mix)', href: '/scaling/da', diff --git a/app/scaling/components/firstScreen.test.tsx b/app/scaling/components/firstScreen.test.tsx index 6a42754..1cec385 100644 --- a/app/scaling/components/firstScreen.test.tsx +++ b/app/scaling/components/firstScreen.test.tsx @@ -39,7 +39,7 @@ describe('CeilingCard', () => { describe('Guardrails', () => { it('shows comparator and unit with the threshold, tags provisional and unset thresholds, and colors only known statuses', () => { - const rows = renderToStaticMarkup().split(').split(' { }); it('sorts rows with a value first, then unknown, then muted not-monitored rows', () => { - const html = renderToStaticMarkup(); + const html = renderToStaticMarkup(); + expect(html).toContain('Usage data as of 2026-01-02; bench rows show their run.'); const rows = html.split(' r.match(/]*>(?:]*>)?([^<]+?) ? { describe('scorecardGuardrails', () => { const LATENCY = { p50Ms: 2, p99Ms: 9, maxMs: 12 }; - const run = (blocksPerSecond: number): Run => ({ + const run = (blocksPerSecond: number, lagP99 = 4, executeP99 = 9): Run => ({ id: 'synthetic', outputDir: 'synthetic-400', workload: 'mix', mgas: 400, gps: 4e8, tps: 6000, passed: true, commit: 'c0ffee', createdAt: Date.parse('2026-01-05T00:00:00Z'), steps: [], da: null, replay: { blocksPerSecond, newPayload: LATENCY, match: true }, proofNode: { blocksPerSecond: 20, newPayload: LATENCY, match: true, - proofs: { headLagBlocks: { samples: 10, p50: 1, p99: 4, max: 9 }, failedProbes: 0, methods: { debug_executePayload: { errors: 0, latency: LATENCY } }, witnessBytes: { samples: 0, p50: 0, p99: 0, max: 0 } }, + proofs: { headLagBlocks: { samples: 10, p50: 1, p99: lagP99, max: 20 }, failedProbes: 0, methods: { debug_executePayload: { errors: 0, latency: { ...LATENCY, p99Ms: executeP99 } } }, witnessBytes: { samples: 0, p50: 0, p99: 0, max: 0 } }, }, }); const byId = (rows: ReturnType) => Object.fromEntries(rows.map((g) => [g.id, g])); @@ -88,12 +89,15 @@ describe('scorecardGuardrails', () => { expect(rows.at(-1)).toMatchObject({ id: 'validator-keep-up', value: null, threshold: 3, status: 'unknown' }); }); - it('adds bench rows only with a bench host: proof-node lag and latency without a threshold, and replay headroom against 5 blocks/s', () => { + it('adds bench rows only with a bench host: proof-node lag ≤ 10 blocks, executePayload ≤ 1000 ms, replay headroom against 5 blocks/s', () => { expect(scorecardGuardrails(fixture().guardrails, undefined)).toHaveLength(6); const rows = byId(scorecardGuardrails(fixture().guardrails, run(36.4))); - expect(rows['proof-node-head-lag']).toMatchObject({ value: 4, unit: 'blocks', threshold: null, thresholdTag: 'unset', status: 'unknown', asOf: '2026-01-05T00:00:00.000Z' }); - expect(rows['proof-node-head-lag'].help).toContain('From bench run synthetic-400. Threshold pending.'); - expect(rows['proof-node-execute-payload']).toMatchObject({ value: 9, unit: 'ms', status: 'unknown' }); + expect(rows['proof-node-head-lag']).toMatchObject({ value: 4, unit: 'blocks', threshold: 10, comparator: '<=', thresholdTag: 'set', status: 'ok', asOf: '2026-01-05T00:00:00.000Z' }); + expect(rows['proof-node-head-lag'].help).toContain('From bench run synthetic-400.'); + expect(rows['proof-node-execute-payload']).toMatchObject({ value: 9, unit: 'ms', threshold: 1000, thresholdTag: 'set', status: 'ok' }); + const over = byId(scorecardGuardrails(fixture().guardrails, run(36.4, 11, 1001))); + expect(over['proof-node-head-lag'].status).toBe('breached'); + expect(over['proof-node-execute-payload'].status).toBe('breached'); expect(rows['validator-replay-headroom']).toMatchObject({ value: 36.4, threshold: 5, comparator: '>=', status: 'ok' }); expect(byId(scorecardGuardrails(fixture().guardrails, run(4)))['validator-replay-headroom'].status).toBe('breached'); }); diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 6ad7ab0..962acd5 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -69,7 +69,7 @@ describe('without bench data', () => { const { a, c, b, d } = render({}); expect(swr.key).toBeNull(); expect(text(a)).toContain('Proven capacity—Not established'); - expect(text(a)).toContain('Used today12.5Mgas/s median · p99 not measured · 24 h'); + expect(text(a)).toContain('Used today12.5Mgas/s median · p99 not measured · 24 hUsage data as of 2026-01-02'); expect(text(a)).toContain('Headroom—'); expect(text(a)).toContain("First limit: DA. Ethereum's blob target fills at ~57 Mgas/s of payments Estimated or 190 Mgas/s of mainnet-like traffic Measured."); expect(text(a)).toContain('1 Mgas/s ≈ 19 payments/s ≈ swaps/s pending'); @@ -124,9 +124,14 @@ describe('with a proven run', () => { it('draws the ladder from 0 to the proven rate with three bands and five markers, keeping help text', () => { swr.result = { data: bench(400, true) }; - const { c } = render({ benchUrl: BENCH, dashboardUrl: 'https://dashboards.example.com/scaling' }); - expect(text(c)).toContain('0 – 400 Mgas/s'); - for (const label of ['used', 'allowed by policy', 'proven, not yet allowed', 'p50 12.5', 'fee target 40', 'gas limit 200', 'proven ≥ 400']) expect(text(c)).toContain(label); + const snapshot = fixture(); + snapshot.ladder.observed.p99 = { ...snapshot.ladder.observed.p99, value: 42.2, tag: 'measured' }; + const { c } = render({ benchUrl: BENCH, dashboardUrl: 'https://dashboards.example.com/scaling' }, snapshot); + expect(text(c)).toContain('Usage data as of 2026-01-02 · 0 – 400 Mgas/s'); + for (const label of ['used', 'allowed by policy', 'proven, not yet allowed', 'p50 12.5', 'p99 42.2', 'fee target 40', 'gas limit 200', 'proven ≥ 400']) expect(text(c)).toContain(label); + // p99 has its own dashed tick at 42.2 / 400, and ends the used band there. + expect(c).toMatch(/border-dashed[^"]*" style="left:10.55%/); + expect(c).toMatch(/bg-base-blue" style="top:\d+px;height:22px;width:10.55%/); expect(text(c)).not.toMatch(/DA ·|Ceiling|payments/); expect(c).toContain('400 Mgas/s is the highest rate the bench runs'); expect(c).toContain('href="https://dashboards.example.com/scaling"'); @@ -145,6 +150,23 @@ describe('with a proven run', () => { expect(text(b)).toContain('State joins once a runway is chosen.'); }); + it('fails the proof node and validator rows over budget, and drops their bars to unknown', () => { + const over = bench(400, true); + const proven = over.current.mix!; + proven.run.proofNode!.proofs!.headLagBlocks.p99 = 11; + over.current.mix = proven; + swr.result = { data: over }; + const snapshot = fixture(); + snapshot.guardrails = snapshot.guardrails.map((g) => (g.id === 'validator-keep-up' ? { ...g, value: 4, status: 'breached' } : g)); + const { b, d } = render({ benchUrl: BENCH }, snapshot); + expect(row(d, 'Proof node')).toMatch(/0\.9× \/ 1\.4×over budget$/); + expect(row(d, 'Validator replay')).toMatch(/production lag over budget$/); + expect(bar(b, 'Proof node')).toContain('over budget at 400 · lag p99 11 blocks'); + proven.run.proofNode!.proofs!.headLagBlocks.p99 = 4; + proven.run.proofNode!.proofs!.methods.debug_executePayload.latency.p99Ms = 1200; + expect(row(render().d, 'Proof node')).toMatch(/over budget$/); + }); + it('binds below the top rate when the proven rate is under every DA ceiling', () => { swr.result = { data: bench(40, false) }; const { a, b } = render(); @@ -159,7 +181,7 @@ describe('with a proven run', () => { expect(text(d)).toContain('Measured at 400 Mgas/s'); expect(row(d, 'Sequencer')).toBe('Sequencer395.0 Mgas/s delivered≥ 95% of target—pass'); expect(row(d, 'Validator replay')).toBe('Validator replay36.4 blocks/s, newPayload p99 60 ms; production lag 3 blocks≥ 5 blocks/s; production lag ≤ 3 blocks7.3×pass'); - expect(row(d, 'Proof node')).toBe('Proof nodelag p99 4 blocks; executePayload p99 733 mspending—budget pending'); + expect(row(d, 'Proof node')).toBe('Proof nodelag p99 4 blocks; executePayload p99 733 ms≤ 10 blocks; ≤ 1000 ms2.5× / 1.4×pass'); expect(row(d, 'DA (bench mix)')).toBe('DA (bench mix)15.5 blobs per L1 slot14 blobs0.9×over target'); expect(row(d, 'State (full node)')).toBe('State (full node)pending45 TB—runway pending'); expect(text(d)).toContain('TEE proving, RPC under syncnot measured yet'); diff --git a/app/scaling/guardrails/page.tsx b/app/scaling/guardrails/page.tsx index 15477b3..e93d33e 100644 --- a/app/scaling/guardrails/page.tsx +++ b/app/scaling/guardrails/page.tsx @@ -13,7 +13,7 @@ export default async function ScalingGuardrailsPage() { return (
    - + {outcomes ? : null}
    ); diff --git a/app/scaling/loadSnapshot.tsx b/app/scaling/loadSnapshot.tsx index c645c85..ae650c0 100644 --- a/app/scaling/loadSnapshot.tsx +++ b/app/scaling/loadSnapshot.tsx @@ -2,19 +2,17 @@ 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'; +import { 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 }; + | { kind: 'ok'; snapshot: Snapshot; ageDays: number }; export async function loadSnapshot(): Promise { const path = process.env.SCALING_SNAPSHOT_PATH; @@ -22,9 +20,8 @@ export async function loadSnapshot(): Promise { 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) }; + const ageDays = Math.floor((Date.now() - Date.parse(result.snapshot.collectedAt)) / DAY_MS); + return { kind: 'ok', snapshot: result.snapshot, ageDays }; } catch (error) { return { kind: 'invalid', errors: [error instanceof Error ? error.message : String(error)] }; } @@ -48,7 +45,7 @@ export function Unavailable({ loaded }: { loaded: Exclude; title: ReactNode; crumb?: string; action?: ReactNode }) { - const { snapshot, ageDays, stale } = loaded; + const { snapshot, ageDays } = loaded; return (
    {crumb ? ( @@ -60,9 +57,6 @@ export function Header({ loaded, title, crumb, action }: { loaded: Extract - {stale ? ( - Stale: this snapshot is older than {STALE_AFTER_DAYS} days. Refresh it before using these numbers. - ) : null}
    ); } diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 713d65c..6024966 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -31,6 +31,13 @@ it('orders the first screen headline, ladder, bottleneck bars, subsystem table, expect(html).not.toContain('Ceilings by bottleneck'); }); +it('labels snapshot usage with its date instead of a stale banner, on every page', async () => { + // The fixture is far older than any staleness window. + const pages = [await ScalingPage(), await ScalingGuardrailsPage()].map((p) => renderToStaticMarkup(p)); + for (const html of pages) expect(html).toContain('Usage data as of 2026-01-02'); + for (const html of [...pages, await area('da')]) expect(html).not.toContain('Stale'); +}); + it('shows every metric row on its detail page', async () => { for (const section of fixture.sections) { const html = section.id === 'outcomes' ? renderToStaticMarkup(await ScalingGuardrailsPage()) : await area(section.id); diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index 2f6f125..3bf3e8b 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -21,7 +21,7 @@ export default async function ScalingPage() {
    Runs →} /> - + diff --git a/app/scaling/snapshot.mjs b/app/scaling/snapshot.mjs index 531f036..e827493 100644 --- a/app/scaling/snapshot.mjs +++ b/app/scaling/snapshot.mjs @@ -56,7 +56,6 @@ const AREAS = ['da', 'execution', 'state', 'networking']; // Each section renders on its area page, or on the guardrails page for outcomes. const SECTION_IDS = ['da', 'execution', 'state', 'outcomes']; const INPUT_STATUSES = ['observed', 'configured', 'provisional', 'pending']; -const DAY_MS = 86_400_000; // Captures a bench run id, or a load test's network and timestamp. export const RUN_PAGE = /^https:\/\/[^/]+\/scaling\/runs\/(?:bench\/([^/?#]+)|load\/([^/?#]+)\/([^/?#]+))\/?(?:[?#].*)?$/; const V1_MESSAGE = @@ -278,12 +277,3 @@ export function deriveBinding(ceilings) { } return binding; } - -/** - * @param {string} collectedAt - * @param {Date} now - * @param {number} [days] - */ -export function isStale(collectedAt, now, days = 10) { - return now.getTime() - Date.parse(collectedAt) > days * DAY_MS; -} diff --git a/app/scaling/snapshot.test.ts b/app/scaling/snapshot.test.ts index f446d15..3fc3a9f 100644 --- a/app/scaling/snapshot.test.ts +++ b/app/scaling/snapshot.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -import { deriveBinding, isStale, parseSnapshot } from './snapshot.mjs'; +import { deriveBinding, parseSnapshot } from './snapshot.mjs'; // Synthetic data only: real snapshots are internal and never committed. function fixture() { @@ -144,13 +144,3 @@ describe('deriveBinding', () => { expect(deriveBinding(ceilings)).toBeNull(); }); }); - -describe('isStale', () => { - const collectedAt = fixture().collectedAt; - - it('flags snapshots older than the threshold', () => { - expect(isStale(collectedAt, new Date('2026-01-13T00:00:00Z'))).toBe(true); - expect(isStale(collectedAt, new Date('2026-01-11T00:00:00Z'))).toBe(false); - expect(isStale(collectedAt, new Date('2026-01-13T00:00:00Z'), 20)).toBe(false); - }); -}); From 71fcbd7702e3b048ecb0fcc77ee21e5ce5976443 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 09:55:06 -0700 Subject: [PATCH 06/33] test(scaling): strip tags until none remain in the scorecard test helper Addresses CodeQL js/incomplete-multi-character-sanitization on a test-only helper that reads rendered markup. --- app/scaling/components/scorecard.test.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 962acd5..8efc24f 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -55,7 +55,15 @@ const render = (props: { benchUrl?: string; dashboardUrl?: string } = { benchUrl const [, a, c, b, d] = html.split(' html.replace(/<[^>]+>/g, '').replaceAll(''', "'"); +// Strip tags until none remain, so a tag split across a removed one cannot survive. +const text = (html: string) => { + let out = html; + for (let prev = ''; prev !== out; ) { + prev = out; + out = out.replace(/<[^>]*>/g, ''); + } + return out.replaceAll(''', "'"); +}; const bar = (b: string, label: string) => text(` li.includes(`>${label}<`)) ?? '>'}`); const row = (d: string, name: string) => text(` tr.includes(`>${name}<`)) ?? '>'}`); From 60617d31c2f8f45adc586dbdcc24814d27e874b6 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 10:12:50 -0700 Subject: [PATCH 07/33] feat(scaling): shade the ladder in two tones and retitle the page --- app/scaling/[area]/page.tsx | 2 +- app/scaling/components/Ladder.tsx | 152 ++++++++++++-------- app/scaling/components/ladderLayout.test.ts | 33 ----- app/scaling/components/ladderLayout.ts | 21 --- app/scaling/components/scorecard.test.tsx | 32 +++-- app/scaling/guardrails/page.tsx | 2 +- app/scaling/loadSnapshot.tsx | 15 +- app/scaling/page.test.tsx | 10 +- app/scaling/page.tsx | 2 +- 9 files changed, 130 insertions(+), 139 deletions(-) delete mode 100644 app/scaling/components/ladderLayout.test.ts delete mode 100644 app/scaling/components/ladderLayout.ts diff --git a/app/scaling/[area]/page.tsx b/app/scaling/[area]/page.tsx index 67feb01..2fc1cf3 100644 --- a/app/scaling/[area]/page.tsx +++ b/app/scaling/[area]/page.tsx @@ -24,7 +24,7 @@ export default async function ScalingAreaPage({ params }: { params: Promise<{ ar const ceilings = withDaWorkloads(snapshot.ceilings); return (
    -
    +
    Ceiling · Mgas/s diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx index 6a76574..cd54b49 100644 --- a/app/scaling/components/Ladder.tsx +++ b/app/scaling/components/Ladder.tsx @@ -7,19 +7,27 @@ import { Text } from '../../components/ui/Text'; import type { Rung, Snapshot } from '../snapshot.mjs'; import { Info } from './Info'; import { usageAsOf } from './ProvenCapacity'; -import { layoutRungs, type Anchor } from './ladderLayout'; -const LABEL_PX = 130; -const ROW = 36; -const BAR = 22; -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%)' }; -const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 2 }); +const BAR_Y = 50; +const BAR_H = 24; +const ROW_PX = 18; +const CHAR_PX = 7.5; +const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 1 }); -type Marker = { id: string; label: string; value: number; help: string; tick: string; text: string }; export type ProvenMark = { value: number; atLeast: boolean; help: string } | null; type Props = Pick & { proven: ProvenMark; max: number; dashboardUrl?: string }; +type Label = { id: string; text: string; help: string; x: number; className: string }; + +const SHADES = { + median: 'fill-base-blue bg-base-blue', + p99: 'fill-bds-blue-30 bg-bds-blue-30', + allowed: 'fill-bds-blue-10 bg-bds-blue-10 dark:fill-bds-blue-80 dark:bg-bds-blue-80', + proven: 'fill-bds-gray-10 bg-bds-gray-10 dark:fill-white/10 dark:bg-white/10', +}; + +// Width of a label with its help button, so fanned labels can be kept apart. +const labelPx = (text: string) => text.length * CHAR_PX + 20; export function Ladder({ ladder, windows, collectedAt, proven, max, dashboardUrl }: Props) { const ref = useRef(null); @@ -31,63 +39,93 @@ export function Ladder({ ladder, windows, collectedAt, proven, max, dashboardUrl }, []); const help = (r: Rung) => (r.window ? `${r.help} Window: ${windows[r.window].label}.` : r.help); - const mark = (id: string, name: string, r: Rung, tick: string, text = ''): Marker[] => - r.value === null ? [] : [{ id, label: `${name} ${n(r.value)}`, value: r.value, help: help(r), tick, text }]; + const frac = (v: number) => Math.min(1, v / max); + const pct = (v: number) => `${frac(v) * 100}%`; + const px = (v: number) => frac(v) * width; const { p50, p99 } = ladder.observed; const limit = ladder.configuredLimit.value; - const markers: Marker[] = [ - ...mark('p50', 'p50', p50, 'border-l-2 border-white'), - ...mark('p99', 'p99', p99, 'border-l border-dashed border-bds-gray-100 dark:border-white'), - ...mark('operating', 'fee target', ladder.operatingPoint, 'border-l border-dashed border-bds-gray-100 dark:border-white'), - ...mark('limit', 'gas limit', ladder.configuredLimit, 'border-l-2 border-bds-orange-60', 'text-bds-orange-60'), - ...(proven - ? [{ id: 'proven', label: `proven ${proven.atLeast ? '≥ ' : ''}${n(proven.value)}`, value: proven.value, help: proven.help, tick: 'border-l-[3px] border-base-blue', text: 'font-[600] text-base-blue' }] - : []), + const fee = ladder.operatingPoint.value; + const median = p50.value; + const tail = median === null ? null : p99.value; + + // p50 and p99 are the shade boundaries; their labels fan out to the right on angled leaders. + let nextFree = 0; + const fanned = ([['p50', median, p50], ['p99', tail, p99]] as const).flatMap(([id, value, rung]) => { + if (value === null) return []; + const text = `${id} ${n(value)}`; + const left = Math.min(Math.max(px(value) + 17, nextFree), width - labelPx(text)); + nextFree = left + labelPx(text) + 8; + return [{ id, value, text, left, help: help(rung) }]; + }); + const usedEnd = tail ?? median ?? 0; + const below: Label[] = [ + ...(fee === null ? [] : [{ id: 'fee', text: `fee target ${n(fee)}`, help: help(ladder.operatingPoint), x: fee, className: 'text-[12px] text-bds-gray-60' }]), + ...(limit === null ? [] : [{ id: 'limit', text: `gas limit ${n(limit)}`, help: help(ladder.configuredLimit), x: limit, className: 'text-bds-orange-60' }]), + ...(proven ? [{ id: 'proven', text: `proven ${proven.atLeast ? '≥ ' : ''}${n(proven.value)}`, help: proven.help, x: proven.value, className: 'font-[600] text-base-blue' }] : []), ]; - const placed = max > 0 ? layoutRungs(markers, max, LABEL_PX / width) : []; - const depth = (side: string) => 1 + Math.max(-1, ...placed.filter((p) => p.side === side).map((p) => p.tier)); - const above = depth('above'); - const barTop = above * ROW + 6; - const pct = (v: number) => `${Math.min(100, (v / max) * 100)}%`; - const used = p99.value ?? p50.value; - const bands = [ - used !== null && { label: 'used', className: 'bg-base-blue' }, - limit !== null && { label: 'allowed by policy', className: 'bg-bds-blue-10 dark:bg-bds-blue-80' }, - proven && limit !== null && proven.value > limit && { label: 'proven, not yet allowed', className: 'bg-bds-gray-10 dark:bg-white/10' }, - ].filter((b) => b !== false && b !== null); + // Labels under the bar take the first row where they clear the previous label. + const rowEnds: number[] = []; + const placedBelow = below.toSorted((a, b) => a.x - b.x).map((l) => { + const w = labelPx(l.text); + const left = Math.min(Math.max(px(l.x) - w / 2, 0), width - w); + let row = rowEnds.findIndex((end) => end + 6 <= left); + if (row === -1) row = rowEnds.length; + rowEnds[row] = left + w; + return { ...l, left, row }; + }); + const axisTop = BAR_Y + BAR_H + 10 + Math.max(1, rowEnds.length) * ROW_PX; + const height = axisTop + 14; + const captions = [ + limit !== null && limit > usedEnd && { text: 'allowed by policy', from: usedEnd, to: limit, className: 'text-bds-blue-60' }, + proven && limit !== null && proven.value > limit && { text: 'proven, not yet allowed', from: limit, to: proven.value, className: 'text-bds-gray-50' }, + ].filter((c) => !!c); + const legend = [ + median !== null && ['up to median', SHADES.median], + tail !== null && ['median to p99', SHADES.p99], + limit !== null && ['allowed by policy', SHADES.allowed], + proven && limit !== null && proven.value > limit && ['proven, not yet allowed', SHADES.proven], + ].filter((l) => !!l); return (
    How close are we, and to what? · Mgas/s -
    -
    - {limit === null ? null :
    } - {used === null ? null :
    } - {placed.map((p) => { - const m = markers.find((x) => x.id === p.id)!; - const top = p.side === 'above' ? (above - 1 - p.tier) * ROW : barTop + BAR + 6 + p.tier * ROW; - return ( -
    -
    -
    - - {m.label} {m.help} - -
    -
    - ); - })} -
    -
    - - {bands.map((b) => ( - +
    + {usageAsOf(collectedAt)} · 0 – {n(max)} Mgas/s + {fee === null ? null : } + {limit === null ? null : } + {proven ? : null} + + {fanned.map((f) => ( + + {f.text} {f.help} + + ))} + {captions.map((c) => ( + {c.text} + ))} + {placedBelow.map((l) => ( + + {l.text} {l.help} + + ))} + 0 + {n(max)} Mgas/s +
    +
    + {legend.map(([label, shade]) => ( + + ))} + {usageAsOf(collectedAt)}
    {dashboardUrl ? Mainnet load, gas limit, DA ceiling → : null}
    diff --git a/app/scaling/components/ladderLayout.test.ts b/app/scaling/components/ladderLayout.test.ts deleted file mode 100644 index 9b073c7..0000000 --- a/app/scaling/components/ladderLayout.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { layoutRungs } from './ladderLayout'; - -const rungs = (values: number[]) => values.map((value, i) => ({ id: `r${i}`, value })); - -describe('layoutRungs', () => { - it('keeps sparse rungs on the first tier, alternating sides in value order', () => { - const placed = layoutRungs(rungs([40, 200, 12.5, 95]), 200, 0.15); - expect(placed.map((p) => p.id)).toEqual(['r2', 'r0', 'r3', 'r1']); - expect(placed.map((p) => p.side)).toEqual(['above', 'below', 'above', 'below']); - expect(placed.every((p) => p.tier === 0)).toBe(true); - }); - - it('moves a rung to the next free tier on its side when its box would overlap', () => { - const placed = layoutRungs(rungs([10, 12, 14, 16, 18]), 200, 0.2); - expect(placed.map((p) => [p.side, p.tier])).toEqual([ - ['above', 0], ['below', 0], ['above', 1], ['below', 1], ['above', 2], - ]); - }); - - it('returns to the first tier once the previous label on that side ends', () => { - const placed = layoutRungs(rungs([10, 20, 30, 150]), 200, 0.2); - expect(placed.map((p) => p.tier)).toEqual([0, 0, 1, 0]); - }); - - it('anchors labels near the ends so they stay inside the axis, and clamps values above the limit', () => { - const [low, mid, high] = layoutRungs(rungs([5, 100, 250]), 200, 0.2); - expect(low).toMatchObject({ x: 0.025, anchor: 'start' }); - expect(mid).toMatchObject({ x: 0.5, anchor: 'middle' }); - expect(high).toMatchObject({ x: 1, anchor: 'end' }); - }); -}); diff --git a/app/scaling/components/ladderLayout.ts b/app/scaling/components/ladderLayout.ts deleted file mode 100644 index 49511e2..0000000 --- a/app/scaling/components/ladderLayout.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type Anchor = 'start' | 'middle' | 'end'; -export type Placement = { id: string; x: number; side: 'above' | 'below'; tier: number; anchor: Anchor }; - -// Positions along a 0–1 axis; `labelWidth` shares that unit. Rungs alternate sides in value -// order; a label takes the first tier on its side where it does not overlap the previous label. -export function layoutRungs(rungs: { id: string; value: number }[], max: number, labelWidth: number): Placement[] { - const rightEdges: Record = { above: [], below: [] }; - return [...rungs] - .sort((a, b) => a.value - b.value) - .map((r, i) => { - const x = Math.min(1, r.value / max); - const anchor: Anchor = x < labelWidth / 2 ? 'start' : x > 1 - labelWidth / 2 ? 'end' : 'middle'; - const left = anchor === 'start' ? x : anchor === 'end' ? x - labelWidth : x - labelWidth / 2; - const side = i % 2 === 0 ? 'above' : 'below'; - const edges = rightEdges[side]; - let tier = edges.findIndex((edge) => edge <= left); - if (tier === -1) tier = edges.length; - edges[tier] = left + labelWidth; - return { id: r.id, x, side, tier, anchor }; - }); -} diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 8efc24f..8c71b42 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -81,7 +81,7 @@ describe('without bench data', () => { expect(text(a)).toContain('Headroom—'); expect(text(a)).toContain("First limit: DA. Ethereum's blob target fills at ~57 Mgas/s of payments Estimated or 190 Mgas/s of mainnet-like traffic Measured."); expect(text(a)).toContain('1 Mgas/s ≈ 19 payments/s ≈ swaps/s pending'); - expect(text(c)).toContain('0 – 200 Mgas/s'); + expect(text(c)).toContain('200 Mgas/s'); expect(text(c)).not.toContain('proven'); expect(bar(b, 'DA · Payments (B20)')).toContain('~57Estimated· binding'); expect(bar(b, 'Execution (sequencer)')).toContain('no proven run'); @@ -130,21 +130,35 @@ describe('with a proven run', () => { expect(a).toContain('href="https://github.com/base/base/compare/aaaa...bbbb"'); }); - it('draws the ladder from 0 to the proven rate with three bands and five markers, keeping help text', () => { + it('shades the used band in two tones at p50 and p99, notches the fee target, and marks the gas limit and proven rate', () => { swr.result = { data: bench(400, true) }; const snapshot = fixture(); - snapshot.ladder.observed.p99 = { ...snapshot.ladder.observed.p99, value: 42.2, tag: 'measured' }; + snapshot.ladder.observed.p99 = { ...snapshot.ladder.observed.p99, value: 42.24, tag: 'measured' }; const { c } = render({ benchUrl: BENCH, dashboardUrl: 'https://dashboards.example.com/scaling' }, snapshot); - expect(text(c)).toContain('Usage data as of 2026-01-02 · 0 – 400 Mgas/s'); - for (const label of ['used', 'allowed by policy', 'proven, not yet allowed', 'p50 12.5', 'p99 42.2', 'fee target 40', 'gas limit 200', 'proven ≥ 400']) expect(text(c)).toContain(label); - // p99 has its own dashed tick at 42.2 / 400, and ends the used band there. - expect(c).toMatch(/border-dashed[^"]*" style="left:10.55%/); - expect(c).toMatch(/bg-base-blue" style="top:\d+px;height:22px;width:10.55%/); - expect(text(c)).not.toMatch(/DA ·|Ceiling|payments/); + // 12.5 / 400 and 42.24 / 400 of the axis. + expect(c).toMatch(/data-band="p50" width="3.125%"/); + expect(c).toMatch(/data-band="p99" width="10.56%"/); + expect(c).toContain('data-notch="fee"'); + for (const label of ['p50 12.5', 'p99 42.2', 'fee target 40', 'gas limit 200', 'proven ≥ 400', 'allowed by policy', 'proven, not yet allowed', '400 Mgas/s']) expect(text(c)).toContain(label); + expect(text(c)).toContain('up to medianmedian to p99allowed by policyproven, not yet allowedUsage data as of 2026-01-02'); + expect(text(c)).not.toMatch(/DA ·|Ceiling|payments|42\.24/); expect(c).toContain('400 Mgas/s is the highest rate the bench runs'); expect(c).toContain('href="https://dashboards.example.com/scaling"'); }); + it('shades only to p50 without a p99, and draws no used band without either', () => { + const { c } = render({}); + expect(c).toMatch(/data-band="p50" width="6.25%"/); + expect(c).not.toContain('data-band="p99"'); + expect(text(c)).toContain('p50 12.5'); + expect(text(c)).not.toMatch(/p99|median to p99/); + const snapshot = fixture(); + snapshot.ladder.observed.p50 = { ...snapshot.ladder.observed.p50, value: null, tag: 'not-measured' }; + const empty = render({}, snapshot).c; + expect(empty).not.toContain('data-band'); + expect(text(empty)).not.toMatch(/p50|up to median/); + }); + it('bars each subsystem on the same scale and highlights the lowest', () => { swr.result = { data: bench(400, true) }; const { b } = render(); diff --git a/app/scaling/guardrails/page.tsx b/app/scaling/guardrails/page.tsx index e93d33e..8d8731d 100644 --- a/app/scaling/guardrails/page.tsx +++ b/app/scaling/guardrails/page.tsx @@ -12,7 +12,7 @@ export default async function ScalingGuardrailsPage() { const outcomes = snapshot.sections.find((s) => s.id === 'outcomes'); return (
    -
    +
    {outcomes ? : null}
    diff --git a/app/scaling/loadSnapshot.tsx b/app/scaling/loadSnapshot.tsx index ae650c0..dc06375 100644 --- a/app/scaling/loadSnapshot.tsx +++ b/app/scaling/loadSnapshot.tsx @@ -7,12 +7,10 @@ import { Text } from '../components/ui/Text'; import { parseSnapshot, type Snapshot } from './snapshot.mjs'; -const DAY_MS = 86_400_000; - export type Loaded = | { kind: 'unconfigured' } | { kind: 'invalid'; errors: string[] } - | { kind: 'ok'; snapshot: Snapshot; ageDays: number }; + | { kind: 'ok'; snapshot: Snapshot }; export async function loadSnapshot(): Promise { const path = process.env.SCALING_SNAPSHOT_PATH; @@ -20,8 +18,7 @@ export async function loadSnapshot(): Promise { try { const result = parseSnapshot(JSON.parse(await readFile(path, 'utf8'))); if (!result.ok) return { kind: 'invalid', errors: result.errors }; - const ageDays = Math.floor((Date.now() - Date.parse(result.snapshot.collectedAt)) / DAY_MS); - return { kind: 'ok', snapshot: result.snapshot, ageDays }; + return { kind: 'ok', snapshot: result.snapshot }; } catch (error) { return { kind: 'invalid', errors: [error instanceof Error ? error.message : String(error)] }; } @@ -44,19 +41,13 @@ export function Unavailable({ loaded }: { loaded: Exclude; title: ReactNode; crumb?: string; action?: ReactNode }) { - const { snapshot, ageDays } = loaded; +export function Header({ title, crumb }: { title: ReactNode; crumb?: string }) { return (
    {crumb ? ( Scaling / {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 6024966..68f794a 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -21,7 +21,9 @@ it('links the first screen to four detail pages instead of listing metric rows', for (const page of ['da', 'execution', 'state', 'guardrails']) expect(html).toContain(`href="/scaling/${page}"`); expect(html).not.toContain('href="/scaling/networking"'); expect(html).not.toContain('Blob utilization'); - expect(html).toContain('href="/scaling/runs"'); + expect(html).not.toContain('href="/scaling/runs"'); + expect(html).toMatch(/]*>synthetic-testnet capacity<\/h1>/); + expect(html).not.toMatch(/As of|manually refreshed|Synthetic fixture: every number/); }); it('orders the first screen headline, ladder, bottleneck bars, subsystem table, guardrails, progress', async () => { @@ -45,10 +47,10 @@ it('shows every metric row on its detail page', async () => { } }); -it('renders an area with breadcrumb, as-of header, ceiling, metric details, gaps and a runs slot', async () => { +it('renders an area with breadcrumb, no as-of line, 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).not.toContain('As of 2026-01-02'); expect(html).toMatch(/>Ceiling ]*>· Mgas\/s<\/span>/); expect(html).toContain('Lowest: Payments (B20) · 14-blob target, no margin'); expect(html).toContain('>1,517<'); @@ -95,7 +97,7 @@ it('links the first-screen guardrail strip to the guardrails page', async () => 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'); + expect(html).not.toContain('As of 2026-01-02'); for (const g of fixture.guardrails) if (g.id !== 'validator-keep-up') expect(html).toContain(g.help); expect(html).toContain('They must stay within 3 blocks.'); expect(html).not.toContain('Proof-node head lag p99'); diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index 3bf3e8b..1d5aeeb 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -18,7 +18,7 @@ export default async function ScalingPage() { const benchUrl = process.env.SCALING_BENCH_BASE_URL; return (
    -
    Runs →} /> +
    From cab4429ed754649643b9f5ef30bc367a0ab461c2 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 10:34:44 -0700 Subject: [PATCH 08/33] feat(scaling): lead with transactions per second and proven payments and swaps --- app/scaling/components/Headline.tsx | 112 ++++++++++++++-------- app/scaling/components/Scorecard.tsx | 5 +- app/scaling/components/scorecard.test.tsx | 60 +++++++++--- app/scaling/fixtures/synthetic.v2.json | 11 ++- app/scaling/snapshot.mjs | 12 +++ app/scaling/snapshot.test.ts | 11 +++ 6 files changed, 153 insertions(+), 58 deletions(-) diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx index 074fcb0..4f7c8af 100644 --- a/app/scaling/components/Headline.tsx +++ b/app/scaling/components/Headline.tsx @@ -1,50 +1,93 @@ import type { ReactNode } from 'react'; import { Text } from '../../components/ui/Text'; +import { BLOB_TARGET, daCapacity, fmt, type DaWorkload } from '../da/workloads'; import { runHref, type Proven, type ProvenCapacity, type Workload } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; import type { Bar } from './Bottlenecks'; +import { Info } from './Info'; import type { ProvenMark } from './Ladder'; import { day, usageAsOf } from './ProvenCapacity'; import { Tag } from './Tag'; export type BenchState = { configured: boolean; data?: ProvenCapacity; error?: unknown }; -const STEP_NAME: Record = { - sequencer: 'sequencer', validator_replay: 'validators', proof_node: 'proof node', da: 'DA encoding', tee_proof: 'TEE proving', rpc_sync: 'RPC sync', -}; const TRAFFIC: Record = { payment: 'payments', swap: 'swaps', mix: 'mainnet-like traffic' }; -const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 2 }); -const list = (xs: string[]) => (xs.length < 2 ? xs.join('') : `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}`); +const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 1 }); +const compact = (v: number) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(v).toLowerCase(); -type Props = Pick & { +type Props = Pick & { bench: BenchState; proven: ProvenMark; + workloads: DaWorkload[]; bars: Bar[]; binding: Bar | null; }; -function provenCaption({ configured, data, error }: BenchState, proven: ProvenMark, qualified: Snapshot['ladder']['qualified']): ReactNode { +// Only states other than a loaded bench result need a note; a loaded result shows in the cards. +function benchNote({ configured, data, error }: BenchState, proven: ProvenMark, qualified: Snapshot['ladder']['qualified']): ReactNode { if (!configured) { - if (!proven) return ; - return <>Mgas/s · from the snapshot{qualified.evidence?.map((l) => · {l.label})}; + if (!proven) return null; + return <>Proven capacity {n(proven.value)} Mgas/s, from the snapshot{qualified.evidence?.map((l) => · {l.label})}; } if (error) return 'Could not load bench results. The bench host is reachable only on VPN and must allow this origin via CORS.'; if (!data) return 'Loading bench results…'; // A run whose files failed to load is skipped, so an older result at its rate may count instead. - const partial = data.unknown.length ? ` · ${data.unknown.length} run(s) could not be loaded, so this may be wrong` : ''; - const mix = data.current.mix; - if (!mix) return `No main run passed in the last 14 days${partial}.`; - const passed = mix.run.steps.filter((s) => s.status === 'passed').map((s) => STEP_NAME[s.name] ?? s.name); - return `Mgas/s · ${list(passed)} ${passed.length > 1 ? 'all pass' : 'pass'} on mainnet state${partial}`; + const partial = data.unknown.length ? `${data.unknown.length} run(s) could not be loaded, so this may be wrong.` : ''; + return data.current.mix ? partial || null : `No main run passed in the last 14 days. ${partial}`.trim(); +} + +function Lead({ ladder, windows, usage, proven, collectedAt }: Pick) { + const { p50, p99 } = ladder.observed; + const tps = usage?.transactionsPerSecond; + const headroom = proven && p50.value ? proven.value / p50.value : null; + const gas = [p50.value !== null && `${n(p50.value)} Mgas/s median`, p99.value !== null && `${n(p99.value)} p99`].filter(Boolean).join(', '); + const details = [ + tps && `${n((tps.value * 86_400) / 1e6)}M per day`, + tps && gas, + !tps && p99.value !== null && `${n(p99.value)} p99`, + headroom !== null && `proven capacity is ${headroom < 10 ? headroom.toFixed(1) : Math.round(headroom)}× that`, + usageAsOf(collectedAt), + ].filter(Boolean); + const [value, unit, over, help] = tps + ? [fmt(tps.value), 'transactions per second', windows[tps.window].label, tps.help] + : p50.value !== null ? [n(p50.value), 'Mgas/s median', p50.window ? windows[p50.window].label : null, p50.help] : ['—', 'usage not measured', null, null]; + return ( +
    +

    + {value} + {unit} + {over ? averaged over {over} : null} + {help ? {help} : null} +

    + {details.join(' · ')} +
    + ); } -function Stat({ label, value, caption }: { label: string; value: string; caption: ReactNode }) { +function Card({ label, proven, workload, todayTps }: { label: string; proven: Proven | null; workload: DaWorkload; todayTps: number | null }) { + const cap = daCapacity(workload, BLOB_TARGET).txPerSecond; + const value = proven?.run.tps; + const pct = (v: number) => `${Math.min(100, (v / value!) * 100)}%`; return (
    - {label} - {value} - {caption} + {label} + {value === undefined ? ( + pending + ) : ( + {proven!.atTop ? '≥ ' : ''}{compact(value)} + )} + {value === undefined ? null : ( +
    +
    + {todayTps === null ? null : } + +
    + )} + + DA caps at ~{fmt(cap)} + +
    ); } @@ -75,38 +118,25 @@ function LastChange({ history }: { history: ProvenCapacity['history'] }) { ); } -function translation({ gasPerPayment, gasPerSwap }: Snapshot['translations']): string { - if (!gasPerPayment && !gasPerSwap) return 'Translation to payments/s and swaps/s: constants pending'; - const per = (c: typeof gasPerPayment, noun: string) => (c ? `${Math.round(1e6 / c.value)} ${noun}/s` : `${noun}/s pending`); - return `1 Mgas/s ≈ ${per(gasPerPayment, 'payments')} ≈ ${per(gasPerSwap, 'swaps')}`; -} - -// Delivered tx/s of each workload's proven run; the rate can only be higher. -function BenchTranslation({ current }: { current: ProvenCapacity['current'] }) { - const rate = (p: Proven | null, noun: string, what: string) => - p ? ≥ {Math.floor(p.run.tps).toLocaleString('en-US')} {noun} ({what}) : `${noun} (${what}) pending`; - return <>{rate(current.payment, 'payments/s', 'B20 transfers')} · {rate(current.swap, 'swaps/s', 'Uniswap v3')}; -} - -export function Headline({ ladder, windows, translations, collectedAt, bench, proven, bars, binding }: Props) { - const { p50, p99 } = ladder.observed; - const window = p50.window ? ` · ${windows[p50.window].label}` : ''; - const headroom = proven && p50.value ? proven.value / p50.value : null; +export function Headline({ ladder, windows, usage, collectedAt, bench, proven, workloads, bars, binding }: Props) { + const note = benchNote(bench, proven, ladder.qualified); + const todayTps = usage?.transactionsPerSecond.value ?? null; + const workload = (w: Workload) => workloads.find((x) => x.payload === w)!; return ( -
    +
    How much can Base handle, and how much do we use? -
    - - Mgas/s median · {p99.value === null ? 'p99 not measured' : `${n(p99.value)} p99`}{window}
    {usageAsOf(collectedAt)}} /> - + +
    + +
    + {note ? {note} : null} {binding ? (

    {bench.data ? : null}

    ) : null} - {bench.data ? : translation(translations)}
    ); } diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index 979f63b..3721855 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -24,12 +24,13 @@ export function Scorecard({ snapshot, benchUrl, dashboardUrl }: { snapshot: Snap const proven = data?.current.mix ?? null; const qualified = ladder.qualified; const mark = proven ? benchMark(proven) : !benchUrl && qualified.value !== null ? { value: qualified.value, atLeast: false, help: qualified.help } : null; - const bars = bottleneckBars(daWorkloads(data?.da), proven); + const workloads = daWorkloads(data?.da); + const bars = bottleneckBars(workloads, proven); const values = [ladder.observed.p50, ladder.observed.p99, ladder.operatingPoint, ladder.configuredLimit].map((r) => r.value ?? 0); const max = Math.max(mark?.value ?? 0, ladder.configuredLimit.value ?? 0) || Math.max(0, ...values); return ( <> - + diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 8c71b42..ea0bbd4 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -65,6 +65,7 @@ const text = (html: string) => { return out.replaceAll(''', "'"); }; const bar = (b: string, label: string) => text(` li.includes(`>${label}<`)) ?? '>'}`); +const cards = (a: string) => a.split('uppercase tracking-wide').slice(1); const row = (d: string, name: string) => text(` tr.includes(`>${name}<`)) ?? '>'}`); beforeEach(() => { @@ -76,11 +77,14 @@ describe('without bench data', () => { it('fetches nothing without a URL and falls back to the snapshot and DA estimates', () => { const { a, c, b, d } = render({}); expect(swr.key).toBeNull(); - expect(text(a)).toContain('Proven capacity—Not established'); - expect(text(a)).toContain('Used today12.5Mgas/s median · p99 not measured · 24 hUsage data as of 2026-01-02'); - expect(text(a)).toContain('Headroom—'); + expect(text(a)).toContain('64transactions per secondaveraged over the last 30 days'); + expect(text(a)).toContain('5.5M per day · 12.5 Mgas/s median · Usage data as of 2026-01-02'); + expect(text(a)).not.toMatch(/Proven capacity|Used today|Headroom|proven capacity is|payments\/s/); + const [payments, swaps] = cards(a); + expect(text(payments)).toContain('Payments per second, provenpendingDA caps at ~1,517Estimated'); + expect(text(swaps)).toContain('Swaps per second, provenpendingDA caps at ~799Estimated'); + expect(a).not.toContain('data-tick'); expect(text(a)).toContain("First limit: DA. Ethereum's blob target fills at ~57 Mgas/s of payments Estimated or 190 Mgas/s of mainnet-like traffic Measured."); - expect(text(a)).toContain('1 Mgas/s ≈ 19 payments/s ≈ swaps/s pending'); expect(text(c)).toContain('200 Mgas/s'); expect(text(c)).not.toContain('proven'); expect(bar(b, 'DA · Payments (B20)')).toContain('~57Estimated· binding'); @@ -91,13 +95,23 @@ describe('without bench data', () => { expect(d).not.toContain('Qualified by'); }); + it('heads with gas per second when the snapshot has no transactions per second', () => { + const snapshot = fixture(); + delete snapshot.usage; + snapshot.ladder.observed.p99 = { ...snapshot.ladder.observed.p99, value: 42.24, tag: 'measured' }; + const { a } = render({}, snapshot); + expect(text(a)).toContain('12.5Mgas/s medianaveraged over 24 h'); + expect(text(a)).toContain('42.2 p99 · Usage data as of 2026-01-02'); + expect(text(a)).not.toMatch(/transactions per second|per day/); + }); + it('uses a qualified snapshot rung and links the runs behind it', () => { const snapshot = fixture(); snapshot.ladder.qualified = { ...snapshot.ladder.qualified, value: 30, tag: 'measured', evidence: [{ label: 'Synthetic run', url: 'https://example.com/scaling/runs/bench/synthetic-1' }] }; const { a, c } = render({}, snapshot); - expect(text(a)).toContain('Proven capacity30Mgas/s · from the snapshot · Synthetic run'); + expect(text(a)).toContain('Proven capacity 30 Mgas/s, from the snapshot · Synthetic run'); expect(a).toContain('href="https://example.com/scaling/runs/bench/synthetic-1"'); - expect(text(a)).toContain('Headroom2.4×'); + expect(text(a)).toContain('12.5 Mgas/s median · proven capacity is 2.4× that'); expect(text(c)).toContain('proven 30'); }); @@ -107,7 +121,7 @@ describe('without bench data', () => { swr.result = { error: new TypeError('Failed to fetch'), data: bench(400, true) }; const { a, c } = render(); expect(text(a)).toContain('VPN'); - expect(text(a)).toContain('Proven capacity—'); + expect(text(cards(a)[0])).toContain('pending'); expect(text(c)).not.toContain('proven'); }); @@ -120,11 +134,10 @@ describe('without bench data', () => { }); describe('with a proven run', () => { - it('heads with proven, used and headroom, names the first limit and the last change', () => { + it('heads with today, the headroom over the median, the first limit and the last change', () => { swr.result = { data: bench(400, true) }; const { a } = render(); - expect(text(a)).toContain('Proven capacity≥ 400Mgas/s · sequencer, validators and proof node all pass on mainnet state'); - expect(text(a)).toContain('Headroom32×'); + expect(text(a)).toContain('5.5M per day · 12.5 Mgas/s median · proven capacity is 32× that · Usage data as of 2026-01-02'); expect(text(a)).toContain('of mainnet-like traffic Measured, below the proven 400.'); expect(text(a)).toContain('↑ from 40 Mgas/s on 2026-01-06 · compare'); expect(a).toContain('href="https://github.com/base/base/compare/aaaa...bbbb"'); @@ -192,7 +205,7 @@ describe('with a proven run', () => { it('binds below the top rate when the proven rate is under every DA ceiling', () => { swr.result = { data: bench(40, false) }; const { a, b } = render(); - expect(text(a)).toContain('Proven capacity40'); + expect(text(a)).toContain('proven capacity is 3.2× that'); expect(text(a)).toContain('First limit: above 40 Mgas/s, where the next rate tested did not pass.'); expect(bar(b, 'Execution (sequencer)')).toMatch(/40· binding$/); }); @@ -228,8 +241,27 @@ describe('with a proven run', () => { expect(text(a)).toContain('fills at 76 Mgas/s of payments Measured'); }); - it('translates to bench payments/s and swaps/s, pending without a qualifying run', () => { - swr.result = { data: bench(400, true, { current: { mix: null, swap: null, payment: { mgas: 200, atTop: false, run: run(200, 'payment') } } }) }; - expect(render().a).toContain('≥ 2,100 payments/s (B20 transfers) · swaps/s (Uniswap v3) pending'); + it('cards payments and swaps per second with today and the DA cap on a 0 to proven bar', () => { + swr.result = { data: bench(400, true, { current: { mix: null, swap: { mgas: 100, atTop: false, run: run(100, 'swap') }, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } } }) }; + const [payments, swaps] = cards(render().a); + expect(payments).toContain('href="/scaling/runs/bench/synthetic-payment-200">≥ 2.1k'); + // Today 64.2 and the DA cap 1,517 tx/s on a 0–2,100 scale. + expect(payments).toMatch(/data-tick="today"[^>]*left:3\.057\d*%/); + expect(payments).toMatch(/data-tick="da"[^>]*left:72\.24\d*%/); + expect(text(payments)).toContain('DA caps at ~1,517Estimated'); + expect(swaps).toContain('>1.1k'); + // The DA cap of ~799 swaps/s sits at 76% of the 1,050 proven. + expect(swaps).toMatch(/data-tick="da"[^>]*left:76\.0\d*%/); + }); + + it('uses the bench DA run for the cap once measured, and omits the today tick without usage data', () => { + const daRun = run(200, 'payment', { result: { seconds: 600, gas_used: 4e10, transactions: 1e6, transaction_bytes: 0, frame_bytes: 8e7, gas_per_second: 0, bytes_per_gas: 0, compression_ratio: 0, blobs_per_l1_slot: 20, blob_fill: 1, blob_target_gas_per_second: 0 }, target: 14, withinTarget: false }); + swr.result = { data: bench(400, true, { current: { mix: null, swap: null, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } }, da: { mix: null, payment: daRun, swap: null } }) }; + const snapshot = fixture(); + delete snapshot.usage; + const [payments] = cards(render({ benchUrl: BENCH }, snapshot).a); + expect(text(payments)).toContain('DA caps at ~1,896Measured'); + expect(payments).not.toContain('data-tick="today"'); + expect(payments).toContain('data-tick="da"'); }); }); diff --git a/app/scaling/fixtures/synthetic.v2.json b/app/scaling/fixtures/synthetic.v2.json index 4ac44f3..0467b15 100644 --- a/app/scaling/fixtures/synthetic.v2.json +++ b/app/scaling/fixtures/synthetic.v2.json @@ -5,7 +5,8 @@ "context": "Synthetic fixture: every number is invented to exercise the schema. Not a measurement of any network.", "windows": { "day": { "label": "24 h", "start": "2026-01-01T00:00:00Z", "end": "2026-01-02T00:00:00Z" }, - "week": { "label": "7 d", "start": "2025-12-26T00:00:00Z", "end": "2026-01-02T00:00:00Z" } + "week": { "label": "7 d", "start": "2025-12-26T00:00:00Z", "end": "2026-01-02T00:00:00Z" }, + "month": { "label": "the last 30 days", "start": "2025-12-03T00:00:00Z", "end": "2026-01-02T00:00:00Z" } }, "ladder": { "observed": { @@ -195,6 +196,14 @@ }, "gasPerSwap": null }, + "usage": { + "transactionsPerSecond": { + "value": 64.2, + "window": "month", + "help": "Invented: synthetic transactions per second averaged over the window.", + "source": { "label": "Synthetic dashboard", "url": "https://example.com/synthetic/tps" } + } + }, "sections": [ { "id": "da", diff --git a/app/scaling/snapshot.mjs b/app/scaling/snapshot.mjs index e827493..35c3198 100644 --- a/app/scaling/snapshot.mjs +++ b/app/scaling/snapshot.mjs @@ -45,8 +45,10 @@ * schemaVersion: 2, network: string, collectedAt: string, context: string, windows: Record, * ladder: Ladder, ceilings: Ceiling[], guardrails: Guardrail[], * translations: { gasPerPayment: Constant | null, gasPerSwap: Constant | null }, + * usage?: { transactionsPerSecond: Usage }, * sections: Section[], work: WorkItem[] * }} Snapshot + * @typedef {{ value: number, window: string, help: string, source?: Link }} Usage */ const STATUSES = ['observed', 'partial', 'unavailable', 'not-measured']; @@ -212,6 +214,16 @@ export function parseSnapshot(data) { windowRef(c, 'window', path); } + // Optional: without it the page heads with gas per second instead of transactions per second. + if (data.usage !== undefined) { + const tps = obj(obj(data, 'usage', 'snapshot'), 'transactionsPerSecond', 'usage'); + const path = 'usage.transactionsPerSecond'; + if (!(Number.isFinite(tps.value) && tps.value >= 0)) fail(`${path}.value`, 'expected a non-negative number'); + if (!Object.hasOwn(windows, tps.window)) fail(`${path}.window`, 'expected a key of windows'); + str(tps, 'help', path); + if (tps.source !== undefined) link(tps.source, `${path}.source`); + } + const metric = (m, path) => { if (!isObj(m)) return fail(path, 'expected object'); str(m, 'label', path); diff --git a/app/scaling/snapshot.test.ts b/app/scaling/snapshot.test.ts index 3fc3a9f..c23c815 100644 --- a/app/scaling/snapshot.test.ts +++ b/app/scaling/snapshot.test.ts @@ -24,6 +24,17 @@ describe('parseSnapshot', () => { expect(result.snapshot.work[1].linear).toBeNull(); }); + it('treats usage as optional, and checks its value, window and help when present', () => { + const { usage, ...withoutUsage } = fixture(); + expect(errorsOf(withoutUsage)).toEqual([]); + const bad = { ...withoutUsage, usage: { transactionsPerSecond: { ...usage.transactionsPerSecond, value: -1, window: 'year', help: '' } } }; + expect(errorsOf(bad)).toEqual([ + 'usage.transactionsPerSecond.value: expected a non-negative number', + 'usage.transactionsPerSecond.window: expected a key of windows', + 'usage.transactionsPerSecond.help: expected non-empty string', + ]); + }); + it('rejects schema version 1 and names the migration', () => { const errors = errorsOf({ ...fixture(), schemaVersion: 1 }); expect(errors).toHaveLength(1); From 6127ed29011f644e47705a798f09856fbabef481 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 10:47:00 -0700 Subject: [PATCH 09/33] feat(scaling): head with proven Mgas/s beside payments and swaps, and translate DA bars to tx/s --- app/scaling/components/Bottlenecks.tsx | 12 ++-- app/scaling/components/Headline.tsx | 76 ++++++++++------------- app/scaling/components/Scorecard.tsx | 2 +- app/scaling/components/scorecard.test.tsx | 64 +++++++++---------- 4 files changed, 70 insertions(+), 84 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index 56e4a67..bba5fab 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -5,7 +5,7 @@ import type { Proven } from '../proven/capacity'; import { EXECUTE_PAYLOAD_P99_MS, PROOF_LAG_P99_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; import { Tag } from './Tag'; -export type Bar = { id: string; label: string; value: number | null; text: string; tag?: DaWorkload['tag'] }; +export type Bar = { id: string; label: string; value: number | null; text: string; tag?: DaWorkload['tag']; perSecond?: number }; const x = (n: number) => `${n < 10 ? n.toFixed(1) : Math.round(n)}×`; @@ -13,8 +13,8 @@ const x = (n: number) => `${n < 10 ? n.toFixed(1) : Math.round(n)}×`; // over budget, their limit is somewhere below it. export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): Bar[] { const da = workloads.map((w): Bar => { - const value = daCapacity(w, BLOB_TARGET).mgasPerSecond; - return { id: `da-${w.payload}`, label: `DA · ${w.label}`, value, text: `${w.tag === 'estimated' ? '~' : ''}${fmt(value)}`, tag: w.tag }; + const { mgasPerSecond: value, txPerSecond } = daCapacity(w, BLOB_TARGET); + return { id: `da-${w.payload}`, label: `DA · ${w.label}`, value, text: `${w.tag === 'estimated' ? '~' : ''}${fmt(value)}`, tag: w.tag, perSecond: txPerSecond }; }); const run = proven?.run; const m = proven?.mgas ?? null; @@ -54,7 +54,7 @@ export function Bottlenecks({ bars, max, p50, limit }: { bars: Bar[]; max: numbe What breaks first? · Mgas/s
      {bars.map((b) => ( -
    • +
    • {b.label}
      {b.value === null ? null : ( @@ -63,14 +63,14 @@ export function Bottlenecks({ bars, max, p50, limit }: { bars: Bar[]; max: numbe {lines.map((l) => (l.value === null ? null :
      ))}
      - {b.text} + {b.text}{b.perSecond === undefined ? null : ` Mgas/s ≈ ${fmt(b.perSecond)}/s`} {b.tag ? : null} {b === binding ? · binding : null}
    • ))}
    -
    +
    {lines.map((l) => (l.value === null ? null : {l.label}))}
    Same scale as the ladder. State joins once a runway is chosen. diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx index 4f7c8af..0b996d2 100644 --- a/app/scaling/components/Headline.tsx +++ b/app/scaling/components/Headline.tsx @@ -16,7 +16,7 @@ const TRAFFIC: Record = { payment: 'payments', swap: 'swaps', const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 1 }); const compact = (v: number) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(v).toLowerCase(); -type Props = Pick & { +type Props = Pick & { bench: BenchState; proven: ProvenMark; workloads: DaWorkload[]; @@ -28,7 +28,7 @@ type Props = Pick & { function benchNote({ configured, data, error }: BenchState, proven: ProvenMark, qualified: Snapshot['ladder']['qualified']): ReactNode { if (!configured) { if (!proven) return null; - return <>Proven capacity {n(proven.value)} Mgas/s, from the snapshot{qualified.evidence?.map((l) => · {l.label})}; + return <>Proven capacity from the snapshot{qualified.evidence?.map((l) => · {l.label})}; } if (error) return 'Could not load bench results. The bench host is reachable only on VPN and must allow this origin via CORS.'; if (!data) return 'Loading bench results…'; @@ -37,55 +37,44 @@ function benchNote({ configured, data, error }: BenchState, proven: ProvenMark, return data.current.mix ? partial || null : `No main run passed in the last 14 days. ${partial}`.trim(); } -function Lead({ ladder, windows, usage, proven, collectedAt }: Pick) { +function Capacity({ ladder, usage, proven, collectedAt }: Pick) { const { p50, p99 } = ladder.observed; const tps = usage?.transactionsPerSecond; const headroom = proven && p50.value ? proven.value / p50.value : null; - const gas = [p50.value !== null && `${n(p50.value)} Mgas/s median`, p99.value !== null && `${n(p99.value)} p99`].filter(Boolean).join(', '); - const details = [ - tps && `${n((tps.value * 86_400) / 1e6)}M per day`, - tps && gas, - !tps && p99.value !== null && `${n(p99.value)} p99`, - headroom !== null && `proven capacity is ${headroom < 10 ? headroom.toFixed(1) : Math.round(headroom)}× that`, + const caption = [ + p50.value !== null && `today ${n(p50.value)} median${tps ? ` (${fmt(tps.value)} tx/s)` : ''}`, + p99.value !== null && `${n(p99.value)} p99`, + headroom !== null && `${headroom < 10 ? headroom.toFixed(1) : Math.round(headroom)}× headroom`, usageAsOf(collectedAt), ].filter(Boolean); - const [value, unit, over, help] = tps - ? [fmt(tps.value), 'transactions per second', windows[tps.window].label, tps.help] - : p50.value !== null ? [n(p50.value), 'Mgas/s median', p50.window ? windows[p50.window].label : null, p50.help] : ['—', 'usage not measured', null, null]; return ( -
    -

    - {value} - {unit} - {over ? averaged over {over} : null} - {help ? {help} : null} -

    - {details.join(' · ')} +
    + Proven capacity + + {proven ? `${proven.atLeast ? '≥ ' : ''}${n(proven.value)}` : '—'} + Mgas/s + + + {caption.join(' · ')} + {tps ? {tps.help} : null} +
    ); } -function Card({ label, proven, workload, todayTps }: { label: string; proven: Proven | null; workload: DaWorkload; todayTps: number | null }) { - const cap = daCapacity(workload, BLOB_TARGET).txPerSecond; - const value = proven?.run.tps; - const pct = (v: number) => `${Math.min(100, (v / value!) * 100)}%`; +function Card({ label, proven, workload }: { label: string; proven: Proven | null; workload: DaWorkload }) { return ( -
    +
    {label} - {value === undefined ? ( - pending + {proven ? ( + + {proven.atTop ? '≥ ' : ''}{compact(proven.run.tps)}/s + ) : ( - {proven!.atTop ? '≥ ' : ''}{compact(value)} + pending )} - {value === undefined ? null : ( -
    -
    - {todayTps === null ? null : } - -
    - )} - - DA caps at ~{fmt(cap)} + + DA caps ~{fmt(daCapacity(workload, BLOB_TARGET).txPerSecond)}
    @@ -118,17 +107,18 @@ function LastChange({ history }: { history: ProvenCapacity['history'] }) { ); } -export function Headline({ ladder, windows, usage, collectedAt, bench, proven, workloads, bars, binding }: Props) { +export function Headline({ ladder, usage, collectedAt, bench, proven, workloads, bars, binding }: Props) { const note = benchNote(bench, proven, ladder.qualified); - const todayTps = usage?.transactionsPerSecond.value ?? null; const workload = (w: Workload) => workloads.find((x) => x.payload === w)!; return (
    How much can Base handle, and how much do we use? - -
    - - +
    + +
    + + +
    {note ? {note} : null} {binding ? ( diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index 3721855..e65e586 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -30,7 +30,7 @@ export function Scorecard({ snapshot, benchUrl, dashboardUrl }: { snapshot: Snap const max = Math.max(mark?.value ?? 0, ladder.configuredLimit.value ?? 0) || Math.max(0, ...values); return ( <> - + diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index ea0bbd4..e0bc297 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -65,6 +65,7 @@ const text = (html: string) => { return out.replaceAll(''', "'"); }; const bar = (b: string, label: string) => text(` li.includes(`>${label}<`)) ?? '>'}`); +// The proven-capacity block, then the payments and swaps cards. const cards = (a: string) => a.split('uppercase tracking-wide').slice(1); const row = (d: string, name: string) => text(` tr.includes(`>${name}<`)) ?? '>'}`); @@ -77,17 +78,16 @@ describe('without bench data', () => { it('fetches nothing without a URL and falls back to the snapshot and DA estimates', () => { const { a, c, b, d } = render({}); expect(swr.key).toBeNull(); - expect(text(a)).toContain('64transactions per secondaveraged over the last 30 days'); - expect(text(a)).toContain('5.5M per day · 12.5 Mgas/s median · Usage data as of 2026-01-02'); - expect(text(a)).not.toMatch(/Proven capacity|Used today|Headroom|proven capacity is|payments\/s/); - const [payments, swaps] = cards(a); - expect(text(payments)).toContain('Payments per second, provenpendingDA caps at ~1,517Estimated'); - expect(text(swaps)).toContain('Swaps per second, provenpendingDA caps at ~799Estimated'); + const [capacity, payments, swaps] = cards(a); + expect(text(capacity)).toContain('Proven capacity—Mgas/stoday 12.5 median (64 tx/s) · Usage data as of 2026-01-02'); + expect(text(a)).not.toMatch(/\dtransactions per second|averaged over the last|per day|headroom|Used today/); + expect(text(payments)).toContain('PaymentspendingDA caps ~1,517Estimated'); + expect(text(swaps)).toContain('SwapspendingDA caps ~799Estimated'); expect(a).not.toContain('data-tick'); expect(text(a)).toContain("First limit: DA. Ethereum's blob target fills at ~57 Mgas/s of payments Estimated or 190 Mgas/s of mainnet-like traffic Measured."); expect(text(c)).toContain('200 Mgas/s'); expect(text(c)).not.toContain('proven'); - expect(bar(b, 'DA · Payments (B20)')).toContain('~57Estimated· binding'); + expect(bar(b, 'DA · Payments (B20)')).toContain('~57 Mgas/s ≈ 1,517/sEstimated· binding'); expect(bar(b, 'Execution (sequencer)')).toContain('no proven run'); expect(bar(b, 'Validators')).toContain('pending'); expect(text(d)).toContain('Measured at the proven rate'); @@ -95,23 +95,22 @@ describe('without bench data', () => { expect(d).not.toContain('Qualified by'); }); - it('heads with gas per second when the snapshot has no transactions per second', () => { + it('drops the tx/s clause when the snapshot has no transactions per second', () => { const snapshot = fixture(); delete snapshot.usage; snapshot.ladder.observed.p99 = { ...snapshot.ladder.observed.p99, value: 42.24, tag: 'measured' }; const { a } = render({}, snapshot); - expect(text(a)).toContain('12.5Mgas/s medianaveraged over 24 h'); - expect(text(a)).toContain('42.2 p99 · Usage data as of 2026-01-02'); - expect(text(a)).not.toMatch(/transactions per second|per day/); + expect(text(a)).toContain('today 12.5 median · 42.2 p99 · Usage data as of 2026-01-02'); + expect(text(a)).not.toContain('tx/s'); }); it('uses a qualified snapshot rung and links the runs behind it', () => { const snapshot = fixture(); snapshot.ladder.qualified = { ...snapshot.ladder.qualified, value: 30, tag: 'measured', evidence: [{ label: 'Synthetic run', url: 'https://example.com/scaling/runs/bench/synthetic-1' }] }; const { a, c } = render({}, snapshot); - expect(text(a)).toContain('Proven capacity 30 Mgas/s, from the snapshot · Synthetic run'); + expect(text(a)).toContain('Proven capacity30Mgas/stoday 12.5 median (64 tx/s) · 2.4× headroom'); + expect(text(a)).toContain('Proven capacity from the snapshot · Synthetic run'); expect(a).toContain('href="https://example.com/scaling/runs/bench/synthetic-1"'); - expect(text(a)).toContain('12.5 Mgas/s median · proven capacity is 2.4× that'); expect(text(c)).toContain('proven 30'); }); @@ -121,7 +120,8 @@ describe('without bench data', () => { swr.result = { error: new TypeError('Failed to fetch'), data: bench(400, true) }; const { a, c } = render(); expect(text(a)).toContain('VPN'); - expect(text(cards(a)[0])).toContain('pending'); + expect(text(cards(a)[0])).toContain('Proven capacity—Mgas/s'); + expect(text(cards(a)[1])).toContain('pending'); expect(text(c)).not.toContain('proven'); }); @@ -137,7 +137,7 @@ describe('with a proven run', () => { it('heads with today, the headroom over the median, the first limit and the last change', () => { swr.result = { data: bench(400, true) }; const { a } = render(); - expect(text(a)).toContain('5.5M per day · 12.5 Mgas/s median · proven capacity is 32× that · Usage data as of 2026-01-02'); + expect(text(cards(a)[0])).toContain('Proven capacity≥ 400Mgas/stoday 12.5 median (64 tx/s) · 32× headroom · Usage data as of 2026-01-02'); expect(text(a)).toContain('of mainnet-like traffic Measured, below the proven 400.'); expect(text(a)).toContain('↑ from 40 Mgas/s on 2026-01-06 · compare'); expect(a).toContain('href="https://github.com/base/base/compare/aaaa...bbbb"'); @@ -179,7 +179,9 @@ describe('with a proven run', () => { expect(bar(b, 'Validators')).toContain('≥ 400 · 7.3× replay headroom'); expect(bar(b, 'Proof node')).toContain('≥ 400 · lag p99 4 blocks'); expect(b.match(/bg-bds-red-60/g)).toHaveLength(1); - expect(bar(b, 'DA · Payments (B20)')).toContain('binding'); + expect(bar(b, 'DA · Payments (B20)')).toContain('~57 Mgas/s ≈ 1,517/sEstimated· binding'); + expect(bar(b, 'DA · Mainnet mix')).toContain('190 Mgas/s ≈ 981/sMeasured'); + expect(bar(b, 'Execution (sequencer)')).not.toContain('/s'); expect(text(b)).toContain('today p50 12.5'); expect(text(b)).toContain('gas limit 200'); expect(text(b)).toContain('State joins once a runway is chosen.'); @@ -205,7 +207,7 @@ describe('with a proven run', () => { it('binds below the top rate when the proven rate is under every DA ceiling', () => { swr.result = { data: bench(40, false) }; const { a, b } = render(); - expect(text(a)).toContain('proven capacity is 3.2× that'); + expect(text(a)).toContain('Proven capacity40Mgas/stoday 12.5 median (64 tx/s) · 3.2× headroom'); expect(text(a)).toContain('First limit: above 40 Mgas/s, where the next rate tested did not pass.'); expect(bar(b, 'Execution (sequencer)')).toMatch(/40· binding$/); }); @@ -236,32 +238,26 @@ describe('with a proven run', () => { it('switches DA bars to measured once payments and swaps have bench DA runs', () => { swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', daResult(8e7, 20, false)), swap: run(200, 'swap', daResult(4e7, 9, true)) } }) }; const { a, b } = render(); - expect(bar(b, 'DA · Payments (B20)')).toContain('76Measured· binding'); - expect(bar(b, 'DA · Swaps')).toContain('152Measured'); + expect(bar(b, 'DA · Payments (B20)')).toContain('76 Mgas/s ≈ 1,896/sMeasured· binding'); + expect(bar(b, 'DA · Swaps')).toContain('152 Mgas/s ≈ 3,793/sMeasured'); expect(text(a)).toContain('fills at 76 Mgas/s of payments Measured'); }); - it('cards payments and swaps per second with today and the DA cap on a 0 to proven bar', () => { + it('cards payments and swaps per second with ≥ only at the top rate, and their DA caps', () => { swr.result = { data: bench(400, true, { current: { mix: null, swap: { mgas: 100, atTop: false, run: run(100, 'swap') }, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } } }) }; - const [payments, swaps] = cards(render().a); - expect(payments).toContain('href="/scaling/runs/bench/synthetic-payment-200">≥ 2.1k'); - // Today 64.2 and the DA cap 1,517 tx/s on a 0–2,100 scale. - expect(payments).toMatch(/data-tick="today"[^>]*left:3\.057\d*%/); - expect(payments).toMatch(/data-tick="da"[^>]*left:72\.24\d*%/); - expect(text(payments)).toContain('DA caps at ~1,517Estimated'); - expect(swaps).toContain('>1.1k'); - // The DA cap of ~799 swaps/s sits at 76% of the 1,050 proven. - expect(swaps).toMatch(/data-tick="da"[^>]*left:76\.0\d*%/); + const [, payments, swaps] = cards(render().a); + expect(payments).toContain('href="/scaling/runs/bench/synthetic-payment-200"'); + expect(text(payments)).toContain('Payments≥ 2.1k/sDA caps ~1,517Estimated'); + expect(text(swaps)).toContain('Swaps1.1k/sDA caps ~799Estimated'); + expect(render().a).not.toContain('data-tick'); }); - it('uses the bench DA run for the cap once measured, and omits the today tick without usage data', () => { + it('uses the bench DA run for the cap once measured', () => { const daRun = run(200, 'payment', { result: { seconds: 600, gas_used: 4e10, transactions: 1e6, transaction_bytes: 0, frame_bytes: 8e7, gas_per_second: 0, bytes_per_gas: 0, compression_ratio: 0, blobs_per_l1_slot: 20, blob_fill: 1, blob_target_gas_per_second: 0 }, target: 14, withinTarget: false }); swr.result = { data: bench(400, true, { current: { mix: null, swap: null, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } }, da: { mix: null, payment: daRun, swap: null } }) }; const snapshot = fixture(); delete snapshot.usage; - const [payments] = cards(render({ benchUrl: BENCH }, snapshot).a); - expect(text(payments)).toContain('DA caps at ~1,896Measured'); - expect(payments).not.toContain('data-tick="today"'); - expect(payments).toContain('data-tick="da"'); + const [, payments] = cards(render({ benchUrl: BENCH }, snapshot).a); + expect(text(payments)).toContain('DA caps ~1,896Measured'); }); }); From 99ae98aa6fed2b8bd47396ca265fea41216446d7 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 11:11:46 -0700 Subject: [PATCH 10/33] feat(scaling): rank bottlenecks, card workloads and compress guardrails on the scorecard --- app/scaling/components/Bottlenecks.tsx | 133 +++++++--- app/scaling/components/Guardrails.tsx | 110 +++++--- app/scaling/components/Headline.tsx | 97 +++---- app/scaling/components/Ladder.tsx | 7 +- app/scaling/components/Progress.tsx | 64 ----- app/scaling/components/RunEvidence.tsx | 28 -- app/scaling/components/Scorecard.tsx | 42 ++- app/scaling/components/Subsystems.tsx | 139 ---------- app/scaling/components/Work.tsx | 49 ++++ app/scaling/components/Workloads.tsx | 58 +++++ app/scaling/components/firstScreen.test.tsx | 57 ++--- app/scaling/components/runBacked.test.tsx | 11 - app/scaling/components/scorecard.test.tsx | 268 +++++++++----------- app/scaling/page.test.tsx | 23 +- app/scaling/page.tsx | 18 +- app/scaling/proven/capacity.test.ts | 4 +- app/scaling/proven/capacity.ts | 4 +- 17 files changed, 494 insertions(+), 618 deletions(-) delete mode 100644 app/scaling/components/Progress.tsx delete mode 100644 app/scaling/components/RunEvidence.tsx delete mode 100644 app/scaling/components/Subsystems.tsx create mode 100644 app/scaling/components/Work.tsx create mode 100644 app/scaling/components/Workloads.tsx diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index bba5fab..c522418 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -1,13 +1,20 @@ +import { Card } from '../../components/ui/Card'; import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; import { BLOB_TARGET, daCapacity, fmt, type DaWorkload } from '../da/workloads'; -import type { Proven } from '../proven/capacity'; +import { runHref, type PipelineStep, type Proven } from '../proven/capacity'; import { EXECUTE_PAYLOAD_P99_MS, PROOF_LAG_P99_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; +import { day } from './ProvenCapacity'; import { Tag } from './Tag'; export type Bar = { id: string; label: string; value: number | null; text: string; tag?: DaWorkload['tag']; perSecond?: number }; +// One row of the ranked table: a bar, or execution, validators and proof node merged when they share a ceiling. +export type Limit = { id: string; label: string; value: number | null; ceiling: string; evidence: string; tag?: DaWorkload['tag']; perSecond?: number; workload?: DaWorkload }; -const x = (n: number) => `${n < 10 ? n.toFixed(1) : Math.round(n)}×`; +export const times = (n: number) => `${n < 10 ? n.toFixed(1) : Math.round(n)}×`; +const TH = 'px-3 py-2 text-left text-[11px] font-[600] uppercase tracking-wide text-bds-gray-60'; +const TD = 'px-3 py-2.5 align-middle'; +const STEP_DOT: Record = { passed: 'bg-bds-green-50', failed: 'bg-bds-red-50', skipped: 'bg-bds-gray-30', 'not-implemented': 'bg-bds-gray-30' }; // Validators and the proof node within budget at the proven rate have a limit of at least that rate; // over budget, their limit is somewhere below it. @@ -28,7 +35,7 @@ export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): !replay ? { id: 'validators', label: 'Validators', value: null, text: 'pending' } : replay.match && replay.blocksPerSecond >= REPLAY_BLOCKS_PER_SECOND - ? { id: 'validators', label: 'Validators', value: m, text: `≥ ${m} · ${x(replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND)} replay headroom` } + ? { id: 'validators', label: 'Validators', value: m, text: `≥ ${m} · ${times(replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND)} replay headroom` } : { id: 'validators', label: 'Validators', value: null, text: `below budget at ${m}` }, !proofs ? { id: 'proof-node', label: 'Proof node', value: null, text: 'pending' } @@ -36,44 +43,102 @@ export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): ]; } -export function bindingBar(bars: Bar[]): Bar | null { - return bars.reduce((low, b) => (b.value !== null && (low === null || b.value < low.value!) ? b : low), null); +function daEvidence(w: DaWorkload): string { + if (w.tag === 'estimated') return 'est. · run pending'; + if (w.bench && w.source?.url === runHref(w.bench.run)) return `measured · ${w.bench.da.result.blobs_per_l1_slot.toFixed(1)} blobs/slot at ${w.bench.run.mgas}`; + return `measured · ${w.source?.label ?? 'source pending'}`; } -type Line = { value: number | null; label: string; className: string }; +// Lowest ceiling first; limits without a ceiling keep their order after the ranked ones. +export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven | null): Limit[] { + const da = bars.filter((b) => b.id.startsWith('da-')).map((b): Limit => { + const w = workloads.find((x) => `da-${x.payload}` === b.id)!; + return { id: b.id, label: b.label, value: b.value, ceiling: `${b.text} ≈ ${fmt(b.perSecond!)}/s`, evidence: daEvidence(w), tag: b.tag, perSecond: b.perSecond, workload: w }; + }); + const core = bars.filter((b) => !b.id.startsWith('da-')); + const m = proven?.mgas ?? null; + const atLeast = (atTop: boolean) => (atTop ? `≥ ${m}` : `${m}`); + const merged: Limit[] = m !== null && core.every((b) => b.value === m) + ? [{ id: 'core', label: 'Execution, validators, proof node', value: m, ceiling: atLeast(proven!.atTop), evidence: `all pass at ${m}` }] + : core.map((b) => ({ + id: b.id, + label: b.label, + value: b.value, + ceiling: b.value === null ? '—' : atLeast(b.id !== 'execution' || proven!.atTop), + evidence: b.value === null ? b.text : b.id === 'execution' && !proven!.atTop ? 'the next rate tested did not pass' : b.text.split(' · ').slice(1).join(' · '), + })); + return [...da, ...merged].toSorted((a, b) => (a.value ?? Infinity) - (b.value ?? Infinity)); +} -export function Bottlenecks({ bars, max, p50, limit }: { bars: Bar[]; max: number; p50: number | null; limit: number | null }) { - const binding = bindingBar(bars); - const pct = (v: number) => `${Math.min(100, (v / max) * 100)}%`; - const lines: Line[] = [ - { value: p50, label: `today p50 ${p50}`, className: 'border-bds-gray-100 dark:border-white' }, - { value: limit, label: `gas limit ${limit}`, className: 'border-bds-orange-60 text-bds-orange-60' }, - ]; +function Evidence({ proven }: { proven: Proven }) { + const { run } = proven; return ( -
    - What breaks first? · Mgas/s -
      - {bars.map((b) => ( -
    • - {b.label} -
      - {b.value === null ? null : ( -
      - )} - {lines.map((l) => (l.value === null ? null :
      ))} -
      - - {b.text}{b.perSecond === undefined ? null : ` Mgas/s ≈ ${fmt(b.perSecond)}/s`} - {b.tag ? : null} - {b === binding ? · binding : null} - +
      + + Proven by {run.outputDir}: {run.mgas} Mgas/s run delivered {(run.gps / 1e6).toFixed(1)} Mgas/s · commit{' '} + {run.commit.slice(0, 7)} · {day(run.createdAt)} + +
        + {run.steps.map((s) => ( +
      • +
      • ))}
      -
      - {lines.map((l) => (l.value === null ? null : {l.label}))} -
      - Same scale as the ladder. State joins once a runway is chosen. +
      + ); +} + +export function Bottlenecks({ limits, max, gasLimit, proven }: { limits: Limit[]; max: number; gasLimit: number | null; proven: Proven | null }) { + const pct = (v: number) => `${Math.min(100, (v / max) * 100)}%`; + const ranked = limits.filter((l) => l.value !== null).length; + const fixed = [ + { label: 'State (full node)', evidence: 'runway pending' }, + { label: 'TEE proving, RPC under sync', evidence: 'not measured yet' }, + ]; + return ( +
      + What breaks first · Mgas/s + + + + + + + + + {limits.map((l, i) => { + const next = i === 0 && l.value !== null; + return ( + + + + + + + + ); + })} + {fixed.map((f) => ( + + + + ))} + +
      #LimitCeilingEvidence
      {i < ranked ? i + 1 : '—'} + {l.label} + {next ? next : null} + + {l.ceiling}{l.tag ? <> : null} + +
      + {l.value === null ? null :
      } + {gasLimit === null ? null : } +
      +
      {l.evidence}
      —{f.label}—{f.evidence}
      +
      + {proven ? : null}
      ); } diff --git a/app/scaling/components/Guardrails.tsx b/app/scaling/components/Guardrails.tsx index 349d291..6001bf0 100644 --- a/app/scaling/components/Guardrails.tsx +++ b/app/scaling/components/Guardrails.tsx @@ -7,7 +7,6 @@ import { Card } from '../../components/ui/Card'; import { Text } from '../../components/ui/Text'; import type { Run } from '../proven/capacity'; import type { Guardrail } from '../snapshot.mjs'; -import { Info } from './Info'; import { useProvenCapacity, usageAsOf } from './ProvenCapacity'; import { Tag } from './Tag'; @@ -15,7 +14,6 @@ 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', 'not-monitored': 'bg-bds-gray-30' }; const STATUS: Record = { ok: 'OK', breached: 'Breached', unknown: 'Unknown', 'not-monitored': 'Not monitored' }; -const rank = (g: Guardrail) => (g.status === 'not-monitored' ? 2 : g.value === null ? 1 : 0); const fmt = (n: number, unit: string) => `${n.toLocaleString('en-US', { maximumFractionDigits: 2 })} ${unit}`.trim(); @@ -64,49 +62,81 @@ export function scorecardGuardrails(snapshot: Guardrail[], run: Run | null | und ]; } +function Table({ rows }: { rows: Guardrail[] }) { + return ( + + + + + + + + + + + + {rows.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} + + + + +
      +
      + ); +} + +// The scorecard shows counts; the guardrails page shows breached and met rows, with the rest collapsed. export function Guardrails({ guardrails, collectedAt, detailed = false, benchUrl }: { guardrails: Guardrail[]; collectedAt: string; detailed?: boolean; benchUrl?: string }) { const bench = useProvenCapacity(benchUrl); const run = benchUrl ? (bench.error ? null : bench.data?.current.mix?.run ?? null) : undefined; + const rows = scorecardGuardrails(guardrails, run); + const breached = rows.filter((g) => g.status === 'breached'); + const met = rows.filter((g) => g.status === 'ok'); + const unmeasured = rows.filter((g) => g.status === 'unknown' || g.status === 'not-monitored'); + const summary = Not measured {unmeasured.length}; + if (!detailed) { + return ( +
      + Guardrails +
      + + Breached {breached.length} + {breached.map((g, i) => {i ? ', ' : ' — '}{g.label} {fmt(g.value!, g.unit)})} + + Met {met.length} +
      + {summary} +
        {unmeasured.map((g) =>
      • {g.label}
      • )}
      +
      + Guardrails → +
      +
      + ); + } return (
      - {detailed ? null : Guardrails →} - - - - - - - - - - - - {scorecardGuardrails(guardrails, run).toSorted((a, b) => rank(a) - rank(b)).map((g) => ( - - - - - - - ))} - -
      GuardrailValueThresholdStatus
      - {detailed ? <>{g.label}

      {g.help}

      : ( - {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} - - - - -
      -
      + +
      + {summary} +
      + {usageAsOf(collectedAt)}; bench rows show their run. Breached SLOs are not attributed to saturation until triaged. ); diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx index 0b996d2..6b7c1ab 100644 --- a/app/scaling/components/Headline.tsx +++ b/app/scaling/components/Headline.tsx @@ -1,30 +1,22 @@ import type { ReactNode } from 'react'; import { Text } from '../../components/ui/Text'; -import { BLOB_TARGET, daCapacity, fmt, type DaWorkload } from '../da/workloads'; -import { runHref, type Proven, type ProvenCapacity, type Workload } from '../proven/capacity'; +import { BLOB_TARGET, fmt } from '../da/workloads'; +import type { ProvenCapacity, Workload } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; -import type { Bar } from './Bottlenecks'; +import type { Limit } from './Bottlenecks'; import { Info } from './Info'; import type { ProvenMark } from './Ladder'; -import { day, usageAsOf } from './ProvenCapacity'; -import { Tag } from './Tag'; +import { day } from './ProvenCapacity'; export type BenchState = { configured: boolean; data?: ProvenCapacity; error?: unknown }; -const TRAFFIC: Record = { payment: 'payments', swap: 'swaps', mix: 'mainnet-like traffic' }; +const TRAFFIC: Record = { payment: 'payments', swap: 'swaps', mix: 'tx' }; const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 1 }); -const compact = (v: number) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(v).toLowerCase(); -type Props = Pick & { - bench: BenchState; - proven: ProvenMark; - workloads: DaWorkload[]; - bars: Bar[]; - binding: Bar | null; -}; +type Props = Pick & { bench: BenchState; proven: ProvenMark; next: Limit | null }; -// Only states other than a loaded bench result need a note; a loaded result shows in the cards. +// Only states other than a loaded bench result need a note. function benchNote({ configured, data, error }: BenchState, proven: ProvenMark, qualified: Snapshot['ladder']['qualified']): ReactNode { if (!configured) { if (!proven) return null; @@ -37,22 +29,21 @@ function benchNote({ configured, data, error }: BenchState, proven: ProvenMark, return data.current.mix ? partial || null : `No main run passed in the last 14 days. ${partial}`.trim(); } -function Capacity({ ladder, usage, proven, collectedAt }: Pick) { +function Capacity({ ladder, usage, proven }: Pick) { const { p50, p99 } = ladder.observed; const tps = usage?.transactionsPerSecond; const headroom = proven && p50.value ? proven.value / p50.value : null; const caption = [ p50.value !== null && `today ${n(p50.value)} median${tps ? ` (${fmt(tps.value)} tx/s)` : ''}`, p99.value !== null && `${n(p99.value)} p99`, - headroom !== null && `${headroom < 10 ? headroom.toFixed(1) : Math.round(headroom)}× headroom`, - usageAsOf(collectedAt), + headroom !== null && `${headroom < 10 ? headroom.toFixed(1) : Math.round(headroom)}×`, ].filter(Boolean); return (
      - Proven capacity + Proven capacity {proven ? `${proven.atLeast ? '≥ ' : ''}${n(proven.value)}` : '—'} - Mgas/s + Mgas/s {caption.join(' · ')} @@ -62,71 +53,43 @@ function Capacity({ ladder, usage, proven, collectedAt }: Pick - {label} - {proven ? ( - - {proven.atTop ? '≥ ' : ''}{compact(proven.run.tps)}/s - - ) : ( - pending - )} - - DA caps ~{fmt(daCapacity(workload, BLOB_TARGET).txPerSecond)} - - +
      + Next bottleneck to solve + {limit.label} at {limit.ceiling.split(' ≈ ')[0]} Mgas/s + {sub}
      ); } -function FirstLimit({ bars, binding, proven }: { bars: Bar[]; binding: Bar; proven: ProvenMark }) { - const below = proven && binding.value! < proven.value ? `, below the proven ${n(proven.value)}` : ''; - if (binding.id.startsWith('da-')) { - const part = (b: Bar) => <>{b.text} Mgas/s of {TRAFFIC[b.id.slice(3) as Workload]} {b.tag ? : null}; - const mix = bars.find((b) => b.id === 'da-mix'); - return ( - <>First limit: DA. Ethereum's blob target fills at {part(binding)}{mix && mix !== binding ? <> or {part(mix)} : null}{below}. - ); - } - return proven?.atLeast - ? <>No limit found up to {n(proven.value)} Mgas/s, the top rate tested. - : <>First limit: above {n(binding.value!)} Mgas/s, where the next rate tested did not pass.; -} - function LastChange({ history }: { history: ProvenCapacity['history'] }) { if (history.length < 2) return null; const [previous, last] = history.slice(-2); const down = last.mgas !== null && previous.mgas !== null && last.mgas < previous.mgas; return ( - - {' '}{down ? '↓' : '↑'} from {previous.mgas === null ? 'none' : `${previous.mgas} Mgas/s`} on {day(last.at)} + + {down ? '↓' : '↑'} from {previous.mgas === null ? 'none' : `${previous.mgas} Mgas/s`} on {day(last.at)} {last.compareUrl ? <> · compare : null} - + ); } -export function Headline({ ladder, usage, collectedAt, bench, proven, workloads, bars, binding }: Props) { +export function Headline({ ladder, usage, bench, proven, next }: Props) { const note = benchNote(bench, proven, ladder.qualified); - const workload = (w: Workload) => workloads.find((x) => x.payload === w)!; return ( -
      - How much can Base handle, and how much do we use? -
      - -
      - - -
      +
      +
      + + {next ? : null}
      + {bench.data ? : null} {note ? {note} : null} - {binding ? ( -

      - - {bench.data ? : null} -

      - ) : null}
      ); } diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx index cd54b49..cfb37e2 100644 --- a/app/scaling/components/Ladder.tsx +++ b/app/scaling/components/Ladder.tsx @@ -6,7 +6,6 @@ import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; import type { Rung, Snapshot } from '../snapshot.mjs'; import { Info } from './Info'; -import { usageAsOf } from './ProvenCapacity'; const BAR_Y = 50; const BAR_H = 24; @@ -16,7 +15,7 @@ const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 1 }) export type ProvenMark = { value: number; atLeast: boolean; help: string } | null; -type Props = Pick & { proven: ProvenMark; max: number; dashboardUrl?: string }; +type Props = Pick & { proven: ProvenMark; max: number }; type Label = { id: string; text: string; help: string; x: number; className: string }; const SHADES = { @@ -29,7 +28,7 @@ const SHADES = { // Width of a label with its help button, so fanned labels can be kept apart. const labelPx = (text: string) => text.length * CHAR_PX + 20; -export function Ladder({ ladder, windows, collectedAt, proven, max, dashboardUrl }: Props) { +export function Ladder({ ladder, windows, proven, max }: Props) { const ref = useRef(null); const [width, setWidth] = useState(960); useLayoutEffect(() => { @@ -125,9 +124,7 @@ export function Ladder({ ladder, windows, collectedAt, proven, max, dashboardUrl {legend.map(([label, shade]) => ( ))} - {usageAsOf(collectedAt)}
      - {dashboardUrl ? Mainnet load, gas limit, DA ceiling → : null}
      ); } diff --git a/app/scaling/components/Progress.tsx b/app/scaling/components/Progress.tsx deleted file mode 100644 index fa37381..0000000 --- a/app/scaling/components/Progress.tsx +++ /dev/null @@ -1,64 +0,0 @@ -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 { RunEvidence } from './RunEvidence'; -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 ( -
      - - {w.title} - -

      Hypothesis: {w.hypothesis}

      -

      Change: {w.change}

      -

      Owner: {w.owner ?? 'none assigned'}

      -
      - - -
      - {l ? ( - - - - - {l.scopeDone}/{l.scopeTotal} - - ) : null} -

      Expected: {w.expected ?? 'Not estimated'}

      - {w.evidence && w.realized ?

      Realized: {w.realized}

      : null} - {w.evidence ? : null} -
      - {[w.evidence, l && { label: 'Linear', url: l.url }, ...w.links].map((link) => link ? ( - {link.label} - ) : null)} -
      -
      - ); -} diff --git a/app/scaling/components/RunEvidence.tsx b/app/scaling/components/RunEvidence.tsx deleted file mode 100644 index 95f7726..0000000 --- a/app/scaling/components/RunEvidence.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import useSWR from 'swr'; - -import { Status } from '../runs/components/parts'; -import { getDataService } from '../runs/lib/dataService'; -import { getTestRunsWithStatus } from '../runs/lib/types'; -import { toUnifiedRun } from '../runs/lib/unified'; -import { RUN_PAGE } from '../snapshot.mjs'; - -export function RunEvidence({ url }: { url: string }) { - const [, id, network, timestamp] = url.match(RUN_PAGE) ?? []; - const { data: metadata } = useSWR(id ? 'scaling-bench-metadata' : null, () => getDataService().getMetadata()); - const { data: result } = useSWR(network ? ['scaling-load-result', network, timestamp] : null, () => getDataService().getLoadTestResult(network, timestamp)); - const runs = id - ? getTestRunsWithStatus({ runs: metadata?.runs.filter((r) => `${r.testConfig.BenchmarkRun}` === id) ?? [] }).map(toUnifiedRun) - : result ? [toUnifiedRun({ network, timestamp, result })] : []; - return ( - - {runs.map((r, i) => ( - - {r.gasPerSecond === null ? '—' : (r.gasPerSecond / 1e6).toFixed(1)} Mgas/s - - - ))} - - ); -} diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index e65e586..2257284 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -2,12 +2,14 @@ import { daWorkloads } from '../da/workloads'; import type { Proven } from '../proven/capacity'; -import type { Snapshot } from '../snapshot.mjs'; -import { bindingBar, bottleneckBars, Bottlenecks } from './Bottlenecks'; +import type { Area, Snapshot } from '../snapshot.mjs'; +import { bottleneckBars, Bottlenecks, rankLimits, type Limit } from './Bottlenecks'; +import { Guardrails } from './Guardrails'; import { Headline } from './Headline'; import { Ladder, type ProvenMark } from './Ladder'; import { day, useProvenCapacity } from './ProvenCapacity'; -import { Subsystems } from './Subsystems'; +import { Work } from './Work'; +import { Workloads } from './Workloads'; function benchMark({ mgas, atTop, run }: Proven): ProvenMark { const top = atTop ? ` ≥ because ${mgas} Mgas/s is the highest rate the bench runs.` : ''; @@ -15,9 +17,23 @@ function benchMark({ mgas, atTop, run }: Proven): ProvenMark { return { value: mgas, atLeast: atTop, help }; } -// Sections A–D share one bench fetch and one scale. Without a bench host, the proven rate falls back to -// the snapshot's qualified rung. -export function Scorecard({ snapshot, benchUrl, dashboardUrl }: { snapshot: Snapshot; benchUrl?: string; dashboardUrl?: string }) { +const at = (t: number) => `${new Date(t).toISOString().slice(0, 16).replace('T', ' ')} UTC`; +const areaOf = (limit: Limit | null): Area | null => (!limit ? null : limit.id.startsWith('da-') ? 'da' : 'execution'); + +function Freshness({ snapshot, latestAt }: { snapshot: Snapshot; latestAt: number | null }) { + const usageWindow = snapshot.usage?.transactionsPerSecond.window ?? snapshot.ladder.observed.p50.window; + const slos = snapshot.guardrails.flatMap((g) => (g.asOf ? [Date.parse(g.asOf)] : [])); + const parts = [ + `Usage to ${day(Date.parse(usageWindow ? snapshot.windows[usageWindow].end : snapshot.collectedAt))}`, + latestAt !== null && `bench ${at(latestAt)}`, + slos.length > 0 && `SLOs ${day(Math.max(...slos))}`, + ].filter(Boolean); + return

      {parts.join(' · ')}

      ; +} + +// Every section shares one bench fetch and the Mgas/s scale. Without a bench host, the proven rate +// falls back to the snapshot's qualified rung. +export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl?: string }) { const bench = useProvenCapacity(benchUrl); const data = bench.error ? undefined : bench.data; const { ladder } = snapshot; @@ -25,15 +41,19 @@ export function Scorecard({ snapshot, benchUrl, dashboardUrl }: { snapshot: Snap const qualified = ladder.qualified; const mark = proven ? benchMark(proven) : !benchUrl && qualified.value !== null ? { value: qualified.value, atLeast: false, help: qualified.help } : null; const workloads = daWorkloads(data?.da); - const bars = bottleneckBars(workloads, proven); + const limits = rankLimits(bottleneckBars(workloads, proven), workloads, proven); + const next = limits[0]?.value != null ? limits[0] : null; const values = [ladder.observed.p50, ladder.observed.p99, ladder.operatingPoint, ladder.configuredLimit].map((r) => r.value ?? 0); const max = Math.max(mark?.value ?? 0, ladder.configuredLimit.value ?? 0) || Math.max(0, ...values); return ( <> - - - - + + + + + + + ); } diff --git a/app/scaling/components/Subsystems.tsx b/app/scaling/components/Subsystems.tsx deleted file mode 100644 index 536c151..0000000 --- a/app/scaling/components/Subsystems.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import Link from 'next/link'; -import type { ReactNode } from 'react'; - -import { Card } from '../../components/ui/Card'; -import { cn } from '../../components/ui/cn'; -import { Text } from '../../components/ui/Text'; -import { BLOB_TARGET } from '../da/workloads'; -import { runHref, type PipelineStep, type Proven } from '../proven/capacity'; -import type { Snapshot } from '../snapshot.mjs'; -import { EXECUTE_PAYLOAD_P99_MS, KEEP_UP_BLOCKS, PROOF_LAG_P99_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; -import { day } from './ProvenCapacity'; -import { Tag } from './Tag'; - -type Tone = 'ok' | 'bad' | 'muted'; -type Row = { name: string; href?: string; measured: ReactNode; budget: ReactNode; headroom: string; status: [string, Tone] }; - -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 TONE: Record = { ok: 'text-bds-green-60', bad: 'text-bds-red-60', muted: 'text-bds-gray-60' }; -const STEP_DOT: Record = { passed: 'bg-bds-green-50', failed: 'bg-bds-red-50', skipped: 'bg-bds-gray-30', 'not-implemented': 'bg-bds-gray-30' }; -const SECONDS_PER_YEAR = 365.25 * 86_400; -const PENDING: [string, Tone] = ['pending', 'muted']; -const x = (v: number) => (!Number.isFinite(v) ? '—' : `${v < 10 ? v.toFixed(1) : Math.round(v)}×`); - -function stepStatus(proven: Proven | null, name: string, passed: [string, Tone]): [string, Tone] { - const status = proven?.run.steps.find((s) => s.name === name)?.status; - return status === 'passed' ? passed : status === 'failed' ? ['fail', 'bad'] : status ? [status, 'muted'] : PENDING; -} - -function rows(proven: Proven | null, { guardrails, ceilings }: Pick): Row[] { - const run = proven?.run; - const { replay, da } = run ?? {}; - const proofs = run?.proofNode?.proofs; - const executePayload = proofs?.methods.debug_executePayload; - const keepUp = guardrails.find((g) => g.id === 'validator-keep-up')?.value ?? null; - const target = da?.target ?? BLOB_TARGET; - const input = (name: string) => ceilings.find((c) => c.area === 'state')?.inputs.find((i) => i.name === name); - const maxSize = input('single_node_max'); - const bytesPerGas = input('state_bytes_per_gas')?.value; - const tbPerYear = proven && typeof bytesPerGas === 'number' ? (bytesPerGas * proven.mgas * 1e6 * SECONDS_PER_YEAR) / 1e12 : null; - return [ - { - name: 'Sequencer', href: '/scaling/execution', - measured: run ? `${(run.gps / 1e6).toFixed(1)} Mgas/s delivered` : 'pending', - budget: '≥ 95% of target', headroom: '—', - status: stepStatus(proven, 'sequencer', ['pass', 'ok']), - }, - { - name: 'Validator replay', - measured: [ - replay && `${replay.blocksPerSecond.toFixed(1)} blocks/s, newPayload p99 ${replay.newPayload.p99Ms} ms`, - keepUp !== null && `production lag ${keepUp} blocks`, - ].filter(Boolean).join('; ') || 'pending', - budget: `≥ ${REPLAY_BLOCKS_PER_SECOND} blocks/s; production lag ≤ ${KEEP_UP_BLOCKS} blocks`, - headroom: replay ? x(replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND) : '—', - status: !replay ? PENDING - : !replay.match ? ['mismatch', 'bad'] - : replay.blocksPerSecond < REPLAY_BLOCKS_PER_SECOND ? ['below budget', 'bad'] - : keepUp !== null && keepUp > KEEP_UP_BLOCKS ? ['production lag over budget', 'bad'] - : ['pass', 'ok'], - }, - { - name: 'Proof node', - measured: proofs ? [`lag p99 ${proofs.headLagBlocks.p99} blocks`, executePayload && `executePayload p99 ${executePayload.latency.p99Ms} ms`].filter(Boolean).join('; ') : 'pending', - budget: `≤ ${PROOF_LAG_P99_BLOCKS} blocks; ≤ ${EXECUTE_PAYLOAD_P99_MS} ms`, - headroom: proofs && executePayload ? `${x(PROOF_LAG_P99_BLOCKS / proofs.headLagBlocks.p99)} / ${x(EXECUTE_PAYLOAD_P99_MS / executePayload.latency.p99Ms)}` : '—', - status: stepStatus(proven, 'proof_node', !proofs || !executePayload ? PENDING - : proofs.headLagBlocks.p99 <= PROOF_LAG_P99_BLOCKS && executePayload.latency.p99Ms <= EXECUTE_PAYLOAD_P99_MS ? ['pass', 'ok'] : ['over budget', 'bad']), - }, - { - name: 'DA (bench mix)', href: '/scaling/da', - measured: da ? `${da.result.blobs_per_l1_slot.toFixed(1)} blobs per L1 slot` : 'pending', - budget: `${target} blobs`, - headroom: da ? x(target / da.result.blobs_per_l1_slot) : '—', - status: !da ? PENDING : da.withinTarget === null ? ['not judged', 'muted'] : da.withinTarget ? ['within target', 'ok'] : ['over target', 'bad'], - }, - { - name: 'State (full node)', href: '/scaling/state', - measured: tbPerYear === null ? 'pending' : <>~{tbPerYear.toFixed(1)} TB/yr at {proven!.mgas} , - budget: maxSize?.value == null ? 'pending' : `${maxSize.value} ${maxSize.unit}`.trim(), - headroom: tbPerYear && maxSize?.unit === 'TB' && typeof maxSize.value === 'number' ? `~${(maxSize.value / tbPerYear).toFixed(1)} yr` : '—', - status: ['runway pending', 'muted'], - }, - ]; -} - -function Evidence({ proven }: { proven: Proven }) { - const { run } = proven; - return ( -
      - - Qualified by {run.outputDir}: {run.mgas} Mgas/s run delivered {(run.gps / 1e6).toFixed(1)} Mgas/s · commit{' '} - {run.commit.slice(0, 7)} · {day(run.createdAt)} - -
        - {run.steps.map((s) => ( -
      • -
      • - ))} -
      -
      - ); -} - -export function Subsystems({ proven, guardrails, ceilings }: Pick & { proven: Proven | null }) { - return ( -
      - Subsystem detail - -
      - - - {['Subsystem', `Measured at ${proven ? `${proven.mgas} Mgas/s` : 'the proven rate'}`, 'Budget', 'Headroom', 'Status'].map((h) => )} - - - - {rows(proven, { guardrails, ceilings }).map((r) => ( - - - - - - - - ))} - - - - - - -
      {h}
      {r.href ? {r.name} : r.name}{r.measured}{r.budget}{r.headroom}{r.status[0]}
      TEE proving, RPC under syncnot measured yet—
      - - {proven ? : null} -
      - ); -} diff --git a/app/scaling/components/Work.tsx b/app/scaling/components/Work.tsx new file mode 100644 index 0000000..b0ad80c --- /dev/null +++ b/app/scaling/components/Work.tsx @@ -0,0 +1,49 @@ +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 type { Area, WorkItem } from '../snapshot.mjs'; +import { Info } from './Info'; + +const LIMIT: Record = { da: 'DA', execution: 'Execution', state: 'State', networking: 'Networking' }; +const TD = 'px-3 py-2.5 align-top'; + +// Work on the binding limit's area comes first; the rest keep the snapshot's order. +export function Work({ work, binding }: { work: WorkItem[]; binding: Area | null }) { + const rows = work.toSorted((a, b) => Number(b.area === binding) - Number(a.area === binding)); + return ( +
      + Work by limit + {rows.length === 0 ? ( + + ) : ( + + + + {rows.map((w) => { + const link = w.linear?.url ?? w.links[0]?.url ?? w.evidence?.url; + return ( + + + + + + ); + })} + +
      {LIMIT[w.area]} + + {link ? {w.title} : w.title} + +

      Hypothesis: {w.hypothesis}

      +

      Change: {w.change}

      +

      Owner: {w.owner ?? 'none assigned'}

      +
      +
      +
      {w.expected ?? 'not estimated'}
      +
      + )} + Expected effects are not summed. +
      + ); +} diff --git a/app/scaling/components/Workloads.tsx b/app/scaling/components/Workloads.tsx new file mode 100644 index 0000000..8d05083 --- /dev/null +++ b/app/scaling/components/Workloads.tsx @@ -0,0 +1,58 @@ +import { Text } from '../../components/ui/Text'; +import { runHref, type ProvenCapacity, type Workload } from '../proven/capacity'; +import { times, type Limit } from './Bottlenecks'; +import { REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; +import { Tag } from './Tag'; + +const CARDS: [Workload, string][] = [['payment', 'Payments (B20)'], ['swap', 'Swaps (Uniswap v3)'], ['mix', 'Mainnet mix']]; +const compact = (v: number) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(v).toLowerCase(); +const TD = 'py-1 text-[12px]'; + +// Each workload's validators and proof node kept up at its proven rate, so DA breaks first when its +// ceiling is below that rate. +export function Workloads({ current, limits }: { current: ProvenCapacity['current'] | undefined; limits: Limit[] }) { + return ( +
      + By workload +
      + {CARDS.map(([w, label]) => { + const proven = current?.[w] ?? null; + const run = proven?.run; + const da = limits.find((l) => l.id === `da-${w}`)!; + const breaksFirst = proven !== null && da.value! < proven.mgas; + const proofs = run?.proofNode?.proofs; + const execute = proofs?.methods.debug_executePayload?.latency.p99Ms; + return ( +
      + {label} + {proven ? ( + + {proven.atTop ? '≥ ' : ''}{compact(proven.run.tps)}{w === 'mix' ? ' tx/s' : '/s'} + {w === 'mix' ? bench mix : null} + + ) : ( + pending + )} + + + + + + + + + + + + + + + +
      DA{da.ceiling.split(' ≈ ')[0]}{breaksFirst ? ' · breaks first' : <> }
      Validators{run?.replay ? times(run.replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND) : '—'}
      Proof node{proofs ? `lag ${proofs.headLagBlocks.p99}${execute === undefined ? '' : ` · ${Math.round(execute)} ms`}` : '—'}
      +
      + ); + })} +
      +
      + ); +} diff --git a/app/scaling/components/firstScreen.test.tsx b/app/scaling/components/firstScreen.test.tsx index 1cec385..1c5a938 100644 --- a/app/scaling/components/firstScreen.test.tsx +++ b/app/scaling/components/firstScreen.test.tsx @@ -8,7 +8,6 @@ import type { Snapshot } from '../snapshot.mjs'; import { AREAS } from './CeilingCards'; import { RunBackedCeilingCard } from './RunBackedCeilingCard'; import { Guardrails, scorecardGuardrails } from './Guardrails'; -import { Progress } from './Progress'; // Synthetic data only: real snapshots are internal and never committed. function fixture(): Snapshot { @@ -38,32 +37,30 @@ describe('CeilingCard', () => { }); describe('Guardrails', () => { - it('shows comparator and unit with the threshold, tags provisional and unset thresholds, and colors only known statuses', () => { - const rows = renderToStaticMarkup().split(' renderToStaticMarkup(); + const labels = (html: string) => html.split(' r.match(/]*>([^<]+) { + const [shown] = detailed().split('Provisional<'); expect(rows[2]).toContain('Synthetic owner'); expect(rows[2]).toContain('bg-bds-green-50'); - expect(rows[3]).toContain('>Unset<'); - expect(rows[3]).toContain('bg-bds-gray-30'); - expect(rows[3]).not.toMatch(/bg-bds-(green|red)/); }); - it('sorts rows with a value first, then unknown, then muted not-monitored rows', () => { - const html = renderToStaticMarkup(); + it('collapses unknown and not-monitored rows under a count, tagging unset thresholds', () => { + const html = detailed(); + const collapsed = html.split('Unset<'); + expect(collapsed).toContain('Not monitored'); + expect(collapsed).not.toMatch(/bg-bds-(green|red)/); expect(html).toContain('Usage data as of 2026-01-02; bench rows show their run.'); - const rows = html.split(' r.match(/]*>(?:]*>)?([^<]+?) ?Unknown'); - expect(rows[5]).toContain('text-bds-gray-60'); - expect(rows[5]).toContain('Not monitored'); - expect(rows.slice(0, 5).join('')).not.toContain('Not monitored'); }); }); @@ -109,21 +106,3 @@ describe('scorecardGuardrails', () => { }); }); -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/components/runBacked.test.tsx b/app/scaling/components/runBacked.test.tsx index 91da4db..0beb179 100644 --- a/app/scaling/components/runBacked.test.tsx +++ b/app/scaling/components/runBacked.test.tsx @@ -11,7 +11,6 @@ import type { Snapshot } from '../snapshot.mjs'; import { AREAS } from './CeilingCards'; import { RunBackedCeilingCard } from './RunBackedCeilingCard'; import { DaWorkloads } from './DaWorkloads'; -import { Progress } from './Progress'; import { RelatedRuns } from './RelatedRuns'; // Synthetic data only: real snapshots and runs are internal and never committed. @@ -71,16 +70,6 @@ it('lists the newest runs of each kind covering an area', () => { expect(links('state')).toEqual([]); }); -it('shows gas/s and status for evidence that points at a run page', () => { - swr.byKey['scaling-bench-metadata'] = { data: metadata }; - swr.byKey['scaling-load-result'] = { data: runFixture.loadTests[0].result }; - const [item] = snapshot().work; - const at = (url: string) => renderToStaticMarkup(); - expect(at('https://example.com/scaling/runs/bench/test-synthetic-1')).toMatch(/42\.0 Mgas\/s.*?success/); - expect(at('https://example.com/scaling/runs/load/sepolia/2026-01-08T00-00-01')).toMatch(/12\.5 Mgas\/s.*?success/); - expect(at('https://example.com/synthetic/doc')).not.toContain('Mgas/s'); -}); - describe('DA from bench runs', () => { const da = (frame_bytes: number, withinTarget: boolean | null): Run['da'] => ({ result: { seconds: 600, gas_used: 4e10, transactions: 1e6, transaction_bytes: 0, frame_bytes, gas_per_second: 1.9e8, bytes_per_gas: 0, compression_ratio: 0, blobs_per_l1_slot: 15.5, blob_fill: 1, blob_target_gas_per_second: 0 }, diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index e0bc297..df13180 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -28,32 +28,33 @@ const daResult = (frame_bytes: number, blobs: number, withinTarget: boolean | nu target: 14, withinTarget, }); -const run = (mgas: number, workload: Run['workload'] = 'mix', da: Run['da'] = null): Run => ({ +const run = (mgas: number, workload: Run['workload'] = 'mix', da: Run['da'] = null, lag = 4): Run => ({ id: `synthetic-${workload}-${mgas}`, outputDir: `synthetic-${mgas}`, workload, mgas, gps: (mgas - 5) * 1e6, tps: mgas * 10.5, passed: true, commit: 'bbbbbbbbbbbbbbbb', createdAt: T0 + 5 * DAY, steps: STEPS, da, replay: { blocksPerSecond: 36.4, newPayload: LATENCY, match: true }, proofNode: { blocksPerSecond: 20, newPayload: LATENCY, match: true, - proofs: { headLagBlocks: { samples: 10, p50: 1, p99: 4, max: 9 }, failedProbes: 0, methods: { debug_executePayload: { errors: 0, latency: { p50Ms: 200, p99Ms: 733, maxMs: 900 } } }, witnessBytes: { samples: 0, p50: 0, p99: 0, max: 0 } }, + proofs: { headLagBlocks: { samples: 10, p50: 1, p99: lag, max: 20 }, failedProbes: 0, methods: { debug_executePayload: { errors: 0, latency: { p50Ms: 200, p99Ms: 733, maxMs: 900 } } }, witnessBytes: { samples: 0, p50: 0, p99: 0, max: 0 } }, }, }); const none = { mix: null, payment: null, swap: null }; const bench = (mgas: number, atTop: boolean, more: Partial = {}): ProvenCapacity => ({ - current: { ...none, mix: { mgas, atTop, run: run(mgas, 'mix', daResult(1, 15.5, false)) } }, + current: { ...none, mix: { mgas, atTop, run: run(mgas) } }, da: none, history: [ { at: T0, mgas: 40, commit: 'aaaaaaaaaaaaaaaa', previousCommit: null, compareUrl: null }, { at: T0 + 5 * DAY, mgas, commit: 'bbbbbbbbbbbbbbbb', previousCommit: 'aaaaaaaaaaaaaaaa', compareUrl: 'https://github.com/base/base/compare/aaaa...bbbb' }, ], + latestAt: T0 + 5 * DAY, unknown: [], ...more, }); const fixture = (): Snapshot => JSON.parse(readFileSync(new URL('../fixtures/synthetic.v2.json', import.meta.url), 'utf8')); -// Sections in page order: A headline, C ladder, B bottleneck bars, D subsystem table. -const render = (props: { benchUrl?: string; dashboardUrl?: string } = { benchUrl: BENCH }, snapshot = fixture()) => { +// In page order: the freshness line, then headline, ladder, ranked limits, workloads, guardrails, work. +const render = (props: { benchUrl?: string } = { benchUrl: BENCH }, snapshot = fixture()) => { const html = renderToStaticMarkup(); - const [, a, c, b, d] = html.split(' { @@ -64,200 +65,157 @@ const text = (html: string) => { } return out.replaceAll(''', "'"); }; -const bar = (b: string, label: string) => text(` li.includes(`>${label}<`)) ?? '>'}`); -// The proven-capacity block, then the payments and swaps cards. -const cards = (a: string) => a.split('uppercase tracking-wide').slice(1); -const row = (d: string, name: string) => text(` tr.includes(`>${name}<`)) ?? '>'}`); +// Table rows after a header row (skip 2) or without one (skip 1). +const rows = (html: string, skip = 2) => html.split(' text(` text(` c.includes(`>${label}<`)) ?? '>'}`.replace(/<[^>]*$/, '')); beforeEach(() => { swr.result = {}; swr.key = undefined; }); -describe('without bench data', () => { - it('fetches nothing without a URL and falls back to the snapshot and DA estimates', () => { - const { a, c, b, d } = render({}); - expect(swr.key).toBeNull(); - const [capacity, payments, swaps] = cards(a); - expect(text(capacity)).toContain('Proven capacity—Mgas/stoday 12.5 median (64 tx/s) · Usage data as of 2026-01-02'); - expect(text(a)).not.toMatch(/\dtransactions per second|averaged over the last|per day|headroom|Used today/); - expect(text(payments)).toContain('PaymentspendingDA caps ~1,517Estimated'); - expect(text(swaps)).toContain('SwapspendingDA caps ~799Estimated'); - expect(a).not.toContain('data-tick'); - expect(text(a)).toContain("First limit: DA. Ethereum's blob target fills at ~57 Mgas/s of payments Estimated or 190 Mgas/s of mainnet-like traffic Measured."); - expect(text(c)).toContain('200 Mgas/s'); - expect(text(c)).not.toContain('proven'); - expect(bar(b, 'DA · Payments (B20)')).toContain('~57 Mgas/s ≈ 1,517/sEstimated· binding'); - expect(bar(b, 'Execution (sequencer)')).toContain('no proven run'); - expect(bar(b, 'Validators')).toContain('pending'); - expect(text(d)).toContain('Measured at the proven rate'); - expect(row(d, 'Validator replay')).toContain('production lag 3 blocks'); - expect(d).not.toContain('Qualified by'); +describe('freshness and headline', () => { + it('dates usage, bench and SLOs, dropping the bench part without bench data', () => { + expect(text(render({}).fresh)).toBe('Usage to 2026-01-02 · SLOs 2026-01-02'); + swr.result = { data: bench(400, true) }; + expect(text(render().fresh)).toBe('Usage to 2026-01-02 · bench 2026-01-06 00:00 UTC · SLOs 2026-01-02'); + expect(render().html).not.toContain('Usage data as of'); }); - it('drops the tx/s clause when the snapshot has no transactions per second', () => { - const snapshot = fixture(); - delete snapshot.usage; - snapshot.ladder.observed.p99 = { ...snapshot.ladder.observed.p99, value: 42.24, tag: 'measured' }; - const { a } = render({}, snapshot); - expect(text(a)).toContain('today 12.5 median · 42.2 p99 · Usage data as of 2026-01-02'); - expect(text(a)).not.toContain('tx/s'); + it('heads with proven capacity, today and headroom, and the next bottleneck with its last change', () => { + swr.result = { data: bench(400, true) }; + const { head } = render(); + expect(text(head)).toContain('Proven capacity≥ 400Mgas/stoday 12.5 median (64 tx/s) · 32×'); + expect(text(head)).toContain('Next bottleneck to solveDA · Payments (B20) at ~57 Mgas/s≈ 1,517 payments/s'); + expect(text(head)).toContain('↑ from 40 Mgas/s on 2026-01-06 · compare'); + expect(text(head)).not.toMatch(/First limit|Payments per second/); }); - it('uses a qualified snapshot rung and links the runs behind it', () => { - const snapshot = fixture(); - snapshot.ladder.qualified = { ...snapshot.ladder.qualified, value: 30, tag: 'measured', evidence: [{ label: 'Synthetic run', url: 'https://example.com/scaling/runs/bench/synthetic-1' }] }; - const { a, c } = render({}, snapshot); - expect(text(a)).toContain('Proven capacity30Mgas/stoday 12.5 median (64 tx/s) · 2.4× headroom'); - expect(text(a)).toContain('Proven capacity from the snapshot · Synthetic run'); - expect(a).toContain('href="https://example.com/scaling/runs/bench/synthetic-1"'); - expect(text(c)).toContain('proven 30'); + it('adds blobs per slot to the callout once the binding DA workload has a bench run', () => { + swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', daResult(8e7, 20, false)), swap: null } }) }; + expect(text(render().head)).toContain('DA · Payments (B20) at 76 Mgas/s≈ 1,896 payments/s · 20.0 blobs/slot vs 14 at 200'); }); - it('says so while loading and after a failed fetch, ignoring stale data', () => { - expect(text(render().a)).toContain('Loading bench results…'); - expect(swr.key).toEqual(['scaling-proven-capacity', BENCH]); + it('falls back to the snapshot without a bench host, and says so while loading or failed', () => { + const snapshot = fixture(); + delete snapshot.usage; + snapshot.ladder.qualified = { ...snapshot.ladder.qualified, value: 30, tag: 'measured', evidence: [{ label: 'Synthetic run', url: 'https://example.com/scaling/runs/bench/synthetic-1' }] }; + const head = text(render({}, snapshot).head); + expect(head).toContain('Proven capacity30Mgas/stoday 12.5 median · 2.4×'); + expect(head).toContain('Proven capacity from the snapshot · Synthetic run'); + expect(text(render().head)).toContain('Loading bench results…'); swr.result = { error: new TypeError('Failed to fetch'), data: bench(400, true) }; - const { a, c } = render(); - expect(text(a)).toContain('VPN'); - expect(text(cards(a)[0])).toContain('Proven capacity—Mgas/s'); - expect(text(cards(a)[1])).toContain('pending'); - expect(text(c)).not.toContain('proven'); + expect(text(render().head)).toMatch(/Proven capacity—Mgas\/s.*VPN/); }); - it('shows when no run counts, and warns when run files failed to load', () => { - swr.result = { data: { current: none, da: none, history: [], unknown: [] } }; - expect(text(render().a)).toContain('No main run passed in the last 14 days.'); - swr.result = { data: { current: none, da: none, history: [], unknown: ['r1', 'r2'] } }; - expect(text(render().a)).toContain('2 run(s) could not be loaded'); + it('names the merged core limit when the proven rate is below every DA ceiling', () => { + swr.result = { data: bench(40, false) }; + expect(text(render().head)).toContain('Execution, validators, proof node at 40 Mgas/sthe next rate tested did not pass'); }); }); -describe('with a proven run', () => { - it('heads with today, the headroom over the median, the first limit and the last change', () => { - swr.result = { data: bench(400, true) }; - const { a } = render(); - expect(text(cards(a)[0])).toContain('Proven capacity≥ 400Mgas/stoday 12.5 median (64 tx/s) · 32× headroom · Usage data as of 2026-01-02'); - expect(text(a)).toContain('of mainnet-like traffic Measured, below the proven 400.'); - expect(text(a)).toContain('↑ from 40 Mgas/s on 2026-01-06 · compare'); - expect(a).toContain('href="https://github.com/base/base/compare/aaaa...bbbb"'); - }); - +describe('ladder', () => { it('shades the used band in two tones at p50 and p99, notches the fee target, and marks the gas limit and proven rate', () => { swr.result = { data: bench(400, true) }; const snapshot = fixture(); snapshot.ladder.observed.p99 = { ...snapshot.ladder.observed.p99, value: 42.24, tag: 'measured' }; - const { c } = render({ benchUrl: BENCH, dashboardUrl: 'https://dashboards.example.com/scaling' }, snapshot); - // 12.5 / 400 and 42.24 / 400 of the axis. - expect(c).toMatch(/data-band="p50" width="3.125%"/); - expect(c).toMatch(/data-band="p99" width="10.56%"/); - expect(c).toContain('data-notch="fee"'); - for (const label of ['p50 12.5', 'p99 42.2', 'fee target 40', 'gas limit 200', 'proven ≥ 400', 'allowed by policy', 'proven, not yet allowed', '400 Mgas/s']) expect(text(c)).toContain(label); - expect(text(c)).toContain('up to medianmedian to p99allowed by policyproven, not yet allowedUsage data as of 2026-01-02'); - expect(text(c)).not.toMatch(/DA ·|Ceiling|payments|42\.24/); - expect(c).toContain('400 Mgas/s is the highest rate the bench runs'); - expect(c).toContain('href="https://dashboards.example.com/scaling"'); + const { ladder } = render({ benchUrl: BENCH }, snapshot); + expect(ladder).toMatch(/data-band="p50" width="3.125%"/); + expect(ladder).toMatch(/data-band="p99" width="10.56%"/); + expect(ladder).toContain('data-notch="fee"'); + for (const label of ['p50 12.5', 'p99 42.2', 'fee target 40', 'gas limit 200', 'proven ≥ 400', '400 Mgas/s']) expect(text(ladder)).toContain(label); + expect(text(ladder)).toContain('up to medianmedian to p99allowed by policyproven, not yet allowed'); + expect(text(ladder)).not.toMatch(/DA ·|42\.24|Usage data/); }); it('shades only to p50 without a p99, and draws no used band without either', () => { - const { c } = render({}); - expect(c).toMatch(/data-band="p50" width="6.25%"/); - expect(c).not.toContain('data-band="p99"'); - expect(text(c)).toContain('p50 12.5'); - expect(text(c)).not.toMatch(/p99|median to p99/); + const { ladder } = render({}); + expect(ladder).toMatch(/data-band="p50" width="6.25%"/); + expect(ladder).not.toContain('data-band="p99"'); const snapshot = fixture(); snapshot.ladder.observed.p50 = { ...snapshot.ladder.observed.p50, value: null, tag: 'not-measured' }; - const empty = render({}, snapshot).c; - expect(empty).not.toContain('data-band'); - expect(text(empty)).not.toMatch(/p50|up to median/); + expect(render({}, snapshot).ladder).not.toContain('data-band'); }); +}); - it('bars each subsystem on the same scale and highlights the lowest', () => { +describe('what breaks first', () => { + it('ranks DA estimates first without bench data and leaves unproven limits unranked', () => { + expect(rows(render({}).ranked)).toEqual([ + '1DA · Payments (B20)next~57 ≈ 1,517/s Estimatedest. · run pending', + '2DA · Swaps~115 ≈ 799/s Estimatedest. · run pending', + '3DA · Mainnet mix190 ≈ 981/s Measuredmeasured · base-bench da, mainnet 2 h', + '—Execution (sequencer)—no proven run', + '—Validators—pending', + '—Proof node—pending', + '—State (full node)—runway pending', + '—TEE proving, RPC under sync—not measured yet', + ]); + }); + + it('merges execution, validators and proof node when they share the proven ceiling, with the run evidence', () => { swr.result = { data: bench(400, true) }; - const { b } = render(); - expect(bar(b, 'Execution (sequencer)')).toContain('≥ 400 · top rate tested'); - expect(bar(b, 'Validators')).toContain('≥ 400 · 7.3× replay headroom'); - expect(bar(b, 'Proof node')).toContain('≥ 400 · lag p99 4 blocks'); - expect(b.match(/bg-bds-red-60/g)).toHaveLength(1); - expect(bar(b, 'DA · Payments (B20)')).toContain('~57 Mgas/s ≈ 1,517/sEstimated· binding'); - expect(bar(b, 'DA · Mainnet mix')).toContain('190 Mgas/s ≈ 981/sMeasured'); - expect(bar(b, 'Execution (sequencer)')).not.toContain('/s'); - expect(text(b)).toContain('today p50 12.5'); - expect(text(b)).toContain('gas limit 200'); - expect(text(b)).toContain('State joins once a runway is chosen.'); + const { ranked } = render(); + expect(rows(ranked)[3]).toBe('4Execution, validators, proof node≥ 400all pass at 400'); + expect(ranked.match(/>next { + it('splits them when the proof node is over budget, leaving it unranked', () => { const over = bench(400, true); - const proven = over.current.mix!; - proven.run.proofNode!.proofs!.headLagBlocks.p99 = 11; - over.current.mix = proven; + over.current.mix = { mgas: 400, atTop: true, run: run(400, 'mix', null, 11) }; swr.result = { data: over }; - const snapshot = fixture(); - snapshot.guardrails = snapshot.guardrails.map((g) => (g.id === 'validator-keep-up' ? { ...g, value: 4, status: 'breached' } : g)); - const { b, d } = render({ benchUrl: BENCH }, snapshot); - expect(row(d, 'Proof node')).toMatch(/0\.9× \/ 1\.4×over budget$/); - expect(row(d, 'Validator replay')).toMatch(/production lag over budget$/); - expect(bar(b, 'Proof node')).toContain('over budget at 400 · lag p99 11 blocks'); - proven.run.proofNode!.proofs!.headLagBlocks.p99 = 4; - proven.run.proofNode!.proofs!.methods.debug_executePayload.latency.p99Ms = 1200; - expect(row(render().d, 'Proof node')).toMatch(/over budget$/); + expect(rows(render().ranked).slice(3, 6)).toEqual([ + '4Execution (sequencer)≥ 400top rate tested', + '5Validators≥ 4007.3× replay headroom', + '—Proof node—over budget at 400 · lag p99 11 blocks', + ]); }); - it('binds below the top rate when the proven rate is under every DA ceiling', () => { + it('marks the core limit next when it is the lowest ceiling', () => { swr.result = { data: bench(40, false) }; - const { a, b } = render(); - expect(text(a)).toContain('Proven capacity40Mgas/stoday 12.5 median (64 tx/s) · 3.2× headroom'); - expect(text(a)).toContain('First limit: above 40 Mgas/s, where the next rate tested did not pass.'); - expect(bar(b, 'Execution (sequencer)')).toMatch(/40· binding$/); + expect(rows(render().ranked)[0]).toBe('1Execution, validators, proof nodenext40all pass at 40'); }); - it('lists measured values, budgets, headroom and status per subsystem, with the run evidence', () => { - swr.result = { data: bench(400, true) }; - const { d } = render(); - expect(text(d)).toContain('Measured at 400 Mgas/s'); - expect(row(d, 'Sequencer')).toBe('Sequencer395.0 Mgas/s delivered≥ 95% of target—pass'); - expect(row(d, 'Validator replay')).toBe('Validator replay36.4 blocks/s, newPayload p99 60 ms; production lag 3 blocks≥ 5 blocks/s; production lag ≤ 3 blocks7.3×pass'); - expect(row(d, 'Proof node')).toBe('Proof nodelag p99 4 blocks; executePayload p99 733 ms≤ 10 blocks; ≤ 1000 ms2.5× / 1.4×pass'); - expect(row(d, 'DA (bench mix)')).toBe('DA (bench mix)15.5 blobs per L1 slot14 blobs0.9×over target'); - expect(row(d, 'State (full node)')).toBe('State (full node)pending45 TB—runway pending'); - expect(text(d)).toContain('TEE proving, RPC under syncnot measured yet'); - for (const href of ['/scaling/execution', '/scaling/da', '/scaling/state']) expect(d).toContain(`href="${href}"`); - expect(text(d)).toContain('Qualified by synthetic-400: 400 Mgas/s run delivered 395.0 Mgas/s'); - expect(d).toMatch(/bg-bds-green-50"><\/span>validator replay passed { + swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', daResult(8e7, 20, false)), swap: null } }) }; + expect(rows(render().ranked)[0]).toBe('1DA · Payments (B20)next76 ≈ 1,896/s Measuredmeasured · 20.0 blobs/slot at 200'); }); +}); - it('estimates state growth at the proven rate once bytes per gas is known', () => { - swr.result = { data: bench(400, true) }; - const snapshot = fixture(); - const state = snapshot.ceilings.find((c) => c.area === 'state')!; - state.inputs = state.inputs.map((i) => (i.name === 'state_bytes_per_gas' ? { ...i, value: 0.001, status: 'observed' } : i)); - expect(row(render({ benchUrl: BENCH }, snapshot).d, 'State (full node)')).toBe('State (full node)~12.6 TB/yr at 400 Estimated45 TB~3.6 yrrunway pending'); +describe('by workload', () => { + it('cards each workload with its proven rate, DA ceiling, replay headroom and proof-node latency', () => { + swr.result = { data: bench(400, true, { current: { mix: { mgas: 400, atTop: true, run: run(400) }, swap: null, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } } }) }; + const { workloads } = render(); + expect(card(workloads, 'Payments (B20)')).toBe('Payments (B20)≥ 2.1k/sDA~57 · breaks firstValidators7.3×Proof nodelag 4 · 733 ms'); + expect(card(workloads, 'Swaps (Uniswap v3)')).toBe('Swaps (Uniswap v3)pendingDA~115 EstimatedValidators—Proof node—'); + expect(card(workloads, 'Mainnet mix')).toBe('Mainnet mix≥ 4.2k tx/sbench mixDA190 · breaks firstValidators7.3×Proof nodelag 4 · 733 ms'); }); - it('switches DA bars to measured once payments and swaps have bench DA runs', () => { - swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', daResult(8e7, 20, false)), swap: run(200, 'swap', daResult(4e7, 9, true)) } }) }; - const { a, b } = render(); - expect(bar(b, 'DA · Payments (B20)')).toContain('76 Mgas/s ≈ 1,896/sMeasured· binding'); - expect(bar(b, 'DA · Swaps')).toContain('152 Mgas/s ≈ 3,793/sMeasured'); - expect(text(a)).toContain('fills at 76 Mgas/s of payments Measured'); + it('keeps the DA tag when the ceiling is above the proven rate', () => { + swr.result = { data: bench(40, false, { current: { mix: null, swap: null, payment: { mgas: 40, atTop: false, run: run(40, 'payment') } } }) }; + expect(card(render().workloads, 'Payments (B20)')).toContain('420/sDA~57 Estimated'); }); +}); - it('cards payments and swaps per second with ≥ only at the top rate, and their DA caps', () => { - swr.result = { data: bench(400, true, { current: { mix: null, swap: { mgas: 100, atTop: false, run: run(100, 'swap') }, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } } }) }; - const [, payments, swaps] = cards(render().a); - expect(payments).toContain('href="/scaling/runs/bench/synthetic-payment-200"'); - expect(text(payments)).toContain('Payments≥ 2.1k/sDA caps ~1,517Estimated'); - expect(text(swaps)).toContain('Swaps1.1k/sDA caps ~799Estimated'); - expect(render().a).not.toContain('data-tick'); +describe('guardrails and work', () => { + it('compresses guardrails to breached, met and not-measured counts with a link', () => { + const { guardrails } = render({}); + expect(text(guardrails)).toContain('Breached 1 — RPC availability 99.5 %Met 2Not measured 3'); + expect(text(guardrails)).toContain('RPC p99 latencyPropagation p99State growth'); + expect(guardrails).toContain('href="/scaling/guardrails"'); + swr.result = { data: bench(400, true) }; + expect(text(render().guardrails)).toContain('Met 5'); }); - it('uses the bench DA run for the cap once measured', () => { - const daRun = run(200, 'payment', { result: { seconds: 600, gas_used: 4e10, transactions: 1e6, transaction_bytes: 0, frame_bytes: 8e7, gas_per_second: 0, bytes_per_gas: 0, compression_ratio: 0, blobs_per_l1_slot: 20, blob_fill: 1, blob_target_gas_per_second: 0 }, target: 14, withinTarget: false }); - swr.result = { data: bench(400, true, { current: { mix: null, swap: null, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } }, da: { mix: null, payment: daRun, swap: null } }) }; - const snapshot = fixture(); - delete snapshot.usage; - const [, payments] = cards(render({ benchUrl: BENCH }, snapshot).a); - expect(text(payments)).toContain('DA caps ~1,896Measured'); + it('lists work by limit with the binding limit first', () => { + expect(rows(render({}).work, 1).map((r) => r.slice(0, 12))).toEqual(['DASynthetic ', 'ExecutionSyn']); + swr.result = { data: bench(40, false) }; + const work = render().work; + expect(rows(work, 1).map((r) => r.slice(0, 12))).toEqual(['ExecutionSyn', 'DASynthetic ']); + expect(text(work)).toContain('not estimated'); + expect(text(work)).toContain('Expected effects are not summed.'); }); }); diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 68f794a..3b4e5ee 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -16,28 +16,27 @@ afterEach(() => vi.unstubAllEnvs()); const area = async (id: string) => renderToStaticMarkup(await ScalingAreaPage({ params: Promise.resolve({ area: id }) })); -it('links the first screen to four detail pages instead of listing metric rows', async () => { +it('ends the first screen with DA and guardrails links, and no benchmark links', async () => { const html = renderToStaticMarkup(await ScalingPage()); - for (const page of ['da', 'execution', 'state', 'guardrails']) expect(html).toContain(`href="/scaling/${page}"`); - expect(html).not.toContain('href="/scaling/networking"'); + expect(html).toContain('DA by workloadGuardrails'); + expect(html).not.toMatch(/href="\/benchmark|href="\/scaling\/networking"/); expect(html).not.toContain('Blob utilization'); expect(html).not.toContain('href="/scaling/runs"'); expect(html).toMatch(/]*>synthetic-testnet capacity<\/h1>/); expect(html).not.toMatch(/As of|manually refreshed|Synthetic fixture: every number/); }); -it('orders the first screen headline, ladder, bottleneck bars, subsystem table, guardrails, progress', async () => { +it('orders the first screen: freshness, headline, ladder, ranked limits, workloads, guardrails, work, links', async () => { const html = renderToStaticMarkup(await ScalingPage()); - const order = ['How much can Base handle', 'How close are we, and to what?', 'What breaks first?', 'Subsystem detail', 'Guardrails →', 'Progress'].map((h) => html.indexOf(h)); + const order = ['Usage to 2026-01-02', 'Proven capacity', 'How close are we, and to what?', 'What breaks first', 'By workload', 'Guardrails', 'Work by limit', 'DA by workload'].map((h) => html.indexOf(h)); expect(order.every((i, k) => i > (order[k - 1] ?? -1))).toBe(true); - expect(html).not.toContain('Ceilings by bottleneck'); + expect(html).not.toMatch(/Subsystem detail|Progress<|Ceilings by bottleneck|Usage data as of/); }); -it('labels snapshot usage with its date instead of a stale banner, on every page', async () => { +it('dates snapshot data without a stale banner', async () => { // The fixture is far older than any staleness window. - const pages = [await ScalingPage(), await ScalingGuardrailsPage()].map((p) => renderToStaticMarkup(p)); - for (const html of pages) expect(html).toContain('Usage data as of 2026-01-02'); - for (const html of [...pages, await area('da')]) expect(html).not.toContain('Stale'); + expect(renderToStaticMarkup(await ScalingGuardrailsPage())).toContain('Usage data as of 2026-01-02'); + for (const html of [renderToStaticMarkup(await ScalingPage()), renderToStaticMarkup(await ScalingGuardrailsPage()), await area('da')]) expect(html).not.toContain('Stale'); }); it('shows every metric row on its detail page', async () => { @@ -73,7 +72,7 @@ it('404s an unknown area and networking, which has no ceiling', async () => { for (const id of ['bogus', 'networking']) await expect(area(id)).rejects.toMatchObject({ digest: 'NEXT_HTTP_ERROR_FALLBACK;404' }); }); -it('passes the runtime bench and dashboard URLs to the ladder', async () => { +it('passes the runtime bench and dashboard URLs to the scorecard and footer', async () => { expect(renderToStaticMarkup(await ScalingPage())).not.toContain('Loading bench results'); vi.stubEnv('SCALING_BENCH_BASE_URL', 'https://bench.example.com'); vi.stubEnv('SCALING_DATADOG_DASHBOARD_URL', 'https://dashboards.example.com/scaling'); @@ -90,7 +89,7 @@ it('adds bench guardrail rows when a bench host is set, unknown until its result expect(renderToStaticMarkup(await ScalingGuardrailsPage())).toContain('No qualifying mainnet-mix bench run is loaded.'); }); -it('links the first-screen guardrail strip to the guardrails page', async () => { +it('links the first-screen guardrail summary to the guardrails page', async () => { expect(renderToStaticMarkup(await ScalingPage())).toContain('Guardrails →'); }); diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index 1d5aeeb..3e5589f 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -2,8 +2,6 @@ import Link from 'next/link'; import { Text } from '../components/ui/Text'; -import { Guardrails } from './components/Guardrails'; -import { Progress } from './components/Progress'; import { Scorecard } from './components/Scorecard'; import { Header, loadSnapshot, Unavailable } from './loadSnapshot'; @@ -15,19 +13,17 @@ export default async function ScalingPage() { if (loaded.kind !== 'ok') return ; const { snapshot } = loaded; - const benchUrl = process.env.SCALING_BENCH_BASE_URL; + const dashboardUrl = process.env.SCALING_DATADOG_DASHBOARD_URL; return (
      - - - - - - + - Experiment results: Benchmarks ·{' '} - Load tests + + DA by workload + Guardrails + {dashboardUrl ? Mainnet load, gas limit, DA ceiling : null} +
      ); diff --git a/app/scaling/proven/capacity.test.ts b/app/scaling/proven/capacity.test.ts index 3933458..b92e600 100644 --- a/app/scaling/proven/capacity.test.ts +++ b/app/scaling/proven/capacity.test.ts @@ -107,7 +107,7 @@ it('counts only pipeline runs built from main, with their steps, replays and DA }); it('keeps each payload separate, with the latest DA run per payload', async () => { - const { current, da, history } = await load([ + const { current, da, history, latestAt } = await load([ { dir: 'pay-mix-400', mgas: 400, daysAgo: 3 }, { dir: 'pay-b20-200', mgas: 200, daysAgo: 3, workload: 'payment' }, { dir: 'pay-b20-300', mgas: 300, daysAgo: 2, workload: 'payment', delivered: 0.5 }, @@ -121,6 +121,8 @@ it('keeps each payload separate, with the latest DA run per payload', async () = expect(da.payment?.outputDir).toBe('pay-b20-300'); expect(da.swap).toBeNull(); expect(history.map((s) => s.mgas)).toEqual([400]); + // The newest counting run, here one without a DA result. + expect(latestAt).toBe(NOW - DAY); }); it('fails a run whose pipeline step failed', async () => { diff --git a/app/scaling/proven/capacity.ts b/app/scaling/proven/capacity.ts index 48c231a..d6a16d3 100644 --- a/app/scaling/proven/capacity.ts +++ b/app/scaling/proven/capacity.ts @@ -64,12 +64,13 @@ export type Step = { }; // `history` is the mainnet mix's; `da` is the latest counting run per workload in the window whose DA -// step passed. Replay, proof-node and DA results are loaded only for the runs in `current` and `da`: +// step passed; `latestAt` is when the newest counting run was created. Replay, proof-node and DA results are loaded only for the runs in `current` and `da`: // replay files list every block, and every other run would download them for nothing. export type ProvenCapacity = { current: Record; da: Record; history: Step[]; + latestAt: number | null; unknown: string[]; }; @@ -127,6 +128,7 @@ export async function loadProvenCapacity( current: Object.fromEntries(WORKLOADS.map((w) => [w, provenAt(of(full, w), now)])) as ProvenCapacity['current'], da: Object.fromEntries(WORKLOADS.map((w) => [w, daRun(w)])) as ProvenCapacity['da'], history: history(of(full, 'mix'), now), + latestAt: runs.length ? Math.max(...runs.map((r) => r.createdAt)) : null, unknown, }; } From b9eda2ee33bdd5608f9986c7fbb67451360aab32 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 11:25:18 -0700 Subject: [PATCH 11/33] fix(scaling): scroll the ranked limits table on narrow screens instead of wrapping --- app/scaling/components/Bottlenecks.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index c522418..e6a3060 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -101,7 +101,7 @@ export function Bottlenecks({ limits, max, gasLimit, proven }: { limits: Limit[]
      What breaks first · Mgas/s - +
      @@ -126,7 +126,7 @@ export function Bottlenecks({ limits, max, gasLimit, proven }: { limits: Limit[] {gasLimit === null ? null : } - + ); })} From 023d89815fb85a7561436328991a417ca7f07fa0 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 14:27:56 -0700 Subject: [PATCH 12/33] feat(scaling): count successful tx/s, show measured swaps DA and disk growth from bench runs --- app/scaling/components/Bottlenecks.tsx | 44 +++++++++++++++-- app/scaling/components/Scorecard.tsx | 4 +- app/scaling/components/Workloads.tsx | 28 +++++++++-- app/scaling/components/firstScreen.test.tsx | 2 +- app/scaling/components/runBacked.test.tsx | 2 +- app/scaling/components/scorecard.test.tsx | 55 +++++++++++++++++++-- app/scaling/da/workloads.test.ts | 13 ++++- app/scaling/da/workloads.ts | 8 +-- app/scaling/proven/capacity.test.ts | 18 +++++-- app/scaling/proven/capacity.ts | 27 ++++++++-- 10 files changed, 172 insertions(+), 29 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index e6a3060..5048e16 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -2,8 +2,9 @@ import { Card } from '../../components/ui/Card'; import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; import { BLOB_TARGET, daCapacity, fmt, type DaWorkload } from '../da/workloads'; -import { runHref, type PipelineStep, type Proven } from '../proven/capacity'; +import { runHref, type PipelineStep, type Proven, type ProvenCapacity, type Run, type Storage, type Workload } from '../proven/capacity'; import { EXECUTE_PAYLOAD_P99_MS, PROOF_LAG_P99_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; +import { Info } from './Info'; import { day } from './ProvenCapacity'; import { Tag } from './Tag'; @@ -70,6 +71,41 @@ export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven return [...da, ...merged].toSorted((a, b) => (a.value ?? Infinity) - (b.value ?? Infinity)); } +export type StorageSource = { storage: Storage; run: Run }; + +export const bytes = (n: number) => (n >= 1e12 ? `${(n / 1e12).toFixed(1)} TB` : `${(n / 1e9).toFixed(1)} GB`); +export const growth = (archive: Storage['archive']) => + `${archive.growthBound === 'lower' ? '≥' : archive.growthBound === 'upper' ? '≤' : '~'} ${bytes(archive.growthBytesPerYear)}/year`; +const WORKLOAD: Record = { mix: 'mainnet mix', payment: 'payments', swap: 'swaps' }; + +// The mix run's storage data when present, else the newest payments or swaps run that has it. +export function pickStorage(data: ProvenCapacity | undefined): StorageSource | null { + if (!data) return null; + const { current, da } = data; + const others = [current.payment?.run, current.swap?.run, da.payment, da.swap].filter((r) => r?.storage).toSorted((a, b) => b!.createdAt - a!.createdAt); + const run = [current.mix?.run, da.mix].find((r) => r?.storage) ?? others[0]; + return run ? { storage: run.storage!, run } : null; +} + +// No Mgas/s ceiling: bench workloads create little new state, so a full-node runway from them is not +// meaningful yet. +function diskRows(source: StorageSource | null): { label: string; evidence: string }[] { + if (!source) return [{ label: 'Disk · full node', evidence: 'runway pending' }, { label: 'Disk · archive node', evidence: 'not measured yet' }]; + const { storage: { archive, fullNode, diskBudgetBytes }, run } = source; + const at = `at ${run.mgas} Mgas/s`; + const from = `from the ${WORKLOAD[run.workload]} run`; + const years = archive.currentBytes === null ? null : archive.runwayYears ?? (diskBudgetBytes - archive.currentBytes) / archive.growthBytesPerYear; + const months = years === null ? 0 : Math.max(1, Math.round(years * 12)); + const runway = years === null ? '' : ` · ~${years < 1 ? `${months} month${months === 1 ? '' : 's'}` : `${years.toFixed(1)} years`} left of ${bytes(diskBudgetBytes)} (${archive.runwayBound} bound)`; + return [ + { + label: 'Disk · full node', + evidence: `history window ~${bytes(fullNode.historyFootprintBytes)} + proofs ~${bytes(fullNode.proofFootprintBytes)} ${at} (${fullNode.historyFootprintBound} bound); state growth per year from this workload: ${bytes(fullNode.stateGrowthBytesPerYear)}, ${from}. Bench workloads create little new state, so this undercounts mainnet state growth; runway pending.`, + }, + { label: 'Disk · archive node', evidence: `grows ${growth(archive)} ${at} (${archive.growthBound} bound)${runway}, ${from}` }, + ]; +} + function Evidence({ proven }: { proven: Proven }) { const { run } = proven; return ( @@ -90,11 +126,11 @@ function Evidence({ proven }: { proven: Proven }) { ); } -export function Bottlenecks({ limits, max, gasLimit, proven }: { limits: Limit[]; max: number; gasLimit: number | null; proven: Proven | null }) { +export function Bottlenecks({ limits, max, gasLimit, proven, storage }: { limits: Limit[]; max: number; gasLimit: number | null; proven: Proven | null; storage: StorageSource | null }) { const pct = (v: number) => `${Math.min(100, (v / max) * 100)}%`; const ranked = limits.filter((l) => l.value !== null).length; const fixed = [ - { label: 'State (full node)', evidence: 'runway pending' }, + ...diskRows(storage), { label: 'TEE proving, RPC under sync', evidence: 'not measured yet' }, ]; return ( @@ -104,7 +140,7 @@ export function Bottlenecks({ limits, max, gasLimit, proven }: { limits: Limit[]
      #LimitCeilingEvidence{l.evidence}{l.evidence}
      - + diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index 2257284..6e053f1 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -3,7 +3,7 @@ import { daWorkloads } from '../da/workloads'; import type { Proven } from '../proven/capacity'; import type { Area, Snapshot } from '../snapshot.mjs'; -import { bottleneckBars, Bottlenecks, rankLimits, type Limit } from './Bottlenecks'; +import { bottleneckBars, Bottlenecks, pickStorage, rankLimits, type Limit } from './Bottlenecks'; import { Guardrails } from './Guardrails'; import { Headline } from './Headline'; import { Ladder, type ProvenMark } from './Ladder'; @@ -50,7 +50,7 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl - + diff --git a/app/scaling/components/Workloads.tsx b/app/scaling/components/Workloads.tsx index 8d05083..76c4f50 100644 --- a/app/scaling/components/Workloads.tsx +++ b/app/scaling/components/Workloads.tsx @@ -1,6 +1,7 @@ import { Text } from '../../components/ui/Text'; -import { runHref, type ProvenCapacity, type Workload } from '../proven/capacity'; -import { times, type Limit } from './Bottlenecks'; +import { runHref, successfulTps, type ProvenCapacity, type Workload } from '../proven/capacity'; +import { growth, times, type Limit } from './Bottlenecks'; +import { Info } from './Info'; import { REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; import { Tag } from './Tag'; @@ -13,7 +14,14 @@ const TD = 'py-1 text-[12px]'; export function Workloads({ current, limits }: { current: ProvenCapacity['current'] | undefined; limits: Limit[] }) { return (
      - By workload + + + By workload + + Successful transactions per second at the proven rate: delivered tx/s × (1 − reverted ÷ submitted). Reverted transactions still use gas and count toward Mgas/s. + + +
      {CARDS.map(([w, label]) => { const proven = current?.[w] ?? null; @@ -27,12 +35,18 @@ export function Workloads({ current, limits }: { current: ProvenCapacity['curren {label} {proven ? ( - {proven.atTop ? '≥ ' : ''}{compact(proven.run.tps)}{w === 'mix' ? ' tx/s' : '/s'} + {proven.atTop ? '≥ ' : ''}{compact(successfulTps(proven.run))}{w === 'mix' ? ' tx/s' : '/s'} {w === 'mix' ? bench mix : null} ) : ( pending )} + {run && run.reverted > 0.01 ? ( + + {Math.round(run.reverted * 100)}% reverted + Reverted transactions still use gas and count toward Mgas/s; tx/s counts successful ones only. + + ) : null}
      #LimitCeilingEvidence#LimitCeiling Mgas/s. For DA rows, ≈ /s converts it to successful transactions per second: blob capacity ÷ frame bytes per successful transaction. Reverted transactions still use gas and blob space.Evidence
      @@ -43,10 +57,14 @@ export function Workloads({ current, limits }: { current: ProvenCapacity['curren - + + + + +
      Validators {run?.replay ? times(run.replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND) : '—'}
      Proof node {proofs ? `lag ${proofs.headLagBlocks.p99}${execute === undefined ? '' : ` · ${Math.round(execute)} ms`}` : '—'}
      Disk{run?.storage ? `archive ${growth(run.storage.archive)}` : '—'}
      diff --git a/app/scaling/components/firstScreen.test.tsx b/app/scaling/components/firstScreen.test.tsx index 1c5a938..ade6505 100644 --- a/app/scaling/components/firstScreen.test.tsx +++ b/app/scaling/components/firstScreen.test.tsx @@ -67,7 +67,7 @@ describe('Guardrails', () => { describe('scorecardGuardrails', () => { const LATENCY = { p50Ms: 2, p99Ms: 9, maxMs: 12 }; const run = (blocksPerSecond: number, lagP99 = 4, executeP99 = 9): Run => ({ - id: 'synthetic', outputDir: 'synthetic-400', workload: 'mix', mgas: 400, gps: 4e8, tps: 6000, passed: true, commit: 'c0ffee', + id: 'synthetic', outputDir: 'synthetic-400', workload: 'mix', mgas: 400, gps: 4e8, tps: 6000, reverted: 0, storage: null, passed: true, commit: 'c0ffee', createdAt: Date.parse('2026-01-05T00:00:00Z'), steps: [], da: null, replay: { blocksPerSecond, newPayload: LATENCY, match: true }, proofNode: { diff --git a/app/scaling/components/runBacked.test.tsx b/app/scaling/components/runBacked.test.tsx index 0beb179..c093a8a 100644 --- a/app/scaling/components/runBacked.test.tsx +++ b/app/scaling/components/runBacked.test.tsx @@ -77,7 +77,7 @@ describe('DA from bench runs', () => { withinTarget, }); const run = (workload: Run['workload'], runDa: Run['da']): Run => ({ - id: `synthetic-${workload}`, outputDir: `synthetic-${workload}`, workload, mgas: 200, gps: 2e8, tps: 4000, passed: true, + id: `synthetic-${workload}`, outputDir: `synthetic-${workload}`, workload, mgas: 200, gps: 2e8, tps: 4000, reverted: 0, storage: null, passed: true, commit: 'c0ffee', createdAt: Date.parse('2026-01-05T00:00:00Z'), steps: [], replay: null, proofNode: null, da: runDa, }); const rows = (html: string) => html.split(' ({ - id: `synthetic-${workload}-${mgas}`, outputDir: `synthetic-${mgas}`, workload, mgas, gps: (mgas - 5) * 1e6, tps: mgas * 10.5, passed: true, + id: `synthetic-${workload}-${mgas}`, outputDir: `synthetic-${mgas}`, workload, mgas, gps: (mgas - 5) * 1e6, tps: mgas * 10.5, reverted: 0, storage: null, passed: true, commit: 'bbbbbbbbbbbbbbbb', createdAt: T0 + 5 * DAY, steps: STEPS, da, replay: { blocksPerSecond: 36.4, newPayload: LATENCY, match: true }, proofNode: { @@ -38,6 +38,11 @@ const run = (mgas: number, workload: Run['workload'] = 'mix', da: Run['da'] = nu }, }); const none = { mix: null, payment: null, swap: null }; +const storage = (currentBytes: number | null = null): NonNullable => ({ + diskBudgetBytes: 45e12, + archive: { growthBytesPerYear: 5e12, growthBound: 'lower', currentBytes, runwayYears: null, runwayBound: 'upper' }, + fullNode: { stateGrowthBytesPerYear: 1.05e10, historyFootprintBytes: 9.2e12, historyFootprintBound: 'lower', proofFootprintBytes: 2.3e12 }, +}); const bench = (mgas: number, atTop: boolean, more: Partial = {}): ProvenCapacity => ({ current: { ...none, mix: { mgas, atTop, run: run(mgas) } }, da: none, @@ -147,7 +152,8 @@ describe('what breaks first', () => { '—Execution (sequencer)—no proven run', '—Validators—pending', '—Proof node—pending', - '—State (full node)—runway pending', + '—Disk · full node—runway pending', + '—Disk · archive node—not measured yet', '—TEE proving, RPC under sync—not measured yet', ]); }); @@ -189,9 +195,9 @@ describe('by workload', () => { it('cards each workload with its proven rate, DA ceiling, replay headroom and proof-node latency', () => { swr.result = { data: bench(400, true, { current: { mix: { mgas: 400, atTop: true, run: run(400) }, swap: null, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } } }) }; const { workloads } = render(); - expect(card(workloads, 'Payments (B20)')).toBe('Payments (B20)≥ 2.1k/sDA~57 · breaks firstValidators7.3×Proof nodelag 4 · 733 ms'); - expect(card(workloads, 'Swaps (Uniswap v3)')).toBe('Swaps (Uniswap v3)pendingDA~115 EstimatedValidators—Proof node—'); - expect(card(workloads, 'Mainnet mix')).toBe('Mainnet mix≥ 4.2k tx/sbench mixDA190 · breaks firstValidators7.3×Proof nodelag 4 · 733 ms'); + expect(card(workloads, 'Payments (B20)')).toBe('Payments (B20)≥ 2.1k/sDA~57 · breaks firstValidators7.3×Proof nodelag 4 · 733 msDisk—'); + expect(card(workloads, 'Swaps (Uniswap v3)')).toBe('Swaps (Uniswap v3)pendingDA~115 EstimatedValidators—Proof node—Disk—'); + expect(card(workloads, 'Mainnet mix')).toBe('Mainnet mix≥ 4.2k tx/sbench mixDA190 · breaks firstValidators7.3×Proof nodelag 4 · 733 msDisk—'); }); it('keeps the DA tag when the ceiling is above the proven rate', () => { @@ -200,6 +206,45 @@ describe('by workload', () => { }); }); +describe('reverts, measured swaps and disk', () => { + it('counts successful tx/s on workload cards and notes reverts above 1%', () => { + const reverting = { ...run(200, 'swap'), reverted: 0.155 }; + swr.result = { data: bench(400, true, { current: { mix: null, payment: { mgas: 200, atTop: true, run: { ...run(200, 'payment'), reverted: 0.005 } }, swap: { mgas: 200, atTop: true, run: reverting } } }) }; + const { workloads } = render(); + // 2,100 delivered × 0.845 successful. + expect(card(workloads, 'Swaps (Uniswap v3)')).toContain('Swaps (Uniswap v3)≥ 1.8k/s16% reverted'); + expect(card(workloads, 'Payments (B20)')).toContain('Payments (B20)≥ 2.1k/sDA'); + expect(card(workloads, 'Payments (B20)')).not.toContain('reverted'); + }); + + it('ranks a measured swaps DA ceiling by its value, past the gas limit', () => { + // 4e10 gas and 2.6158e7 frame bytes put the 14-blob ceiling at ~232 Mgas/s. + swr.result = { data: bench(400, true, { da: { mix: null, payment: null, swap: run(400, 'swap', daResult(2.6158e7, 22.4, false)) } }) }; + const ranked = render().ranked; + expect(rows(ranked).slice(0, 4).map((r) => r.slice(0, 22))).toEqual(['1DA · Payments (B20)ne', '2DA · Mainnet mix190 ≈', '3DA · Swaps232 ≈ 5,800', '4Execution, validators']); + expect(rows(ranked)[2]).toContain('Measuredmeasured · 22.4 blobs/slot at 400'); + const swapsRow = ranked.split(' r.includes('>DA · Swaps<'))!; + expect(swapsRow).toMatch(/style="width:58\.\d+%"/); + expect(swapsRow).toMatch(/border-dashed[^"]*" style="left:50%"/); + }); + + it('shows full-node and archive disk rows from the mix run, with bounds and no full-node runway', () => { + const data = bench(400, true); + data.current.mix = { mgas: 400, atTop: true, run: { ...run(400), storage: storage() } }; + swr.result = { data }; + const [full, archive] = rows(render().ranked).slice(4, 6); + expect(full).toBe('—Disk · full node—history window ~9.2 TB + proofs ~2.3 TB at 400 Mgas/s (lower bound); state growth per year from this workload: 10.5 GB, from the mainnet mix run. Bench workloads create little new state, so this undercounts mainnet state growth; runway pending.'); + expect(archive).toBe('—Disk · archive node—grows ≥ 5.0 TB/year at 400 Mgas/s (lower bound), from the mainnet mix run'); + expect(full).not.toMatch(/years? left/); + expect(card(render().workloads, 'Mainnet mix')).toContain('Diskarchive ≥ 5.0 TB/year'); + }); + + it('adds an archive runway only when the current archive size is known, and falls back to the newest other workload', () => { + swr.result = { data: bench(400, true, { current: { mix: null, payment: null, swap: { mgas: 400, atTop: true, run: { ...run(400, 'swap'), storage: storage(35e12) } } } }) }; + expect(rows(render().ranked).find((r) => r.includes('archive node'))).toBe('—Disk · archive node—grows ≥ 5.0 TB/year at 400 Mgas/s (lower bound) · ~2.0 years left of 45.0 TB (upper bound), from the swaps run'); + }); +}); + describe('guardrails and work', () => { it('compresses guardrails to breached, met and not-measured counts with a link', () => { const { guardrails } = render({}); diff --git a/app/scaling/da/workloads.test.ts b/app/scaling/da/workloads.test.ts index bfe0d63..3d6617c 100644 --- a/app/scaling/da/workloads.test.ts +++ b/app/scaling/da/workloads.test.ts @@ -35,7 +35,7 @@ it('is modeled once every workload is measured', () => { }); const run = (workload: Run['workload'], da: Run['da']): Run => ({ - id: `synthetic-${workload}`, outputDir: `synthetic-${workload}`, workload, mgas: 200, gps: 2e8, tps: 4000, passed: true, + id: `synthetic-${workload}`, outputDir: `synthetic-${workload}`, workload, mgas: 200, gps: 2e8, tps: 4000, reverted: 0, storage: null, passed: true, commit: 'c0ffee', createdAt: Date.parse('2026-01-05T00:00:00Z'), steps: [], replay: null, proofNode: null, da, }); const da = (frame_bytes: number, gas_used: number, transactions: number): Da => ({ @@ -58,6 +58,17 @@ it('measures bytes/tx and gas/tx from a bench DA run, keeping the mainnet mix an expect(daWorkloads(undefined)).toEqual(DA_WORKLOADS); }); +it('counts only successful transactions per second, keeping gas per second', () => { + const swap = { ...run('swap', da(1.58e8, 2.4e11, 2e6)), reverted: 0.155 }; + const [, swaps] = daWorkloads({ payment: null, swap, mix: null }); + // 2,000,000 × 0.845 = 1,690,000 successful transactions carry all the bytes and gas. + expect(swaps.gasPerTx).toBeCloseTo(2.4e11 / 1.69e6); + expect(swaps.bytesPerTx).toBeCloseTo(1.58e8 / 1.69e6); + const allTx = daWorkloads({ payment: null, swap: { ...swap, reverted: 0 }, mix: null })[1]; + expect(daCapacity(swaps, BLOB_TARGET).txPerSecond).toBeCloseTo(0.845 * daCapacity(allTx, BLOB_TARGET).txPerSecond); + expect(daCapacity(swaps, BLOB_TARGET).mgasPerSecond).toBeCloseTo(daCapacity(allTx, BLOB_TARGET).mgasPerSecond); +}); + it('is modeled once payments and swaps are measured', () => { const [da2] = withDaWorkloads([ceiling('da', 820)], daWorkloads({ payment: run('payment', da(8e7, 4e10, 1e6)), swap: run('swap', da(2e8, 1.4e11, 1e6)), mix: null })); expect(da2.tag).toBe('modeled'); diff --git a/app/scaling/da/workloads.ts b/app/scaling/da/workloads.ts index 63a3e17..9331a17 100644 --- a/app/scaling/da/workloads.ts +++ b/app/scaling/da/workloads.ts @@ -38,8 +38,9 @@ export const DA_WORKLOADS: DaWorkload[] = [ }, ]; -// A bench DA run replaces an estimate with frame_bytes ÷ transactions and gas_used ÷ transactions, so -// bytes/tx ÷ gas/tx is the run's bytes per gas. The mainnet mix keeps its measurement from real +// A bench DA run replaces an estimate with frame_bytes and gas_used per successful transaction (reverted +// ones still use gas and blob space), so bytes/tx ÷ gas/tx is the run's bytes per gas and tx/s counts +// successful transactions only. The mainnet mix keeps its measurement from real // mainnet blocks, which the synthetic bench mix only approximates. export function daWorkloads(da: ProvenCapacity['da'] | undefined, workloads: DaWorkload[] = DA_WORKLOADS): DaWorkload[] { return workloads.map((w) => { @@ -47,7 +48,8 @@ export function daWorkloads(da: ProvenCapacity['da'] | undefined, workloads: DaW if (!run?.da) return w; const bench = { run, da: run.da }; if (w.tag === 'measured') return { ...w, bench }; - const { frame_bytes, gas_used, transactions } = run.da.result; + const { frame_bytes, gas_used } = run.da.result; + const transactions = run.da.result.transactions * (1 - run.reverted); return { ...w, gasPerTx: gas_used / transactions, diff --git a/app/scaling/proven/capacity.test.ts b/app/scaling/proven/capacity.test.ts index b92e600..646a1ca 100644 --- a/app/scaling/proven/capacity.test.ts +++ b/app/scaling/proven/capacity.test.ts @@ -38,6 +38,11 @@ const PROOFS = { witnessBytes: { samples: 3000, p50: 4096, p99: 8192, max: 9000 }, }; const DA = { seconds: 600, gas_used: 6e10, transactions: 1e6, transaction_bytes: 1.5e8, frame_bytes: 6e7, gas_per_second: 1e8, bytes_per_gas: 0.001, compression_ratio: 2.5, blobs_per_l1_slot: 15.5, blob_fill: 0.98, blob_target_gas_per_second: 1.5e8 }; +const STORAGE = { + diskBudgetBytes: 45e12, + archive: { growthBytesPerYear: 1e14, growthBound: 'lower', currentBytes: null, runwayYears: null, runwayBound: 'upper' }, + fullNode: { stateGrowthBytesPerYear: 1e10, historyFootprintBytes: 9e12, historyFootprintBound: 'lower', proofFootprintBytes: 2e12 }, +}; // Run outputs are cached by outputDir across calls, so every test uses its own dirs. function load(specs: Spec[]) { @@ -65,11 +70,12 @@ function load(specs: Spec[]) { const delivered = (s.delivered ?? 1) * s.mgas; switch (file) { case 'validation.json': return Response.json({ comparable: s.comparable ?? true, reasons: [] }); - case 'load-test-result.json': return Response.json({ throughput: { gps: delivered * 1e6, tps: delivered * 10 } }); + case 'load-test-result.json': return Response.json({ throughput: { gps: delivered * 1e6, tps: delivered * 10, total_submitted: 1000, total_reverted: 155 } }); case 'pipeline-result.json': return Response.json({ suite: 'synthetic', comparable: s.comparable ?? true, steps: STEPS(s) }); case 'replay-result.json': return Response.json(REPLAY); case 'proof-node/replay-result.json': return Response.json({ ...REPLAY, blocksPerSecond: 20, proofs: PROOFS }); case 'da-result.json': return Response.json(DA); + case 'storage-growth.json': return Response.json(STORAGE); } if (s.ref === null) return new Response(HTML); return Response.json({ source: { repository: 'base/base', ref: s.ref ?? 'main', revision: `rev-${dir}` } }); @@ -94,6 +100,7 @@ it('counts only pipeline runs built from main, with their steps, replays and DA mgas: 100, gps: 100e6, tps: 1000, + reverted: 0.155, passed: true, commit: 'rev-main-100', createdAt: NOW - 3 * DAY, @@ -101,6 +108,7 @@ it('counts only pipeline runs built from main, with their steps, replays and DA replay: { blocksPerSecond: 36.4, newPayload: LATENCY, match: true }, proofNode: { blocksPerSecond: 20, newPayload: LATENCY, match: true, proofs: PROOFS }, da: { result: DA, target: 14, withinTarget: false }, + storage: STORAGE, }, }); expect(unknown).toEqual([]); @@ -133,11 +141,11 @@ it('fails a run whose pipeline step failed', async () => { expect(current.mix).toMatchObject({ mgas: 100, atTop: false }); }); -it('reads missing replay, proof-node and DA files as null, and an unjudged blob target as null', async () => { +it('reads missing replay, proof-node and storage files as null, and an unjudged blob target as null', async () => { const { current } = await load([ - { dir: 'partial-100', mgas: 100, daysAgo: 1, missing: ['replay-result.json', 'proof-node/replay-result.json'], withinTarget: null }, + { dir: 'partial-100', mgas: 100, daysAgo: 1, missing: ['replay-result.json', 'proof-node/replay-result.json', 'storage-growth.json'], withinTarget: null }, ]); - expect(current.mix?.run).toMatchObject({ replay: null, proofNode: null, da: { target: 14, withinTarget: null } }); + expect(current.mix?.run).toMatchObject({ replay: null, proofNode: null, storage: null, da: { target: 14, withinTarget: null } }); }); it('ignores alias copies and non-sustained scenarios', async () => { @@ -247,6 +255,8 @@ it('fetches replay, proof-node and DA files only for proven and latest-DA runs', }; const { current, da } = await loadProvenCapacity('https://bench.example', fetchImpl as typeof fetch, NOW); expect(current.mix?.run.outputDir).toBe('sel-200'); + // Without submitted and reverted totals, nothing counts as reverted. + expect(current.mix?.run.reverted).toBe(0); expect(da.mix?.outputDir).toBe('sel-300'); const details = fetched.filter((p) => /replay-result|da-result/.test(p)).map((p) => p.split('/')[0]); expect([...new Set(details)].toSorted()).toEqual(['sel-200', 'sel-300']); diff --git a/app/scaling/proven/capacity.ts b/app/scaling/proven/capacity.ts index d6a16d3..8cf0fa2 100644 --- a/app/scaling/proven/capacity.ts +++ b/app/scaling/proven/capacity.ts @@ -37,6 +37,13 @@ export type DaResult = { // `target` and `withinTarget` come from the pipeline's DA step; the blob target is recorded, not gating. export type Da = { result: DaResult; target: number | null; withinTarget: boolean | null }; +// `storage-growth.json`: disk growth measured over the run, projected to a year at its gas rate. +export type Storage = { + diskBudgetBytes: number; + archive: { growthBytesPerYear: number; growthBound: string; currentBytes: number | null; runwayYears: number | null; runwayBound: string }; + fullNode: { stateGrowthBytesPerYear: number; historyFootprintBytes: number; historyFootprintBound: string; proofFootprintBytes: number }; +}; + export type Run = { id: string; outputDir: string; @@ -44,6 +51,8 @@ export type Run = { mgas: number; gps: number; tps: number; + // Share of submitted transactions that reverted; they still use gas. + reverted: number; passed: boolean; commit: string; createdAt: number; @@ -51,8 +60,12 @@ export type Run = { replay: Replay | null; proofNode: (Replay & { proofs: Proofs | null }) | null; da: Da | null; + storage: Storage | null; }; +// Reverted transactions count toward gas/s but not toward transactions per second. +export const successfulTps = (run: Run) => run.tps * (1 - run.reverted); + export type Proven = { mgas: number; atTop: boolean; run: Run }; export type Step = { @@ -83,7 +96,7 @@ const WORKLOADS = Object.keys(PAYLOADS) as Workload[]; // Run outputs never change once written. `null` marks a run that does not count: not built from // main, or written before the step pipeline. const runCache = new Map(); -const detailCache = new Map>(); +const detailCache = new Map>(); export const runHref = (run: Run) => `/scaling/runs/bench/${encodeURIComponent(run.id)}`; @@ -172,6 +185,7 @@ async function loadRun(root: string, meta: BenchmarkRun, fetchImpl: typeof fetch mgas, gps, tps: load.throughput.tps, + reverted: load.throughput.total_submitted ? (load.throughput.total_reverted ?? 0) / load.throughput.total_submitted : 0, passed: validation.comparable === true && gps >= PASS_RATIO * mgas * 1e6, commit: provenance.source.revision, createdAt: Date.parse(meta.createdAt), @@ -179,17 +193,24 @@ async function loadRun(root: string, meta: BenchmarkRun, fetchImpl: typeof fetch replay: null, proofNode: null, da: null, + storage: null, }; } -async function loadDetails(root: string, run: Run, fetchImpl: typeof fetch): Promise> { +async function loadDetails(root: string, run: Run, fetchImpl: typeof fetch): Promise> { const get = (file: string) => getJson(fetchImpl, `${root}${run.outputDir}/${file}`); - const [replay, proofNode, da] = await Promise.all([get('replay-result.json'), get('proof-node/replay-result.json'), get('da-result.json')]); + const [replay, proofNode, da, storage] = await Promise.all([ + get('replay-result.json'), + get('proof-node/replay-result.json'), + get('da-result.json'), + get('storage-growth.json'), + ]); const daOutputs = run.steps.find((s) => s.name === 'da')?.outputs; return { replay: replay && pickReplay(replay), proofNode: proofNode && { ...pickReplay(proofNode), proofs: proofNode.proofs ?? null }, da: da && { result: da, target: (daOutputs?.target as number) ?? null, withinTarget: (daOutputs?.withinTarget as boolean) ?? null }, + storage, }; } From 7566a98dd3a22bb17774bf927afb7d949dc09a3c Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 14:30:15 -0700 Subject: [PATCH 13/33] fix(scaling): keep run details when storage growth fails, and flag an exceeded archive budget --- app/scaling/components/Bottlenecks.tsx | 4 +++- app/scaling/components/scorecard.test.tsx | 2 ++ app/scaling/proven/capacity.test.ts | 6 ++++++ app/scaling/proven/capacity.ts | 3 ++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index 5048e16..b1447e1 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -96,7 +96,9 @@ function diskRows(source: StorageSource | null): { label: string; evidence: stri const from = `from the ${WORKLOAD[run.workload]} run`; const years = archive.currentBytes === null ? null : archive.runwayYears ?? (diskBudgetBytes - archive.currentBytes) / archive.growthBytesPerYear; const months = years === null ? 0 : Math.max(1, Math.round(years * 12)); - const runway = years === null ? '' : ` · ~${years < 1 ? `${months} month${months === 1 ? '' : 's'}` : `${years.toFixed(1)} years`} left of ${bytes(diskBudgetBytes)} (${archive.runwayBound} bound)`; + const runway = years === null ? '' + : years <= 0 ? ` · over the ${bytes(diskBudgetBytes)} budget` + : ` · ~${years < 1 ? `${months} month${months === 1 ? '' : 's'}` : `${years.toFixed(1)} years`} left of ${bytes(diskBudgetBytes)} (${archive.runwayBound} bound)`; return [ { label: 'Disk · full node', diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 2b331d9..1a1b056 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -242,6 +242,8 @@ describe('reverts, measured swaps and disk', () => { it('adds an archive runway only when the current archive size is known, and falls back to the newest other workload', () => { swr.result = { data: bench(400, true, { current: { mix: null, payment: null, swap: { mgas: 400, atTop: true, run: { ...run(400, 'swap'), storage: storage(35e12) } } } }) }; expect(rows(render().ranked).find((r) => r.includes('archive node'))).toBe('—Disk · archive node—grows ≥ 5.0 TB/year at 400 Mgas/s (lower bound) · ~2.0 years left of 45.0 TB (upper bound), from the swaps run'); + swr.result = { data: bench(400, true, { current: { mix: null, payment: null, swap: { mgas: 400, atTop: true, run: { ...run(400, 'swap'), storage: storage(50e12) } } } }) }; + expect(rows(render().ranked).find((r) => r.includes('archive node'))).toContain('(lower bound) · over the 45.0 TB budget, from the swaps run'); }); }); diff --git a/app/scaling/proven/capacity.test.ts b/app/scaling/proven/capacity.test.ts index 646a1ca..68ff925 100644 --- a/app/scaling/proven/capacity.test.ts +++ b/app/scaling/proven/capacity.test.ts @@ -141,6 +141,12 @@ it('fails a run whose pipeline step failed', async () => { expect(current.mix).toMatchObject({ mgas: 100, atTop: false }); }); +it('keeps a run and its other details when only storage-growth.json fails', async () => { + const { current, unknown } = await load([{ dir: 'storage-500', mgas: 100, daysAgo: 1, fail: 'storage-growth.json' }]); + expect(current.mix?.run).toMatchObject({ storage: null, da: { target: 14 }, replay: { match: true } }); + expect(unknown).toEqual([]); +}); + it('reads missing replay, proof-node and storage files as null, and an unjudged blob target as null', async () => { const { current } = await load([ { dir: 'partial-100', mgas: 100, daysAgo: 1, missing: ['replay-result.json', 'proof-node/replay-result.json', 'storage-growth.json'], withinTarget: null }, diff --git a/app/scaling/proven/capacity.ts b/app/scaling/proven/capacity.ts index 8cf0fa2..8033162 100644 --- a/app/scaling/proven/capacity.ts +++ b/app/scaling/proven/capacity.ts @@ -203,7 +203,8 @@ async function loadDetails(root: string, run: Run, fetchImpl: typeof fetch): Pro get('replay-result.json'), get('proof-node/replay-result.json'), get('da-result.json'), - get('storage-growth.json'), + // Newer than the other files and only shown as context, so its failure must not drop them. + get('storage-growth.json').catch(() => null), ]); const daOutputs = run.steps.find((s) => s.name === 'da')?.outputs; return { From 09401cd0813b627a4ca1f0a24165762c5e403441 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 15:25:55 -0700 Subject: [PATCH 14/33] feat(scaling): head with workload tiles in transactions and move subsystem detail into a table --- app/scaling/components/Headline.tsx | 121 ++++++++++++++++-- app/scaling/components/Scorecard.tsx | 6 +- app/scaling/components/WorkloadSubsystems.tsx | 51 ++++++++ app/scaling/components/Workloads.tsx | 76 ----------- app/scaling/components/scorecard.test.tsx | 98 +++++++++----- app/scaling/page.test.tsx | 4 +- 6 files changed, 229 insertions(+), 127 deletions(-) create mode 100644 app/scaling/components/WorkloadSubsystems.tsx delete mode 100644 app/scaling/components/Workloads.tsx diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx index 6b7c1ab..0d04eb3 100644 --- a/app/scaling/components/Headline.tsx +++ b/app/scaling/components/Headline.tsx @@ -1,10 +1,11 @@ import type { ReactNode } from 'react'; +import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; import { BLOB_TARGET, fmt } from '../da/workloads'; -import type { ProvenCapacity, Workload } from '../proven/capacity'; +import { successfulTps, type ProvenCapacity, type Workload } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; -import type { Limit } from './Bottlenecks'; +import { times, type Limit } from './Bottlenecks'; import { Info } from './Info'; import type { ProvenMark } from './Ladder'; import { day } from './ProvenCapacity'; @@ -14,7 +15,7 @@ export type BenchState = { configured: boolean; data?: ProvenCapacity; error?: u const TRAFFIC: Record = { payment: 'payments', swap: 'swaps', mix: 'tx' }; const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 1 }); -type Props = Pick & { bench: BenchState; proven: ProvenMark; next: Limit | null }; +type Props = Pick & { bench: BenchState; proven: ProvenMark; next: Limit | null; limits: Limit[] }; // Only states other than a loaded bench result need a note. function benchNote({ configured, data, error }: BenchState, proven: ProvenMark, qualified: Snapshot['ladder']['qualified']): ReactNode { @@ -34,14 +35,17 @@ function Capacity({ ladder, usage, proven }: Pick - Proven capacity - +
      + + Proven capacity + Execution, validators and proof node all pass at this rate on the mainnet snapshot. Workload tiles count successful transactions per second. + + {proven ? `${proven.atLeast ? '≥ ' : ''}${n(proven.value)}` : '—'} Mgas/s @@ -53,6 +57,96 @@ function Capacity({ ladder, usage, proven }: Pick (c[1] < low[1] ? c : low)); + return { mgas, cap }; +} + +// The headline column and the tiles share these slots so their label and value rows line up. +const SLOTS = 'flex flex-col gap-1.5 border py-3'; +const LABEL = 'flex h-4 items-center gap-1 overflow-hidden whitespace-nowrap text-[11px] font-[600] uppercase leading-4 tracking-wide text-bds-gray-60'; +const VALUE = 'whitespace-nowrap leading-[44px] tabular-nums'; +const CAP_TONE: Record = { DA: 'text-bds-red-60', 'gas limit': 'text-bds-orange-60', execution: '' }; +const compact = (v: number) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(v).toLowerCase(); + +function Tile({ row: r, gasLimit }: { row: Translation; gasLimit: number | null }) { + const s = r.proven && sustainable(r.proven.mgas, r.da, gasLimit); + const tx = (mgas: number) => (mgas * 1e6) / r.gasPerTx!; + const vs = (v: number) => (r.vsToday && r.today ? ({times(v / r.today)}) : null); + return ( +
      + {r.label} + + {!r.proven ? 'pending' : r.gasPerTx === null ? '—' : <>{r.proven.atLeast ? '≥ ' : ''}{compact(tx(r.proven.mgas))}/s{vs(tx(r.proven.mgas))}} + + + sustainable today {!s ? 'pending' : r.gasPerTx === null ? '—' : <>{fmt(tx(s.mgas))}/s{vs(tx(s.mgas))}} + + Highest rate we can run continuously now: below the L1 blob target and within the current gas limit{gasLimit === null ? '' : ` (${n(gasLimit)} Mgas/s)`}. Capped by DA or the gas limit, not execution. + + + {s ? `capped by ${s.cap}` : ''} + {/* Reserved on every tile so a note on one does not shift the others. */} + + {r.reverted !== undefined && r.reverted > 0.01 ? ( + <> + {Math.round(r.reverted * 100)}% reverted + Reverted transactions still use gas and count toward Mgas/s; tx/s counts successful ones only. + + ) : null} + +
      + ); +} + +// Bench workloads convert with their proven run's gas per successful transaction; today's traffic mix +// with mainnet's own gas per transaction, since the bench mix only approximates mainnet. +function translations({ ladder, usage, bench, proven, limits }: Pick): Translation[] { + const daOf = (w: Workload) => limits.find((l) => l.id === `da-${w}`)!.value!; + const benchRow = (w: Workload, label: string): Translation => { + const p = bench.data?.current[w] ?? null; + return { + label, + today: null, + gasPerTx: p ? p.run.gps / successfulTps(p.run) : null, + proven: p && { mgas: p.mgas, atLeast: p.atTop }, + da: daOf(w), + reverted: p?.run.reverted, + }; + }; + const p50 = ladder.observed.p50.value; + const tps = usage?.transactionsPerSecond.value ?? null; + const gasPerTx = p50 !== null && tps ? (p50 * 1e6) / tps : null; + const mixHelp = gasPerTx !== null + ? `At today's mainnet average of ~${Math.round(gasPerTx / 1e3)}k gas per transaction (${n(p50!)} Mgas/s ÷ ${fmt(tps!)} tx/s), not the bench mix.` + : `The snapshot has no transactions per second, so this row stays in gas: today ${p50 === null ? 'not measured' : `${n(p50)} Mgas/s`}.`; + return [ + benchRow('payment', 'Payments (B20)'), + benchRow('swap', 'Swaps (Uniswap v3)'), + { + label: Today's traffic mix {mixHelp}, + today: tps, + gasPerTx, + proven: proven && { mgas: proven.value, atLeast: proven.atLeast }, + da: daOf('mix'), + vsToday: true, + }, + ]; +} + function Next({ limit, proven }: { limit: Limit; proven: ProvenMark }) { const w = limit.workload; const bench = w?.bench; @@ -80,14 +174,15 @@ function LastChange({ history }: { history: ProvenCapacity['history'] }) { ); } -export function Headline({ ladder, usage, bench, proven, next }: Props) { +export function Headline({ ladder, usage, bench, proven, next, limits }: Props) { const note = benchNote(bench, proven, ladder.qualified); return (
      -
      - - {next ? : null} +
      +
      + {translations({ ladder, usage, bench, proven, limits }).map((row, i) => )}
      + {next ? : null} {bench.data ? : null} {note ? {note} : null}
      diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index 6e053f1..3153ab1 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -9,7 +9,7 @@ import { Headline } from './Headline'; import { Ladder, type ProvenMark } from './Ladder'; import { day, useProvenCapacity } from './ProvenCapacity'; import { Work } from './Work'; -import { Workloads } from './Workloads'; +import { WorkloadSubsystems } from './WorkloadSubsystems'; function benchMark({ mgas, atTop, run }: Proven): ProvenMark { const top = atTop ? ` ≥ because ${mgas} Mgas/s is the highest rate the bench runs.` : ''; @@ -48,10 +48,10 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl return ( <> - + - + diff --git a/app/scaling/components/WorkloadSubsystems.tsx b/app/scaling/components/WorkloadSubsystems.tsx new file mode 100644 index 0000000..59bd31f --- /dev/null +++ b/app/scaling/components/WorkloadSubsystems.tsx @@ -0,0 +1,51 @@ +import { Card } from '../../components/ui/Card'; +import { cn } from '../../components/ui/cn'; +import { Text } from '../../components/ui/Text'; +import type { ProvenCapacity, Run, Workload } from '../proven/capacity'; +import { growth, times } from './Bottlenecks'; +import { REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; + +const COLUMNS: [Workload, string][] = [['payment', 'Payments'], ['swap', 'Swaps'], ['mix', 'Mainnet mix (bench run)']]; +const TH = 'px-3 py-2 text-left text-[11px] font-[600] uppercase tracking-wide text-bds-gray-60'; +const TD = 'px-3 py-2 whitespace-nowrap'; + +// Each value comes from that workload's own proven run. +const ROWS: [string, (run: Run) => string | null][] = [ + ['Validators', (r) => (r.replay ? `${times(r.replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND)} replay headroom` : null)], + ['Proof node', (r) => { + const proofs = r.proofNode?.proofs; + const execute = proofs?.methods.debug_executePayload?.latency.p99Ms; + return proofs ? `lag p99 ${proofs.headLagBlocks.p99}${execute === undefined ? '' : ` · executePayload p99 ${Math.round(execute)} ms`}` : null; + }], + ['Disk · archive', (r) => (r.storage ? `${growth(r.storage.archive)} at ${r.mgas} Mgas/s` : null)], +]; + +export function WorkloadSubsystems({ current }: { current: ProvenCapacity['current'] | undefined }) { + return ( +
      + Subsystems by workload + + + + + )} + + + + {ROWS.map(([label, value]) => ( + + + {COLUMNS.map(([w]) => { + const run = current?.[w]?.run; + const v = run ? value(run) : null; + return ; + })} + + ))} + +
      + {COLUMNS.map(([w, label]) => {label}
      {label}{v ?? '—'}
      +
      +
      + ); +} diff --git a/app/scaling/components/Workloads.tsx b/app/scaling/components/Workloads.tsx deleted file mode 100644 index 76c4f50..0000000 --- a/app/scaling/components/Workloads.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Text } from '../../components/ui/Text'; -import { runHref, successfulTps, type ProvenCapacity, type Workload } from '../proven/capacity'; -import { growth, times, type Limit } from './Bottlenecks'; -import { Info } from './Info'; -import { REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; -import { Tag } from './Tag'; - -const CARDS: [Workload, string][] = [['payment', 'Payments (B20)'], ['swap', 'Swaps (Uniswap v3)'], ['mix', 'Mainnet mix']]; -const compact = (v: number) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(v).toLowerCase(); -const TD = 'py-1 text-[12px]'; - -// Each workload's validators and proof node kept up at its proven rate, so DA breaks first when its -// ceiling is below that rate. -export function Workloads({ current, limits }: { current: ProvenCapacity['current'] | undefined; limits: Limit[] }) { - return ( -
      - - - By workload - - Successful transactions per second at the proven rate: delivered tx/s × (1 − reverted ÷ submitted). Reverted transactions still use gas and count toward Mgas/s. - - - -
      - {CARDS.map(([w, label]) => { - const proven = current?.[w] ?? null; - const run = proven?.run; - const da = limits.find((l) => l.id === `da-${w}`)!; - const breaksFirst = proven !== null && da.value! < proven.mgas; - const proofs = run?.proofNode?.proofs; - const execute = proofs?.methods.debug_executePayload?.latency.p99Ms; - return ( -
      - {label} - {proven ? ( - - {proven.atTop ? '≥ ' : ''}{compact(successfulTps(proven.run))}{w === 'mix' ? ' tx/s' : '/s'} - {w === 'mix' ? bench mix : null} - - ) : ( - pending - )} - {run && run.reverted > 0.01 ? ( - - {Math.round(run.reverted * 100)}% reverted - Reverted transactions still use gas and count toward Mgas/s; tx/s counts successful ones only. - - ) : null} - - - - - - - - - - - - - - - - - - - -
      DA{da.ceiling.split(' ≈ ')[0]}{breaksFirst ? ' · breaks first' : <> }
      Validators{run?.replay ? times(run.replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND) : '—'}
      Proof node{proofs ? `lag ${proofs.headLagBlocks.p99}${execute === undefined ? '' : ` · ${Math.round(execute)} ms`}` : '—'}
      Disk{run?.storage ? `archive ${growth(run.storage.archive)}` : '—'}
      -
      - ); - })} -
      -
      - ); -} diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 1a1b056..e521112 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ProvenCapacity, Run } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; +import { sustainable } from './Headline'; import { Scorecard } from './Scorecard'; // Synthetic data only: real bench results and snapshots are internal and never committed. @@ -58,8 +59,8 @@ const fixture = (): Snapshot => JSON.parse(readFileSync(new URL('../fixtures/syn // In page order: the freshness line, then headline, ladder, ranked limits, workloads, guardrails, work. const render = (props: { benchUrl?: string } = { benchUrl: BENCH }, snapshot = fixture()) => { const html = renderToStaticMarkup(); - const [fresh, head, ladder, ranked, workloads, guardrails, work] = html.split(' { @@ -72,7 +73,8 @@ const text = (html: string) => { }; // Table rows after a header row (skip 2) or without one (skip 1). const rows = (html: string, skip = 2) => html.split(' text(` text(` c.includes(`>${label}<`)) ?? '>'}`.replace(/<[^>]*$/, '')); +// A header tile, found by text in its label. +const tile = (head: string, label: string) => text(` c.includes(label)) ?? '>'}`.replace(/<[^>]*$/, '')); beforeEach(() => { swr.result = {}; @@ -90,10 +92,10 @@ describe('freshness and headline', () => { it('heads with proven capacity, today and headroom, and the next bottleneck with its last change', () => { swr.result = { data: bench(400, true) }; const { head } = render(); - expect(text(head)).toContain('Proven capacity≥ 400Mgas/stoday 12.5 median (64 tx/s) · 32×'); + expect(text(head)).toContain('≥ 400Mgas/stoday 12.5 (64 tx/s) · 32×'); expect(text(head)).toContain('Next bottleneck to solveDA · Payments (B20) at ~57 Mgas/s≈ 1,517 payments/s'); expect(text(head)).toContain('↑ from 40 Mgas/s on 2026-01-06 · compare'); - expect(text(head)).not.toMatch(/First limit|Payments per second/); + expect(text(head)).not.toMatch(/First limit|Payments per second|In transactions/); }); it('adds blobs per slot to the callout once the binding DA workload has a bench run', () => { @@ -106,11 +108,11 @@ describe('freshness and headline', () => { delete snapshot.usage; snapshot.ladder.qualified = { ...snapshot.ladder.qualified, value: 30, tag: 'measured', evidence: [{ label: 'Synthetic run', url: 'https://example.com/scaling/runs/bench/synthetic-1' }] }; const head = text(render({}, snapshot).head); - expect(head).toContain('Proven capacity30Mgas/stoday 12.5 median · 2.4×'); + expect(head).toContain('30Mgas/stoday 12.5 · 2.4×'); expect(head).toContain('Proven capacity from the snapshot · Synthetic run'); expect(text(render().head)).toContain('Loading bench results…'); swr.result = { error: new TypeError('Failed to fetch'), data: bench(400, true) }; - expect(text(render().head)).toMatch(/Proven capacity—Mgas\/s.*VPN/); + expect(text(render().head)).toMatch(/per second\.—Mgas\/s.*VPN/); }); it('names the merged core limit when the proven rate is below every DA ceiling', () => { @@ -119,6 +121,61 @@ describe('freshness and headline', () => { }); }); +describe('workload tiles', () => { + it('takes the lowest of the proven, DA and gas-limit ceilings, preferring DA then the gas limit on ties', () => { + expect(sustainable(400, 82, 200)).toEqual({ mgas: 82, cap: 'DA' }); + expect(sustainable(400, 232, 200)).toEqual({ mgas: 200, cap: 'gas limit' }); + expect(sustainable(40, 82, 200)).toEqual({ mgas: 40, cap: 'execution' }); + expect(sustainable(200, 200, 200)).toEqual({ mgas: 200, cap: 'DA' }); + expect(sustainable(300, 400, 300)).toEqual({ mgas: 300, cap: 'gas limit' }); + expect(sustainable(300, 400, null)).toEqual({ mgas: 300, cap: 'execution' }); + }); + + it('converts proven and sustainable rates with each proven run’s gas per successful tx, and today’s mix with mainnet gas per tx', () => { + swr.result = { data: bench(400, true, { current: { mix: { mgas: 400, atTop: true, run: run(400) }, swap: null, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } } }) }; + const { head } = render(); + // 195 Mgas/s ÷ 2,100 tx/s = 92,857 gas per tx; DA payments ~57.3 Mgas/s. + expect(tile(head, 'Payments (B20)')).toMatch(/≥ 2\.2k\/ssustainable today 617\/s.*capped by DA$/); + expect(tile(head, 'Swaps (Uniswap v3)')).toContain('pendingsustainable today pending'); + // 12.5 Mgas/s ÷ 64.2 tx/s = ~195k gas per tx on mainnet; DA mix ~190 Mgas/s. + expect(tile(head, 'traffic mix')).toMatch(/≥ 2\.1k\/s \(32×\)sustainable today 974\/s \(15×\).*capped by DA/); + expect(tile(head, 'traffic mix')).toContain("At today's mainnet average of ~195k gas per transaction (12.5 Mgas/s ÷ 64 tx/s), not the bench mix."); + }); + + it('caps swaps at the gas limit once their DA is measured above it, with successful tx/s and the reverted note', () => { + const swap = { ...run(400, 'swap'), reverted: 0.155 }; + swr.result = { data: bench(400, true, { current: { mix: null, payment: null, swap: { mgas: 400, atTop: true, run: swap } }, da: { mix: null, payment: null, swap: run(400, 'swap', daResult(2.6158e7, 22.4, false)) } }) }; + expect(tile(render().head, 'Swaps (Uniswap v3)')).toMatch(/≥ 3.6k\/ssustainable today.*capped by gas limit16% reverted/); + expect(tile(render().head, 'Swaps (Uniswap v3)')).toMatch(/sustainable today 1,797\/s.*capped by gas limit/); + expect(render().head).toContain('text-bds-orange-60">sustainable today'); + }); + + it('caps by execution below every other ceiling, without ≥ below the top rate', () => { + swr.result = { data: bench(40, false, { current: { mix: null, swap: null, payment: { mgas: 40, atTop: false, run: run(40, 'payment') } } }) }; + expect(tile(render().head, 'Payments (B20)')).toMatch(/Payments \(B20\)480\/ssustainable today 480\/s.*capped by execution$/); + }); + + it('keeps the mix tile in gas without the snapshot’s transactions per second', () => { + swr.result = { data: bench(400, true) }; + const snapshot = fixture(); + delete snapshot.usage; + const mix = tile(render({ benchUrl: BENCH }, snapshot).head, 'traffic mix'); + expect(mix).toContain('—sustainable today —'); + expect(mix).toContain('stays in gas: today 12.5 Mgas/s'); + }); +}); + +describe('subsystems by workload', () => { + it('lists replay headroom, proof-node latency and archive growth per workload, with — when missing', () => { + swr.result = { data: bench(400, true, { current: { mix: { mgas: 400, atTop: true, run: { ...run(400), storage: storage() } }, swap: null, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } } }) }; + expect(rows(render().subsystems)).toEqual([ + 'Validators7.3× replay headroom—7.3× replay headroom', + 'Proof nodelag p99 4 · executePayload p99 733 ms—lag p99 4 · executePayload p99 733 ms', + 'Disk · archive——≥ 5.0 TB/year at 400 Mgas/s', + ]); + }); +}); + describe('ladder', () => { it('shades the used band in two tones at p50 and p99, notches the fee target, and marks the gas limit and proven rate', () => { swr.result = { data: bench(400, true) }; @@ -191,32 +248,7 @@ describe('what breaks first', () => { }); }); -describe('by workload', () => { - it('cards each workload with its proven rate, DA ceiling, replay headroom and proof-node latency', () => { - swr.result = { data: bench(400, true, { current: { mix: { mgas: 400, atTop: true, run: run(400) }, swap: null, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } } }) }; - const { workloads } = render(); - expect(card(workloads, 'Payments (B20)')).toBe('Payments (B20)≥ 2.1k/sDA~57 · breaks firstValidators7.3×Proof nodelag 4 · 733 msDisk—'); - expect(card(workloads, 'Swaps (Uniswap v3)')).toBe('Swaps (Uniswap v3)pendingDA~115 EstimatedValidators—Proof node—Disk—'); - expect(card(workloads, 'Mainnet mix')).toBe('Mainnet mix≥ 4.2k tx/sbench mixDA190 · breaks firstValidators7.3×Proof nodelag 4 · 733 msDisk—'); - }); - - it('keeps the DA tag when the ceiling is above the proven rate', () => { - swr.result = { data: bench(40, false, { current: { mix: null, swap: null, payment: { mgas: 40, atTop: false, run: run(40, 'payment') } } }) }; - expect(card(render().workloads, 'Payments (B20)')).toContain('420/sDA~57 Estimated'); - }); -}); - describe('reverts, measured swaps and disk', () => { - it('counts successful tx/s on workload cards and notes reverts above 1%', () => { - const reverting = { ...run(200, 'swap'), reverted: 0.155 }; - swr.result = { data: bench(400, true, { current: { mix: null, payment: { mgas: 200, atTop: true, run: { ...run(200, 'payment'), reverted: 0.005 } }, swap: { mgas: 200, atTop: true, run: reverting } } }) }; - const { workloads } = render(); - // 2,100 delivered × 0.845 successful. - expect(card(workloads, 'Swaps (Uniswap v3)')).toContain('Swaps (Uniswap v3)≥ 1.8k/s16% reverted'); - expect(card(workloads, 'Payments (B20)')).toContain('Payments (B20)≥ 2.1k/sDA'); - expect(card(workloads, 'Payments (B20)')).not.toContain('reverted'); - }); - it('ranks a measured swaps DA ceiling by its value, past the gas limit', () => { // 4e10 gas and 2.6158e7 frame bytes put the 14-blob ceiling at ~232 Mgas/s. swr.result = { data: bench(400, true, { da: { mix: null, payment: null, swap: run(400, 'swap', daResult(2.6158e7, 22.4, false)) } }) }; @@ -236,7 +268,7 @@ describe('reverts, measured swaps and disk', () => { expect(full).toBe('—Disk · full node—history window ~9.2 TB + proofs ~2.3 TB at 400 Mgas/s (lower bound); state growth per year from this workload: 10.5 GB, from the mainnet mix run. Bench workloads create little new state, so this undercounts mainnet state growth; runway pending.'); expect(archive).toBe('—Disk · archive node—grows ≥ 5.0 TB/year at 400 Mgas/s (lower bound), from the mainnet mix run'); expect(full).not.toMatch(/years? left/); - expect(card(render().workloads, 'Mainnet mix')).toContain('Diskarchive ≥ 5.0 TB/year'); + expect(rows(render().subsystems)[2]).toBe('Disk · archive——≥ 5.0 TB/year at 400 Mgas/s'); }); it('adds an archive runway only when the current archive size is known, and falls back to the newest other workload', () => { diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 3b4e5ee..5939561 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -26,9 +26,9 @@ it('ends the first screen with DA and guardrails links, and no benchmark links', expect(html).not.toMatch(/As of|manually refreshed|Synthetic fixture: every number/); }); -it('orders the first screen: freshness, headline, ladder, ranked limits, workloads, guardrails, work, links', async () => { +it('orders the first screen: freshness, headline, ladder, ranked limits, subsystems by workload, guardrails, work, links', async () => { const html = renderToStaticMarkup(await ScalingPage()); - const order = ['Usage to 2026-01-02', 'Proven capacity', 'How close are we, and to what?', 'What breaks first', 'By workload', 'Guardrails', 'Work by limit', 'DA by workload'].map((h) => html.indexOf(h)); + const order = ['Usage to 2026-01-02', 'Proven capacity', 'How close are we, and to what?', 'What breaks first', 'Subsystems by workload', 'Guardrails', 'Work by limit', 'DA by workload'].map((h) => html.indexOf(h)); expect(order.every((i, k) => i > (order[k - 1] ?? -1))).toBe(true); expect(html).not.toMatch(/Subsystem detail|Progress<|Ceilings by bottleneck|Usage data as of/); }); From 3f9c6f4295a5eaaadeed0b448ae23ffb8ce6cfd8 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 15:32:41 -0700 Subject: [PATCH 15/33] feat(scaling): rank full-node and archive disk ceilings from runway policy and production sizes --- app/scaling/components/Bottlenecks.tsx | 88 ++++++++++++++------- app/scaling/components/Headline.tsx | 21 ++++- app/scaling/components/Scorecard.tsx | 8 +- app/scaling/components/scorecard.test.tsx | 93 ++++++++++++++++++----- app/scaling/fixtures/synthetic.v2.json | 7 ++ app/scaling/proven/capacity.ts | 12 ++- app/scaling/snapshot.mjs | 36 +++++++++ app/scaling/snapshot.test.ts | 19 +++++ 8 files changed, 232 insertions(+), 52 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index b1447e1..445e2f2 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -6,11 +6,23 @@ import { runHref, type PipelineStep, type Proven, type ProvenCapacity, type Run, import { EXECUTE_PAYLOAD_P99_MS, PROOF_LAG_P99_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; import { Info } from './Info'; import { day } from './ProvenCapacity'; +import type { Snapshot } from '../snapshot.mjs'; import { Tag } from './Tag'; export type Bar = { id: string; label: string; value: number | null; text: string; tag?: DaWorkload['tag']; perSecond?: number }; // One row of the ranked table: a bar, or execution, validators and proof node merged when they share a ceiling. -export type Limit = { id: string; label: string; value: number | null; ceiling: string; evidence: string; tag?: DaWorkload['tag']; perSecond?: number; workload?: DaWorkload }; +export type Limit = { + id: string; + label: string; + value: number | null; + ceiling: string; + evidence: string; + tag?: DaWorkload['tag']; + perSecond?: number; + workload?: DaWorkload; + help?: string; + runwayYears?: number; +}; export const times = (n: number) => `${n < 10 ? n.toFixed(1) : Math.round(n)}×`; const TH = 'px-3 py-2 text-left text-[11px] font-[600] uppercase tracking-wide text-bds-gray-60'; @@ -51,7 +63,7 @@ function daEvidence(w: DaWorkload): string { } // Lowest ceiling first; limits without a ceiling keep their order after the ranked ones. -export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven | null): Limit[] { +export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven | null, disk: Limit[] = []): Limit[] { const da = bars.filter((b) => b.id.startsWith('da-')).map((b): Limit => { const w = workloads.find((x) => `da-${x.payload}` === b.id)!; return { id: b.id, label: b.label, value: b.value, ceiling: `${b.text} ≈ ${fmt(b.perSecond!)}/s`, evidence: daEvidence(w), tag: b.tag, perSecond: b.perSecond, workload: w }; @@ -68,7 +80,7 @@ export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven ceiling: b.value === null ? '—' : atLeast(b.id !== 'execution' || proven!.atTop), evidence: b.value === null ? b.text : b.id === 'execution' && !proven!.atTop ? 'the next rate tested did not pass' : b.text.split(' · ').slice(1).join(' · '), })); - return [...da, ...merged].toSorted((a, b) => (a.value ?? Infinity) - (b.value ?? Infinity)); + return [...da, ...merged, ...disk].toSorted((a, b) => (a.value ?? Infinity) - (b.value ?? Infinity)); } export type StorageSource = { storage: Storage; run: Run }; @@ -87,25 +99,49 @@ export function pickStorage(data: ProvenCapacity | undefined): StorageSource | n return run ? { storage: run.storage!, run } : null; } -// No Mgas/s ceiling: bench workloads create little new state, so a full-node runway from them is not -// meaningful yet. -function diskRows(source: StorageSource | null): { label: string; evidence: string }[] { - if (!source) return [{ label: 'Disk · full node', evidence: 'runway pending' }, { label: 'Disk · archive node', evidence: 'not measured yet' }]; - const { storage: { archive, fullNode, diskBudgetBytes }, run } = source; - const at = `at ${run.mgas} Mgas/s`; - const from = `from the ${WORKLOAD[run.workload]} run`; - const years = archive.currentBytes === null ? null : archive.runwayYears ?? (diskBudgetBytes - archive.currentBytes) / archive.growthBytesPerYear; - const months = years === null ? 0 : Math.max(1, Math.round(years * 12)); - const runway = years === null ? '' - : years <= 0 ? ` · over the ${bytes(diskBudgetBytes)} budget` - : ` · ~${years < 1 ? `${months} month${months === 1 ? '' : 's'}` : `${years.toFixed(1)} years`} left of ${bytes(diskBudgetBytes)} (${archive.runwayBound} bound)`; - return [ - { - label: 'Disk · full node', - evidence: `history window ~${bytes(fullNode.historyFootprintBytes)} + proofs ~${bytes(fullNode.proofFootprintBytes)} ${at} (${fullNode.historyFootprintBound} bound); state growth per year from this workload: ${bytes(fullNode.stateGrowthBytesPerYear)}, ${from}. Bench workloads create little new state, so this undercounts mainnet state growth; runway pending.`, - }, - { label: 'Disk · archive node', evidence: `grows ${growth(archive)} ${at} (${archive.growthBound} bound)${runway}, ${from}` }, - ]; +const YEAR_SECONDS = 31_557_600; +const days = (seconds: number) => Math.round(seconds / 86_400); + +// Disk ceilings: the gas rate at which the budget just holds what each node keeps. Bench bytes per gas +// is a lower bound (the sequencer prunes history), so both ceilings are upper bounds. +export function diskLimits(source: StorageSource | null, { policy, production }: Pick): Limit[] { + const runway = policy?.runwayYears; + const statePerGas = production?.stateBytesPerGas?.value; + const archiveNow = production?.archiveBytes; + const policyHelp = runway + ? ` Runway policy: full node ${runway.fullNode} years, archive ${runway.archive} year${runway.archive === 1 ? '' : 's'}${runway.help ? ` (${runway.help})` : ''}.` + : ' No runway policy is set.'; + const row = (id: string, label: string, need: string | null, mgas: number, years: number | undefined, evidence: string, help: string): Limit => + need !== null + ? { id, label, value: null, ceiling: '—', evidence: `needs ${need}`, help: help + policyHelp } + : mgas <= 0 + ? { id, label, value: 0, ceiling: 'over budget', evidence, help: help + policyHelp, runwayYears: years } + : { id, label, value: mgas, ceiling: `≤ ${fmt(mgas)}`, evidence: `upper bound · ${evidence}`, help: help + policyHelp, runwayYears: years }; + if (!source) { + const need = 'a bench storage-growth run'; + return [row('disk-full', 'Disk · full node', need, 0, undefined, '', ''), row('disk-archive', 'Disk · archive node', need, 0, undefined, '', '')]; + } + const { storage: { diskBudgetBytes: budget, bytesPerGas, current, fullNode }, run } = source; + const from = `the ${WORKLOAD[run.workload]} run`; + + const fullNeed = !runway ? 'a runway policy' : statePerGas === undefined ? 'production state growth' : bytesPerGas.proofs === null ? 'proof storage growth' : null; + const perMgas = fullNeed ? 0 : 1e6 * (bytesPerGas.history * fullNode.historyWindowSeconds + bytesPerGas.proofs! * fullNode.proofWindowSeconds + statePerGas! * YEAR_SECONDS * runway!.fullNode); + const full = row( + 'disk-full', 'Disk · full node', fullNeed, fullNeed ? 0 : (budget - current.state) / perMgas, runway?.fullNode, + `${runway?.fullNode}-year runway · state ${bytes(current.state)} now · history ${days(fullNode.historyWindowSeconds)} d + proofs ${days(fullNode.proofWindowSeconds)} d at ${from}'s bytes/gas · production state growth ${statePerGas} B/gas`, + `The ${bytes(budget)} disk budget must hold today's state, the history window (${days(fullNode.historyWindowSeconds)} d) and proof window (${days(fullNode.proofWindowSeconds)} d) at this rate, and the runway's state growth at production's bytes per gas. History bytes per gas comes from ${from} and is a lower bound, because the sequencer prunes history, so this ceiling is an upper bound.`, + ); + + // storage-growth.json's archive bytes per gas already sums every MDBX table, state included, with + // static files and RocksDB, so production state growth is not added on top. + const archiveNeed = !runway ? 'a runway policy' : !archiveNow ? 'the production archive size' : null; + const archive = row( + 'disk-archive', 'Disk · archive node', archiveNeed, + archiveNeed ? 0 : (budget - archiveNow!.value) / (1e6 * bytesPerGas.archive * YEAR_SECONDS * runway!.archive), runway?.archive, + `${runway?.archive}-year runway · archive ${archiveNow ? bytes(archiveNow.value) : ''} on ${archiveNow?.asOf} (${archiveNow?.source}) · growth at ${from}'s bytes/gas`, + `The ${bytes(budget)} disk budget minus today's archive must cover the runway's growth. Growth per gas comes from ${from} and is a lower bound for that workload, because the sequencer prunes history, so the ceiling is an upper bound. That workload may write more bytes per gas than mainnet traffic does${archiveNow ? ` (Base's whole archive is ${bytes(archiveNow.value)})` : ''}, which makes the ceiling conservative in the other direction.${archiveNow?.note ? ` Archive size: ${archiveNow.note}.` : ''}`, + ); + return [full, archive]; } function Evidence({ proven }: { proven: Proven }) { @@ -128,13 +164,10 @@ function Evidence({ proven }: { proven: Proven }) { ); } -export function Bottlenecks({ limits, max, gasLimit, proven, storage }: { limits: Limit[]; max: number; gasLimit: number | null; proven: Proven | null; storage: StorageSource | null }) { +export function Bottlenecks({ limits, max, gasLimit, proven }: { limits: Limit[]; max: number; gasLimit: number | null; proven: Proven | null }) { const pct = (v: number) => `${Math.min(100, (v / max) * 100)}%`; const ranked = limits.filter((l) => l.value !== null).length; - const fixed = [ - ...diskRows(storage), - { label: 'TEE proving, RPC under sync', evidence: 'not measured yet' }, - ]; + const fixed = [{ label: 'TEE proving, RPC under sync', evidence: 'not measured yet' }]; return (
      What breaks first · Mgas/s @@ -153,6 +186,7 @@ export function Bottlenecks({ limits, max, gasLimit, proven, storage }: { limits {i < ranked ? i + 1 : '—'} {l.label} + {l.help ? <> {l.help} : null} {next ? next : null} diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx index 0d04eb3..6e7e5bf 100644 --- a/app/scaling/components/Headline.tsx +++ b/app/scaling/components/Headline.tsx @@ -150,13 +150,15 @@ function translations({ ladder, usage, bench, proven, limits }: Pick Next bottleneck to solve - {limit.label} at {limit.ceiling.split(' ≈ ')[0]} Mgas/s + {limit.label} {limit.value === 0 ? 'is over budget' : `at ${limit.ceiling.split(' ≈ ')[0]} Mgas/s`} {sub}
      ); @@ -174,14 +176,27 @@ function LastChange({ history }: { history: ProvenCapacity['history'] }) { ); } +// Disk ceilings come from one workload's storage run, so they stay out of the tiles; a note flags one +// that is lower than what a tile calls sustainable. +function diskNote(rows: Translation[], limits: Limit[], gasLimit: number | null): string | null { + const sustained = rows.flatMap((r) => (r.proven ? [sustainable(r.proven.mgas, r.da, gasLimit).mgas] : [])); + const disk = limits.filter((l) => l.id.startsWith('disk-') && l.value !== null).toSorted((a, b) => a.value! - b.value!)[0]; + if (!disk || !sustained.length || disk.value! >= Math.max(...sustained)) return null; + const which = disk.id === 'disk-archive' ? 'Archive' : 'Full-node'; + return disk.value === 0 ? `${which} disk is over budget` : `${which} disk may cap lower (≤ ${fmt(disk.value!)} Mgas/s at ${disk.runwayYears}-year runway)`; +} + export function Headline({ ladder, usage, bench, proven, next, limits }: Props) { const note = benchNote(bench, proven, ladder.qualified); + const rows = translations({ ladder, usage, bench, proven, limits }); + const disk = diskNote(rows, limits, ladder.configuredLimit.value); return (
      - {translations({ ladder, usage, bench, proven, limits }).map((row, i) => )} + {rows.map((row, i) => )}
      + {disk ? {disk} : null} {next ? : null} {bench.data ? : null} {note ? {note} : null} diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index 3153ab1..38cb661 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -3,7 +3,7 @@ import { daWorkloads } from '../da/workloads'; import type { Proven } from '../proven/capacity'; import type { Area, Snapshot } from '../snapshot.mjs'; -import { bottleneckBars, Bottlenecks, pickStorage, rankLimits, type Limit } from './Bottlenecks'; +import { bottleneckBars, Bottlenecks, diskLimits, pickStorage, rankLimits, type Limit } from './Bottlenecks'; import { Guardrails } from './Guardrails'; import { Headline } from './Headline'; import { Ladder, type ProvenMark } from './Ladder'; @@ -18,7 +18,7 @@ function benchMark({ mgas, atTop, run }: Proven): ProvenMark { } const at = (t: number) => `${new Date(t).toISOString().slice(0, 16).replace('T', ' ')} UTC`; -const areaOf = (limit: Limit | null): Area | null => (!limit ? null : limit.id.startsWith('da-') ? 'da' : 'execution'); +const areaOf = (limit: Limit | null): Area | null => (!limit ? null : limit.id.startsWith('da-') ? 'da' : limit.id.startsWith('disk-') ? 'state' : 'execution'); function Freshness({ snapshot, latestAt }: { snapshot: Snapshot; latestAt: number | null }) { const usageWindow = snapshot.usage?.transactionsPerSecond.window ?? snapshot.ladder.observed.p50.window; @@ -41,7 +41,7 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl const qualified = ladder.qualified; const mark = proven ? benchMark(proven) : !benchUrl && qualified.value !== null ? { value: qualified.value, atLeast: false, help: qualified.help } : null; const workloads = daWorkloads(data?.da); - const limits = rankLimits(bottleneckBars(workloads, proven), workloads, proven); + const limits = rankLimits(bottleneckBars(workloads, proven), workloads, proven, diskLimits(pickStorage(data), snapshot)); const next = limits[0]?.value != null ? limits[0] : null; const values = [ladder.observed.p50, ladder.observed.p99, ladder.operatingPoint, ladder.configuredLimit].map((r) => r.value ?? 0); const max = Math.max(mark?.value ?? 0, ladder.configuredLimit.value ?? 0) || Math.max(0, ...values); @@ -50,7 +50,7 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl - + diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index e521112..1df35cd 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -41,8 +41,13 @@ const run = (mgas: number, workload: Run['workload'] = 'mix', da: Run['da'] = nu const none = { mix: null, payment: null, swap: null }; const storage = (currentBytes: number | null = null): NonNullable => ({ diskBudgetBytes: 45e12, + bytesPerGas: { history: 0.0086, proofs: 0.0045, archive: 0.0086 }, + current: { state: 1.1e12 }, archive: { growthBytesPerYear: 5e12, growthBound: 'lower', currentBytes, runwayYears: null, runwayBound: 'upper' }, - fullNode: { stateGrowthBytesPerYear: 1.05e10, historyFootprintBytes: 9.2e12, historyFootprintBound: 'lower', proofFootprintBytes: 2.3e12 }, + fullNode: { + stateGrowthBytesPerYear: 1.05e10, historyWindowSeconds: 2_678_400, historyFootprintBytes: 9.2e12, historyFootprintBound: 'lower', + proofWindowSeconds: 1_296_000, proofFootprintBytes: 2.3e12, + }, }); const bench = (mgas: number, atTop: boolean, more: Partial = {}): ProvenCapacity => ({ current: { ...none, mix: { mgas, atTop, run: run(mgas) } }, @@ -71,8 +76,8 @@ const text = (html: string) => { } return out.replaceAll(''', "'"); }; -// Table rows after a header row (skip 2) or without one (skip 1). -const rows = (html: string, skip = 2) => html.split(' text(` html.replace(/ ?[^<]*<\/span>/g, '').split(' text(` text(` c.includes(label)) ?? '>'}`.replace(/<[^>]*$/, '')); @@ -209,8 +214,8 @@ describe('what breaks first', () => { '—Execution (sequencer)—no proven run', '—Validators—pending', '—Proof node—pending', - '—Disk · full node—runway pending', - '—Disk · archive node—not measured yet', + '—Disk · full node—needs a bench storage-growth run', + '—Disk · archive node—needs a bench storage-growth run', '—TEE proving, RPC under sync—not measured yet', ]); }); @@ -260,22 +265,76 @@ describe('reverts, measured swaps and disk', () => { expect(swapsRow).toMatch(/border-dashed[^"]*" style="left:50%"/); }); - it('shows full-node and archive disk rows from the mix run, with bounds and no full-node runway', () => { - const data = bench(400, true); +}); + +describe('disk ceilings', () => { + const withStorage = (more: Partial = {}) => { + const data = bench(400, true, more); data.current.mix = { mgas: 400, atTop: true, run: { ...run(400), storage: storage() } }; + return data; + }; + const disk = (ranked: string, label: string) => rows(ranked).find((r) => r.includes(label))!; + + it('ranks both disk rows by their upper-bound ceilings among the other limits', () => { + swr.result = { data: withStorage() }; + const { ranked } = render(); + expect(rows(ranked).map((r) => r.slice(0, 26))).toEqual([ + '1DA · Payments (B20)next~5', + '2DA · Swaps~115 ≈ 799/s Es', + '3Disk · archive node≤ 151u', + '4DA · Mainnet mix190 ≈ 981', + '5Execution, validators, pr', + '6Disk · full node≤ 553uppe', + '—TEE proving, RPC under sy', + ]); + // (45 TB − 4 TB) ÷ (1e6 × 0.0086 B/gas × 1 year); (45 TB − 1.1 TB state) ÷ (1e6 × (history 31 d + proofs 15 d + 2 years of 0.0008 B/gas state)). + expect(disk(ranked, 'archive node')).toBe("3Disk · archive node≤ 151upper bound · 1-year runway · archive 4.0 TB on 2026-01-01 (Synthetic archive snapshot) · growth at the mainnet mix run's bytes/gas"); + expect(disk(ranked, 'full node')).toBe("6Disk · full node≤ 553upper bound · 2-year runway · state 1.1 TB now · history 31 d + proofs 15 d at the mainnet mix run's bytes/gas · production state growth 0.0008 B/gas"); + }); + + it('explains the runway policy and why the ceilings are upper bounds in help', () => { + swr.result = { data: withStorage() }; + const help = render().ranked; + expect(help).toContain('Runway policy: full node 2 years, archive 1 year (Invented runway policy.).'); + expect(help).toContain('is a lower bound, because the sequencer prunes history, so this ceiling is an upper bound.'); + expect(help).toContain("may write more bytes per gas than mainnet traffic does (Base's whole archive is 4.0 TB)"); + expect(help).toContain('Archive size: invented size.'); + }); + + it('names the missing input and leaves the row unranked', () => { + swr.result = { data: withStorage() }; + const noPolicy = fixture(); + delete noPolicy.policy; + expect(disk(render({ benchUrl: BENCH }, noPolicy).ranked, 'archive node')).toBe('—Disk · archive node—needs a runway policy'); + expect(disk(render({ benchUrl: BENCH }, noPolicy).ranked, 'full node')).toBe('—Disk · full node—needs a runway policy'); + const noProduction = fixture(); + delete noProduction.production; + expect(disk(render({ benchUrl: BENCH }, noProduction).ranked, 'archive node')).toBe('—Disk · archive node—needs the production archive size'); + expect(disk(render({ benchUrl: BENCH }, noProduction).ranked, 'full node')).toBe('—Disk · full node—needs production state growth'); + const data = withStorage(); + data.current.mix!.run.storage = { ...storage(), bytesPerGas: { history: 0.0086, proofs: null, archive: 0.0086 } }; swr.result = { data }; - const [full, archive] = rows(render().ranked).slice(4, 6); - expect(full).toBe('—Disk · full node—history window ~9.2 TB + proofs ~2.3 TB at 400 Mgas/s (lower bound); state growth per year from this workload: 10.5 GB, from the mainnet mix run. Bench workloads create little new state, so this undercounts mainnet state growth; runway pending.'); - expect(archive).toBe('—Disk · archive node—grows ≥ 5.0 TB/year at 400 Mgas/s (lower bound), from the mainnet mix run'); - expect(full).not.toMatch(/years? left/); - expect(rows(render().subsystems)[2]).toBe('Disk · archive——≥ 5.0 TB/year at 400 Mgas/s'); + expect(disk(render().ranked, 'full node')).toBe('—Disk · full node—needs proof storage growth'); + }); + + it('ranks an archive already over budget first', () => { + swr.result = { data: withStorage() }; + const snapshot = fixture(); + snapshot.production!.archiveBytes!.value = 50e12; + const { head, ranked } = render({ benchUrl: BENCH }, snapshot); + expect(rows(ranked)[0]).toMatch(/^1Disk · archive nodenextover budget1-year runway/); + expect(text(head)).toContain('Next bottleneck to solveDisk · archive node is over budget'); }); - it('adds an archive runway only when the current archive size is known, and falls back to the newest other workload', () => { - swr.result = { data: bench(400, true, { current: { mix: null, payment: null, swap: { mgas: 400, atTop: true, run: { ...run(400, 'swap'), storage: storage(35e12) } } } }) }; - expect(rows(render().ranked).find((r) => r.includes('archive node'))).toBe('—Disk · archive node—grows ≥ 5.0 TB/year at 400 Mgas/s (lower bound) · ~2.0 years left of 45.0 TB (upper bound), from the swaps run'); - swr.result = { data: bench(400, true, { current: { mix: null, payment: null, swap: { mgas: 400, atTop: true, run: { ...run(400, 'swap'), storage: storage(50e12) } } } }) }; - expect(rows(render().ranked).find((r) => r.includes('archive node'))).toContain('(lower bound) · over the 45.0 TB budget, from the swaps run'); + it('notes a disk ceiling below a tile’s sustainable rate under the tiles', () => { + const swapDa = run(400, 'swap', daResult(2.6158e7, 22.4, false)); + swr.result = { data: withStorage({ current: { mix: null, payment: null, swap: { mgas: 400, atTop: true, run: run(400, 'swap') } }, da: { mix: null, payment: null, swap: swapDa } }) }; + // Swaps are sustainable to the 200 Mgas/s gas limit, above the ~151 archive ceiling. + expect(text(render().head)).toContain('Archive disk may cap lower (≤ 151 Mgas/s at 1-year runway)'); + // A half-year archive runway lifts the ceiling to ~302 Mgas/s, above every tile. + const shortRunway = fixture(); + shortRunway.policy!.runwayYears!.archive = 0.5; + expect(text(render({ benchUrl: BENCH }, shortRunway).head)).not.toContain('may cap lower'); }); }); diff --git a/app/scaling/fixtures/synthetic.v2.json b/app/scaling/fixtures/synthetic.v2.json index 0467b15..85d8bfb 100644 --- a/app/scaling/fixtures/synthetic.v2.json +++ b/app/scaling/fixtures/synthetic.v2.json @@ -196,6 +196,13 @@ }, "gasPerSwap": null }, + "policy": { + "runwayYears": { "fullNode": 2, "archive": 1, "help": "Invented runway policy." } + }, + "production": { + "archiveBytes": { "value": 4000000000000, "asOf": "2026-01-01", "source": "Synthetic archive snapshot", "note": "invented size" }, + "stateBytesPerGas": { "value": 0.0008, "window": "week", "source": "Synthetic dashboard" } + }, "usage": { "transactionsPerSecond": { "value": 64.2, diff --git a/app/scaling/proven/capacity.ts b/app/scaling/proven/capacity.ts index 8033162..c98ab70 100644 --- a/app/scaling/proven/capacity.ts +++ b/app/scaling/proven/capacity.ts @@ -40,8 +40,18 @@ export type Da = { result: DaResult; target: number | null; withinTarget: boolea // `storage-growth.json`: disk growth measured over the run, projected to a year at its gas rate. export type Storage = { diskBudgetBytes: number; + // Proofs are null when the proofs disk usage could not be measured. + bytesPerGas: { history: number; proofs: number | null; archive: number }; + current: { state: number }; archive: { growthBytesPerYear: number; growthBound: string; currentBytes: number | null; runwayYears: number | null; runwayBound: string }; - fullNode: { stateGrowthBytesPerYear: number; historyFootprintBytes: number; historyFootprintBound: string; proofFootprintBytes: number }; + fullNode: { + stateGrowthBytesPerYear: number; + historyWindowSeconds: number; + historyFootprintBytes: number; + historyFootprintBound: string; + proofWindowSeconds: number; + proofFootprintBytes: number; + }; }; export type Run = { diff --git a/app/scaling/snapshot.mjs b/app/scaling/snapshot.mjs index 35c3198..ffaa41d 100644 --- a/app/scaling/snapshot.mjs +++ b/app/scaling/snapshot.mjs @@ -46,6 +46,11 @@ * ladder: Ladder, ceilings: Ceiling[], guardrails: Guardrail[], * translations: { gasPerPayment: Constant | null, gasPerSwap: Constant | null }, * usage?: { transactionsPerSecond: Usage }, + * policy?: { runwayYears?: { fullNode: number, archive: number, help?: string, source?: Link } }, + * production?: { + * archiveBytes?: { value: number, asOf: string, source: string, note?: string }, + * stateBytesPerGas?: { value: number, window?: string, source?: string } + * }, * sections: Section[], work: WorkItem[] * }} Snapshot * @typedef {{ value: number, window: string, help: string, source?: Link }} Usage @@ -224,6 +229,37 @@ export function parseSnapshot(data) { if (tps.source !== undefined) link(tps.source, `${path}.source`); } + // Optional disk inputs: without them the disk rows say which input they need instead of a ceiling. + const positive = (o, key, path) => { + if (!(Number.isFinite(o[key]) && o[key] > 0)) fail(`${path}.${key}`, 'expected a positive number'); + }; + if (data.policy !== undefined) { + const policy = obj(data, 'policy', 'snapshot'); + if (policy.runwayYears !== undefined) { + const runway = obj(policy, 'runwayYears', 'policy'); + positive(runway, 'fullNode', 'policy.runwayYears'); + positive(runway, 'archive', 'policy.runwayYears'); + if (runway.help !== undefined) str(runway, 'help', 'policy.runwayYears'); + if (runway.source !== undefined) link(runway.source, 'policy.runwayYears.source'); + } + } + if (data.production !== undefined) { + const production = obj(data, 'production', 'snapshot'); + if (production.archiveBytes !== undefined) { + const archive = obj(production, 'archiveBytes', 'production'); + positive(archive, 'value', 'production.archiveBytes'); + date(archive, 'asOf', 'production.archiveBytes'); + str(archive, 'source', 'production.archiveBytes'); + if (archive.note !== undefined) str(archive, 'note', 'production.archiveBytes'); + } + if (production.stateBytesPerGas !== undefined) { + const state = obj(production, 'stateBytesPerGas', 'production'); + positive(state, 'value', 'production.stateBytesPerGas'); + if (state.window !== undefined && !Object.hasOwn(windows, state.window)) fail('production.stateBytesPerGas.window', 'expected a key of windows'); + if (state.source !== undefined) str(state, 'source', 'production.stateBytesPerGas'); + } + } + const metric = (m, path) => { if (!isObj(m)) return fail(path, 'expected object'); str(m, 'label', path); diff --git a/app/scaling/snapshot.test.ts b/app/scaling/snapshot.test.ts index c23c815..457cd2a 100644 --- a/app/scaling/snapshot.test.ts +++ b/app/scaling/snapshot.test.ts @@ -35,6 +35,25 @@ describe('parseSnapshot', () => { ]); }); + it('treats the disk policy and production inputs as optional, and checks them when present', () => { + const { policy, production, ...without } = fixture(); + expect(errorsOf(without)).toEqual([]); + const bad = { + ...without, + policy: { runwayYears: { ...policy.runwayYears, fullNode: 0 } }, + production: { + archiveBytes: { ...production.archiveBytes, asOf: 'soon', source: '' }, + stateBytesPerGas: { ...production.stateBytesPerGas, window: 'decade' }, + }, + }; + expect(errorsOf(bad)).toEqual([ + 'policy.runwayYears.fullNode: expected a positive number', + 'production.archiveBytes.asOf: expected ISO timestamp', + 'production.archiveBytes.source: expected non-empty string', + 'production.stateBytesPerGas.window: expected a key of windows', + ]); + }); + it('rejects schema version 1 and names the migration', () => { const errors = errorsOf({ ...fixture(), schemaVersion: 1 }); expect(errors).toHaveLength(1); From abe1116d7d906bcd7493fd6414061553390bf327 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 15:40:12 -0700 Subject: [PATCH 16/33] feat(scaling): show headroom to the first limit for today's traffic on a today-to-proven span --- app/scaling/components/Headline.tsx | 77 ++++++++++++++++++----- app/scaling/components/scorecard.test.tsx | 64 +++++++++++++++++-- 2 files changed, 118 insertions(+), 23 deletions(-) diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx index 6e7e5bf..7673e75 100644 --- a/app/scaling/components/Headline.tsx +++ b/app/scaling/components/Headline.tsx @@ -30,15 +30,34 @@ function benchNote({ configured, data, error }: BenchState, proven: ProvenMark, return data.current.mix ? partial || null : `No main run passed in the last 14 days. ${partial}`.trim(); } -function Capacity({ ladder, usage, proven }: Pick) { - const { p50, p99 } = ladder.observed; +export type FirstLimit = { mgas: number; bound: '≤' | '≥' | ''; name: string; reason: string }; + +// The lowest limit that applies to today's traffic mix. Payment- and swap-specific DA rows do not; +// on ties the earlier entry wins, so an exact limit beats a bound. +export function firstLimit(limits: Limit[], gasLimit: number | null, proven: ProvenMark): FirstLimit | null { + const value = (id: string) => limits.find((l) => l.id === id)?.value ?? null; + const candidates: (FirstLimit | null)[] = [ + value('da-mix') === null ? null : { mgas: value('da-mix')!, bound: '', name: 'mix DA', reason: '' }, + gasLimit === null ? null : { mgas: gasLimit, bound: '', name: 'gas limit', reason: '' }, + value('disk-archive') === null ? null : { mgas: value('disk-archive')!, bound: '≤', name: 'archive disk', reason: '≤ because the archive disk ceiling is an upper bound.' }, + value('disk-full') === null ? null : { mgas: value('disk-full')!, bound: '≤', name: 'full-node disk', reason: '≤ because the full-node disk ceiling is an upper bound.' }, + !proven ? null : { + mgas: proven.value, + bound: proven.atLeast ? '≥' : '', + name: 'proven', + reason: proven.atLeast ? `≥ because ${n(proven.value)} Mgas/s is the highest rate tested, a floor.` : '', + }, + ]; + return candidates.reduce((low, c) => (c && (!low || c.mgas < low.mgas) ? c : low), null); +} + +export const headroomLabel = (limit: FirstLimit, today: number) => `${limit.bound ? `${limit.bound} ` : ''}${times(limit.mgas / today)}`; + +function Capacity({ ladder, usage, proven, limit }: Pick & { limit: FirstLimit | null }) { + const today = ladder.observed.p50.value; const tps = usage?.transactionsPerSecond; - const headroom = proven && p50.value ? proven.value / p50.value : null; - const caption = [ - p50.value !== null && `today ${n(p50.value)}${tps ? ` (${fmt(tps.value)} tx/s)` : ''}`, - p99.value !== null && `p99 ${n(p99.value)}`, - headroom !== null && times(headroom), - ].filter(Boolean); + const end = Math.max(proven?.value ?? 0, limit?.mgas ?? 0, today ?? 0); + const pct = (v: number) => (end > 0 ? Math.min(100, Math.max(0, (v / end) * 100)) : 0); return (
      @@ -49,10 +68,32 @@ function Capacity({ ladder, usage, proven }: PickMgas/s - - {caption.join(' · ')} - {tps ? {tps.help} : null} - + {today !== null && limit ? ( +
      +
      +
      + + {headroomLabel(limit, today)} + First limit for today's traffic mix ÷ today's median gas rate. {limit.reason} + + + + +
      +
      + + today {n(today)} + {tps ? · {fmt(tps.value)} tx/s : null} + {tps ? <> {tps.help} : null} + + {limit.name} {limit.bound ? `${limit.bound} ` : ''}{fmt(limit.mgas)} +
      +
      + ) : null}
      ); } @@ -82,15 +123,16 @@ const VALUE = 'whitespace-nowrap leading-[44px] tabular-nums'; const CAP_TONE: Record = { DA: 'text-bds-red-60', 'gas limit': 'text-bds-orange-60', execution: '' }; const compact = (v: number) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(v).toLowerCase(); -function Tile({ row: r, gasLimit }: { row: Translation; gasLimit: number | null }) { +// The traffic tile repeats the headroom multiple only when its sustainable rate is the same first limit. +function Tile({ row: r, gasLimit, headroomMgas }: { row: Translation; gasLimit: number | null; headroomMgas: number | null }) { const s = r.proven && sustainable(r.proven.mgas, r.da, gasLimit); const tx = (mgas: number) => (mgas * 1e6) / r.gasPerTx!; - const vs = (v: number) => (r.vsToday && r.today ? ({times(v / r.today)}) : null); + const vs = (v: number) => (r.vsToday && r.today && s?.mgas === headroomMgas ? ({times(v / r.today)}) : null); return (
      {r.label} - {!r.proven ? 'pending' : r.gasPerTx === null ? '—' : <>{r.proven.atLeast ? '≥ ' : ''}{compact(tx(r.proven.mgas))}/s{vs(tx(r.proven.mgas))}} + {!r.proven ? 'pending' : r.gasPerTx === null ? '—' : <>{r.proven.atLeast ? '≥ ' : ''}{compact(tx(r.proven.mgas))}/s} sustainable today {!s ? 'pending' : r.gasPerTx === null ? '—' : <>{fmt(tx(s.mgas))}/s{vs(tx(s.mgas))}} @@ -190,11 +232,12 @@ export function Headline({ ladder, usage, bench, proven, next, limits }: Props) const note = benchNote(bench, proven, ladder.qualified); const rows = translations({ ladder, usage, bench, proven, limits }); const disk = diskNote(rows, limits, ladder.configuredLimit.value); + const limit = firstLimit(limits, ladder.configuredLimit.value, proven); return (
      -
      - {rows.map((row, i) => )} +
      + {rows.map((row, i) => )}
      {disk ? {disk} : null} {next ? : null} diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 1df35cd..9332de4 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -6,7 +6,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ProvenCapacity, Run } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; -import { sustainable } from './Headline'; +import type { Limit } from './Bottlenecks'; +import { firstLimit, headroomLabel, sustainable } from './Headline'; +import type { ProvenMark } from './Ladder'; import { Scorecard } from './Scorecard'; // Synthetic data only: real bench results and snapshots are internal and never committed. @@ -79,7 +81,7 @@ const text = (html: string) => { // Table rows after a header row (skip 2) or without one (skip 1), without the mocked help popovers. const rows = (html: string, skip = 2) => html.replace(/ ?[^<]*<\/span>/g, '').split(' text(` text(` c.includes(label)) ?? '>'}`.replace(/<[^>]*$/, '')); +const tile = (head: string, label: string) => text(` c.includes(label)) ?? '>'}`.replace(/<[^>]*$/, '')); beforeEach(() => { swr.result = {}; @@ -97,10 +99,13 @@ describe('freshness and headline', () => { it('heads with proven capacity, today and headroom, and the next bottleneck with its last change', () => { swr.result = { data: bench(400, true) }; const { head } = render(); - expect(text(head)).toContain('≥ 400Mgas/stoday 12.5 (64 tx/s) · 32×'); + // First limit for today's mix is the ~190 Mgas/s mix DA ceiling: 189.7 ÷ 12.5 = 15×. + expect(text(head)).toContain("≥ 400Mgas/s15×First limit for today's traffic mix ÷ today's median gas rate. today 12.5 · 64 tx/s"); + expect(text(head)).toContain('mix DA 190Payments (B20)'); + expect(text(head)).not.toMatch(/32×|p99/); expect(text(head)).toContain('Next bottleneck to solveDA · Payments (B20) at ~57 Mgas/s≈ 1,517 payments/s'); expect(text(head)).toContain('↑ from 40 Mgas/s on 2026-01-06 · compare'); - expect(text(head)).not.toMatch(/First limit|Payments per second|In transactions/); + expect(text(head)).not.toMatch(/First limit:|Payments per second|In transactions/); }); it('adds blobs per slot to the callout once the binding DA workload has a bench run', () => { @@ -113,7 +118,8 @@ describe('freshness and headline', () => { delete snapshot.usage; snapshot.ladder.qualified = { ...snapshot.ladder.qualified, value: 30, tag: 'measured', evidence: [{ label: 'Synthetic run', url: 'https://example.com/scaling/runs/bench/synthetic-1' }] }; const head = text(render({}, snapshot).head); - expect(head).toContain('30Mgas/stoday 12.5 · 2.4×'); + expect(head).toContain('30Mgas/s2.4×'); + expect(head).toContain('today 12.5proven 30'); expect(head).toContain('Proven capacity from the snapshot · Synthetic run'); expect(text(render().head)).toContain('Loading bench results…'); swr.result = { error: new TypeError('Failed to fetch'), data: bench(400, true) }; @@ -143,7 +149,8 @@ describe('workload tiles', () => { expect(tile(head, 'Payments (B20)')).toMatch(/≥ 2\.2k\/ssustainable today 617\/s.*capped by DA$/); expect(tile(head, 'Swaps (Uniswap v3)')).toContain('pendingsustainable today pending'); // 12.5 Mgas/s ÷ 64.2 tx/s = ~195k gas per tx on mainnet; DA mix ~190 Mgas/s. - expect(tile(head, 'traffic mix')).toMatch(/≥ 2\.1k\/s \(32×\)sustainable today 974\/s \(15×\).*capped by DA/); + // The traffic tile repeats the 15× headroom because its sustainable rate is the same mix DA limit. + expect(tile(head, 'traffic mix')).toMatch(/≥ 2\.1k\/ssustainable today 974\/s \(15×\).*capped by DA/); expect(tile(head, 'traffic mix')).toContain("At today's mainnet average of ~195k gas per transaction (12.5 Mgas/s ÷ 64 tx/s), not the bench mix."); }); @@ -170,6 +177,51 @@ describe('workload tiles', () => { }); }); +describe('headroom', () => { + const limit = (id: string, value: number): Limit => ({ id, label: id, value, ceiling: '', evidence: '' }); + const proven = (value: number, atLeast: boolean): ProvenMark => ({ value, atLeast, help: '' }); + + it('takes the lowest limit that applies to today’s traffic, with its bound', () => { + const mix = [limit('da-payment', 57), limit('da-mix', 190), limit('disk-archive', 150), limit('disk-full', 568)]; + expect(firstLimit(mix, 200, proven(400, true))).toMatchObject({ mgas: 150, bound: '≤', name: 'archive disk' }); + expect(firstLimit([limit('da-mix', 190)], 200, proven(400, true))).toMatchObject({ mgas: 190, bound: '', name: 'mix DA' }); + expect(firstLimit([limit('da-mix', 250)], 200, proven(400, true))).toMatchObject({ mgas: 200, bound: '', name: 'gas limit' }); + expect(firstLimit([limit('da-mix', 250)], 200, proven(100, true))).toMatchObject({ mgas: 100, bound: '≥', name: 'proven' }); + expect(firstLimit([limit('da-mix', 250)], null, proven(300, false))).toMatchObject({ mgas: 250, name: 'mix DA' }); + expect(firstLimit([], null, null)).toBeNull(); + }); + + it('formats the multiple with one decimal below 10 and the limit’s bound', () => { + expect(headroomLabel({ mgas: 150, bound: '≤', name: 'archive disk', reason: '' }, 18.7)).toBe('≤ 8.0×'); + expect(headroomLabel({ mgas: 400, bound: '≥', name: 'proven', reason: '' }, 18.7)).toBe('≥ 21×'); + expect(headroomLabel({ mgas: 190, bound: '', name: 'mix DA', reason: '' }, 18.7)).toBe('10×'); + }); + + it('places today, the first limit and the headroom label on a 0–proven span, and explains an upper bound', () => { + const data = bench(400, true); + data.current.mix = { mgas: 400, atTop: true, run: { ...run(400), storage: storage() } }; + swr.result = { data }; + const { head } = render(); + // Today 12.5 and the ~151 archive ceiling on a 0–400 span; the label sits halfway between. + expect(head).toMatch(/data-span="today"[^>]*left:3\.125%/); + expect(head).toMatch(/data-span="limit"[^>]*left:37\.76\d*%/); + expect(head).toMatch(/data-span="headroom"[^>]*left:20\.44\d*%/); + expect(text(head)).toContain("≤ 12×First limit for today's traffic mix ÷ today's median gas rate. ≤ because the archive disk ceiling is an upper bound."); + expect(text(head)).toContain('archive disk ≤ 151'); + // The traffic tile's sustainable rate (mix DA) is not the first limit, so it shows no multiple. + expect(tile(head, 'traffic mix')).toMatch(/sustainable today 974\/sHighest/); + }); + + it('hides the span without today’s median', () => { + swr.result = { data: bench(400, true) }; + const snapshot = fixture(); + snapshot.ladder.observed.p50 = { ...snapshot.ladder.observed.p50, value: null, tag: 'not-measured' }; + const { head } = render({ benchUrl: BENCH }, snapshot); + expect(head).not.toContain('data-span'); + expect(text(head)).toContain('≥ 400Mgas/s'); + }); +}); + describe('subsystems by workload', () => { it('lists replay headroom, proof-node latency and archive growth per workload, with — when missing', () => { swr.result = { data: bench(400, true, { current: { mix: { mgas: 400, atTop: true, run: { ...run(400), storage: storage() } }, swap: null, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } } }) }; From a3e5163c29109e779ff1b4e899917b6fc6aedae0 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 15:49:07 -0700 Subject: [PATCH 17/33] feat(scaling): add mainnet state growth to the archive ceiling and show proven over today on a header span --- app/scaling/components/Bottlenecks.tsx | 13 ++- app/scaling/components/Headline.tsx | 117 ++++++++++------------ app/scaling/components/scorecard.test.tsx | 88 ++++++++-------- 3 files changed, 103 insertions(+), 115 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index 445e2f2..71a34d2 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -132,14 +132,17 @@ export function diskLimits(source: StorageSource | null, { policy, production }: `The ${bytes(budget)} disk budget must hold today's state, the history window (${days(fullNode.historyWindowSeconds)} d) and proof window (${days(fullNode.proofWindowSeconds)} d) at this rate, and the runway's state growth at production's bytes per gas. History bytes per gas comes from ${from} and is a lower bound, because the sequencer prunes history, so this ceiling is an upper bound.`, ); - // storage-growth.json's archive bytes per gas already sums every MDBX table, state included, with - // static files and RocksDB, so production state growth is not added on top. + // storage-growth.json's archive bytes per gas sums every MDBX table, state included, with static files + // and RocksDB. Bench runs write almost no new state, so production state growth is added on top; that + // double counts the bench run's tiny state growth. const archiveNeed = !runway ? 'a runway policy' : !archiveNow ? 'the production archive size' : null; + const archivePerGas = bytesPerGas.archive + (statePerGas ?? 0); + const stateBasis = statePerGas === undefined ? 'bench state only' : `+ mainnet state growth ${statePerGas} B/gas`; const archive = row( 'disk-archive', 'Disk · archive node', archiveNeed, - archiveNeed ? 0 : (budget - archiveNow!.value) / (1e6 * bytesPerGas.archive * YEAR_SECONDS * runway!.archive), runway?.archive, - `${runway?.archive}-year runway · archive ${archiveNow ? bytes(archiveNow.value) : ''} on ${archiveNow?.asOf} (${archiveNow?.source}) · growth at ${from}'s bytes/gas`, - `The ${bytes(budget)} disk budget minus today's archive must cover the runway's growth. Growth per gas comes from ${from} and is a lower bound for that workload, because the sequencer prunes history, so the ceiling is an upper bound. That workload may write more bytes per gas than mainnet traffic does${archiveNow ? ` (Base's whole archive is ${bytes(archiveNow.value)})` : ''}, which makes the ceiling conservative in the other direction.${archiveNow?.note ? ` Archive size: ${archiveNow.note}.` : ''}`, + archiveNeed ? 0 : (budget - archiveNow!.value) / (1e6 * archivePerGas * YEAR_SECONDS * runway!.archive), runway?.archive, + `${runway?.archive}-year runway · archive ${archiveNow ? bytes(archiveNow.value) : ''} on ${archiveNow?.asOf} (${archiveNow?.source}) · growth at ${from}'s bytes/gas ${stateBasis}`, + `The ${bytes(budget)} disk budget minus today's archive must cover the runway's growth. ${statePerGas === undefined ? 'Without production state growth, only the bench run\'s state growth is counted.' : 'The rate adds mainnet state growth; the bench run writes almost no new state.'} Growth per gas comes from ${from} and is a lower bound for that workload, because the sequencer prunes history, so the ceiling is an upper bound. That workload may write more bytes per gas than mainnet traffic does${archiveNow ? ` (Base's whole archive is ${bytes(archiveNow.value)})` : ''}, which makes the ceiling conservative in the other direction.${archiveNow?.note ? ` Archive size: ${archiveNow.note}.` : ''}`, ); return [full, archive]; } diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx index 7673e75..01e7776 100644 --- a/app/scaling/components/Headline.tsx +++ b/app/scaling/components/Headline.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from 'react'; +import { useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'; import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; @@ -30,36 +30,39 @@ function benchNote({ configured, data, error }: BenchState, proven: ProvenMark, return data.current.mix ? partial || null : `No main run passed in the last 14 days. ${partial}`.trim(); } -export type FirstLimit = { mgas: number; bound: '≤' | '≥' | ''; name: string; reason: string }; - -// The lowest limit that applies to today's traffic mix. Payment- and swap-specific DA rows do not; -// on ties the earlier entry wins, so an exact limit beats a bound. -export function firstLimit(limits: Limit[], gasLimit: number | null, proven: ProvenMark): FirstLimit | null { - const value = (id: string) => limits.find((l) => l.id === id)?.value ?? null; - const candidates: (FirstLimit | null)[] = [ - value('da-mix') === null ? null : { mgas: value('da-mix')!, bound: '', name: 'mix DA', reason: '' }, - gasLimit === null ? null : { mgas: gasLimit, bound: '', name: 'gas limit', reason: '' }, - value('disk-archive') === null ? null : { mgas: value('disk-archive')!, bound: '≤', name: 'archive disk', reason: '≤ because the archive disk ceiling is an upper bound.' }, - value('disk-full') === null ? null : { mgas: value('disk-full')!, bound: '≤', name: 'full-node disk', reason: '≤ because the full-node disk ceiling is an upper bound.' }, - !proven ? null : { - mgas: proven.value, - bound: proven.atLeast ? '≥' : '', - name: 'proven', - reason: proven.atLeast ? `≥ because ${n(proven.value)} Mgas/s is the highest rate tested, a floor.` : '', - }, - ]; - return candidates.reduce((low, c) => (c && (!low || c.mgas < low.mgas) ? c : low), null); -} +// "≥ 21×" when proven is the top rate tested, a floor. +export const headroomLabel = (proven: NonNullable, today: number) => `${proven.atLeast ? '≥ ' : ''}${times(proven.value / today)}`; -export const headroomLabel = (limit: FirstLimit, today: number) => `${limit.bound ? `${limit.bound} ` : ''}${times(limit.mgas / today)}`; +export type Placement = { side: 'center' | 'left' | 'right'; x: number }; + +// Centers a label on x, or pins it to the nearer edge when centering would overflow. `estimate` is an +// approximate label width; a centered label renders with translateX(-50%), so it stays exact. +export function place(x: number, estimate: number, width: number): Placement { + if (x - estimate / 2 < 0) return { side: 'left', x: 0 }; + if (x + estimate / 2 > width) return { side: 'right', x: width }; + return { side: 'center', x }; +} -function Capacity({ ladder, usage, proven, limit }: Pick & { limit: FirstLimit | null }) { +const placed = (p: Placement): CSSProperties => + p.side === 'left' ? { left: 0 } : p.side === 'right' ? { right: 0 } : { left: p.x, transform: 'translateX(-50%)' }; + +function Capacity({ ladder, usage, proven }: Pick) { + const ref = useRef(null); + const [width, setWidth] = useState(240); + useLayoutEffect(() => { + if (!ref.current) return; + const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width)); + observer.observe(ref.current); + return () => observer.disconnect(); + }, []); const today = ladder.observed.p50.value; const tps = usage?.transactionsPerSecond; - const end = Math.max(proven?.value ?? 0, limit?.mgas ?? 0, today ?? 0); - const pct = (v: number) => (end > 0 ? Math.min(100, Math.max(0, (v / end) * 100)) : 0); + const span = proven && today !== null && today > 0 ? proven : null; + const todayX = span ? Math.min(width, (today! / span.value) * width) : 0; + const headroom = span ? headroomLabel(span, today!) : ''; + const todayText = `today ${today === null ? '' : n(today)}${tps ? ` · ${fmt(tps.value)} tx/s` : ''}`; return ( -
      +
      Proven capacity Execution, validators and proof node all pass at this rate on the mainnet snapshot. Workload tiles count successful transactions per second. @@ -68,30 +71,28 @@ function Capacity({ ladder, usage, proven, limit }: PickMgas/s - {today !== null && limit ? ( -
      -
      -
      - - {headroomLabel(limit, today)} - First limit for today's traffic mix ÷ today's median gas rate. {limit.reason} - - - - -
      -
      - - today {n(today)} - {tps ? · {fmt(tps.value)} tx/s : null} - {tps ? <> {tps.help} : null} - - {limit.name} {limit.bound ? `${limit.bound} ` : ''}{fmt(limit.mgas)} -
      + {span ? ( +
      + + {headroom} + Proven capacity ÷ today's median gas rate. What breaks first lists the limits that apply before proven capacity. + +
      +
      + + + + today {n(today!)} + {tps ? · {fmt(tps.value)} tx/s : null} + {tps ? {tps.help} : null} + + + proven {span.atLeast ? '≥ ' : ''}{n(span.value)} +
      ) : null}
      @@ -101,12 +102,10 @@ function Capacity({ ladder, usage, proven, limit }: Pick = { DA: 'text-bds-red-60', 'gas limit': 'text-bds-orange-60', execution: '' }; const compact = (v: number) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(v).toLowerCase(); -// The traffic tile repeats the headroom multiple only when its sustainable rate is the same first limit. -function Tile({ row: r, gasLimit, headroomMgas }: { row: Translation; gasLimit: number | null; headroomMgas: number | null }) { +function Tile({ row: r, gasLimit }: { row: Translation; gasLimit: number | null }) { const s = r.proven && sustainable(r.proven.mgas, r.da, gasLimit); const tx = (mgas: number) => (mgas * 1e6) / r.gasPerTx!; - const vs = (v: number) => (r.vsToday && r.today && s?.mgas === headroomMgas ? ({times(v / r.today)}) : null); return (
      {r.label} @@ -135,7 +132,7 @@ function Tile({ row: r, gasLimit, headroomMgas }: { row: Translation; gasLimit: {!r.proven ? 'pending' : r.gasPerTx === null ? '—' : <>{r.proven.atLeast ? '≥ ' : ''}{compact(tx(r.proven.mgas))}/s} - sustainable today {!s ? 'pending' : r.gasPerTx === null ? '—' : <>{fmt(tx(s.mgas))}/s{vs(tx(s.mgas))}} + sustainable today {!s ? 'pending' : r.gasPerTx === null ? '—' : `${fmt(tx(s.mgas))}/s`} Highest rate we can run continuously now: below the L1 blob target and within the current gas limit{gasLimit === null ? '' : ` (${n(gasLimit)} Mgas/s)`}. Capped by DA or the gas limit, not execution. @@ -162,7 +159,6 @@ function translations({ ladder, usage, bench, proven, limits }: PickToday's traffic mix {mixHelp}, - today: tps, gasPerTx, proven: proven && { mgas: proven.value, atLeast: proven.atLeast }, da: daOf('mix'), - vsToday: true, }, ]; } @@ -232,12 +226,11 @@ export function Headline({ ladder, usage, bench, proven, next, limits }: Props) const note = benchNote(bench, proven, ladder.qualified); const rows = translations({ ladder, usage, bench, proven, limits }); const disk = diskNote(rows, limits, ladder.configuredLimit.value); - const limit = firstLimit(limits, ladder.configuredLimit.value, proven); return (
      -
      - {rows.map((row, i) => )} +
      + {rows.map((row, i) => )}
      {disk ? {disk} : null} {next ? : null} diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 9332de4..ebbdc18 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -6,9 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ProvenCapacity, Run } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; -import type { Limit } from './Bottlenecks'; -import { firstLimit, headroomLabel, sustainable } from './Headline'; -import type { ProvenMark } from './Ladder'; +import { headroomLabel, place, sustainable } from './Headline'; import { Scorecard } from './Scorecard'; // Synthetic data only: real bench results and snapshots are internal and never committed. @@ -99,10 +97,10 @@ describe('freshness and headline', () => { it('heads with proven capacity, today and headroom, and the next bottleneck with its last change', () => { swr.result = { data: bench(400, true) }; const { head } = render(); - // First limit for today's mix is the ~190 Mgas/s mix DA ceiling: 189.7 ÷ 12.5 = 15×. - expect(text(head)).toContain("≥ 400Mgas/s15×First limit for today's traffic mix ÷ today's median gas rate. today 12.5 · 64 tx/s"); - expect(text(head)).toContain('mix DA 190Payments (B20)'); - expect(text(head)).not.toMatch(/32×|p99/); + // 400 proven ÷ 12.5 today = 32×, a floor because 400 is the top rate tested. + expect(text(head)).toContain("≥ 400Mgas/s≥ 32×Proven capacity ÷ today's median gas rate. What breaks first lists the limits that apply before proven capacity.today 12.5· 64 tx/s"); + expect(text(head)).toContain('proven ≥ 400Payments (B20)'); + expect(text(head)).not.toMatch(/p99|mix DA|archive disk ≤/); expect(text(head)).toContain('Next bottleneck to solveDA · Payments (B20) at ~57 Mgas/s≈ 1,517 payments/s'); expect(text(head)).toContain('↑ from 40 Mgas/s on 2026-01-06 · compare'); expect(text(head)).not.toMatch(/First limit:|Payments per second|In transactions/); @@ -149,8 +147,9 @@ describe('workload tiles', () => { expect(tile(head, 'Payments (B20)')).toMatch(/≥ 2\.2k\/ssustainable today 617\/s.*capped by DA$/); expect(tile(head, 'Swaps (Uniswap v3)')).toContain('pendingsustainable today pending'); // 12.5 Mgas/s ÷ 64.2 tx/s = ~195k gas per tx on mainnet; DA mix ~190 Mgas/s. - // The traffic tile repeats the 15× headroom because its sustainable rate is the same mix DA limit. - expect(tile(head, 'traffic mix')).toMatch(/≥ 2\.1k\/ssustainable today 974\/s \(15×\).*capped by DA/); + // Tiles carry no multiples; the header owns the one multiple. + expect(tile(head, 'traffic mix')).toMatch(/≥ 2\.1k\/ssustainable today 974\/sHighest.*capped by DA/); + expect(head.split('rounded-lg border').slice(1).join('')).not.toContain('×'); expect(tile(head, 'traffic mix')).toContain("At today's mainnet average of ~195k gas per transaction (12.5 Mgas/s ÷ 64 tx/s), not the bench mix."); }); @@ -177,48 +176,33 @@ describe('workload tiles', () => { }); }); -describe('headroom', () => { - const limit = (id: string, value: number): Limit => ({ id, label: id, value, ceiling: '', evidence: '' }); - const proven = (value: number, atLeast: boolean): ProvenMark => ({ value, atLeast, help: '' }); - - it('takes the lowest limit that applies to today’s traffic, with its bound', () => { - const mix = [limit('da-payment', 57), limit('da-mix', 190), limit('disk-archive', 150), limit('disk-full', 568)]; - expect(firstLimit(mix, 200, proven(400, true))).toMatchObject({ mgas: 150, bound: '≤', name: 'archive disk' }); - expect(firstLimit([limit('da-mix', 190)], 200, proven(400, true))).toMatchObject({ mgas: 190, bound: '', name: 'mix DA' }); - expect(firstLimit([limit('da-mix', 250)], 200, proven(400, true))).toMatchObject({ mgas: 200, bound: '', name: 'gas limit' }); - expect(firstLimit([limit('da-mix', 250)], 200, proven(100, true))).toMatchObject({ mgas: 100, bound: '≥', name: 'proven' }); - expect(firstLimit([limit('da-mix', 250)], null, proven(300, false))).toMatchObject({ mgas: 250, name: 'mix DA' }); - expect(firstLimit([], null, null)).toBeNull(); +describe('headroom span', () => { + it('divides proven capacity by today’s median, marking a floor with ≥', () => { + expect(headroomLabel({ value: 400, atLeast: true, help: '' }, 18.67)).toBe('≥ 21×'); + expect(headroomLabel({ value: 80, atLeast: false, help: '' }, 18.67)).toBe('4.3×'); }); - it('formats the multiple with one decimal below 10 and the limit’s bound', () => { - expect(headroomLabel({ mgas: 150, bound: '≤', name: 'archive disk', reason: '' }, 18.7)).toBe('≤ 8.0×'); - expect(headroomLabel({ mgas: 400, bound: '≥', name: 'proven', reason: '' }, 18.7)).toBe('≥ 21×'); - expect(headroomLabel({ mgas: 190, bound: '', name: 'mix DA', reason: '' }, 18.7)).toBe('10×'); - }); - - it('places today, the first limit and the headroom label on a 0–proven span, and explains an upper bound', () => { - const data = bench(400, true); - data.current.mix = { mgas: 400, atTop: true, run: { ...run(400), storage: storage() } }; - swr.result = { data }; + it('places today on a 0–proven track and centers the multiple over the gap, clamping labels inside', () => { + swr.result = { data: bench(400, true) }; const { head } = render(); - // Today 12.5 and the ~151 archive ceiling on a 0–400 span; the label sits halfway between. - expect(head).toMatch(/data-span="today"[^>]*left:3\.125%/); - expect(head).toMatch(/data-span="limit"[^>]*left:37\.76\d*%/); - expect(head).toMatch(/data-span="headroom"[^>]*left:20\.44\d*%/); - expect(text(head)).toContain("≤ 12×First limit for today's traffic mix ÷ today's median gas rate. ≤ because the archive disk ceiling is an upper bound."); - expect(text(head)).toContain('archive disk ≤ 151'); - // The traffic tile's sustainable rate (mix DA) is not the first limit, so it shows no multiple. - expect(tile(head, 'traffic mix')).toMatch(/sustainable today 974\/sHighest/); + // The server render assumes a 240 px column: today 12.5 of 400 sits at 7.5 px, so its label pins left. + expect(head).toMatch(/data-span="today"[^>]*left:7.5px/); + expect(head).toMatch(/data-span="gap"[^>]*left:7.5px/); + expect(head).toMatch(/data-span="headroom"[^>]*left:123.75px;transform:translateX\(-50%\)/); + expect(head).toMatch(/data-span-label="today"[^>]*style="left:0"/); + expect(head).not.toContain('data-span="limit"'); + expect(place(120, 40, 240)).toEqual({ side: 'center', x: 120 }); + expect(place(3, 40, 240)).toEqual({ side: 'left', x: 0 }); + expect(place(238, 40, 240)).toEqual({ side: 'right', x: 240 }); }); - it('hides the span without today’s median', () => { + it('hides the span without today’s median or a proven rate', () => { swr.result = { data: bench(400, true) }; const snapshot = fixture(); snapshot.ladder.observed.p50 = { ...snapshot.ladder.observed.p50, value: null, tag: 'not-measured' }; - const { head } = render({ benchUrl: BENCH }, snapshot); - expect(head).not.toContain('data-span'); - expect(text(head)).toContain('≥ 400Mgas/s'); + expect(render({ benchUrl: BENCH }, snapshot).head).not.toContain('data-span'); + swr.result = { data: { current: none, da: none, history: [], latestAt: null, unknown: [] } }; + expect(render().head).not.toContain('data-span'); }); }); @@ -333,14 +317,14 @@ describe('disk ceilings', () => { expect(rows(ranked).map((r) => r.slice(0, 26))).toEqual([ '1DA · Payments (B20)next~5', '2DA · Swaps~115 ≈ 799/s Es', - '3Disk · archive node≤ 151u', + '3Disk · archive node≤ 138u', '4DA · Mainnet mix190 ≈ 981', '5Execution, validators, pr', '6Disk · full node≤ 553uppe', '—TEE proving, RPC under sy', ]); - // (45 TB − 4 TB) ÷ (1e6 × 0.0086 B/gas × 1 year); (45 TB − 1.1 TB state) ÷ (1e6 × (history 31 d + proofs 15 d + 2 years of 0.0008 B/gas state)). - expect(disk(ranked, 'archive node')).toBe("3Disk · archive node≤ 151upper bound · 1-year runway · archive 4.0 TB on 2026-01-01 (Synthetic archive snapshot) · growth at the mainnet mix run's bytes/gas"); + // (45 TB − 4 TB) ÷ (1e6 × (0.0086 bench + 0.0008 mainnet state) B/gas × 1 year); (45 TB − 1.1 TB state) ÷ (1e6 × (history 31 d + proofs 15 d + 2 years of 0.0008 B/gas state)). + expect(disk(ranked, 'archive node')).toBe("3Disk · archive node≤ 138upper bound · 1-year runway · archive 4.0 TB on 2026-01-01 (Synthetic archive snapshot) · growth at the mainnet mix run's bytes/gas + mainnet state growth 0.0008 B/gas"); expect(disk(ranked, 'full node')).toBe("6Disk · full node≤ 553upper bound · 2-year runway · state 1.1 TB now · history 31 d + proofs 15 d at the mainnet mix run's bytes/gas · production state growth 0.0008 B/gas"); }); @@ -353,6 +337,14 @@ describe('disk ceilings', () => { expect(help).toContain('Archive size: invented size.'); }); + it('counts only the bench run’s state in the archive ceiling without production state growth', () => { + swr.result = { data: withStorage() }; + const snapshot = fixture(); + delete snapshot.production!.stateBytesPerGas; + // (45 TB − 4 TB) ÷ (1e6 × 0.0086 B/gas × 1 year). + expect(disk(render({ benchUrl: BENCH }, snapshot).ranked, 'archive node')).toMatch(/≤ 151upper bound .*bytes\/gas bench state only$/); + }); + it('names the missing input and leaves the row unranked', () => { swr.result = { data: withStorage() }; const noPolicy = fixture(); @@ -381,8 +373,8 @@ describe('disk ceilings', () => { it('notes a disk ceiling below a tile’s sustainable rate under the tiles', () => { const swapDa = run(400, 'swap', daResult(2.6158e7, 22.4, false)); swr.result = { data: withStorage({ current: { mix: null, payment: null, swap: { mgas: 400, atTop: true, run: run(400, 'swap') } }, da: { mix: null, payment: null, swap: swapDa } }) }; - // Swaps are sustainable to the 200 Mgas/s gas limit, above the ~151 archive ceiling. - expect(text(render().head)).toContain('Archive disk may cap lower (≤ 151 Mgas/s at 1-year runway)'); + // Swaps are sustainable to the 200 Mgas/s gas limit, above the ~138 archive ceiling. + expect(text(render().head)).toContain('Archive disk may cap lower (≤ 138 Mgas/s at 1-year runway)'); // A half-year archive runway lifts the ceiling to ~302 Mgas/s, above every tile. const shortRunway = fixture(); shortRunway.policy!.runwayYears!.archive = 0.5; From 74e36c6f535370b9fd1512c2b0da8e1d42763fdb Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 15:52:21 -0700 Subject: [PATCH 18/33] feat(scaling): drop the header callout and disk note, and move the last change under the ranked limits --- app/scaling/components/Bottlenecks.tsx | 23 ++++++++--- app/scaling/components/Headline.tsx | 49 ++--------------------- app/scaling/components/Scorecard.tsx | 8 ++-- app/scaling/components/scorecard.test.tsx | 42 +++++-------------- 4 files changed, 36 insertions(+), 86 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index 71a34d2..d08d0da 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -58,7 +58,7 @@ export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): function daEvidence(w: DaWorkload): string { if (w.tag === 'estimated') return 'est. · run pending'; - if (w.bench && w.source?.url === runHref(w.bench.run)) return `measured · ${w.bench.da.result.blobs_per_l1_slot.toFixed(1)} blobs/slot at ${w.bench.run.mgas}`; + if (w.bench && w.source?.url === runHref(w.bench.run)) return `measured · ${w.bench.da.result.blobs_per_l1_slot.toFixed(1)} blobs/slot vs ${w.bench.da.target ?? BLOB_TARGET} at ${w.bench.run.mgas}`; return `measured · ${w.source?.label ?? 'source pending'}`; } @@ -72,7 +72,7 @@ export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven const m = proven?.mgas ?? null; const atLeast = (atTop: boolean) => (atTop ? `≥ ${m}` : `${m}`); const merged: Limit[] = m !== null && core.every((b) => b.value === m) - ? [{ id: 'core', label: 'Execution, validators, proof node', value: m, ceiling: atLeast(proven!.atTop), evidence: `all pass at ${m}` }] + ? [{ id: 'core', label: 'Execution, validators, proof node', value: m, ceiling: atLeast(proven!.atTop), evidence: `all pass at ${m} · ${proven!.atTop ? 'no limit found up to the top rate tested' : 'the next rate tested did not pass'}` }] : core.map((b) => ({ id: b.id, label: b.label, @@ -147,13 +147,26 @@ export function diskLimits(source: StorageSource | null, { policy, production }: return [full, archive]; } -function Evidence({ proven }: { proven: Proven }) { +function LastChange({ history }: { history: ProvenCapacity['history'] }) { + if (history.length < 2) return null; + const [previous, last] = history.slice(-2); + const down = last.mgas !== null && previous.mgas !== null && last.mgas < previous.mgas; + return ( + <> + {' · '}{down ? '↓' : '↑'} from {previous.mgas === null ? 'none' : `${previous.mgas} Mgas/s`} on {day(last.at)} + {last.compareUrl ? <> · compare : null} + + ); +} + +function Evidence({ proven, history }: { proven: Proven; history: ProvenCapacity['history'] }) { const { run } = proven; return (
      Proven by {run.outputDir}: {run.mgas} Mgas/s run delivered {(run.gps / 1e6).toFixed(1)} Mgas/s · commit{' '} {run.commit.slice(0, 7)} · {day(run.createdAt)} +
        {run.steps.map((s) => ( @@ -167,7 +180,7 @@ function Evidence({ proven }: { proven: Proven }) { ); } -export function Bottlenecks({ limits, max, gasLimit, proven }: { limits: Limit[]; max: number; gasLimit: number | null; proven: Proven | null }) { +export function Bottlenecks({ limits, max, gasLimit, proven, history }: { limits: Limit[]; max: number; gasLimit: number | null; proven: Proven | null; history: ProvenCapacity['history'] }) { const pct = (v: number) => `${Math.min(100, (v / max) * 100)}%`; const ranked = limits.filter((l) => l.value !== null).length; const fixed = [{ label: 'TEE proving, RPC under sync', evidence: 'not measured yet' }]; @@ -213,7 +226,7 @@ export function Bottlenecks({ limits, max, gasLimit, proven }: { limits: Limit[] - {proven ? : null} + {proven ? : null}
      ); } diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx index 01e7776..1c39395 100644 --- a/app/scaling/components/Headline.tsx +++ b/app/scaling/components/Headline.tsx @@ -15,7 +15,7 @@ export type BenchState = { configured: boolean; data?: ProvenCapacity; error?: u const TRAFFIC: Record = { payment: 'payments', swap: 'swaps', mix: 'tx' }; const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 1 }); -type Props = Pick & { bench: BenchState; proven: ProvenMark; next: Limit | null; limits: Limit[] }; +type Props = Pick & { bench: BenchState; proven: ProvenMark; limits: Limit[] }; // Only states other than a loaded bench result need a note. function benchNote({ configured, data, error }: BenchState, proven: ProvenMark, qualified: Snapshot['ladder']['qualified']): ReactNode { @@ -183,58 +183,15 @@ function translations({ ladder, usage, bench, proven, limits }: Pick - Next bottleneck to solve - {limit.label} {limit.value === 0 ? 'is over budget' : `at ${limit.ceiling.split(' ≈ ')[0]} Mgas/s`} - {sub} -
      - ); -} - -function LastChange({ history }: { history: ProvenCapacity['history'] }) { - if (history.length < 2) return null; - const [previous, last] = history.slice(-2); - const down = last.mgas !== null && previous.mgas !== null && last.mgas < previous.mgas; - return ( - - {down ? '↓' : '↑'} from {previous.mgas === null ? 'none' : `${previous.mgas} Mgas/s`} on {day(last.at)} - {last.compareUrl ? <> · compare : null} - - ); -} - -// Disk ceilings come from one workload's storage run, so they stay out of the tiles; a note flags one -// that is lower than what a tile calls sustainable. -function diskNote(rows: Translation[], limits: Limit[], gasLimit: number | null): string | null { - const sustained = rows.flatMap((r) => (r.proven ? [sustainable(r.proven.mgas, r.da, gasLimit).mgas] : [])); - const disk = limits.filter((l) => l.id.startsWith('disk-') && l.value !== null).toSorted((a, b) => a.value! - b.value!)[0]; - if (!disk || !sustained.length || disk.value! >= Math.max(...sustained)) return null; - const which = disk.id === 'disk-archive' ? 'Archive' : 'Full-node'; - return disk.value === 0 ? `${which} disk is over budget` : `${which} disk may cap lower (≤ ${fmt(disk.value!)} Mgas/s at ${disk.runwayYears}-year runway)`; -} - -export function Headline({ ladder, usage, bench, proven, next, limits }: Props) { +export function Headline({ ladder, usage, bench, proven, limits }: Props) { const note = benchNote(bench, proven, ladder.qualified); const rows = translations({ ladder, usage, bench, proven, limits }); - const disk = diskNote(rows, limits, ladder.configuredLimit.value); return ( -
      +
      {rows.map((row, i) => )}
      - {disk ? {disk} : null} - {next ? : null} - {bench.data ? : null} {note ? {note} : null}
      ); diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index 38cb661..0567993 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -42,18 +42,18 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl const mark = proven ? benchMark(proven) : !benchUrl && qualified.value !== null ? { value: qualified.value, atLeast: false, help: qualified.help } : null; const workloads = daWorkloads(data?.da); const limits = rankLimits(bottleneckBars(workloads, proven), workloads, proven, diskLimits(pickStorage(data), snapshot)); - const next = limits[0]?.value != null ? limits[0] : null; + const binding = limits[0]?.value != null ? limits[0] : null; const values = [ladder.observed.p50, ladder.observed.p99, ladder.operatingPoint, ladder.configuredLimit].map((r) => r.value ?? 0); const max = Math.max(mark?.value ?? 0, ladder.configuredLimit.value ?? 0) || Math.max(0, ...values); return ( <> - + - + - + ); } diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index ebbdc18..cf7f0b0 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -94,21 +94,17 @@ describe('freshness and headline', () => { expect(render().html).not.toContain('Usage data as of'); }); - it('heads with proven capacity, today and headroom, and the next bottleneck with its last change', () => { + it('heads with proven capacity, today and headroom only, leaving the next bottleneck and last change to What breaks first', () => { swr.result = { data: bench(400, true) }; - const { head } = render(); + const { head, ranked } = render(); // 400 proven ÷ 12.5 today = 32×, a floor because 400 is the top rate tested. expect(text(head)).toContain("≥ 400Mgas/s≥ 32×Proven capacity ÷ today's median gas rate. What breaks first lists the limits that apply before proven capacity.today 12.5· 64 tx/s"); expect(text(head)).toContain('proven ≥ 400Payments (B20)'); expect(text(head)).not.toMatch(/p99|mix DA|archive disk ≤/); - expect(text(head)).toContain('Next bottleneck to solveDA · Payments (B20) at ~57 Mgas/s≈ 1,517 payments/s'); - expect(text(head)).toContain('↑ from 40 Mgas/s on 2026-01-06 · compare'); - expect(text(head)).not.toMatch(/First limit:|Payments per second|In transactions/); - }); - - it('adds blobs per slot to the callout once the binding DA workload has a bench run', () => { - swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', daResult(8e7, 20, false)), swap: null } }) }; - expect(text(render().head)).toContain('DA · Payments (B20) at 76 Mgas/s≈ 1,896 payments/s · 20.0 blobs/slot vs 14 at 200'); + expect(text(head)).not.toMatch(/Next bottleneck|may cap lower|↑ from|First limit:|Payments per second|In transactions/); + // The last change sits on the run line under the ranked table. + expect(text(ranked)).toMatch(/Proven by synthetic-400: .* · 2026-01-06 · ↑ from 40 Mgas\/s on 2026-01-06 · compare/); + expect(ranked).toContain('href="https://github.com/base/base/compare/aaaa...bbbb"'); }); it('falls back to the snapshot without a bench host, and says so while loading or failed', () => { @@ -124,10 +120,6 @@ describe('freshness and headline', () => { expect(text(render().head)).toMatch(/per second\.—Mgas\/s.*VPN/); }); - it('names the merged core limit when the proven rate is below every DA ceiling', () => { - swr.result = { data: bench(40, false) }; - expect(text(render().head)).toContain('Execution, validators, proof node at 40 Mgas/sthe next rate tested did not pass'); - }); }); describe('workload tiles', () => { @@ -259,7 +251,7 @@ describe('what breaks first', () => { it('merges execution, validators and proof node when they share the proven ceiling, with the run evidence', () => { swr.result = { data: bench(400, true) }; const { ranked } = render(); - expect(rows(ranked)[3]).toBe('4Execution, validators, proof node≥ 400all pass at 400'); + expect(rows(ranked)[3]).toBe('4Execution, validators, proof node≥ 400all pass at 400 · no limit found up to the top rate tested'); expect(ranked.match(/>next { it('marks the core limit next when it is the lowest ceiling', () => { swr.result = { data: bench(40, false) }; - expect(rows(render().ranked)[0]).toBe('1Execution, validators, proof nodenext40all pass at 40'); + expect(rows(render().ranked)[0]).toBe('1Execution, validators, proof nodenext40all pass at 40 · the next rate tested did not pass'); }); it('shows a measured DA workload with its blobs per slot', () => { swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', daResult(8e7, 20, false)), swap: null } }) }; - expect(rows(render().ranked)[0]).toBe('1DA · Payments (B20)next76 ≈ 1,896/s Measuredmeasured · 20.0 blobs/slot at 200'); + expect(rows(render().ranked)[0]).toBe('1DA · Payments (B20)next76 ≈ 1,896/s Measuredmeasured · 20.0 blobs/slot vs 14 at 200'); }); }); @@ -295,7 +287,7 @@ describe('reverts, measured swaps and disk', () => { swr.result = { data: bench(400, true, { da: { mix: null, payment: null, swap: run(400, 'swap', daResult(2.6158e7, 22.4, false)) } }) }; const ranked = render().ranked; expect(rows(ranked).slice(0, 4).map((r) => r.slice(0, 22))).toEqual(['1DA · Payments (B20)ne', '2DA · Mainnet mix190 ≈', '3DA · Swaps232 ≈ 5,800', '4Execution, validators']); - expect(rows(ranked)[2]).toContain('Measuredmeasured · 22.4 blobs/slot at 400'); + expect(rows(ranked)[2]).toContain('Measuredmeasured · 22.4 blobs/slot vs 14 at 400'); const swapsRow = ranked.split(' r.includes('>DA · Swaps<'))!; expect(swapsRow).toMatch(/style="width:58\.\d+%"/); expect(swapsRow).toMatch(/border-dashed[^"]*" style="left:50%"/); @@ -365,20 +357,8 @@ describe('disk ceilings', () => { swr.result = { data: withStorage() }; const snapshot = fixture(); snapshot.production!.archiveBytes!.value = 50e12; - const { head, ranked } = render({ benchUrl: BENCH }, snapshot); + const { ranked } = render({ benchUrl: BENCH }, snapshot); expect(rows(ranked)[0]).toMatch(/^1Disk · archive nodenextover budget1-year runway/); - expect(text(head)).toContain('Next bottleneck to solveDisk · archive node is over budget'); - }); - - it('notes a disk ceiling below a tile’s sustainable rate under the tiles', () => { - const swapDa = run(400, 'swap', daResult(2.6158e7, 22.4, false)); - swr.result = { data: withStorage({ current: { mix: null, payment: null, swap: { mgas: 400, atTop: true, run: run(400, 'swap') } }, da: { mix: null, payment: null, swap: swapDa } }) }; - // Swaps are sustainable to the 200 Mgas/s gas limit, above the ~138 archive ceiling. - expect(text(render().head)).toContain('Archive disk may cap lower (≤ 138 Mgas/s at 1-year runway)'); - // A half-year archive runway lifts the ceiling to ~302 Mgas/s, above every tile. - const shortRunway = fixture(); - shortRunway.policy!.runwayYears!.archive = 0.5; - expect(text(render({ benchUrl: BENCH }, shortRunway).head)).not.toContain('may cap lower'); }); }); From d743b0fdc6ba0ebd8860a165b7bc25a77b28a90e Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 15:59:40 -0700 Subject: [PATCH 19/33] feat(scaling): mark every ranked breaking point on the ladder with labels hugging the bar --- app/scaling/components/Ladder.tsx | 199 ++++++++++++++-------- app/scaling/components/Scorecard.tsx | 2 +- app/scaling/components/scorecard.test.tsx | 59 ++++++- 3 files changed, 184 insertions(+), 76 deletions(-) diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx index cfb37e2..45fea67 100644 --- a/app/scaling/components/Ladder.tsx +++ b/app/scaling/components/Ladder.tsx @@ -5,30 +5,87 @@ import { useLayoutEffect, useRef, useState } from 'react'; import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; import type { Rung, Snapshot } from '../snapshot.mjs'; +import type { Limit } from './Bottlenecks'; import { Info } from './Info'; -const BAR_Y = 50; const BAR_H = 24; -const ROW_PX = 18; -const CHAR_PX = 7.5; +// Each label is two 12px lines; rows sit GAP_PX from the bar and touch each other, so a second row stays +// within 30px of the bar. +const ROW_PX = 24; +const GAP_PX = 6; const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 1 }); export type ProvenMark = { value: number; atLeast: boolean; help: string } | null; -type Props = Pick & { proven: ProvenMark; max: number }; -type Label = { id: string; text: string; help: string; x: number; className: string }; +type Tone = 'today' | 'p99' | 'fee' | 'da' | 'disk' | 'gas' | 'proven'; +export type Point = { id: string; value: number; valueText: string; name: string; tone: Tone; help: string }; +export type PlacedPoint = Point & { x: number; offScale: boolean; side: 'above' | 'below'; tier: number; left: number; width: number; compact: boolean }; +const TEXT: Record = { + today: 'text-bds-blue-90 dark:text-white', + p99: 'text-bds-blue-50', + fee: 'text-bds-gray-60', + da: 'text-bds-red-60', + disk: 'text-bds-purple-60', + gas: 'text-bds-orange-60', + proven: 'text-base-blue', +}; +const TICK: Record = { + today: 'border-bds-blue-90 dark:border-white', + p99: 'border-bds-blue-50', + fee: 'border-dotted border-bds-gray-60', + da: 'border-bds-red-60', + disk: 'border-bds-purple-60', + gas: 'border-dashed border-bds-orange-60', + proven: 'border-base-blue', +}; const SHADES = { median: 'fill-base-blue bg-base-blue', p99: 'fill-bds-blue-30 bg-bds-blue-30', allowed: 'fill-bds-blue-10 bg-bds-blue-10 dark:fill-bds-blue-80 dark:bg-bds-blue-80', proven: 'fill-bds-gray-10 bg-bds-gray-10 dark:fill-white/10 dark:bg-white/10', }; +const SHORT: Record = { 'da-payment': 'DA · payments', 'da-swap': 'DA · swaps', 'da-mix': 'DA · mix', 'disk-archive': 'archive disk', 'disk-full': 'full-node disk' }; + +// Approximate label widths: a bold 12px value with its help icon over an 11px name. +const widthOf = (p: Point, offScale: boolean, compact: boolean) => + Math.max((p.valueText.length + (offScale ? 2 : 0)) * 7.6 + 24, compact ? 0 : p.name.length * 6) + 2; + +// Labels alternate above and below in value order, in one row hugging the bar on each side. A label +// that collides first tries the other side, then a nudge that keeps it over its tick, then drops its +// name (listed under the ladder); only then does it take a second row. Off-scale points sit at the +// right edge without a tick. +export function layoutPoints(points: Point[], max: number, width: number): PlacedPoint[] { + const ends: Record<'above' | 'below', number[]> = { above: [], below: [] }; + return points.toSorted((a, b) => a.value - b.value).map((p, i) => { + const offScale = p.value > max; + const x = offScale ? width : (Math.max(0, p.value) / max) * width; + const preferred = i % 2 === 0 ? 'above' : 'below'; + const sides = [preferred, preferred === 'above' ? 'below' : 'above'] as const; + for (let tier = 0; ; tier++) { + for (const compact of [false, true]) { + const w = Math.min(width, widthOf(p, offScale, compact)); + const centered = Math.min(Math.max(x - w / 2, 0), width - w); + for (const nudge of [false, true]) { + for (const side of sides) { + const end = ends[side][tier] ?? -Infinity; + const left = nudge ? Math.max(centered, end + GAP_PX) : centered; + const clear = left >= end + GAP_PX && left + w <= width && Math.abs(left + w / 2 - x) <= w; + // A new, empty row always fits, so the loop ends. + if (clear || end === -Infinity) { + ends[side][tier] = left + w; + return { ...p, x, offScale, side, tier, left, width: w, compact }; + } + } + } + } + } + }); +} -// Width of a label with its help button, so fanned labels can be kept apart. -const labelPx = (text: string) => text.length * CHAR_PX + 20; +type Props = Pick & { proven: ProvenMark; max: number; limits: Limit[] }; -export function Ladder({ ladder, windows, proven, max }: Props) { +export function Ladder({ ladder, windows, proven, max, limits }: Props) { const ref = useRef(null); const [width, setWidth] = useState(960); useLayoutEffect(() => { @@ -38,88 +95,88 @@ export function Ladder({ ladder, windows, proven, max }: Props) { }, []); const help = (r: Rung) => (r.window ? `${r.help} Window: ${windows[r.window].label}.` : r.help); - const frac = (v: number) => Math.min(1, v / max); - const pct = (v: number) => `${frac(v) * 100}%`; - const px = (v: number) => frac(v) * width; + const pct = (v: number) => `${Math.min(1, v / max) * 100}%`; const { p50, p99 } = ladder.observed; const limit = ladder.configuredLimit.value; - const fee = ladder.operatingPoint.value; const median = p50.value; const tail = median === null ? null : p99.value; - - // p50 and p99 are the shade boundaries; their labels fan out to the right on angled leaders. - let nextFree = 0; - const fanned = ([['p50', median, p50], ['p99', tail, p99]] as const).flatMap(([id, value, rung]) => { - if (value === null) return []; - const text = `${id} ${n(value)}`; - const left = Math.min(Math.max(px(value) + 17, nextFree), width - labelPx(text)); - nextFree = left + labelPx(text) + 8; - return [{ id, value, text, left, help: help(rung) }]; - }); - const usedEnd = tail ?? median ?? 0; - const below: Label[] = [ - ...(fee === null ? [] : [{ id: 'fee', text: `fee target ${n(fee)}`, help: help(ladder.operatingPoint), x: fee, className: 'text-[12px] text-bds-gray-60' }]), - ...(limit === null ? [] : [{ id: 'limit', text: `gas limit ${n(limit)}`, help: help(ladder.configuredLimit), x: limit, className: 'text-bds-orange-60' }]), - ...(proven ? [{ id: 'proven', text: `proven ${proven.atLeast ? '≥ ' : ''}${n(proven.value)}`, help: proven.help, x: proven.value, className: 'font-[600] text-base-blue' }] : []), + const rung = (id: string, r: Rung, name: string, tone: Tone): Point[] => (r.value === null ? [] : [{ id, value: r.value, valueText: n(r.value), name, tone, help: help(r) }]); + const points: Point[] = [ + ...rung('today', p50, 'today', 'today'), + ...(tail === null ? [] : rung('p99', p99, 'p99', 'p99')), + ...rung('fee', ladder.operatingPoint, 'fee target', 'fee'), + ...rung('gas', ladder.configuredLimit, 'gas limit', 'gas'), + ...(proven ? [{ id: 'proven', value: proven.value, valueText: `${proven.atLeast ? '≥ ' : ''}${n(proven.value)}`, name: 'proven', tone: 'proven' as const, help: proven.help }] : []), + // The ranked breaking points from What breaks first; unranked rows have no ceiling to mark. + ...limits.filter((l) => l.value !== null && SHORT[l.id]).map((l): Point => ({ + id: l.id, + value: l.value!, + valueText: l.value === 0 ? 'over budget' : l.ceiling.split(' ≈ ')[0], + name: SHORT[l.id], + tone: l.id.startsWith('disk-') ? 'disk' : 'da', + help: l.help ?? l.evidence, + })), ]; - // Labels under the bar take the first row where they clear the previous label. - const rowEnds: number[] = []; - const placedBelow = below.toSorted((a, b) => a.x - b.x).map((l) => { - const w = labelPx(l.text); - const left = Math.min(Math.max(px(l.x) - w / 2, 0), width - w); - let row = rowEnds.findIndex((end) => end + 6 <= left); - if (row === -1) row = rowEnds.length; - rowEnds[row] = left + w; - return { ...l, left, row }; - }); - const axisTop = BAR_Y + BAR_H + 10 + Math.max(1, rowEnds.length) * ROW_PX; - const height = axisTop + 14; - const captions = [ - limit !== null && limit > usedEnd && { text: 'allowed by policy', from: usedEnd, to: limit, className: 'text-bds-blue-60' }, - proven && limit !== null && proven.value > limit && { text: 'proven, not yet allowed', from: limit, to: proven.value, className: 'text-bds-gray-50' }, - ].filter((c) => !!c); + const placed = layoutPoints(points, max, width); + const tiers = (side: 'above' | 'below') => placed.filter((p) => p.side === side).reduce((h, p) => Math.max(h, (p.tier + 1) * ROW_PX), 0); + const barTop = tiers('above') + GAP_PX; + const height = barTop + BAR_H + GAP_PX + tiers('below') + 16; + const nearEdge = (p: PlacedPoint) => (p.side === 'above' ? barTop - GAP_PX - p.tier * ROW_PX : barTop + BAR_H + GAP_PX + p.tier * ROW_PX); const legend = [ median !== null && ['up to median', SHADES.median], tail !== null && ['median to p99', SHADES.p99], limit !== null && ['allowed by policy', SHADES.allowed], proven && limit !== null && proven.value > limit && ['proven, not yet allowed', SHADES.proven], ].filter((l) => !!l); + const compact = placed.filter((p) => p.compact); return (
      How close are we, and to what? · Mgas/s -
      +
      - {fanned.map((f) => ( - - {f.text} {f.help} - - ))} - {captions.map((c) => ( - {c.text} - ))} - {placedBelow.map((l) => ( - - {l.text} {l.help} - + {placed.filter((p) => !p.offScale).map((p) => ( + ))} - 0 - {n(max)} Mgas/s + {placed.map((p) => { + const top = p.side === 'above' ? nearEdge(p) - ROW_PX : nearEdge(p); + return ( + + {p.tier > 0 && !p.offScale ? ( + + ) : null} + + + {p.valueText}{p.offScale ? ' →' : ''} + {p.help} + + {p.compact ? null : {p.name}} + + + ); + })} + 0 + {n(max)} Mgas/s
      + {compact.length ? ( +

      + {compact.map((p) => {p.valueText} {p.name})} +

      + ) : null}
      {legend.map(([label, shade]) => ( diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index 0567993..5366a0b 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -49,7 +49,7 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl <> - + diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index cf7f0b0..ccaff29 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ProvenCapacity, Run } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; import { headroomLabel, place, sustainable } from './Headline'; +import { layoutPoints, type PlacedPoint, type Point } from './Ladder'; import { Scorecard } from './Scorecard'; // Synthetic data only: real bench results and snapshots are internal and never committed. @@ -210,17 +211,67 @@ describe('subsystems by workload', () => { }); describe('ladder', () => { - it('shades the used band in two tones at p50 and p99, notches the fee target, and marks the gas limit and proven rate', () => { + it('shades the used band in two tones and labels today, p99, fee target, gas limit and proven', () => { swr.result = { data: bench(400, true) }; const snapshot = fixture(); snapshot.ladder.observed.p99 = { ...snapshot.ladder.observed.p99, value: 42.24, tag: 'measured' }; const { ladder } = render({ benchUrl: BENCH }, snapshot); expect(ladder).toMatch(/data-band="p50" width="3.125%"/); expect(ladder).toMatch(/data-band="p99" width="10.56%"/); - expect(ladder).toContain('data-notch="fee"'); - for (const label of ['p50 12.5', 'p99 42.2', 'fee target 40', 'gas limit 200', 'proven ≥ 400', '400 Mgas/s']) expect(text(ladder)).toContain(label); + const label = (id: string) => text(ladder.split(`data-label="${id}"`)[1]?.split('data-label=')[0] ?? '').replace(/^[^>]*>/, ''); + expect(label('today')).toMatch(/^12\.5.*today/); + expect(label('p99')).toMatch(/^42\.2.*p99/); + expect(label('fee')).toMatch(/^40.*fee target/); + expect(label('gas')).toMatch(/^200.*gas limit/); + expect(label('proven')).toMatch(/^≥ 400.*proven/); + expect(ladder).toMatch(/data-tick="gas" class="[^"]*border-dashed border-bds-orange-60/); expect(text(ladder)).toContain('up to medianmedian to p99allowed by policyproven, not yet allowed'); - expect(text(ladder)).not.toMatch(/DA ·|42\.24|Usage data/); + expect(text(ladder)).not.toMatch(/42\.24|Usage data/); + }); + + it('ticks every ranked breaking point in its colour at its value, and none for unranked rows', () => { + swr.result = { data: bench(400, true) }; + const { ladder } = render(); + // On the 960 px server-render width: DA payments ~57, swaps ~115, mix ~190 Mgas/s of 400. + expect(ladder).toMatch(/data-tick="da-payment" class="[^"]*border-bds-red-60" style="left:137\.43\d*px/); + expect(ladder).toMatch(/data-tick="da-swap" class="[^"]*border-bds-red-60" style="left:275\.94\d*px/); + expect(ladder).toMatch(/data-tick="da-mix" class="[^"]*border-bds-red-60" style="left:455\.09\d*px/); + // Without a storage run both disk rows are unranked, so neither gets a tick. + expect(ladder).not.toMatch(/data-(tick|label)="disk-/); + }); + + it('pins a breaking point beyond the scale to the right edge without a tick', () => { + const data = bench(400, true); + data.current.mix = { mgas: 400, atTop: true, run: { ...run(400), storage: storage() } }; + swr.result = { data }; + const { ladder } = render(); + expect(ladder).toMatch(/data-tick="disk-archive" class="[^"]*border-bds-purple-60"/); + expect(ladder).not.toContain('data-tick="disk-full"'); + expect(text(ladder.split('data-label="disk-full"')[1].split('data-label=')[0])).toMatch(/≤ 553 →.*full-node disk/); + }); + + it('keeps labels apart in a crowded set, dropping names to a legend before stacking tiers', () => { + const point = (id: string, value: number): Point => ({ id, value, valueText: String(value), name: `limit ${id}`, tone: 'da', help: '' }); + const crowded = [10, 12, 14, 16, 18, 20, 22, 24, 26, 600].map((v, i) => point(`p${i}`, v)); + for (const width of [960, 600, 320]) { + const placed = layoutPoints(crowded, 400, width); + const top = (p: PlacedPoint) => (p.side === 'above' ? -1 : 1) * (p.tier + 1); + for (const a of placed) { + expect(a.left).toBeGreaterThanOrEqual(0); + expect(a.left + a.width).toBeLessThanOrEqual(width + 0.001); + for (const b of placed) { + if (a !== b && top(a) === top(b)) expect(a.left + a.width <= b.left || b.left + b.width <= a.left).toBe(true); + } + } + expect(placed.at(-1)).toMatchObject({ id: 'p9', offScale: true, x: width }); + } + // Long names on a 320 px track: the third label fits beside the first only without its name. + const long = (id: string, value: number): Point => ({ ...point(id, value), name: 'a breaking point with a long name' }); + expect(layoutPoints([long('a', 100), long('b', 180), long('c', 270)], 320, 320).map((p) => [p.side, p.tier, p.compact])).toEqual([ + ['above', 0, false], + ['below', 0, false], + ['above', 0, true], + ]); }); it('shades only to p50 without a p99, and draws no used band without either', () => { From e23ccc681567555238489325dfb8e1d3df278091 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 16:21:23 -0700 Subject: [PATCH 20/33] feat(scaling): state ceilings with units and plain evidence, move details into help, and list the proven run --- app/scaling/components/Bottlenecks.tsx | 194 ++++++++++++++++------ app/scaling/components/Ladder.tsx | 2 +- app/scaling/components/Scorecard.tsx | 2 +- app/scaling/components/scorecard.test.tsx | 100 +++++++---- app/scaling/da/workloads.ts | 4 +- 5 files changed, 209 insertions(+), 93 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index d08d0da..6608773 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -1,7 +1,7 @@ import { Card } from '../../components/ui/Card'; import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; -import { BLOB_TARGET, daCapacity, fmt, type DaWorkload } from '../da/workloads'; +import { BLOB_DATA_BYTES, BLOB_TARGET, daCapacity, fmt, L1_SLOT_SECONDS, type DaWorkload } from '../da/workloads'; import { runHref, type PipelineStep, type Proven, type ProvenCapacity, type Run, type Storage, type Workload } from '../proven/capacity'; import { EXECUTE_PAYLOAD_P99_MS, PROOF_LAG_P99_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; import { Info } from './Info'; @@ -56,23 +56,72 @@ export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): ]; } -function daEvidence(w: DaWorkload): string { - if (w.tag === 'estimated') return 'est. · run pending'; - if (w.bench && w.source?.url === runHref(w.bench.run)) return `measured · ${w.bench.da.result.blobs_per_l1_slot.toFixed(1)} blobs/slot vs ${w.bench.da.target ?? BLOB_TARGET} at ${w.bench.run.mgas}`; - return `measured · ${w.source?.label ?? 'source pending'}`; +const TRAFFIC: Record = { payment: 'payments', swap: 'swaps', mix: 'transactions' }; +const blobRule = (target: number) => + `The ceiling is the gas rate that fills the ${target}-blob target: ${target} × ${fmt(BLOB_DATA_BYTES)} bytes per blob ÷ ${L1_SLOT_SECONDS} s per slot ÷ bytes per gas.`; + +// Evidence and help for a DA row. `first` marks the lowest DA row, where the shared target is named. +function daText(w: DaWorkload, ceiling: number, first: boolean, today: number | null, provenMgas: number | null): { evidence: string; help: string } { + const shared = first ? ', shared by all rollups' : ''; + const bench = w.bench && w.source?.url === runHref(w.bench.run) ? w.bench : null; + if (bench) { + const { run, da } = bench; + const target = da.target ?? BLOB_TARGET; + const windowMgas = da.result.gas_per_second / 1e6; + // The DA run reports blobs over its posting window, which runs below the load rate; rescale to the load. + const blobs = Math.round((da.result.blobs_per_l1_slot * run.mgas) / windowMgas); + const reverted = run.reverted > 0.01 ? ` ${Math.round(run.reverted * 100)}% of ${TRAFFIC[w.payload]} reverted.` : ''; + return { + evidence: `Measured in the bench: at ${run.mgas} Mgas/s, needs ${blobs} blobs per L1 slot. Target is ${target}${shared}.${reverted}`, + help: `Run ${run.outputDir}: ${da.result.bytes_per_gas.toPrecision(3)} compressed bytes per gas (compression ${da.result.compression_ratio.toFixed(1)}×). Over the batch posting window it averaged ${da.result.blobs_per_l1_slot.toFixed(1)} blobs per L1 slot at ${windowMgas.toFixed(1)} Mgas/s; scaling that window to the ${run.mgas} Mgas/s load gives ${blobs}. ${blobRule(target)}`, + }; + } + const target = BLOB_TARGET; + if (w.tag === 'estimated') { + return { + evidence: `Estimated until a bench run measures it. Target is ${target}${shared}.`, + help: `Estimated ${fmt(w.bytesPerTx)} compressed bytes and ${fmt(w.gasPerTx)} gas per transaction. ${blobRule(target)}`, + }; + } + const now = today === null ? null : `today's traffic uses about ${((target * today) / ceiling).toFixed(1)} blobs per L1 slot`; + const atProven = provenMgas === null ? null : `at ${provenMgas} Mgas/s it would need ${Math.round((target * provenMgas) / ceiling)}`; + return { + evidence: `Measured on mainnet${now || atProven ? `: ${[now, atProven].filter(Boolean).join('; ')}` : ''}. Target is ${target}${shared}.`, + help: `Source: ${w.source?.label ?? 'mainnet'} (mainnet blocks run through the production batcher encoder). Mainnet averages ${fmt(w.gasPerTx)} gas and ${w.bytesPerTx} compressed bytes per transaction. ${blobRule(target)}`, + }; +} + +function coreHelp(run: Run): string { + const replay = run.replay; + const proofs = run.proofNode?.proofs; + const execute = proofs?.methods.debug_executePayload?.latency.p99Ms; + return [ + `Run ${run.outputDir}.`, + replay && `Validator replay: ${replay.blocksPerSecond.toFixed(1)} blocks/s, ${times(replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND)} the ${REPLAY_BLOCKS_PER_SECOND} needed.`, + proofs && `Proof node: lag p99 ${proofs.headLagBlocks.p99} blocks${execute === undefined ? '' : `, executePayload p99 ${Math.round(execute)} ms`}.`, + ].filter(Boolean).join(' '); } // Lowest ceiling first; limits without a ceiling keep their order after the ranked ones. -export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven | null, disk: Limit[] = []): Limit[] { +export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven | null, disk: Limit[] = [], today: number | null = null): Limit[] { + const m = proven?.mgas ?? null; + const text = new Map { evidence: string; help: string }>(); const da = bars.filter((b) => b.id.startsWith('da-')).map((b): Limit => { const w = workloads.find((x) => `da-${x.payload}` === b.id)!; - return { id: b.id, label: b.label, value: b.value, ceiling: `${b.text} ≈ ${fmt(b.perSecond!)}/s`, evidence: daEvidence(w), tag: b.tag, perSecond: b.perSecond, workload: w }; + text.set(b.id, (first) => daText(w, b.value!, first, today, m)); + return { id: b.id, label: b.label, value: b.value, ceiling: b.text, evidence: '', tag: b.tag, perSecond: b.perSecond, workload: w }; }); const core = bars.filter((b) => !b.id.startsWith('da-')); - const m = proven?.mgas ?? null; const atLeast = (atTop: boolean) => (atTop ? `≥ ${m}` : `${m}`); const merged: Limit[] = m !== null && core.every((b) => b.value === m) - ? [{ id: 'core', label: 'Execution, validators, proof node', value: m, ceiling: atLeast(proven!.atTop), evidence: `all pass at ${m} · ${proven!.atTop ? 'no limit found up to the top rate tested' : 'the next rate tested did not pass'}` }] + ? [{ + id: 'core', + label: 'Execution, validators, proof node', + value: m, + ceiling: atLeast(proven!.atTop), + evidence: `Measured in the bench: all passed at ${m} Mgas/s${proven!.atTop ? ', the highest rate tested.' : '; the next rate tested did not pass.'}`, + help: coreHelp(proven!.run), + }] : core.map((b) => ({ id: b.id, label: b.label, @@ -80,7 +129,9 @@ export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven ceiling: b.value === null ? '—' : atLeast(b.id !== 'execution' || proven!.atTop), evidence: b.value === null ? b.text : b.id === 'execution' && !proven!.atTop ? 'the next rate tested did not pass' : b.text.split(' · ').slice(1).join(' · '), })); - return [...da, ...merged, ...disk].toSorted((a, b) => (a.value ?? Infinity) - (b.value ?? Infinity)); + const sorted = [...da, ...merged, ...disk].toSorted((a, b) => (a.value ?? Infinity) - (b.value ?? Infinity)); + const firstDa = sorted.find((l) => l.id.startsWith('da-'))?.id; + return sorted.map((l) => (text.has(l.id) ? { ...l, ...text.get(l.id)!(l.id === firstDa) } : l)); } export type StorageSource = { storage: Storage; run: Run }; @@ -101,81 +152,114 @@ export function pickStorage(data: ProvenCapacity | undefined): StorageSource | n const YEAR_SECONDS = 31_557_600; const days = (seconds: number) => Math.round(seconds / 86_400); +const years = (n: number) => (n === 1 ? 'one year' : `${n} years`); // Disk ceilings: the gas rate at which the budget just holds what each node keeps. Bench bytes per gas // is a lower bound (the sequencer prunes history), so both ceilings are upper bounds. export function diskLimits(source: StorageSource | null, { policy, production }: Pick): Limit[] { const runway = policy?.runwayYears; - const statePerGas = production?.stateBytesPerGas?.value; + const state = production?.stateBytesPerGas; const archiveNow = production?.archiveBytes; - const policyHelp = runway - ? ` Runway policy: full node ${runway.fullNode} years, archive ${runway.archive} year${runway.archive === 1 ? '' : 's'}${runway.help ? ` (${runway.help})` : ''}.` - : ' No runway policy is set.'; - const row = (id: string, label: string, need: string | null, mgas: number, years: number | undefined, evidence: string, help: string): Limit => - need !== null - ? { id, label, value: null, ceiling: '—', evidence: `needs ${need}`, help: help + policyHelp } - : mgas <= 0 - ? { id, label, value: 0, ceiling: 'over budget', evidence, help: help + policyHelp, runwayYears: years } - : { id, label, value: mgas, ceiling: `≤ ${fmt(mgas)}`, evidence: `upper bound · ${evidence}`, help: help + policyHelp, runwayYears: years }; + const policyText = runway + ? `runway policy: full node ${years(runway.fullNode)}, archive ${years(runway.archive)}${runway.help ? ` (${runway.help})` : ''}` + : 'no runway policy is set'; + type Row = { mgas: number; years: number; evidence: string; overBudget: string; help: string }; + const row = (id: string, label: string, need: string | null, r?: () => Row): Limit => { + if (need !== null) return { id, label, value: null, ceiling: '—', evidence: `needs ${need}`, help: `Needs ${need}; ${policyText}.` }; + const { mgas, years: runwayYears, evidence, overBudget, help } = r!(); + return mgas <= 0 + ? { id, label, value: 0, ceiling: 'over budget', evidence: overBudget, help, runwayYears } + : { id, label, value: mgas, ceiling: `≤ ${fmt(mgas)}`, evidence, help, runwayYears }; + }; if (!source) { const need = 'a bench storage-growth run'; - return [row('disk-full', 'Disk · full node', need, 0, undefined, '', ''), row('disk-archive', 'Disk · archive node', need, 0, undefined, '', '')]; + return [row('disk-full', 'Disk · full node', need), row('disk-archive', 'Disk · archive node', need)]; } const { storage: { diskBudgetBytes: budget, bytesPerGas, current, fullNode }, run } = source; const from = `the ${WORKLOAD[run.workload]} run`; + const budgetText = `${bytes(budget)} disk budget (a bench config assumption)`; - const fullNeed = !runway ? 'a runway policy' : statePerGas === undefined ? 'production state growth' : bytesPerGas.proofs === null ? 'proof storage growth' : null; - const perMgas = fullNeed ? 0 : 1e6 * (bytesPerGas.history * fullNode.historyWindowSeconds + bytesPerGas.proofs! * fullNode.proofWindowSeconds + statePerGas! * YEAR_SECONDS * runway!.fullNode); const full = row( - 'disk-full', 'Disk · full node', fullNeed, fullNeed ? 0 : (budget - current.state) / perMgas, runway?.fullNode, - `${runway?.fullNode}-year runway · state ${bytes(current.state)} now · history ${days(fullNode.historyWindowSeconds)} d + proofs ${days(fullNode.proofWindowSeconds)} d at ${from}'s bytes/gas · production state growth ${statePerGas} B/gas`, - `The ${bytes(budget)} disk budget must hold today's state, the history window (${days(fullNode.historyWindowSeconds)} d) and proof window (${days(fullNode.proofWindowSeconds)} d) at this rate, and the runway's state growth at production's bytes per gas. History bytes per gas comes from ${from} and is a lower bound, because the sequencer prunes history, so this ceiling is an upper bound.`, + 'disk-full', 'Disk · full node', + !runway ? 'a runway policy' : !state ? 'production state growth' : bytesPerGas.proofs === null ? 'proof storage growth' : null, + () => { + const historyDays = days(fullNode.historyWindowSeconds); + const proofDays = days(fullNode.proofWindowSeconds); + const perMgas = 1e6 * (bytesPerGas.history * fullNode.historyWindowSeconds + bytesPerGas.proofs! * fullNode.proofWindowSeconds + state!.value * YEAR_SECONDS * runway!.fullNode); + const kept = `${historyDays} days of history, ${proofDays} days of proofs and ${years(runway!.fullNode)} of state growth`; + return { + mgas: (budget - current.state) / perMgas, + years: runway!.fullNode, + evidence: `Projected from bench growth: ${kept} fit in ${bytes(budget)}. Upper bound.`, + overBudget: `Projected from bench growth: today's state (${bytes(current.state)}) leaves no room in ${bytes(budget)}.`, + help: `Inputs: ${budgetText}; today's state ${bytes(current.state)}, measured at ${from}'s snapshot; ${bytesPerGas.history.toPrecision(3)} bytes per gas of history and ${bytesPerGas.proofs!.toPrecision(3)} of proofs from ${from}; ${state!.value} bytes per gas of production state growth${state!.source ? ` from ${state!.source}` : ''}; ${policyText}. Upper bound: history bytes per gas is a lower bound, because the sequencer prunes history.`, + }; + }, ); // storage-growth.json's archive bytes per gas sums every MDBX table, state included, with static files // and RocksDB. Bench runs write almost no new state, so production state growth is added on top; that // double counts the bench run's tiny state growth. - const archiveNeed = !runway ? 'a runway policy' : !archiveNow ? 'the production archive size' : null; - const archivePerGas = bytesPerGas.archive + (statePerGas ?? 0); - const stateBasis = statePerGas === undefined ? 'bench state only' : `+ mainnet state growth ${statePerGas} B/gas`; const archive = row( - 'disk-archive', 'Disk · archive node', archiveNeed, - archiveNeed ? 0 : (budget - archiveNow!.value) / (1e6 * archivePerGas * YEAR_SECONDS * runway!.archive), runway?.archive, - `${runway?.archive}-year runway · archive ${archiveNow ? bytes(archiveNow.value) : ''} on ${archiveNow?.asOf} (${archiveNow?.source}) · growth at ${from}'s bytes/gas ${stateBasis}`, - `The ${bytes(budget)} disk budget minus today's archive must cover the runway's growth. ${statePerGas === undefined ? 'Without production state growth, only the bench run\'s state growth is counted.' : 'The rate adds mainnet state growth; the bench run writes almost no new state.'} Growth per gas comes from ${from} and is a lower bound for that workload, because the sequencer prunes history, so the ceiling is an upper bound. That workload may write more bytes per gas than mainnet traffic does${archiveNow ? ` (Base's whole archive is ${bytes(archiveNow.value)})` : ''}, which makes the ceiling conservative in the other direction.${archiveNow?.note ? ` Archive size: ${archiveNow.note}.` : ''}`, + 'disk-archive', 'Disk · archive node', + !runway ? 'a runway policy' : !archiveNow ? 'the production archive size' : null, + () => { + const perGas = bytesPerGas.archive + (state?.value ?? 0); + const stateText = state + ? ` plus ${state.value} bytes per gas of mainnet state growth${state.source ? ` from ${state.source}` : ''}, since the bench run writes almost no new state` + : ' (bench state only)'; + return { + mgas: (budget - archiveNow!.value) / (1e6 * perGas * YEAR_SECONDS * runway!.archive), + years: runway!.archive, + evidence: `Projected from bench growth: ${years(runway!.archive)} ${runway!.archive === 1 ? 'fills' : 'fill'} the ${bytes(budget)} disk (${bytes(archiveNow!.value)} used today). Upper bound.`, + overBudget: `Projected from bench growth: today's archive (${bytes(archiveNow!.value)}) already fills the ${bytes(budget)} disk.`, + help: `Inputs: ${budgetText}; today's archive ${bytes(archiveNow!.value)} from ${archiveNow!.source} on ${archiveNow!.asOf}${archiveNow!.note ? ` (${archiveNow!.note})` : ''}; ${bytesPerGas.archive.toPrecision(3)} bytes per gas from ${from}${stateText}; ${policyText}. Upper bound: bench bytes per gas is a lower bound, because the sequencer prunes history. That workload may write more bytes per gas than mainnet traffic does (Base's whole archive is ${bytes(archiveNow!.value)}), which makes the ceiling conservative in the other direction.`, + }; + }, ); return [full, archive]; } function LastChange({ history }: { history: ProvenCapacity['history'] }) { - if (history.length < 2) return null; const [previous, last] = history.slice(-2); const down = last.mgas !== null && previous.mgas !== null && last.mgas < previous.mgas; return ( <> - {' · '}{down ? '↓' : '↑'} from {previous.mgas === null ? 'none' : `${previous.mgas} Mgas/s`} on {day(last.at)} + {down ? '↓' : '↑'} from {previous.mgas === null ? 'none' : `${previous.mgas} Mgas/s`} on {day(last.at)} {last.compareUrl ? <> · compare : null} ); } -function Evidence({ proven, history }: { proven: Proven; history: ProvenCapacity['history'] }) { +const DT = 'text-bds-gray-60'; + +function ProvenBy({ proven, history }: { proven: Proven; history: ProvenCapacity['history'] }) { const { run } = proven; return ( -
      - - Proven by {run.outputDir}: {run.mgas} Mgas/s run delivered {(run.gps / 1e6).toFixed(1)} Mgas/s · commit{' '} - {run.commit.slice(0, 7)} · {day(run.createdAt)} - - -
        - {run.steps.map((s) => ( -
      • -
      • - ))} -
      +
      + Proven by +
      +
      Run
      +
      {run.outputDir}
      +
      Rate
      +
      {run.mgas} → {(run.gps / 1e6).toFixed(1)} Mgas/s
      +
      Commit
      +
      {run.commit.slice(0, 7)}
      +
      Date
      +
      {day(run.createdAt)}
      + {history.length >= 2 ? <>
      Change
      : null} +
      Steps
      +
      +
        + {run.steps.map((s) => ( +
      • +
      • + ))} +
      +
      +
      ); } @@ -183,7 +267,7 @@ function Evidence({ proven, history }: { proven: Proven; history: ProvenCapacity export function Bottlenecks({ limits, max, gasLimit, proven, history }: { limits: Limit[]; max: number; gasLimit: number | null; proven: Proven | null; history: ProvenCapacity['history'] }) { const pct = (v: number) => `${Math.min(100, (v / max) * 100)}%`; const ranked = limits.filter((l) => l.value !== null).length; - const fixed = [{ label: 'TEE proving, RPC under sync', evidence: 'not measured yet' }]; + const fixed = [{ label: 'TEE proving, RPC under sync', evidence: 'Not measured yet.' }]; return (
      What breaks first · Mgas/s @@ -205,8 +289,10 @@ export function Bottlenecks({ limits, max, gasLimit, proven, history }: { limits {l.help ? <> {l.help} : null} {next ? next : null} - - {l.ceiling}{l.tag ? <> : null} + + {l.ceiling}{l.value !== null && l.value > 0 ? ' Mgas/s' : ''} + {l.perSecond === undefined ? null : ≈ {fmt(l.perSecond)} tx/s} + {l.tag ? : null}
      @@ -226,7 +312,7 @@ export function Bottlenecks({ limits, max, gasLimit, proven, history }: { limits - {proven ? : null} + {proven ? : null}
      ); } diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx index 45fea67..43b1bf4 100644 --- a/app/scaling/components/Ladder.tsx +++ b/app/scaling/components/Ladder.tsx @@ -111,7 +111,7 @@ export function Ladder({ ladder, windows, proven, max, limits }: Props) { ...limits.filter((l) => l.value !== null && SHORT[l.id]).map((l): Point => ({ id: l.id, value: l.value!, - valueText: l.value === 0 ? 'over budget' : l.ceiling.split(' ≈ ')[0], + valueText: l.ceiling, name: SHORT[l.id], tone: l.id.startsWith('disk-') ? 'disk' : 'da', help: l.help ?? l.evidence, diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index 5366a0b..fba146e 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -41,7 +41,7 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl const qualified = ladder.qualified; const mark = proven ? benchMark(proven) : !benchUrl && qualified.value !== null ? { value: qualified.value, atLeast: false, help: qualified.help } : null; const workloads = daWorkloads(data?.da); - const limits = rankLimits(bottleneckBars(workloads, proven), workloads, proven, diskLimits(pickStorage(data), snapshot)); + const limits = rankLimits(bottleneckBars(workloads, proven), workloads, proven, diskLimits(pickStorage(data), snapshot), ladder.observed.p50.value); const binding = limits[0]?.value != null ? limits[0] : null; const values = [ladder.observed.p50, ladder.observed.p99, ladder.operatingPoint, ladder.configuredLimit].map((r) => r.value ?? 0); const max = Math.max(mark?.value ?? 0, ladder.configuredLimit.value ?? 0) || Math.max(0, ...values); diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index ccaff29..818b6c9 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -26,7 +26,7 @@ const STEPS: Run['steps'] = [ { name: 'tee_proof', enabled: false, status: 'not-implemented', reason: 'needs a synthetic L1' }, ]; const daResult = (frame_bytes: number, blobs: number, withinTarget: boolean | null): Run['da'] => ({ - result: { seconds: 600, gas_used: 4e10, transactions: 1e6, transaction_bytes: 0, frame_bytes, gas_per_second: 0, bytes_per_gas: 0, compression_ratio: 0, blobs_per_l1_slot: blobs, blob_fill: 1, blob_target_gas_per_second: 0 }, + result: { seconds: 600, gas_used: 4e10, transactions: 1e6, transaction_bytes: 0, frame_bytes, gas_per_second: 1.6e8, bytes_per_gas: frame_bytes / 4e10, compression_ratio: 4.5, blobs_per_l1_slot: blobs, blob_fill: 1, blob_target_gas_per_second: 0 }, target: 14, withinTarget, }); @@ -104,7 +104,7 @@ describe('freshness and headline', () => { expect(text(head)).not.toMatch(/p99|mix DA|archive disk ≤/); expect(text(head)).not.toMatch(/Next bottleneck|may cap lower|↑ from|First limit:|Payments per second|In transactions/); // The last change sits on the run line under the ranked table. - expect(text(ranked)).toMatch(/Proven by synthetic-400: .* · 2026-01-06 · ↑ from 40 Mgas\/s on 2026-01-06 · compare/); + expect(text(ranked)).toMatch(/Proven by.*Change↑ from 40 Mgas\/s on 2026-01-06 · compare/); expect(ranked).toContain('href="https://github.com/base/base/compare/aaaa...bbbb"'); }); @@ -285,29 +285,31 @@ describe('ladder', () => { }); describe('what breaks first', () => { - it('ranks DA estimates first without bench data and leaves unproven limits unranked', () => { + it('ranks DA estimates first without bench data, with units, and leaves unproven limits unranked', () => { expect(rows(render({}).ranked)).toEqual([ - '1DA · Payments (B20)next~57 ≈ 1,517/s Estimatedest. · run pending', - '2DA · Swaps~115 ≈ 799/s Estimatedest. · run pending', - '3DA · Mainnet mix190 ≈ 981/s Measuredmeasured · base-bench da, mainnet 2 h', + '1DA · Payments (B20)next~57 Mgas/s≈ 1,517 tx/sEstimatedEstimated until a bench run measures it. Target is 14, shared by all rollups.', + '2DA · Swaps~115 Mgas/s≈ 799 tx/sEstimatedEstimated until a bench run measures it. Target is 14.', + // 14 blobs × 12.5 Mgas/s today ÷ the ~190 Mgas/s mix ceiling ≈ 0.9 blobs per slot. + "3DA · Mainnet mix190 Mgas/s≈ 981 tx/sMeasuredMeasured on mainnet: today's traffic uses about 0.9 blobs per L1 slot. Target is 14.", '—Execution (sequencer)—no proven run', '—Validators—pending', '—Proof node—pending', '—Disk · full node—needs a bench storage-growth run', '—Disk · archive node—needs a bench storage-growth run', - '—TEE proving, RPC under sync—not measured yet', + '—TEE proving, RPC under sync—Not measured yet.', ]); }); - it('merges execution, validators and proof node when they share the proven ceiling, with the run evidence', () => { + it('merges execution, validators and proof node when they share the proven ceiling, with the run in help', () => { swr.result = { data: bench(400, true) }; const { ranked } = render(); - expect(rows(ranked)[3]).toBe('4Execution, validators, proof node≥ 400all pass at 400 · no limit found up to the top rate tested'); + expect(rows(ranked)[3]).toBe('4Execution, validators, proof node≥ 400 Mgas/sMeasured in the bench: all passed at 400 Mgas/s, the highest rate tested.'); + // 14 × 400 ÷ ~190 ≈ 30 blobs per slot at the proven rate. + expect(rows(ranked)[2]).toContain("today's traffic uses about 0.9 blobs per L1 slot; at 400 Mgas/s it would need 30. Target is 14."); + expect(ranked).toContain('Run synthetic-400. Validator replay: 36.4 blocks/s, 7.3× the 5 needed. Proof node: lag p99 4 blocks, executePayload p99 733 ms.'); + expect(ranked).toContain('Source: base-bench da, mainnet 2 h (mainnet blocks run through the production batcher encoder). Mainnet averages 193,349 gas and 154.7 compressed bytes per transaction.'); expect(ranked.match(/>next { @@ -315,35 +317,52 @@ describe('what breaks first', () => { over.current.mix = { mgas: 400, atTop: true, run: run(400, 'mix', null, 11) }; swr.result = { data: over }; expect(rows(render().ranked).slice(3, 6)).toEqual([ - '4Execution (sequencer)≥ 400top rate tested', - '5Validators≥ 4007.3× replay headroom', + '4Execution (sequencer)≥ 400 Mgas/stop rate tested', + '5Validators≥ 400 Mgas/s7.3× replay headroom', '—Proof node—over budget at 400 · lag p99 11 blocks', ]); }); - it('marks the core limit next when it is the lowest ceiling', () => { + it('says the next rate did not pass when the core limit is lowest below the top rate', () => { swr.result = { data: bench(40, false) }; - expect(rows(render().ranked)[0]).toBe('1Execution, validators, proof nodenext40all pass at 40 · the next rate tested did not pass'); + expect(rows(render().ranked)[0]).toBe('1Execution, validators, proof nodenext40 Mgas/sMeasured in the bench: all passed at 40 Mgas/s; the next rate tested did not pass.'); }); - it('shows a measured DA workload with its blobs per slot', () => { + it('rescales a bench DA run’s posting-window blobs to its load rate, with the details in help', () => { + // 20 blobs per slot over a window at 160 Mgas/s, rescaled to the 200 Mgas/s load: 25. swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', daResult(8e7, 20, false)), swap: null } }) }; - expect(rows(render().ranked)[0]).toBe('1DA · Payments (B20)next76 ≈ 1,896/s Measuredmeasured · 20.0 blobs/slot vs 14 at 200'); + const { ranked } = render(); + expect(rows(ranked)[0]).toBe('1DA · Payments (B20)next76 Mgas/s≈ 1,896 tx/sMeasuredMeasured in the bench: at 200 Mgas/s, needs 25 blobs per L1 slot. Target is 14, shared by all rollups.'); + expect(ranked).toContain('Run synthetic-200: 0.00200 compressed bytes per gas (compression 4.5×). Over the batch posting window it averaged 20.0 blobs per L1 slot at 160.0 Mgas/s; scaling that window to the 200 Mgas/s load gives 25.'); + expect(ranked).toContain('14 × 130,044 bytes per blob ÷ 12 s per slot ÷ bytes per gas.'); + }); + + it('notes swaps reverts above 1% only', () => { + const swapDa = (reverted: number) => ({ ...run(400, 'swap', daResult(2.6158e7, 22.4, false)), reverted }); + swr.result = { data: bench(400, true, { da: { mix: null, payment: null, swap: swapDa(0.155) } }) }; + expect(rows(render().ranked).find((r) => r.includes('DA · Swaps'))).toMatch(/needs 56 blobs per L1 slot\. Target is 14\. 16% of swaps reverted\.$/); + swr.result = { data: bench(400, true, { da: { mix: null, payment: null, swap: swapDa(0.005) } }) }; + expect(rows(render().ranked).find((r) => r.includes('DA · Swaps'))).toMatch(/Target is 14\.$/); }); -}); -describe('reverts, measured swaps and disk', () => { it('ranks a measured swaps DA ceiling by its value, past the gas limit', () => { // 4e10 gas and 2.6158e7 frame bytes put the 14-blob ceiling at ~232 Mgas/s. swr.result = { data: bench(400, true, { da: { mix: null, payment: null, swap: run(400, 'swap', daResult(2.6158e7, 22.4, false)) } }) }; const ranked = render().ranked; - expect(rows(ranked).slice(0, 4).map((r) => r.slice(0, 22))).toEqual(['1DA · Payments (B20)ne', '2DA · Mainnet mix190 ≈', '3DA · Swaps232 ≈ 5,800', '4Execution, validators']); - expect(rows(ranked)[2]).toContain('Measuredmeasured · 22.4 blobs/slot vs 14 at 400'); + expect(rows(ranked).slice(0, 4).map((r) => r.slice(0, 22))).toEqual(['1DA · Payments (B20)ne', '2DA · Mainnet mix190 M', '3DA · Swaps232 Mgas/s≈', '4Execution, validators']); const swapsRow = ranked.split(' r.includes('>DA · Swaps<'))!; expect(swapsRow).toMatch(/style="width:58\.\d+%"/); expect(swapsRow).toMatch(/border-dashed[^"]*" style="left:50%"/); }); + it('lays out the proven run as a definition list, with the change only when there is one', () => { + swr.result = { data: bench(400, true) }; + const block = text(render().ranked.split(']*>Runsynthetic-400Rate400 → 395\.0 Mgas\/sCommitbbbbbbbDate2026-01-06Change↑ from 40 Mgas\/s on 2026-01-06 · compareSteps/); + expect(render().ranked).toContain('href="https://github.com/base/base/compare/aaaa...bbbb"'); + swr.result = { data: bench(400, true, { history: [] }) }; + expect(text(render().ranked.split(' { @@ -354,30 +373,39 @@ describe('disk ceilings', () => { }; const disk = (ranked: string, label: string) => rows(ranked).find((r) => r.includes(label))!; - it('ranks both disk rows by their upper-bound ceilings among the other limits', () => { + it('ranks both disk rows by their upper-bound ceilings, with plain-language evidence', () => { swr.result = { data: withStorage() }; const { ranked } = render(); expect(rows(ranked).map((r) => r.slice(0, 26))).toEqual([ '1DA · Payments (B20)next~5', - '2DA · Swaps~115 ≈ 799/s Es', - '3Disk · archive node≤ 138u', - '4DA · Mainnet mix190 ≈ 981', + '2DA · Swaps~115 Mgas/s≈ 79', + '3Disk · archive node≤ 138 ', + '4DA · Mainnet mix190 Mgas/', '5Execution, validators, pr', - '6Disk · full node≤ 553uppe', + '6Disk · full node≤ 553 Mga', '—TEE proving, RPC under sy', ]); // (45 TB − 4 TB) ÷ (1e6 × (0.0086 bench + 0.0008 mainnet state) B/gas × 1 year); (45 TB − 1.1 TB state) ÷ (1e6 × (history 31 d + proofs 15 d + 2 years of 0.0008 B/gas state)). - expect(disk(ranked, 'archive node')).toBe("3Disk · archive node≤ 138upper bound · 1-year runway · archive 4.0 TB on 2026-01-01 (Synthetic archive snapshot) · growth at the mainnet mix run's bytes/gas + mainnet state growth 0.0008 B/gas"); - expect(disk(ranked, 'full node')).toBe("6Disk · full node≤ 553upper bound · 2-year runway · state 1.1 TB now · history 31 d + proofs 15 d at the mainnet mix run's bytes/gas · production state growth 0.0008 B/gas"); + expect(disk(ranked, 'archive node')).toBe('3Disk · archive node≤ 138 Mgas/sProjected from bench growth: one year fills the 45.0 TB disk (4.0 TB used today). Upper bound.'); + expect(disk(ranked, 'full node')).toBe('6Disk · full node≤ 553 Mgas/sProjected from bench growth: 31 days of history, 15 days of proofs and 2 years of state growth fit in 45.0 TB. Upper bound.'); }); - it('explains the runway policy and why the ceilings are upper bounds in help', () => { + it('pluralizes the runway', () => { + swr.result = { data: withStorage() }; + const snapshot = fixture(); + snapshot.policy!.runwayYears = { fullNode: 1, archive: 2 }; + const { ranked } = render({ benchUrl: BENCH }, snapshot); + expect(disk(ranked, 'archive node')).toContain('Projected from bench growth: 2 years fill the 45.0 TB disk'); + expect(disk(ranked, 'full node')).toContain('and one year of state growth fit in 45.0 TB'); + }); + + it('lists every input and why the ceilings are upper bounds in help', () => { swr.result = { data: withStorage() }; const help = render().ranked; - expect(help).toContain('Runway policy: full node 2 years, archive 1 year (Invented runway policy.).'); - expect(help).toContain('is a lower bound, because the sequencer prunes history, so this ceiling is an upper bound.'); + expect(help).toContain("Inputs: 45.0 TB disk budget (a bench config assumption); today's archive 4.0 TB from Synthetic archive snapshot on 2026-01-01 (invented size); 0.00860 bytes per gas from the mainnet mix run plus 0.0008 bytes per gas of mainnet state growth from Synthetic dashboard, since the bench run writes almost no new state; runway policy: full node 2 years, archive one year (Invented runway policy.)."); + expect(help).toContain("Inputs: 45.0 TB disk budget (a bench config assumption); today's state 1.1 TB, measured at the mainnet mix run's snapshot; 0.00860 bytes per gas of history and 0.00450 of proofs from the mainnet mix run; 0.0008 bytes per gas of production state growth from Synthetic dashboard;"); + expect(help).toContain('Upper bound: bench bytes per gas is a lower bound, because the sequencer prunes history.'); expect(help).toContain("may write more bytes per gas than mainnet traffic does (Base's whole archive is 4.0 TB)"); - expect(help).toContain('Archive size: invented size.'); }); it('counts only the bench run’s state in the archive ceiling without production state growth', () => { @@ -385,7 +413,9 @@ describe('disk ceilings', () => { const snapshot = fixture(); delete snapshot.production!.stateBytesPerGas; // (45 TB − 4 TB) ÷ (1e6 × 0.0086 B/gas × 1 year). - expect(disk(render({ benchUrl: BENCH }, snapshot).ranked, 'archive node')).toMatch(/≤ 151upper bound .*bytes\/gas bench state only$/); + const { ranked } = render({ benchUrl: BENCH }, snapshot); + expect(disk(ranked, 'archive node')).toMatch(/≤ 151 Mgas\/sProjected from bench growth/); + expect(ranked).toContain('0.00860 bytes per gas from the mainnet mix run (bench state only)'); }); it('names the missing input and leaves the row unranked', () => { @@ -409,7 +439,7 @@ describe('disk ceilings', () => { const snapshot = fixture(); snapshot.production!.archiveBytes!.value = 50e12; const { ranked } = render({ benchUrl: BENCH }, snapshot); - expect(rows(ranked)[0]).toMatch(/^1Disk · archive nodenextover budget1-year runway/); + expect(rows(ranked)[0]).toBe('1Disk · archive nodenextover budgetProjected from bench growth: today\'s archive (50.0 TB) already fills the 45.0 TB disk.'); }); }); diff --git a/app/scaling/da/workloads.ts b/app/scaling/da/workloads.ts index 9331a17..318c784 100644 --- a/app/scaling/da/workloads.ts +++ b/app/scaling/da/workloads.ts @@ -4,8 +4,8 @@ import type { Ceiling, Link } from '../snapshot.mjs'; // Ethereum's blob target and max per L1 block since BPO2 (2026-01-07), and the data one blob carries. export const BLOB_TARGET = 14; export const BLOB_MAX = 21; -const BLOB_DATA_BYTES = 130_044; -const L1_SLOT_SECONDS = 12; +export const BLOB_DATA_BYTES = 130_044; +export const L1_SLOT_SECONDS = 12; export type DaWorkload = { label: string; From 1f7e61e6758620e3274e5fc2590959732cc37f67 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 16:22:00 -0700 Subject: [PATCH 21/33] test(scaling): find the swaps row by label now that DA rows carry help --- app/scaling/components/scorecard.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 818b6c9..16ecd27 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -350,7 +350,7 @@ describe('what breaks first', () => { swr.result = { data: bench(400, true, { da: { mix: null, payment: null, swap: run(400, 'swap', daResult(2.6158e7, 22.4, false)) } }) }; const ranked = render().ranked; expect(rows(ranked).slice(0, 4).map((r) => r.slice(0, 22))).toEqual(['1DA · Payments (B20)ne', '2DA · Mainnet mix190 M', '3DA · Swaps232 Mgas/s≈', '4Execution, validators']); - const swapsRow = ranked.split(' r.includes('>DA · Swaps<'))!; + const swapsRow = ranked.split(' r.includes('>DA · Swaps'))!; expect(swapsRow).toMatch(/style="width:58\.\d+%"/); expect(swapsRow).toMatch(/border-dashed[^"]*" style="left:50%"/); }); From 956e69f7f2cc7bd1ceedd4d0d5e4f3665f805dab Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 16:36:32 -0700 Subject: [PATCH 22/33] feat(scaling): show subsystems by workload as one small table per category, ordered by what breaks first --- app/scaling/components/Bottlenecks.tsx | 2 - app/scaling/components/Scorecard.tsx | 4 +- app/scaling/components/WorkloadSubsystems.tsx | 299 +++++++++++++++--- app/scaling/components/firstScreen.test.tsx | 2 +- app/scaling/components/runBacked.test.tsx | 2 +- app/scaling/components/scorecard.test.tsx | 96 +++++- app/scaling/da/workloads.test.ts | 2 +- app/scaling/proven/capacity.test.ts | 17 +- app/scaling/proven/capacity.ts | 6 + 9 files changed, 373 insertions(+), 57 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index 6608773..7230780 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -137,8 +137,6 @@ export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven export type StorageSource = { storage: Storage; run: Run }; export const bytes = (n: number) => (n >= 1e12 ? `${(n / 1e12).toFixed(1)} TB` : `${(n / 1e9).toFixed(1)} GB`); -export const growth = (archive: Storage['archive']) => - `${archive.growthBound === 'lower' ? '≥' : archive.growthBound === 'upper' ? '≤' : '~'} ${bytes(archive.growthBytesPerYear)}/year`; const WORKLOAD: Record = { mix: 'mainnet mix', payment: 'payments', swap: 'swaps' }; // The mix run's storage data when present, else the newest payments or swaps run that has it. diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index fba146e..60bb914 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -9,7 +9,7 @@ import { Headline } from './Headline'; import { Ladder, type ProvenMark } from './Ladder'; import { day, useProvenCapacity } from './ProvenCapacity'; import { Work } from './Work'; -import { WorkloadSubsystems } from './WorkloadSubsystems'; +import { subsystemCards, WorkloadSubsystems } from './WorkloadSubsystems'; function benchMark({ mgas, atTop, run }: Proven): ProvenMark { const top = atTop ? ` ≥ because ${mgas} Mgas/s is the highest rate the bench runs.` : ''; @@ -51,7 +51,7 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl - + diff --git a/app/scaling/components/WorkloadSubsystems.tsx b/app/scaling/components/WorkloadSubsystems.tsx index 59bd31f..9821fcc 100644 --- a/app/scaling/components/WorkloadSubsystems.tsx +++ b/app/scaling/components/WorkloadSubsystems.tsx @@ -1,51 +1,266 @@ import { Card } from '../../components/ui/Card'; import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; -import type { ProvenCapacity, Run, Workload } from '../proven/capacity'; -import { growth, times } from './Bottlenecks'; -import { REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; - -const COLUMNS: [Workload, string][] = [['payment', 'Payments'], ['swap', 'Swaps'], ['mix', 'Mainnet mix (bench run)']]; -const TH = 'px-3 py-2 text-left text-[11px] font-[600] uppercase tracking-wide text-bds-gray-60'; -const TD = 'px-3 py-2 whitespace-nowrap'; - -// Each value comes from that workload's own proven run. -const ROWS: [string, (run: Run) => string | null][] = [ - ['Validators', (r) => (r.replay ? `${times(r.replay.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND)} replay headroom` : null)], - ['Proof node', (r) => { - const proofs = r.proofNode?.proofs; - const execute = proofs?.methods.debug_executePayload?.latency.p99Ms; - return proofs ? `lag p99 ${proofs.headLagBlocks.p99}${execute === undefined ? '' : ` · executePayload p99 ${Math.round(execute)} ms`}` : null; - }], - ['Disk · archive', (r) => (r.storage ? `${growth(r.storage.archive)} at ${r.mgas} Mgas/s` : null)], -]; - -export function WorkloadSubsystems({ current }: { current: ProvenCapacity['current'] | undefined }) { +import { BLOB_DATA_BYTES, BLOB_TARGET, fmt, L1_SLOT_SECONDS } from '../da/workloads'; +import { successfulTps, type ProvenCapacity, type Run, type Workload } from '../proven/capacity'; +import type { Snapshot } from '../snapshot.mjs'; +import { bytes, diskLimits, times, type Limit } from './Bottlenecks'; +import { EXECUTE_PAYLOAD_P99_MS, PROOF_LAG_P99_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; +import { Info } from './Info'; + +const COLUMNS: [Workload, string, string][] = [['payment', 'Pay', 'Payments (B20)'], ['swap', 'Swap', 'Swaps (Uniswap v3)'], ['mix', 'Mix', 'Mainnet mix (bench run)']]; +const NAME: Record = { payment: 'payments', swap: 'swaps', mix: 'mix' }; +const YEAR_SECONDS = 31_557_600; +const NO_RUN = 'no proven run for this workload'; +const NOT_IN_RUN = 'not in this run'; +const NO_STORAGE = 'needs a run with storage measurement'; + +type Cell = { value: string; sub?: string[]; bad?: boolean } | { why: string }; +type Row = { label: string; help: string; cells: Record }; +type Category = 'da' | 'disk-archive' | 'disk-full' | 'proof-node' | 'validators' | 'execution'; +type SubsystemCard = { id: Category; title: string; takeaway: string; bad: boolean; rows: Row[] }; + +const and = (items: string[]) => (items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${items.at(-1)}`); +const ms = (n: number) => `${Math.round(n)} ms`; +const size = (n: number) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)} MB` : `${(n / 1e3).toFixed(1)} KB`); +const seconds = (msValue: number) => (msValue % 1000 === 0 ? `${msValue / 1000} s` : `${msValue} ms`); +// " at 400" when every run shares a rate; otherwise each cell is at its own run's rate. +const atRate = (runs: (Run | undefined)[]) => { + const rates = [...new Set(runs.filter((r) => r !== undefined).map((r) => r.mgas))]; + return rates.length === 1 ? ` at ${rates[0]}` : rates.length > 1 ? ' at each run’s rate' : ''; +}; +const cells = (runs: Record, value: (run: Run) => Cell, missing = NO_RUN) => + Object.fromEntries(COLUMNS.map(([w]) => [w, runs[w] ? value(runs[w]) : { why: missing }])) as Record; + +// DA ceiling at the blob target with the run's own compressed bytes per gas, as in the ranked table. +const daCeiling = ({ result }: NonNullable) => (BLOB_TARGET * BLOB_DATA_BYTES) / L1_SLOT_SECONDS / (result.frame_bytes / result.gas_used) / 1e6; +// The posting window's blobs per slot, rescaled from its gas rate to the run's load rate. +const blobsAt = (run: Run) => Math.round((run.da!.result.blobs_per_l1_slot * run.mgas) / (run.da!.result.gas_per_second / 1e6)); + +function daCard(da: ProvenCapacity['da'], gasLimit: number | null, mainnetMix: Limit | undefined): SubsystemCard { + const runs = Object.fromEntries(COLUMNS.map(([w]) => [w, da[w]?.da ? da[w]! : undefined])) as Record; + const measured = COLUMNS.filter(([w]) => runs[w]); + const binding = measured.filter(([w]) => gasLimit !== null && daCeiling(runs[w]!.da!) < gasLimit).map(([w]) => NAME[w]); + const mainnet = mainnetMix?.value ? ` What breaks first uses real mainnet traffic for the mix instead: ${fmt(mainnetMix.value)} Mgas/s.` : ''; + return { + id: 'da', + title: 'DA', + takeaway: !measured.length ? 'not measured yet' : binding.length ? `binding for ${and(binding)}` : `below target at the ${gasLimit} Mgas/s gas limit`, + bad: binding.length > 0, + rows: [ + { + label: `Blobs per L1 slot${atRate(Object.values(runs))}`, + help: `Blobs each L1 slot needs at the run's load rate: the batch posting window's average blobs per slot, scaled from the window's gas rate to the load rate. The target is ${BLOB_TARGET}, shared by all rollups.`, + cells: cells(runs, (r) => ({ value: fmt(blobsAt(r)) }), NOT_IN_RUN), + }, + { + label: 'Ceiling (Mgas/s)', + help: `Gas rate that fills the ${BLOB_TARGET}-blob target: ${BLOB_TARGET} × ${BLOB_DATA_BYTES.toLocaleString('en-US')} bytes per blob ÷ ${L1_SLOT_SECONDS} s per slot ÷ the run's compressed bytes per gas. Mix is the bench run's mainnet mix.${mainnet}`, + cells: cells(runs, (r) => ({ value: fmt(daCeiling(r.da!)), bad: gasLimit !== null && daCeiling(r.da!) < gasLimit }), NOT_IN_RUN), + }, + ], + }; +} + +function diskCards(runs: Record, snapshot: Pick): SubsystemCard[] { + const withStorage = Object.values(runs).filter((r) => r?.storage); + const runway = snapshot.policy?.runwayYears; + const state = snapshot.production?.stateBytesPerGas?.value; + const storageCells = (value: (run: Run) => Cell) => cells(runs, (r) => (r.storage ? value(r) : { why: NO_STORAGE })); + const ceiling = (id: string) => (r: Run): Cell => { + const limit = diskLimits({ storage: r.storage!, run: r }, snapshot).find((l) => l.id === id)!; + return limit.value === null ? { why: limit.evidence } : limit.value === 0 ? { value: 'over budget', bad: true } : { value: limit.ceiling }; + }; + const card = (id: Category, title: string, years: number | undefined, rows: Row[]): SubsystemCard => ({ + id, title, rows, bad: false, + takeaway: !withStorage.length ? 'not measured yet' : years ? `${years}-year runway` : 'no runway policy', + }); + const ceilingLabel = (years: number | undefined) => `Ceiling${years ? ` (${years}-year runway)` : ''}`; + const ceilingHelp = 'Highest gas rate at which the disk budget holds the runway, from What breaks first. An upper bound: bench bytes per gas is a lower bound, because the sequencer prunes history.'; + return [ + card('disk-archive', 'Disk · archive', runway?.archive, [ + { + label: `Growth${atRate(withStorage)}`, + help: `Archive growth per year at the run's rate: the run's archive bytes per gas${state ? ` plus ${state} bytes per gas of mainnet state growth` : ''}. A lower bound, because the sequencer prunes history.`, + cells: storageCells((r) => ({ value: `≥ ${bytes((r.storage!.bytesPerGas.archive + (state ?? 0)) * r.mgas * 1e6 * YEAR_SECONDS)}/yr` })), + }, + { label: ceilingLabel(runway?.archive), help: ceilingHelp, cells: storageCells(ceiling('disk-archive')) }, + ]), + card('disk-full', 'Disk · full node', runway?.fullNode, [ + { + label: `Size${atRate(withStorage)}`, + help: 'History and proofs a full node keeps at the run\'s rate: history bytes per gas over the history window plus proof bytes per gas over the proof window.', + cells: storageCells(({ mgas, storage }) => { + const { bytesPerGas: b, fullNode: f } = storage!; + return b.proofs === null ? { why: NOT_IN_RUN } : { value: bytes(mgas * 1e6 * (b.history * f.historyWindowSeconds + b.proofs * f.proofWindowSeconds)) }; + }), + }, + { label: ceilingLabel(runway?.fullNode), help: ceilingHelp, cells: storageCells(ceiling('disk-full')) }, + ]), + ]; +} + +function proofCard(runs: Record): SubsystemCard { + const proofs = (r: Run) => r.proofNode?.proofs ?? null; + const execute = (r: Run) => proofs(r)?.methods.debug_executePayload?.latency.p99Ms; + const measured = COLUMNS.filter(([w]) => runs[w] && proofs(runs[w])); + const lagOver = measured.filter(([w]) => proofs(runs[w]!)!.headLagBlocks.p99 > PROOF_LAG_P99_BLOCKS).map(([w]) => NAME[w]); + const executeOver = measured.filter(([w]) => (execute(runs[w]!) ?? 0) > EXECUTE_PAYLOAD_P99_MS).map(([w]) => NAME[w]); + const over = [ + lagOver.length && `lag over ${PROOF_LAG_P99_BLOCKS} for ${and(lagOver)}`, + executeOver.length && `executePayload over ${seconds(EXECUTE_PAYLOAD_P99_MS)} for ${and(executeOver)}`, + ].filter((s): s is string => !!s); + const proofCells = (value: (p: NonNullable>, r: Run) => Cell) => cells(runs, (r) => { + const p = proofs(r); + return p ? value(p, r) : { why: NOT_IN_RUN }; + }); + return { + id: 'proof-node', + title: 'Proof node', + takeaway: !measured.length ? 'not measured yet' : over.length ? over.join('; ') : `within budget (lag ≤ ${PROOF_LAG_P99_BLOCKS}, ≤ ${seconds(EXECUTE_PAYLOAD_P99_MS)})`, + bad: over.length > 0, + rows: [ + { + label: 'Lag p99 (max)', + help: `Blocks the proof node trails the sequencer's head, p99 and max over the run. Budget: p99 ≤ ${PROOF_LAG_P99_BLOCKS}.`, + cells: proofCells(({ headLagBlocks: l }) => ({ value: `${l.p99} (${l.max})`, bad: l.p99 > PROOF_LAG_P99_BLOCKS })), + }, + { + label: 'executePayload p99', + help: `p99 latency of debug_executePayload on the proof node. Budget: ≤ ${seconds(EXECUTE_PAYLOAD_P99_MS)}.`, + cells: proofCells((_, r) => { + const p99 = execute(r); + return p99 === undefined ? { why: NOT_IN_RUN } : { value: ms(p99), bad: p99 > EXECUTE_PAYLOAD_P99_MS }; + }), + }, + { + label: 'Witness p50', + help: 'Median execution witness size the proof node serves per block.', + cells: proofCells(({ witnessBytes: w }) => (w.samples ? { value: size(w.p50) } : { why: NOT_IN_RUN })), + }, + ], + }; +} + +function validatorCard(runs: Record): SubsystemCard { + const measured = COLUMNS.filter(([w]) => runs[w]?.replay); + const behind = measured.filter(([w]) => !runs[w]!.replay!.match || runs[w]!.replay!.blocksPerSecond < REPLAY_BLOCKS_PER_SECOND).map(([w]) => NAME[w]); + const replayCells = (value: (r: NonNullable) => Cell) => cells(runs, (r) => (r.replay ? value(r.replay) : { why: NOT_IN_RUN })); + return { + id: 'validators', + title: 'Validators', + takeaway: !measured.length ? 'not measured yet' : behind.length ? `${and(behind)} ${behind.length === 1 ? 'does' : 'do'} not keep up` : 'all keep up', + bad: behind.length > 0, + rows: [ + { + label: 'Replay speed', + help: `How many times faster than real time a validator replays the run's blocks: blocks replayed per second ÷ the ${REPLAY_BLOCKS_PER_SECOND} the chain produces. Below 1× it falls behind.`, + cells: replayCells((r) => ({ value: times(r.blocksPerSecond / REPLAY_BLOCKS_PER_SECOND), bad: !r.match || r.blocksPerSecond < REPLAY_BLOCKS_PER_SECOND })), + }, + { + label: 'newPayload p50 / p99', + help: 'Validator engine_newPayload latency during replay, p50 and p99.', + cells: replayCells(({ newPayload: n }) => ({ value: `${Math.round(n.p50Ms)} / ${ms(n.p99Ms)}` })), + }, + ], + }; +} + +function sequencerCard(runs: Record): SubsystemCard { + const rates = [...new Set(Object.values(runs).flatMap((r) => (r ? [r.mgas] : [])))].sort((a, b) => a - b); + return { + id: 'execution', + title: 'Sequencer', + takeaway: !rates.length ? 'no proven run' : `${rates.length === 1 ? rates[0] : `${rates[0]}–${rates.at(-1)}`} delivered`, + bad: false, + rows: [ + { + label: 'Delivered (Mgas/s)', + help: 'Gas rate the load test delivered, and successful transactions per second; reverted transactions use gas but are not counted.', + cells: cells(runs, (r) => ({ + value: (r.gps / 1e6).toFixed(1), + sub: [`${fmt(successfulTps(r))} tx/s`, ...(r.reverted > 0.01 ? [`${Math.round(r.reverted * 100)}% reverted`] : [])], + })), + }, + { + label: 'Block latency p50 / p99', + help: 'Time from sending a transaction to seeing it in a block, from the load test.', + cells: cells(runs, (r) => (r.blockLatency ? { value: `${Math.round(r.blockLatency.p50Ms)} / ${ms(r.blockLatency.p99Ms)}` } : { why: NOT_IN_RUN })), + }, + ], + }; +} + +const MATCHES: Record boolean> = { + da: (id) => id.startsWith('da-'), + 'disk-archive': (id) => id === 'disk-archive', + 'disk-full': (id) => id === 'disk-full', + 'proof-node': (id) => id === 'proof-node' || id === 'core', + validators: (id) => id === 'validators' || id === 'core', + execution: (id) => id === 'execution' || id === 'core', +}; + +// Cards follow the ranked table: a category's first ranked row sets its place; unranked ones keep the default order. +export function subsystemCards(data: ProvenCapacity | undefined, limits: Limit[], snapshot: Pick, gasLimit: number | null): SubsystemCard[] { + const runs = Object.fromEntries(COLUMNS.map(([w]) => [w, data?.current[w]?.run])) as Record; + const da = data?.da ?? { mix: null, payment: null, swap: null }; + const cards = [daCard(da, gasLimit, limits.find((l) => l.id === 'da-mix')), ...diskCards(runs, snapshot), proofCard(runs), validatorCard(runs), sequencerCard(runs)]; + const rank = (c: SubsystemCard) => { + const i = limits.findIndex((l) => l.value !== null && MATCHES[c.id](l.id)); + return i < 0 ? Infinity : i; + }; + return cards.toSorted((a, b) => rank(a) - rank(b)); +} + +const TH = 'px-2 py-1.5 min-[760px]:px-3 text-right text-[11px] font-[600] uppercase tracking-wide text-bds-gray-60'; +const TD = 'px-2 py-1.5 min-[760px]:px-3'; + +function Value({ cell }: { cell: Cell }) { + if ('why' in cell) { + return — ({cell.why}); + } + return ( + <> + {/* Each side of " / " stays whole; narrow cards break between them. */} + {cell.value.split(' / ').map((part, i) => {i > 0 ? ' / ' : null}{part})} + {cell.sub?.map((s) => {s})} + + ); +} + +export function WorkloadSubsystems({ cards }: { cards: SubsystemCard[] }) { return (
      Subsystems by workload - - - - - )} - - - - {ROWS.map(([label, value]) => ( - - - {COLUMNS.map(([w]) => { - const run = current?.[w]?.run; - const v = run ? value(run) : null; - return ; - })} - - ))} - -
      - {COLUMNS.map(([w, label]) => {label}
      {label}{v ?? '—'}
      -
      +
      + {cards.map((c) => ( + +

      + {c.title} · {c.takeaway} +

      +
      + + + + + {COLUMNS.map(([w, short, full]) => )} + + + + {c.rows.map((r) => ( + + + {COLUMNS.map(([w]) => )} + + ))} + +
      Metric{short}
      + {r.label} {r.help} +
      +
      +
      + ))} +
      ); } diff --git a/app/scaling/components/firstScreen.test.tsx b/app/scaling/components/firstScreen.test.tsx index ade6505..419b518 100644 --- a/app/scaling/components/firstScreen.test.tsx +++ b/app/scaling/components/firstScreen.test.tsx @@ -67,7 +67,7 @@ describe('Guardrails', () => { describe('scorecardGuardrails', () => { const LATENCY = { p50Ms: 2, p99Ms: 9, maxMs: 12 }; const run = (blocksPerSecond: number, lagP99 = 4, executeP99 = 9): Run => ({ - id: 'synthetic', outputDir: 'synthetic-400', workload: 'mix', mgas: 400, gps: 4e8, tps: 6000, reverted: 0, storage: null, passed: true, commit: 'c0ffee', + id: 'synthetic', outputDir: 'synthetic-400', workload: 'mix', mgas: 400, gps: 4e8, tps: 6000, reverted: 0, storage: null, blockLatency: null, passed: true, commit: 'c0ffee', createdAt: Date.parse('2026-01-05T00:00:00Z'), steps: [], da: null, replay: { blocksPerSecond, newPayload: LATENCY, match: true }, proofNode: { diff --git a/app/scaling/components/runBacked.test.tsx b/app/scaling/components/runBacked.test.tsx index c093a8a..ae565cd 100644 --- a/app/scaling/components/runBacked.test.tsx +++ b/app/scaling/components/runBacked.test.tsx @@ -77,7 +77,7 @@ describe('DA from bench runs', () => { withinTarget, }); const run = (workload: Run['workload'], runDa: Run['da']): Run => ({ - id: `synthetic-${workload}`, outputDir: `synthetic-${workload}`, workload, mgas: 200, gps: 2e8, tps: 4000, reverted: 0, storage: null, passed: true, + id: `synthetic-${workload}`, outputDir: `synthetic-${workload}`, workload, mgas: 200, gps: 2e8, tps: 4000, reverted: 0, storage: null, blockLatency: null, passed: true, commit: 'c0ffee', createdAt: Date.parse('2026-01-05T00:00:00Z'), steps: [], replay: null, proofNode: null, da: runDa, }); const rows = (html: string) => html.split(' ({ - id: `synthetic-${workload}-${mgas}`, outputDir: `synthetic-${mgas}`, workload, mgas, gps: (mgas - 5) * 1e6, tps: mgas * 10.5, reverted: 0, storage: null, passed: true, + id: `synthetic-${workload}-${mgas}`, outputDir: `synthetic-${mgas}`, workload, mgas, gps: (mgas - 5) * 1e6, tps: mgas * 10.5, reverted: 0, storage: null, blockLatency: null, passed: true, commit: 'bbbbbbbbbbbbbbbb', createdAt: T0 + 5 * DAY, steps: STEPS, da, replay: { blocksPerSecond: 36.4, newPayload: LATENCY, match: true }, proofNode: { @@ -200,12 +200,94 @@ describe('headroom span', () => { }); describe('subsystems by workload', () => { - it('lists replay headroom, proof-node latency and archive growth per workload, with — when missing', () => { - swr.result = { data: bench(400, true, { current: { mix: { mgas: 400, atTop: true, run: { ...run(400), storage: storage() } }, swap: null, payment: { mgas: 200, atTop: true, run: run(200, 'payment') } } }) }; - expect(rows(render().subsystems)).toEqual([ - 'Validators7.3× replay headroom—7.3× replay headroom', - 'Proof nodelag p99 4 · executePayload p99 733 ms—lag p99 4 · executePayload p99 733 ms', - 'Disk · archive——≥ 5.0 TB/year at 400 Mgas/s', + const cards = (html: string) => html.split(' ` cards(html).map((c) => text(c.split('')[0])); + const card = (html: string, title: string) => cards(html).find((c) => text(c.split('')[0]).startsWith(title))!; + const withStorage = () => { + const data = bench(400, true); + data.current.mix = { mgas: 400, atTop: true, run: { ...run(400), storage: storage() } }; + return data; + }; + + it('orders cards by their category’s place in What breaks first, unranked ones last in the default order', () => { + swr.result = { data: bench(400, true) }; + // DA ~57 first; the merged core row ties proof node, validators and sequencer; no storage leaves both disks unranked. + expect(titles(render().subsystems).map((t) => t.split(' · ')[0])).toEqual(['DA', 'Proof node', 'Validators', 'Sequencer', 'Disk', 'Disk']); + swr.result = { data: withStorage() }; + expect(titles(render().subsystems)).toEqual([ + 'DA · not measured yet', + 'Disk · archive · 1-year runway', + 'Proof node · within budget (lag ≤ 10, ≤ 1 s)', + 'Validators · all keep up', + 'Sequencer · 400 delivered', + 'Disk · full node · 2-year runway', + ]); + }); + + it('stacks one card per row below 760 px and labels columns with full names', () => { + swr.result = { data: bench(400, true) }; + const html = render().subsystems; + expect(html).toContain('grid grid-cols-1 gap-3 min-[760px]:grid-cols-2'); + expect(html).toContain('Pay'); + expect(html).toContain('title="Mainnet mix (bench run)"'); + }); + + it('rescales DA blobs to the load rate and names workloads whose ceiling is under the gas limit', () => { + // Payments: 20 blobs over a 160 Mgas/s window at a 200 Mgas/s load → 25, ceiling ~76. Swaps: ~232, above the 200 gas limit. + swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', daResult(8e7, 20, false)), swap: run(400, 'swap', daResult(2.6158e7, 22.4, false)) } }) }; + const da = card(render().subsystems, 'DA'); + expect(text(da.split('')[0])).toBe('DA · binding for payments'); + expect(da.split('')[0]).toContain('text-bds-red-60'); + expect(rows(da)).toEqual(['Blobs per L1 slot at each run’s rate 2556— (not in this run)', 'Ceiling (Mgas/s) 76232— (not in this run)']); + swr.result = { data: bench(400, true, { da: { mix: null, payment: null, swap: run(400, 'swap', daResult(2.6158e7, 22.4, false)) } }) }; + expect(text(card(render().subsystems, 'DA').split('')[0])).toBe('DA · below target at the 200 Mgas/s gas limit'); + }); + + it('projects disk per workload run, saying which runs lack a storage measurement', () => { + const data = withStorage(); + data.current.payment = { mgas: 200, atTop: true, run: run(200, 'payment') }; + swr.result = { data }; + const html = render().subsystems; + // (0.0086 bench + 0.0008 mainnet state) B/gas × 400 Mgas/s × 1 year; (0.0086 × 31 d + 0.0045 × 15 d) × 400 Mgas/s. + expect(rows(card(html, 'Disk · archive'))).toEqual([ + 'Growth at 400 — (needs a run with storage measurement)— (no proven run for this workload)≥ 118.7 TB/yr', + 'Ceiling (1-year runway) — (needs a run with storage measurement)— (no proven run for this workload)≤ 138', + ]); + expect(rows(card(html, 'Disk · full node'))[0]).toBe('Size at 400 — (needs a run with storage measurement)— (no proven run for this workload)11.5 TB'); + expect(html).toContain('title="needs a run with storage measurement"'); + }); + + it('flags proof-node budgets and validators that fall behind', () => { + const data = bench(400, true); + const slow = run(400, 'mix', null, 11); + slow.proofNode!.proofs!.methods.debug_executePayload.latency.p99Ms = 1200; + slow.replay = { ...slow.replay!, blocksPerSecond: 4 }; + data.current.mix = { mgas: 400, atTop: true, run: slow }; + data.current.swap = { mgas: 400, atTop: true, run: run(400, 'swap') }; + swr.result = { data }; + const html = render().subsystems; + expect(text(card(html, 'Proof node').split('')[0])).toBe('Proof node · lag over 10 for mix; executePayload over 1 s for mix'); + expect(rows(card(html, 'Proof node'))).toEqual([ + 'Lag p99 (max) — (no proven run for this workload)4 (20)11 (20)', + 'executePayload p99 — (no proven run for this workload)733 ms1200 ms', + 'Witness p50 — (no proven run for this workload)— (not in this run)— (not in this run)', + ]); + expect(text(card(html, 'Validators').split('')[0])).toBe('Validators · mix does not keep up'); + expect(rows(card(html, 'Validators'))[0]).toBe('Replay speed — (no proven run for this workload)7.3×0.8×'); + expect(card(html, 'Validators').split('')[0]).toContain('text-bds-red-60'); + }); + + it('shows delivered rate, successful tx/s with reverts over 1%, and block latency', () => { + const data = bench(400, true); + data.current.swap = { mgas: 400, atTop: true, run: { ...run(400, 'swap'), reverted: 0.155, blockLatency: { p50Ms: 205.4, p99Ms: 306.3 } } }; + data.current.payment = { mgas: 200, atTop: true, run: { ...run(200, 'payment'), reverted: 0.005 } }; + swr.result = { data }; + const seq = card(render().subsystems, 'Sequencer'); + expect(text(seq.split('')[0])).toBe('Sequencer · 200–400 delivered'); + // 4,200 submitted tx/s × (1 − 15.5% reverted) = 3,549. + expect(rows(seq)).toEqual([ + 'Delivered (Mgas/s) 195.02,090 tx/s395.03,549 tx/s16% reverted395.04,200 tx/s', + 'Block latency p50 / p99 — (not in this run)205 / 306 ms— (not in this run)', ]); }); }); diff --git a/app/scaling/da/workloads.test.ts b/app/scaling/da/workloads.test.ts index 3d6617c..3d83a2a 100644 --- a/app/scaling/da/workloads.test.ts +++ b/app/scaling/da/workloads.test.ts @@ -35,7 +35,7 @@ it('is modeled once every workload is measured', () => { }); const run = (workload: Run['workload'], da: Run['da']): Run => ({ - id: `synthetic-${workload}`, outputDir: `synthetic-${workload}`, workload, mgas: 200, gps: 2e8, tps: 4000, reverted: 0, storage: null, passed: true, + id: `synthetic-${workload}`, outputDir: `synthetic-${workload}`, workload, mgas: 200, gps: 2e8, tps: 4000, reverted: 0, storage: null, blockLatency: null, passed: true, commit: 'c0ffee', createdAt: Date.parse('2026-01-05T00:00:00Z'), steps: [], replay: null, proofNode: null, da, }); const da = (frame_bytes: number, gas_used: number, transactions: number): Da => ({ diff --git a/app/scaling/proven/capacity.test.ts b/app/scaling/proven/capacity.test.ts index 68ff925..c4c0a6a 100644 --- a/app/scaling/proven/capacity.test.ts +++ b/app/scaling/proven/capacity.test.ts @@ -20,6 +20,8 @@ type Spec = { // false: written before the step pipeline, so no pipeline-result.json. pipeline?: boolean; missing?: string[]; + // Older load-test-result.json files have no block_latency. + noLatency?: boolean; withinTarget?: boolean | null; }; @@ -70,7 +72,10 @@ function load(specs: Spec[]) { const delivered = (s.delivered ?? 1) * s.mgas; switch (file) { case 'validation.json': return Response.json({ comparable: s.comparable ?? true, reasons: [] }); - case 'load-test-result.json': return Response.json({ throughput: { gps: delivered * 1e6, tps: delivered * 10, total_submitted: 1000, total_reverted: 155 } }); + case 'load-test-result.json': return Response.json({ + throughput: { gps: delivered * 1e6, tps: delivered * 10, total_submitted: 1000, total_reverted: 155 }, + ...(s.noLatency ? {} : { block_latency: { p50: { secs: 0, nanos: 205_382_593 }, p99: { secs: 1, nanos: 6_265_374 } } }), + }); case 'pipeline-result.json': return Response.json({ suite: 'synthetic', comparable: s.comparable ?? true, steps: STEPS(s) }); case 'replay-result.json': return Response.json(REPLAY); case 'proof-node/replay-result.json': return Response.json({ ...REPLAY, blocksPerSecond: 20, proofs: PROOFS }); @@ -101,6 +106,7 @@ it('counts only pipeline runs built from main, with their steps, replays and DA gps: 100e6, tps: 1000, reverted: 0.155, + blockLatency: { p50Ms: 205.382593, p99Ms: 1006.265374 }, passed: true, commit: 'rev-main-100', createdAt: NOW - 3 * DAY, @@ -277,3 +283,12 @@ it('revalidates the run index instead of trusting a cached copy', async () => { await loadProvenCapacity('https://bench.example', fetchImpl as typeof fetch, NOW); expect(inits.get('https://bench.example/output/metadata.json')?.cache).toBe('no-cache'); }); + +it('reads block latency from load-test-result.json as milliseconds, and its absence as null', async () => { + const { current } = await load([ + { dir: 'latency-100', mgas: 100, daysAgo: 1 }, + { dir: 'old-200', mgas: 200, daysAgo: 1, noLatency: true, workload: 'swap' }, + ]); + expect(current.mix?.run.blockLatency).toEqual({ p50Ms: 205.382593, p99Ms: 1006.265374 }); + expect(current.swap?.run.blockLatency).toBeNull(); +}); diff --git a/app/scaling/proven/capacity.ts b/app/scaling/proven/capacity.ts index c98ab70..30faab2 100644 --- a/app/scaling/proven/capacity.ts +++ b/app/scaling/proven/capacity.ts @@ -71,6 +71,8 @@ export type Run = { proofNode: (Replay & { proofs: Proofs | null }) | null; da: Da | null; storage: Storage | null; + // load-test-result.json's block_latency: transaction send to inclusion. + blockLatency: { p50Ms: number; p99Ms: number } | null; }; // Reverted transactions count toward gas/s but not toward transactions per second. @@ -196,6 +198,7 @@ async function loadRun(root: string, meta: BenchmarkRun, fetchImpl: typeof fetch gps, tps: load.throughput.tps, reverted: load.throughput.total_submitted ? (load.throughput.total_reverted ?? 0) / load.throughput.total_submitted : 0, + blockLatency: load.block_latency?.p50 && load.block_latency?.p99 ? { p50Ms: durationMs(load.block_latency.p50), p99Ms: durationMs(load.block_latency.p99) } : null, passed: validation.comparable === true && gps >= PASS_RATIO * mgas * 1e6, commit: provenance.source.revision, createdAt: Date.parse(meta.createdAt), @@ -225,6 +228,9 @@ async function loadDetails(root: string, run: Run, fetchImpl: typeof fetch): Pro }; } +// Rust Durations serialize as { secs, nanos }. +const durationMs = (d: { secs: number; nanos: number }) => d.secs * 1e3 + d.nanos / 1e6; + // replay-result.json also lists every block's timings; keep only the summary. const pickReplay = ({ blocksPerSecond, newPayload, match }: Replay): Replay => ({ blocksPerSecond, newPayload, match }); From 59e5823bac611762168d83ceecd1ff2b082a7510 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 16:40:02 -0700 Subject: [PATCH 23/33] feat(scaling): take the mainnet archive size, proofs included, from chain.base.org, falling back to the snapshot --- app/scaling/archiveSize.test.ts | 65 +++++++++++++++++++++++++++++++++ app/scaling/archiveSize.ts | 61 +++++++++++++++++++++++++++++++ app/scaling/page.test.tsx | 3 ++ app/scaling/page.tsx | 3 +- 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 app/scaling/archiveSize.test.ts create mode 100644 app/scaling/archiveSize.ts diff --git a/app/scaling/archiveSize.test.ts b/app/scaling/archiveSize.test.ts new file mode 100644 index 0000000..0b732c9 --- /dev/null +++ b/app/scaling/archiveSize.test.ts @@ -0,0 +1,65 @@ +import { readFileSync } from 'node:fs'; + +import { afterEach, expect, it, vi } from 'vitest'; + +import { ARCHIVE_SNAPSHOTS_URL, fetchArchiveSize, loadArchiveSize, parseArchiveSize, withArchiveSize } from './archiveSize'; +import type { Snapshot } from './snapshot.mjs'; + +// Synthetic sizes in the chain.base.org shape, with extra fields the parser must ignore. +const ENTRIES = [ + { network: 'sepolia', profile: 'archive', block: 9, date: '2026-01-01', size: 1e12, components: [] }, + { + chainId: '8453', network: 'mainnet', profile: 'archive', block: 1_234_567, timestamp: '1767225600', date: '2026-01-01', size: 6e12, isModular: true, + components: [{ name: 'state', displayName: 'State', size: 1e12 }, { name: 'proofs', size: 1.5e12 }, { name: 'receipts', size: 2e12, fullSize: 1e11 }], + }, +]; +const ARCHIVE = { size: 6e12, sizeWithoutProofs: 4.5e12, block: 1_234_567, date: '2026-01-01', source: 'chain.base.org' }; +const fixture = (network: string): Snapshot => ({ ...JSON.parse(readFileSync(new URL('./fixtures/synthetic.v2.json', import.meta.url), 'utf8')), network }); + +afterEach(() => vi.restoreAllMocks()); + +it('reads the mainnet archive entry, total including proofs and without them', () => { + expect(parseArchiveSize(ENTRIES)).toEqual(ARCHIVE); +}); + +it('rejects a response that is not the expected shape', () => { + const mainnet = ENTRIES[1]; + for (const bad of [ + {}, + [ENTRIES[0]], + [{ ...mainnet, size: '6e12' }], + [{ ...mainnet, size: 0 }], + [{ ...mainnet, block: 1.5 }], + [{ ...mainnet, date: 'yesterday' }], + [{ ...mainnet, components: null }], + [{ ...mainnet, components: [{ name: 'proofs', size: -1 }] }], + [{ ...mainnet, components: [{ name: 'proofs', size: 7e12 }] }], + ]) expect(parseArchiveSize(bad)).toBeNull(); +}); + +it('fetches with a timeout and throws on a failed or malformed response', async () => { + const fetchImpl = vi.fn(async () => Response.json(ENTRIES)); + expect(await fetchArchiveSize(fetchImpl as unknown as typeof fetch)).toEqual(ARCHIVE); + expect(fetchImpl).toHaveBeenCalledWith(ARCHIVE_SNAPSHOTS_URL, { signal: expect.any(AbortSignal) }); + await expect(fetchArchiveSize((async () => new Response('down', { status: 502 })) as typeof fetch)).rejects.toThrow('502'); + await expect(fetchArchiveSize((async () => Response.json([{ network: 'mainnet' }])) as typeof fetch)).rejects.toThrow('unexpected response shape'); +}); + +it('falls back to null on a timeout, logging why', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const timeout = () => Promise.reject(new DOMException('The operation was aborted due to timeout', 'TimeoutError')); + expect(await loadArchiveSize(timeout)).toBeNull(); + expect(warn).toHaveBeenCalledOnce(); + expect(await loadArchiveSize(async () => ARCHIVE as never)).toEqual(ARCHIVE); +}); + +it('replaces a mainnet snapshot’s archive size and leaves others or a failed fetch alone', () => { + const mainnet = fixture('Base mainnet'); + expect(withArchiveSize(mainnet, ARCHIVE as never).production!.archiveBytes).toEqual({ + value: 6e12, asOf: '2026-01-01', source: 'chain.base.org snapshot, block 1,234,567', note: 'incl. proofs', + }); + expect(withArchiveSize(mainnet, ARCHIVE as never).production!.stateBytesPerGas).toEqual(mainnet.production!.stateBytesPerGas); + expect(withArchiveSize(mainnet, null)).toBe(mainnet); + const synthetic = fixture('synthetic-testnet'); + expect(withArchiveSize(synthetic, ARCHIVE as never)).toBe(synthetic); +}); diff --git a/app/scaling/archiveSize.ts b/app/scaling/archiveSize.ts new file mode 100644 index 0000000..f007d2c --- /dev/null +++ b/app/scaling/archiveSize.ts @@ -0,0 +1,61 @@ +import { unstable_cache } from 'next/cache'; + +import type { Snapshot } from './snapshot.mjs'; + +// chain.base.org lists the latest node snapshots publicly. It sends no CORS header, so the server fetches it. +export const ARCHIVE_SNAPSHOTS_URL = 'https://chain.base.org/api/snapshots'; +const TIMEOUT_MS = 5_000; +const REVALIDATE_SECONDS = 3_600; + +// `size` is the whole archive snapshot, proofs included: production nodes run archive with proofs. +export type ArchiveSize = { size: number; sizeWithoutProofs: number; block: number; date: string; source: 'chain.base.org' }; + +const isObj = (v: unknown): v is Record => typeof v === 'object' && v !== null && !Array.isArray(v); +const isSize = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v) && v >= 0; + +// The mainnet archive entry, checked field by field; extra fields are ignored. Null when the shape is off. +export function parseArchiveSize(json: unknown): ArchiveSize | null { + if (!Array.isArray(json)) return null; + const entry = json.find((e) => isObj(e) && e.network === 'mainnet' && e.profile === 'archive'); + if (!isObj(entry)) return null; + const { size, block, date, components } = entry; + if (!isSize(size) || size === 0 || !Number.isSafeInteger(block) || (block as number) <= 0) return null; + if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date) || !Array.isArray(components)) return null; + if (!components.every((c) => isObj(c) && typeof c.name === 'string' && isSize(c.size))) return null; + const proofs = (components as { name: string; size: number }[]).find((c) => c.name === 'proofs')?.size ?? 0; + if (proofs > size) return null; + return { size, sizeWithoutProofs: size - proofs, block: block as number, date, source: 'chain.base.org' }; +} + +// Throws on any failure so a failed fetch is not cached. +export async function fetchArchiveSize(fetchImpl: typeof fetch = fetch): Promise { + const res = await fetchImpl(ARCHIVE_SNAPSHOTS_URL, { signal: AbortSignal.timeout(TIMEOUT_MS) }); + if (!res.ok) throw new Error(`${ARCHIVE_SNAPSHOTS_URL}: ${res.status}`); + const archive = parseArchiveSize(await res.json()); + if (!archive) throw new Error(`${ARCHIVE_SNAPSHOTS_URL}: unexpected response shape`); + return archive; +} + +const fetchArchiveSizeCached = unstable_cache(() => fetchArchiveSize(), ['scaling-archive-size'], { revalidate: REVALIDATE_SECONDS }); + +// Optional context: on failure the page keeps the snapshot's archive size. +export async function loadArchiveSize(load: () => Promise = fetchArchiveSizeCached): Promise { + try { + return await load(); + } catch (error) { + console.warn('[scaling] archive size from chain.base.org unavailable, using the snapshot value:', error); + return null; + } +} + +// Only mainnet snapshots take the live mainnet archive size; testnet and synthetic ones keep their own. +export function withArchiveSize(snapshot: Snapshot, archive: ArchiveSize | null): Snapshot { + if (!archive || !/\bmainnet\b/i.test(snapshot.network)) return snapshot; + return { + ...snapshot, + production: { + ...snapshot.production, + archiveBytes: { value: archive.size, asOf: archive.date, source: `chain.base.org snapshot, block ${archive.block.toLocaleString('en-US')}`, note: 'incl. proofs' }, + }, + }; +} diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 5939561..bccef45 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -11,6 +11,9 @@ import fixture from './fixtures/synthetic.v2.json'; import ScalingGuardrailsPage from './guardrails/page'; import ScalingPage from './page'; +// No network in tests: the live archive size is unavailable, so the page keeps the snapshot's. +vi.mock('./archiveSize', async (original) => ({ ...(await original()), loadArchiveSize: async () => null })); + beforeEach(() => vi.stubEnv('SCALING_SNAPSHOT_PATH', fileURLToPath(new URL('./fixtures/synthetic.v2.json', import.meta.url)))); afterEach(() => vi.unstubAllEnvs()); diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index 3e5589f..8f27b5b 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; import { Text } from '../components/ui/Text'; import { Scorecard } from './components/Scorecard'; +import { loadArchiveSize, withArchiveSize } from './archiveSize'; import { Header, loadSnapshot, Unavailable } from './loadSnapshot'; // The snapshot is a runtime file, so render per request rather than at build. @@ -12,7 +13,7 @@ export default async function ScalingPage() { const loaded = await loadSnapshot(); if (loaded.kind !== 'ok') return ; - const { snapshot } = loaded; + const snapshot = withArchiveSize(loaded.snapshot, await loadArchiveSize()); const dashboardUrl = process.env.SCALING_DATADOG_DASHBOARD_URL; return (
      From 8f4210cef324d9fdc8fccdb3ee354dd13d89123f Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 16:42:40 -0700 Subject: [PATCH 24/33] feat(scaling): rank the mix by the bench mix run's DA, like payments and swaps --- app/scaling/components/Bottlenecks.tsx | 20 ++++++------------ app/scaling/components/Headline.tsx | 6 ++++-- app/scaling/components/Scorecard.tsx | 10 ++++++--- app/scaling/components/WorkloadSubsystems.tsx | 7 +++---- app/scaling/components/scorecard.test.tsx | 21 ++++++++++++------- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index 7230780..8a024c8 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -61,7 +61,7 @@ const blobRule = (target: number) => `The ceiling is the gas rate that fills the ${target}-blob target: ${target} × ${fmt(BLOB_DATA_BYTES)} bytes per blob ÷ ${L1_SLOT_SECONDS} s per slot ÷ bytes per gas.`; // Evidence and help for a DA row. `first` marks the lowest DA row, where the shared target is named. -function daText(w: DaWorkload, ceiling: number, first: boolean, today: number | null, provenMgas: number | null): { evidence: string; help: string } { +function daText(w: DaWorkload, first: boolean): { evidence: string; help: string } { const shared = first ? ', shared by all rollups' : ''; const bench = w.bench && w.source?.url === runHref(w.bench.run) ? w.bench : null; if (bench) { @@ -76,18 +76,10 @@ function daText(w: DaWorkload, ceiling: number, first: boolean, today: number | help: `Run ${run.outputDir}: ${da.result.bytes_per_gas.toPrecision(3)} compressed bytes per gas (compression ${da.result.compression_ratio.toFixed(1)}×). Over the batch posting window it averaged ${da.result.blobs_per_l1_slot.toFixed(1)} blobs per L1 slot at ${windowMgas.toFixed(1)} Mgas/s; scaling that window to the ${run.mgas} Mgas/s load gives ${blobs}. ${blobRule(target)}`, }; } - const target = BLOB_TARGET; - if (w.tag === 'estimated') { - return { - evidence: `Estimated until a bench run measures it. Target is ${target}${shared}.`, - help: `Estimated ${fmt(w.bytesPerTx)} compressed bytes and ${fmt(w.gasPerTx)} gas per transaction. ${blobRule(target)}`, - }; - } - const now = today === null ? null : `today's traffic uses about ${((target * today) / ceiling).toFixed(1)} blobs per L1 slot`; - const atProven = provenMgas === null ? null : `at ${provenMgas} Mgas/s it would need ${Math.round((target * provenMgas) / ceiling)}`; + // Without a bench DA run every /scaling workload, the mix included, is an estimate. return { - evidence: `Measured on mainnet${now || atProven ? `: ${[now, atProven].filter(Boolean).join('; ')}` : ''}. Target is ${target}${shared}.`, - help: `Source: ${w.source?.label ?? 'mainnet'} (mainnet blocks run through the production batcher encoder). Mainnet averages ${fmt(w.gasPerTx)} gas and ${w.bytesPerTx} compressed bytes per transaction. ${blobRule(target)}`, + evidence: `Estimated until a bench run measures it. Target is ${BLOB_TARGET}${shared}.`, + help: `Estimated ${fmt(w.bytesPerTx)} compressed bytes and ${fmt(w.gasPerTx)} gas per transaction. ${blobRule(BLOB_TARGET)}`, }; } @@ -103,12 +95,12 @@ function coreHelp(run: Run): string { } // Lowest ceiling first; limits without a ceiling keep their order after the ranked ones. -export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven | null, disk: Limit[] = [], today: number | null = null): Limit[] { +export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven | null, disk: Limit[] = []): Limit[] { const m = proven?.mgas ?? null; const text = new Map { evidence: string; help: string }>(); const da = bars.filter((b) => b.id.startsWith('da-')).map((b): Limit => { const w = workloads.find((x) => `da-${x.payload}` === b.id)!; - text.set(b.id, (first) => daText(w, b.value!, first, today, m)); + text.set(b.id, (first) => daText(w, first)); return { id: b.id, label: b.label, value: b.value, ceiling: b.text, evidence: '', tag: b.tag, perSecond: b.perSecond, workload: w }; }); const core = bars.filter((b) => !b.id.startsWith('da-')); diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx index 1c39395..201fa09 100644 --- a/app/scaling/components/Headline.tsx +++ b/app/scaling/components/Headline.tsx @@ -151,6 +151,8 @@ function Tile({ row: r, gasLimit }: { row: Translation; gasLimit: number | null ); } +const MIX_DA = 'Its DA cap comes from the bench mix run, which is lighter per transaction and compresses worse than mainnet, so the cap is conservative.'; + // Bench workloads convert with their proven run's gas per successful transaction; today's traffic mix // with mainnet's own gas per transaction, since the bench mix only approximates mainnet. function translations({ ladder, usage, bench, proven, limits }: Pick): Translation[] { @@ -169,8 +171,8 @@ function translations({ ladder, usage, bench, proven, limits }: Pick (w.payload === 'mix' ? { ...w, label: 'Mix (bench)', tag: 'estimated', asOf: null, source: undefined } : w)); + const at = (t: number) => `${new Date(t).toISOString().slice(0, 16).replace('T', ' ')} UTC`; const areaOf = (limit: Limit | null): Area | null => (!limit ? null : limit.id.startsWith('da-') ? 'da' : limit.id.startsWith('disk-') ? 'state' : 'execution'); @@ -40,8 +44,8 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl const proven = data?.current.mix ?? null; const qualified = ladder.qualified; const mark = proven ? benchMark(proven) : !benchUrl && qualified.value !== null ? { value: qualified.value, atLeast: false, help: qualified.help } : null; - const workloads = daWorkloads(data?.da); - const limits = rankLimits(bottleneckBars(workloads, proven), workloads, proven, diskLimits(pickStorage(data), snapshot), ladder.observed.p50.value); + const workloads = daWorkloads(data?.da, SCALING_WORKLOADS); + const limits = rankLimits(bottleneckBars(workloads, proven), workloads, proven, diskLimits(pickStorage(data), snapshot)); const binding = limits[0]?.value != null ? limits[0] : null; const values = [ladder.observed.p50, ladder.observed.p99, ladder.operatingPoint, ladder.configuredLimit].map((r) => r.value ?? 0); const max = Math.max(mark?.value ?? 0, ladder.configuredLimit.value ?? 0) || Math.max(0, ...values); diff --git a/app/scaling/components/WorkloadSubsystems.tsx b/app/scaling/components/WorkloadSubsystems.tsx index 9821fcc..e7c7d7c 100644 --- a/app/scaling/components/WorkloadSubsystems.tsx +++ b/app/scaling/components/WorkloadSubsystems.tsx @@ -37,11 +37,10 @@ const daCeiling = ({ result }: NonNullable) => (BLOB_TARGET * BLOB_DA // The posting window's blobs per slot, rescaled from its gas rate to the run's load rate. const blobsAt = (run: Run) => Math.round((run.da!.result.blobs_per_l1_slot * run.mgas) / (run.da!.result.gas_per_second / 1e6)); -function daCard(da: ProvenCapacity['da'], gasLimit: number | null, mainnetMix: Limit | undefined): SubsystemCard { +function daCard(da: ProvenCapacity['da'], gasLimit: number | null): SubsystemCard { const runs = Object.fromEntries(COLUMNS.map(([w]) => [w, da[w]?.da ? da[w]! : undefined])) as Record; const measured = COLUMNS.filter(([w]) => runs[w]); const binding = measured.filter(([w]) => gasLimit !== null && daCeiling(runs[w]!.da!) < gasLimit).map(([w]) => NAME[w]); - const mainnet = mainnetMix?.value ? ` What breaks first uses real mainnet traffic for the mix instead: ${fmt(mainnetMix.value)} Mgas/s.` : ''; return { id: 'da', title: 'DA', @@ -55,7 +54,7 @@ function daCard(da: ProvenCapacity['da'], gasLimit: number | null, mainnetMix: L }, { label: 'Ceiling (Mgas/s)', - help: `Gas rate that fills the ${BLOB_TARGET}-blob target: ${BLOB_TARGET} × ${BLOB_DATA_BYTES.toLocaleString('en-US')} bytes per blob ÷ ${L1_SLOT_SECONDS} s per slot ÷ the run's compressed bytes per gas. Mix is the bench run's mainnet mix.${mainnet}`, + help: `Gas rate that fills the ${BLOB_TARGET}-blob target: ${BLOB_TARGET} × ${BLOB_DATA_BYTES.toLocaleString('en-US')} bytes per blob ÷ ${L1_SLOT_SECONDS} s per slot ÷ the run's compressed bytes per gas. Mix is the bench run's mainnet mix, as in What breaks first.`, cells: cells(runs, (r) => ({ value: fmt(daCeiling(r.da!)), bad: gasLimit !== null && daCeiling(r.da!) < gasLimit }), NOT_IN_RUN), }, ], @@ -204,7 +203,7 @@ const MATCHES: Record boolean> = { export function subsystemCards(data: ProvenCapacity | undefined, limits: Limit[], snapshot: Pick, gasLimit: number | null): SubsystemCard[] { const runs = Object.fromEntries(COLUMNS.map(([w]) => [w, data?.current[w]?.run])) as Record; const da = data?.da ?? { mix: null, payment: null, swap: null }; - const cards = [daCard(da, gasLimit, limits.find((l) => l.id === 'da-mix')), ...diskCards(runs, snapshot), proofCard(runs), validatorCard(runs), sequencerCard(runs)]; + const cards = [daCard(da, gasLimit), ...diskCards(runs, snapshot), proofCard(runs), validatorCard(runs), sequencerCard(runs)]; const rank = (c: SubsystemCard) => { const i = limits.findIndex((l) => l.value !== null && MATCHES[c.id](l.id)); return i < 0 ? Infinity : i; diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 91043e7..d779a02 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -371,8 +371,7 @@ describe('what breaks first', () => { expect(rows(render({}).ranked)).toEqual([ '1DA · Payments (B20)next~57 Mgas/s≈ 1,517 tx/sEstimatedEstimated until a bench run measures it. Target is 14, shared by all rollups.', '2DA · Swaps~115 Mgas/s≈ 799 tx/sEstimatedEstimated until a bench run measures it. Target is 14.', - // 14 blobs × 12.5 Mgas/s today ÷ the ~190 Mgas/s mix ceiling ≈ 0.9 blobs per slot. - "3DA · Mainnet mix190 Mgas/s≈ 981 tx/sMeasuredMeasured on mainnet: today's traffic uses about 0.9 blobs per L1 slot. Target is 14.", + '3DA · Mix (bench)~190 Mgas/s≈ 981 tx/sEstimatedEstimated until a bench run measures it. Target is 14.', '—Execution (sequencer)—no proven run', '—Validators—pending', '—Proof node—pending', @@ -386,10 +385,7 @@ describe('what breaks first', () => { swr.result = { data: bench(400, true) }; const { ranked } = render(); expect(rows(ranked)[3]).toBe('4Execution, validators, proof node≥ 400 Mgas/sMeasured in the bench: all passed at 400 Mgas/s, the highest rate tested.'); - // 14 × 400 ÷ ~190 ≈ 30 blobs per slot at the proven rate. - expect(rows(ranked)[2]).toContain("today's traffic uses about 0.9 blobs per L1 slot; at 400 Mgas/s it would need 30. Target is 14."); expect(ranked).toContain('Run synthetic-400. Validator replay: 36.4 blocks/s, 7.3× the 5 needed. Proof node: lag p99 4 blocks, executePayload p99 733 ms.'); - expect(ranked).toContain('Source: base-bench da, mainnet 2 h (mainnet blocks run through the production batcher encoder). Mainnet averages 193,349 gas and 154.7 compressed bytes per transaction.'); expect(ranked.match(/>next { ]); }); + it('ranks the mix by the bench mix run’s DA everywhere on the page, with the bench evidence', () => { + // 4.3e7 frame bytes over 4e10 gas put the 14-blob ceiling at ~141 Mgas/s; 16 blobs over a 160 Mgas/s window at 400 → 40. + swr.result = { data: bench(400, true, { da: { mix: run(400, 'mix', daResult(4.3e7, 16, false)), payment: null, swap: null } }) }; + const { head, ladder, ranked, subsystems } = render(); + expect(rows(ranked)[2]).toMatch(/^3DA · Mix \(bench\)141 Mgas\/s≈ 3,528 tx\/sMeasuredMeasured in the bench: at 400 Mgas\/s, needs 40 blobs per L1 slot\. Target is 14\.$/); + expect(text(ladder)).toMatch(/141.*DA · mix/); + expect(tile(head, 'traffic mix')).toContain('capped by DA'); + expect(head).toContain('Its DA cap comes from the bench mix run, which is lighter per transaction and compresses worse than mainnet, so the cap is conservative.'); + expect(rows(subsystems.split(' { swr.result = { data: bench(40, false) }; expect(rows(render().ranked)[0]).toBe('1Execution, validators, proof nodenext40 Mgas/sMeasured in the bench: all passed at 40 Mgas/s; the next rate tested did not pass.'); @@ -431,7 +438,7 @@ describe('what breaks first', () => { // 4e10 gas and 2.6158e7 frame bytes put the 14-blob ceiling at ~232 Mgas/s. swr.result = { data: bench(400, true, { da: { mix: null, payment: null, swap: run(400, 'swap', daResult(2.6158e7, 22.4, false)) } }) }; const ranked = render().ranked; - expect(rows(ranked).slice(0, 4).map((r) => r.slice(0, 22))).toEqual(['1DA · Payments (B20)ne', '2DA · Mainnet mix190 M', '3DA · Swaps232 Mgas/s≈', '4Execution, validators']); + expect(rows(ranked).slice(0, 4).map((r) => r.slice(0, 22))).toEqual(['1DA · Payments (B20)ne', '2DA · Mix (bench)~190 ', '3DA · Swaps232 Mgas/s≈', '4Execution, validators']); const swapsRow = ranked.split(' r.includes('>DA · Swaps'))!; expect(swapsRow).toMatch(/style="width:58\.\d+%"/); expect(swapsRow).toMatch(/border-dashed[^"]*" style="left:50%"/); @@ -462,7 +469,7 @@ describe('disk ceilings', () => { '1DA · Payments (B20)next~5', '2DA · Swaps~115 Mgas/s≈ 79', '3Disk · archive node≤ 138 ', - '4DA · Mainnet mix190 Mgas/', + '4DA · Mix (bench)~190 Mgas', '5Execution, validators, pr', '6Disk · full node≤ 553 Mga', '—TEE proving, RPC under sy', From c9752ef19631f0822f126877233dd5496c41013b Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 16:48:51 -0700 Subject: [PATCH 25/33] feat(scaling): leave guardrails off the first screen for now, keeping the unlinked guardrails page --- app/scaling/components/Scorecard.tsx | 4 ---- app/scaling/components/scorecard.test.tsx | 28 +++++++++++++---------- app/scaling/page.test.tsx | 21 ++++++++--------- app/scaling/page.tsx | 1 - 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index a272463..517a6d7 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -4,7 +4,6 @@ import { daWorkloads, DA_WORKLOADS, type DaWorkload } from '../da/workloads'; import type { Proven } from '../proven/capacity'; import type { Area, Snapshot } from '../snapshot.mjs'; import { bottleneckBars, Bottlenecks, diskLimits, pickStorage, rankLimits, type Limit } from './Bottlenecks'; -import { Guardrails } from './Guardrails'; import { Headline } from './Headline'; import { Ladder, type ProvenMark } from './Ladder'; import { day, useProvenCapacity } from './ProvenCapacity'; @@ -26,11 +25,9 @@ const areaOf = (limit: Limit | null): Area | null => (!limit ? null : limit.id.s function Freshness({ snapshot, latestAt }: { snapshot: Snapshot; latestAt: number | null }) { const usageWindow = snapshot.usage?.transactionsPerSecond.window ?? snapshot.ladder.observed.p50.window; - const slos = snapshot.guardrails.flatMap((g) => (g.asOf ? [Date.parse(g.asOf)] : [])); const parts = [ `Usage to ${day(Date.parse(usageWindow ? snapshot.windows[usageWindow].end : snapshot.collectedAt))}`, latestAt !== null && `bench ${at(latestAt)}`, - slos.length > 0 && `SLOs ${day(Math.max(...slos))}`, ].filter(Boolean); return

      {parts.join(' · ')}

      ; } @@ -56,7 +53,6 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl - ); diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index d779a02..e6dbd47 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ProvenCapacity, Run } from '../proven/capacity'; import type { Snapshot } from '../snapshot.mjs'; +import { Guardrails } from './Guardrails'; import { headroomLabel, place, sustainable } from './Headline'; import { layoutPoints, type PlacedPoint, type Point } from './Ladder'; import { Scorecard } from './Scorecard'; @@ -62,11 +63,11 @@ const bench = (mgas: number, atTop: boolean, more: Partial = {}) ...more, }); const fixture = (): Snapshot => JSON.parse(readFileSync(new URL('../fixtures/synthetic.v2.json', import.meta.url), 'utf8')); -// In page order: the freshness line, then headline, ladder, ranked limits, workloads, guardrails, work. +// In page order: the freshness line, then headline, ladder, ranked limits, workloads, work. const render = (props: { benchUrl?: string } = { benchUrl: BENCH }, snapshot = fixture()) => { const html = renderToStaticMarkup(); - const [fresh, head, ladder, ranked, subsystems, guardrails, work] = html.split(' { @@ -88,10 +89,10 @@ beforeEach(() => { }); describe('freshness and headline', () => { - it('dates usage, bench and SLOs, dropping the bench part without bench data', () => { - expect(text(render({}).fresh)).toBe('Usage to 2026-01-02 · SLOs 2026-01-02'); + it('dates usage and bench, dropping the bench part without bench data, and no SLOs', () => { + expect(text(render({}).fresh)).toBe('Usage to 2026-01-02'); swr.result = { data: bench(400, true) }; - expect(text(render().fresh)).toBe('Usage to 2026-01-02 · bench 2026-01-06 00:00 UTC · SLOs 2026-01-02'); + expect(text(render().fresh)).toBe('Usage to 2026-01-02 · bench 2026-01-06 00:00 UTC'); expect(render().html).not.toContain('Usage data as of'); }); @@ -532,14 +533,17 @@ describe('disk ceilings', () => { }); }); -describe('guardrails and work', () => { +describe('guardrails summary and work', () => { + // The summary is off the first screen for now; the component stays for when it returns. + const guardrails = (benchUrl?: string) => renderToStaticMarkup(); + it('compresses guardrails to breached, met and not-measured counts with a link', () => { - const { guardrails } = render({}); - expect(text(guardrails)).toContain('Breached 1 — RPC availability 99.5 %Met 2Not measured 3'); - expect(text(guardrails)).toContain('RPC p99 latencyPropagation p99State growth'); - expect(guardrails).toContain('href="/scaling/guardrails"'); + const summary = guardrails(); + expect(text(summary)).toContain('Breached 1 — RPC availability 99.5 %Met 2Not measured 3'); + expect(text(summary)).toContain('RPC p99 latencyPropagation p99State growth'); + expect(summary).toContain('href="/scaling/guardrails"'); swr.result = { data: bench(400, true) }; - expect(text(render().guardrails)).toContain('Met 5'); + expect(text(guardrails(BENCH))).toContain('Met 5'); }); it('lists work by limit with the binding limit first', () => { diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index bccef45..d682ce2 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -19,9 +19,11 @@ afterEach(() => vi.unstubAllEnvs()); const area = async (id: string) => renderToStaticMarkup(await ScalingAreaPage({ params: Promise.resolve({ area: id }) })); -it('ends the first screen with DA and guardrails links, and no benchmark links', async () => { +it('ends the first screen with the DA link, and no guardrails or benchmark links', async () => { const html = renderToStaticMarkup(await ScalingPage()); - expect(html).toContain('DA by workloadGuardrails'); + expect(html).toContain('DA by workload'); + // The guardrails page stays, unlinked, while the first screen leaves guardrails out. + expect(html).not.toMatch(/href="\/scaling\/guardrails"|Guardrails<\/h2>|SLOs/); expect(html).not.toMatch(/href="\/benchmark|href="\/scaling\/networking"/); expect(html).not.toContain('Blob utilization'); expect(html).not.toContain('href="/scaling/runs"'); @@ -29,9 +31,9 @@ it('ends the first screen with DA and guardrails links, and no benchmark links', expect(html).not.toMatch(/As of|manually refreshed|Synthetic fixture: every number/); }); -it('orders the first screen: freshness, headline, ladder, ranked limits, subsystems by workload, guardrails, work, links', async () => { +it('orders the first screen: freshness, headline, ladder, ranked limits, subsystems by workload, work, links', async () => { const html = renderToStaticMarkup(await ScalingPage()); - const order = ['Usage to 2026-01-02', 'Proven capacity', 'How close are we, and to what?', 'What breaks first', 'Subsystems by workload', 'Guardrails', 'Work by limit', 'DA by workload'].map((h) => html.indexOf(h)); + const order = ['Usage to 2026-01-02', 'Proven capacity', 'How close are we, and to what?', 'What breaks first', 'Subsystems by workload', 'Work by limit', 'DA by workload'].map((h) => html.indexOf(h)); expect(order.every((i, k) => i > (order[k - 1] ?? -1))).toBe(true); expect(html).not.toMatch(/Subsystem detail|Progress<|Ceilings by bottleneck|Usage data as of/); }); @@ -86,14 +88,9 @@ it('passes the runtime bench and dashboard URLs to the scorecard and footer', as it('adds bench guardrail rows when a bench host is set, unknown until its results load', async () => { vi.stubEnv('SCALING_BENCH_BASE_URL', 'https://bench.example.com'); - for (const html of [renderToStaticMarkup(await ScalingGuardrailsPage()), renderToStaticMarkup(await ScalingPage())]) { - for (const label of ['Proof-node head lag p99', 'debug_executePayload p99', 'Validator replay headroom']) expect(html).toContain(label); - } - expect(renderToStaticMarkup(await ScalingGuardrailsPage())).toContain('No qualifying mainnet-mix bench run is loaded.'); -}); - -it('links the first-screen guardrail summary to the guardrails page', async () => { - expect(renderToStaticMarkup(await ScalingPage())).toContain('Guardrails →'); + const html = renderToStaticMarkup(await ScalingGuardrailsPage()); + for (const label of ['Proof-node head lag p99', 'debug_executePayload p99', 'Validator replay headroom']) expect(html).toContain(label); + expect(html).toContain('No qualifying mainnet-mix bench run is loaded.'); }); it('shows guardrail help inline and the outcomes table on the guardrails page', async () => { diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index 8f27b5b..0fec28a 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -22,7 +22,6 @@ export default async function ScalingPage() { DA by workload - Guardrails {dashboardUrl ? Mainnet load, gas limit, DA ceiling : null} From 9e4a3093cfaade5d6898439c7179f483dbc0ecad Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 16:49:45 -0700 Subject: [PATCH 26/33] =?UTF-8?q?feat(scaling):=20label=20the=20mix=20DA?= =?UTF-8?q?=20tick=20DA=20=C2=B7=20mix=20(bench),=20matching=20the=20ranke?= =?UTF-8?q?d=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scaling/components/Ladder.tsx | 2 +- app/scaling/components/scorecard.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx index 43b1bf4..548f017 100644 --- a/app/scaling/components/Ladder.tsx +++ b/app/scaling/components/Ladder.tsx @@ -45,7 +45,7 @@ const SHADES = { allowed: 'fill-bds-blue-10 bg-bds-blue-10 dark:fill-bds-blue-80 dark:bg-bds-blue-80', proven: 'fill-bds-gray-10 bg-bds-gray-10 dark:fill-white/10 dark:bg-white/10', }; -const SHORT: Record = { 'da-payment': 'DA · payments', 'da-swap': 'DA · swaps', 'da-mix': 'DA · mix', 'disk-archive': 'archive disk', 'disk-full': 'full-node disk' }; +const SHORT: Record = { 'da-payment': 'DA · payments', 'da-swap': 'DA · swaps', 'da-mix': 'DA · mix (bench)', 'disk-archive': 'archive disk', 'disk-full': 'full-node disk' }; // Approximate label widths: a bold 12px value with its help icon over an 11px name. const widthOf = (p: Point, offScale: boolean, compact: boolean) => diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index e6dbd47..e423c4f 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -407,7 +407,7 @@ describe('what breaks first', () => { swr.result = { data: bench(400, true, { da: { mix: run(400, 'mix', daResult(4.3e7, 16, false)), payment: null, swap: null } }) }; const { head, ladder, ranked, subsystems } = render(); expect(rows(ranked)[2]).toMatch(/^3DA · Mix \(bench\)141 Mgas\/s≈ 3,528 tx\/sMeasuredMeasured in the bench: at 400 Mgas\/s, needs 40 blobs per L1 slot\. Target is 14\.$/); - expect(text(ladder)).toMatch(/141.*DA · mix/); + expect(text(ladder)).toMatch(/141.*DA · mix \(bench\)/); expect(tile(head, 'traffic mix')).toContain('capped by DA'); expect(head).toContain('Its DA cap comes from the bench mix run, which is lighter per transaction and compresses worse than mainnet, so the cap is conservative.'); expect(rows(subsystems.split(' Date: Tue, 27 Oct 2026 16:56:41 -0700 Subject: [PATCH 27/33] feat(scaling): show tx/s on every ceiling, tagging only estimates --- app/scaling/components/Bottlenecks.tsx | 32 +++++++++--- app/scaling/components/Headline.tsx | 9 +++- app/scaling/components/Scorecard.tsx | 6 +-- app/scaling/components/scorecard.test.tsx | 59 ++++++++++++++++++----- 4 files changed, 82 insertions(+), 24 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index 8a024c8..f5d4790 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -21,6 +21,8 @@ export type Limit = { perSecond?: number; workload?: DaWorkload; help?: string; + // Help beside the ceiling itself. + ceilingHelp?: string; runwayYears?: number; }; @@ -78,7 +80,7 @@ function daText(w: DaWorkload, first: boolean): { evidence: string; help: string } // Without a bench DA run every /scaling workload, the mix included, is an estimate. return { - evidence: `Estimated until a bench run measures it. Target is ${BLOB_TARGET}${shared}.`, + evidence: `Estimated: no bench DA run yet, so this uses the formula. Target is ${BLOB_TARGET}${shared}.`, help: `Estimated ${fmt(w.bytesPerTx)} compressed bytes and ${fmt(w.gasPerTx)} gas per transaction. ${blobRule(BLOB_TARGET)}`, }; } @@ -95,13 +97,19 @@ function coreHelp(run: Run): string { } // Lowest ceiling first; limits without a ceiling keep their order after the ranked ones. -export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven | null, disk: Limit[] = []): Limit[] { +// `mainnetGasPerTx` (today's usage p50 ÷ tx/s) converts every row not specific to one workload, as the traffic tile does. +export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven | null, disk: Limit[] = [], mainnetGasPerTx: number | null = null): Limit[] { const m = proven?.mgas ?? null; const text = new Map { evidence: string; help: string }>(); const da = bars.filter((b) => b.id.startsWith('da-')).map((b): Limit => { const w = workloads.find((x) => `da-${x.payload}` === b.id)!; text.set(b.id, (first) => daText(w, first)); - return { id: b.id, label: b.label, value: b.value, ceiling: b.text, evidence: '', tag: b.tag, perSecond: b.perSecond, workload: w }; + // Payments and swaps convert with their own transactions; the mix, like every shared row, at today's mainnet average below. + const mix = b.id === 'da-mix'; + const perSecond = mix ? undefined : b.perSecond; + const bench = w.bench ? ` (the bench mix${w.gasPerTx < mainnetGasPerTx! ? ' is lighter' : ''}: ~${Math.round(w.gasPerTx / 1e3)}k)` : ''; + const ceilingHelp = mix && mainnetGasPerTx ? `tx/s at today's mainnet average of ~${Math.round(mainnetGasPerTx! / 1e3)}k gas per transaction${bench}.` : undefined; + return { id: b.id, label: b.label, value: b.value, ceiling: b.text, evidence: '', tag: b.tag, perSecond, workload: w, ceilingHelp }; }); const core = bars.filter((b) => !b.id.startsWith('da-')); const atLeast = (atTop: boolean) => (atTop ? `≥ ${m}` : `${m}`); @@ -123,7 +131,8 @@ export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven })); const sorted = [...da, ...merged, ...disk].toSorted((a, b) => (a.value ?? Infinity) - (b.value ?? Infinity)); const firstDa = sorted.find((l) => l.id.startsWith('da-'))?.id; - return sorted.map((l) => (text.has(l.id) ? { ...l, ...text.get(l.id)!(l.id === firstDa) } : l)); + const atMainnet = (l: Limit) => l.perSecond === undefined && mainnetGasPerTx !== null && l.value !== null && l.value > 0 ? { perSecond: (l.value * 1e6) / mainnetGasPerTx } : {}; + return sorted.map((l) => ({ ...l, ...atMainnet(l), ...(text.has(l.id) ? text.get(l.id)!(l.id === firstDa) : {}) })); } export type StorageSource = { storage: Storage; run: Run }; @@ -254,7 +263,12 @@ function ProvenBy({ proven, history }: { proven: Proven; history: ProvenCapacity ); } -export function Bottlenecks({ limits, max, gasLimit, proven, history }: { limits: Limit[]; max: number; gasLimit: number | null; proven: Proven | null; history: ProvenCapacity['history'] }) { +// A ≤ or ≥ ceiling bounds its tx/s the same way; other ceilings convert approximately. +const bound = (ceiling: string) => /^[≤≥]/.exec(ceiling)?.[0]; +const ceilingHelp = (gasPerTx: number | null) => + `tx/s for payments and swaps uses that workload's transactions; ${gasPerTx ? `every other row uses today's mainnet average of ~${Math.round(gasPerTx / 1e3)}k gas per transaction` : 'the snapshot has no transactions per second, so every other row stays in Mgas/s'}. Reverted transactions still use gas and blob space.`; + +export function Bottlenecks({ limits, max, gasLimit, proven, history, mainnetGasPerTx = null }: { limits: Limit[]; max: number; gasLimit: number | null; proven: Proven | null; history: ProvenCapacity['history']; mainnetGasPerTx?: number | null }) { const pct = (v: number) => `${Math.min(100, (v / max) * 100)}%`; const ranked = limits.filter((l) => l.value !== null).length; const fixed = [{ label: 'TEE proving, RPC under sync', evidence: 'Not measured yet.' }]; @@ -265,7 +279,7 @@ export function Bottlenecks({ limits, max, gasLimit, proven, history }: { limits - + @@ -281,8 +295,10 @@ export function Bottlenecks({ limits, max, gasLimit, proven, history }: { limits
      #LimitCeiling Mgas/s. For DA rows, ≈ /s converts it to successful transactions per second: blob capacity ÷ frame bytes per successful transaction. Reverted transactions still use gas and blob space.Evidence#LimitCeiling {ceilingHelp(mainnetGasPerTx)}Evidence
      {l.ceiling}{l.value !== null && l.value > 0 ? ' Mgas/s' : ''} - {l.perSecond === undefined ? null : ≈ {fmt(l.perSecond)} tx/s} - {l.tag ? : null} + {l.perSecond === undefined ? null : {bound(l.ceiling) ?? '≈'} {fmt(l.perSecond)} tx/s} + {l.ceilingHelp ? {l.ceilingHelp} : null} + {/* Measured is the norm; only the exception is tagged. */} + {l.tag === 'estimated' ? : null}
      diff --git a/app/scaling/components/Headline.tsx b/app/scaling/components/Headline.tsx index 201fa09..8229e66 100644 --- a/app/scaling/components/Headline.tsx +++ b/app/scaling/components/Headline.tsx @@ -33,6 +33,13 @@ function benchNote({ configured, data, error }: BenchState, proven: ProvenMark, // "≥ 21×" when proven is the top rate tested, a floor. export const headroomLabel = (proven: NonNullable, today: number) => `${proven.atLeast ? '≥ ' : ''}${times(proven.value / today)}`; +// Today's mainnet gas per transaction: median gas rate ÷ transactions per second. Null without either. +export const mainnetGasPerTx = ({ ladder, usage }: Pick) => { + const p50 = ladder.observed.p50.value; + const tps = usage?.transactionsPerSecond.value ?? null; + return p50 !== null && tps ? (p50 * 1e6) / tps : null; +}; + export type Placement = { side: 'center' | 'left' | 'right'; x: number }; // Centers a label on x, or pins it to the nearer edge when centering would overflow. `estimate` is an @@ -168,8 +175,8 @@ function translations({ ladder, usage, bench, proven, limits }: Pick r.value ?? 0); const max = Math.max(mark?.value ?? 0, ladder.configuredLimit.value ?? 0) || Math.max(0, ...values); @@ -51,7 +51,7 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl - + diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index e423c4f..abc115e 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -370,9 +370,9 @@ describe('ladder', () => { describe('what breaks first', () => { it('ranks DA estimates first without bench data, with units, and leaves unproven limits unranked', () => { expect(rows(render({}).ranked)).toEqual([ - '1DA · Payments (B20)next~57 Mgas/s≈ 1,517 tx/sEstimatedEstimated until a bench run measures it. Target is 14, shared by all rollups.', - '2DA · Swaps~115 Mgas/s≈ 799 tx/sEstimatedEstimated until a bench run measures it. Target is 14.', - '3DA · Mix (bench)~190 Mgas/s≈ 981 tx/sEstimatedEstimated until a bench run measures it. Target is 14.', + '1DA · Payments (B20)next~57 Mgas/s≈ 1,517 tx/sEstimatedEstimated: no bench DA run yet, so this uses the formula. Target is 14, shared by all rollups.', + '2DA · Swaps~115 Mgas/s≈ 799 tx/sEstimatedEstimated: no bench DA run yet, so this uses the formula. Target is 14.', + '3DA · Mix (bench)~190 Mgas/s≈ 974 tx/sEstimatedEstimated: no bench DA run yet, so this uses the formula. Target is 14.', '—Execution (sequencer)—no proven run', '—Validators—pending', '—Proof node—pending', @@ -385,7 +385,7 @@ describe('what breaks first', () => { it('merges execution, validators and proof node when they share the proven ceiling, with the run in help', () => { swr.result = { data: bench(400, true) }; const { ranked } = render(); - expect(rows(ranked)[3]).toBe('4Execution, validators, proof node≥ 400 Mgas/sMeasured in the bench: all passed at 400 Mgas/s, the highest rate tested.'); + expect(rows(ranked)[3]).toBe('4Execution, validators, proof node≥ 400 Mgas/s≥ 2,054 tx/sMeasured in the bench: all passed at 400 Mgas/s, the highest rate tested.'); expect(ranked).toContain('Run synthetic-400. Validator replay: 36.4 blocks/s, 7.3× the 5 needed. Proof node: lag p99 4 blocks, executePayload p99 733 ms.'); expect(ranked.match(/>next { over.current.mix = { mgas: 400, atTop: true, run: run(400, 'mix', null, 11) }; swr.result = { data: over }; expect(rows(render().ranked).slice(3, 6)).toEqual([ - '4Execution (sequencer)≥ 400 Mgas/stop rate tested', - '5Validators≥ 400 Mgas/s7.3× replay headroom', + '4Execution (sequencer)≥ 400 Mgas/s≥ 2,054 tx/stop rate tested', + '5Validators≥ 400 Mgas/s≥ 2,054 tx/s7.3× replay headroom', '—Proof node—over budget at 400 · lag p99 11 blocks', ]); }); @@ -406,23 +406,58 @@ describe('what breaks first', () => { // 4.3e7 frame bytes over 4e10 gas put the 14-blob ceiling at ~141 Mgas/s; 16 blobs over a 160 Mgas/s window at 400 → 40. swr.result = { data: bench(400, true, { da: { mix: run(400, 'mix', daResult(4.3e7, 16, false)), payment: null, swap: null } }) }; const { head, ladder, ranked, subsystems } = render(); - expect(rows(ranked)[2]).toMatch(/^3DA · Mix \(bench\)141 Mgas\/s≈ 3,528 tx\/sMeasuredMeasured in the bench: at 400 Mgas\/s, needs 40 blobs per L1 slot\. Target is 14\.$/); + // The mix converts at today's mainnet gas per transaction, so it equals the traffic tile's sustainable today. + expect(rows(ranked)[2]).toMatch(/^3DA · Mix \(bench\)141 Mgas\/s≈ 725 tx\/sMeasured in the bench: at 400 Mgas\/s, needs 40 blobs per L1 slot\. Target is 14\.$/); + // 12.5 Mgas/s ÷ 64.2 tx/s today ≈ 195k gas per transaction; the bench mix run used 40k. + expect(ranked).toContain('tx/s at today's mainnet average of ~195k gas per transaction (the bench mix is lighter: ~40k).'); + expect(ranked).not.toContain('>Measured<'); expect(text(ladder)).toMatch(/141.*DA · mix \(bench\)/); expect(tile(head, 'traffic mix')).toContain('capped by DA'); expect(head).toContain('Its DA cap comes from the bench mix run, which is lighter per transaction and compresses worse than mainnet, so the cap is conservative.'); expect(rows(subsystems.split(' { + const data = bench(400, true, { da: { mix: run(400, 'mix', daResult(4.3e7, 16, false)), payment: run(200, 'payment', daResult(8e7, 20, false)), swap: null } }); + data.current.mix = { mgas: 400, atTop: true, run: { ...run(400), storage: storage() } }; + swr.result = { data }; + const { head, ranked } = render(); + const row = (label: string) => rows(ranked).find((r) => r.includes(label))!; + // Payments: its DA run's 40k gas per successful transaction. Every other row: 12.5 Mgas/s ÷ 64.2 tx/s ≈ 195k, a bound kept on both numbers. + expect(row('Payments')).toContain('76 Mgas/s≈ 1,896 tx/s'); + expect(row('Mix (bench)')).toContain('141 Mgas/s≈ 725 tx/s'); + expect(row('archive node')).toContain('≤ 138 Mgas/s≤ 710 tx/s'); + expect(row('Execution, validators')).toContain('≥ 400 Mgas/s≥ 2,054 tx/s'); + expect(row('full node')).toContain('≤ 553 Mgas/s≤ 2,841 tx/s'); + expect(row('TEE proving')).not.toContain('tx/s'); + // Same function and rounding as the traffic tile's sustainable today. + expect(tile(head, 'traffic mix')).toContain(`sustainable today ${/≈ ([\d,]+) tx\/s/.exec(row('Mix (bench)'))![1]}/s`); + expect(ranked).toContain('tx/s for payments and swaps uses that workload's transactions; every other row uses today's mainnet average of ~195k gas per transaction.'); + }); + + it('keeps shared rows in Mgas/s without today’s transactions per second', () => { + const data = bench(400, true, { da: { mix: run(400, 'mix', daResult(4.3e7, 16, false)), payment: run(200, 'payment', daResult(8e7, 20, false)), swap: null } }); + data.current.mix = { mgas: 400, atTop: true, run: { ...run(400), storage: storage() } }; + swr.result = { data }; + const snapshot = fixture(); + delete snapshot.usage; + const { ranked } = render({ benchUrl: BENCH }, snapshot); + const row = (label: string) => rows(ranked).find((r) => r.includes(label))!; + expect(row('Payments')).toContain('76 Mgas/s≈ 1,896 tx/s'); + for (const label of ['Mix (bench)', 'archive node', 'Execution, validators', 'full node']) expect(row(label)).not.toContain('tx/s'); + expect(ranked).toContain('the snapshot has no transactions per second, so every other row stays in Mgas/s.'); + }); + it('says the next rate did not pass when the core limit is lowest below the top rate', () => { swr.result = { data: bench(40, false) }; - expect(rows(render().ranked)[0]).toBe('1Execution, validators, proof nodenext40 Mgas/sMeasured in the bench: all passed at 40 Mgas/s; the next rate tested did not pass.'); + expect(rows(render().ranked)[0]).toBe('1Execution, validators, proof nodenext40 Mgas/s≈ 205 tx/sMeasured in the bench: all passed at 40 Mgas/s; the next rate tested did not pass.'); }); it('rescales a bench DA run’s posting-window blobs to its load rate, with the details in help', () => { // 20 blobs per slot over a window at 160 Mgas/s, rescaled to the 200 Mgas/s load: 25. swr.result = { data: bench(400, true, { da: { mix: null, payment: run(200, 'payment', daResult(8e7, 20, false)), swap: null } }) }; const { ranked } = render(); - expect(rows(ranked)[0]).toBe('1DA · Payments (B20)next76 Mgas/s≈ 1,896 tx/sMeasuredMeasured in the bench: at 200 Mgas/s, needs 25 blobs per L1 slot. Target is 14, shared by all rollups.'); + expect(rows(ranked)[0]).toBe('1DA · Payments (B20)next76 Mgas/s≈ 1,896 tx/sMeasured in the bench: at 200 Mgas/s, needs 25 blobs per L1 slot. Target is 14, shared by all rollups.'); expect(ranked).toContain('Run synthetic-200: 0.00200 compressed bytes per gas (compression 4.5×). Over the batch posting window it averaged 20.0 blobs per L1 slot at 160.0 Mgas/s; scaling that window to the 200 Mgas/s load gives 25.'); expect(ranked).toContain('14 × 130,044 bytes per blob ÷ 12 s per slot ÷ bytes per gas.'); }); @@ -476,8 +511,8 @@ describe('disk ceilings', () => { '—TEE proving, RPC under sy', ]); // (45 TB − 4 TB) ÷ (1e6 × (0.0086 bench + 0.0008 mainnet state) B/gas × 1 year); (45 TB − 1.1 TB state) ÷ (1e6 × (history 31 d + proofs 15 d + 2 years of 0.0008 B/gas state)). - expect(disk(ranked, 'archive node')).toBe('3Disk · archive node≤ 138 Mgas/sProjected from bench growth: one year fills the 45.0 TB disk (4.0 TB used today). Upper bound.'); - expect(disk(ranked, 'full node')).toBe('6Disk · full node≤ 553 Mgas/sProjected from bench growth: 31 days of history, 15 days of proofs and 2 years of state growth fit in 45.0 TB. Upper bound.'); + expect(disk(ranked, 'archive node')).toBe('3Disk · archive node≤ 138 Mgas/s≤ 710 tx/sProjected from bench growth: one year fills the 45.0 TB disk (4.0 TB used today). Upper bound.'); + expect(disk(ranked, 'full node')).toBe('6Disk · full node≤ 553 Mgas/s≤ 2,841 tx/sProjected from bench growth: 31 days of history, 15 days of proofs and 2 years of state growth fit in 45.0 TB. Upper bound.'); }); it('pluralizes the runway', () => { @@ -504,7 +539,7 @@ describe('disk ceilings', () => { delete snapshot.production!.stateBytesPerGas; // (45 TB − 4 TB) ÷ (1e6 × 0.0086 B/gas × 1 year). const { ranked } = render({ benchUrl: BENCH }, snapshot); - expect(disk(ranked, 'archive node')).toMatch(/≤ 151 Mgas\/sProjected from bench growth/); + expect(disk(ranked, 'archive node')).toMatch(/≤ 151 Mgas\/s≤ 776 tx\/sProjected from bench growth/); expect(ranked).toContain('0.00860 bytes per gas from the mainnet mix run (bench state only)'); }); From e8c7c0b99d48aa35fab398aeaf4a8faecbe44b02 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 16:59:06 -0700 Subject: [PATCH 28/33] fix(scaling): say when there is no gas limit to compare DA against, and reject impossible archive snapshot dates --- app/scaling/archiveSize.test.ts | 1 + app/scaling/archiveSize.ts | 4 +++- app/scaling/components/WorkloadSubsystems.tsx | 2 +- app/scaling/components/scorecard.test.tsx | 3 +++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/scaling/archiveSize.test.ts b/app/scaling/archiveSize.test.ts index 0b732c9..c160c4c 100644 --- a/app/scaling/archiveSize.test.ts +++ b/app/scaling/archiveSize.test.ts @@ -31,6 +31,7 @@ it('rejects a response that is not the expected shape', () => { [{ ...mainnet, size: 0 }], [{ ...mainnet, block: 1.5 }], [{ ...mainnet, date: 'yesterday' }], + [{ ...mainnet, date: '2026-02-30' }], [{ ...mainnet, components: null }], [{ ...mainnet, components: [{ name: 'proofs', size: -1 }] }], [{ ...mainnet, components: [{ name: 'proofs', size: 7e12 }] }], diff --git a/app/scaling/archiveSize.ts b/app/scaling/archiveSize.ts index f007d2c..a0eb202 100644 --- a/app/scaling/archiveSize.ts +++ b/app/scaling/archiveSize.ts @@ -11,6 +11,7 @@ const REVALIDATE_SECONDS = 3_600; export type ArchiveSize = { size: number; sizeWithoutProofs: number; block: number; date: string; source: 'chain.base.org' }; const isObj = (v: unknown): v is Record => typeof v === 'object' && v !== null && !Array.isArray(v); +const isDay = (date: string) => !Number.isNaN(Date.parse(date)) && new Date(date).toISOString().slice(0, 10) === date; const isSize = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v) && v >= 0; // The mainnet archive entry, checked field by field; extra fields are ignored. Null when the shape is off. @@ -20,7 +21,8 @@ export function parseArchiveSize(json: unknown): ArchiveSize | null { if (!isObj(entry)) return null; const { size, block, date, components } = entry; if (!isSize(size) || size === 0 || !Number.isSafeInteger(block) || (block as number) <= 0) return null; - if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date) || !Array.isArray(components)) return null; + // A real calendar day: 2026-02-30 matches the pattern but is not one. + if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date) || !isDay(date) || !Array.isArray(components)) return null; if (!components.every((c) => isObj(c) && typeof c.name === 'string' && isSize(c.size))) return null; const proofs = (components as { name: string; size: number }[]).find((c) => c.name === 'proofs')?.size ?? 0; if (proofs > size) return null; diff --git a/app/scaling/components/WorkloadSubsystems.tsx b/app/scaling/components/WorkloadSubsystems.tsx index e7c7d7c..56bf746 100644 --- a/app/scaling/components/WorkloadSubsystems.tsx +++ b/app/scaling/components/WorkloadSubsystems.tsx @@ -44,7 +44,7 @@ function daCard(da: ProvenCapacity['da'], gasLimit: number | null): SubsystemCar return { id: 'da', title: 'DA', - takeaway: !measured.length ? 'not measured yet' : binding.length ? `binding for ${and(binding)}` : `below target at the ${gasLimit} Mgas/s gas limit`, + takeaway: !measured.length ? 'not measured yet' : binding.length ? `binding for ${and(binding)}` : gasLimit === null ? 'no gas limit to compare against' : `below target at the ${gasLimit} Mgas/s gas limit`, bad: binding.length > 0, rows: [ { diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index abc115e..32d9728 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -242,6 +242,9 @@ describe('subsystems by workload', () => { expect(rows(da)).toEqual(['Blobs per L1 slot at each run’s rate 2556— (not in this run)', 'Ceiling (Mgas/s) 76232— (not in this run)']); swr.result = { data: bench(400, true, { da: { mix: null, payment: null, swap: run(400, 'swap', daResult(2.6158e7, 22.4, false)) } }) }; expect(text(card(render().subsystems, 'DA').split('')[0])).toBe('DA · below target at the 200 Mgas/s gas limit'); + const noLimit = fixture(); + noLimit.ladder.configuredLimit = { ...noLimit.ladder.configuredLimit, value: null }; + expect(text(card(render({ benchUrl: BENCH }, noLimit).subsystems, 'DA').split('')[0])).toBe('DA · no gas limit to compare against'); }); it('projects disk per workload run, saying which runs lack a storage measurement', () => { From c2b5ed71d45a5dc0c38e6d31ab8b1e646125adc8 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 17:04:16 -0700 Subject: [PATCH 29/33] feat(scaling): replace work by limit with a work log of in-progress and planned work --- app/scaling/components/Scorecard.tsx | 8 ++- app/scaling/components/Work.tsx | 64 ++++++++++++++++------- app/scaling/components/scorecard.test.tsx | 32 +++++++++--- app/scaling/fixtures/synthetic.v2.json | 20 ++++++- app/scaling/page.test.tsx | 4 +- app/scaling/snapshot.mjs | 29 ++++++---- app/scaling/snapshot.test.ts | 17 ++++++ 7 files changed, 129 insertions(+), 45 deletions(-) diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index 545df0d..eb01a8f 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -2,8 +2,8 @@ import { daWorkloads, DA_WORKLOADS, type DaWorkload } from '../da/workloads'; import type { Proven } from '../proven/capacity'; -import type { Area, Snapshot } from '../snapshot.mjs'; -import { bottleneckBars, Bottlenecks, diskLimits, pickStorage, rankLimits, type Limit } from './Bottlenecks'; +import type { Snapshot } from '../snapshot.mjs'; +import { bottleneckBars, Bottlenecks, diskLimits, pickStorage, rankLimits } from './Bottlenecks'; import { Headline, mainnetGasPerTx } from './Headline'; import { Ladder, type ProvenMark } from './Ladder'; import { day, useProvenCapacity } from './ProvenCapacity'; @@ -21,7 +21,6 @@ function benchMark({ mgas, atTop, run }: Proven): ProvenMark { const SCALING_WORKLOADS: DaWorkload[] = DA_WORKLOADS.map((w) => (w.payload === 'mix' ? { ...w, label: 'Mix (bench)', tag: 'estimated', asOf: null, source: undefined } : w)); const at = (t: number) => `${new Date(t).toISOString().slice(0, 16).replace('T', ' ')} UTC`; -const areaOf = (limit: Limit | null): Area | null => (!limit ? null : limit.id.startsWith('da-') ? 'da' : limit.id.startsWith('disk-') ? 'state' : 'execution'); function Freshness({ snapshot, latestAt }: { snapshot: Snapshot; latestAt: number | null }) { const usageWindow = snapshot.usage?.transactionsPerSecond.window ?? snapshot.ladder.observed.p50.window; @@ -43,7 +42,6 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl const mark = proven ? benchMark(proven) : !benchUrl && qualified.value !== null ? { value: qualified.value, atLeast: false, help: qualified.help } : null; const workloads = daWorkloads(data?.da, SCALING_WORKLOADS); const limits = rankLimits(bottleneckBars(workloads, proven), workloads, proven, diskLimits(pickStorage(data), snapshot), mainnetGasPerTx(snapshot)); - const binding = limits[0]?.value != null ? limits[0] : null; const values = [ladder.observed.p50, ladder.observed.p99, ladder.operatingPoint, ladder.configuredLimit].map((r) => r.value ?? 0); const max = Math.max(mark?.value ?? 0, ladder.configuredLimit.value ?? 0) || Math.max(0, ...values); return ( @@ -53,7 +51,7 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl - + ); } diff --git a/app/scaling/components/Work.tsx b/app/scaling/components/Work.tsx index b0ad80c..5ce2a37 100644 --- a/app/scaling/components/Work.tsx +++ b/app/scaling/components/Work.tsx @@ -2,40 +2,67 @@ 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 type { Area, WorkItem } from '../snapshot.mjs'; +import { WORK_STATUSES } from '../snapshot.mjs'; +import type { Area, Link, WorkCategory, WorkItem } from '../snapshot.mjs'; import { Info } from './Info'; -const LIMIT: Record = { da: 'DA', execution: 'Execution', state: 'State', networking: 'Networking' }; +// Status labels follow Linear's project states. +const STATUS: Record = { 'in-progress': 'In Progress', planned: 'Planned', done: 'Completed' }; +// Older entries name an area instead of a category. +const CATEGORY: Record = { da: 'DA', execution: 'Execution', state: 'Disk', networking: 'Latency' }; +const TH = 'px-3 py-2 text-left text-[11px] font-[600] uppercase tracking-wide text-bds-gray-60'; const TD = 'px-3 py-2.5 align-top'; -// Work on the binding limit's area comes first; the rest keep the snapshot's order. -export function Work({ work, binding }: { work: WorkItem[]; binding: Area | null }) { - const rows = work.toSorted((a, b) => Number(b.area === binding) - Number(a.area === binding)); +// Links in the entry's order, then its Linear project and evidence when an older entry carries them. +const linksOf = (w: WorkItem): Link[] => { + const links = [...(w.links ?? [])]; + if (w.linear && !links.some((l) => l.url === w.linear!.url)) links.push({ label: 'Linear', url: w.linear.url }); + if (w.evidence && !links.some((l) => l.url === w.evidence!.url)) links.push(w.evidence); + return links; +}; + +// In progress first, then planned, then done; entries with any other status are not shown. +export function Work({ work }: { work: WorkItem[] }) { + const rows = work.filter((w) => WORK_STATUSES.includes(w.status)).toSorted((a, b) => WORK_STATUSES.indexOf(a.status) - WORK_STATUSES.indexOf(b.status)); return (
      - Work by limit + Work log {rows.length === 0 ? ( - + ) : ( - +
      + + + {['Status', 'Work', 'Impact category', 'Effect', 'Links'].map((h) => )} + + {rows.map((w) => { - const link = w.linear?.url ?? w.links[0]?.url ?? w.evidence?.url; + const effect = w.effect ?? w.expected ?? null; + const about = [w.hypothesis && `Hypothesis: ${w.hypothesis}`, w.change && `Change: ${w.change}`, `Owner: ${w.owner ?? 'none assigned'}`].filter(Boolean); + const links = linksOf(w); return ( - + - + + + ); })} @@ -43,7 +70,6 @@ export function Work({ work, binding }: { work: WorkItem[]; binding: Area | null
      {h}
      {LIMIT[w.area]} + {STATUS[w.status]} + - - {link ? {w.title} : w.title} - -

      Hypothesis: {w.hypothesis}

      -

      Change: {w.change}

      -

      Owner: {w.owner ?? 'none assigned'}

      -
      + + {w.title} + {about.join(' ')}
      {w.expected ?? 'not estimated'}{w.category ?? (w.area ? CATEGORY[w.area] : '—')}{effect ?? 'not measured yet'} + {links.length === 0 ? — : links.map((l, i) => ( + + {i > 0 ? ' · ' : null} + {l.label} + + ))} +
      )} - Expected effects are not summed.
      ); } diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 32d9728..3a31beb 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -584,12 +584,30 @@ describe('guardrails summary and work', () => { expect(text(guardrails(BENCH))).toContain('Met 5'); }); - it('lists work by limit with the binding limit first', () => { - expect(rows(render({}).work, 1).map((r) => r.slice(0, 12))).toEqual(['DASynthetic ', 'ExecutionSyn']); - swr.result = { data: bench(40, false) }; - const work = render().work; - expect(rows(work, 1).map((r) => r.slice(0, 12))).toEqual(['ExecutionSyn', 'DASynthetic ']); - expect(text(work)).toContain('not estimated'); - expect(text(work)).toContain('Expected effects are not summed.'); + it('logs work in progress, then planned, then done, hiding other statuses', () => { + const work = render({}).work; + expect(text(work.split('')[0])).toContain('StatusWorkImpact categoryEffectLinks'); + // The legacy "Research-derived proposal" entry is not shown; the Linear project and evidence of an older entry become links. + expect(rows(work)).toEqual([ + 'In ProgressSynthetic unified binaryExecutionabout 20% faster block processing (synthetic demo)Linear · Synthetic run', + 'PlannedSynthetic blob batching changeDAnot measured yetSynthetic doc', + ]); + expect(work).toContain('Linear'); + expect(text(work)).not.toMatch(/legacy proposal|Expected effects are not summed/); + }); + + it('falls back to an older entry’s expected effect and area, and to an empty state', () => { + const snapshot = fixture(); + const [first, second] = snapshot.work; + snapshot.work = [ + { ...second, status: 'done', category: undefined, links: [{ label: 'Doc', url: 'https://example.com/doc' }, { label: 'Linear', url: 'https://example.com/linear' }] }, + { ...first, effect: undefined, expected: 'about 10% faster' }, + ]; + expect(rows(render({}, snapshot).work)).toEqual([ + 'In ProgressSynthetic unified binaryExecutionabout 10% fasterLinear · Synthetic run', + 'CompletedSynthetic blob batching changeDAnot measured yetDoc · Linear', + ]); + snapshot.work = [{ ...first, status: 'Proposed' }]; + expect(text(render({}, snapshot).work)).toContain('No work in progress or planned for this snapshot.'); }); }); diff --git a/app/scaling/fixtures/synthetic.v2.json b/app/scaling/fixtures/synthetic.v2.json index 85d8bfb..49dfca5 100644 --- a/app/scaling/fixtures/synthetic.v2.json +++ b/app/scaling/fixtures/synthetic.v2.json @@ -262,7 +262,9 @@ { "area": "execution", "title": "Synthetic unified binary", - "status": "In progress", + "status": "in-progress", + "category": "Execution", + "effect": "about 20% faster block processing (synthetic demo)", "owner": "Synthetic owner", "hypothesis": "Fewer process hops per block speed up block processing.", "change": "Run the consensus and execution clients in one process.", @@ -284,7 +286,8 @@ { "area": "da", "title": "Synthetic blob batching change", - "status": "Proposed", + "status": "planned", + "category": "DA", "owner": null, "hypothesis": "Denser batches post fewer bytes per gas.", "change": "Not yet specified.", @@ -293,6 +296,19 @@ "evidence": null, "linear": null, "links": [{ "label": "Synthetic doc", "url": "https://example.com/synthetic/doc" }] + }, + { + "area": "networking", + "title": "Synthetic legacy proposal", + "status": "Research-derived proposal", + "owner": null, + "hypothesis": "An entry from before the work log, which the page does not show.", + "change": "Not yet specified.", + "expected": null, + "realized": null, + "evidence": null, + "linear": null, + "links": [] } ] } diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index d682ce2..159770f 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -33,9 +33,9 @@ it('ends the first screen with the DA link, and no guardrails or benchmark links it('orders the first screen: freshness, headline, ladder, ranked limits, subsystems by workload, work, links', async () => { const html = renderToStaticMarkup(await ScalingPage()); - const order = ['Usage to 2026-01-02', 'Proven capacity', 'How close are we, and to what?', 'What breaks first', 'Subsystems by workload', 'Work by limit', 'DA by workload'].map((h) => html.indexOf(h)); + const order = ['Usage to 2026-01-02', 'Proven capacity', 'How close are we, and to what?', 'What breaks first', 'Subsystems by workload', 'Work log', 'DA by workload'].map((h) => html.indexOf(h)); expect(order.every((i, k) => i > (order[k - 1] ?? -1))).toBe(true); - expect(html).not.toMatch(/Subsystem detail|Progress<|Ceilings by bottleneck|Usage data as of/); + expect(html).not.toMatch(/Subsystem detail|>Progress<|Ceilings by bottleneck|Usage data as of/); }); it('dates snapshot data without a stale banner', async () => { diff --git a/app/scaling/snapshot.mjs b/app/scaling/snapshot.mjs index ffaa41d..a1ecefe 100644 --- a/app/scaling/snapshot.mjs +++ b/app/scaling/snapshot.mjs @@ -36,10 +36,11 @@ * projectId: string, issueId?: string, state: string, progress: number, scopeDone: number, * scopeTotal: number, targetDate?: string, updatedAt: string, url: string * }} LinearStatus + * @typedef {'DA' | 'Disk' | 'Execution' | 'Proving' | 'Latency'} WorkCategory * @typedef {{ - * area: Area, title: string, status: string, owner: string | null, hypothesis: string, change: string, - * expected: string | null, realized: string | null, evidence: Link | null, linear: LinearStatus | null, - * links: Link[] + * title: string, status: string, category?: WorkCategory, effect?: string | null, links?: Link[], + * owner?: string | null, area?: Area, hypothesis?: string, change?: string, + * expected?: string | null, realized?: string | null, evidence?: Link | null, linear?: LinearStatus | null * }} WorkItem * @typedef {{ * schemaVersion: 2, network: string, collectedAt: string, context: string, windows: Record, @@ -60,6 +61,9 @@ 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']; +// Work log statuses follow Linear's project states; impact categories are the page's limit names. +export const WORK_STATUSES = ['in-progress', 'planned', 'done']; +export const WORK_CATEGORIES = ['DA', 'Disk', 'Execution', 'Proving', 'Latency']; // Each section renders on its area page, or on the guardrails page for outcomes. const SECTION_IDS = ['da', 'execution', 'state', 'outcomes']; const INPUT_STATUSES = ['observed', 'configured', 'provisional', 'pending']; @@ -297,16 +301,21 @@ export function parseSnapshot(data) { date(l, 'updatedAt', path); if (typeof l.url !== 'string' || !l.url.startsWith('https://')) fail(`${path}.url`, 'expected https URL'); }; + // Work log entries need a title and a status; the page shows only WORK_STATUSES. Older entries keep their + // area, hypothesis, change, expected, realized, evidence and linear fields, now optional but still checked. list(data, 'work', 'snapshot').forEach((w, i) => { const path = `work[${i}]`; if (!isObj(w)) return fail(path, 'expected object'); - oneOf(w, 'area', path, AREAS); - for (const key of ['title', 'status', 'hypothesis', 'change']) str(w, key, path); - for (const key of ['owner', 'expected', 'realized']) nullableStr(w, key, path); - if (w.evidence !== null) link(w.evidence, `${path}.evidence`); - else if (w.realized !== null) fail(`${path}.evidence`, 'required when realized is set'); - if (w.linear !== null) linear(w.linear, `${path}.linear`); - list(w, 'links', path).forEach((l, j) => link(l, `${path}.links[${j}]`)); + for (const key of ['title', 'status']) str(w, key, path); + if (w.area !== undefined) oneOf(w, 'area', path, AREAS); + if (w.category !== undefined) oneOf(w, 'category', path, WORK_CATEGORIES); + if (w.area === undefined && w.category === undefined) fail(`${path}.category`, 'required without area'); + for (const key of ['hypothesis', 'change']) if (w[key] !== undefined) str(w, key, path); + for (const key of ['owner', 'expected', 'realized', 'effect']) if (w[key] !== undefined) nullableStr(w, key, path); + if (w.evidence != null) link(w.evidence, `${path}.evidence`); + else if (w.realized != null) fail(`${path}.evidence`, 'required when realized is set'); + if (w.linear != null) linear(w.linear, `${path}.linear`); + if (w.links !== undefined) list(w, 'links', path).forEach((l, j) => link(l, `${path}.links[${j}]`)); }); return errors.length ? { ok: false, errors } : { ok: true, snapshot: data }; diff --git a/app/scaling/snapshot.test.ts b/app/scaling/snapshot.test.ts index 457cd2a..5d8bff2 100644 --- a/app/scaling/snapshot.test.ts +++ b/app/scaling/snapshot.test.ts @@ -90,6 +90,23 @@ describe('parseSnapshot', () => { ]); }); + it('accepts a work log entry with only title, status, category, effect, links and owner, and checks each', () => { + const data = fixture(); + data.work.push({ title: 'Synthetic planned work', status: 'planned', category: 'Execution', effect: null, links: [{ label: 'Linear', url: 'https://example.com/linear' }], owner: 'Synthetic owner' }); + expect(errorsOf(data)).toEqual([]); + data.work.push( + { title: 'Bad category', status: 'planned', category: 'Gas' }, + { title: 'No category', status: 'planned' }, + { title: 'Bad link', status: 'planned', category: 'DA', effect: 3, links: [{ label: 'Doc', url: 'http://example.com' }] }, + ); + expect(errorsOf(data)).toEqual([ + 'work[4].category: expected one of DA, Disk, Execution, Proving, Latency', + 'work[5].category: required without area', + 'work[6].effect: expected string or null', + 'work[6].links[0].url: expected https URL', + ]); + }); + it('rejects a missing ceiling area and a duplicate guardrail id', () => { const data = fixture(); data.ceilings.pop(); From 1e1d13a5023f9ee7276dbcb50f058b32cf068707 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 17:10:59 -0700 Subject: [PATCH 30/33] feat(scaling): show live Linear status in the work log when LINEAR_API_KEY is set --- app/scaling/components/Scorecard.tsx | 6 +- app/scaling/components/Work.tsx | 8 ++- app/scaling/linearStatus.test.tsx | 84 +++++++++++++++++++++++++ app/scaling/linearStatus.ts | 91 ++++++++++++++++++++++++++++ app/scaling/page.test.tsx | 6 +- app/scaling/page.tsx | 3 +- 6 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 app/scaling/linearStatus.test.tsx create mode 100644 app/scaling/linearStatus.ts diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index eb01a8f..140bee3 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -2,6 +2,7 @@ import { daWorkloads, DA_WORKLOADS, type DaWorkload } from '../da/workloads'; import type { Proven } from '../proven/capacity'; +import type { WorkRow } from '../linearStatus'; import type { Snapshot } from '../snapshot.mjs'; import { bottleneckBars, Bottlenecks, diskLimits, pickStorage, rankLimits } from './Bottlenecks'; import { Headline, mainnetGasPerTx } from './Headline'; @@ -33,7 +34,8 @@ function Freshness({ snapshot, latestAt }: { snapshot: Snapshot; latestAt: numbe // Every section shares one bench fetch and the Mgas/s scale. Without a bench host, the proven rate // falls back to the snapshot's qualified rung. -export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl?: string }) { +// `work` is the snapshot's work with live Linear status applied on the server, when available. +export function Scorecard({ snapshot, benchUrl, work = snapshot.work }: { snapshot: Snapshot; benchUrl?: string; work?: WorkRow[] }) { const bench = useProvenCapacity(benchUrl); const data = bench.error ? undefined : bench.data; const { ladder } = snapshot; @@ -51,7 +53,7 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl - + ); } diff --git a/app/scaling/components/Work.tsx b/app/scaling/components/Work.tsx index 5ce2a37..c57ee2c 100644 --- a/app/scaling/components/Work.tsx +++ b/app/scaling/components/Work.tsx @@ -3,6 +3,7 @@ import { cn } from '../../components/ui/cn'; import { EmptyState } from '../../components/ui/EmptyState'; import { Text } from '../../components/ui/Text'; import { WORK_STATUSES } from '../snapshot.mjs'; +import type { WorkRow } from '../linearStatus'; import type { Area, Link, WorkCategory, WorkItem } from '../snapshot.mjs'; import { Info } from './Info'; @@ -10,6 +11,7 @@ import { Info } from './Info'; const STATUS: Record = { 'in-progress': 'In Progress', planned: 'Planned', done: 'Completed' }; // Older entries name an area instead of a category. const CATEGORY: Record = { da: 'DA', execution: 'Execution', state: 'Disk', networking: 'Latency' }; +const at = (iso: string) => `${iso.slice(0, 16).replace('T', ' ')} UTC`; const TH = 'px-3 py-2 text-left text-[11px] font-[600] uppercase tracking-wide text-bds-gray-60'; const TD = 'px-3 py-2.5 align-top'; @@ -22,7 +24,7 @@ const linksOf = (w: WorkItem): Link[] => { }; // In progress first, then planned, then done; entries with any other status are not shown. -export function Work({ work }: { work: WorkItem[] }) { +export function Work({ work }: { work: WorkRow[] }) { const rows = work.filter((w) => WORK_STATUSES.includes(w.status)).toSorted((a, b) => WORK_STATUSES.indexOf(a.status) - WORK_STATUSES.indexOf(b.status)); return (
      @@ -45,7 +47,9 @@ export function Work({ work }: { work: WorkItem[] }) { return (
      - {STATUS[w.status]} + {/* Live rows show Linear's own status name and progress; the title says where the status came from. */} + {w.live?.name ?? STATUS[w.status]} + {w.live && w.live.progress > 0 ? {Math.round(w.live.progress * 100)}% : null} diff --git a/app/scaling/linearStatus.test.tsx b/app/scaling/linearStatus.test.tsx new file mode 100644 index 0000000..4496f70 --- /dev/null +++ b/app/scaling/linearStatus.test.tsx @@ -0,0 +1,84 @@ +import { renderToStaticMarkup } from 'react-dom/server'; +import { afterEach, expect, it, vi } from 'vitest'; + +import { Work } from './components/Work'; +import { applyLinear, fetchLinearProjects, loadLiveWork, projectIdOf } from './linearStatus'; +import type { WorkItem } from './snapshot.mjs'; + +// Synthetic projects and slugs only. +const row = (title: string, slugId: string | null, status = 'planned'): WorkItem => ({ + title, status, category: 'Execution', + links: slugId ? [{ label: 'Doc', url: 'https://example.com/doc' }, { label: 'Linear', url: `https://linear.app/example/project/synthetic-${slugId}` }] : [], +}); +const project = (type: string, name = type, progress = 0.25) => ({ name: `Project ${name}`, status: { name, type }, progress, lead: { name: 'Synthetic lead' }, url: 'https://linear.app/example/project/x' }); +const AT = '2026-01-02T03:04:00.000Z'; + +afterEach(() => { + vi.unstubAllEnvs(); + vi.restoreAllMocks(); +}); + +it('finds the project slug id in a linear.app project link', () => { + expect(projectIdOf(row('a', 'a1b2c3d4e5f6'))).toBe('a1b2c3d4e5f6'); + expect(projectIdOf({ title: 'b', status: 'planned', links: [{ label: 'Linear', url: 'https://linear.app/example/issue/ABC-1' }] })).toBeNull(); + expect(projectIdOf(row('c', null))).toBeNull(); +}); + +it('maps Linear status types onto the log, hides canceled projects and keeps the snapshot otherwise', () => { + const work = ['started', 'planned', 'backlog', 'unstarted', 'completed', 'canceled', 'paused'].map((t, i) => row(t, `${i}`.repeat(12), 'in-progress')); + const projects = Object.fromEntries(work.map((w) => [projectIdOf(w)!, project(w.title, `${w.title} name`)])); + const rows = applyLinear([...work, row('no project', null, 'planned'), row('unknown', 'ffffffffffff', 'done')], projects, AT); + expect(rows.map((r) => [r.title, r.status, r.live?.name ?? null])).toEqual([ + ['started', 'in-progress', 'started name'], + ['planned', 'planned', 'planned name'], + ['backlog', 'planned', 'backlog name'], + ['unstarted', 'planned', 'unstarted name'], + ['completed', 'done', 'completed name'], + // canceled is gone; paused is not a type the log maps, so the snapshot's status stays. + ['paused', 'in-progress', null], + ['no project', 'planned', null], + ['unknown', 'done', null], + ]); +}); + +it('asks for every project in one request with aliases, sending the key only as the Authorization header', async () => { + const fetchImpl = vi.fn(async () => Response.json({ data: { p0: project('started'), p1: null, p2: { name: 'bad shape' } } })); + const projects = await fetchLinearProjects(['aaaaaaaaaaaa', 'bbbbbbbbbbbb', 'cccccccccccc'], 'synthetic-key', fetchImpl as unknown as typeof fetch); + expect(Object.keys(projects)).toEqual(['aaaaaaaaaaaa']); + expect(fetchImpl).toHaveBeenCalledOnce(); + const [url, init] = fetchImpl.mock.calls[0] as unknown as [string, RequestInit]; + expect(url).toBe('https://api.linear.app/graphql'); + expect(init.headers).toEqual({ 'Content-Type': 'application/json', Authorization: 'synthetic-key' }); + expect(init.signal).toBeInstanceOf(AbortSignal); + const { query } = JSON.parse(init.body as string); + expect(query).toContain('p0: project(id: "aaaaaaaaaaaa") { name status { name type } progress lead { name } url }'); + expect(query).toContain('p2: project(id: "cccccccccccc")'); + await expect(fetchLinearProjects(['a'], 'k', (async () => new Response('', { status: 401 })) as typeof fetch)).rejects.toThrow('Linear GraphQL: 401'); + await expect(fetchLinearProjects(['a'], 'k', (async () => Response.json({ errors: [{ message: 'x' }] })) as typeof fetch)).rejects.toThrow('no data'); +}); + +it('keeps the snapshot without a key, without project links, or when Linear fails, logging no key', async () => { + const work = [row('a', 'aaaaaaaaaaaa')]; + const load = vi.fn(async () => ({ projects: { aaaaaaaaaaaa: project('completed', 'Completed') }, at: AT })); + vi.stubEnv('LINEAR_API_KEY', ''); + expect(await loadLiveWork(work, load)).toBe(work); + expect(load).not.toHaveBeenCalled(); + + vi.stubEnv('LINEAR_API_KEY', 'synthetic-secret'); + expect(await loadLiveWork([row('b', null)], load)).toEqual([row('b', null)]); + expect(load).not.toHaveBeenCalled(); + expect((await loadLiveWork(work, load))[0]).toMatchObject({ status: 'done', live: { name: 'Completed', progress: 0.25, at: AT } }); + expect(load).toHaveBeenCalledWith(['aaaaaaaaaaaa']); + + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + expect(await loadLiveWork(work, async () => Promise.reject(new DOMException('timed out', 'TimeoutError')))).toBe(work); + expect(JSON.stringify(warn.mock.calls)).not.toContain('synthetic-secret'); +}); + +it('labels live rows with Linear’s status name, progress and fetch time, and snapshot rows as such', () => { + const [live] = applyLinear([row('Live work', 'aaaaaaaaaaaa')], { aaaaaaaaaaaa: project('started', 'In Progress', 0.4) }, AT); + const html = renderToStaticMarkup(); + expect(html).toContain('title="Live from Linear, 2026-01-02 03:04 UTC">In Progress'); + expect(html).toContain('>40%'); + expect(html).toContain('title="From snapshot">Planned'); +}); diff --git a/app/scaling/linearStatus.ts b/app/scaling/linearStatus.ts new file mode 100644 index 0000000..a6fd511 --- /dev/null +++ b/app/scaling/linearStatus.ts @@ -0,0 +1,91 @@ +import { unstable_cache } from 'next/cache'; + +import type { WorkItem } from './snapshot.mjs'; + +// Server-only: reads LINEAR_API_KEY, which must never reach the client or a log line. +const LINEAR_GRAPHQL_URL = 'https://api.linear.app/graphql'; +const TIMEOUT_MS = 5_000; +const REVALIDATE_SECONDS = 600; +// https://linear.app//project/-; the slugId identifies the project. +const PROJECT_URL = /^https:\/\/linear\.app\/[^/]+\/project\/(?:[\w-]+-)?([0-9a-f]{8,})\/?$/; + +type Status = 'in-progress' | 'planned' | 'done'; +export type LiveStatus = { name: string; progress: number; at: string }; +// A work row as the work log renders it: live rows carry Linear's status name, progress and fetch time. +export type WorkRow = WorkItem & { live?: LiveStatus }; +type Project = { name: string; status: { name: string; type: string }; progress: number; url: string }; + +// Linear status types map onto the log's statuses; canceled projects drop out, other types keep the snapshot's. +const STATUS_OF: Record = { + started: 'in-progress', planned: 'planned', backlog: 'planned', unstarted: 'planned', completed: 'done', canceled: 'canceled', +}; + +export const projectIdOf = (w: WorkItem): string | null => { + for (const l of w.links ?? []) { + const match = PROJECT_URL.exec(l.url); + if (match) return match[1]; + } + return null; +}; + +const isProject = (p: unknown): p is Project => { + if (typeof p !== 'object' || p === null) return false; + const { name, status, progress, url } = p as Record; + const s = status as Record | null; + return typeof name === 'string' && typeof url === 'string' && typeof progress === 'number' && progress >= 0 && progress <= 1 + && typeof s === 'object' && s !== null && typeof s.name === 'string' && typeof s.type === 'string'; +}; + +// One request for every project, one alias each. Unknown projects and malformed entries come back missing. +// Throws on transport failure so a failed fetch is not cached. +export async function fetchLinearProjects(ids: string[], key: string, fetchImpl: typeof fetch = fetch): Promise> { + const fields = 'name status { name type } progress lead { name } url'; + const query = `query { ${ids.map((id, i) => `p${i}: project(id: ${JSON.stringify(id)}) { ${fields} }`).join(' ')} }`; + const res = await fetchImpl(LINEAR_GRAPHQL_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: key }, + body: JSON.stringify({ query }), + signal: AbortSignal.timeout(TIMEOUT_MS), + }); + if (!res.ok) throw new Error(`Linear GraphQL: ${res.status}`); + const body = (await res.json()) as { data?: Record | null }; + if (typeof body?.data !== 'object' || body.data === null) throw new Error('Linear GraphQL: no data'); + const projects: Record = {}; + ids.forEach((id, i) => { + const p = body.data![`p${i}`]; + if (isProject(p)) projects[id] = p; + }); + return projects; +} + +const fetchCached = unstable_cache( + async (ids: string[]) => ({ projects: await fetchLinearProjects(ids, process.env.LINEAR_API_KEY!), at: new Date().toISOString() }), + ['scaling-linear-projects'], + { revalidate: REVALIDATE_SECONDS }, +); + +// Rows whose Linear project resolves take its status; the rest keep the snapshot's. Any failure keeps the snapshot. +export function applyLinear(work: WorkItem[], projects: Record, at: string): WorkRow[] { + return work.flatMap((w): WorkRow[] => { + const id = projectIdOf(w); + const p = id ? projects[id] : undefined; + const status = p ? STATUS_OF[p.status.type] : undefined; + if (!p || !status) return [w]; + if (status === 'canceled') return []; + return [{ ...w, status, live: { name: p.status.name, progress: p.progress, at } }]; + }); +} + +export async function loadLiveWork(work: WorkItem[], load = fetchCached): Promise { + if (!process.env.LINEAR_API_KEY) return work; + const ids = [...new Set(work.map(projectIdOf).filter((id) => id !== null))].sort(); + if (!ids.length) return work; + try { + const { projects, at } = await load(ids); + return applyLinear(work, projects, at); + } catch (error) { + // The message names the status or timeout only, never the key. + console.warn('[scaling] Linear status unavailable, using the snapshot:', error instanceof Error ? error.message : 'unknown error'); + return work; + } +} diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 159770f..9a081a7 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -14,7 +14,11 @@ import ScalingPage from './page'; // No network in tests: the live archive size is unavailable, so the page keeps the snapshot's. vi.mock('./archiveSize', async (original) => ({ ...(await original()), loadArchiveSize: async () => null })); -beforeEach(() => vi.stubEnv('SCALING_SNAPSHOT_PATH', fileURLToPath(new URL('./fixtures/synthetic.v2.json', import.meta.url)))); +beforeEach(() => { + vi.stubEnv('SCALING_SNAPSHOT_PATH', fileURLToPath(new URL('./fixtures/synthetic.v2.json', import.meta.url))); + // Without a key the work log keeps the snapshot's status and makes no request. + vi.stubEnv('LINEAR_API_KEY', ''); +}); afterEach(() => vi.unstubAllEnvs()); const area = async (id: string) => renderToStaticMarkup(await ScalingAreaPage({ params: Promise.resolve({ area: id }) })); diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index 0fec28a..ccff4e6 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -4,6 +4,7 @@ import { Text } from '../components/ui/Text'; import { Scorecard } from './components/Scorecard'; import { loadArchiveSize, withArchiveSize } from './archiveSize'; +import { loadLiveWork } from './linearStatus'; import { Header, loadSnapshot, Unavailable } from './loadSnapshot'; // The snapshot is a runtime file, so render per request rather than at build. @@ -18,7 +19,7 @@ export default async function ScalingPage() { return (
      - + DA by workload From a2d1d4c6d67249cb9c7a4a4062cd987122386ad6 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 17:12:42 -0700 Subject: [PATCH 31/33] fix(scaling): show 0% progress on live Linear rows too --- app/scaling/components/Work.tsx | 2 +- app/scaling/linearStatus.test.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/scaling/components/Work.tsx b/app/scaling/components/Work.tsx index c57ee2c..56f6f22 100644 --- a/app/scaling/components/Work.tsx +++ b/app/scaling/components/Work.tsx @@ -49,7 +49,7 @@ export function Work({ work }: { work: WorkRow[] }) {
      {/* Live rows show Linear's own status name and progress; the title says where the status came from. */} {w.live?.name ?? STATUS[w.status]} - {w.live && w.live.progress > 0 ? {Math.round(w.live.progress * 100)}% : null} + {w.live ? {Math.round(w.live.progress * 100)}% : null} diff --git a/app/scaling/linearStatus.test.tsx b/app/scaling/linearStatus.test.tsx index 4496f70..5bbbfc8 100644 --- a/app/scaling/linearStatus.test.tsx +++ b/app/scaling/linearStatus.test.tsx @@ -81,4 +81,6 @@ it('labels live rows with Linear’s status name, progress and fetch time, and s expect(html).toContain('title="Live from Linear, 2026-01-02 03:04 UTC">In Progress'); expect(html).toContain('>40%'); expect(html).toContain('title="From snapshot">Planned'); + const [started] = applyLinear([row('Just started', 'aaaaaaaaaaaa')], { aaaaaaaaaaaa: project('started', 'In Progress', 0) }, AT); + expect(renderToStaticMarkup()).toContain('>0%'); }); From 4c406c3dc03d4908f7921d888a203d886f6e775c Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 21:39:18 -0700 Subject: [PATCH 32/33] revert(scaling): drop live Linear status from the work log, keeping the snapshot status and links --- app/scaling/components/Scorecard.tsx | 6 +- app/scaling/components/Work.tsx | 8 +-- app/scaling/linearStatus.test.tsx | 86 -------------------------- app/scaling/linearStatus.ts | 91 ---------------------------- app/scaling/page.test.tsx | 6 +- app/scaling/page.tsx | 3 +- 6 files changed, 6 insertions(+), 194 deletions(-) delete mode 100644 app/scaling/linearStatus.test.tsx delete mode 100644 app/scaling/linearStatus.ts diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index 140bee3..eb01a8f 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -2,7 +2,6 @@ import { daWorkloads, DA_WORKLOADS, type DaWorkload } from '../da/workloads'; import type { Proven } from '../proven/capacity'; -import type { WorkRow } from '../linearStatus'; import type { Snapshot } from '../snapshot.mjs'; import { bottleneckBars, Bottlenecks, diskLimits, pickStorage, rankLimits } from './Bottlenecks'; import { Headline, mainnetGasPerTx } from './Headline'; @@ -34,8 +33,7 @@ function Freshness({ snapshot, latestAt }: { snapshot: Snapshot; latestAt: numbe // Every section shares one bench fetch and the Mgas/s scale. Without a bench host, the proven rate // falls back to the snapshot's qualified rung. -// `work` is the snapshot's work with live Linear status applied on the server, when available. -export function Scorecard({ snapshot, benchUrl, work = snapshot.work }: { snapshot: Snapshot; benchUrl?: string; work?: WorkRow[] }) { +export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl?: string }) { const bench = useProvenCapacity(benchUrl); const data = bench.error ? undefined : bench.data; const { ladder } = snapshot; @@ -53,7 +51,7 @@ export function Scorecard({ snapshot, benchUrl, work = snapshot.work }: { snapsh - + ); } diff --git a/app/scaling/components/Work.tsx b/app/scaling/components/Work.tsx index 56f6f22..5ce2a37 100644 --- a/app/scaling/components/Work.tsx +++ b/app/scaling/components/Work.tsx @@ -3,7 +3,6 @@ import { cn } from '../../components/ui/cn'; import { EmptyState } from '../../components/ui/EmptyState'; import { Text } from '../../components/ui/Text'; import { WORK_STATUSES } from '../snapshot.mjs'; -import type { WorkRow } from '../linearStatus'; import type { Area, Link, WorkCategory, WorkItem } from '../snapshot.mjs'; import { Info } from './Info'; @@ -11,7 +10,6 @@ import { Info } from './Info'; const STATUS: Record = { 'in-progress': 'In Progress', planned: 'Planned', done: 'Completed' }; // Older entries name an area instead of a category. const CATEGORY: Record = { da: 'DA', execution: 'Execution', state: 'Disk', networking: 'Latency' }; -const at = (iso: string) => `${iso.slice(0, 16).replace('T', ' ')} UTC`; const TH = 'px-3 py-2 text-left text-[11px] font-[600] uppercase tracking-wide text-bds-gray-60'; const TD = 'px-3 py-2.5 align-top'; @@ -24,7 +22,7 @@ const linksOf = (w: WorkItem): Link[] => { }; // In progress first, then planned, then done; entries with any other status are not shown. -export function Work({ work }: { work: WorkRow[] }) { +export function Work({ work }: { work: WorkItem[] }) { const rows = work.filter((w) => WORK_STATUSES.includes(w.status)).toSorted((a, b) => WORK_STATUSES.indexOf(a.status) - WORK_STATUSES.indexOf(b.status)); return (
      @@ -47,9 +45,7 @@ export function Work({ work }: { work: WorkRow[] }) { return (
      - {/* Live rows show Linear's own status name and progress; the title says where the status came from. */} - {w.live?.name ?? STATUS[w.status]} - {w.live ? {Math.round(w.live.progress * 100)}% : null} + {STATUS[w.status]} diff --git a/app/scaling/linearStatus.test.tsx b/app/scaling/linearStatus.test.tsx deleted file mode 100644 index 5bbbfc8..0000000 --- a/app/scaling/linearStatus.test.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { renderToStaticMarkup } from 'react-dom/server'; -import { afterEach, expect, it, vi } from 'vitest'; - -import { Work } from './components/Work'; -import { applyLinear, fetchLinearProjects, loadLiveWork, projectIdOf } from './linearStatus'; -import type { WorkItem } from './snapshot.mjs'; - -// Synthetic projects and slugs only. -const row = (title: string, slugId: string | null, status = 'planned'): WorkItem => ({ - title, status, category: 'Execution', - links: slugId ? [{ label: 'Doc', url: 'https://example.com/doc' }, { label: 'Linear', url: `https://linear.app/example/project/synthetic-${slugId}` }] : [], -}); -const project = (type: string, name = type, progress = 0.25) => ({ name: `Project ${name}`, status: { name, type }, progress, lead: { name: 'Synthetic lead' }, url: 'https://linear.app/example/project/x' }); -const AT = '2026-01-02T03:04:00.000Z'; - -afterEach(() => { - vi.unstubAllEnvs(); - vi.restoreAllMocks(); -}); - -it('finds the project slug id in a linear.app project link', () => { - expect(projectIdOf(row('a', 'a1b2c3d4e5f6'))).toBe('a1b2c3d4e5f6'); - expect(projectIdOf({ title: 'b', status: 'planned', links: [{ label: 'Linear', url: 'https://linear.app/example/issue/ABC-1' }] })).toBeNull(); - expect(projectIdOf(row('c', null))).toBeNull(); -}); - -it('maps Linear status types onto the log, hides canceled projects and keeps the snapshot otherwise', () => { - const work = ['started', 'planned', 'backlog', 'unstarted', 'completed', 'canceled', 'paused'].map((t, i) => row(t, `${i}`.repeat(12), 'in-progress')); - const projects = Object.fromEntries(work.map((w) => [projectIdOf(w)!, project(w.title, `${w.title} name`)])); - const rows = applyLinear([...work, row('no project', null, 'planned'), row('unknown', 'ffffffffffff', 'done')], projects, AT); - expect(rows.map((r) => [r.title, r.status, r.live?.name ?? null])).toEqual([ - ['started', 'in-progress', 'started name'], - ['planned', 'planned', 'planned name'], - ['backlog', 'planned', 'backlog name'], - ['unstarted', 'planned', 'unstarted name'], - ['completed', 'done', 'completed name'], - // canceled is gone; paused is not a type the log maps, so the snapshot's status stays. - ['paused', 'in-progress', null], - ['no project', 'planned', null], - ['unknown', 'done', null], - ]); -}); - -it('asks for every project in one request with aliases, sending the key only as the Authorization header', async () => { - const fetchImpl = vi.fn(async () => Response.json({ data: { p0: project('started'), p1: null, p2: { name: 'bad shape' } } })); - const projects = await fetchLinearProjects(['aaaaaaaaaaaa', 'bbbbbbbbbbbb', 'cccccccccccc'], 'synthetic-key', fetchImpl as unknown as typeof fetch); - expect(Object.keys(projects)).toEqual(['aaaaaaaaaaaa']); - expect(fetchImpl).toHaveBeenCalledOnce(); - const [url, init] = fetchImpl.mock.calls[0] as unknown as [string, RequestInit]; - expect(url).toBe('https://api.linear.app/graphql'); - expect(init.headers).toEqual({ 'Content-Type': 'application/json', Authorization: 'synthetic-key' }); - expect(init.signal).toBeInstanceOf(AbortSignal); - const { query } = JSON.parse(init.body as string); - expect(query).toContain('p0: project(id: "aaaaaaaaaaaa") { name status { name type } progress lead { name } url }'); - expect(query).toContain('p2: project(id: "cccccccccccc")'); - await expect(fetchLinearProjects(['a'], 'k', (async () => new Response('', { status: 401 })) as typeof fetch)).rejects.toThrow('Linear GraphQL: 401'); - await expect(fetchLinearProjects(['a'], 'k', (async () => Response.json({ errors: [{ message: 'x' }] })) as typeof fetch)).rejects.toThrow('no data'); -}); - -it('keeps the snapshot without a key, without project links, or when Linear fails, logging no key', async () => { - const work = [row('a', 'aaaaaaaaaaaa')]; - const load = vi.fn(async () => ({ projects: { aaaaaaaaaaaa: project('completed', 'Completed') }, at: AT })); - vi.stubEnv('LINEAR_API_KEY', ''); - expect(await loadLiveWork(work, load)).toBe(work); - expect(load).not.toHaveBeenCalled(); - - vi.stubEnv('LINEAR_API_KEY', 'synthetic-secret'); - expect(await loadLiveWork([row('b', null)], load)).toEqual([row('b', null)]); - expect(load).not.toHaveBeenCalled(); - expect((await loadLiveWork(work, load))[0]).toMatchObject({ status: 'done', live: { name: 'Completed', progress: 0.25, at: AT } }); - expect(load).toHaveBeenCalledWith(['aaaaaaaaaaaa']); - - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - expect(await loadLiveWork(work, async () => Promise.reject(new DOMException('timed out', 'TimeoutError')))).toBe(work); - expect(JSON.stringify(warn.mock.calls)).not.toContain('synthetic-secret'); -}); - -it('labels live rows with Linear’s status name, progress and fetch time, and snapshot rows as such', () => { - const [live] = applyLinear([row('Live work', 'aaaaaaaaaaaa')], { aaaaaaaaaaaa: project('started', 'In Progress', 0.4) }, AT); - const html = renderToStaticMarkup(); - expect(html).toContain('title="Live from Linear, 2026-01-02 03:04 UTC">In Progress'); - expect(html).toContain('>40%'); - expect(html).toContain('title="From snapshot">Planned'); - const [started] = applyLinear([row('Just started', 'aaaaaaaaaaaa')], { aaaaaaaaaaaa: project('started', 'In Progress', 0) }, AT); - expect(renderToStaticMarkup()).toContain('>0%'); -}); diff --git a/app/scaling/linearStatus.ts b/app/scaling/linearStatus.ts deleted file mode 100644 index a6fd511..0000000 --- a/app/scaling/linearStatus.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { unstable_cache } from 'next/cache'; - -import type { WorkItem } from './snapshot.mjs'; - -// Server-only: reads LINEAR_API_KEY, which must never reach the client or a log line. -const LINEAR_GRAPHQL_URL = 'https://api.linear.app/graphql'; -const TIMEOUT_MS = 5_000; -const REVALIDATE_SECONDS = 600; -// https://linear.app//project/-; the slugId identifies the project. -const PROJECT_URL = /^https:\/\/linear\.app\/[^/]+\/project\/(?:[\w-]+-)?([0-9a-f]{8,})\/?$/; - -type Status = 'in-progress' | 'planned' | 'done'; -export type LiveStatus = { name: string; progress: number; at: string }; -// A work row as the work log renders it: live rows carry Linear's status name, progress and fetch time. -export type WorkRow = WorkItem & { live?: LiveStatus }; -type Project = { name: string; status: { name: string; type: string }; progress: number; url: string }; - -// Linear status types map onto the log's statuses; canceled projects drop out, other types keep the snapshot's. -const STATUS_OF: Record = { - started: 'in-progress', planned: 'planned', backlog: 'planned', unstarted: 'planned', completed: 'done', canceled: 'canceled', -}; - -export const projectIdOf = (w: WorkItem): string | null => { - for (const l of w.links ?? []) { - const match = PROJECT_URL.exec(l.url); - if (match) return match[1]; - } - return null; -}; - -const isProject = (p: unknown): p is Project => { - if (typeof p !== 'object' || p === null) return false; - const { name, status, progress, url } = p as Record; - const s = status as Record | null; - return typeof name === 'string' && typeof url === 'string' && typeof progress === 'number' && progress >= 0 && progress <= 1 - && typeof s === 'object' && s !== null && typeof s.name === 'string' && typeof s.type === 'string'; -}; - -// One request for every project, one alias each. Unknown projects and malformed entries come back missing. -// Throws on transport failure so a failed fetch is not cached. -export async function fetchLinearProjects(ids: string[], key: string, fetchImpl: typeof fetch = fetch): Promise> { - const fields = 'name status { name type } progress lead { name } url'; - const query = `query { ${ids.map((id, i) => `p${i}: project(id: ${JSON.stringify(id)}) { ${fields} }`).join(' ')} }`; - const res = await fetchImpl(LINEAR_GRAPHQL_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json', Authorization: key }, - body: JSON.stringify({ query }), - signal: AbortSignal.timeout(TIMEOUT_MS), - }); - if (!res.ok) throw new Error(`Linear GraphQL: ${res.status}`); - const body = (await res.json()) as { data?: Record | null }; - if (typeof body?.data !== 'object' || body.data === null) throw new Error('Linear GraphQL: no data'); - const projects: Record = {}; - ids.forEach((id, i) => { - const p = body.data![`p${i}`]; - if (isProject(p)) projects[id] = p; - }); - return projects; -} - -const fetchCached = unstable_cache( - async (ids: string[]) => ({ projects: await fetchLinearProjects(ids, process.env.LINEAR_API_KEY!), at: new Date().toISOString() }), - ['scaling-linear-projects'], - { revalidate: REVALIDATE_SECONDS }, -); - -// Rows whose Linear project resolves take its status; the rest keep the snapshot's. Any failure keeps the snapshot. -export function applyLinear(work: WorkItem[], projects: Record, at: string): WorkRow[] { - return work.flatMap((w): WorkRow[] => { - const id = projectIdOf(w); - const p = id ? projects[id] : undefined; - const status = p ? STATUS_OF[p.status.type] : undefined; - if (!p || !status) return [w]; - if (status === 'canceled') return []; - return [{ ...w, status, live: { name: p.status.name, progress: p.progress, at } }]; - }); -} - -export async function loadLiveWork(work: WorkItem[], load = fetchCached): Promise { - if (!process.env.LINEAR_API_KEY) return work; - const ids = [...new Set(work.map(projectIdOf).filter((id) => id !== null))].sort(); - if (!ids.length) return work; - try { - const { projects, at } = await load(ids); - return applyLinear(work, projects, at); - } catch (error) { - // The message names the status or timeout only, never the key. - console.warn('[scaling] Linear status unavailable, using the snapshot:', error instanceof Error ? error.message : 'unknown error'); - return work; - } -} diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 9a081a7..159770f 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -14,11 +14,7 @@ import ScalingPage from './page'; // No network in tests: the live archive size is unavailable, so the page keeps the snapshot's. vi.mock('./archiveSize', async (original) => ({ ...(await original()), loadArchiveSize: async () => null })); -beforeEach(() => { - vi.stubEnv('SCALING_SNAPSHOT_PATH', fileURLToPath(new URL('./fixtures/synthetic.v2.json', import.meta.url))); - // Without a key the work log keeps the snapshot's status and makes no request. - vi.stubEnv('LINEAR_API_KEY', ''); -}); +beforeEach(() => vi.stubEnv('SCALING_SNAPSHOT_PATH', fileURLToPath(new URL('./fixtures/synthetic.v2.json', import.meta.url)))); afterEach(() => vi.unstubAllEnvs()); const area = async (id: string) => renderToStaticMarkup(await ScalingAreaPage({ params: Promise.resolve({ area: id }) })); diff --git a/app/scaling/page.tsx b/app/scaling/page.tsx index ccff4e6..0fec28a 100644 --- a/app/scaling/page.tsx +++ b/app/scaling/page.tsx @@ -4,7 +4,6 @@ import { Text } from '../components/ui/Text'; import { Scorecard } from './components/Scorecard'; import { loadArchiveSize, withArchiveSize } from './archiveSize'; -import { loadLiveWork } from './linearStatus'; import { Header, loadSnapshot, Unavailable } from './loadSnapshot'; // The snapshot is a runtime file, so render per request rather than at build. @@ -19,7 +18,7 @@ export default async function ScalingPage() { return (
      - + DA by workload From b86499dd1c0235279d8f0edfbd042199800f5ad4 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 27 Oct 2026 22:25:09 -0700 Subject: [PATCH 33/33] fix(scaling): take disk growth from the workload with the heaviest transactions --- app/scaling/components/Bottlenecks.tsx | 15 +++++++++------ app/scaling/components/scorecard.test.tsx | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index f5d4790..fb8af1c 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -2,7 +2,7 @@ import { Card } from '../../components/ui/Card'; import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; import { BLOB_DATA_BYTES, BLOB_TARGET, daCapacity, fmt, L1_SLOT_SECONDS, type DaWorkload } from '../da/workloads'; -import { runHref, type PipelineStep, type Proven, type ProvenCapacity, type Run, type Storage, type Workload } from '../proven/capacity'; +import { runHref, successfulTps, type PipelineStep, type Proven, type ProvenCapacity, type Run, type Storage, type Workload } from '../proven/capacity'; import { EXECUTE_PAYLOAD_P99_MS, PROOF_LAG_P99_BLOCKS, REPLAY_BLOCKS_PER_SECOND } from './Guardrails'; import { Info } from './Info'; import { day } from './ProvenCapacity'; @@ -140,16 +140,19 @@ export type StorageSource = { storage: Storage; run: Run }; export const bytes = (n: number) => (n >= 1e12 ? `${(n / 1e12).toFixed(1)} TB` : `${(n / 1e9).toFixed(1)} GB`); const WORKLOAD: Record = { mix: 'mainnet mix', payment: 'payments', swap: 'swaps' }; -// The mix run's storage data when present, else the newest payments or swaps run that has it. +// Storage data from the run with the heaviest transactions (most gas per successful transaction); ties go to the newest. +// Lighter bench transactions than mainnet's overstate history bytes per gas. export function pickStorage(data: ProvenCapacity | undefined): StorageSource | null { if (!data) return null; const { current, da } = data; - const others = [current.payment?.run, current.swap?.run, da.payment, da.swap].filter((r) => r?.storage).toSorted((a, b) => b!.createdAt - a!.createdAt); - const run = [current.mix?.run, da.mix].find((r) => r?.storage) ?? others[0]; + const gasPerTx = (r: Run) => (successfulTps(r) > 0 ? r.gps / successfulTps(r) : 0); + const runs = [current.mix?.run, current.payment?.run, current.swap?.run, da.mix, da.payment, da.swap].filter((r): r is Run => !!r?.storage); + const run = runs.toSorted((a, b) => gasPerTx(b) - gasPerTx(a) || b.createdAt - a.createdAt)[0]; return run ? { storage: run.storage!, run } : null; } const YEAR_SECONDS = 31_557_600; +const HEAVIEST = 'Uses the workload with the heaviest transactions, because bench workloads with lighter transactions than mainnet overstate history bytes per gas. A production measurement will replace this.'; const days = (seconds: number) => Math.round(seconds / 86_400); const years = (n: number) => (n === 1 ? 'one year' : `${n} years`); @@ -191,7 +194,7 @@ export function diskLimits(source: StorageSource | null, { policy, production }: years: runway!.fullNode, evidence: `Projected from bench growth: ${kept} fit in ${bytes(budget)}. Upper bound.`, overBudget: `Projected from bench growth: today's state (${bytes(current.state)}) leaves no room in ${bytes(budget)}.`, - help: `Inputs: ${budgetText}; today's state ${bytes(current.state)}, measured at ${from}'s snapshot; ${bytesPerGas.history.toPrecision(3)} bytes per gas of history and ${bytesPerGas.proofs!.toPrecision(3)} of proofs from ${from}; ${state!.value} bytes per gas of production state growth${state!.source ? ` from ${state!.source}` : ''}; ${policyText}. Upper bound: history bytes per gas is a lower bound, because the sequencer prunes history.`, + help: `Inputs: ${budgetText}; today's state ${bytes(current.state)}, measured at ${from}'s snapshot; ${bytesPerGas.history.toPrecision(3)} bytes per gas of history and ${bytesPerGas.proofs!.toPrecision(3)} of proofs from ${from}; ${state!.value} bytes per gas of production state growth${state!.source ? ` from ${state!.source}` : ''}; ${policyText}. Upper bound: history bytes per gas is a lower bound, because the sequencer prunes history. ${HEAVIEST}`, }; }, ); @@ -212,7 +215,7 @@ export function diskLimits(source: StorageSource | null, { policy, production }: years: runway!.archive, evidence: `Projected from bench growth: ${years(runway!.archive)} ${runway!.archive === 1 ? 'fills' : 'fill'} the ${bytes(budget)} disk (${bytes(archiveNow!.value)} used today). Upper bound.`, overBudget: `Projected from bench growth: today's archive (${bytes(archiveNow!.value)}) already fills the ${bytes(budget)} disk.`, - help: `Inputs: ${budgetText}; today's archive ${bytes(archiveNow!.value)} from ${archiveNow!.source} on ${archiveNow!.asOf}${archiveNow!.note ? ` (${archiveNow!.note})` : ''}; ${bytesPerGas.archive.toPrecision(3)} bytes per gas from ${from}${stateText}; ${policyText}. Upper bound: bench bytes per gas is a lower bound, because the sequencer prunes history. That workload may write more bytes per gas than mainnet traffic does (Base's whole archive is ${bytes(archiveNow!.value)}), which makes the ceiling conservative in the other direction.`, + help: `Inputs: ${budgetText}; today's archive ${bytes(archiveNow!.value)} from ${archiveNow!.source} on ${archiveNow!.asOf}${archiveNow!.note ? ` (${archiveNow!.note})` : ''}; ${bytesPerGas.archive.toPrecision(3)} bytes per gas from ${from}${stateText}; ${policyText}. Upper bound: bench bytes per gas is a lower bound, because the sequencer prunes history. That workload may write more bytes per gas than mainnet traffic does (Base's whole archive is ${bytes(archiveNow!.value)}), which makes the ceiling conservative in the other direction. ${HEAVIEST}`, }; }, ); diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 3a31beb..e8c6edc 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -569,6 +569,25 @@ describe('disk ceilings', () => { const { ranked } = render({ benchUrl: BENCH }, snapshot); expect(rows(ranked)[0]).toBe('1Disk · archive nodenextover budgetProjected from bench growth: today\'s archive (50.0 TB) already fills the 45.0 TB disk.'); }); + + it('takes storage growth from the run with the most gas per successful transaction, saying why', () => { + const data = withStorage(); + // Mix: 395 Mgas/s ÷ 4,200 tx/s ≈ 94k gas per tx. Swaps: 395 Mgas/s ÷ 2,000 tx/s ≈ 198k, the heaviest. + data.current.swap = { mgas: 400, atTop: true, run: { ...run(400, 'swap'), tps: 2000, storage: { ...storage(), bytesPerGas: { history: 0.0043, proofs: 0.0045, archive: 0.0043 } } } }; + swr.result = { data }; + const { ranked } = render(); + // (45 TB − 4 TB) ÷ (1e6 × (0.0043 swaps + 0.0008 mainnet state) B/gas × 1 year). + expect(disk(ranked, 'archive node')).toMatch(/^\d+Disk · archive node≤ 255 Mgas\/s/); + expect(ranked).toContain('0.00430 bytes per gas from the swaps run'); + expect(ranked).not.toContain('from the mainnet mix run'); + expect(ranked.match(/Uses the workload with the heaviest transactions, because bench workloads with lighter transactions than mainnet overstate history bytes per gas\. A production measurement will replace this\./g)).toHaveLength(2); + }); + + it('falls back to the only run with storage growth, even a light one', () => { + const data = bench(400, true, { da: { mix: null, payment: { ...run(200, 'payment'), storage: storage() }, swap: null } }); + swr.result = { data }; + expect(render().ranked).toContain('0.00860 bytes per gas from the payments run'); + }); }); describe('guardrails summary and work', () => {