Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 131 additions & 1 deletion report/src/components/ConfigCard.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
import { describe, expect, it } from "vitest";

import { formatTransactions } from "./ConfigCard";
import { buildRows, formatTransactions } from "./ConfigCard";
import { LoadTestConfig } from "../types";

const baseConfig = (
overrides: Partial<LoadTestConfig> = {},
): 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", () => {
Expand Down Expand Up @@ -32,3 +57,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");
});
});
76 changes: 57 additions & 19 deletions report/src/components/ConfigCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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[] }) => (
Expand Down
17 changes: 13 additions & 4 deletions report/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string, unknown> | null;
}
Expand Down
Loading