From bd7093381ef4cf827544e90d1f3279b755ba95bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 6 Jun 2026 04:37:29 +0000 Subject: [PATCH 1/4] fix(engine): scale V8 heap and GPU budget proportionally to system RAM On 8 GB systems, the fixed 512 MB V8 heap limit (--max-old-space-size=512) caused severe GC pressure during page load for complex compositions, pushing DOMContentLoaded past the 60 s navigation timeout. The hard-coded 1024 MB GPU budget similarly under-provisioned the GPU process. Replace the fixed tiers with proportional scaling: - V8 heap: total/8 (1024 MB on 8 GB, 768 MB on 6 GB) - GPU budget: total/4 (2048 MB on 8 GB, up from 1024 MB) Systems below 4 GB keep the existing aggressive floors (256 MB heap, 512 MB GPU). Systems above the low-memory threshold remain unconstrained. Fixes a regression introduced in v0.6.74 where 8 GB machines went from no heap constraint to a 512 MB cap, causing navigation timeouts on compositions with multiple video elements. --- .../src/services/browserManager.test.ts | 63 +++++++++++++++++++ .../engine/src/services/browserManager.ts | 4 +- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index 1f92aaf6b..640b1e93f 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -14,6 +14,15 @@ import { resolveBrowserGpuMode, } from "./browserManager.js"; +vi.mock("./systemMemory.js", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, getSystemTotalMb: vi.fn(() => 32768) }; +}); + +import { getSystemTotalMb } from "./systemMemory.js"; + +const mockGetSystemTotalMb = vi.mocked(getSystemTotalMb); + describe("buildChromeArgs browser GPU mode", () => { const base = { width: 1920, height: 1080 }; @@ -308,3 +317,57 @@ describe("browser pool", () => { await acquirePromise.catch(() => {}); }); }); + +describe("memory-adaptive Chrome flags", () => { + const base = { width: 1920, height: 1080 }; + + afterEach(() => { + mockGetSystemTotalMb.mockReturnValue(32768); + }); + + function heapFlag(args: string[]): number | null { + const flag = args.find((a) => a.includes("--max-old-space-size=")); + if (!flag) return null; + return parseInt(flag.match(/--max-old-space-size=(\d+)/)?.[1] ?? "", 10); + } + + function gpuBudget(args: string[]): number { + const flag = args.find((a) => a.startsWith("--force-gpu-mem-available-mb="))!; + return parseInt(flag.split("=")[1]!, 10); + } + + it("does not set heap limit above 8 GB", () => { + mockGetSystemTotalMb.mockReturnValue(16384); + expect(heapFlag(buildChromeArgs(base))).toBeNull(); + }); + + it("scales heap to total/8 on 8 GB systems", () => { + mockGetSystemTotalMb.mockReturnValue(8192); + expect(heapFlag(buildChromeArgs(base))).toBe(1024); + }); + + it("scales heap to total/8 on 6 GB systems", () => { + mockGetSystemTotalMb.mockReturnValue(6144); + expect(heapFlag(buildChromeArgs(base))).toBe(768); + }); + + it("uses 256 MB heap floor below 4 GB", () => { + mockGetSystemTotalMb.mockReturnValue(3072); + expect(heapFlag(buildChromeArgs(base))).toBe(256); + }); + + it("scales GPU budget to total/4 on 8 GB systems", () => { + mockGetSystemTotalMb.mockReturnValue(8192); + expect(gpuBudget(buildChromeArgs(base))).toBe(2048); + }); + + it("scales GPU budget to total/2 above threshold", () => { + mockGetSystemTotalMb.mockReturnValue(16384); + expect(gpuBudget(buildChromeArgs(base))).toBe(8192); + }); + + it("uses 512 MB GPU budget below 4 GB", () => { + mockGetSystemTotalMb.mockReturnValue(3072); + expect(gpuBudget(buildChromeArgs(base))).toBe(512); + }); +}); diff --git a/packages/engine/src/services/browserManager.ts b/packages/engine/src/services/browserManager.ts index 48648a5c0..a5d6eb482 100644 --- a/packages/engine/src/services/browserManager.ts +++ b/packages/engine/src/services/browserManager.ts @@ -512,14 +512,14 @@ function getGpuMemBudgetMb(): number { const total = getSystemTotalMb(); if (total < 4096) return 512; - if (total <= LOW_MEMORY_TOTAL_MB_THRESHOLD) return 1024; + if (total <= LOW_MEMORY_TOTAL_MB_THRESHOLD) return Math.floor(total / 4); return Math.min(Math.floor(total / 2), 16384); } function getLowMemoryFlags(): string[] { const total = getSystemTotalMb(); if (total > LOW_MEMORY_TOTAL_MB_THRESHOLD) return []; - const heapMb = total < 4096 ? 256 : 512; + const heapMb = total < 4096 ? 256 : Math.floor(total / 8); return [`--js-flags=--max-old-space-size=${heapMb}`]; } From c555cd18272da09001350c0d7ae9b2c66f6d4241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 6 Jun 2026 04:42:34 +0000 Subject: [PATCH 2/4] test: add 4 GB boundary tests, use threshold constant --- .../src/services/browserManager.test.ts | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index 640b1e93f..f53d14e99 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -19,7 +19,7 @@ vi.mock("./systemMemory.js", async (importOriginal) => { return { ...actual, getSystemTotalMb: vi.fn(() => 32768) }; }); -import { getSystemTotalMb } from "./systemMemory.js"; +import { getSystemTotalMb, LOW_MEMORY_TOTAL_MB_THRESHOLD } from "./systemMemory.js"; const mockGetSystemTotalMb = vi.mocked(getSystemTotalMb); @@ -336,14 +336,14 @@ describe("memory-adaptive Chrome flags", () => { return parseInt(flag.split("=")[1]!, 10); } - it("does not set heap limit above 8 GB", () => { - mockGetSystemTotalMb.mockReturnValue(16384); + it("does not set heap limit above threshold", () => { + mockGetSystemTotalMb.mockReturnValue(LOW_MEMORY_TOTAL_MB_THRESHOLD + 1); expect(heapFlag(buildChromeArgs(base))).toBeNull(); }); - it("scales heap to total/8 on 8 GB systems", () => { - mockGetSystemTotalMb.mockReturnValue(8192); - expect(heapFlag(buildChromeArgs(base))).toBe(1024); + it("scales heap to total/8 at threshold", () => { + mockGetSystemTotalMb.mockReturnValue(LOW_MEMORY_TOTAL_MB_THRESHOLD); + expect(heapFlag(buildChromeArgs(base))).toBe(Math.floor(LOW_MEMORY_TOTAL_MB_THRESHOLD / 8)); }); it("scales heap to total/8 on 6 GB systems", () => { @@ -351,19 +351,29 @@ describe("memory-adaptive Chrome flags", () => { expect(heapFlag(buildChromeArgs(base))).toBe(768); }); + it("uses proportional heap at 4 GB boundary", () => { + mockGetSystemTotalMb.mockReturnValue(4096); + expect(heapFlag(buildChromeArgs(base))).toBe(512); + }); + it("uses 256 MB heap floor below 4 GB", () => { mockGetSystemTotalMb.mockReturnValue(3072); expect(heapFlag(buildChromeArgs(base))).toBe(256); }); - it("scales GPU budget to total/4 on 8 GB systems", () => { - mockGetSystemTotalMb.mockReturnValue(8192); - expect(gpuBudget(buildChromeArgs(base))).toBe(2048); + it("scales GPU budget to total/4 at threshold", () => { + mockGetSystemTotalMb.mockReturnValue(LOW_MEMORY_TOTAL_MB_THRESHOLD); + expect(gpuBudget(buildChromeArgs(base))).toBe(Math.floor(LOW_MEMORY_TOTAL_MB_THRESHOLD / 4)); + }); + + it("uses proportional GPU budget at 4 GB boundary", () => { + mockGetSystemTotalMb.mockReturnValue(4096); + expect(gpuBudget(buildChromeArgs(base))).toBe(1024); }); it("scales GPU budget to total/2 above threshold", () => { - mockGetSystemTotalMb.mockReturnValue(16384); - expect(gpuBudget(buildChromeArgs(base))).toBe(8192); + mockGetSystemTotalMb.mockReturnValue(LOW_MEMORY_TOTAL_MB_THRESHOLD * 2); + expect(gpuBudget(buildChromeArgs(base))).toBe(LOW_MEMORY_TOTAL_MB_THRESHOLD); }); it("uses 512 MB GPU budget below 4 GB", () => { From ef99eaa11981c0401f728da71865d2d91bd2e57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 6 Jun 2026 04:46:01 +0000 Subject: [PATCH 3/4] test: add threshold edge test at 8193 MiB boundary --- packages/engine/src/services/browserManager.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index f53d14e99..c527fb9ea 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -371,7 +371,14 @@ describe("memory-adaptive Chrome flags", () => { expect(gpuBudget(buildChromeArgs(base))).toBe(1024); }); - it("scales GPU budget to total/2 above threshold", () => { + it("switches to total/2 GPU and no heap limit just above threshold", () => { + const above = LOW_MEMORY_TOTAL_MB_THRESHOLD + 1; + mockGetSystemTotalMb.mockReturnValue(above); + expect(gpuBudget(buildChromeArgs(base))).toBe(Math.floor(above / 2)); + expect(heapFlag(buildChromeArgs(base))).toBeNull(); + }); + + it("scales GPU budget to total/2 well above threshold", () => { mockGetSystemTotalMb.mockReturnValue(LOW_MEMORY_TOTAL_MB_THRESHOLD * 2); expect(gpuBudget(buildChromeArgs(base))).toBe(LOW_MEMORY_TOTAL_MB_THRESHOLD); }); From 712f5ee68f80f049e4546d081e50790dd63865bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 6 Jun 2026 04:49:26 +0000 Subject: [PATCH 4/4] test: add beforeEach for deterministic mock state --- packages/engine/src/services/browserManager.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/engine/src/services/browserManager.test.ts b/packages/engine/src/services/browserManager.test.ts index c527fb9ea..76c91f41f 100644 --- a/packages/engine/src/services/browserManager.test.ts +++ b/packages/engine/src/services/browserManager.test.ts @@ -321,6 +321,10 @@ describe("browser pool", () => { describe("memory-adaptive Chrome flags", () => { const base = { width: 1920, height: 1080 }; + beforeEach(() => { + mockGetSystemTotalMb.mockReturnValue(32768); + }); + afterEach(() => { mockGetSystemTotalMb.mockReturnValue(32768); });