From deb12973a7321ae1a08878cfd1b322d74d9addf9 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:02:21 +0200 Subject: [PATCH 1/2] share-card: fix ms->min unit flip; cap leaderboard rows; strip chart chrome from PNG screenshot --- .../benchmarks/[slug]/share-card/route.tsx | 24 ++++++++++++++++++- src/components/chart-export-button.tsx | 9 +++++-- src/components/ranked-bar-chart.tsx | 10 ++++++-- src/lib/format.ts | 6 +++++ 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/app/benchmarks/[slug]/share-card/route.tsx b/src/app/benchmarks/[slug]/share-card/route.tsx index b2b43d69..86e1963d 100644 --- a/src/app/benchmarks/[slug]/share-card/route.tsx +++ b/src/app/benchmarks/[slug]/share-card/route.tsx @@ -798,7 +798,14 @@ async function renderLeaderboard( colors: Map, chainLabel?: string | null ) { - const sorted = sortByP50(benchmark); + const allSorted = sortByP50(benchmark); + // Hard row cap so the last row can't collide with the CardFooter + // border in the 630 px canvas (observed on wormhole-vaa-latency with + // 14 chains: Moonbeam overprinted the "OPENCHAINBENCH.COM · No 101" + // divider). If truncated, an "and N more" line is appended below. + const MAX_ROWS = 10; + const sorted = allSorted.slice(0, MAX_ROWS); + const truncatedCount = Math.max(0, allSorted.length - sorted.length); const maxP50 = Math.max(...sorted.map((r) => r.ms.p50)) || 1; const subtitleLB = `Ranked by p50 · ${benchmark.metric}.`; // Scale down type + spacing when the roster is dense OR the title is @@ -958,6 +965,21 @@ async function renderLeaderboard( ); })} + {truncatedCount > 0 && ( +
+ and {truncatedCount} more on openchainbench.com +
+ )} diff --git a/src/components/chart-export-button.tsx b/src/components/chart-export-button.tsx index 2ba377da..f0845001 100644 --- a/src/components/chart-export-button.tsx +++ b/src/components/chart-export-button.tsx @@ -81,10 +81,15 @@ export function ChartExportButton({ cacheBust: true, skipFonts: false, // Drop the export button itself from the capture — no point - // baking the "Copy" pill into every screenshot. + // baking the "Copy" pill into every screenshot. Also drop any + // element marked with data-chart-export-omit (e.g. the chart's + // header metadata row and the Top-N selector) so the exported + // PNG focuses on the plot itself, not the interactive UI chrome. filter: (node) => { if (!(node instanceof HTMLElement)) return true; - return !node.dataset.chartExportButton; + if (node.dataset.chartExportButton) return false; + if (node.dataset.chartExportOmit) return false; + return true; }, style: { boxShadow: "none" }, }); diff --git a/src/components/ranked-bar-chart.tsx b/src/components/ranked-bar-chart.tsx index 69a314ba..3eeb5a53 100644 --- a/src/components/ranked-bar-chart.tsx +++ b/src/components/ranked-bar-chart.tsx @@ -130,7 +130,10 @@ export function RankedBarChart({ return (
-
+

{benchmark.metric} · last 24 hours @@ -152,7 +155,10 @@ export function RankedBarChart({ {headerActions}

-
+
    diff --git a/src/lib/format.ts b/src/lib/format.ts index a55fd62d..022ac628 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -107,6 +107,12 @@ export function fmtUnit(value: number, unit: string) { if (abs < 1000) return `$${value.toLocaleString("en-US", { maximumFractionDigits: 2 })}`; return `$${formatCompactCount(value)}`; } + // Auto-flip to minutes at 60 s and to seconds at 1 s, mirroring the + // suffix returned by unitSuffix(). Without the min flip, wormhole- + // vaa-latency ranking cards rendered "858.79 min" for what was really + // 14.3 min (858.79 s), because fmtValue stripped the trailing " s" that + // fmtUnit produced but unitSuffix independently returned " min". + if (value >= 60000) return `${(value / 60000).toFixed(1)} min`; if (value >= 1000) return `${(value / 1000).toFixed(2)} s`; // Sub-millisecond values keep one decimal: pm-data-freshness's 0.5 ms // anchor rendered "1 ms", contradicting the 0.5 published by the From 5018d7df25135a9e1f9702e625195834af0e65b2 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:54:55 +0200 Subject: [PATCH 2/2] lint: unblock react-hooks/set-state-in-effect + rules-of-hooks on pre-existing files --- src/components/benchmark-body.tsx | 31 +++++++++++++++++------------ src/components/live/live-number.tsx | 25 +++++++++++++++-------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/components/benchmark-body.tsx b/src/components/benchmark-body.tsx index 1208363e..2ae3579c 100644 --- a/src/components/benchmark-body.tsx +++ b/src/components/benchmark-body.tsx @@ -603,25 +603,18 @@ export function BenchmarkBody({ !hlArchiveCache[longRangeKey] ); - if (!benchmark || !viewBenchmark) return null; - - const pendingCls = variantPending - ? " opacity-40 animate-pulse pointer-events-none" - : ""; - const pendingLabel = [effectiveChain, effectiveRegion, effectiveKind] - .filter((v): v is string => !!v && v !== "all") - .join(" · "); - const isDraft = viewBenchmark.status === "draft"; - const { fieldMin, fieldMedian, fieldMax, tailMin, tailMax, tailSpread } = - computeFieldStats(viewBenchmark.results); - const activePanel = - benchmark.metricPanels?.find((p) => p.id === activePanelId) ?? null; // Value views (ranked bars) swap each provider's headline p50 for the // active panel's scalar so the size tabs work on the default chart, // not only on the timeseries view. Providers the panel has no value // for (book could not fill the tier) drop out of the ranking, which // is the skipped-not-extrapolated rule made visible. + // + // Kept ABOVE the `if (!benchmark || !viewBenchmark) return null` early + // return so this useMemo is called on every render (rules-of-hooks). + const activePanel = + benchmark?.metricPanels?.find((p) => p.id === activePanelId) ?? null; const panelViewBenchmark = useMemo(() => { + if (!viewBenchmark) return null; if (!activePanel) return viewBenchmark; const vals = activePanel.values ?? {}; return { @@ -634,6 +627,18 @@ export function BenchmarkBody({ }; }, [viewBenchmark, activePanel]); + if (!benchmark || !viewBenchmark || !panelViewBenchmark) return null; + + const pendingCls = variantPending + ? " opacity-40 animate-pulse pointer-events-none" + : ""; + const pendingLabel = [effectiveChain, effectiveRegion, effectiveKind] + .filter((v): v is string => !!v && v !== "all") + .join(" · "); + const isDraft = viewBenchmark.status === "draft"; + const { fieldMin, fieldMedian, fieldMax, tailMin, tailMax, tailSpread } = + computeFieldStats(viewBenchmark.results); + const sharedHeaderActions = ( <> diff --git a/src/components/live/live-number.tsx b/src/components/live/live-number.tsx index 6508076c..c7ebd685 100644 --- a/src/components/live/live-number.tsx +++ b/src/components/live/live-number.tsx @@ -55,21 +55,30 @@ export function LiveNumber({ const prevRef = useRef<{ value: number; ts: number } | null>(null); const rafRef = useRef(null); + // Render-time reset when the incoming value exceeds the ceiling AND + // the displayed value is already poisoned. React allows setState during + // render on the same component, and moving this out of the effect + // avoids the react-hooks/set-state-in-effect rule while keeping the + // same "flush poison before a healthy push seeds fresh" behavior. + if ( + value != null && + maxValue != null && + value > maxValue && + display != null && + display > maxValue + ) { + setDisplay(undefined); + } + useEffect(() => { if (value == null || !Number.isFinite(value)) return; if (maxValue != null && value > maxValue) { - // Flush a poisoned lastRef AND the displayed value so a healthy - // push can seed the ticker fresh on the next tick. Without - // resetting `display`, the monotonic guard below would keep - // rejecting the healthy value as smaller than the garbage still - // shown to the user. + // Flush a poisoned lastRef so a healthy push can seed the ticker + // fresh on the next tick. if (lastRef.current && lastRef.current.value > maxValue) { lastRef.current = null; prevRef.current = null; } - setDisplay((d) => - d != null && maxValue != null && d > maxValue ? undefined : d, - ); return; } const now = performance.now();