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
1 change: 1 addition & 0 deletions src/app/benchmarks/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,7 @@ export default async function BenchmarkPage({
regionOptions={regionOptions}
kindOptions={kindOptions}
venueOptions={venueOptions}
venuesForChain={aggregate.extras?.venuesForChain}
initialChain={chain ?? null}
initialRegion={region ?? null}
initialKind={kind ?? null}
Expand Down
47 changes: 39 additions & 8 deletions src/components/benchmark-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
regionOptions,
kindOptions = [],
venueOptions = [],
venuesForChain,
initialChain,
initialRegion,
initialKind = null,
Expand All @@ -153,6 +154,9 @@
regionOptions: ChainOption[];
kindOptions?: ChainOption[];
venueOptions?: ChainOption[];
/** Per-chain venue availability map. When present, venue tabs are filtered
* to only show venues that have data for the currently selected chain. */
venuesForChain?: Record<string, string[]>;
initialChain: string | null;
initialRegion: string | null;
initialKind?: string | null;
Expand Down Expand Up @@ -218,6 +222,33 @@
const effectiveKind = kindOptions.length > 0 ? (kind ?? fallbackKind) : null;
const effectiveVenue = venueOptions.length > 0 ? (venue ?? fallbackVenue) : null;

// Cross-dimension filtering: hide venue tabs with no data for the active
// chain, and hide chain tabs with no data for the active venue.
const filteredVenueOptions = useMemo(() => {
if (!venuesForChain || !effectiveChain || effectiveChain === "all") return venueOptions;
const valid = venuesForChain[effectiveChain];
if (!valid || valid.length === 0) return venueOptions;
const validSet = new Set(valid);
return venueOptions.filter((v) => v.value === "all" || validSet.has(v.value));
}, [venueOptions, venuesForChain, effectiveChain]);

const chainsForVenue = useMemo(() => {
if (!venuesForChain) return undefined;
const out: Record<string, string[]> = {};
for (const [c, venues] of Object.entries(venuesForChain)) {
for (const v of venues) (out[v] ??= []).push(c);
}
return out;
}, [venuesForChain]);

const filteredChainOptions = useMemo(() => {
if (!chainsForVenue || !effectiveVenue || effectiveVenue === "all") return chainOptions;
const valid = chainsForVenue[effectiveVenue];
if (!valid || valid.length === 0) return chainOptions;
const validSet = new Set(valid);
return chainOptions.filter((c) => c.value === "all" || validSet.has(c.value));
}, [chainOptions, chainsForVenue, effectiveVenue]);

// 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
Expand Down Expand Up @@ -528,7 +559,7 @@
// 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.
const panelViewBenchmark = useMemo(() => {

Check failure on line 562 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions / check

React Hook "useMemo" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
if (!activePanel) return viewBenchmark;
const vals = activePanel.values ?? {};
return {
Expand All @@ -544,10 +575,10 @@
return (
<>
{(hasLayerSplit ||
chainOptions.length > 0 ||
filteredChainOptions.length > 0 ||
regionOptions.length > 0 ||
kindOptions.length > 0 ||
venueOptions.length > 0) && (
filteredVenueOptions.length > 0) && (
<div className="mt-8 space-y-3">
{hasLayerSplit && (
<DimensionRow
Expand All @@ -560,14 +591,14 @@
onSelect={(v) => setLayer(v as ProviderLayer)}
/>
)}
{venueOptions.length > 0 && (
{filteredVenueOptions.length > 0 && (
<DimensionRow
label="Venue"
options={venueOptions}
options={filteredVenueOptions}
selected={venue ?? fallbackVenue}
onSelect={setVenue}
metaByValue={Object.fromEntries(
venueOptions
filteredVenueOptions
.map((o) => [
o.value,
summarize(
Expand Down Expand Up @@ -596,14 +627,14 @@
)}
/>
)}
{chainOptions.length > 0 && (
{filteredChainOptions.length > 0 && (
<DimensionRow
label="Chain"
options={chainOptions}
options={filteredChainOptions}
selected={chain ?? fallbackChain}
onSelect={setChain}
metaByValue={Object.fromEntries(
chainOptions
filteredChainOptions
.map((o) => [
o.value,
summarize(variantMap[variantKey(o.value, effectiveRegion, effectiveKind, effectiveVenue)]),
Expand Down
18 changes: 16 additions & 2 deletions src/lib/materialize/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ export async function specToBenchmark(
if (cellRankResult?.venuesWithData?.length) {
live.extras.venuesWithData = cellRankResult.venuesWithData;
}
if (cellRankResult?.venuesForChain && Object.keys(cellRankResult.venuesForChain).length > 0) {
live.extras.venuesForChain = cellRankResult.venuesForChain;
}

// Per-provider sample-health classification. When the spec declares
// expected_n, every live provider gets `dataConfidence` (healthy /
Expand Down Expand Up @@ -441,6 +444,7 @@ export function propagateNullsToCoarser(
type CellRankResult = {
ranks: Record<string, CellRankEntry[]>;
venuesWithData: string[];
venuesForChain: Record<string, string[]>;
};

async function tryLoadCellRanks(
Expand All @@ -466,6 +470,7 @@ async function tryLoadCellRanks(
.map((v) => [v.value.toLowerCase(), v.value] as const),
);
const venuesWithDataSet = new Set<string>();
const venuesForChainMap = new Map<string, Set<string>>();
const chainByLower = new Map(
(spec.dimensions?.chain ?? [])
.filter((c) => c.value !== "all")
Expand Down Expand Up @@ -497,7 +502,14 @@ async function tryLoadCellRanks(
if (!Number.isFinite(v) || v <= 0) continue;
if (venueByLower.size > 0 && sample.metric.venue) {
const venue = venueByLower.get(sample.metric.venue.toLowerCase());
if (venue) venuesWithDataSet.add(venue);
if (venue) {
venuesWithDataSet.add(venue);
if (chain) {
const set = venuesForChainMap.get(chain) ?? new Set<string>();
set.add(venue);
venuesForChainMap.set(chain, set);
}
}
}
const key = `${chain ?? "all"}|${region ?? "all"}`;
const cell = acc.get(key) ?? new Map<string, number[]>();
Expand Down Expand Up @@ -571,7 +583,9 @@ async function tryLoadCellRanks(
(region) => `all|${region}`,
);
}
return { ranks: out, venuesWithData: [...venuesWithDataSet] };
const venuesForChain: Record<string, string[]> = {};
for (const [c, set] of venuesForChainMap) venuesForChain[c] = [...set];
return { ranks: out, venuesWithData: [...venuesWithDataSet], venuesForChain };
} catch (e) {
console.warn(
`cellRanks skip: ${spec.slug} matrix query failed: ${e instanceof Error ? e.message : String(e)}`,
Expand Down
1 change: 1 addition & 0 deletions src/lib/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const ResultExtrasSchema = z.object({
.optional(),
regions: z.record(z.string(), z.array(RegionPointSchema)),
venuesWithData: z.array(z.string()).optional(),
venuesForChain: z.record(z.string(), z.array(z.string())).optional(),
});

const MetricPanelSchema = z.object({
Expand Down
4 changes: 4 additions & 0 deletions src/types/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ export type ResultExtras = {
regions: Record<string, RegionPoint[]>;
/** Venue dimension values that have actual Prom data in the current window. */
venuesWithData?: string[];
/** Per-chain venue availability: chain value → venue values that have Prom
* data for that chain. Powers cross-dimension tab filtering so clicking
* "Robinhood" hides venues that have no data on Robinhood. */
venuesForChain?: Record<string, string[]>;
};

export type Benchmark = {
Expand Down
Loading