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
25 changes: 25 additions & 0 deletions benchmarks/hyperliquid-frontends.yml
Original file line number Diff line number Diff line change
Expand Up @@ -928,3 +928,28 @@ metric_panels:
metric: hl_frontend_last_fill_age_seconds_v2
unit: s
description: "Seconds since this frontend's most recent attributed fill. An outage signal."
- id: revenue_7d
label: Revenue 7d
metric: hl_frontend_fees_usd_7d_v2
unit: usd
higher_is_better: true
description: "USD builder fees collected over the rolling 7 day window. Smooths promo cycles and one off campaigns; use for ranking stability."
- id: revenue_30d
label: Revenue 30d
metric: hl_frontend_fees_usd_30d_v2
unit: usd
higher_is_better: true
description: "USD builder fees collected over the rolling 30 day window."

# Honest column labels for the main table. The p50/p90/p99/mean slots are
# repurposed on this bench (USD revenue, no percentile semantics), so the
# ledger renders these labeled columns instead of latency headers, with
# unique users surfaced inline from the users panel. Matches the column
# set of the reference builder-code dashboards (volume, users, $/user,
# revenue) so readers can cross check without clicking through tabs.
ledger_columns:
- { label: "Revenue (24h)", slot: p50 }
- { label: "Volume (24h)", slot: p90 }
- { label: "Users (24h)", panel: users }
- { label: "$ / user (24h)", slot: p99 }
- { label: "Rev / day (7d avg)", slot: mean }
11 changes: 9 additions & 2 deletions src/app/api/badge/[slug]/[provider]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,12 @@ export async function GET(
.join(" · ");
} else {
r = rankOf(b.results, provider, b.higherIsBetter);
if (!r) return new NextResponse("not found", { status: 404 });
if (!r) {
return new NextResponse("not found", {
status: 404,
headers: { "cache-control": "public, s-maxage=60" },
});
}
// Hint the aggregate scope when the bench declares dimensions so
// embedders can read it. Benches without dimensions get no scope
// label (it would be noise).
Expand Down Expand Up @@ -281,7 +286,9 @@ export async function GET(

// Scope marker: small caps tspan appended to the figure line, so the
// badge height stays constant whether or not a scope is present.
const scopeAriaSuffix = scopeLabel ? ` (${scopeLabel})` : "";
// Escaped at construction: scopeLabel comes from YAML dimension labels
// and lands in XML attribute/text contexts below.
const scopeAriaSuffix = scopeLabel ? ` (${escapeXml(scopeLabel)})` : "";
const scopeTspan = scopeLabel
? `<tspan dx="7" font-size="8" font-weight="600" fill="#9aa0a8" letter-spacing="0.6">${escapeXml(scopeLabel.toUpperCase())}</tspan>`
: "";
Expand Down
18 changes: 15 additions & 3 deletions src/app/products/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,16 @@ export default async function ProviderPage({
(k) => cellRanks[k][0]?.slug.toLowerCase() === me,
),
);
if (wonKeys.size === finestKeys.length) {
// Collapsed claims require FULL declared coverage, not just the
// cells that happen to have data this cycle. Without this, a
// degraded matrix (one surviving cell) would mint an unscoped
// global "#1" from a single win.
const expectedCells =
Math.max(chainDims.length, 1) * regionDims.length;
if (
wonKeys.size === finestKeys.length &&
finestKeys.length === expectedCells
) {
badgeCards.push({ key: benchSlug, title, benchSlug });
continue;
}
Expand All @@ -141,7 +150,9 @@ export default async function ProviderPage({
const covered = new Set<string>();
for (const c of chainDims) {
const row = finestKeys.filter((k) => chainOf(k) === c.value);
if (row.length === 0 || !row.every((k) => wonKeys.has(k))) continue;
// Row collapse only when every DECLARED region reported a cell
// for this chain and the provider won them all.
if (row.length !== regionDims.length || !row.every((k) => wonKeys.has(k))) continue;
badgeCards.push({
key: `${benchSlug}-${c.value}`,
title,
Expand All @@ -152,7 +163,8 @@ export default async function ProviderPage({
}
for (const r of regionDims) {
const col = finestKeys.filter((k) => regionOf(k) === r.value);
if (col.length === 0 || !col.every((k) => wonKeys.has(k))) continue;
const expectedCols = Math.max(chainDims.length, 1);
if (col.length !== expectedCols || !col.every((k) => wonKeys.has(k))) continue;
if (col.every((k) => covered.has(k))) continue;
badgeCards.push({
key: `${benchSlug}-r-${r.value}`,
Expand Down
4 changes: 3 additions & 1 deletion src/components/benchmark-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@
// L1/L2 layer counts. When both > 0 the bench mixes L1 and L2 chains
// and we render a top-level Layer toggle that filters the entire page
// (chart + summary + ledger) to one layer at a time. Default is L1.
const layerCounts = useMemo(() => {

Check failure on line 200 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
let l1 = 0;
let l2 = 0;
for (const r of benchmark.results) {
Expand All @@ -212,7 +212,7 @@
// hasLayerSplit is false the original benchmark is returned untouched
// so non-layer benches keep their existing behavior. The chart, the
// summary stats and the ledger all read from `viewBenchmark`.
const viewBenchmark = useMemo(() => {

Check failure on line 215 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 (!hasLayerSplit) return benchmark;
return {
...benchmark,
Expand All @@ -230,7 +230,7 @@
// they always saw.
const allowedViews = viewsForBenchmark(viewBenchmark);
const defaultView = defaultViewFor(viewBenchmark);
const [view, setView, viewMounted] = useViewPreference(

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

View workflow job for this annotation

GitHub Actions / check

React Hook "useViewPreference" 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?
viewBenchmark.slug,
defaultView,
allowedViews,
Expand All @@ -241,7 +241,7 @@
// hidden when they switch to distribution or donut - the model is
// "this is the field of providers the reader chose to focus on",
// not "what each view chose to drop". Resets on bench navigation.
const [excluded, setExcluded] = useState<Set<string>>(() => new Set());

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

View workflow job for this annotation

GitHub Actions / check

React Hook "useState" 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?
const toggleExclude = (slug: string) =>
setExcluded((prev) => {
const next = new Set(prev);
Expand All @@ -257,22 +257,22 @@
// being split between the dimension row and the chart toolbar.
const chartRegions = chartOnlyRegions(benchmark);
const showChartRegionRow = regionOptions.length === 0 && chartRegions.length > 1;
const [chartRegion, setChartRegion] = useState<string>("all");

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

View workflow job for this annotation

GitHub Actions / check

React Hook "useState" 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?

// Active companion-metric panel. null = main spec metric (default chart
// data, default unit, default header). When a panel id is set, the chart
// pulls its per-provider series from panel.seriesByProvider, swaps the
// header label to panel.label, and the Y-axis unit to panel.unit.
const [activePanelId, setActivePanelId] = useState<string | null>(null);

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

View workflow job for this annotation

GitHub Actions / check

React Hook "useState" 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?
// Single Top-N value shared across every chart view AND the ledger
// so a reader who picks "Top 5" sees the same 5 providers in every
// surface. Each chart still computes its own option set off its own
// post-filter cohort, but the active value is parent-controlled.
const [topN, setTopN] = useState<number | null>(null);

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

View workflow job for this annotation

GitHub Actions / check

React Hook "useState" 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?
const topNControl = useMemo(() => ({ topN, setTopN }), [topN]);

Check failure on line 272 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?
const activePanel =
benchmark.metricPanels?.find((p) => p.id === activePanelId) ?? null;
const chartRegionOptions: ChainOption[] = useMemo(

Check failure on line 275 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?
() => [
{ value: "all", label: "All" },
...chartRegions.map((r) => ({ value: r, label: REGION_DISPLAY[r] ?? r })),
Expand Down Expand Up @@ -492,7 +492,9 @@
? "Product ledger"
: activePanel
? `Product ledger · sorted by ${activePanel.label}`
: "Product ledger · sorted by p50"}
: viewBenchmark.ledgerColumns?.length
? `Product ledger · sorted by ${viewBenchmark.ledgerColumns[0].label}`
: "Product ledger · sorted by p50"}
</p>
<LedgerTable benchmark={viewBenchmark} activePanel={activePanel} topN={topN} />
</div>
Expand Down
122 changes: 99 additions & 23 deletions src/components/ledger-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import { useMemo } from "react";

import Link from "next/link";
import type { Benchmark, MetricPanel, ProviderResult } from "@/types/benchmark";
import type {
Benchmark,
LedgerColumn,
MetricPanel,
ProviderResult,
} from "@/types/benchmark";
import { ChainCoverageChip } from "@/components/chain-coverage-chip";
import { Hint } from "@/components/hint";
import { Sparkline } from "@/components/sparkline";
Expand Down Expand Up @@ -41,12 +46,35 @@ export function LedgerTable({ benchmark, activePanel, topN }: Props) {
const pickValue = (r: ProviderResult): number =>
activePanel ? (activePanel.values[r.slug] ?? 0) : r.ms.p50;
const panelActive = !!activePanel;
const secondary = results[0]?.secondary?.label;
// Custom column mode: benches that repurpose the p50/p90/p99/mean slots
// (USD revenue leaderboards) declare ledger_columns in their YAML so
// every column carries an honest label + unit, and panel-backed columns
// (e.g. unique users) surface inline instead of behind a tab click.
// Disabled while a panel tab is active — the panel sort already owns
// the table and the aggregate columns are dashed out.
const customCols = !panelActive ? benchmark.ledgerColumns : undefined;
const panelById = useMemo(
() => new Map((benchmark.metricPanels ?? []).map((p) => [p.id, p])),
[benchmark.metricPanels],
);
const secondary = customCols ? undefined : results[0]?.secondary?.label;

// Resolve one custom column's value for a row. Slot columns read the
// repurposed headline slots; panel columns read the panel's values map
// (null when the provider returned no data for that metric this cycle).
const colValue = (r: ProviderResult, col: LedgerColumn): number | null => {
if (col.slot) return r.ms[col.slot];
const v = panelById.get(col.panel ?? "")?.values[r.slug];
return v != null && Number.isFinite(v) ? v : null;
};
const colUnit = (col: LedgerColumn): string =>
col.unit ??
(col.panel ? (panelById.get(col.panel)?.unit ?? unit) : unit);
// Detected from the first provider's results — if ANY provider declares
// slot_p50/slot_p99 in its YAML queries, every row gets the column (with
// "-" 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);
const hasSlots = !customCols && results.some((r) => r.slots != null);
// 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"
Expand Down Expand Up @@ -110,12 +138,14 @@ export function LedgerTable({ benchmark, activePanel, topN }: Props) {
Product
</th>
<th
colSpan={5}
colSpan={customCols ? customCols.length + 1 : 5}
className="border-y-2 border-ink py-2 px-3 text-center hidden md:table-cell"
>
Latency aggregates
{customCols ? benchmark.metric : "Latency aggregates"}
</th>
<th className="border-y-2 border-ink py-2 px-3 text-right md:hidden">
{customCols ? customCols[0].label : "p50"}
</th>
<th className="border-y-2 border-ink py-2 px-3 text-right md:hidden">p50</th>
<th className="border-y-2 border-ink py-2 pl-3 text-right hidden md:table-cell">
Reliability
</th>
Expand All @@ -138,10 +168,23 @@ export function LedgerTable({ benchmark, activePanel, topN }: Props) {
<th className="py-2 pr-2 text-left w-2"></th>
<th className="py-2 pr-3 text-left w-10">№</th>
<th className="py-2 pr-3 text-left">Name</th>
<th className="py-2 px-3 text-right">p50</th>
<th className="py-2 px-3 text-right hidden md:table-cell">p90</th>
<th className="py-2 px-3 text-right hidden md:table-cell">p99</th>
<th className="py-2 px-3 text-right hidden md:table-cell">Mean</th>
{customCols ? (
customCols.map((c, idx) => (
<th
key={c.label}
className={`py-2 px-3 text-right ${idx === 0 ? "" : "hidden md:table-cell"}`}
>
{c.label}
</th>
))
) : (
<>
<th className="py-2 px-3 text-right">p50</th>
<th className="py-2 px-3 text-right hidden md:table-cell">p90</th>
<th className="py-2 px-3 text-right hidden md:table-cell">p99</th>
<th className="py-2 px-3 text-right hidden md:table-cell">Mean</th>
</>
)}
<th className="py-2 px-3 text-right hidden md:table-cell">Δ field</th>
<th className="py-2 px-3 text-right hidden md:table-cell">Success</th>
<th className="py-2 pl-3 text-right">24h</th>
Expand All @@ -150,7 +193,11 @@ export function LedgerTable({ benchmark, activePanel, topN }: Props) {
</tr>
<tr className="border-b border-ink">
<th
colSpan={10 + (hasSlots ? 1 : 0) + (secondary ? 1 : 0)}
colSpan={
(customCols ? 6 + customCols.length : 10) +
(hasSlots ? 1 : 0) +
(secondary ? 1 : 0)
}
className="h-px p-0"
/>
</tr>
Expand All @@ -168,6 +215,10 @@ export function LedgerTable({ benchmark, activePanel, topN }: Props) {
panelActive={panelActive}
hasSecondary={!!secondary}
hasSlots={hasSlots}
customCells={customCols?.map((c) => ({
v: colValue(r, c),
unit: colUnit(c),
}))}
series={
activePanel
? (activePanel.seriesByProvider?.[r.slug] ?? [])
Expand Down Expand Up @@ -195,6 +246,7 @@ function Row({
panelActive,
hasSecondary,
hasSlots,
customCells,
series,
sparkMin,
sparkMax,
Expand All @@ -210,6 +262,9 @@ function Row({
panelActive: boolean;
hasSecondary: boolean;
hasSlots: boolean;
/** Custom-column mode (benchmark.ledgerColumns): one pre-resolved
* {value, unit} per declared column, replacing p50/p90/p99/Mean. */
customCells?: { v: number | null; unit: string }[];
series: number[];
sparkMin: number;
sparkMax: number;
Expand Down Expand Up @@ -294,14 +349,18 @@ function Row({
</td>
{isOffline ? (
<td
colSpan={7 + (hasSlots ? 1 : 0) + (hasSecondary ? 1 : 0)}
colSpan={
(customCells ? customCells.length + 3 : 7) +
(hasSlots ? 1 : 0) +
(hasSecondary ? 1 : 0)
}
className="py-2.5 px-3 text-right text-ink-faint italic text-[12px]"
>
Awaiting next successful scrape
</td>
) : (
<>
{/* p50 with inline data bar */}
{/* Headline column with inline data bar */}
<td className="py-2.5 px-3 text-right whitespace-nowrap">
<span className="inline-flex items-center gap-2 justify-end">
<span
Expand All @@ -314,19 +373,36 @@ function Row({
aria-hidden
/>
<span className="text-ink whitespace-nowrap">
{fmtUnit(value, unit)}
{customCells
? customCells[0].v != null
? fmtUnit(customCells[0].v, customCells[0].unit)
: "-"
: fmtUnit(value, unit)}
</span>
</span>
</td>
<td className="py-2.5 px-3 text-right text-ink-soft whitespace-nowrap hidden md:table-cell">
{panelActive ? "—" : fmtUnit(r.ms.p90, unit)}
</td>
<td className="py-2.5 px-3 text-right text-ink-soft whitespace-nowrap hidden md:table-cell">
{panelActive ? "—" : fmtUnit(r.ms.p99, unit)}
</td>
<td className="py-2.5 px-3 text-right text-ink-soft whitespace-nowrap hidden md:table-cell">
{panelActive ? "—" : fmtUnit(r.ms.mean, unit)}
</td>
{customCells ? (
customCells.slice(1).map((c, idx) => (
<td
key={idx}
className="py-2.5 px-3 text-right text-ink-soft whitespace-nowrap hidden md:table-cell"
>
{c.v != null ? fmtUnit(c.v, c.unit) : "-"}
</td>
))
) : (
<>
<td className="py-2.5 px-3 text-right text-ink-soft whitespace-nowrap hidden md:table-cell">
{panelActive ? "—" : fmtUnit(r.ms.p90, unit)}
</td>
<td className="py-2.5 px-3 text-right text-ink-soft whitespace-nowrap hidden md:table-cell">
{panelActive ? "—" : fmtUnit(r.ms.p99, unit)}
</td>
<td className="py-2.5 px-3 text-right text-ink-soft whitespace-nowrap hidden md:table-cell">
{panelActive ? "—" : fmtUnit(r.ms.mean, unit)}
</td>
</>
)}
<td className="py-2.5 px-3 text-right text-ink-muted whitespace-nowrap hidden md:table-cell">
{fieldValue > 0 ? `${deltaSign}${Math.abs(deltaPct).toFixed(0)}%` : "-"}
</td>
Expand Down
6 changes: 4 additions & 2 deletions src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function fmtUnit(value: number, unit: string) {
if (abs < 0.001) return `$${value.toFixed(6)}`;
if (abs < 0.01) return `$${value.toFixed(5)}`;
if (abs < 1) return `$${value.toFixed(4)}`;
if (abs < 1000) return `$${value.toLocaleString(undefined, { maximumFractionDigits: 2 })}`;
if (abs < 1000) return `$${value.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
return `$${formatCompactCount(value)}`;
}
if (value >= 1000) return `${(value / 1000).toFixed(2)} s`;
Expand Down Expand Up @@ -102,7 +102,9 @@ function formatCompactCount(value: number): string {
if (abs >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
if (abs >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
if (abs >= 1e4) return `${(value / 1e3).toFixed(1)}K`;
return value.toLocaleString();
// Locale pinned: rendered both server-side and client-side ("use client"
// ledger). Browser-default locale produced "5 560,409" on fr-FR machines.
return value.toLocaleString("en-US", { maximumFractionDigits: 2 });
}

/** Smart-precision percent formatter. picks decimals based on magnitude
Expand Down
30 changes: 30 additions & 0 deletions src/lib/prometheus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ export class Prometheus {
// / metadata / ULA / CGNAT.
await assertPublicHost(url);

// Global concurrency cap. At cold start / build every bench loads at
// once, which used to fire hundreds of 24h-window quantile queries
// within seconds and brown out the single Prom instance (providers
// timing out → partial leaderboards). Queueing here keeps the burst
// at a level Prom absorbs; total wall time barely moves because Prom
// was serializing on CPU anyway.
await acquireQuerySlot();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
try {
Expand All @@ -187,10 +194,33 @@ export class Prometheus {
return json.data;
} finally {
clearTimeout(timeout);
releaseQuerySlot();
}
}
}

/** Module-level semaphore for fetchEnvelope. 8 concurrent queries keeps
* a Railway-sized Prom responsive while ~30 benches load in parallel. */
const MAX_CONCURRENT_QUERIES = 8;
let activeQueries = 0;
const queryWaiters: (() => void)[] = [];

function acquireQuerySlot(): Promise<void> {
if (activeQueries < MAX_CONCURRENT_QUERIES) {
activeQueries++;
return Promise.resolve();
}
return new Promise((resolve) => queryWaiters.push(resolve));
}

function releaseQuerySlot(): void {
const next = queryWaiters.shift();
// Hand the slot directly to the next waiter (activeQueries unchanged)
// or free it when the queue is empty.
if (next) next();
else activeQueries--;
}

/** PromQL built-in functions and keywords we should skip when scanning
* for the first raw metric name in a query string. Updated from the
* Prometheus 2.49 function reference - any identifier here is guaranteed
Expand Down
Loading
Loading