diff --git a/report/src/components/StatCard.tsx b/report/src/components/StatCard.tsx
index d268706..df64ecc 100644
--- a/report/src/components/StatCard.tsx
+++ b/report/src/components/StatCard.tsx
@@ -1,13 +1,20 @@
import { ReactNode } from "react";
import clsx from "clsx";
+import Tooltip from "./Tooltip";
interface StatCardProps {
title: string;
children: ReactNode;
className?: string;
+ titleTooltip?: ReactNode;
}
-const StatCard = ({ title, children, className }: StatCardProps) => {
+const StatCard = ({
+ title,
+ children,
+ className,
+ titleTooltip,
+}: StatCardProps) => {
return (
{
className,
)}
>
-
- {title}
+
+ {title}
+ {titleTooltip && (
+
+
+ i
+
+
+ )}
{children}
diff --git a/report/src/components/Tooltip.tsx b/report/src/components/Tooltip.tsx
index 453cb49..f57b258 100644
--- a/report/src/components/Tooltip.tsx
+++ b/report/src/components/Tooltip.tsx
@@ -3,7 +3,7 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
interface TooltipProps {
children: React.ReactNode;
- content: string;
+ content: React.ReactNode;
className?: string;
delayDuration?: number;
side?: "top" | "right" | "bottom" | "left";
diff --git a/report/src/pages/LoadTestDetail.tsx b/report/src/pages/LoadTestDetail.tsx
index d902355..23009a5 100644
--- a/report/src/pages/LoadTestDetail.tsx
+++ b/report/src/pages/LoadTestDetail.tsx
@@ -19,11 +19,24 @@ import {
formatTps,
} from "../utils/formatters";
import {
+ BlockRange,
FlashblocksLatencyStats,
LatencyStats,
LoadTestResult,
+ ObservedWindowMetrics,
+ TailMetrics,
} from "../types";
+const formatBlockRange = (range: BlockRange): string => {
+ if (
+ typeof range.first_block === "number" &&
+ typeof range.last_block === "number"
+ ) {
+ return `${range.first_block.toLocaleString()} → ${range.last_block.toLocaleString()}`;
+ }
+ return "No confirmed transactions";
+};
+
const buildLatencyRows = (
stats: LatencyStats | FlashblocksLatencyStats,
): PercentileBarRow[] => {
@@ -75,7 +88,7 @@ const buildLatencyRows = (
return rows;
};
-const SwapsPerSecondHero = ({ tps }: { tps: number }) => (
+const SwapsPerSecondHero = ({ tps, label }: { tps: number; label: string }) => (
{tps.toLocaleString(undefined, {
@@ -83,72 +96,329 @@ const SwapsPerSecondHero = ({ tps }: { tps: number }) => (
maximumFractionDigits: 1,
})}
- Swaps/s
+ {label}
);
-const SummarySection = ({ result }: { result: LoadTestResult }) => {
- const submitted = result.throughput.total_submitted;
- const confirmed = result.throughput.total_confirmed;
- const failed = result.throughput.total_failed;
- const reverted = result.throughput.total_reverted;
- const blockRange = result.block_range;
- const hasConfirmedBlockRange =
- typeof blockRange?.first_block === "number" &&
- typeof blockRange.last_block === "number";
+// The full observed-window block range, matching the CLI's display:
+// `first_block ..= first_block + expected_block_count - 1`. The
+// `window.block_range` field is the range of blocks that actually contained
+// confirmed test txs and is typically smaller — surfaced as a hint.
+const formatObservedWindowRange = (
+ window: ObservedWindowMetrics,
+): { value: string; hint: string } | null => {
+ const first = window.block_range.first_block;
+ if (typeof first !== "number" || window.expected_block_count === 0) {
+ return null;
+ }
+ const end = first + window.expected_block_count - 1;
+ const confirmedCount = window.block_range.block_count;
+ return {
+ value: `${first.toLocaleString()} → ${end.toLocaleString()}`,
+ hint: `${window.expected_block_count.toLocaleString()} blocks · txs landed in ${confirmedCount.toLocaleString()}`,
+ };
+};
+
+const OBSERVED_WINDOW_TOOLTIP = (
+
+ The clean, first portion of the run, sized to the configured duration. This
+ mirrors what you witness watching the chain live: sustained TPS over a
+ period of time. Use this against OKRs like “hit 3k swaps/s on the
+ chain.”
+
+);
+
+const TAIL_INCLUSION_TOOLTIP = (
+
+ Txs that landed in blocks past the observed window. Including them in the
+ headline would lower TPS and raise block / FB latency, but the data is
+ critical: it surfaces where inclusion-side optimization is still needed.
+
+);
+
+const ObservedWindowSummary = ({
+ window,
+}: {
+ window: ObservedWindowMetrics;
+}) => {
+ const blockRange = window.block_range;
+ const windowRange = formatObservedWindowRange(window);
return (
-
+
-
-
- {reverted > 0 && (
+
+
+ {windowRange ? (
+ ) : (
+ blockRange && (
+
+ )
)}
-
-
-
-
- {blockRange && (
+
+
+ );
+};
+
+const TailSection = ({
+ tail,
+ totalConfirmed,
+}: {
+ tail: TailMetrics;
+ totalConfirmed: number;
+}) => {
+ const blockRange = tail.block_range;
+ const hasReceiptDelay =
+ tail.block_receipt_delay &&
+ durationToNanos(tail.block_receipt_delay.max) > 0;
+
+ const timePastRows = useMemo(
+ () => buildLatencyRows(tail.time_past_observed_window),
+ [tail.time_past_observed_window],
+ );
+ const blockLatencyRows = useMemo(
+ () => buildLatencyRows(tail.block_latency),
+ [tail.block_latency],
+ );
+ const receiptDelayRows = useMemo(
+ () => (hasReceiptDelay ? buildLatencyRows(tail.block_receipt_delay) : []),
+ [tail.block_receipt_delay, hasReceiptDelay],
+ );
+ const flashblocksRows = useMemo(
+ () => buildLatencyRows(tail.flashblocks_latency),
+ [tail.flashblocks_latency],
+ );
+
+ if (tail.count === 0) {
+ return (
+
+
+ No transactions landed past the observed window
+ {typeof tail.observed_window_end_block === "number" && (
+ <>
+ {" "}
+ (boundary: block {tail.observed_window_end_block.toLocaleString()}
+ )
+ >
+ )}
+ .
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {typeof tail.observed_window_end_block === "number" && (
+
+ )}
+ {blockRange && (
+
+ )}
+
+
+
+
+ Time past observed window
+
+
+
+
+
+
+ Block latency (tail)
+
+
+
+
+ {hasReceiptDelay && (
+
+
+ Block receipt delay (tail)
+
+
+
)}
-
+
+
+
+ Flashblocks latency (tail) ·{" "}
+ {tail.flashblocks_latency.count.toLocaleString()} samples
+
+
+
+
);
};
+const FullRunBaselineSection = ({ result }: { result: LoadTestResult }) => {
+ const submitted = result.throughput.total_submitted;
+ const confirmed = result.throughput.total_confirmed;
+ const failed = result.throughput.total_failed;
+ const reverted = result.throughput.total_reverted;
+ const blockRange = result.block_range;
+
+ const blockLatencyRows = useMemo(
+ () => buildLatencyRows(result.block_latency),
+ [result.block_latency],
+ );
+ const flashblocksRows = useMemo(
+ () => buildLatencyRows(result.flashblocks_latency),
+ [result.flashblocks_latency],
+ );
+ const receiptDelayRows = useMemo(
+ () =>
+ result.block_receipt_delay
+ ? buildLatencyRows(result.block_receipt_delay)
+ : [],
+ [result.block_receipt_delay],
+ );
+
+ return (
+
+
+ Full-run baseline (observed window + tail combined)
+
+
+
+ Full-run averages dilute the clean reporting window with tail
+ stragglers. Use the observed-window numbers above for headline
+ comparisons; this section is included for completeness.
+
+
+
+
+
+
+
+ {reverted > 0 && (
+
+ )}
+
+
+
+
+ {blockRange && (
+
+ )}
+
+
+
+
+ Block latency (full run)
+
+
+
+
+ {result.block_receipt_delay && (
+
+
+ Block receipt delay (full run)
+
+
+
+ )}
+
+
+
+ Flashblocks latency (full run) ·{" "}
+ {result.flashblocks_latency.count.toLocaleString()} samples
+
+
+
+
+
+ );
+};
+
interface LoadTestReportContentProps {
result: LoadTestResult;
title: string;
@@ -165,14 +435,34 @@ export const LoadTestReportContent = ({
subtitle,
backLink,
}: LoadTestReportContentProps) => {
- const blockLatencyRows = useMemo(
- () => buildLatencyRows(result.block_latency),
- [result],
+ const observedWindow = result.observed_window;
+ const tail = result.tail ?? undefined;
+
+ // Headline numbers come from observed_window when available, otherwise fall
+ // back to the legacy full-run fields so older S3 runs still render.
+ const headlineTps = observedWindow?.tps ?? result.throughput.tps;
+ const headlineBlockLatency =
+ observedWindow?.block_latency ?? result.block_latency;
+ const headlineFlashblocksLatency =
+ observedWindow?.flashblocks_latency ?? result.flashblocks_latency;
+ const headlineReceiptDelay =
+ observedWindow?.block_receipt_delay ?? result.block_receipt_delay;
+
+ const headlineBlockLatencyRows = useMemo(
+ () => buildLatencyRows(headlineBlockLatency),
+ [headlineBlockLatency],
);
- const flashblocksLatencyRows = useMemo(
- () => buildLatencyRows(result.flashblocks_latency),
- [result],
+ const headlineFlashblocksRows = useMemo(
+ () => buildLatencyRows(headlineFlashblocksLatency),
+ [headlineFlashblocksLatency],
);
+ const headlineReceiptDelayRows = useMemo(
+ () => (headlineReceiptDelay ? buildLatencyRows(headlineReceiptDelay) : []),
+ [headlineReceiptDelay],
+ );
+
+ const headlineLabel = observedWindow ? "Observed-window TPS" : "Swaps/s";
+ const latencyScopeLabel = observedWindow ? "observed window" : "full run";
return (
<>
@@ -193,7 +483,7 @@ export const LoadTestReportContent = ({
-
+
{result.throughput_timeseries &&
result.throughput_timeseries.length > 1 && (
@@ -208,24 +498,42 @@ export const LoadTestReportContent = ({
{result.config && }
-
+ {observedWindow && }
-
+
+ {headlineReceiptDelay && (
+
+
+
+ )}
+
+ {tail && (
+
+ )}
+
{(() => {
const reverted = result.throughput.total_reverted;
@@ -254,6 +562,8 @@ export const LoadTestReportContent = ({
);
})()}
+
+ {observedWindow && }
>
);
};
diff --git a/report/src/types.ts b/report/src/types.ts
index 8f63426..62ad784 100644
--- a/report/src/types.ts
+++ b/report/src/types.ts
@@ -211,6 +211,8 @@ export interface LoadTestConfig {
transactions: Array<{ type: string; weight: number }>;
looper_contract: string | null;
swap_token_amount: string;
+ // Producer omits unless real-token setup is enabled; loosely typed.
+ real_token_setup?: Record | null;
}
/**
@@ -224,8 +226,46 @@ export interface ThroughputSample {
gps: number;
}
+/**
+ * Clean reporting window for a configured-duration run. Defined as the first
+ * `expected_block_count` blocks starting at `block_range.first_block`. TPS/GPS
+ * denominator is `duration` (= expected_block_count * BLOCK_INTERVAL), not the
+ * full wall-clock run, so headline numbers are not diluted by tail stragglers.
+ * Producer added in base/base#3358.
+ */
+export interface ObservedWindowMetrics {
+ expected_block_count: number;
+ block_range: BlockRange;
+ duration: RustDuration;
+ confirmed_count: number;
+ tps: number;
+ gps: number;
+ block_latency: LatencyStats;
+ block_receipt_delay: LatencyStats;
+ flashblocks_latency: FlashblocksLatencyStats;
+}
+
+/**
+ * Inclusion-delay tail: txs landing in blocks past the observed window
+ * (`block_number > observed_window_end_block`). `null` on continuous runs
+ * (no configured duration). Producer added in base/base#3358.
+ */
+export interface TailMetrics {
+ observed_window_end_block: number | null;
+ count: number;
+ confirmed_pct: number;
+ block_range: BlockRange;
+ time_past_observed_window: LatencyStats;
+ block_latency: LatencyStats;
+ block_receipt_delay: LatencyStats;
+ flashblocks_latency: FlashblocksLatencyStats;
+}
+
export interface LoadTestResult {
block_latency: LatencyStats;
+ // Submit-to-receipt-observation delay, full-run baseline. Optional for
+ // back-compat: producer added in base/base#3358.
+ block_receipt_delay?: LatencyStats;
flashblocks_latency: FlashblocksLatencyStats;
throughput: ThroughputStats;
throughput_percentiles: ThroughputPercentiles;
@@ -241,6 +281,12 @@ export interface LoadTestResult {
// Optional for back-compat: older runs predate this field. The summary
// section gates the block range stats on its presence.
block_range?: BlockRange;
+ // Observed reporting window (clean TPS / latency). Optional for back-compat:
+ // older S3 runs predate this; the page falls back to full-run fields.
+ observed_window?: ObservedWindowMetrics;
+ // Inclusion-delay tail. `null` on continuous runs; `undefined` on older
+ // runs that predate the field.
+ tail?: TailMetrics | null;
}
/**