fix(engine): scale Chrome memory budget and frame cache to available RAM

On low-memory systems (<4GB free), Chrome's --force-gpu-mem-available-mb=4096
causes the renderer to allocate more GPU texture memory than the system can
provide, leading to OOM crashes during frame capture ("Target closed").

Changes:
- Scale --force-gpu-mem-available-mb to match available system RAM
  (512MB when <2GB free, 1024MB when <4GB, 4096MB otherwise)
- Add --js-flags=--max-old-space-size=N on low-memory systems to cap
  Chrome's V8 heap (256MB when <2GB free, 512MB when <4GB)
- Scale frame data URI cache defaults: 32 entries/128MB when <2GB free,
  64 entries/256MB when <4GB, unchanged otherwise

Closes #1072
This commit is contained in:
Miguel Ángel
2026-05-25 15:37:13 -04:00
parent c46adb52e2
commit 2015d93d2a
2 changed files with 47 additions and 8 deletions
+25 -5
View File
@@ -208,6 +208,29 @@ export const DEFAULT_CONFIG: EngineConfig = {
debug: false, debug: false,
}; };
function getSystemFreeMb(): number {
try {
const { freemem } = require("os") as typeof import("os");
return Math.floor(freemem() / (1024 * 1024));
} catch {
return 8192;
}
}
function memoryAdaptiveCacheLimit(): number {
const free = getSystemFreeMb();
if (free < 2048) return 32;
if (free < 4096) return 64;
return DEFAULT_CONFIG.frameDataUriCacheLimit;
}
function memoryAdaptiveCacheBytesMb(): number {
const free = getSystemFreeMb();
if (free < 2048) return 128;
if (free < 4096) return 256;
return DEFAULT_CONFIG.frameDataUriCacheBytesLimitMb;
}
/** /**
* Resolve configuration by merging: defaults ← env vars ← explicit overrides. * Resolve configuration by merging: defaults ← env vars ← explicit overrides.
* Env vars provide backward compatibility during migration; explicit config * Env vars provide backward compatibility during migration; explicit config
@@ -298,14 +321,11 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
audioGain: envNum("PRODUCER_AUDIO_GAIN", DEFAULT_CONFIG.audioGain), audioGain: envNum("PRODUCER_AUDIO_GAIN", DEFAULT_CONFIG.audioGain),
frameDataUriCacheLimit: Math.max( frameDataUriCacheLimit: Math.max(
32, 32,
envNum("PRODUCER_FRAME_DATA_URI_CACHE_LIMIT", DEFAULT_CONFIG.frameDataUriCacheLimit), envNum("PRODUCER_FRAME_DATA_URI_CACHE_LIMIT", memoryAdaptiveCacheLimit()),
), ),
frameDataUriCacheBytesLimitMb: Math.max( frameDataUriCacheBytesLimitMb: Math.max(
64, 64,
envNum( envNum("PRODUCER_FRAME_DATA_URI_CACHE_BYTES_MB", memoryAdaptiveCacheBytesMb()),
"PRODUCER_FRAME_DATA_URI_CACHE_BYTES_MB",
DEFAULT_CONFIG.frameDataUriCacheBytesLimitMb,
),
), ),
playerReadyTimeout: envNum( playerReadyTimeout: envNum(
+22 -3
View File
@@ -8,7 +8,7 @@
import type { Browser, PuppeteerNode } from "puppeteer-core"; import type { Browser, PuppeteerNode } from "puppeteer-core";
import { existsSync, readdirSync } from "fs"; import { existsSync, readdirSync } from "fs";
import { join } from "path"; import { join } from "path";
import { homedir } from "os"; import { freemem, homedir } from "os";
import { DEFAULT_CONFIG, type EngineConfig } from "../config.js"; import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
let _puppeteer: PuppeteerNode | undefined; let _puppeteer: PuppeteerNode | undefined;
@@ -476,6 +476,24 @@ export function _setPuppeteerForTests(mock: PuppeteerNode | undefined): void {
_puppeteer = mock; _puppeteer = mock;
} }
function getFreeMb(): number {
return Math.floor(freemem() / (1024 * 1024));
}
function getGpuMemBudgetMb(): number {
const free = getFreeMb();
if (free < 2048) return 512;
if (free < 4096) return 1024;
return 4096;
}
function getLowMemoryFlags(): string[] {
const free = getFreeMb();
if (free >= 4096) return [];
const heapMb = free < 2048 ? 256 : 512;
return [`--js-flags=--max-old-space-size=${heapMb}`];
}
export interface BuildChromeArgsOptions { export interface BuildChromeArgsOptions {
width: number; width: number;
height: number; height: number;
@@ -530,9 +548,10 @@ export function buildChromeArgs(
"--disable-print-preview", "--disable-print-preview",
"--no-pings", "--no-pings",
"--no-zygote", "--no-zygote",
// Memory // Memory — scale GPU budget to available system RAM
"--force-gpu-mem-available-mb=4096", `--force-gpu-mem-available-mb=${getGpuMemBudgetMb()}`,
"--disk-cache-size=268435456", "--disk-cache-size=268435456",
...getLowMemoryFlags(),
// Disable features that add overhead // Disable features that add overhead
"--disable-features=AudioServiceOutOfProcess,IsolateOrigins,site-per-process,Translate,BackForwardCache,IntensiveWakeUpThrottling", "--disable-features=AudioServiceOutOfProcess,IsolateOrigins,site-per-process,Translate,BackForwardCache,IntensiveWakeUpThrottling",
]; ];