mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
feat(producer): auto low-memory safe render profile (#1225)
## What Adds an auto-detected **low-memory safe render profile**. On hosts at or below 8 GB total RAM, the render pipeline collapses to its cheapest shape instead of running multiple concurrent Chrome instances. When `lowMemoryMode` is active and the user hasn't passed `--workers`, the orchestrator: - **skips auto-worker calibration** — no throwaway second Chrome just to time 5 frames; - **pins to a single worker** — so the probe Chrome is reused for capture, never N concurrent; - **prefers screenshot capture over BeginFrame** — avoids the BeginFrame protocol-timeout → relaunch churn on slow hardware; - logs a one-line explanation of what it did and how to override. Builds on #1221 (merged), which fixed the calibration timeout cap, the `<= 8192` boundary, and added the CLI timeout flags. ## Why Reported in #1218 / #1219: renders on 8 GB laptops sit at low progress for minutes or stall. Root cause (per the triage thread) is architectural — the default pipeline launches up to 4 Chrome instances sequentially/overlapping (probe, calibration, capture, screenshot-fallback), each ~256 MB+, on machines with ~3 GB free. The concurrent browsers drive memory pressure that makes every CDP call slow and spikes V8 GC pauses. #1221 made the timeouts and memory flags *apply correctly*; this PR removes the expensive shape entirely on the machines that can't afford it, rather than tuning it. "Smarter by default." ## How - **`packages/engine/src/services/systemMemory.ts`** (new): one shared `isLowMemorySystem()` / `getSystemTotalMb()`, de-duplicating the `totalmem()` reads previously copied in `config.ts` and `browserManager.ts`. Threshold is inclusive (`<= 8192 MB`) — real "8 GB" hardware reports ~7600–8192 MB after firmware/iGPU reservations, so a strict `<` would skip the optimisation on the very hardware that needs it. - **`config.ts`**: new `lowMemoryMode` field on `EngineConfig`, resolved tri-state — explicit override → `PRODUCER_LOW_MEMORY_MODE` (on/off) → auto-detect from total RAM. - **`renderOrchestrator.ts`**: gate calibration off, pin workers to 1, force screenshot capture, and emit a safe-mode log line when `lowMemoryMode` is set and `--workers` is absent. - **`render.ts`**: `--low-memory-mode` / `--no-low-memory-mode` override (sets the env var the producer's `resolveConfig` reads) + docs table entry. Fully overridable: an explicit `--workers N` restores calibration-free parallelism; `--no-low-memory-mode` / `PRODUCER_LOW_MEMORY_MODE=false` restores the full default shape. ### Deliberately deferred (separate PRs) - **Reuse the probe session for calibration**: only executes on the tier *above* 8 GB (safe-mode skips calibration on the target boxes). A correct BeginFrame-mode reuse would lose calibration's fast-fail-to-screenshot timeout — real risk on a path the reported scenario never hits. Better scoped on its own. - **Retuning `calculateOptimalWorkers`'s `totalmem*0.5/256` memory model**: hot path for *all* renders incl. servers/Lambda, outside this PR's local-laptop scope. ## Test plan - [x] Unit tests added/updated — `systemMemory.test.ts` (8192 boundary cases), `config.test.ts` (tri-state env resolution + explicit-override precedence). Engine suite passes (25 relevant tests). - [x] `tsc` clean across engine/producer/cli; `oxlint` + `oxfmt` clean; removed an unused export so the `fallow --fail-on-issues` dead-code gate stays green. - [x] Documentation updated — `docs/packages/cli.mdx` render-flags table. - [ ] Manual testing on a real ≤ 8 GB host — not yet run; behaviour is unit-covered and the safe path (1 worker + screenshot) is already a supported render shape. Note: one pre-existing producer test (`rejects a maliciously crafted key…`) fails identically on `main` — environment-specific path test, unrelated to this change.
This commit is contained in:
@@ -6,7 +6,11 @@
|
||||
* fallbacks for backward compatibility during migration.
|
||||
*/
|
||||
|
||||
import { totalmem } from "os";
|
||||
import {
|
||||
getSystemTotalMb,
|
||||
isLowMemorySystem,
|
||||
LOW_MEMORY_TOTAL_MB_THRESHOLD,
|
||||
} from "./services/systemMemory.js";
|
||||
|
||||
/**
|
||||
* Full engine configuration. All fields are wired through the config
|
||||
@@ -50,6 +54,16 @@ export interface EngineConfig {
|
||||
expectedChromiumMajor?: number;
|
||||
/** Force screenshot capture mode (skip BeginFrame even on Linux). */
|
||||
forceScreenshot: boolean;
|
||||
/**
|
||||
* Low-memory render profile. When `true`, the orchestrator collapses the
|
||||
* pipeline to its cheapest shape on memory-constrained hosts: it skips the
|
||||
* throwaway auto-worker calibration browser, pins capture to a single
|
||||
* worker (unless the user passed an explicit `--workers`), and prefers
|
||||
* screenshot capture over BeginFrame. Resolved automatically from total
|
||||
* RAM (`isLowMemorySystem()`); force on/off via `PRODUCER_LOW_MEMORY_MODE`
|
||||
* or the `--low-memory-mode` CLI flag.
|
||||
*/
|
||||
lowMemoryMode: boolean;
|
||||
/**
|
||||
* Opt-in: page-side shader-transition compositing.
|
||||
*
|
||||
@@ -194,6 +208,9 @@ export const DEFAULT_CONFIG: EngineConfig = {
|
||||
browserTimeout: 120_000,
|
||||
protocolTimeout: 300_000,
|
||||
forceScreenshot: false,
|
||||
// Auto-detected per host in `resolveConfig`; defaults off for the raw
|
||||
// DEFAULT_CONFIG (used directly by tests and worker-sizing fallbacks).
|
||||
lowMemoryMode: false,
|
||||
enablePageSideCompositing: true,
|
||||
|
||||
enableChunkedEncode: false,
|
||||
@@ -221,21 +238,17 @@ export const DEFAULT_CONFIG: EngineConfig = {
|
||||
debug: false,
|
||||
};
|
||||
|
||||
function getSystemTotalMb(): number {
|
||||
return Math.floor(totalmem() / (1024 * 1024));
|
||||
}
|
||||
|
||||
function memoryAdaptiveCacheLimit(): number {
|
||||
const total = getSystemTotalMb();
|
||||
if (total < 4096) return 32;
|
||||
if (total <= 8192) return 64;
|
||||
if (total <= LOW_MEMORY_TOTAL_MB_THRESHOLD) return 64;
|
||||
return DEFAULT_CONFIG.frameDataUriCacheLimit;
|
||||
}
|
||||
|
||||
function memoryAdaptiveCacheBytesMb(): number {
|
||||
const total = getSystemTotalMb();
|
||||
if (total < 4096) return 128;
|
||||
if (total <= 8192) return 256;
|
||||
if (total <= LOW_MEMORY_TOTAL_MB_THRESHOLD) return 256;
|
||||
return DEFAULT_CONFIG.frameDataUriCacheBytesLimitMb;
|
||||
}
|
||||
|
||||
@@ -262,6 +275,13 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
|
||||
if (raw === "hardware" || raw === "software" || raw === "auto") return raw;
|
||||
return DEFAULT_CONFIG.browserGpuMode;
|
||||
};
|
||||
// Tri-state: explicit on/off via env, otherwise auto-detect from total RAM.
|
||||
const resolveLowMemoryMode = (): boolean => {
|
||||
const raw = env("PRODUCER_LOW_MEMORY_MODE")?.toLowerCase();
|
||||
if (raw === "true" || raw === "on" || raw === "1") return true;
|
||||
if (raw === "false" || raw === "off" || raw === "0") return false;
|
||||
return isLowMemorySystem();
|
||||
};
|
||||
|
||||
// Env-var layer (backward compat)
|
||||
const fromEnv: Partial<EngineConfig> = {
|
||||
@@ -287,6 +307,7 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
|
||||
: undefined,
|
||||
|
||||
forceScreenshot: envBool("PRODUCER_FORCE_SCREENSHOT", DEFAULT_CONFIG.forceScreenshot),
|
||||
lowMemoryMode: resolveLowMemoryMode(),
|
||||
enablePageSideCompositing: envBool(
|
||||
"HF_PAGE_SIDE_COMPOSITING",
|
||||
DEFAULT_CONFIG.enablePageSideCompositing,
|
||||
|
||||
Reference in New Issue
Block a user