diff --git a/src/components/ledger-table.tsx b/src/components/ledger-table.tsx index cdefb410..8d4e4e5e 100644 --- a/src/components/ledger-table.tsx +++ b/src/components/ledger-table.tsx @@ -32,16 +32,31 @@ export function LedgerTable({ benchmark }: Props) { // "-" for providers that don't declare it). Used by Solana-native benches // where slot_delta is the canonical metric and ms is wall-clock derived. const hasSlots = results.some((r) => r.slots != null); - // Sort by p50 then push unavailable providers to the bottom. Without - // the secondary sort they'd land at rank #1 on lower-is-better benches - // because their placeholder p50 is 0 - which is what made 0slot, then - // cardano, show up as "fastest" in the recent SERP screenshots. - const sorted = [...results].sort((a, b) => { - const aOff = a.availability === "unavailable" ? 1 : 0; - const bOff = b.availability === "unavailable" ? 1 : 0; - if (aOff !== bOff) return aOff - bOff; - return benchmark.higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50; - }); + // Drop unscored providers (availability=unavailable AND p50=0). They + // stay in the underlying spec so /products/ pages still resolve + // and SEO coverage holds, but they're noise in a "ranked by performance" + // ledger. Mirrors the filter the ranked-bar chart applies above so the + // two surfaces tell the same story. + // Sort by p50; unavailable rows with non-zero p50 (rare, e.g. cached + // values served while a brief Prom outage was recovering) still get + // pushed to the bottom. + // Drop rows with no headline-metric value. Catches three flavours + // collapsed into a single check: + // 1. true unavailable (Prom returned nothing, augmented as zero) + // 2. backstop-promoted rows that flipped availability=live based on + // companion-panel data but still have p50=0 on the headline + // 3. rare genuine zero (e.g. a builder that levied zero fees in the + // 24h window). Edge case — acceptable cost to keep the leaderboard + // free of "0% / -100% Δ field" rows that read as broken to the + // first-time visitor. + // The chart's panel tabs still surface those providers via + // seriesByProvider when the reader switches metric, so coverage isn't + // lost — only the noisy ledger rows are pruned. + const sorted = [...results] + .filter((r) => r.ms.p50 > 0 || r.ms.p90 > 0 || r.ms.p99 > 0) + .sort((a, b) => + benchmark.higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50 + ); const colors = useMemo(() => buildProviderColors(results), [results]); const allSeries = Object.values(extras.series24h).flat(); diff --git a/src/components/ranked-bar-chart.tsx b/src/components/ranked-bar-chart.tsx index 636a48d7..05366f96 100644 --- a/src/components/ranked-bar-chart.tsx +++ b/src/components/ranked-bar-chart.tsx @@ -42,8 +42,20 @@ export function RankedBarChart({ [benchmark.results] ); - const rows = useMemo(() => { - const sorted = [...benchmark.results].sort((a, b) => + // Unscored providers (availability=unavailable AND no p50) are dropped + // up front: they're SEO-relevant for /products page coverage but they + // are pure visual noise on a "ranked by performance" view. The + // ledger below mirrors the same filter so the two surfaces tell the + // same story. + const allRows = useMemo(() => { + // See ledger-table for the rationale on this filter — same intent + // here: drop rows whose headline metric is zero so the ranked-bar + // surface doesn't open with a long stack of empty bars that tie at + // the bottom (or, when lower-is-better, falsely lead the ranking). + const scored = benchmark.results.filter( + (r) => r.ms.p50 > 0 || r.ms.p90 > 0 || r.ms.p99 > 0 + ); + const sorted = scored.sort((a, b) => benchmark.higherIsBetter ? b.ms.p50 - a.ms.p50 : a.ms.p50 - b.ms.p50 ); return sorted.map((r) => ({ @@ -57,6 +69,18 @@ export function RankedBarChart({ })); }, [benchmark, colors]); + // Top-N selector — sized off the registered cohort, mirroring the + // time-series chart. When the bench ships more than 10 providers the + // toolbar exposes Top 5 / 10 / 20 / All so a cluttered leaderboard + // can be focused without losing the option to widen. + const cohortSize = benchmark.results.length; + const TOP_N_DEFAULT = cohortSize > 20 ? 20 : null; + const [topN, setTopN] = useState(TOP_N_DEFAULT); + const rows = useMemo(() => { + if (topN == null) return allRows; + return allRows.slice(0, topN); + }, [allRows, topN]); + // The bar scale recomputes from visible rows only - excluding the // tail outliers gives the remaining bars more room to breathe. const visibleValues = rows @@ -107,6 +131,32 @@ export function RankedBarChart({ {headerActions} + {cohortSize > 10 && ( +
+ + Show + + {([5, 10, 20, null] as const).map((n) => { + const active = topN === n; + const label = n == null ? "All" : `Top ${n}`; + return ( + + ); + })} +
+ )}