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
93 changes: 93 additions & 0 deletions src/app/api/bench/[slug]/variant/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* On-demand bench variant for the client-side chain/region/kind tabs.
*
* GET /api/bench/<slug>/variant?chain=<c>&region=<r>&kind=<k>
*
* Returns the filtered Benchmark as JSON. Exists so the bench page can
* ship ONLY the aggregate view (the old embedded variant map multiplied
* every ISR regeneration by chains × regions × kinds full provider
* loads). Each (slug, filters) combo is deduped across users by the
* per-variant unstable_cache in the spec loader, so the first tab flip
* per minute pays one Prom roundtrip and everyone else gets cache hits.
*/

import { type NextRequest, NextResponse } from "next/server";
import { getBenchmark } from "@/data/benchmarks";
import { clientKey, rateLimit, tooManyRequests } from "@/lib/rate-limit";
import { SLUG_RE } from "@/lib/slug";

export const revalidate = 60;

type Params = { slug: string };

export async function GET(
req: NextRequest,
{ params }: { params: Promise<Params> },
) {
const rl = rateLimit(clientKey(req, "variant"), 120, 60);
if (!rl.ok) return tooManyRequests(rl.retryAfterSec);

const { slug } = await params;
if (!SLUG_RE.test(slug)) {
return new NextResponse("bad_input", {
status: 400,
headers: { "cache-control": "public, s-maxage=60" },
});
}
const aggregate = await getBenchmark(slug);
if (!aggregate || aggregate.editorialStatus !== "live") {
return new NextResponse("not found", {
status: 404,
headers: { "cache-control": "public, s-maxage=60" },
});
}

// Validate every filter against the declared dimensions and use the
// canonical value: these end up in PromQL label selectors downstream.
const url = new URL(req.url);
const filters: { chain?: string; region?: string; kind?: string } = {};
for (const dim of ["chain", "region", "kind"] as const) {
const raw = url.searchParams.get(dim)?.toLowerCase().trim();
if (!raw || raw === "all") continue;
const known = (aggregate.dimensions?.[dim] ?? []).find(
(d) => d.value.toLowerCase() === raw,
);
if (!known) {
return new NextResponse(`unknown ${dim}`, {
status: 400,
headers: { "cache-control": "public, s-maxage=60" },
});
}
filters[dim] = known.value;
}

const variant =
Object.keys(filters).length > 0
? ((await getBenchmark(slug, filters)) ?? aggregate)
: aggregate;

// Editorial copy resolves chain placeholders against the aggregate's
// stashes (computed unfiltered only); without this override a chain
// tab would surface raw `{{best_name:chain:X}}` strings.
const payload =
variant === aggregate
? variant
: {
...variant,
findings: aggregate.findings,
faq: aggregate.faq,
seoIntro: aggregate.seoIntro,
abstract: aggregate.abstract,
methodology: aggregate.methodology,
perChainExplainer: aggregate.perChainExplainer,
bestPerChain: aggregate.bestPerChain,
worstPerChain: aggregate.worstPerChain,
};

return NextResponse.json(payload, {
headers: {
"cache-control": "public, s-maxage=60, stale-while-revalidate=300",
Vary: "Accept-Encoding",
},
});
}
77 changes: 15 additions & 62 deletions src/app/benchmarks/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,68 +135,21 @@ export default async function BenchmarkPage({
const region = regionOptions[0]?.value ?? null;
const kind = kindOptions[0]?.value ?? null;

// Pre-fetch every (chain × region × kind) variant in parallel so client flips
// are zero round-trip. unstable_cache dedupes each (slug, filters) combo
// across users - first miss warms it, every later viewer gets it instant.
// `all` is the "no filter" sentinel - same as the unscoped fetch.
//
// SKIPPED during `next build`: a dimensioned bench multiplies its full
// provider fan-out by chains × regions (evm-quote-latency: 20 variants),
// which blew the 240s per-page budget and failed deploys. The first ISR
// revalidation (60s after deploy) runs off-band with no page budget and
// repopulates the full variant map; until then client flips fall back to
// the aggregate view, which benchmark-body already handles.
const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
const chainsForFetch = chainOptions.length > 0 ? chainOptions.map((c) => c.value) : [null];
const regionsForFetch = regionOptions.length > 0 ? regionOptions.map((r) => r.value) : [null];
const kindsForFetch = kindOptions.length > 0 ? kindOptions.map((k) => k.value) : [null];

const variantPairs = isBuildPhase
? []
: chainsForFetch.flatMap((c) =>
regionsForFetch.flatMap((r) =>
kindsForFetch.map((k) => [c, r, k] as const)
)
);
const [variantList, all] = await Promise.all([
Promise.all(
variantPairs.map(async ([c, r, k]) => {
const filters: { chain?: string; region?: string; kind?: string } = {};
if (c && c !== "all") filters.chain = c;
if (r && r !== "all") filters.region = r;
if (k && k !== "all") filters.kind = k;
const b = await getBenchmark(slug, filters);
return [variantKey(c, r, k), b ?? aggregate] as const;
})
),
getBenchmarks(),
]);
// Variants only contribute chart / leaderboard / extras to the displayed
// bench (those legitimately differ per (chain, region) filter). Editorial
// copy (findings, faq, seoIntro, abstract, methodology) is the SAME on
// every tab and only resolves chain placeholders against the aggregate's
// bestPerChain/worstPerChain stash (computed unfiltered only), so we
// override these fields onto every variant. Without this, switching to
// a chain tab surfaces raw `{{best_name:chain:X}}` strings.
const variants: Record<string, Benchmark> = Object.fromEntries(
variantList.map(([key, v]) => [
key,
v === aggregate
? v
: {
...v,
findings: aggregate.findings,
faq: aggregate.faq,
seoIntro: aggregate.seoIntro,
abstract: aggregate.abstract,
methodology: aggregate.methodology,
perChainExplainer: aggregate.perChainExplainer,
bestPerChain: aggregate.bestPerChain,
worstPerChain: aggregate.worstPerChain,
},
]),
);
const benchmark = variants[variantKey(chain, region, kind)] ?? aggregate;
// Variants (chain × region × kind) are NOT embedded anymore. The old
// pre-fetch awaited every variant (rpc-capabilities: 39 full provider
// loads) on EVERY ISR regeneration and shipped them all in the page
// payload — regenerations took 30-60 s, and any visitor landing on a
// blocking render path (post-deploy, cache eviction) ate that wait.
// BenchmarkBody now fetches a variant on demand from
// /api/bench/[slug]/variant when a tab is flipped (per-variant
// unstable_cache keeps that at one cheap Prom roundtrip per 60 s
// across all users), and renders the aggregate while it loads.
const all = await getBenchmarks();
const variants: Record<string, Benchmark> = {
[variantKey(chain, region, kind)]: aggregate,
[variantKey(null, null, null)]: aggregate,
};
const benchmark = aggregate;

const isDraft = benchmark.status === "draft";
const isAwaiting = isDraft && benchmark.editorialStatus === "live";
Expand Down
46 changes: 39 additions & 7 deletions src/components/benchmark-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,42 @@ export function BenchmarkBody({
const effectiveChain = chainOptions.length > 0 ? (chain ?? fallbackChain) : null;
const effectiveRegion = regionOptions.length > 0 ? (region ?? fallbackRegion) : null;
const effectiveKind = kindOptions.length > 0 ? (kind ?? fallbackKind) : null;
const benchmark =
variants[variantKey(effectiveChain, effectiveRegion, effectiveKind)] ??
variants[variantKey(null, null, null)] ??
Object.values(variants)[0];

// The page ships ONLY the aggregate view (embedding every variant made
// ISR regenerations take 30-60 s). Filtered variants are fetched here
// on demand; while one loads, the aggregate keeps rendering so the tab
// flip never blanks the page. Failed fetches keep the aggregate (the
// tab still works, numbers stay cross-dimension) and may retry on the
// next flip.
const [variantMap, setVariantMap] = useState<Record<string, Benchmark>>(variants);
const activeKey = variantKey(effectiveChain, effectiveRegion, effectiveKind);
const aggregateBench =
variants[variantKey(null, null, null)] ?? Object.values(variants)[0];
useEffect(() => {
if (variantMap[activeKey] || !aggregateBench) return;
const isAll = (v: string | null) => !v || v === "all";
if (isAll(effectiveChain) && isAll(effectiveRegion) && isAll(effectiveKind)) {
setVariantMap((m) => ({ ...m, [activeKey]: aggregateBench }));
return;
}
const qs = new URLSearchParams();
if (!isAll(effectiveChain)) qs.set("chain", effectiveChain!);
if (!isAll(effectiveRegion)) qs.set("region", effectiveRegion!);
if (!isAll(effectiveKind)) qs.set("kind", effectiveKind!);
let cancelled = false;
fetch(`/api/bench/${aggregateBench.slug}/variant?${qs.toString()}`)
.then((r) => (r.ok ? r.json() : null))
.then((v: Benchmark | null) => {
if (!cancelled && v) setVariantMap((m) => ({ ...m, [activeKey]: v }));
})
.catch(() => {});
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeKey]);

const benchmark = variantMap[activeKey] ?? aggregateBench;
if (!benchmark) return null;

// L1/L2 layer counts. When both > 0 the bench mixes L1 and L2 chains
Expand Down Expand Up @@ -309,7 +341,7 @@ export function BenchmarkBody({
.map((o) => [
o.value,
summarize(
variants[variantKey(effectiveChain, effectiveRegion, o.value)],
variantMap[variantKey(effectiveChain, effectiveRegion, o.value)],
),
])
.filter(([, v]) => v !== null) as [string, ChainMeta][]
Expand All @@ -326,7 +358,7 @@ export function BenchmarkBody({
chainOptions
.map((o) => [
o.value,
summarize(variants[variantKey(o.value, effectiveRegion, effectiveKind)]),
summarize(variantMap[variantKey(o.value, effectiveRegion, effectiveKind)]),
])
.filter(([, v]) => v !== null) as [string, ChainMeta][]
)}
Expand All @@ -342,7 +374,7 @@ export function BenchmarkBody({
regionOptions
.map((o) => [
o.value,
summarize(variants[variantKey(effectiveChain, o.value, effectiveKind)]),
summarize(variantMap[variantKey(effectiveChain, o.value, effectiveKind)]),
])
.filter(([, v]) => v !== null) as [string, ChainMeta][]
)}
Expand Down
Loading