Fix OOM in CI: make WorldCupSimulator simulation count configurable for tests

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 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-03-29 10:23:23 -07:00
parent 54d1c92b09
commit f85f81673f
2 changed files with 23 additions and 17 deletions

View file

@ -17,31 +17,31 @@ describe("simGroupMatch", () => {
it("equal teams draw ≈28% of the time", () => { it("equal teams draw ≈28% of the time", () => {
let draws = 0; let draws = 0;
const N = 50_000; const N = 5_000;
for (let i = 0; i < N; i++) { for (let i = 0; i < N; i++) {
if (simGroupMatch(1500, 1500) === "draw") draws++; if (simGroupMatch(1500, 1500) === "draw") draws++;
} }
const drawRate = draws / N; const drawRate = draws / N;
// BASE_DRAW_RATE = 0.28 at eloDiff=0; accept ±3% from sampling noise // BASE_DRAW_RATE = 0.28 at eloDiff=0; accept ±5% from sampling noise at N=5k
expect(drawRate).toBeGreaterThan(0.25); expect(drawRate).toBeGreaterThan(0.23);
expect(drawRate).toBeLessThan(0.31); expect(drawRate).toBeLessThan(0.33);
}); });
it("draw rate decays with large Elo gap", () => { it("draw rate decays with large Elo gap", () => {
let draws = 0; let draws = 0;
const N = 50_000; const N = 5_000;
for (let i = 0; i < N; i++) { for (let i = 0; i < N; i++) {
if (simGroupMatch(2000, 1400) === "draw") draws++; if (simGroupMatch(2000, 1400) === "draw") draws++;
} }
const drawRate = draws / N; const drawRate = draws / N;
// eloDiff = 600 → pDraw ≈ 0.28 * exp(-1.2) ≈ 0.084; accept ±3% // eloDiff = 600 → pDraw ≈ 0.28 * exp(-1.2) ≈ 0.084; accept ±5%
expect(drawRate).toBeLessThan(0.13); expect(drawRate).toBeLessThan(0.15);
}); });
it("strong favorite wins more often than underdog", () => { it("strong favorite wins more often than underdog", () => {
let wins = 0; let wins = 0;
let losses = 0; let losses = 0;
const N = 50_000; const N = 5_000;
for (let i = 0; i < N; i++) { for (let i = 0; i < N; i++) {
const r = simGroupMatch(1900, 1600); const r = simGroupMatch(1900, 1600);
if (r === "win") wins++; if (r === "win") wins++;
@ -110,7 +110,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.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"); await expect(sim.simulate("season-1")).rejects.toThrow("No participants found");
}); });
@ -121,7 +121,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(); const sim = new WorldCupSimulator(500);
const results = await sim.simulate("season-1"); const results = await sim.simulate("season-1");
expect(results).toHaveLength(48); expect(results).toHaveLength(48);
const ids = new Set(results.map((r) => r.participantId)); const ids = new Set(results.map((r) => r.participantId));
@ -137,7 +137,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(); const sim = new WorldCupSimulator(500);
const results = await sim.simulate("season-1"); const results = await sim.simulate("season-1");
const sumFirst = results.reduce((s, r) => s + r.probabilities.probFirst, 0); const sumFirst = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
@ -158,7 +158,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(); const sim = new WorldCupSimulator(500);
const results = await sim.simulate("season-1"); const results = await sim.simulate("season-1");
for (const r of results) { for (const r of results) {
@ -179,7 +179,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(); const sim = new WorldCupSimulator(500);
const results = await sim.simulate("season-1"); const results = await sim.simulate("season-1");
// probFirst + probSecond + probThird + probFourth should cover all probability mass // probFirst + probSecond + probThird + probFourth should cover all probability mass
@ -235,7 +235,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([group, ...remainingGroups]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([group, ...remainingGroups]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(); const sim = new WorldCupSimulator(500);
const results = await sim.simulate("season-1"); const results = await sim.simulate("season-1");
const p3Result = results.find((r) => r.participantId === "p3"); const p3Result = results.find((r) => r.participantId === "p3");
@ -255,7 +255,7 @@ describe("WorldCupSimulator", () => {
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
mockDb.query.playoffMatches.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
const sim = new WorldCupSimulator(); const sim = new WorldCupSimulator(500);
const results = await sim.simulate("season-1"); const results = await sim.simulate("season-1");
for (const r of results) { for (const r of results) {

View file

@ -303,6 +303,12 @@ function normalizeProbabilities(
// ─── Main simulator ─────────────────────────────────────────────────────────── // ─── Main simulator ───────────────────────────────────────────────────────────
export class WorldCupSimulator implements Simulator { export class WorldCupSimulator implements Simulator {
private readonly numSimulations: number;
constructor(numSimulations = NUM_SIMULATIONS) {
this.numSimulations = numSimulations;
}
async simulate(sportsSeasonId: string): Promise<SimulationResult[]> { async simulate(sportsSeasonId: string): Promise<SimulationResult[]> {
const db = database(); const db = database();
@ -465,7 +471,7 @@ export class WorldCupSimulator implements Simulator {
counts.qfLoser.set(id, 0); counts.qfLoser.set(id, 0);
} }
for (let sim = 0; sim < NUM_SIMULATIONS; sim++) { for (let sim = 0; sim < this.numSimulations; sim++) {
// ── Group stage ────────────────────────────────────────────── // ── Group stage ──────────────────────────────────────────────
const advancingFromGroup: string[] = []; // group winners + runners-up (24 teams) const advancingFromGroup: string[] = []; // group winners + runners-up (24 teams)
const thirdPlaceTeams: TeamStats[] = []; // 12 third-place teams const thirdPlaceTeams: TeamStats[] = []; // 12 third-place teams
@ -570,7 +576,7 @@ export class WorldCupSimulator implements Simulator {
} }
// 8. Convert counts to probabilities // 8. Convert counts to probabilities
const N = NUM_SIMULATIONS; const N = this.numSimulations;
const numQfLosers = 4; // 4 QF losers per sim const numQfLosers = 4; // 4 QF losers per sim
const results: SimulationResult[] = participantIds.map((id) => { const results: SimulationResult[] = participantIds.map((id) => {