From 8dab9753397548d57139ef83d05ba5f26342a827 Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Tue, 15 Sep 2026 17:44:16 -0700 Subject: [PATCH 1/2] Rank registry leaderboard by equally weighted fixture wins --- .github/workflows/test.yml | 24 +++++++ README.md | 15 ++++ app/src/components/header.tsx | 82 +++++++++++++++------ app/src/lib/registry-leaderboard.ts | 68 ++++++++++++++++++ app/src/lib/utils.ts | 57 ++++++++------- scripts/registry-leaderboard.test.js | 103 +++++++++++++++++++++++++++ 6 files changed, 302 insertions(+), 47 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 app/src/lib/registry-leaderboard.ts create mode 100644 scripts/registry-leaderboard.test.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..52c14048a0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Data and App Tests +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "24" + package-manager-cache: false + - run: npm install --ignore-scripts --no-package-lock + working-directory: app + - run: node --test scripts/*.test.js + - name: Test app data semantics + run: | + if [ -d app/tests ]; then node --test app/tests/*.test.*; fi + - run: npm run build + working-directory: app diff --git a/README.md b/README.md index b4dc5610e0..cf9c7ba9d9 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,21 @@ The generated charts show: ### View Results Online +Registry leaderboard cards rank registries by **fixture wins**, not an average +installation time. Each selected fixture/variation pair counts once, so a large +fixture has the same influence as a small fixture. The `average` tab counts pairs +from both registry variations. Cards show wins out of the number of selected +pairs; filtering fixtures recomputes both values. Tool toggles only hide cards: +wins are always measured against all registries in the data. + +The fastest complete positive timing earns a win; exact ties each earn a win +and share a rank (ordered alphabetically). Failed (DNF), partial, missing, and +invalid results earn no wins and receive no invented timing penalty. They remain +in the common denominator, including pairs where every registry failed. A card +with incomplete coverage also shows its number of complete results. This summary +describes how often a registry wins, not the magnitude of its speed advantage; +the fixture charts provide the underlying timings. + Results are automatically deployed to GitHub Pages when running on the main branch: diff --git a/app/src/components/header.tsx b/app/src/components/header.tsx index 31de21d3bd..8a4ae5c9ee 100644 --- a/app/src/components/header.tsx +++ b/app/src/components/header.tsx @@ -307,7 +307,8 @@ HeaderSection.displayName = "HeaderSection"; interface LeaderBoardItemProps { packageManager: PackageManager; - averageTime: number; + averageTime?: number; + registryStats?: { wins: number; totalTests: number; completedTests: number }; idx: number; unit?: string; isBaseline?: boolean; @@ -315,7 +316,8 @@ interface LeaderBoardItemProps { const LeaderBoardItem = ({ packageManager, - averageTime, + averageTime = 0, + registryStats, idx, unit = "ms/pkg", isBaseline = false, @@ -362,7 +364,18 @@ const LeaderBoardItem = ({ )}

-

{formattedTime}

+

+ {registryStats + ? `${registryStats.wins}/${registryStats.totalTests} wins` + : formattedTime} +

+ {registryStats && + registryStats.completedTests < registryStats.totalTests && ( +

+ {registryStats.completedTests}/{registryStats.totalTests}{" "} + complete +

+ )} @@ -404,25 +417,50 @@ const HeaderLeaderboard = forwardRef>( if (leaderboard && leaderboard.length === 0) return null; return ( - -
- {leaderboard && - leaderboard.map((item, idx) => ( - - ))} -
- -
+
+ {isRegistryRoute && ( +

+ Fixture wins: each selected fixture and variation counts equally. + Fastest complete result wins; tied fastest results each earn a win. + Failed, partial, or missing results earn no wins. Compared across + all registries. +

+ )} + +
+ {leaderboard && + leaderboard.map((item, idx) => ( + entry.wins === item.wins, + ) + : idx + } + averageTime={item.averageTime} + registryStats={ + isRegistryRoute + ? { + wins: item.wins, + totalTests: item.totalTests, + completedTests: item.completedTests ?? 0, + } + : undefined + } + packageManager={item.packageManager} + unit={unit} + isBaseline={isBaselinePackageManager( + item.packageManager, + isRegistryRoute, + )} + /> + ))} +
+ +
+
); }, ); diff --git a/app/src/lib/registry-leaderboard.ts b/app/src/lib/registry-leaderboard.ts new file mode 100644 index 0000000000..94fdfc1414 --- /dev/null +++ b/app/src/lib/registry-leaderboard.ts @@ -0,0 +1,68 @@ +import type { + BenchmarkChartData, + Fixture, + PackageManager, + Variation, +} from "@/types/chart-data"; + +export interface RegistryRanking { + packageManager: PackageManager; + wins: number; + totalTests: number; + completedTests: number; +} + +/** Each selected fixture/variation is one contest, independent of its duration. */ +export function calculateRegistryLeaderboard( + chartData: BenchmarkChartData, + specificVariation?: Variation, + enabledFixtures?: Set, +): RegistryRanking[] { + const source = chartData.registryChartData; + if (!source) return []; + const variations = + specificVariation && specificVariation !== "average" + ? [specificVariation] + : source.variations.filter((variation) => variation !== "average"); + const rankings = source.packageManagers.map((packageManager) => ({ + packageManager, + wins: 0, + totalTests: 0, + completedTests: 0, + })); + + for (const variation of variations) { + for (const fixture of source.data[variation] ?? []) { + if (enabledFixtures && !enabledFixtures.has(fixture.fixture)) continue; + // Missing, DNF and partial samples never earn wins. Preserve a common + // denominator even when a registry failed or the whole contest failed. + const metadata = fixture as typeof fixture & Record; + const times = rankings.map(({ packageManager: pm }) => { + const value = fixture[pm]; + return metadata[`${pm}_dnf`] !== true && + metadata[`${pm}_partial`] !== true && + typeof value === "number" && + Number.isFinite(value) && + value > 0 + ? value + : undefined; + }); + const best = Math.min(...times.filter((time) => time !== undefined)); + rankings.forEach((ranking, index) => { + ranking.totalTests++; + const time = times[index]; + if (time === undefined) return; + ranking.completedTests++; + // Exact ties each receive a win; no registry gets arbitrary priority. + if (time === best) ranking.wins++; + }); + } + } + + return rankings + .filter((ranking) => ranking.totalTests > 0) + .sort( + (a, b) => + b.wins - a.wins || a.packageManager.localeCompare(b.packageManager), + ); +} diff --git a/app/src/lib/utils.ts b/app/src/lib/utils.ts index 1e615c0b82..35e178cfac 100644 --- a/app/src/lib/utils.ts +++ b/app/src/lib/utils.ts @@ -1,5 +1,6 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; +import { calculateRegistryLeaderboard } from "./registry-leaderboard.ts"; import type { Variation, Fixture, @@ -216,14 +217,13 @@ export const getVariationCategories = ( interface RankingData { packageManager: PackageManager; wins: number; - averageTime: number; + averageTime?: number; totalTests: number; + completedTests?: number; } export type LeaderboardRoute = - | "package-managers" - | "task-runners" - | "registries"; + "package-managers" | "task-runners" | "registries"; export const calculateLeaderboard = ( chartData: BenchmarkChartData, @@ -231,6 +231,13 @@ export const calculateLeaderboard = ( route?: LeaderboardRoute, enabledFixtures?: Set, ): RankingData[] => { + if ((route ?? "registries") === "registries") { + return calculateRegistryLeaderboard( + chartData, + specificVariation, + enabledFixtures, + ); + } const categories = getVariationCategories(chartData.chartData.variations); const effectiveRoute = route ?? "registries"; @@ -362,30 +369,30 @@ export const calculateLeaderboard = ( }); // Calculate final rankings - const leaderboard: RankingData[] = ( - availablePackageManagers as PackageManager[] - ).map((pm) => { - const stats = packageManagerStats[pm]; - if (!stats) { + const leaderboard = (availablePackageManagers as PackageManager[]).map( + (pm) => { + const stats = packageManagerStats[pm]; + if (!stats) { + return { + packageManager: pm, + wins: 0, + averageTime: Number.MAX_SAFE_INTEGER, + totalTests: 0, + }; + } + const averageTime = + stats.testCount > 0 + ? stats.totalTime / stats.testCount + : Number.MAX_SAFE_INTEGER; + return { packageManager: pm, - wins: 0, - averageTime: Number.MAX_SAFE_INTEGER, - totalTests: 0, + wins: stats.wins, + averageTime, + totalTests: stats.testCount, }; - } - const averageTime = - stats.testCount > 0 - ? stats.totalTime / stats.testCount - : Number.MAX_SAFE_INTEGER; - - return { - packageManager: pm, - wins: stats.wins, - averageTime, - totalTests: stats.testCount, - }; - }); + }, + ); // Filter out PMs with no data, then sort by average time (lower is // better) so card order matches the displayed values, wins as tiebreaker diff --git a/scripts/registry-leaderboard.test.js b/scripts/registry-leaderboard.test.js new file mode 100644 index 0000000000..b50e99c8fa --- /dev/null +++ b/scripts/registry-leaderboard.test.js @@ -0,0 +1,103 @@ +const assert = require("node:assert/strict"); +const test = require("node:test"); +const { + calculateRegistryLeaderboard, +} = require("../app/src/lib/registry-leaderboard.ts"); + +function chart(data) { + return { + registryChartData: { + variations: Object.keys(data), + packageManagers: ["npm", "vlt", "aws"], + data, + }, + }; +} + +test("five fixture wins outweigh one arbitrarily large fixture", () => { + const data = chart({ + "registry-clean": [ + ...["next", "astro", "vue", "svelte", "large"].map((fixture) => ({ + fixture, + npm: 10, + vlt: 6, + aws: 20, + })), + { fixture: "babylon", npm: 100, vlt: 10000, aws: 20000 }, + ], + }); + assert.deepEqual( + calculateRegistryLeaderboard(data).map( + ({ packageManager, wins, totalTests }) => ({ + packageManager, + wins, + totalTests, + }), + ), + [ + { packageManager: "vlt", wins: 5, totalTests: 6 }, + { packageManager: "npm", wins: 1, totalTests: 6 }, + { packageManager: "aws", wins: 0, totalTests: 6 }, + ], + ); +}); + +test("DNF, partial, missing and non-finite values earn no wins or completions", () => { + const data = chart({ + "registry-clean": [ + { fixture: "next", npm: 3, vlt: 1, vlt_dnf: true }, + { fixture: "astro", npm: 3, vlt: 1, vlt_partial: true, aws: Infinity }, + { fixture: "vue", npm: 3, vlt: 1, vlt_dnf: true, npm_dnf: true }, + { fixture: "svelte", npm: 0, vlt: -1, aws: NaN }, + ], + }); + assert.deepEqual(calculateRegistryLeaderboard(data), [ + { packageManager: "npm", wins: 2, totalTests: 4, completedTests: 2 }, + { packageManager: "aws", wins: 0, totalTests: 4, completedTests: 0 }, + { packageManager: "vlt", wins: 0, totalTests: 4, completedTests: 0 }, + ]); +}); + +test("ties each earn a win and sort alphabetically without timing tiebreakers", () => { + const data = chart({ + "registry-clean": [{ fixture: "next", npm: 4, vlt: 4, aws: 1000 }], + }); + assert.deepEqual( + calculateRegistryLeaderboard(data).map(({ packageManager, wins }) => [ + packageManager, + wins, + ]), + [ + ["npm", 1], + ["vlt", 1], + ["aws", 0], + ], + ); +}); + +test("fixture and variation filters recompute wins and common denominators", () => { + const data = chart({ + "registry-clean": [ + { fixture: "next", npm: 4, vlt: 3 }, + { fixture: "astro", npm: 4, vlt: 5 }, + ], + "registry-lockfile": [{ fixture: "next", npm: 4, vlt: 5 }], + }); + const filtered = calculateRegistryLeaderboard( + data, + "registry-clean", + new Set(["next"]), + ); + assert.equal(filtered[0].packageManager, "vlt"); + assert.equal(filtered[0].wins, 1); + assert.equal(filtered[0].totalTests, 1); + const average = calculateRegistryLeaderboard(data, "average"); + assert.equal(average[0].packageManager, "npm"); + assert.equal(average[0].wins, 2); + assert.equal(average[0].totalTests, 3); + assert.deepEqual( + calculateRegistryLeaderboard(data, "average", new Set()), + [], + ); + assert.deepEqual(calculateRegistryLeaderboard({}), []); +}); From 1b46b739fc8b2e3ea6459bd3ec4925a61eb38d8c Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Tue, 15 Sep 2026 17:50:05 -0700 Subject: [PATCH 2/2] Clarify partial results in registry win rankings --- README.md | 8 +++++--- app/src/components/variation/chart.tsx | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c8d345b9e0..579c0096c6 100644 --- a/README.md +++ b/README.md @@ -218,8 +218,10 @@ Chart data adds optional `_partial`, `_attempted_runs`, per-package datasets. Tables and chart notices label partial results with their success counts. Synthetic averages retain that warning and omit the affected command's value. Commands with any -partial result in the selected comparisons are excluded from rankings; history -omits that command's affected daily variation and category average. +partial result in the selected comparisons are excluded from timing rankings; +registry win rankings retain the common denominator and award no wins for +incomplete fixtures. History omits that command's affected daily variation and +category average. This is an additive schema change. Historical JSON without these fields remains readable, but its completeness is unknown: failed attempts discarded by older @@ -249,7 +251,7 @@ same median multiplied by 1000 and divided by package count (ms/package). Failed attempts are excluded from timing statistics but retained in completeness metadata. Partial survivor samples are labeled and excluded from averages, history, leaderboard timing/win calculations, and registry speed alerts. -All-failed results remain DNF; the existing leaderboard DNF penalty uses the +All-failed results remain DNF; timing leaderboards' DNF penalty uses the slowest successful median for that fixture. Average views and leaderboard timing values are arithmetic averages of diff --git a/app/src/components/variation/chart.tsx b/app/src/components/variation/chart.tsx index a3de50f14d..0e1a3602dd 100644 --- a/app/src/components/variation/chart.tsx +++ b/app/src/components/variation/chart.tsx @@ -341,7 +341,10 @@ export const VariationChart = ({

Partial results include successful runs only and may appear faster because failed or timed-out runs were dropped. Commands with partial - results are excluded from rankings and history averages. + results are excluded from history averages.{" "} + {isRegistry + ? "Incomplete fixture results earn no leaderboard wins." + : "They are also excluded from timing rankings."}

    {partialResults.map((label) =>
  • {label}
  • )}