From c7a05d8563d6c4de2c241b3233033da4354e4471 Mon Sep 17 00:00:00 2001 From: GiFTED! Date: Wed, 8 Jul 2026 11:06:01 +0100 Subject: [PATCH 1/3] feat(a16): real in-session benchmark capture and compare --- app/src/hooks/useBenchmark.ts | 11 +- .../panels/A16-benchmark/BenchmarkPanel.tsx | 199 ++++++++++++------ app/src/rpc/client.ts | 1 + app/src/rpc/contract.ts | 2 + packages/bridge/handlers.ts | 114 +++++++++- packages/bridge/state.ts | 16 +- packages/mock-bridge/server.mjs | 34 ++- packages/mock-bridge/src/server.ts | 36 +++- 8 files changed, 339 insertions(+), 74 deletions(-) diff --git a/app/src/hooks/useBenchmark.ts b/app/src/hooks/useBenchmark.ts index dca3658..7f1a377 100644 --- a/app/src/hooks/useBenchmark.ts +++ b/app/src/hooks/useBenchmark.ts @@ -1,7 +1,16 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { rpc } from '../rpc/bootstrap'; export const useBenchmarkSnapshots = (workspaceId: string) => useQuery({ queryKey: ['bench', workspaceId], queryFn: () => rpc.bench.listSnapshots(workspaceId) }); + +export function useBenchmarkCapture(workspaceId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (name: string) => rpc.bench.capture(workspaceId, name), + onSuccess: () => qc.invalidateQueries({ queryKey: ['bench', workspaceId] }) + }); +} + export const useBenchmarkCompare = (workspaceId: string) => useMutation({ mutationFn: ({ runId, baselineId }: { runId: string; baselineId: string }) => rpc.bench.compare(workspaceId, runId, baselineId) }); diff --git a/app/src/panels/A16-benchmark/BenchmarkPanel.tsx b/app/src/panels/A16-benchmark/BenchmarkPanel.tsx index f0a86e1..a846c05 100644 --- a/app/src/panels/A16-benchmark/BenchmarkPanel.tsx +++ b/app/src/panels/A16-benchmark/BenchmarkPanel.tsx @@ -1,12 +1,13 @@ import { useEffect, useState } from 'react'; -import { ArrowDown, ArrowUp, GitCompareArrows, Minus } from 'lucide-react'; +import { ArrowDown, ArrowUp, Camera, GitCompareArrows, Minus } from 'lucide-react'; import { Panel } from '../../components/Panel'; -import { useBenchmarkCompare, useBenchmarkSnapshots } from '../../hooks/useBenchmark'; +import { useBenchmarkCapture, useBenchmarkCompare, useBenchmarkSnapshots } from '../../hooks/useBenchmark'; import { usePontoonStore } from '../../state/store'; import { Badge, Button, EmptyState, + Input, SectionLabel, StatCard, Table, @@ -40,6 +41,7 @@ export function BenchmarkPanel() { const workspaceId = usePontoonStore((s) => s.activeWorkspaceId); const query = useBenchmarkSnapshots(workspaceId); const compare = useBenchmarkCompare(workspaceId); + const capture = useBenchmarkCapture(workspaceId); return ( @@ -50,6 +52,9 @@ export function BenchmarkPanel() { comparing={compare.isPending} metrics={compare.data?.metrics} error={compare.error} + onCapture={(name) => capture.mutate(name)} + capturing={capture.isPending} + captureError={capture.error} /> )} @@ -62,9 +67,21 @@ interface BenchmarkBodyProps { comparing: boolean; metrics?: Metric[]; error: unknown; + onCapture: (name: string) => void; + capturing: boolean; + captureError: unknown; } -function BenchmarkBody({ snapshots, onCompare, comparing, metrics, error }: BenchmarkBodyProps) { +function BenchmarkBody({ + snapshots, + onCompare, + comparing, + metrics, + error, + onCapture, + capturing, + captureError +}: BenchmarkBodyProps) { const [runId, setRunId] = useState(''); const [baselineId, setBaselineId] = useState(''); @@ -78,73 +95,129 @@ function BenchmarkBody({ snapshots, onCompare, comparing, metrics, error }: Benc const regressions = metrics?.filter((m) => m.regression).length ?? 0; const canCompare = Boolean(runId && baselineId && runId !== baselineId); - if (snapshots.length === 0) { - return ( - + - ); - } + + {snapshots.length === 0 ? ( + + ) : ( + <> +
+ +
+ +
+ +
+ +
+
+ + {error ? ( +
+ {error instanceof Error ? error.message : String(error)} +
+ ) : metrics ? ( + metrics.length === 0 ? ( + + ) : ( +
+
+ + + {regressions} + {regressions > 0 ? ( + attention + ) : ( + clean + )} + + } + hint="Metrics flagged worse than baseline." + /> + m.deltaPct < 0 && !m.regression).length} + hint="Metrics moved in a good direction." + /> +
+ +
+ ) + ) : ( + + )} + + )} + + ); +} + +function CaptureBar({ + onCapture, + capturing, + error, + count +}: { + onCapture: (name: string) => void; + capturing: boolean; + error: unknown; + count: number; +}) { + const [name, setName] = useState(''); + const submit = () => { + onCapture(name.trim() || `Run ${count + 1}`); + setName(''); + }; return ( -
-
- -
- -
- -
- -
+
+
+ +
- +

+ Snapshots total fees, compute gas, and message-tree metrics from the transactions executed so far. +

{error ? ( -
+
{error instanceof Error ? error.message : String(error)}
- ) : metrics ? ( - metrics.length === 0 ? ( - - ) : ( -
-
- - - {regressions} - {regressions > 0 ? ( - attention - ) : ( - clean - )} - - } - hint="Metrics flagged worse than baseline." - /> - m.deltaPct < 0 && !m.regression).length} - hint="Metrics moved in a good direction." - /> -
- -
- ) - ) : ( - - )} + ) : null}
); } diff --git a/app/src/rpc/client.ts b/app/src/rpc/client.ts index 0da2350..f7133b8 100644 --- a/app/src/rpc/client.ts +++ b/app/src/rpc/client.ts @@ -113,6 +113,7 @@ export class RpcClient { merge: (workspaceId: string, runs: string[]) => this.call(METHODS.coverage.merge, { workspaceId, runs }) }; bench = { + capture: (workspaceId: string, name: string) => this.call(METHODS.bench.capture, { workspaceId, name }), listSnapshots: (workspaceId: string, dir?: string) => this.call(METHODS.bench.listSnapshots, { workspaceId, dir }), compare: (workspaceId: string, runId: string, baselineId: string) => this.call(METHODS.bench.compare, { workspaceId, runId, baselineId }) }; diff --git a/app/src/rpc/contract.ts b/app/src/rpc/contract.ts index e1315f9..d0766a7 100644 --- a/app/src/rpc/contract.ts +++ b/app/src/rpc/contract.ts @@ -66,6 +66,7 @@ export const METHODS = { merge: 'coverage.merge' }, bench: { + capture: 'bench.capture', listSnapshots: 'bench.listSnapshots', compare: 'bench.compare' }, @@ -321,6 +322,7 @@ export type MethodMap = { 'coverage.enable': { req: { workspaceId: string }; res: { ok: boolean } }; 'coverage.get': { req: { workspaceId: string; contract: string }; res: { source: 'func' | 'tact'; sourcePath: string; lines: { line: number; hits: number; opcodes?: number }[] } }; 'coverage.merge': { req: { workspaceId: string; runs: string[] }; res: { source: 'func' | 'tact'; sourcePath: string; lines: { line: number; hits: number; opcodes?: number }[] } }; + 'bench.capture': { req: { workspaceId: string; name: string }; res: { id: string; path: string; createdAt: number } }; 'bench.listSnapshots': { req: { workspaceId: string; dir?: string }; res: { snapshots: { id: string; path: string; createdAt: number }[] } }; 'bench.compare': { req: { workspaceId: string; runId: string; baselineId: string }; res: { metrics: { name: string; current: string; baseline: string; deltaPct: number; regression: boolean }[] } }; 'step.begin': { req: { workspaceId: string; message: SerializedMessage }; res: { iterId: string; done: boolean } }; diff --git a/packages/bridge/handlers.ts b/packages/bridge/handlers.ts index 8b99594..39269d7 100644 --- a/packages/bridge/handlers.ts +++ b/packages/bridge/handlers.ts @@ -22,9 +22,9 @@ import { snapshotToSerializable, type CoverageData } from '@ton/sandbox'; -import { getWorkspace, listWorkspaces, recordTransactions, type Workspace } from './state.js'; +import { getWorkspace, listWorkspaces, recordTransactions, type BenchRun, type Workspace } from './state.js'; import { addrToWire, big, cellToWire, tupleItemToWire, wireToCell, wireToTupleItem } from './encode.js'; -import { exitName, toTxNode, txId, txSummary } from './tree.js'; +import { exitName, toTxNode, txSummary } from './tree.js'; export interface Conn { notify: (method: string, params: unknown) => void; @@ -650,8 +650,37 @@ export const HANDLERS: Record = { }, // ── bench ── - 'bench.listSnapshots': async () => ({ snapshots: [] }), - 'bench.compare': async () => ({ metrics: [] }), + 'bench.capture': async (p) => { + const ws = await getWorkspace(p.workspaceId); + const run = captureBench(ws, p.name); + return { id: run.id, path: run.path, createdAt: run.createdAt }; + }, + 'bench.listSnapshots': async (p) => { + const ws = await getWorkspace(p.workspaceId); + // Newest first so the panel defaults to comparing the latest run to the one before. + const snapshots = [...ws.benchRuns.values()] + .reverse() + .map((r) => ({ id: r.id, path: r.path, createdAt: r.createdAt })); + return { snapshots }; + }, + 'bench.compare': async (p) => { + const ws = await getWorkspace(p.workspaceId); + const run = ws.benchRuns.get(p.runId); + const baseline = ws.benchRuns.get(p.baselineId); + if (!run || !baseline) throw new Error('Unknown benchmark run'); + const metrics = Object.keys(run.metrics).map((name) => { + const current = BigInt(run.metrics[name] ?? '0'); + const base = BigInt(baseline.metrics[name] ?? '0'); + return { + name, + current: current.toString(), + baseline: base.toString(), + deltaPct: pctDelta(current, base), + regression: LOWER_IS_BETTER.has(name) && current > base + }; + }); + return { metrics }; + }, // ── step ── 'step.begin': async (p) => { @@ -734,7 +763,7 @@ export const HANDLERS: Record = { // ── test ── 'test.run': async () => ({ runId: `run-${++subSeq}` }), - 'test.subscribe': async (p, conn) => { + 'test.subscribe': async (_p, conn) => { const id = `test-${++subSeq}`; conn.subIds.add(id); setTimeout(() => { @@ -817,3 +846,78 @@ async function takeSnapshotFrom(ws: Workspace, name: string, snap: any, parentId ws.snapshots.push(entry); return { id, name: entry.name, createdAt: entry.createdAt, parentId }; } + +// ── benchmark ───────────────────────────────────────────────────────────── +// Metrics where a larger value is worse; compare() flags these as regressions. +const LOWER_IS_BETTER = new Set([ + 'Total fees', + 'Compute gas', + 'Compute fees', + 'Action fees', + 'Storage fees', + 'Message cells', + 'Message bits' +]); + +function txDepth(tx: any): number { + let depth = 1; + for (let cur = tx.parent; cur; cur = cur.parent) depth++; + return depth; +} + +/** Aggregate real per-phase fees/gas over the workspace's executed transactions. */ +function captureBench(ws: Workspace, name: string): BenchRun { + let totalFees = 0n; + let computeGas = 0n; + let computeFees = 0n; + let actionFees = 0n; + let storageFees = 0n; + let messageCells = 0n; + let messageBits = 0n; + let maxDepth = 0; + + for (const tx of ws.txHistory) { + const d = tx.description ?? {}; + totalFees += BigInt(tx.totalFees?.coins ?? 0n); + storageFees += BigInt(d.storagePhase?.storageFeesCollected ?? 0n); + const cp = d.computePhase; + if (cp && cp.type !== 'skipped') { + computeGas += BigInt(cp.gasUsed ?? 0n); + computeFees += BigInt(cp.gasFees ?? 0n); + } + const ap = d.actionPhase; + if (ap) { + actionFees += BigInt(ap.totalActionFees ?? 0n); + messageCells += BigInt(ap.totalMessageSize?.cells ?? 0n); + messageBits += BigInt(ap.totalMessageSize?.bits ?? 0n); + } + maxDepth = Math.max(maxDepth, txDepth(tx)); + } + + const id = `bench-${++ws.benchCounter}`; + const run: BenchRun = { + id, + name: name || id, + path: `mem://${id}`, + createdAt: Date.now(), + metrics: { + 'Transactions': String(ws.txHistory.length), + 'Total fees': totalFees.toString(), + 'Compute gas': computeGas.toString(), + 'Compute fees': computeFees.toString(), + 'Action fees': actionFees.toString(), + 'Storage fees': storageFees.toString(), + 'Message cells': messageCells.toString(), + 'Message bits': messageBits.toString(), + 'Max tree depth': String(maxDepth) + } + }; + ws.benchRuns.set(id, run); + return run; +} + +/** (current - baseline) / baseline * 100, bigint-safe, divide-by-zero guarded. */ +function pctDelta(current: bigint, baseline: bigint): number { + if (baseline === 0n) return current === 0n ? 0 : 100; + return Number(((current - baseline) * 10000n) / baseline) / 100; +} diff --git a/packages/bridge/state.ts b/packages/bridge/state.ts index 85fa382..29e0820 100644 --- a/packages/bridge/state.ts +++ b/packages/bridge/state.ts @@ -32,6 +32,16 @@ export interface Subscription { send: (method: string, params: unknown) => void; } +/** A captured benchmark run: real aggregate metrics over executed transactions. */ +export interface BenchRun { + id: string; + name: string; + path: string; + createdAt: number; + /** Metric label -> decimal string (nanoton/gas can exceed 2^53, so no numbers). */ + metrics: Record; +} + export interface Workspace { id: string; name: string; @@ -44,6 +54,7 @@ export interface Workspace { vmLogsByTx: Map; txByTx: Map; snapshots: SnapshotEntry[]; + benchRuns: Map; subscriptions: Map; stepIters: Map>; // Instruction-level coverage snapshots keyed by a run id (contract ref), for coverage.merge. @@ -53,6 +64,7 @@ export interface Workspace { randomSeed?: Buffer; paused: boolean; snapCounter: number; + benchCounter: number; } const workspaces = new Map(); @@ -89,13 +101,15 @@ async function build(id: string, name: string, path: string): Promise vmLogsByTx: new Map(), txByTx: new Map(), snapshots: [], + benchRuns: new Map(), subscriptions: new Map(), stepIters: new Map(), coverageRuns: new Map(), libs: new Map(), autoDeployLibs: false, paused: false, - snapCounter: 0 + snapCounter: 0, + benchCounter: 0 }; } diff --git a/packages/mock-bridge/server.mjs b/packages/mock-bridge/server.mjs index da3bea2..148467c 100644 --- a/packages/mock-bridge/server.mjs +++ b/packages/mock-bridge/server.mjs @@ -38,6 +38,14 @@ const defaultContractState = (address) => ({ address: address ?? '0:jetton-maste const accountStates = {}; // address -> ContractState (edited via the A8 state editor) const vmSteps = { steps: [{ index: 0, opcode: 'PUSHINT 0', gas: '5', stackBefore: [], stackAfter: ['0'], stackDiff: '+0', exitCode: 0, exitCodeName: 'ok' }] }; +let benchSeq = 2; +const benchRuns = [ + { id: 'bench-1', name: 'baseline', path: 'mem://bench-1', createdAt: Date.now() - 60000, metrics: { 'Transactions': '2', 'Total fees': '4200000', 'Compute gas': '2400', 'Compute fees': '2400000', 'Action fees': '1000000', 'Storage fees': '0', 'Message cells': '4', 'Message bits': '2048', 'Max tree depth': '2' } }, + { id: 'bench-2', name: 'current', path: 'mem://bench-2', createdAt: Date.now() - 30000, metrics: { 'Transactions': '5', 'Total fees': '9800000', 'Compute gas': '5200', 'Compute fees': '5200000', 'Action fees': '2200000', 'Storage fees': '0', 'Message cells': '9', 'Message bits': '4608', 'Max tree depth': '3' } } +]; +const benchLowerIsBetter = new Set(['Total fees', 'Compute gas', 'Compute fees', 'Action fees', 'Storage fees', 'Message cells', 'Message bits']); +const benchPctDelta = (current, baseline) => (baseline === 0n ? (current === 0n ? 0 : 100) : Number(((current - baseline) * 10000n) / baseline) / 100); + wss.on('connection', (ws) => { ws.on('message', (raw) => { const req = JSON.parse(String(raw)); @@ -70,8 +78,6 @@ wss.on('connection', (ws) => { 'coverage.enable': { ok: true }, 'coverage.get': { source: 'func', sourcePath: 'contracts/jetton.fc', lines: [{ line: 1, hits: 1, opcodes: 3 }] }, 'coverage.merge': { source: 'func', sourcePath: 'contracts/jetton.fc', lines: [{ line: 1, hits: 2, opcodes: 3 }] }, - 'bench.listSnapshots': { snapshots: [{ id: 'bench-1', path: '.snapshot/bench-1.json', createdAt: Date.now() }] }, - 'bench.compare': { metrics: [{ name: 'gas', current: '1000', baseline: '900', deltaPct: 11.1, regression: true }] }, 'step.begin': { iterId: 'iter-1', done: false }, 'step.next': { node: jettonTransfer.tree, done: true }, 'step.state': { address: req.params?.address ?? '0:jetton-master', balance: '1000000000', lastTransactionLt: '1003' }, @@ -134,6 +140,30 @@ wss.on('connection', (ws) => { accounts.push(acc); return ok(acc); } + if (req.method === 'bench.capture') { + const prev = benchRuns[benchRuns.length - 1]; + const id = `bench-${++benchSeq}`; + const metrics = {}; + for (const [k, v] of Object.entries(prev.metrics)) { + metrics[k] = (BigInt(v) + BigInt(k === 'Max tree depth' ? 0 : Math.ceil(Number(v) * 0.15) + 1)).toString(); + } + const run = { id, name: req.params?.name || id, path: `mem://${id}`, createdAt: Date.now(), metrics }; + benchRuns.push(run); + return ok({ id: run.id, path: run.path, createdAt: run.createdAt }); + } + if (req.method === 'bench.listSnapshots') { + return ok({ snapshots: [...benchRuns].reverse().map((r) => ({ id: r.id, path: r.path, createdAt: r.createdAt })) }); + } + if (req.method === 'bench.compare') { + const run = benchRuns.find((r) => r.id === req.params?.runId) ?? benchRuns[benchRuns.length - 1]; + const base = benchRuns.find((r) => r.id === req.params?.baselineId) ?? benchRuns[0]; + const metrics = Object.keys(run.metrics).map((name) => { + const current = BigInt(run.metrics[name] ?? '0'); + const baseline = BigInt(base.metrics[name] ?? '0'); + return { name, current: current.toString(), baseline: baseline.toString(), deltaPct: benchPctDelta(current, baseline), regression: benchLowerIsBetter.has(name) && current > baseline }; + }); + return ok({ metrics }); + } if (req.method === 'logs.subscribe') { ok({ subscriptionId: 'logs-1' }); setTimeout(() => ws.send(JSON.stringify({ method: 'logs.notify', params: { subscriptionId: 'logs-1', entry: { ts: Date.now(), kind: 'debug', text: 'mock log entry' } } })), 25); diff --git a/packages/mock-bridge/src/server.ts b/packages/mock-bridge/src/server.ts index 36f7a62..17e0a6f 100644 --- a/packages/mock-bridge/src/server.ts +++ b/packages/mock-bridge/src/server.ts @@ -35,6 +35,16 @@ const workspaceContracts: Record = { 'demo-workspace': const defaultContractState = (address: string) => ({ address: address || '0:jetton-master', balance: '1000000000', stateInit: { code: 'te6ccgEBAQEAAgAAAA==', data: 'te6ccgEBAQEAAgAAAA==' } as { code: string; data: string } | undefined, lastTransactionLt: '1003', status: 'active', codeHash: 'a3f1c9e84b27d6510f8b2a4e9c7d3b1068f5e2a0c4d9b7e6f1a8c3d52b09e7f4a' }); const accountStates: Record> = {}; +type MockBenchRun = { id: string; name: string; path: string; createdAt: number; metrics: Record }; +let benchSeq = 2; +const benchRuns: MockBenchRun[] = [ + { id: 'bench-1', name: 'baseline', path: 'mem://bench-1', createdAt: Date.now() - 60000, metrics: { 'Transactions': '2', 'Total fees': '4200000', 'Compute gas': '2400', 'Compute fees': '2400000', 'Action fees': '1000000', 'Storage fees': '0', 'Message cells': '4', 'Message bits': '2048', 'Max tree depth': '2' } }, + { id: 'bench-2', name: 'current', path: 'mem://bench-2', createdAt: Date.now() - 30000, metrics: { 'Transactions': '5', 'Total fees': '9800000', 'Compute gas': '5200', 'Compute fees': '5200000', 'Action fees': '2200000', 'Storage fees': '0', 'Message cells': '9', 'Message bits': '4608', 'Max tree depth': '3' } } +]; +const benchLowerIsBetter = new Set(['Total fees', 'Compute gas', 'Compute fees', 'Action fees', 'Storage fees', 'Message cells', 'Message bits']); +const benchPctDelta = (current: bigint, baseline: bigint): number => + baseline === 0n ? (current === 0n ? 0 : 100) : Number(((current - baseline) * 10000n) / baseline) / 100; + wss.on('connection', (ws) => { ws.on('message', (raw) => { const req = JSON.parse(String(raw)); @@ -150,8 +160,30 @@ wss.on('connection', (ws) => { case 'logs.setVerbosityForAddress': ok({ ok: true }); break; case 'coverage.get': case 'coverage.merge': ok({ source: 'func', sourcePath: 'contracts/jetton.fc', lines: [{ line: 1, hits: 1, opcodes: 3 }] }); break; - case 'bench.listSnapshots': ok({ snapshots: [{ id: 'bench-1', path: '.snapshot/bench-1.json', createdAt: Date.now() }] }); break; - case 'bench.compare': ok({ metrics: [{ name: 'gas', current: '1000', baseline: '900', deltaPct: 11.1, regression: true }] }); break; + case 'bench.capture': { + const prev = benchRuns[benchRuns.length - 1]; + const id = `bench-${++benchSeq}`; + const metrics: Record = {}; + for (const [k, v] of Object.entries(prev.metrics)) { + metrics[k] = (BigInt(v) + BigInt(k === 'Max tree depth' ? 0 : Math.ceil(Number(v) * 0.15) + 1)).toString(); + } + const run: MockBenchRun = { id, name: req.params?.name || id, path: `mem://${id}`, createdAt: Date.now(), metrics }; + benchRuns.push(run); + ok({ id: run.id, path: run.path, createdAt: run.createdAt }); + break; + } + case 'bench.listSnapshots': ok({ snapshots: [...benchRuns].reverse().map((r) => ({ id: r.id, path: r.path, createdAt: r.createdAt })) }); break; + case 'bench.compare': { + const run = benchRuns.find((r) => r.id === req.params?.runId) ?? benchRuns[benchRuns.length - 1]; + const base = benchRuns.find((r) => r.id === req.params?.baselineId) ?? benchRuns[0]; + const metrics = Object.keys(run.metrics).map((name) => { + const current = BigInt(run.metrics[name] ?? '0'); + const baseline = BigInt(base.metrics[name] ?? '0'); + return { name, current: current.toString(), baseline: baseline.toString(), deltaPct: benchPctDelta(current, baseline), regression: benchLowerIsBetter.has(name) && current > baseline }; + }); + ok({ metrics }); + break; + } case 'step.begin': ok({ iterId: 'iter-1', done: false }); break; case 'step.next': ok({ node: jettonTransfer.tree, done: true }); break; case 'step.state': ok({ address: req.params.address, balance: '1000000000', lastTransactionLt: '1003' }); break; From c9fff98b43d37f911e8311a6b9cd7fdada489354 Mon Sep 17 00:00:00 2001 From: GiFTED! Date: Wed, 8 Jul 2026 11:17:34 +0100 Subject: [PATCH 2/3] fix(a16): surface captured run names in snapshot pickers --- app/src/panels/A16-benchmark/BenchmarkPanel.tsx | 4 ++-- app/src/rpc/contract.ts | 2 +- packages/bridge/handlers.ts | 2 +- packages/mock-bridge/server.mjs | 2 +- packages/mock-bridge/src/server.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/panels/A16-benchmark/BenchmarkPanel.tsx b/app/src/panels/A16-benchmark/BenchmarkPanel.tsx index a846c05..91e4feb 100644 --- a/app/src/panels/A16-benchmark/BenchmarkPanel.tsx +++ b/app/src/panels/A16-benchmark/BenchmarkPanel.tsx @@ -19,7 +19,7 @@ import { } from '../../components/ui'; import { cn } from '../../lib/cn'; -type Snapshot = { id: string; path: string; createdAt: number }; +type Snapshot = { id: string; name: string; path: string; createdAt: number }; type Metric = { name: string; current: string; @@ -244,7 +244,7 @@ function Picker({ > {snapshots.map((s) => ( ))} diff --git a/app/src/rpc/contract.ts b/app/src/rpc/contract.ts index d0766a7..a6d8cdf 100644 --- a/app/src/rpc/contract.ts +++ b/app/src/rpc/contract.ts @@ -323,7 +323,7 @@ export type MethodMap = { 'coverage.get': { req: { workspaceId: string; contract: string }; res: { source: 'func' | 'tact'; sourcePath: string; lines: { line: number; hits: number; opcodes?: number }[] } }; 'coverage.merge': { req: { workspaceId: string; runs: string[] }; res: { source: 'func' | 'tact'; sourcePath: string; lines: { line: number; hits: number; opcodes?: number }[] } }; 'bench.capture': { req: { workspaceId: string; name: string }; res: { id: string; path: string; createdAt: number } }; - 'bench.listSnapshots': { req: { workspaceId: string; dir?: string }; res: { snapshots: { id: string; path: string; createdAt: number }[] } }; + 'bench.listSnapshots': { req: { workspaceId: string; dir?: string }; res: { snapshots: { id: string; name: string; path: string; createdAt: number }[] } }; 'bench.compare': { req: { workspaceId: string; runId: string; baselineId: string }; res: { metrics: { name: string; current: string; baseline: string; deltaPct: number; regression: boolean }[] } }; 'step.begin': { req: { workspaceId: string; message: SerializedMessage }; res: { iterId: string; done: boolean } }; 'step.next': { req: { iterId: string }; res: { node: TxNode | null; done: boolean } }; diff --git a/packages/bridge/handlers.ts b/packages/bridge/handlers.ts index 39269d7..6943b27 100644 --- a/packages/bridge/handlers.ts +++ b/packages/bridge/handlers.ts @@ -660,7 +660,7 @@ export const HANDLERS: Record = { // Newest first so the panel defaults to comparing the latest run to the one before. const snapshots = [...ws.benchRuns.values()] .reverse() - .map((r) => ({ id: r.id, path: r.path, createdAt: r.createdAt })); + .map((r) => ({ id: r.id, name: r.name, path: r.path, createdAt: r.createdAt })); return { snapshots }; }, 'bench.compare': async (p) => { diff --git a/packages/mock-bridge/server.mjs b/packages/mock-bridge/server.mjs index 148467c..847db03 100644 --- a/packages/mock-bridge/server.mjs +++ b/packages/mock-bridge/server.mjs @@ -152,7 +152,7 @@ wss.on('connection', (ws) => { return ok({ id: run.id, path: run.path, createdAt: run.createdAt }); } if (req.method === 'bench.listSnapshots') { - return ok({ snapshots: [...benchRuns].reverse().map((r) => ({ id: r.id, path: r.path, createdAt: r.createdAt })) }); + return ok({ snapshots: [...benchRuns].reverse().map((r) => ({ id: r.id, name: r.name, path: r.path, createdAt: r.createdAt })) }); } if (req.method === 'bench.compare') { const run = benchRuns.find((r) => r.id === req.params?.runId) ?? benchRuns[benchRuns.length - 1]; diff --git a/packages/mock-bridge/src/server.ts b/packages/mock-bridge/src/server.ts index 17e0a6f..5b96ff2 100644 --- a/packages/mock-bridge/src/server.ts +++ b/packages/mock-bridge/src/server.ts @@ -172,7 +172,7 @@ wss.on('connection', (ws) => { ok({ id: run.id, path: run.path, createdAt: run.createdAt }); break; } - case 'bench.listSnapshots': ok({ snapshots: [...benchRuns].reverse().map((r) => ({ id: r.id, path: r.path, createdAt: r.createdAt })) }); break; + case 'bench.listSnapshots': ok({ snapshots: [...benchRuns].reverse().map((r) => ({ id: r.id, name: r.name, path: r.path, createdAt: r.createdAt })) }); break; case 'bench.compare': { const run = benchRuns.find((r) => r.id === req.params?.runId) ?? benchRuns[benchRuns.length - 1]; const base = benchRuns.find((r) => r.id === req.params?.baselineId) ?? benchRuns[0]; From 1b1ca946ed2ad33a6568521a52018ce82a9c5021 Mon Sep 17 00:00:00 2001 From: GiFTED! Date: Wed, 8 Jul 2026 11:28:17 +0100 Subject: [PATCH 3/3] fix(bench): capture per-interval metrics so compare measures per-scenario cost --- app/src/panels/A16-benchmark/BenchmarkPanel.tsx | 4 ++-- packages/bridge/handlers.ts | 14 +++++++++++--- packages/bridge/state.ts | 7 ++++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/src/panels/A16-benchmark/BenchmarkPanel.tsx b/app/src/panels/A16-benchmark/BenchmarkPanel.tsx index 91e4feb..e6b351c 100644 --- a/app/src/panels/A16-benchmark/BenchmarkPanel.tsx +++ b/app/src/panels/A16-benchmark/BenchmarkPanel.tsx @@ -108,7 +108,7 @@ function BenchmarkBody({ ) : ( <> @@ -211,7 +211,7 @@ function CaptureBar({

- Snapshots total fees, compute gas, and message-tree metrics from the transactions executed so far. + Aggregates total fees, compute gas, and message-tree metrics from the transactions executed since the previous capture.

{error ? (
diff --git a/packages/bridge/handlers.ts b/packages/bridge/handlers.ts index 6943b27..6d110ad 100644 --- a/packages/bridge/handlers.ts +++ b/packages/bridge/handlers.ts @@ -865,7 +865,13 @@ function txDepth(tx: any): number { return depth; } -/** Aggregate real per-phase fees/gas over the workspace's executed transactions. */ +/** + * Aggregate real per-phase fees/gas over the transactions executed since the + * previous capture. Interval-based on purpose: txHistory is append-only (even + * across snapshot restores), so cumulative totals would flag every + * lower-is-better metric as a regression whenever any transaction ran between + * two captures. Per-interval metrics make compare() measure per-scenario cost. + */ function captureBench(ws: Workspace, name: string): BenchRun { let totalFees = 0n; let computeGas = 0n; @@ -876,7 +882,8 @@ function captureBench(ws: Workspace, name: string): BenchRun { let messageBits = 0n; let maxDepth = 0; - for (const tx of ws.txHistory) { + const txs = ws.txHistory.slice(ws.benchWatermark); + for (const tx of txs) { const d = tx.description ?? {}; totalFees += BigInt(tx.totalFees?.coins ?? 0n); storageFees += BigInt(d.storagePhase?.storageFeesCollected ?? 0n); @@ -901,7 +908,7 @@ function captureBench(ws: Workspace, name: string): BenchRun { path: `mem://${id}`, createdAt: Date.now(), metrics: { - 'Transactions': String(ws.txHistory.length), + 'Transactions': String(txs.length), 'Total fees': totalFees.toString(), 'Compute gas': computeGas.toString(), 'Compute fees': computeFees.toString(), @@ -913,6 +920,7 @@ function captureBench(ws: Workspace, name: string): BenchRun { } }; ws.benchRuns.set(id, run); + ws.benchWatermark = ws.txHistory.length; return run; } diff --git a/packages/bridge/state.ts b/packages/bridge/state.ts index 29e0820..7ff2203 100644 --- a/packages/bridge/state.ts +++ b/packages/bridge/state.ts @@ -65,6 +65,10 @@ export interface Workspace { paused: boolean; snapCounter: number; benchCounter: number; + // txHistory index where the last benchmark capture ended; the next capture + // aggregates only transactions appended after it (txHistory is append-only, + // snapshot restores never rewind it). + benchWatermark: number; } const workspaces = new Map(); @@ -109,7 +113,8 @@ async function build(id: string, name: string, path: string): Promise autoDeployLibs: false, paused: false, snapCounter: 0, - benchCounter: 0 + benchCounter: 0, + benchWatermark: 0 }; }