From 57f531f3e8393643c065b07d2d8c170df18cd4ee Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 30 Jul 2026 12:46:08 -0400 Subject: [PATCH 1/2] fix(report): tolerate open-loop config schema in ConfigCard Producer configs may omit batch_size and emit funding_batch_size instead; skip missing fields instead of calling toLocaleString on undefined. --- report/src/components/ConfigCard.test.ts | 130 ++++++++++++++++++++++- report/src/components/ConfigCard.tsx | 76 +++++++++---- report/src/types.ts | 17 ++- 3 files changed, 199 insertions(+), 24 deletions(-) diff --git a/report/src/components/ConfigCard.test.ts b/report/src/components/ConfigCard.test.ts index f5351a6..f511ce5 100644 --- a/report/src/components/ConfigCard.test.ts +++ b/report/src/components/ConfigCard.test.ts @@ -1,6 +1,29 @@ import { describe, expect, it } from "vitest"; -import { formatTransactions } from "./ConfigCard"; +import { buildRows, formatTransactions } from "./ConfigCard"; +import { LoadTestConfig } from "../types"; + +const baseConfig = ( + overrides: Partial = {}, +): LoadTestConfig => ({ + funding_amount: "1000000000000000000", + sender_count: 10, + sender_offset: 0, + in_flight_per_sender: 4, + batch_size: 50, + batch_timeout: "100ms", + duration: "60s", + target_gps: 1_000_000, + seed: 42, + chain_id: 84532, + transactions: [{ type: "swap", weight: 100 }], + looper_contract: null, + swap_token_amount: "0", + ...overrides, +}); + +const flatLabels = (config: LoadTestConfig): string[] => + buildRows(config).flat().map((r) => r.label); describe("formatTransactions", () => { it("formats a weighted transaction mix", () => { @@ -32,3 +55,108 @@ describe("formatTransactions", () => { ).toBe("transfer (100%, 25% account-create)"); }); }); + +describe("buildRows", () => { + it("renders the full closed-loop config when all Option fields are set", () => { + const labels = flatLabels(baseConfig()); + expect(labels).toEqual([ + "Senders", + "In-flight / sender", + "Batch size", + "Batch timeout", + "Duration", + "Target gas/s", + "Funding / sender", + "Seed", + "Chain ID", + ]); + }); + + it("omits null Option fields instead of calling toLocaleString on them", () => { + const labels = flatLabels( + baseConfig({ + batch_timeout: null, + duration: null, + target_gps: null, + chain_id: null, + }), + ); + expect(labels).toEqual([ + "Senders", + "In-flight / sender", + "Batch size", + "Funding / sender", + "Seed", + ]); + expect(labels).not.toContain("Target gas/s"); + expect(labels).not.toContain("Duration"); + expect(labels).not.toContain("Batch timeout"); + expect(labels).not.toContain("Chain ID"); + }); + + it("omits Option fields that are missing (undefined) on older payloads", () => { + // Simulate a JSON payload where keys were absent rather than null. + const sparse = baseConfig(); + delete (sparse as { batch_timeout?: string | null }).batch_timeout; + delete (sparse as { duration?: string | null }).duration; + delete (sparse as { target_gps?: number | null }).target_gps; + delete (sparse as { chain_id?: number | null }).chain_id; + delete (sparse as { sender_offset?: number }).sender_offset; + + expect(() => buildRows(sparse)).not.toThrow(); + const labels = flatLabels(sparse); + expect(labels).not.toContain("Target gas/s"); + expect(labels).not.toContain("Sender offset"); + expect(labels).not.toContain("Chain ID"); + }); + + it("formats target_gps with SI suffixes when present", () => { + const rows = buildRows(baseConfig({ target_gps: 25_000_000 })).flat(); + const target = rows.find((r) => r.label === "Target gas/s"); + expect(target?.value).toBe("25M gas/s"); + }); + + it("renders the open-loop sepolia payload without batch_size (live crash repro)", () => { + // Shape from https://base-benchmarking-api-dev.cbhq.net/api/v1/load-tests/sepolia/2026-07-27T00-01-29 + // โ€” producer dropped batch_size/batch_timeout and emits funding_batch_size. + const openLoop = baseConfig({ + sender_count: 300, + in_flight_per_sender: 40, + duration: "30s", + target_gps: 375_000_000, + seed: 234521, + chain_id: null, + funding_batch_size: 16, + funding_amount: "20000000000000000", + swap_token_amount: "1000000000000000000000", + transactions: [ + { type: "uniswap_v3", weight: 50 }, + { type: "aerodrome_cl", weight: 50 }, + ], + }); + delete (openLoop as { batch_size?: number | null }).batch_size; + delete (openLoop as { batch_timeout?: string | null }).batch_timeout; + + expect(() => buildRows(openLoop)).not.toThrow(); + const labels = flatLabels(openLoop); + expect(labels).toEqual([ + "Senders", + "In-flight / sender", + "Duration", + "Target gas/s", + "Funding / sender", + "Funding batch size", + "Seed", + ]); + expect(labels).not.toContain("Batch size"); + expect(labels).not.toContain("Batch timeout"); + }); + + it("falls back to max_target_gps when target_gps is absent", () => { + const rows = buildRows( + baseConfig({ target_gps: undefined, max_target_gps: 50_000_000 }), + ).flat(); + const target = rows.find((r) => r.label === "Target gas/s"); + expect(target?.value).toBe("50M gas/s"); + }); +}); diff --git a/report/src/components/ConfigCard.tsx b/report/src/components/ConfigCard.tsx index 2a94c4a..c154f5d 100644 --- a/report/src/components/ConfigCard.tsx +++ b/report/src/components/ConfigCard.tsx @@ -35,37 +35,65 @@ export const formatTransactions = ( .join(" ยท "); }; -const formatTargetGps = (gps: number): string => { +export const formatTargetGps = (gps: number): string => { if (gps >= 1e9) return `${(gps / 1e9).toFixed(1)}B gas/s`; if (gps >= 1e6) return `${(gps / 1e6).toFixed(0)}M gas/s`; if (gps >= 1e3) return `${(gps / 1e3).toFixed(0)}k gas/s`; return `${gps.toLocaleString()} gas/s`; }; -const buildRows = (config: LoadTestConfig): Row[][] => { - const loadShape: Row[] = [ - { label: "Senders", value: config.sender_count.toLocaleString() }, - { +/** Build labeled config rows, omitting null/undefined / schema-drift fields. */ +export const buildRows = (config: LoadTestConfig): Row[][] => { + const loadShape: Row[] = []; + if (typeof config.sender_count === "number") { + loadShape.push({ + label: "Senders", + value: config.sender_count.toLocaleString(), + }); + } + if (typeof config.in_flight_per_sender === "number") { + loadShape.push({ label: "In-flight / sender", value: config.in_flight_per_sender.toLocaleString(), - }, - { label: "Batch size", value: config.batch_size.toLocaleString() }, - { label: "Batch timeout", value: config.batch_timeout }, - ]; - if (config.sender_offset !== 0) { + }); + } + // Closed-loop: batch_size + batch_timeout. Open-loop dropped both. + if (typeof config.batch_size === "number") { + loadShape.push({ + label: "Batch size", + value: config.batch_size.toLocaleString(), + }); + } + if (config.batch_timeout != null) { + loadShape.push({ label: "Batch timeout", value: config.batch_timeout }); + } + if (typeof config.sender_offset === "number" && config.sender_offset !== 0) { loadShape.push({ label: "Sender offset", value: config.sender_offset.toLocaleString(), }); } - const target: Row[] = [ - { label: "Duration", value: config.duration }, - { label: "Target gas/s", value: formatTargetGps(config.target_gps) }, - ]; + const target: Row[] = []; + if (config.duration != null) { + target.push({ label: "Duration", value: config.duration }); + } + // Prefer target_gps; fall back to open-loop max_target_gps. + const targetGps = + typeof config.target_gps === "number" + ? config.target_gps + : typeof config.max_target_gps === "number" + ? config.max_target_gps + : null; + if (targetGps !== null) { + target.push({ + label: "Target gas/s", + value: formatTargetGps(targetGps), + }); + } // Surface the storage workload's cold-slot count (from the `storage` tx's // `slots_per_tx`) so proofs/storage runs show their per-tx write budget. - const storageTx = config.transactions.find( + const storageTx = config.transactions?.find( (t) => t.type === "storage" && t.slots_per_tx != null, ); if (storageTx?.slots_per_tx != null) { @@ -81,8 +109,14 @@ const buildRows = (config: LoadTestConfig): Row[][] => { value: formatEthFromWeiString(config.funding_amount), }, ]; + if (typeof config.funding_batch_size === "number") { + funding.push({ + label: "Funding batch size", + value: config.funding_batch_size.toLocaleString(), + }); + } const hasSwapToken = - config.transactions.some((t) => t.type === "swap") && + config.transactions?.some((t) => t.type === "swap") && config.swap_token_amount && config.swap_token_amount !== "0"; if (hasSwapToken) { @@ -92,15 +126,19 @@ const buildRows = (config: LoadTestConfig): Row[][] => { }); } - const repro: Row[] = [{ label: "Seed", value: config.seed.toLocaleString() }]; - if (config.chain_id !== null) { + const repro: Row[] = []; + if (typeof config.seed === "number") { + repro.push({ label: "Seed", value: config.seed.toLocaleString() }); + } + if (typeof config.chain_id === "number") { repro.push({ label: "Chain ID", value: config.chain_id.toLocaleString() }); } if (config.looper_contract) { repro.push({ label: "Looper contract", value: config.looper_contract }); } - return [loadShape, target, funding, repro]; + // Drop empty groups so continuous / open-loop runs don't leave blank sections. + return [loadShape, target, funding, repro].filter((g) => g.length > 0); }; const RowGroup = ({ rows }: { rows: Row[] }) => ( diff --git a/report/src/types.ts b/report/src/types.ts index 39e5b7f..9d7e3cb 100644 --- a/report/src/types.ts +++ b/report/src/types.ts @@ -201,10 +201,17 @@ export interface LoadTestConfig { sender_count: number; sender_offset: number; in_flight_per_sender: number; - batch_size: number; - batch_timeout: string; - duration: string; - target_gps: number; + // Closed-loop producer fields. Open-loop (base#30092f0) dropped these in + // favor of funding_batch_size โ€” treat as optional so both schemas render. + batch_size?: number | null; + batch_timeout?: string | null; + // Open-loop / funding-phase RPC batch size. Absent on older closed-loop runs. + funding_batch_size?: number | null; + // Option in Rust ConfigSummary โ€” null when unset; may be absent on older runs. + duration: string | null; + target_gps?: number | null; + // Open-loop adaptive pacing ceiling; prefer when target_gps is absent. + max_target_gps?: number | null; seed: number; chain_id: number | null; // Storage-type entries also carry `contract` and `slots_per_tx` (flattened @@ -218,6 +225,8 @@ export interface LoadTestConfig { fresh_recipient_ratio?: number; looper_contract: string | null; swap_token_amount: string; + // Present on newer producers; unused in the UI today. + b20_mint_amount?: string; // Producer omits unless real-token setup is enabled; loosely typed. real_token_setup?: Record | null; } From 5a4fdf9898eed26cd3e79da448f61ba4f8ff0bec Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 30 Jul 2026 13:15:53 -0400 Subject: [PATCH 2/2] style(report): prettier-format ConfigCard test helper --- report/src/components/ConfigCard.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/report/src/components/ConfigCard.test.ts b/report/src/components/ConfigCard.test.ts index f511ce5..b180a6b 100644 --- a/report/src/components/ConfigCard.test.ts +++ b/report/src/components/ConfigCard.test.ts @@ -23,7 +23,9 @@ const baseConfig = ( }); const flatLabels = (config: LoadTestConfig): string[] => - buildRows(config).flat().map((r) => r.label); + buildRows(config) + .flat() + .map((r) => r.label); describe("formatTransactions", () => { it("formats a weighted transaction mix", () => {