From 3afcb2708ac046e377d50e70b52180d36ee3a4db Mon Sep 17 00:00:00 2001 From: Francis Li Date: Sat, 31 Oct 2026 07:33:33 -0700 Subject: [PATCH 1/3] feat(scaling): show proof history and witness generation as separate limits --- app/scaling/components/Bottlenecks.tsx | 71 ++++++++--- app/scaling/components/Guardrails.tsx | 18 +-- app/scaling/components/Ladder.tsx | 8 +- app/scaling/components/Scorecard.tsx | 2 +- app/scaling/components/WorkloadSubsystems.tsx | 114 +++++++++++------- app/scaling/components/firstScreen.test.tsx | 27 +++-- app/scaling/components/scorecard.test.tsx | 100 ++++++++++----- app/scaling/page.test.tsx | 4 +- app/scaling/proven/capacity.ts | 6 + 9 files changed, 236 insertions(+), 114 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index 7b9529c..4fafb4d 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -2,15 +2,15 @@ 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, 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 { proofLimits, runHref, successfulTps, type PipelineStep, type Proven, type ProvenCapacity, type Run, type Storage, type Workload } from '../proven/capacity'; +import { 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 Bar = { id: string; label: string; value: number | null; text: string; tag?: DaWorkload['tag']; perSecond?: number; help?: string }; +// One row of the ranked table. export type Limit = { id: string; label: string; @@ -31,8 +31,41 @@ const TH = 'px-3 py-2 text-left text-[11px] font-[600] uppercase tracking-wide t 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. +export const OLDER_RUN = 'the run predates per-limit proof-node budgets'; + +// Proof history and witness generation scale the proven rate by the run's own budgets. Older runs +// report no budgets, so their limits stay unranked. +function proofBars(proven: Proven | null): Bar[] { + const run = proven?.run; + const m = proven?.mgas ?? 0; + const limits = run ? proofLimits(run) : null; + const proofs = run?.proofNode?.proofs; + const execute = proofs?.methods.debug_executePayload?.latency.p99Ms; + if (!limits) { + const older = (what: string) => `Measured in the bench: ${what} at ${m} Mgas/s. Budget unknown: ${OLDER_RUN}.`; + return [ + { id: 'proof-history', label: 'Proof history', value: null, text: proofs ? older(`lag p99 ${proofs.headLagBlocks.p99} blocks`) : 'pending' }, + { id: 'witness-generation', label: 'Witness generation', value: null, text: execute !== undefined ? older(`executePayload p99 ${Math.round(execute)} ms`) : 'pending' }, + ]; + } + const { proofHistory: h, witnessGeneration: w } = limits; + const history = m * h.headroom; + const witness = (m * w.maxP99Ms) / w.p99Ms; + return [ + { + id: 'proof-history', label: 'Proof history', value: history, + text: `≥ ${fmt(history)} · Measured in the bench: at ${m} Mgas/s, proof history runs at ${times(h.headroom)} real time. Budget is ≥ ${times(h.minHeadroom)}.`, + help: `Run ${run!.outputDir}: ${h.throughputBlocksPerSecond.toFixed(1)} blocks/s against ${h.realtimeBlocksPerSecond} in real time. Ceiling: ${m} Mgas/s × ${h.headroom.toFixed(2)}. A lower bound, because the bench estimates throughput from the largest head lag, which overstates the final lag.`, + }, + { + id: 'witness-generation', label: 'Witness generation', value: witness, tag: 'estimated', + text: `~${fmt(witness)} · Estimated (assumes linear scaling): at ${m} Mgas/s, executePayload p99 is ${Math.round(w.p99Ms)} ms. Budget is ${w.maxP99Ms} ms.${w.errors ? ` Failed calls: ${w.errors}.` : ''}`, + help: `Run ${run!.outputDir}: debug_executePayload p99 ${Math.round(w.p99Ms)} ms, p50 ${Math.round(w.p50Ms)} ms. Ceiling: ${m} Mgas/s × ${w.maxP99Ms} ms ÷ ${Math.round(w.p99Ms)} ms, assuming p99 grows linearly with gas per block.`, + }, + ]; +} + +// Validators within budget at the proven rate have a limit of at least that rate; over budget, it is somewhere below it. export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): Bar[] { const da = workloads.map((w): Bar => { const { mgasPerSecond: value, txPerSecond } = daCapacity(w, BLOB_TARGET); @@ -40,10 +73,7 @@ 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: 'Sequencer', value: m, text: !proven ? 'no proven run' : proven.atTop ? `≥ ${m} · top rate tested` : `${m}` }, @@ -52,9 +82,7 @@ export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): : replay.match && replay.blocksPerSecond >= REPLAY_BLOCKS_PER_SECOND ? { 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' } - : { id: 'proof-node', label: 'Proof node', value: proofOk ? m : null, text: `${proofOk ? `≥ ${m}` : `over budget at ${m}`} · lag p99 ${proofs.headLagBlocks.p99} blocks` }, + ...proofBars(proven), ]; } @@ -106,7 +134,9 @@ export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven id: b.id, label: b.label, value: b.value, - ceiling: b.value === null ? '—' : atLeast(b.id !== 'execution' || proven!.atTop), + tag: b.tag, + help: b.help, + ceiling: b.value === null ? '—' : b.id === 'execution' ? atLeast(proven!.atTop) : b.text.split(' · ')[0], evidence: b.value === null ? b.text : b.id === 'execution' && !proven!.atTop ? 'the next rate tested did not pass' : b.text.split(' · ').slice(1).join(' · '), })); const sorted = [...da, ...execution, ...disk].toSorted((a, b) => (a.value ?? Infinity) - (b.value ?? Infinity)); @@ -120,19 +150,21 @@ 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' }; -// 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. +// Storage data from a swaps run, else 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 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]; + const swap = (r: Run) => (r.workload === 'swap' ? 1 : 0); + const run = runs.toSorted((a, b) => swap(b) - swap(a) || 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 SWAPS = 'Uses the swaps run, because the mix’s storage stand-ins overwrite the same slots and overstate history bytes per gas until they are tuned.'; const days = (seconds: number) => Math.round(seconds / 86_400); const years = (n: number) => (n === 1 ? 'one year' : `${n} years`); @@ -159,6 +191,7 @@ export function diskLimits(source: StorageSource | null, { policy, production }: } const { storage: { diskBudgetBytes: budget, bytesPerGas, current, fullNode }, run } = source; const from = `the ${WORKLOAD[run.workload]} run`; + const why = run.workload === 'swap' ? SWAPS : HEAVIEST; const budgetText = `${bytes(budget)} disk budget (a bench config assumption)`; const full = row( @@ -174,7 +207,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. ${HEAVIEST}`, + 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. ${why}`, }; }, ); @@ -195,7 +228,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. ${HEAVIEST}`, + 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. ${why}`, }; }, ); @@ -251,7 +284,7 @@ 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.`; -// Validators and the proof node count as execution, alongside the sequencer. +// Validators, proof history and witness generation count as execution, alongside the sequencer. const category = (id: string) => (id.startsWith('da-') ? 'DA' : id.startsWith('disk-') ? 'Disk' : 'Execution'); 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 }) { diff --git a/app/scaling/components/Guardrails.tsx b/app/scaling/components/Guardrails.tsx index 6001bf0..3b9f9d3 100644 --- a/app/scaling/components/Guardrails.tsx +++ b/app/scaling/components/Guardrails.tsx @@ -5,7 +5,7 @@ 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 { proofLimits, type Run } from '../proven/capacity'; import type { Guardrail } from '../snapshot.mjs'; import { useProvenCapacity, usageAsOf } from './ProvenCapacity'; import { Tag } from './Tag'; @@ -19,9 +19,6 @@ 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, @@ -42,16 +39,19 @@ export function scorecardGuardrails(snapshot: Guardrail[], run: Run | null | und 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 }; + // Budgets come from the run; an older run has none, so its proof rows stay unknown. + const limits = run ? proofLimits(run) : null; + const budget = limits ? { thresholdTag: 'set' as const } : {}; return [ ...rows, judge({ - ...bench, id: 'proof-node-head-lag', label: 'Proof-node head lag p99', value: run?.proofNode?.proofs?.headLagBlocks.p99 ?? null, unit: 'blocks', - 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}`, + ...bench, ...budget, id: 'proof-history-headroom', label: 'Proof history headroom', value: limits?.proofHistory.headroom ?? null, unit: '×', + comparator: '>=', threshold: limits?.proofHistory.minHeadroom ?? null, + help: `Proof history throughput ÷ the chain's real-time block rate 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', - threshold: EXECUTE_PAYLOAD_P99_MS, thresholdTag: 'set', + ...bench, ...budget, id: 'proof-node-execute-payload', label: 'debug_executePayload p99', value: run?.proofNode?.proofs?.methods.debug_executePayload?.latency.p99Ms ?? null, unit: 'ms', + threshold: limits?.witnessGeneration.maxP99Ms ?? null, help: `Latency of the witness call the prover makes for each block, on the proof node.${source}`, }), judge({ diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx index b4c121d..0033179 100644 --- a/app/scaling/components/Ladder.tsx +++ b/app/scaling/components/Ladder.tsx @@ -17,7 +17,7 @@ const n = (v: number) => v.toLocaleString('en-US', { maximumFractionDigits: 1 }) export type ProvenMark = { value: number; atLeast: boolean; help: string } | null; -type Tone = 'today' | 'p99' | 'fee' | 'da' | 'disk' | 'gas' | 'proven'; +type Tone = 'today' | 'p99' | 'fee' | 'da' | 'disk' | 'execution' | '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 }; @@ -27,6 +27,7 @@ const TEXT: Record = { fee: 'text-bds-gray-60', da: 'text-bds-red-60', disk: 'text-bds-purple-60', + execution: 'text-bds-green-60', gas: 'text-bds-orange-60', proven: 'text-base-blue', }; @@ -36,6 +37,7 @@ const TICK: Record = { fee: 'border-dotted border-bds-gray-60', da: 'border-bds-red-60', disk: 'border-bds-purple-60', + execution: 'border-bds-green-60', gas: 'border-dashed border-bds-orange-60', proven: 'border-base-blue', }; @@ -45,7 +47,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 (bench)', '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', 'proof-history': 'proof history', 'witness-generation': 'witness generation' }; // Approximate label widths: a bold 12px value with its help icon over an 11px name. const widthOf = (p: Point, offScale: boolean, compact: boolean) => @@ -113,7 +115,7 @@ export function Ladder({ ladder, windows, proven, max, limits }: Props) { value: l.value!, valueText: l.ceiling, name: SHORT[l.id], - tone: l.id.startsWith('disk-') ? 'disk' : 'da', + tone: l.id.startsWith('disk-') ? 'disk' : l.id.startsWith('da-') ? 'da' : 'execution', help: l.help ?? l.evidence, })), ]; diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index ac017ee..847898b 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -13,7 +13,7 @@ import { GlamsterdamComparisonSection } from './GlamsterdamComparison'; 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.`; + 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 is recorded, not gated.`; return { value: mgas, atLeast: atTop, help }; } diff --git a/app/scaling/components/WorkloadSubsystems.tsx b/app/scaling/components/WorkloadSubsystems.tsx index 0187abd..0faa20e 100644 --- a/app/scaling/components/WorkloadSubsystems.tsx +++ b/app/scaling/components/WorkloadSubsystems.tsx @@ -2,10 +2,10 @@ import { Card } from '../../components/ui/Card'; import { cn } from '../../components/ui/cn'; import { Text } from '../../components/ui/Text'; 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 { proofLimits, 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 { bytes, diskLimits, OLDER_RUN, times, type Limit } from './Bottlenecks'; +import { 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)']]; @@ -17,13 +17,12 @@ 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 Category = 'da' | 'disk-archive' | 'disk-full' | 'proof-history' | 'witness-generation' | '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))]; @@ -99,46 +98,78 @@ function diskCards(runs: Record, snapshot: Pick): SubsystemCard { +// One card per proof-node limit, judged by each run's own budget; older runs report none. +function proofCards(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 names = (keep: (r: Run) => boolean) => measured.filter(([w]) => keep(runs[w]!)).map(([w]) => NAME[w]); + const older = names((r) => !proofLimits(r)); + const takeaway = (failed: string[], verdict: string) => { + const parts = [failed.length && `${verdict} for ${and(failed)}`, older.length && `budget unknown for ${and(older)}`].filter((s): s is string => !!s); + return !measured.length ? 'not measured yet' : parts.join('; ') || 'within budget'; + }; 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 })), - }, - ], - }; + const limitCells = (value: (l: NonNullable>, r: Run) => Cell) => proofCells((_, r) => { + const l = proofLimits(r); + return l ? value(l, r) : { why: OLDER_RUN }; + }); + const slow = names((r) => proofLimits(r)?.proofHistory.pass === false); + const over = names((r) => proofLimits(r)?.witnessGeneration.pass === false); + return [ + { + id: 'proof-history', + title: 'Proof history', + takeaway: takeaway(slow, 'below budget'), + bad: slow.length > 0, + rows: [ + { + label: 'Headroom (budget)', + help: 'Proof history throughput ÷ the chain’s real-time block rate, and the minimum the bench requires.', + cells: limitCells(({ proofHistory: h }) => ({ value: `${times(h.headroom)} (≥ ${times(h.minHeadroom)})`, bad: !h.pass })), + }, + { + label: 'Ceiling (Mgas/s)', + help: 'The run’s rate × headroom. A lower bound, because the bench underestimates throughput.', + cells: limitCells(({ proofHistory: h }, r) => ({ value: `≥ ${fmt(r.mgas * h.headroom)}` })), + }, + { + label: 'Lag p99 (max)', + help: 'Blocks proof history trails the inserted head, p99 and max over the run. Recorded only: the unpaced replay runs ahead of real time.', + cells: proofCells(({ headLagBlocks: l }) => ({ value: `${l.p99} (${l.max})` })), + }, + ], + }, + { + id: 'witness-generation', + title: 'Witness generation', + takeaway: takeaway(over, 'over budget'), + bad: over.length > 0, + rows: [ + { + label: 'executePayload p99 (budget)', + help: 'p99 latency of debug_executePayload on the proof node, and the budget the bench applies.', + cells: proofCells((p, r) => { + const w = proofLimits(r)?.witnessGeneration; + const p99 = p.methods.debug_executePayload?.latency.p99Ms; + return w ? { value: `${ms(w.p99Ms)} (≤ ${ms(w.maxP99Ms)})`, bad: !w.pass } : p99 === undefined ? { why: NOT_IN_RUN } : { value: ms(p99), sub: ['budget unknown'] }; + }), + }, + { + label: 'Estimated ceiling (Mgas/s)', + help: 'The run’s rate × budget ÷ p99, assuming p99 grows linearly with gas per block.', + cells: limitCells(({ witnessGeneration: w }, r) => ({ value: `~${fmt((r.mgas * w.maxP99Ms) / w.p99Ms)}` })), + }, + { + 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 { @@ -194,7 +225,8 @@ 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', + 'proof-history': (id) => id === 'proof-history', + 'witness-generation': (id) => id === 'witness-generation', validators: (id) => id === 'validators', execution: (id) => id === 'execution', }; @@ -203,7 +235,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), ...diskCards(runs, snapshot), proofCard(runs), validatorCard(runs), sequencerCard(runs)]; + const cards = [daCard(da, gasLimit), ...diskCards(runs, snapshot), ...proofCards(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/firstScreen.test.tsx b/app/scaling/components/firstScreen.test.tsx index 419b518..e3f45b1 100644 --- a/app/scaling/components/firstScreen.test.tsx +++ b/app/scaling/components/firstScreen.test.tsx @@ -66,13 +66,18 @@ describe('Guardrails', () => { describe('scorecardGuardrails', () => { const LATENCY = { p50Ms: 2, p99Ms: 9, maxMs: 12 }; - const run = (blocksPerSecond: number, lagP99 = 4, executeP99 = 9): Run => ({ + const limits = (headroom: number, p99Ms: number) => ({ + proofHistory: { pass: headroom >= 2, throughputBlocksPerSecond: headroom * 5, realtimeBlocksPerSecond: 5, headroom, minHeadroom: 2 }, + witnessGeneration: { pass: p99Ms <= 3000, p99Ms, p50Ms: 2, maxP99Ms: 3000, errors: 0 }, + }); + const run = (blocksPerSecond: number, headroom: number | null = 3, executeP99 = 9): Run => ({ 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, + createdAt: Date.parse('2026-01-05T00:00:00Z'), da: null, + steps: [{ name: 'proof_node', enabled: true, status: 'passed', outputs: headroom === null ? {} : { limits: limits(headroom, executeP99) } }], replay: { blocksPerSecond, newPayload: LATENCY, match: true }, proofNode: { blocksPerSecond: 20, newPayload: LATENCY, match: true, - 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 } }, + proofs: { headLagBlocks: { samples: 10, p50: 1, p99: 4, 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])); @@ -86,15 +91,19 @@ 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 ≤ 10 blocks, executePayload ≤ 1000 ms, replay headroom against 5 blocks/s', () => { + it('adds bench rows only with a bench host: proof history headroom and executePayload against the run’s budgets, 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: 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(rows['proof-history-headroom']).toMatchObject({ value: 3, threshold: 2, comparator: '>=', thresholdTag: 'set', status: 'ok', asOf: '2026-01-05T00:00:00.000Z' }); + expect(rows['proof-history-headroom'].help).toContain('From bench run synthetic-400.'); + expect(rows['proof-node-execute-payload']).toMatchObject({ value: 9, unit: 'ms', threshold: 3000, thresholdTag: 'set', status: 'ok' }); + const over = byId(scorecardGuardrails(fixture().guardrails, run(36.4, 1.5, 3001))); + expect(over['proof-history-headroom'].status).toBe('breached'); expect(over['proof-node-execute-payload'].status).toBe('breached'); + // A run without budgets is not judged against any. + const older = byId(scorecardGuardrails(fixture().guardrails, run(36.4, null, 1001))); + expect(older['proof-history-headroom']).toMatchObject({ value: null, threshold: null, thresholdTag: 'unset', status: 'unknown' }); + expect(older['proof-node-execute-payload']).toMatchObject({ value: 1001, threshold: null, thresholdTag: 'unset', 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'); }); diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index b1b25fa..f9fc317 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -41,6 +41,12 @@ const run = (mgas: number, workload: Run['workload'] = 'mix', da: Run['da'] = nu }, }); const none = { mix: null, payment: null, swap: null }; +// The proof_node step's per-limit judgement; `run` leaves it out, like runs from before the bench reported it. +const LIMITS = { + proofHistory: { pass: true, throughputBlocksPerSecond: 15, realtimeBlocksPerSecond: 5, headroom: 3, minHeadroom: 2 }, + witnessGeneration: { pass: true, p99Ms: 900, p50Ms: 250, maxP99Ms: 3000, errors: 0 }, +}; +const withLimits = (r: Run, limits: typeof LIMITS = LIMITS): Run => ({ ...r, steps: r.steps.map((s) => (s.name === 'proof_node' ? { ...s, outputs: { limits } } : s)) }); const storage = (currentBytes: number | null = null): NonNullable => ({ diskBudgetBytes: 45e12, bytesPerGas: { history: 0.0086, proofs: 0.0045, archive: 0.0086 }, @@ -249,16 +255,19 @@ describe('subsystems by workload', () => { 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; sequencer, validators and proof node tie at 400 in table order; no storage leaves both disks unranked. - expect(titles(render().subsystems).map((t) => t.split(' · ')[0])).toEqual(['DA', 'Sequencer', 'Validators', 'Proof node', 'Disk', 'Disk']); - swr.result = { data: withStorage() }; + // DA ~57 first; sequencer and validators tie at 400 in table order; the older run leaves the proof limits unranked, no storage both disks. + expect(titles(render().subsystems).map((t) => t.split(' · ')[0])).toEqual(['DA', 'Sequencer', 'Validators', 'Disk', 'Disk', 'Proof history', 'Witness generation']); + const data = withStorage(); + data.current.mix!.run = withLimits(data.current.mix!.run); + swr.result = { data }; expect(titles(render().subsystems)).toEqual([ 'DA · not measured yet', 'Disk · archive · 1-year runway', 'Sequencer · 400 delivered', 'Validators · all keep up', - 'Proof node · within budget (lag ≤ 10, ≤ 1 s)', 'Disk · full node · 2-year runway', + 'Proof history · within budget', + 'Witness generation · within budget', ]); }); @@ -298,19 +307,29 @@ describe('subsystems by workload', () => { expect(html).toContain('title="needs a run with storage measurement"'); }); - it('flags proof-node budgets and validators that fall behind', () => { + it('judges proof history and witness generation by each run’s budgets, unknown for older runs, and flags 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; + const slow = withLimits(run(400, 'mix', null, 11), { + proofHistory: { ...LIMITS.proofHistory, pass: false, headroom: 1.5 }, + witnessGeneration: { ...LIMITS.witnessGeneration, pass: false, p99Ms: 4000 }, + }); 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([ + expect(text(card(html, 'Proof history').split('')[0])).toBe('Proof history · below budget for mix; budget unknown for swaps'); + expect(card(html, 'Proof history').split('')[0]).toContain('text-bds-red-60'); + // Mix: 400 × 1.5 = 600, and 400 × 3,000 ms ÷ 4,000 ms = 300. + expect(rows(card(html, 'Proof history'))).toEqual([ + 'Headroom (budget) — (no proven run for this workload)— (the run predates per-limit proof-node budgets)1.5× (≥ 2.0×)', + 'Ceiling (Mgas/s) — (no proven run for this workload)— (the run predates per-limit proof-node budgets)≥ 600', 'Lag p99 (max) — (no proven run for this workload)4 (20)11 (20)', - 'executePayload p99 — (no proven run for this workload)733 ms1200 ms', + ]); + expect(text(card(html, 'Witness generation').split('')[0])).toBe('Witness generation · over budget for mix; budget unknown for swaps'); + expect(rows(card(html, 'Witness generation'))).toEqual([ + 'executePayload p99 (budget) — (no proven run for this workload)733 msbudget unknown4000 ms (≤ 3000 ms)', + 'Estimated ceiling (Mgas/s) — (no proven run for this workload)— (the run predates per-limit proof-node budgets)~300', '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'); @@ -418,7 +437,8 @@ describe('what breaks first', () => { '3DAMix (bench)~190 Mgas/s≈ 974 tx/sEstimatedEstimated: no bench DA run yet, so this uses the formula. Target is 14.', '—ExecutionSequencer—no proven run', '—ExecutionValidators—pending', - '—ExecutionProof node—pending', + '—ExecutionProof history—pending', + '—ExecutionWitness generation—pending', '—DiskFull node—needs a bench storage-growth run', '—DiskArchive node—needs a bench storage-growth run', '—ExecutionTEE proving—Not measured yet.', @@ -426,27 +446,34 @@ describe('what breaks first', () => { ]); }); - it('lists sequencer, validators and proof node as separate rows when they share the proven ceiling', () => { + it('leaves proof history and witness generation unranked for a run without budgets, applying none', () => { swr.result = { data: bench(400, true) }; const { ranked } = render(); - expect(rows(ranked).slice(3, 6)).toEqual([ + expect(rows(ranked).slice(3, 7)).toEqual([ '4ExecutionSequencer≥ 400 Mgas/s≥ 2,054 tx/stop rate tested', '5ExecutionValidators≥ 400 Mgas/s≥ 2,054 tx/s7.3× replay headroom', - '6ExecutionProof node≥ 400 Mgas/s≥ 2,054 tx/slag p99 4 blocks', + '—ExecutionProof history—Measured in the bench: lag p99 4 blocks at 400 Mgas/s. Budget unknown: the run predates per-limit proof-node budgets.', + '—ExecutionWitness generation—Measured in the bench: executePayload p99 733 ms at 400 Mgas/s. Budget unknown: the run predates per-limit proof-node budgets.', ]); expect(ranked.match(/>next { - const over = bench(400, true); - 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([ - '4ExecutionSequencer≥ 400 Mgas/s≥ 2,054 tx/stop rate tested', - '5ExecutionValidators≥ 400 Mgas/s≥ 2,054 tx/s7.3× replay headroom', - '—ExecutionProof node—over budget at 400 · lag p99 11 blocks', + it('ranks proof history at rate × headroom and witness generation at rate × budget ÷ p99, from the run’s outputs', () => { + const data = bench(400, true); + data.current.mix!.run = withLimits(data.current.mix!.run, { ...LIMITS, witnessGeneration: { ...LIMITS.witnessGeneration, p99Ms: 2000 } }); + swr.result = { data }; + const { ladder, ranked } = render(); + // 400 × 3 = 1,200; 400 × 3,000 ms ÷ 2,000 ms = 600. + expect(rows(ranked).slice(5, 7)).toEqual([ + '6ExecutionWitness generation~600 Mgas/s≈ 3,082 tx/sEstimatedEstimated (assumes linear scaling): at 400 Mgas/s, executePayload p99 is 2000 ms. Budget is 3000 ms.', + '7ExecutionProof history≥ 1,200 Mgas/s≥ 6,163 tx/sMeasured in the bench: at 400 Mgas/s, proof history runs at 3.0× real time. Budget is ≥ 2.0×.', ]); + expect(ranked).toContain('Ceiling: 400 Mgas/s × 3.00. A lower bound'); + expect(ranked).toContain('Ceiling: 400 Mgas/s × 3000 ms ÷ 2000 ms, assuming p99 grows linearly with gas per block.'); + // 1,200 is the largest value, so it ends the ladder. + expect(ladder).toContain('data-tick="proof-history" class="absolute w-0 border-l-2 border-bds-green-60" style="left:960px'); + expect(ladder).toMatch(/data-tick="witness-generation" class="[^"]*border-bds-green-60" style="left:480px/); }); it('ranks the mix by the bench mix run’s DA everywhere on the page, with the bench evidence', () => { @@ -565,14 +592,15 @@ describe('disk ceilings', () => { '4DAMix (bench)~190 Mgas/s≈', '5ExecutionSequencer≥ 400 M', '6ExecutionValidators≥ 400 ', - '7ExecutionProof node≥ 400 ', - '8DiskFull node≤ 553 Mgas/s', + '7DiskFull node≤ 553 Mgas/s', + '—ExecutionProof history—Me', + '—ExecutionWitness generati', '—ExecutionTEE proving—Not ', '—RPCRPC under sync—Not mea', ]); // (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('3DiskArchive 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('8DiskFull 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.'); + expect(disk(ranked, 'Full node')).toBe('7DiskFull 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', () => { @@ -627,17 +655,27 @@ describe('disk ceilings', () => { expect(rows(ranked)[0]).toBe('1DiskArchive 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', () => { + it('takes storage growth from a swaps run, even when another run has heavier transactions, 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 } } } }; + // Mix: 395 Mgas/s ÷ 1,000 tx/s ≈ 395k gas per tx, heavier than swaps at 395 Mgas/s ÷ 4,200 tx/s ≈ 94k. + data.current.mix!.run.tps = 1000; + data.current.swap = { mgas: 400, atTop: true, run: { ...run(400, 'swap'), 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+DiskArchive 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); + expect(ranked.match(/Uses the swaps run, because the mix’s storage stand-ins overwrite the same slots and overstate history bytes per gas until they are tuned\./g)).toHaveLength(2); + }); + + it('falls back to the heaviest transactions without a swaps storage run', () => { + const data = withStorage({ da: { mix: null, payment: { ...run(200, 'payment'), tps: 4000, storage: storage() }, swap: null } }); + swr.result = { data }; + const { ranked } = render(); + // Mix: 395 Mgas/s ÷ 4,200 tx/s ≈ 94k gas per tx; payments: 195 Mgas/s ÷ 4,000 tx/s ≈ 49k. + expect(ranked).toContain('from the mainnet mix run'); + expect(ranked.match(/Uses the workload with the heaviest transactions/g)).toHaveLength(2); }); it('falls back to the only run with storage growth, even a light one', () => { @@ -656,7 +694,9 @@ describe('guardrails summary and work', () => { 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) }; + const data = bench(400, true); + data.current.mix!.run = withLimits(data.current.mix!.run); + swr.result = { data }; expect(text(guardrails(BENCH))).toContain('Met 5'); }); diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx index 77dc3b1..0c1a17c 100644 --- a/app/scaling/page.test.tsx +++ b/app/scaling/page.test.tsx @@ -105,7 +105,7 @@ 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'); const html = renderToStaticMarkup(await ScalingGuardrailsPage()); - for (const label of ['Proof-node head lag p99', 'debug_executePayload p99', 'Validator replay headroom']) expect(html).toContain(label); + for (const label of ['Proof history headroom', 'debug_executePayload p99', 'Validator replay headroom']) expect(html).toContain(label); expect(html).toContain('No qualifying mainnet-mix bench run is loaded.'); }); @@ -115,7 +115,7 @@ it('shows guardrail help inline and the outcomes table on the guardrails page', 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'); + expect(html).not.toContain('Proof history headroom'); expect(html).not.toContain('Guardrails →'); expect(html).toContain('Inclusion latency p50'); expect(html).toContain('synthetic.inclusion_latency'); diff --git a/app/scaling/proven/capacity.ts b/app/scaling/proven/capacity.ts index b41fcc3..eb1a0a6 100644 --- a/app/scaling/proven/capacity.ts +++ b/app/scaling/proven/capacity.ts @@ -20,6 +20,12 @@ export type Proofs = { methods: Record; witnessBytes: Counts; }; +// The proof_node step's judgement of each limit; runs from before the bench judged them separately have none. +export type ProofLimits = { + proofHistory: { pass: boolean; throughputBlocksPerSecond: number; realtimeBlocksPerSecond: number; headroom: number; minHeadroom: number }; + witnessGeneration: { pass: boolean; p99Ms: number; p50Ms: number; maxP99Ms: number; errors: number }; +}; +export const proofLimits = (run: Run) => (run.steps.find((s) => s.name === 'proof_node')?.outputs?.limits ?? null) as ProofLimits | null; // `base-bench da` writes snake_case. export type DaResult = { seconds: number; From 9fd005d3fe52c1a1f85b84ee68146b0d2686934e Mon Sep 17 00:00:00 2001 From: Francis Li Date: Sat, 31 Oct 2026 07:35:09 -0700 Subject: [PATCH 2/3] Shorten the executePayload cell so it fits the card --- app/scaling/components/WorkloadSubsystems.tsx | 4 ++-- app/scaling/components/scorecard.test.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scaling/components/WorkloadSubsystems.tsx b/app/scaling/components/WorkloadSubsystems.tsx index 0faa20e..dc36379 100644 --- a/app/scaling/components/WorkloadSubsystems.tsx +++ b/app/scaling/components/WorkloadSubsystems.tsx @@ -149,12 +149,12 @@ function proofCards(runs: Record): SubsystemCard[] { bad: over.length > 0, rows: [ { - label: 'executePayload p99 (budget)', + label: 'executePayload p99, ms (budget)', help: 'p99 latency of debug_executePayload on the proof node, and the budget the bench applies.', cells: proofCells((p, r) => { const w = proofLimits(r)?.witnessGeneration; const p99 = p.methods.debug_executePayload?.latency.p99Ms; - return w ? { value: `${ms(w.p99Ms)} (≤ ${ms(w.maxP99Ms)})`, bad: !w.pass } : p99 === undefined ? { why: NOT_IN_RUN } : { value: ms(p99), sub: ['budget unknown'] }; + return w ? { value: `${Math.round(w.p99Ms)} (≤ ${w.maxP99Ms})`, bad: !w.pass } : p99 === undefined ? { why: NOT_IN_RUN } : { value: `${Math.round(p99)}`, sub: ['budget unknown'] }; }), }, { diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index f9fc317..dce15b8 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -328,7 +328,7 @@ describe('subsystems by workload', () => { ]); expect(text(card(html, 'Witness generation').split('')[0])).toBe('Witness generation · over budget for mix; budget unknown for swaps'); expect(rows(card(html, 'Witness generation'))).toEqual([ - 'executePayload p99 (budget) — (no proven run for this workload)733 msbudget unknown4000 ms (≤ 3000 ms)', + 'executePayload p99, ms (budget) — (no proven run for this workload)733budget unknown4000 (≤ 3000)', 'Estimated ceiling (Mgas/s) — (no proven run for this workload)— (the run predates per-limit proof-node budgets)~300', 'Witness p50 — (no proven run for this workload)— (not in this run)— (not in this run)', ]); From ce5676ef1f94630139923f73ecccfa78da45258d Mon Sep 17 00:00:00 2001 From: Francis Li Date: Sat, 31 Oct 2026 07:41:03 -0700 Subject: [PATCH 3/3] Read proof-node limits even when the proof-node result file is missing --- app/scaling/components/Guardrails.tsx | 2 +- app/scaling/components/WorkloadSubsystems.tsx | 10 +++++----- app/scaling/components/scorecard.test.tsx | 12 ++++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/scaling/components/Guardrails.tsx b/app/scaling/components/Guardrails.tsx index 3b9f9d3..7ae9dac 100644 --- a/app/scaling/components/Guardrails.tsx +++ b/app/scaling/components/Guardrails.tsx @@ -50,7 +50,7 @@ export function scorecardGuardrails(snapshot: Guardrail[], run: Run | null | und help: `Proof history throughput ÷ the chain's real-time block rate while the proof node replays the bench blocks.${source}`, }), judge({ - ...bench, ...budget, id: 'proof-node-execute-payload', label: 'debug_executePayload p99', value: run?.proofNode?.proofs?.methods.debug_executePayload?.latency.p99Ms ?? null, unit: 'ms', + ...bench, ...budget, id: 'proof-node-execute-payload', label: 'debug_executePayload p99', value: limits?.witnessGeneration.p99Ms ?? run?.proofNode?.proofs?.methods.debug_executePayload?.latency.p99Ms ?? null, unit: 'ms', threshold: limits?.witnessGeneration.maxP99Ms ?? null, help: `Latency of the witness call the prover makes for each block, on the proof node.${source}`, }), diff --git a/app/scaling/components/WorkloadSubsystems.tsx b/app/scaling/components/WorkloadSubsystems.tsx index dc36379..3fe653e 100644 --- a/app/scaling/components/WorkloadSubsystems.tsx +++ b/app/scaling/components/WorkloadSubsystems.tsx @@ -101,7 +101,7 @@ function diskCards(runs: Record, snapshot: Pick): SubsystemCard[] { const proofs = (r: Run) => r.proofNode?.proofs ?? null; - const measured = COLUMNS.filter(([w]) => runs[w] && proofs(runs[w])); + const measured = COLUMNS.filter(([w]) => runs[w] && (proofLimits(runs[w]) || proofs(runs[w]))); const names = (keep: (r: Run) => boolean) => measured.filter(([w]) => keep(runs[w]!)).map(([w]) => NAME[w]); const older = names((r) => !proofLimits(r)); const takeaway = (failed: string[], verdict: string) => { @@ -112,9 +112,9 @@ function proofCards(runs: Record): SubsystemCard[] { const p = proofs(r); return p ? value(p, r) : { why: NOT_IN_RUN }; }); - const limitCells = (value: (l: NonNullable>, r: Run) => Cell) => proofCells((_, r) => { + const limitCells = (value: (l: NonNullable>, r: Run) => Cell) => cells(runs, (r) => { const l = proofLimits(r); - return l ? value(l, r) : { why: OLDER_RUN }; + return l ? value(l, r) : { why: proofs(r) ? OLDER_RUN : NOT_IN_RUN }; }); const slow = names((r) => proofLimits(r)?.proofHistory.pass === false); const over = names((r) => proofLimits(r)?.witnessGeneration.pass === false); @@ -151,9 +151,9 @@ function proofCards(runs: Record): SubsystemCard[] { { label: 'executePayload p99, ms (budget)', help: 'p99 latency of debug_executePayload on the proof node, and the budget the bench applies.', - cells: proofCells((p, r) => { + cells: cells(runs, (r) => { const w = proofLimits(r)?.witnessGeneration; - const p99 = p.methods.debug_executePayload?.latency.p99Ms; + const p99 = proofs(r)?.methods.debug_executePayload?.latency.p99Ms; return w ? { value: `${Math.round(w.p99Ms)} (≤ ${w.maxP99Ms})`, bad: !w.pass } : p99 === undefined ? { why: NOT_IN_RUN } : { value: `${Math.round(p99)}`, sub: ['budget unknown'] }; }), }, diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index dce15b8..d3e4dba 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -243,6 +243,8 @@ describe('headroom span', () => { }); }); +const guardrailsHtml = () => renderToStaticMarkup(); + describe('subsystems by workload', () => { const cards = (html: string) => html.split(' ` cards(html).map((c) => text(c.split('')[0])); @@ -333,6 +335,16 @@ describe('subsystems by workload', () => { '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'); + // The limits come from the pipeline outputs, so they show even when the proof-node result file did not load. + data.current.mix = { mgas: 400, atTop: true, run: { ...withLimits(run(400)), proofNode: null } }; + data.current.swap = null; + const outputsOnly = render().subsystems; + expect(text(card(outputsOnly, 'Proof history').split('')[0])).toBe('Proof history · within budget'); + expect(rows(card(outputsOnly, 'Witness generation')).slice(0, 2)).toEqual([ + 'executePayload p99, ms (budget) — (no proven run for this workload)— (no proven run for this workload)900 (≤ 3000)', + 'Estimated ceiling (Mgas/s) — (no proven run for this workload)— (no proven run for this workload)~1,333', + ]); + expect(text(guardrailsHtml())).toContain('Met 5'); 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'); });