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
92 changes: 92 additions & 0 deletions src/lib/bench-blob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Per-bench + per-variant CDN reader (Phase 3 of the SRH-elimination).
*
* The materialize worker publishes:
* - `bench-aggregate/latest.json` (all benches, homepage)
* - `bench-aggregate/benches/<slug>.json` (one unfiltered bench)
* - `bench-aggregate/variants/<slug>/<sig>.json` (one filtered variant)
*
* Aggregate is consumed by `src/lib/aggregate-blob.ts` (Phase 2). This
* module handles the two per-bench shapes — replaces the SRH GETs that
* `loadBenchmarkUnfilteredCached` and `loadBenchmarkFiltered` used to
* make in `src/lib/spec.ts`.
*
* Failure model: any error (fetch throw, 4xx/5xx, malformed JSON, schema
* mismatch) resolves to `null` so callers fall through to the Redis
* path. Never throws.
*/

import type { Benchmark } from "@/types/benchmark";

const DEFAULT_BASE_URL = "https://kv.openchainbench.com/aggregate";
const FETCH_TIMEOUT_MS = 5_000;

type BenchEnvelope = {
v: number;
builtAt: number;
slug: string;
bench: Benchmark;
};

type VariantEnvelope = BenchEnvelope & { sig: string };

function baseUrl(): string {
return process.env.AGGREGATE_BLOB_BASE_URL || DEFAULT_BASE_URL;
}

function isBenchEnvelope(x: unknown): x is BenchEnvelope {
if (typeof x !== "object" || x === null) return false;
const o = x as Record<string, unknown>;
return (
typeof o.v === "number" &&
typeof o.builtAt === "number" &&
typeof o.slug === "string" &&
typeof o.bench === "object" &&
o.bench !== null
);
}

async function fetchJson(url: string): Promise<unknown | null> {
try {
const res = await fetch(url, {
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
cache: "no-store",
});
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}

/**
* Fetch a single unfiltered bench from the CDN. Returns the raw
* `Benchmark` from the store (no editorial overlay applied — caller
* layers that on top so a stale blob doesn't outlive a spec edit).
*/
export async function loadBenchFromBlob(
slug: string,
): Promise<Benchmark | null> {
const url = `${baseUrl()}/benches/${encodeURIComponent(slug)}.json`;
const raw = await fetchJson(url);
if (!isBenchEnvelope(raw) || raw.v !== 1 || raw.slug !== slug) return null;
return raw.bench;
}

/**
* Fetch a single filtered variant from the CDN. `sig` is the filterSig
* string (empty for unfiltered — callers with sig="" should use
* `loadBenchFromBlob` instead).
*/
export async function loadVariantFromBlob(
slug: string,
sig: string,
): Promise<Benchmark | null> {
if (!sig) return null;
const url = `${baseUrl()}/variants/${encodeURIComponent(slug)}/${encodeURIComponent(sig)}.json`;
const raw = await fetchJson(url);
if (!isBenchEnvelope(raw) || raw.v !== 1 || raw.slug !== slug) return null;
const env = raw as VariantEnvelope;
if (env.sig !== sig) return null;
return env.bench;
}
24 changes: 14 additions & 10 deletions src/lib/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "@/lib/materialize/load";
import { readMaterialized } from "@/lib/materialize/store";
import { loadAggregateFromBlob } from "@/lib/aggregate-blob";
import { loadBenchFromBlob, loadVariantFromBlob } from "@/lib/bench-blob";

export type { Spec } from "@/lib/spec-schema";
export type { BenchmarkFilters } from "@/lib/materialize/load";
Expand Down Expand Up @@ -200,11 +201,13 @@ const loadBenchmarkUnfilteredCached = unstable_cache(
const specs = await loadSpecs();
const spec = specs.find((s) => s.slug === slug);
if (!spec) return undefined;
// Blob-only: serve whatever the worker last published. Missing or
// unparseable blobs fall through to the aggregator's draft
// placeholder instead of fanning out a Prom build at render time —
// that fan-out is what saturated Prom's query queue under Vercel
// ISR re-render bursts.
// Fast path: per-bench blob written by the materialize worker
// after every tier A sweep. One CDN fetch (~20 ms edge, ~1 s cold)
// replaces the 2-3 SRH GETs this used to run. See bench-blob.ts
// and worker/publish-aggregate.ts.
const fromBlob = await loadBenchFromBlob(slug);
if (fromBlob) return slimBenchmarkForCache(overlayEditorial(fromBlob, spec));
// Fallback: Redis-via-SRH. Kept until Phase 3 fully retires SRH.
const stored = await benchFromStore(slug, "");
if (stored) return slimBenchmarkForCache(overlayEditorial(stored, spec));
if (spec.status === "live") {
Expand Down Expand Up @@ -579,11 +582,12 @@ const loadBenchmarkFiltered = unstable_cache(
const specs = await loadSpecs();
const spec = specs.find((s) => s.slug === slug);
if (!spec) return undefined;
// Blob-only: variant blobs are published by the worker's tier-B
// sweep. A missing variant blob means the worker has not covered
// this filter combination yet — return undefined so the caller can
// fall back to the unfiltered "All" view rather than spinning up a
// render-time Prom build.
// Fast path: per-variant blob written by the worker's tier B
// sweep. Missing blob → this filter combo hasn't been covered yet
// (or the tier B has never run since worker start) — fall through
// to Redis-via-SRH.
const fromBlob = await loadVariantFromBlob(slug, sig);
if (fromBlob) return slimBenchmarkForCache(overlayEditorial(fromBlob, spec));
const stored = await benchFromStore(slug, sig);
if (stored) return slimBenchmarkForCache(overlayEditorial(stored, spec));
return undefined;
Expand Down
Loading