Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions packages/engine/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
scaleProtocolTimeoutForComposition,
shouldClampToScreenshotForConcreteGpu,
applyConcreteGpuScreenshotClamp,
shouldAutoDisableStreamingEncodeOnWin32Compound,
} from "./config.js";
import type { EngineConfig } from "./config.js";
import { isLowMemorySystem } from "./services/systemMemory.js";
Expand Down Expand Up @@ -518,6 +519,216 @@ describe("resolveConfig", () => {
});
});

describe("shouldAutoDisableStreamingEncodeOnWin32Compound (helper)", () => {
// Baseline: field-signal compound — win32 + software-GPU forced + workers=1,
// duration unknown, user hasn't touched the env / overrides.
const compound = {
platform: "win32" as NodeJS.Platform,
softwareGpuForced: true,
workers: 1,
compositionDurationSec: undefined as number | undefined,
userExplicitlySet: false,
};

it("triggers on the field-signal compound (win32 + software-GPU + workers=1)", () => {
expect(shouldAutoDisableStreamingEncodeOnWin32Compound(compound)).toBe(true);
});

it("does NOT trigger on linux or darwin (platform gate)", () => {
expect(
shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, platform: "linux" }),
).toBe(false);
expect(
shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, platform: "darwin" }),
).toBe(false);
});

it("does NOT trigger without software-GPU forced (bypass on hardware paths)", () => {
expect(
shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, softwareGpuForced: false }),
).toBe(false);
});

it("does NOT trigger with parallel workers (workers > 1 has a different failure surface)", () => {
expect(shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, workers: 2 })).toBe(
false,
);
expect(shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, workers: 4 })).toBe(
false,
);
});

it("does NOT trigger when the user explicitly set enableStreamingEncode (escape hatch)", () => {
expect(
shouldAutoDisableStreamingEncodeOnWin32Compound({ ...compound, userExplicitlySet: true }),
).toBe(false);
});

it("does NOT trigger for short (<=120s) compositions when duration is known", () => {
// 120s boundary is off (edge)
expect(
shouldAutoDisableStreamingEncodeOnWin32Compound({
...compound,
compositionDurationSec: 120,
}),
).toBe(false);
// 60s — clearly short, off
expect(
shouldAutoDisableStreamingEncodeOnWin32Compound({
...compound,
compositionDurationSec: 60,
}),
).toBe(false);
});

it("triggers when duration is known and exceeds 120s (heavy Windows composition)", () => {
// 121s — just past the boundary, on
expect(
shouldAutoDisableStreamingEncodeOnWin32Compound({
...compound,
compositionDurationSec: 121,
}),
).toBe(true);
// 156s — matches the field signal, on
expect(
shouldAutoDisableStreamingEncodeOnWin32Compound({
...compound,
compositionDurationSec: 156,
}),
).toBe(true);
});

it("triggers when duration is undefined (config-layer wire-up reduces to 3-cond)", () => {
// resolveConfig can't see composition duration at config time; the
// three-condition compound is conservative on its own.
expect(
shouldAutoDisableStreamingEncodeOnWin32Compound({
...compound,
compositionDurationSec: undefined,
}),
).toBe(true);
});
});

describe("enableStreamingEncode (Windows compound auto-disable wire-up)", () => {
const originalPlatform = process.platform;

function setPlatform(platform: NodeJS.Platform) {
Object.defineProperty(process, "platform", { value: platform, configurable: true });
}

afterEach(() => {
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
});

it("auto-disables on win32 + software-GPU + workers=1 + no user opt-in", () => {
setPlatform("win32");
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
setEnv("PRODUCER_MAX_WORKERS", "1");
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
unsetEnv("PRODUCER_ENABLE_STREAMING_ENCODE");

const config = resolveConfig();
expect(config.enableStreamingEncode).toBe(false);
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBe(true);
});

it("leaves streaming-encode on when platform is linux", () => {
setPlatform("linux");
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
setEnv("PRODUCER_MAX_WORKERS", "1");
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
unsetEnv("PRODUCER_ENABLE_STREAMING_ENCODE");

const config = resolveConfig();
expect(config.enableStreamingEncode).toBe(true);
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBeUndefined();
});

it("leaves streaming-encode on when workers > 1", () => {
setPlatform("win32");
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
setEnv("PRODUCER_MAX_WORKERS", "4");
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
unsetEnv("PRODUCER_ENABLE_STREAMING_ENCODE");

const config = resolveConfig();
expect(config.enableStreamingEncode).toBe(true);
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBeUndefined();
});

it("respects explicit env opt-in (PRODUCER_ENABLE_STREAMING_ENCODE=true) on the compound", () => {
setPlatform("win32");
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
setEnv("PRODUCER_MAX_WORKERS", "1");
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
setEnv("PRODUCER_ENABLE_STREAMING_ENCODE", "true");

const config = resolveConfig();
expect(config.enableStreamingEncode).toBe(true);
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBeUndefined();
});

it("respects programmatic override enableStreamingEncode=true on the compound", () => {
setPlatform("win32");
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
setEnv("PRODUCER_MAX_WORKERS", "1");
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
unsetEnv("PRODUCER_ENABLE_STREAMING_ENCODE");

const config = resolveConfig({ enableStreamingEncode: true });
expect(config.enableStreamingEncode).toBe(true);
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBeUndefined();
});

it("triggers via disableGpu on win32 + workers=1 (browserGpuMode may still be 'auto')", () => {
// --disable-gpu path: browserGpuMode may not be literal "software" but
// Chrome is still routed to CPU raster. Field-signal compound applies.
setPlatform("win32");
unsetEnv("PRODUCER_BROWSER_GPU_MODE");
setEnv("PRODUCER_DISABLE_GPU", "true");
setEnv("PRODUCER_MAX_WORKERS", "1");
unsetEnv("PRODUCER_LOW_MEMORY_MODE");
unsetEnv("PRODUCER_ENABLE_STREAMING_ENCODE");

const config = resolveConfig();
expect(config.enableStreamingEncode).toBe(false);
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBe(true);
});

it("triggers via lowMemoryMode alone on win32 + workers=1 (screenshot capture implied)", () => {
// --low-memory-mode implies screenshot capture. Compound applies even
// if browserGpuMode is not literal "software" (defense-in-depth: matches
// the OR semantics in `softwareGpuForced`).
setPlatform("win32");
unsetEnv("PRODUCER_BROWSER_GPU_MODE");
unsetEnv("PRODUCER_DISABLE_GPU");
setEnv("PRODUCER_MAX_WORKERS", "1");
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
unsetEnv("PRODUCER_ENABLE_STREAMING_ENCODE");

const config = resolveConfig();
expect(config.enableStreamingEncode).toBe(false);
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBe(true);
});

it("does not trigger when concurrency is 'auto' (workers not explicitly pinned)", () => {
// Config-layer sees `concurrency === "auto"`, not a number — the
// helper's numeric workers check treats NaN as "unknown, don't trigger".
// Downstream workers may still resolve to 1 via lowMemoryMode, but the
// config-time clamp is deliberately conservative.
setPlatform("win32");
setEnv("PRODUCER_BROWSER_GPU_MODE", "software");
unsetEnv("PRODUCER_MAX_WORKERS");
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
unsetEnv("PRODUCER_ENABLE_STREAMING_ENCODE");

const config = resolveConfig();
expect(config.enableStreamingEncode).toBe(true);
expect(config.streamingEncodeAutoDisabledOnWin32Compound).toBeUndefined();
});
});

describe("lowMemoryMode", () => {
it("forces on for truthy PRODUCER_LOW_MEMORY_MODE values", () => {
setEnv("PRODUCER_LOW_MEMORY_MODE", "true");
Expand Down
111 changes: 111 additions & 0 deletions packages/engine/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ export interface EngineConfig {
enableChunkedEncode: boolean;
chunkSizeFrames: number;
enableStreamingEncode: boolean;
/**
* INTERNAL. Set by `resolveConfig` when the Windows software-GPU compound
* heuristic (`shouldAutoDisableStreamingEncodeOnWin32Compound`) turned
* `enableStreamingEncode` off on the caller's behalf. Not intended to be
* set by callers; surfaces the auto-decision for downstream observability
* (log lines, telemetry) so operators can tell an auto-disable apart from
* an explicit user opt-out.
*/
streamingEncodeAutoDisabledOnWin32Compound?: boolean;
/**
* Max composition duration eligible for streaming encode (seconds).
* Mirrors GSAP rendering's 4-minute streaming guard: production has seen
Expand Down Expand Up @@ -355,6 +364,62 @@ export function scaleProtocolTimeoutForComposition(
return Math.min(ceiling, Math.max(baseTimeoutMs, scaled));
}

/**
* Auto-disable `enableStreamingEncode` on Windows software-GPU compound.
*
* Field signal (`ts=1784131903`, win32/x64, CLI 0.7.58, 156s UI-heavy
* composition): the render was stable ONLY with FOUR flags together —
* `--workers 1 --no-browser-gpu --low-memory-mode` + explicit
* `PRODUCER_ENABLE_STREAMING_ENCODE=false`. Every recent Windows-related
* fix (#2359, #2245, #2298, #2331) already shipped in 0.7.58; the residual
* failure is screenshot streaming-encode via CDP `Page.captureScreenshot`
* on Windows even after software fallback. Since `--low-memory-mode` and
* `--no-browser-gpu` already imply screenshot capture, three of the four
* flags are structurally coupled — auto-detect the compound and disable
* streaming-encode automatically so callers don't have to memorize the
* four-flag combination.
*
* Conservative gates (all must hold):
* 1. `platform === "win32"` — the failure is Windows-specific to CDP's
* screenshot streaming path.
* 2. `softwareGpuForced` — the render is already on the SwiftShader /
* forced-screenshot path (from `--no-browser-gpu`, `disableGpu`, or
* `--low-memory-mode` implying screenshot capture).
* 3. `workers === 1` — the field signal reproduces on single-worker
* captures; parallel workers have a different failure surface
* (missing media frames) already handled by the worker-count route.
* 4. Composition duration >120s WHEN KNOWN. When unknown at the config
* layer (composition duration is parsed downstream), the guard
* reduces to the three-condition compound. Trade-off documented in
* the PR body: false positives possible for short (~<120s) Windows
* software-GPU single-worker renders. Mitigation: the explicit
* opt-in escape hatch (`PRODUCER_ENABLE_STREAMING_ENCODE=true` or
* `overrides.enableStreamingEncode !== undefined`) always wins.
*
* Pure function; exported for tests.
*/
export function shouldAutoDisableStreamingEncodeOnWin32Compound(opts: {
platform: NodeJS.Platform;
softwareGpuForced: boolean;
workers: number;
compositionDurationSec: number | undefined;
userExplicitlySet: boolean;
}): boolean {
if (opts.userExplicitlySet) return false;
if (opts.platform !== "win32") return false;
if (!opts.softwareGpuForced) return false;
// Strict equality: NaN (concurrency: "auto") and fractional / zero worker
// counts do NOT match. The field-signal compound is `--workers 1`.
if (opts.workers !== 1) return false;
// Duration boundary: when known, only auto-disable if >120s (avoid
// over-triggering on short renders). When unknown, skip this check —
// the three conditions above are already conservative on their own.
if (opts.compositionDurationSec !== undefined && opts.compositionDurationSec <= 120) {
return false;
}
return true;
}

function memoryAdaptiveCacheLimit(): number {
const total = getSystemTotalMb();
if (total < 4096) return 32;
Expand Down Expand Up @@ -608,6 +673,52 @@ export function resolveConfig(overrides?: Partial<EngineConfig>): EngineConfig {
merged.forceScreenshot = true;
}

// Windows software-GPU compound auto-disable for streaming-encode.
//
// Field signal ts=1784131903 (win32/x64, CLI 0.7.58, 156s UI-heavy):
// stable ONLY with FOUR flags — `--workers 1 --no-browser-gpu
// --low-memory-mode` + `PRODUCER_ENABLE_STREAMING_ENCODE=false`. Since
// `--no-browser-gpu` and `--low-memory-mode` already imply screenshot
// capture, three of the four flags are structurally coupled: auto-detect
// the compound and disable streaming-encode on the caller's behalf.
//
// Explicit user intent wins: if `PRODUCER_ENABLE_STREAMING_ENCODE` env
// is set to any value OR the caller passed `overrides.enableStreamingEncode`,
// this clamp is a no-op (the user's explicit choice — including
// `PRODUCER_ENABLE_STREAMING_ENCODE=true` — is preserved).
//
// Composition duration is not known at the config-resolution layer
// (the composition is parsed downstream of `resolveConfig`), so this
// wire-up passes `compositionDurationSec: undefined` and the helper
// reduces to the three-condition compound. Trade-off documented in the
// helper's JSDoc and the PR body: false positives possible for short
// Windows software-GPU single-worker renders; the explicit opt-in
// escape hatch is the mitigation.
const streamingEncodeUserExplicitlySet =
env("PRODUCER_ENABLE_STREAMING_ENCODE") !== undefined ||
overrides?.enableStreamingEncode !== undefined;
const softwareGpuForced =
merged.browserGpuMode === "software" || merged.disableGpu || merged.lowMemoryMode;
const resolvedWorkers = typeof merged.concurrency === "number" ? merged.concurrency : NaN;
if (
merged.enableStreamingEncode &&
shouldAutoDisableStreamingEncodeOnWin32Compound({
platform: process.platform,
softwareGpuForced,
workers: resolvedWorkers,
compositionDurationSec: undefined,
userExplicitlySet: streamingEncodeUserExplicitlySet,
})
) {
merged.enableStreamingEncode = false;
merged.streamingEncodeAutoDisabledOnWin32Compound = true;
console.error(
"[hyperframes] Windows compound-workaround auto-detected — disabling streaming-encode " +
"(platform=win32, software-GPU forced, workers=1). Field signal ts=1784131903. " +
"Override: PRODUCER_ENABLE_STREAMING_ENCODE=true.",
);
}

// drawElement capture and page-side shader compositing are mutually
// incompatible capture strategies (drawElement reads paint records directly
// and bypasses the page-side prepare→composite→resolve protocol). When
Expand Down
Loading