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
35 changes: 25 additions & 10 deletions src/components/ledger-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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/<slug> 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();
Expand Down
54 changes: 52 additions & 2 deletions src/components/ranked-bar-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand All @@ -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<number | null>(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
Expand Down Expand Up @@ -107,6 +131,32 @@ export function RankedBarChart({
{headerActions}
</div>
</div>
{cohortSize > 10 && (
<div className="mb-4 flex flex-wrap items-center justify-end gap-1">
<span className="mr-2 text-[10px] uppercase tracking-[0.16em] text-ink-faint">
Show
</span>
{([5, 10, 20, null] as const).map((n) => {
const active = topN === n;
const label = n == null ? "All" : `Top ${n}`;
return (
<button
key={String(n)}
type="button"
onClick={() => setTopN(n)}
className={`rounded-md border px-2 py-1 text-[11px] font-sans font-medium uppercase tracking-[0.1em] transition-all ${
active
? "border-ink bg-ink text-paper"
: "border-ink/15 bg-paper text-ink hover:border-ink/40"
}`}
aria-pressed={active}
>
{label}
</button>
);
})}
</div>
)}
<ul className="space-y-2">
{rows.map((r) => {
const isOff = excluded.has(r.slug);
Expand Down
43 changes: 11 additions & 32 deletions src/components/site-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Menu, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { SiteLogoSwitcher } from "@/components/site-logo-switcher";
import { ThemeToggle } from "@/components/theme-toggle";

Expand All @@ -24,10 +24,6 @@ const NAV = [

export function SiteHeader() {
const [open, setOpen] = useState(false);
const headerRef = useRef<HTMLDivElement>(null);
// Initial estimate matches the rendered header height so the spacer
// doesn't jump on first paint; ResizeObserver refines it after mount.
const [headerH, setHeaderH] = useState(64);

// Close the mobile menu when the viewport crosses md so the dropdown
// doesn't stick around as the desktop nav reappears.
Expand All @@ -41,29 +37,16 @@ export function SiteHeader() {
return () => mql.removeEventListener("change", onChange);
}, [open]);

// Measure the fixed header so we can reserve the same height in the
// flow with a spacer. ResizeObserver picks up viewport resizes.
useEffect(() => {
const el = headerRef.current;
if (!el) return;
const update = () => setHeaderH(el.offsetHeight);
update();
const ro = new ResizeObserver(update);
ro.observe(el);
return () => ro.disconnect();
}, []);

return (
<>
<div
ref={headerRef}
// position: fixed (not sticky) — sticky is flaky on iOS Safari when
// the URL bar animates; fixed keeps the navbar glued. No ancestor
// may carry overflow: clip / transform / will-change or fixed
// positions relative to that ancestor instead of the viewport
// (Telegram WKWebView gap symptom). Verified clean as of dev.
className="fixed top-0 left-0 right-0 z-50 flex flex-col font-sans bg-surface"
>
<div
// position: sticky (not fixed) — WebKit iOS 26 has a known bug where
// position: fixed elements jitter 10-24 px on scroll direction change
// (bugs.webkit.org/297779). The bug affects Safari, Chrome AND every
// WKWebView host (Telegram in-app browser is the symptom we hit).
// sticky uses a different render path that doesn't trip the bug, and
// since it stays in normal flow there's no spacer div to keep in sync.
className="sticky top-0 z-50 flex flex-col font-sans bg-surface"
>
<header className="border-b border-rule py-4 md:py-5 px-4 sm:px-6 shrink-0 text-sm bg-surface relative">
<div className="max-w-[1400px] mx-auto flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
Expand Down Expand Up @@ -148,10 +131,6 @@ export function SiteHeader() {
</nav>
)}
</header>
</div>
{/* Spacer reserves the fixed header's height in the document flow
so page content does not start under it. */}
<div aria-hidden style={{ height: headerH }} />
</>
</div>
);
}
Loading