From f85f81673f023d9a75af9d04063bef24c9626c40 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sun, 29 Mar 2026 10:23:23 -0700 Subject: [PATCH] Fix OOM in CI: make WorldCupSimulator simulation count configurable for tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests now pass numSimulations=500 instead of the production default of 50,000. Six simulator tests × 50k iterations each was exhausting the 4GB heap on GitHub Actions runners. Also reduce simGroupMatch stat tests from 50k to 5k iterations. Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/world-cup-simulator.test.ts | 30 +++++++++---------- .../simulations/world-cup-simulator.ts | 10 +++++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/services/simulations/__tests__/world-cup-simulator.test.ts b/app/services/simulations/__tests__/world-cup-simulator.test.ts index 0234b96..66991d6 100644 --- a/app/services/simulations/__tests__/world-cup-simulator.test.ts +++ b/app/services/simulations/__tests__/world-cup-simulator.test.ts @@ -17,31 +17,31 @@ describe("simGroupMatch", () => { it("equal teams draw ≈28% of the time", () => { let draws = 0; - const N = 50_000; + const N = 5_000; for (let i = 0; i < N; i++) { if (simGroupMatch(1500, 1500) === "draw") draws++; } const drawRate = draws / N; - // BASE_DRAW_RATE = 0.28 at eloDiff=0; accept ±3% from sampling noise - expect(drawRate).toBeGreaterThan(0.25); - expect(drawRate).toBeLessThan(0.31); + // BASE_DRAW_RATE = 0.28 at eloDiff=0; accept ±5% from sampling noise at N=5k + expect(drawRate).toBeGreaterThan(0.23); + expect(drawRate).toBeLessThan(0.33); }); it("draw rate decays with large Elo gap", () => { let draws = 0; - const N = 50_000; + const N = 5_000; for (let i = 0; i < N; i++) { if (simGroupMatch(2000, 1400) === "draw") draws++; } const drawRate = draws / N; - // eloDiff = 600 → pDraw ≈ 0.28 * exp(-1.2) ≈ 0.084; accept ±3% - expect(drawRate).toBeLessThan(0.13); + // eloDiff = 600 → pDraw ≈ 0.28 * exp(-1.2) ≈ 0.084; accept ±5% + expect(drawRate).toBeLessThan(0.15); }); it("strong favorite wins more often than underdog", () => { let wins = 0; let losses = 0; - const N = 50_000; + const N = 5_000; for (let i = 0; i < N; i++) { const r = simGroupMatch(1900, 1600); if (r === "win") wins++; @@ -110,7 +110,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(); + const sim = new WorldCupSimulator(500); await expect(sim.simulate("season-1")).rejects.toThrow("No participants found"); }); @@ -121,7 +121,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(); + const sim = new WorldCupSimulator(500); const results = await sim.simulate("season-1"); expect(results).toHaveLength(48); const ids = new Set(results.map((r) => r.participantId)); @@ -137,7 +137,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(); + const sim = new WorldCupSimulator(500); const results = await sim.simulate("season-1"); const sumFirst = results.reduce((s, r) => s + r.probabilities.probFirst, 0); @@ -158,7 +158,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(); + const sim = new WorldCupSimulator(500); const results = await sim.simulate("season-1"); for (const r of results) { @@ -179,7 +179,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(); + const sim = new WorldCupSimulator(500); const results = await sim.simulate("season-1"); // probFirst + probSecond + probThird + probFourth should cover all probability mass @@ -235,7 +235,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([group, ...remainingGroups]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(); + const sim = new WorldCupSimulator(500); const results = await sim.simulate("season-1"); const p3Result = results.find((r) => r.participantId === "p3"); @@ -255,7 +255,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(); + const sim = new WorldCupSimulator(500); const results = await sim.simulate("season-1"); for (const r of results) { diff --git a/app/services/simulations/world-cup-simulator.ts b/app/services/simulations/world-cup-simulator.ts index 133f4df..9a1a587 100644 --- a/app/services/simulations/world-cup-simulator.ts +++ b/app/services/simulations/world-cup-simulator.ts @@ -303,6 +303,12 @@ function normalizeProbabilities( // ─── Main simulator ─────────────────────────────────────────────────────────── export class WorldCupSimulator implements Simulator { + private readonly numSimulations: number; + + constructor(numSimulations = NUM_SIMULATIONS) { + this.numSimulations = numSimulations; + } + async simulate(sportsSeasonId: string): Promise { const db = database(); @@ -465,7 +471,7 @@ export class WorldCupSimulator implements Simulator { counts.qfLoser.set(id, 0); } - for (let sim = 0; sim < NUM_SIMULATIONS; sim++) { + for (let sim = 0; sim < this.numSimulations; sim++) { // ── Group stage ────────────────────────────────────────────── const advancingFromGroup: string[] = []; // group winners + runners-up (24 teams) const thirdPlaceTeams: TeamStats[] = []; // 12 third-place teams @@ -570,7 +576,7 @@ export class WorldCupSimulator implements Simulator { } // 8. Convert counts to probabilities - const N = NUM_SIMULATIONS; + const N = this.numSimulations; const numQfLosers = 4; // 4 QF losers per sim const results: SimulationResult[] = participantIds.map((id) => {