mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix: budget workers for expensive captures
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { distributeFrames } from "./parallelCoordinator.js";
|
||||
import { calculateOptimalWorkers, distributeFrames } from "./parallelCoordinator.js";
|
||||
|
||||
describe("distributeFrames", () => {
|
||||
it("distributes frames evenly across workers", () => {
|
||||
@@ -42,3 +42,29 @@ describe("distributeFrames", () => {
|
||||
expect(tasks.map((t) => t.workerId)).toEqual([0, 1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateOptimalWorkers", () => {
|
||||
it("lets high-cost auto renders fall back to one worker when CPU budget requires it", () => {
|
||||
const workers = calculateOptimalWorkers(180, undefined, {
|
||||
concurrency: 6,
|
||||
coresPerWorker: 100,
|
||||
minParallelFrames: 120,
|
||||
largeRenderThreshold: 1000,
|
||||
captureCostMultiplier: 4,
|
||||
});
|
||||
|
||||
expect(workers).toBe(1);
|
||||
});
|
||||
|
||||
it("does not apply capture cost to explicit worker requests", () => {
|
||||
const workers = calculateOptimalWorkers(180, 4, {
|
||||
concurrency: 6,
|
||||
coresPerWorker: 100,
|
||||
minParallelFrames: 120,
|
||||
largeRenderThreshold: 1000,
|
||||
captureCostMultiplier: 4,
|
||||
});
|
||||
|
||||
expect(workers).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,20 @@ export interface ParallelProgress {
|
||||
workerProgress: Map<number, number>;
|
||||
}
|
||||
|
||||
export interface WorkerSizingConfig extends Partial<
|
||||
Pick<
|
||||
EngineConfig,
|
||||
"concurrency" | "coresPerWorker" | "minParallelFrames" | "largeRenderThreshold"
|
||||
>
|
||||
> {
|
||||
/**
|
||||
* Relative per-frame capture cost for auto worker sizing. Values above 1
|
||||
* represent compositions that put more CPU pressure on each Chrome worker
|
||||
* than a plain DOM screenshot. Explicit --workers requests ignore this hint.
|
||||
*/
|
||||
captureCostMultiplier?: number;
|
||||
}
|
||||
|
||||
const MEMORY_PER_WORKER_MB = 256;
|
||||
const MIN_WORKERS = 1;
|
||||
const ABSOLUTE_MAX_WORKERS = 10;
|
||||
@@ -57,12 +71,7 @@ const MIN_FRAMES_PER_WORKER = 30;
|
||||
export function calculateOptimalWorkers(
|
||||
totalFrames: number,
|
||||
requested?: number,
|
||||
config?: Partial<
|
||||
Pick<
|
||||
EngineConfig,
|
||||
"concurrency" | "coresPerWorker" | "minParallelFrames" | "largeRenderThreshold"
|
||||
>
|
||||
>,
|
||||
config?: WorkerSizingConfig,
|
||||
): number {
|
||||
// Resolve effective values: config overrides → DEFAULT_CONFIG fallback.
|
||||
const effectiveMaxWorkers = (() => {
|
||||
@@ -76,6 +85,7 @@ export function calculateOptimalWorkers(
|
||||
const effectiveMinParallelFrames = config?.minParallelFrames ?? DEFAULT_CONFIG.minParallelFrames;
|
||||
const effectiveLargeRenderThreshold =
|
||||
config?.largeRenderThreshold ?? DEFAULT_CONFIG.largeRenderThreshold;
|
||||
const captureCostMultiplier = Math.max(1, config?.captureCostMultiplier ?? 1);
|
||||
|
||||
if (requested !== undefined) {
|
||||
return Math.max(MIN_WORKERS, Math.min(effectiveMaxWorkers, requested));
|
||||
@@ -98,13 +108,19 @@ export function calculateOptimalWorkers(
|
||||
const minWorkersForJob = totalFrames >= effectiveMinParallelFrames ? 2 : MIN_WORKERS;
|
||||
let finalWorkers = Math.max(minWorkersForJob, Math.min(effectiveMaxWorkers, optimal));
|
||||
|
||||
// Adaptive scaling: cap workers for large renders to prevent CPU contention.
|
||||
// Each Chrome process (with SwiftShader) is CPU-heavy; too many on a long
|
||||
// render causes protocol timeouts from compositor starvation.
|
||||
// Scale proportionally to CPU count: ~3 cores per worker (benchmarked).
|
||||
// Adaptive scaling: cap workers for large or expensive renders to prevent
|
||||
// CPU contention. Each Chrome process (with SwiftShader) is CPU-heavy; too
|
||||
// many concurrent captures can starve the compositor and surface as CDP
|
||||
// protocol timeouts. Scale proportionally to CPU count and composition cost:
|
||||
// 8 cores → 2 workers, 16 cores → 5 workers, 32 cores → 10 workers.
|
||||
if (totalFrames >= effectiveLargeRenderThreshold) {
|
||||
const cpuScaledMax = Math.max(2, Math.floor(cpuCount / effectiveCoresPerWorker));
|
||||
const weightedFrames = totalFrames * captureCostMultiplier;
|
||||
const contentionThreshold = Math.max(
|
||||
effectiveMinParallelFrames,
|
||||
Math.floor(effectiveLargeRenderThreshold / 3),
|
||||
);
|
||||
if (totalFrames >= effectiveLargeRenderThreshold || weightedFrames >= contentionThreshold) {
|
||||
const weightedCoresPerWorker = effectiveCoresPerWorker * captureCostMultiplier;
|
||||
const cpuScaledMax = Math.max(MIN_WORKERS, Math.floor(cpuCount / weightedCoresPerWorker));
|
||||
if (finalWorkers > cpuScaledMax) {
|
||||
finalWorkers = cpuScaledMax;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user