Skip to content

Commit e10077d

Browse files
authored
Add load-test frontend (#174)
* Add load-test frontend * fixes * Add throughput chart and run config to load-test detail page - ThroughputChart: D3 dual-axis line chart (TPS left, gas/s right) with hover tooltip; positioned by elapsed_secs to handle irregular sample spacing - ConfigCard: grouped grid showing load shape, target, funding, repro seed, and workload mix; omits null fields rather than rendering '\u2014' - formatEthFromWeiString: BigInt-based wei\u2192ETH for u128 strings that exceed Number.MAX_SAFE_INTEGER - Fix top_failure_reasons schema mismatch: API returns [reason, count] tuples, not {reason, count} objects - New config and throughput_timeseries fields are optional on LoadTestResult; sections are gated on presence so older runs that predate these fields still render cleanly * Apply prettier formatting to frontend components * update pinned git
1 parent 48f0e04 commit e10077d

16 files changed

Lines changed: 1325 additions & 10 deletions

.github/workflows/_build-binaries.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ on:
1919
description: "Base Reth Node version to build"
2020
required: false
2121
type: string
22-
default: "feature/load-test-benchmark"
22+
default: "1e4a8a7"
2323

2424
# Set minimal permissions for all jobs by default
2525
permissions:

clients/versions.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ GETH_VERSION="v1.101604.0"
1212

1313
# Base Reth Node Configuration
1414
BASE_RETH_NODE_REPO="https://github.com/base/base"
15-
BASE_RETH_NODE_VERSION="feature/load-test-benchmark"
15+
BASE_RETH_NODE_VERSION="1e4a8a7"
1616

1717
# Build Configuration
1818
# BUILD_DIR="./build"

report/.eslintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.eslintrc.cjs
22
vite.config.ts
3-
tailwind.config.js
3+
tailwind.config.js
4+
dev-mock/

report/src/App.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
1-
import { Route, Routes } from "react-router-dom";
1+
import { Navigate, Route, Routes } from "react-router-dom";
22
import RunIndex from "./pages/RunIndex";
33
import RunComparison from "./pages/RunComparison";
44
import RedirectToLatestRun from "./pages/RedirectToLatestRun";
5+
import LoadTestLanding from "./pages/LoadTestLanding";
6+
import LoadTestAllRuns from "./pages/LoadTestAllRuns";
7+
import LoadTestDetail from "./pages/LoadTestDetail";
58
import ErrorBoundary from "./components/ErrorBoundary";
69

710
function App() {
811
return (
912
<ErrorBoundary>
1013
<Routes>
1114
<Route path="/" element={<RedirectToLatestRun />} />
15+
<Route
16+
path="/load-tests"
17+
element={<Navigate to="/load-tests/sepolia" replace />}
18+
/>
19+
<Route path="/load-tests/:network" element={<LoadTestLanding />} />
20+
<Route path="/load-tests/:network/all" element={<LoadTestAllRuns />} />
21+
<Route
22+
path="/load-tests/:network/:timestamp"
23+
element={<LoadTestDetail />}
24+
/>
1225
<Route path="/:benchmarkRunId" element={<RunIndex />} />
1326
<Route
1427
path="/run-comparison/:benchmarkRunId"
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { LoadTestConfig } from "../types";
2+
import { formatEthFromWeiString } from "../utils/formatters";
3+
import StatCard from "./StatCard";
4+
5+
interface ConfigCardProps {
6+
config: LoadTestConfig;
7+
}
8+
9+
interface Row {
10+
label: string;
11+
value: string;
12+
}
13+
14+
const formatTransactions = (txs: LoadTestConfig["transactions"]): string => {
15+
if (!txs || txs.length === 0) return "—";
16+
const total = txs.reduce((acc, t) => acc + t.weight, 0);
17+
if (total === 0) return txs.map((t) => t.type).join(" · ");
18+
return txs
19+
.map((t) => `${t.type} (${Math.round((t.weight / total) * 100)}%)`)
20+
.join(" · ");
21+
};
22+
23+
const formatTargetGps = (gps: number): string => {
24+
if (gps >= 1e9) return `${(gps / 1e9).toFixed(1)}B gas/s`;
25+
if (gps >= 1e6) return `${(gps / 1e6).toFixed(0)}M gas/s`;
26+
if (gps >= 1e3) return `${(gps / 1e3).toFixed(0)}k gas/s`;
27+
return `${gps.toLocaleString()} gas/s`;
28+
};
29+
30+
const buildRows = (config: LoadTestConfig): Row[][] => {
31+
const loadShape: Row[] = [
32+
{ label: "Senders", value: config.sender_count.toLocaleString() },
33+
{
34+
label: "In-flight / sender",
35+
value: config.in_flight_per_sender.toLocaleString(),
36+
},
37+
{ label: "Batch size", value: config.batch_size.toLocaleString() },
38+
{ label: "Batch timeout", value: config.batch_timeout },
39+
];
40+
if (config.sender_offset !== 0) {
41+
loadShape.push({
42+
label: "Sender offset",
43+
value: config.sender_offset.toLocaleString(),
44+
});
45+
}
46+
47+
const target: Row[] = [
48+
{ label: "Duration", value: config.duration },
49+
{ label: "Target gas/s", value: formatTargetGps(config.target_gps) },
50+
];
51+
52+
const funding: Row[] = [
53+
{
54+
label: "Funding / sender",
55+
value: formatEthFromWeiString(config.funding_amount),
56+
},
57+
];
58+
const hasSwapToken =
59+
config.transactions.some((t) => t.type === "swap") &&
60+
config.swap_token_amount &&
61+
config.swap_token_amount !== "0";
62+
if (hasSwapToken) {
63+
funding.push({
64+
label: "Swap token amount",
65+
value: formatEthFromWeiString(config.swap_token_amount),
66+
});
67+
}
68+
69+
const repro: Row[] = [{ label: "Seed", value: config.seed.toLocaleString() }];
70+
if (config.chain_id !== null) {
71+
repro.push({ label: "Chain ID", value: config.chain_id.toLocaleString() });
72+
}
73+
if (config.looper_contract) {
74+
repro.push({ label: "Looper contract", value: config.looper_contract });
75+
}
76+
77+
return [loadShape, target, funding, repro];
78+
};
79+
80+
const RowGroup = ({ rows }: { rows: Row[] }) => (
81+
<div className="grid grid-cols-2 gap-x-6 gap-y-3">
82+
{rows.map((r) => (
83+
<div key={r.label} className="flex flex-col">
84+
<span className="text-xs uppercase tracking-wide text-slate-500">
85+
{r.label}
86+
</span>
87+
<span className="text-sm text-slate-900 font-mono mt-0.5 break-all">
88+
{r.value}
89+
</span>
90+
</div>
91+
))}
92+
</div>
93+
);
94+
95+
const ConfigCard = ({ config }: ConfigCardProps) => {
96+
const groups = buildRows(config);
97+
const txLine = formatTransactions(config.transactions);
98+
99+
return (
100+
<StatCard title="Run config">
101+
<div className="flex flex-col gap-y-5">
102+
{groups.map((rows, i) => (
103+
<div key={i}>
104+
{i > 0 && <hr className="border-slate-100 mb-5" />}
105+
<RowGroup rows={rows} />
106+
</div>
107+
))}
108+
<hr className="border-slate-100" />
109+
<div className="flex flex-col">
110+
<span className="text-xs uppercase tracking-wide text-slate-500">
111+
Workload
112+
</span>
113+
<span className="text-sm text-slate-900 mt-0.5">{txLine}</span>
114+
</div>
115+
</div>
116+
</StatCard>
117+
);
118+
};
119+
120+
export default ConfigCard;

report/src/components/Navbar.tsx

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
import {
22
Link,
3+
useLocation,
34
useNavigate,
45
useParams,
56
useSearchParams,
67
} from "react-router-dom";
8+
import clsx from "clsx";
79
import Logo from "../assets/logo.svg";
8-
import { useTestMetadata } from "../utils/useDataSeries";
10+
import { useLoadTestList, useTestMetadata } from "../utils/useDataSeries";
911
import { useCallback, useMemo } from "react";
1012
import { uniqBy } from "lodash";
1113
import {} from "react-router-dom";
1214
import Select from "./Select";
15+
import { formatLoadTestTimestamp } from "../utils/formatters";
1316

1417
interface ProvidedProps {
1518
urlPrefix?: string;
1619
}
1720

21+
const DEFAULT_LOAD_TEST_NETWORK = "sepolia";
22+
1823
const Navbar = ({ urlPrefix }: ProvidedProps) => {
24+
const location = useLocation();
25+
const isLoadTestsRoute = location.pathname.startsWith("/load-tests");
26+
1927
const { data: allBenchmarkRuns, isLoading } = useTestMetadata();
2028

2129
const [searchParams] = useSearchParams();
@@ -31,7 +39,33 @@ const Navbar = ({ urlPrefix }: ProvidedProps) => {
3139
[urlPrefix, searchParams, navigate],
3240
);
3341

34-
const { benchmarkRunId } = useParams();
42+
const {
43+
benchmarkRunId,
44+
network: loadTestNetwork,
45+
timestamp: loadTestTimestamp,
46+
} = useParams();
47+
48+
const activeLoadTestNetwork = loadTestNetwork ?? DEFAULT_LOAD_TEST_NETWORK;
49+
50+
const { data: loadTestEntries, isLoading: isLoadingLoadTests } =
51+
useLoadTestList(isLoadTestsRoute ? activeLoadTestNetwork : null);
52+
53+
const navigateToLoadTestRun = useCallback(
54+
(timestamp: string) => {
55+
navigate({
56+
pathname: `/load-tests/${activeLoadTestNetwork}/${timestamp}`,
57+
});
58+
},
59+
[activeLoadTestNetwork, navigate],
60+
);
61+
62+
const loadTestOptions = useMemo(() => {
63+
if (!loadTestEntries) return [];
64+
return loadTestEntries.map((entry) => ({
65+
label: formatLoadTestTimestamp(entry.timestamp),
66+
value: entry.timestamp,
67+
}));
68+
}, [loadTestEntries]);
3569

3670
const latestRun = useMemo(() => {
3771
return allBenchmarkRuns?.runs.sort(
@@ -80,6 +114,14 @@ const Navbar = ({ urlPrefix }: ProvidedProps) => {
80114
return optionsWithTestNum;
81115
}, [allBenchmarkRuns, latestRun]);
82116

117+
const tabClass = (active: boolean) =>
118+
clsx(
119+
"px-3 py-4 text-sm border-b-2 -mb-px",
120+
active
121+
? "border-blue-600 text-slate-900 font-medium"
122+
: "border-transparent text-slate-500 hover:text-slate-900",
123+
);
124+
83125
return (
84126
<nav className="flex px-8 border-b border-slate-300 items-center bg-white gap-x-4">
85127
<div className="flex items-center gap-x-4 flex-grow">
@@ -89,8 +131,16 @@ const Navbar = ({ urlPrefix }: ProvidedProps) => {
89131
</Link>
90132
<div className="font-medium">Client Benchmark Report</div>
91133
</div>
134+
<div className="flex items-center gap-x-2 ml-4 self-stretch">
135+
<Link to="/" className={tabClass(!isLoadTestsRoute)}>
136+
Benchmarks
137+
</Link>
138+
<Link to="/load-tests/sepolia" className={tabClass(isLoadTestsRoute)}>
139+
Load Tests
140+
</Link>
141+
</div>
92142
</div>
93-
{!isLoading && !!allBenchmarkRuns?.runs.length && (
143+
{!isLoadTestsRoute && !isLoading && !!allBenchmarkRuns?.runs.length && (
94144
<div>
95145
<Select
96146
value={benchmarkRunId}
@@ -104,6 +154,23 @@ const Navbar = ({ urlPrefix }: ProvidedProps) => {
104154
</Select>
105155
</div>
106156
)}
157+
{isLoadTestsRoute &&
158+
!!loadTestTimestamp &&
159+
!isLoadingLoadTests &&
160+
loadTestOptions.length > 0 && (
161+
<div>
162+
<Select
163+
value={loadTestTimestamp}
164+
onChange={(e) => navigateToLoadTestRun(e.target.value)}
165+
>
166+
{loadTestOptions.map((option) => (
167+
<option key={option.value} value={option.value}>
168+
{option.label}
169+
</option>
170+
))}
171+
</Select>
172+
</div>
173+
)}
107174
</nav>
108175
);
109176
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { ReactNode } from "react";
2+
import clsx from "clsx";
3+
4+
export interface PercentileBarRow {
5+
label: string;
6+
numericValue: number;
7+
display: ReactNode;
8+
emphasized?: boolean;
9+
}
10+
11+
interface PercentileBarChartProps {
12+
rows: PercentileBarRow[];
13+
barColorClass?: string;
14+
}
15+
16+
const PercentileBarChart = ({
17+
rows,
18+
barColorClass = "bg-blue-500",
19+
}: PercentileBarChartProps) => {
20+
const max = rows.reduce((m, r) => Math.max(m, r.numericValue), 0);
21+
22+
return (
23+
<div className="flex flex-col gap-y-2">
24+
{rows.map((row) => {
25+
const pct = max === 0 ? 0 : (row.numericValue / max) * 100;
26+
return (
27+
<div
28+
key={row.label}
29+
className="grid grid-cols-[3rem_1fr_auto] items-center gap-x-3"
30+
>
31+
<div
32+
className={clsx(
33+
"text-xs text-slate-500 font-mono",
34+
row.emphasized && "font-semibold text-slate-900",
35+
)}
36+
>
37+
{row.label}
38+
</div>
39+
<div className="bg-slate-100 rounded h-2 relative overflow-hidden">
40+
<div
41+
className={clsx("h-2 rounded transition-all", barColorClass)}
42+
style={{ width: `${pct}%` }}
43+
/>
44+
</div>
45+
<div
46+
className={clsx(
47+
"text-sm text-slate-700 tabular-nums min-w-[6rem] text-right",
48+
row.emphasized && "font-semibold text-slate-900",
49+
)}
50+
>
51+
{row.display}
52+
</div>
53+
</div>
54+
);
55+
})}
56+
</div>
57+
);
58+
};
59+
60+
export default PercentileBarChart;

0 commit comments

Comments
 (0)