From 742277dbc8e22f55114995942f171bf1e9359a30 Mon Sep 17 00:00:00 2001 From: Francis Li Date: Fri, 30 Oct 2026 19:28:03 -0700 Subject: [PATCH] feat(scaling): categorize limits and scale the ladder to its largest value Split each What breaks first limit into a category (DA, Execution, Disk, RPC) and the limit itself. Show sequencer, validators and proof node as separate execution rows instead of merging them, and split TEE proving (Execution) from RPC under sync (RPC). End the shared ladder and table scale at the largest value shown, so ceilings above the gas limit, such as DA swaps and the full-node disk, draw past its line instead of clipping at it. The gray proven band now stops at the proven mark, and the axis-end label is dropped because a labeled point always ends the bar. --- app/scaling/components/Bottlenecks.tsx | 61 ++++----- app/scaling/components/Ladder.tsx | 4 +- app/scaling/components/Scorecard.tsx | 3 +- app/scaling/components/WorkloadSubsystems.tsx | 6 +- app/scaling/components/scorecard.test.tsx | 124 ++++++++++-------- 5 files changed, 103 insertions(+), 95 deletions(-) diff --git a/app/scaling/components/Bottlenecks.tsx b/app/scaling/components/Bottlenecks.tsx index fb8af1c..7b9529c 100644 --- a/app/scaling/components/Bottlenecks.tsx +++ b/app/scaling/components/Bottlenecks.tsx @@ -36,7 +36,7 @@ const STEP_DOT: Record = { passed: 'bg-bds-green export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): Bar[] { const da = workloads.map((w): Bar => { 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 }; + return { id: `da-${w.payload}`, label: w.label, value, text: `${w.tag === 'estimated' ? '~' : ''}${fmt(value)}`, tag: w.tag, perSecond: txPerSecond }; }); const run = proven?.run; const m = proven?.mgas ?? null; @@ -46,7 +46,7 @@ export function bottleneckBars(workloads: DaWorkload[], proven: Proven | null): 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}` }, + { id: 'execution', label: 'Sequencer', value: m, text: !proven ? 'no proven run' : proven.atTop ? `≥ ${m} · top rate tested` : `${m}` }, !replay ? { id: 'validators', label: 'Validators', value: null, text: 'pending' } : replay.match && replay.blocksPerSecond >= REPLAY_BLOCKS_PER_SECOND @@ -85,17 +85,6 @@ function daText(w: DaWorkload, first: boolean): { evidence: string; help: string }; } -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. // `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[] { @@ -113,23 +102,14 @@ export function rankLimits(bars: Bar[], workloads: DaWorkload[], proven: Proven }); const core = bars.filter((b) => !b.id.startsWith('da-')); 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: `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, - 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(' · '), - })); - const sorted = [...da, ...merged, ...disk].toSorted((a, b) => (a.value ?? Infinity) - (b.value ?? Infinity)); + const execution: Limit[] = 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(' · '), + })); + const sorted = [...da, ...execution, ...disk].toSorted((a, b) => (a.value ?? Infinity) - (b.value ?? Infinity)); const firstDa = sorted.find((l) => l.id.startsWith('da-'))?.id; 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) : {}) })); @@ -175,14 +155,14 @@ export function diskLimits(source: StorageSource | null, { policy, production }: }; if (!source) { const need = 'a bench storage-growth run'; - return [row('disk-full', 'Disk · full node', need), row('disk-archive', 'Disk · archive node', need)]; + return [row('disk-full', 'Full node', need), row('disk-archive', '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 full = row( - 'disk-full', 'Disk · full node', + 'disk-full', 'Full node', !runway ? 'a runway policy' : !state ? 'production state growth' : bytesPerGas.proofs === null ? 'proof storage growth' : null, () => { const historyDays = days(fullNode.historyWindowSeconds); @@ -203,7 +183,7 @@ export function diskLimits(source: StorageSource | null, { policy, production }: // 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 archive = row( - 'disk-archive', 'Disk · archive node', + 'disk-archive', 'Archive node', !runway ? 'a runway policy' : !archiveNow ? 'the production archive size' : null, () => { const perGas = bytesPerGas.archive + (state?.value ?? 0); @@ -271,10 +251,16 @@ 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. +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 }) { - const pct = (v: number) => `${Math.min(100, (v / max) * 100)}%`; + const pct = (v: number) => `${(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 = [ + { category: 'Execution', label: 'TEE proving', evidence: 'Not measured yet.' }, + { category: 'RPC', label: 'RPC under sync', evidence: 'Not measured yet.' }, + ]; return (
What breaks first · Mgas/s @@ -282,7 +268,7 @@ export function Bottlenecks({ limits, max, gasLimit, proven, history, mainnetGas - + @@ -291,6 +277,7 @@ export function Bottlenecks({ limits, max, gasLimit, proven, history, mainnetGas return ( + - + ))} diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx index 548f017..b4c121d 100644 --- a/app/scaling/components/Ladder.tsx +++ b/app/scaling/components/Ladder.tsx @@ -135,7 +135,8 @@ export function Ladder({ ladder, windows, proven, max, limits }: Props) { How close are we, and to what? · Mgas/s
0 - {n(max)} Mgas/s
{compact.length ? (

diff --git a/app/scaling/components/Scorecard.tsx b/app/scaling/components/Scorecard.tsx index 3d1bf18..ac017ee 100644 --- a/app/scaling/components/Scorecard.tsx +++ b/app/scaling/components/Scorecard.tsx @@ -44,7 +44,8 @@ export function Scorecard({ snapshot, benchUrl }: { snapshot: Snapshot; benchUrl const workloads = daWorkloads(data?.da, SCALING_WORKLOADS); const limits = rankLimits(bottleneckBars(workloads, proven), workloads, proven, diskLimits(pickStorage(data), snapshot), mainnetGasPerTx(snapshot)); 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); + // The scale ends at the largest value shown, so no breaking point or rung runs off the bar. + const max = Math.max(0, mark?.value ?? 0, ...values, ...limits.map((l) => l.value ?? 0)); return ( <> diff --git a/app/scaling/components/WorkloadSubsystems.tsx b/app/scaling/components/WorkloadSubsystems.tsx index 56bf746..0187abd 100644 --- a/app/scaling/components/WorkloadSubsystems.tsx +++ b/app/scaling/components/WorkloadSubsystems.tsx @@ -194,9 +194,9 @@ 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', + 'proof-node': (id) => id === 'proof-node', + validators: (id) => id === 'validators', + execution: (id) => id === 'execution', }; // Cards follow the ranked table: a category's first ranked row sets its place; unranked ones keep the default order. diff --git a/app/scaling/components/scorecard.test.tsx b/app/scaling/components/scorecard.test.tsx index 260247d..b1b25fa 100644 --- a/app/scaling/components/scorecard.test.tsx +++ b/app/scaling/components/scorecard.test.tsx @@ -249,15 +249,15 @@ 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; 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']); + // 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() }; 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', + 'Validators · all keep up', + 'Proof node · within budget (lag ≤ 10, ≤ 1 s)', 'Disk · full node · 2-year runway', ]); }); @@ -363,14 +363,17 @@ describe('ladder', () => { expect(ladder).not.toMatch(/data-(tick|label)="disk-/); }); - it('pins a breaking point beyond the scale to the right edge without a tick', () => { + it('ends the scale at the largest value, so every breaking point gets a tick and the gray band stops at proven', () => { 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/); + // The ~553 full-node ceiling is the largest value: it ends the bar, with a tick and no off-scale arrow. + expect(ladder).toContain('data-tick="disk-full" class="absolute w-0 border-l-2 border-bds-purple-60" style="left:960px'); + expect(text(ladder.split('data-label="disk-full"')[1].split('data-label=')[0])).toMatch(/^[^→]*≤ 553[^→]*full-node disk/); + expect(text(ladder)).not.toContain('553.2'); + expect(ladder).toMatch(/data-band="proven" width="72\.\d+%"/); }); it('keeps labels apart in a crowded set, dropping names to a legend before stacking tiers', () => { @@ -410,23 +413,27 @@ 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: 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', - '—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.', + '1DAPayments (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.', + '2DASwaps~115 Mgas/s≈ 799 tx/sEstimatedEstimated: no bench DA run yet, so this uses the formula. Target is 14.', + '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', + '—DiskFull node—needs a bench storage-growth run', + '—DiskArchive node—needs a bench storage-growth run', + '—ExecutionTEE proving—Not measured yet.', + '—RPCRPC under sync—Not measured yet.', ]); }); - it('merges execution, validators and proof node when they share the proven ceiling, with the run in help', () => { + it('lists sequencer, validators and proof node as separate rows when they share the proven ceiling', () => { swr.result = { data: bench(400, true) }; const { ranked } = render(); - 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(rows(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', + '6ExecutionProof node≥ 400 Mgas/s≥ 2,054 tx/slag p99 4 blocks', + ]); 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/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', + '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', ]); }); @@ -447,7 +454,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(); // 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\.$/); + expect(rows(ranked)[2]).toMatch(/^3DAMix \(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<'); @@ -466,9 +473,9 @@ describe('what breaks first', () => { // 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('Archive node')).toContain('≤ 138 Mgas/s≤ 710 tx/s'); + expect(row('ExecutionSequencer')).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`); @@ -484,20 +491,20 @@ describe('what breaks first', () => { 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'); + for (const label of ['Mix (bench)', 'Archive node', 'ExecutionSequencer', '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/s≈ 205 tx/sMeasured in the bench: all passed at 40 Mgas/s; the next rate tested did not pass.'); + expect(rows(render().ranked)[0]).toBe('1ExecutionSequencernext40 Mgas/s≈ 205 tx/sthe 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/sMeasured 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('1DAPayments (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.'); }); @@ -505,17 +512,17 @@ describe('what breaks first', () => { 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\.$/); + expect(rows(render().ranked).find((r) => r.includes('DASwaps'))).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\.$/); + expect(rows(render().ranked).find((r) => r.includes('DASwaps'))).toMatch(/Target is 14\.$/); }); 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 · Mix (bench)~190 ', '3DA · Swaps232 Mgas/s≈', '4Execution, validators']); - const swapsRow = ranked.split(' r.includes('>DA · Swaps'))!; + expect(rows(ranked).slice(0, 4).map((r) => r.slice(0, 22))).toEqual(['1DAPayments (B20)next~', '2DAMix (bench)~190 Mga', '3DASwaps232 Mgas/s≈ 5,', '4ExecutionSequencer≥ 4']); + const swapsRow = ranked.split(' r.includes('>Swaps'))!; expect(swapsRow).toMatch(/style="width:58\.\d+%"/); expect(swapsRow).toMatch(/border-dashed[^"]*" style="left:50%"/); }); @@ -538,21 +545,34 @@ describe('disk ceilings', () => { }; const disk = (ranked: string, label: string) => rows(ranked).find((r) => r.includes(label))!; + it('draws a ceiling above the gas limit past its line when proven capacity equals the limit', () => { + // Proven 200 equals the 200 Mgas/s gas limit; the ~553 full-node ceiling sets the scale. + const data = bench(200, true); + data.current.mix = { mgas: 200, atTop: true, run: { ...run(200), storage: storage() } }; + swr.result = { data }; + const full = render().ranked.split(' r.includes('>Full node'))!; + expect(full).toMatch(/style="width:100%"/); + expect(full).toMatch(/border-dashed[^"]*" style="left:36\.\d+%"/); + }); + 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 Mgas/s≈ 79', - '3Disk · archive node≤ 138 ', - '4DA · Mix (bench)~190 Mgas', - '5Execution, validators, pr', - '6Disk · full node≤ 553 Mga', - '—TEE proving, RPC under sy', + '1DAPayments (B20)next~57 M', + '2DASwaps~115 Mgas/s≈ 799 t', + '3DiskArchive node≤ 138 Mga', + '4DAMix (bench)~190 Mgas/s≈', + '5ExecutionSequencer≥ 400 M', + '6ExecutionValidators≥ 400 ', + '7ExecutionProof node≥ 400 ', + '8DiskFull node≤ 553 Mgas/s', + '—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('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.'); + 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.'); }); it('pluralizes the runway', () => { @@ -560,8 +580,8 @@ describe('disk ceilings', () => { 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'); + 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', () => { @@ -579,7 +599,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\/s≤ 776 tx\/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)'); }); @@ -587,16 +607,16 @@ describe('disk ceilings', () => { 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'); + expect(disk(render({ benchUrl: BENCH }, noPolicy).ranked, 'Archive node')).toBe('—DiskArchive node—needs a runway policy'); + expect(disk(render({ benchUrl: BENCH }, noPolicy).ranked, 'Full node')).toBe('—DiskFull 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'); + expect(disk(render({ benchUrl: BENCH }, noProduction).ranked, 'Archive node')).toBe('—DiskArchive node—needs the production archive size'); + expect(disk(render({ benchUrl: BENCH }, noProduction).ranked, 'Full node')).toBe('—DiskFull 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 }; - expect(disk(render().ranked, 'full node')).toBe('—Disk · full node—needs proof storage growth'); + expect(disk(render().ranked, 'Full node')).toBe('—DiskFull node—needs proof storage growth'); }); it('ranks an archive already over budget first', () => { @@ -604,7 +624,7 @@ describe('disk ceilings', () => { const snapshot = fixture(); snapshot.production!.archiveBytes!.value = 50e12; 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.'); + 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', () => { @@ -614,7 +634,7 @@ describe('disk ceilings', () => { 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(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);

#LimitCeiling {ceilingHelp(mainnetGasPerTx)}Evidence#CategoryLimitCeiling {ceilingHelp(mainnetGasPerTx)}Evidence
{i < ranked ? i + 1 : '—'}{category(l.id)} {l.label} {l.help ? <> {l.help} : null} @@ -315,7 +302,7 @@ export function Bottlenecks({ limits, max, gasLimit, proven, history, mainnetGas })} {fixed.map((f) => (
—{f.label}—{f.evidence}—{f.category}{f.label}—{f.evidence}