Skip to content

Commit 4f44bfc

Browse files
authored
Merge pull request #98 from heygen-com/feat/cli-smart-worker-default
feat(cli): smart default worker count based on CPU cores
2 parents e3fad30 + 9012a89 commit 4f44bfc

2 files changed

Lines changed: 114 additions & 26 deletions

File tree

docs/guides/rendering.mdx

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,19 +116,64 @@ Render your Hyperframes [compositions](/concepts/compositions) to MP4 with the [
116116
| `--output` | path | `renders/<name>.mp4` | Output file path |
117117
| `--fps` | 24, 30, 60 | 30 | Frames per second |
118118
| `--quality` | draft, standard, high | standard | Encoding quality preset |
119-
| `--workers` | 1-8 | 4 | Parallel render workers |
119+
| `--workers` | 1-8 or `auto` | auto | Parallel render workers (see [Workers](#workers) below) |
120120
| `--gpu` || off | GPU encoding (NVENC, VideoToolbox, VAAPI) |
121121
| `--docker` || off | Use Docker for [deterministic rendering](/concepts/determinism) |
122122
| `--quiet` || off | Suppress verbose output |
123123

124+
## Workers
125+
126+
Each render worker launches a **separate Chrome browser process** to capture frames in parallel. More workers can speed up rendering, but each one consumes ~256 MB of RAM and significant CPU.
127+
128+
### Default behavior
129+
130+
By default, Hyperframes uses **half of your CPU cores, capped at 4**:
131+
132+
| Machine | CPU cores | Default workers |
133+
|---------|-----------|----------------|
134+
| MacBook Air (M1) | 8 | 4 |
135+
| MacBook Pro (M3) | 12 | 4 (capped) |
136+
| 4-core laptop | 4 | 2 |
137+
| 2-core VM | 2 | 1 |
138+
139+
This is intentionally conservative. Each worker spawns its own Chrome process, so the per-worker overhead is significant. Fewer workers avoids resource contention with FFmpeg encoding and your other applications.
140+
141+
### Choosing a worker count
142+
143+
```bash Terminal
144+
# Explicit worker count
145+
npx hyperframes render --workers 1 --output output.mp4
146+
147+
# Let Hyperframes pick based on your CPU
148+
npx hyperframes render --workers auto --output output.mp4
149+
150+
# Maximum parallelism (use with caution on laptops)
151+
npx hyperframes render --workers 8 --output output.mp4
152+
```
153+
154+
<Tip>
155+
Start with the default. If renders feel slow and your system has headroom (check Activity Monitor / `htop`), try increasing `--workers`. If you see high memory pressure or fan noise, reduce it.
156+
</Tip>
157+
158+
### When to use 1 worker
159+
160+
- Short compositions (under 2 seconds / 60 frames) — parallelism overhead exceeds the benefit
161+
- Low-memory machines (4 GB or less)
162+
- Running renders alongside other heavy processes (video editing, large builds)
163+
164+
### When to increase workers
165+
166+
- Long compositions (30+ seconds) on a machine with 8+ cores and 16+ GB RAM
167+
- Dedicated render machines or CI runners
168+
- Docker mode on a well-provisioned host
169+
124170
## Tips
125171

126172
<Tip>
127173
Use `draft` quality during development for fast previews. Switch to `standard` or `high` for final output.
128174
</Tip>
129175

130176
- Use `npx hyperframes benchmark` to find optimal settings for your system
131-
- 4 workers is usually the sweet spot for most compositions
132177
- Docker mode is slower but guarantees [identical output](/concepts/determinism) across platforms
133178
- For compositions with many frames, `--gpu` can significantly speed up local encoding
134179

packages/cli/src/commands/render.ts

Lines changed: 67 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { defineCommand } from "citty";
22
import { existsSync, mkdirSync, statSync } from "node:fs";
3+
import { cpus } from "node:os";
34
import { resolve, dirname, join } from "node:path";
45
import { resolveProject } from "../utils/project.js";
56
import { loadProducer } from "../utils/producer.js";
@@ -12,6 +13,13 @@ const VALID_FPS = new Set([24, 30, 60]);
1213
const VALID_QUALITY = new Set(["draft", "standard", "high"]);
1314
const VALID_FORMAT = new Set(["mp4", "webm"]);
1415

16+
const CPU_CORE_COUNT = cpus().length;
17+
18+
/** Half of CPU cores, capped at 4. Each worker spawns a Chrome process (~256 MB). */
19+
function defaultWorkerCount(): number {
20+
return Math.max(1, Math.min(Math.floor(CPU_CORE_COUNT / 2), 4));
21+
}
22+
1523
export default defineCommand({
1624
meta: {
1725
name: "render",
@@ -24,19 +32,47 @@ Examples:
2432
hyperframes render --docker --output deterministic.mp4`,
2533
},
2634
args: {
27-
dir: { type: "positional", description: "Project directory", required: false },
28-
output: { type: "string", description: "Output path (default: renders/<name>.mp4)" },
29-
fps: { type: "string", description: "Frame rate: 24, 30, 60", default: "30" },
30-
quality: { type: "string", description: "Quality: draft, standard, high", default: "standard" },
35+
dir: {
36+
type: "positional",
37+
description: "Project directory",
38+
required: false,
39+
},
40+
output: {
41+
type: "string",
42+
description: "Output path (default: renders/<name>.mp4)",
43+
},
44+
fps: {
45+
type: "string",
46+
description: "Frame rate: 24, 30, 60",
47+
default: "30",
48+
},
49+
quality: {
50+
type: "string",
51+
description: "Quality: draft, standard, high",
52+
default: "standard",
53+
},
3154
format: {
3255
type: "string",
3356
description: "Output format: mp4, webm (WebM renders with transparency)",
3457
default: "mp4",
3558
},
36-
workers: { type: "string", description: "Parallel workers 1-8" },
37-
docker: { type: "boolean", description: "Use Docker for deterministic render", default: false },
59+
workers: {
60+
type: "string",
61+
description:
62+
"Parallel render workers (1-8 or 'auto'). Default: half your CPU cores, max 4. " +
63+
"Each worker launches a separate Chrome process.",
64+
},
65+
docker: {
66+
type: "boolean",
67+
description: "Use Docker for deterministic render",
68+
default: false,
69+
},
3870
gpu: { type: "boolean", description: "Use GPU encoding", default: false },
39-
quiet: { type: "boolean", description: "Suppress verbose output", default: false },
71+
quiet: {
72+
type: "boolean",
73+
description: "Suppress verbose output",
74+
default: false,
75+
},
4076
},
4177
async run({ args }) {
4278
// ── Resolve project ────────────────────────────────────────────────────
@@ -68,10 +104,10 @@ Examples:
68104

69105
// ── Validate workers ──────────────────────────────────────────────────
70106
let workers: number | undefined;
71-
if (args.workers != null) {
107+
if (args.workers != null && args.workers !== "auto") {
72108
const parsed = parseInt(args.workers, 10);
73109
if (isNaN(parsed) || parsed < 1 || parsed > 8) {
74-
errorBox("Invalid workers", `Got "${args.workers}". Must be between 1 and 8.`);
110+
errorBox("Invalid workers", `Got "${args.workers}". Must be 1-8 or "auto".`);
75111
process.exit(1);
76112
}
77113
workers = parsed;
@@ -85,28 +121,27 @@ Examples:
85121
: join(rendersDir, `${project.name}${ext}`);
86122

87123
// Ensure output directory exists
88-
const outputDir = dirname(outputPath);
89-
if (!existsSync(outputDir)) {
90-
mkdirSync(outputDir, { recursive: true });
91-
}
124+
mkdirSync(dirname(outputPath), { recursive: true });
92125

93126
const useDocker = args.docker ?? false;
94127
const useGpu = args.gpu ?? false;
95128
const quiet = args.quiet ?? false;
96129

97130
// ── Print render plan ─────────────────────────────────────────────────
98-
const workerCount = workers ?? 4;
131+
const workerCount = workers ?? defaultWorkerCount();
99132
if (!quiet) {
133+
const workerLabel =
134+
args.workers != null
135+
? `${workerCount} workers`
136+
: `${workerCount} workers (auto \u2014 half of ${CPU_CORE_COUNT} cores)`;
100137
console.log("");
101138
console.log(
102139
c.accent("\u25C6") +
103140
" Rendering " +
104141
c.accent(project.name) +
105142
c.dim(" \u2192 " + outputPath),
106143
);
107-
console.log(
108-
c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerCount + " workers"),
109-
);
144+
console.log(c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerLabel));
110145
console.log("");
111146
}
112147

@@ -159,7 +194,7 @@ Examples:
159194
fps,
160195
quality,
161196
format,
162-
workers,
197+
workers: workerCount,
163198
gpu: useGpu,
164199
quiet,
165200
});
@@ -168,7 +203,7 @@ Examples:
168203
fps,
169204
quality,
170205
format,
171-
workers,
206+
workers: workerCount,
172207
gpu: useGpu,
173208
quiet,
174209
browserPath,
@@ -181,7 +216,7 @@ interface RenderOptions {
181216
fps: 24 | 30 | 60;
182217
quality: "draft" | "standard" | "high";
183218
format: "mp4" | "webm";
184-
workers?: number;
219+
workers: number;
185220
gpu: boolean;
186221
quiet: boolean;
187222
browserPath?: string;
@@ -205,7 +240,11 @@ async function renderDocker(
205240
});
206241
await producer.executeRenderJob(job, projectDir, outputPath);
207242
} catch (error: unknown) {
208-
trackRenderError({ fps: options.fps, quality: options.quality, docker: true });
243+
trackRenderError({
244+
fps: options.fps,
245+
quality: options.quality,
246+
docker: true,
247+
});
209248
const message = error instanceof Error ? error.message : String(error);
210249
errorBox("Render failed", message, "Check Docker is running: docker info");
211250
process.exit(1);
@@ -216,7 +255,7 @@ async function renderDocker(
216255
durationMs: elapsed,
217256
fps: options.fps,
218257
quality: options.quality,
219-
workers: options.workers ?? 4,
258+
workers: options.workers,
220259
docker: true,
221260
gpu: options.gpu,
222261
});
@@ -256,7 +295,11 @@ async function renderLocal(
256295
try {
257296
await producer.executeRenderJob(job, projectDir, outputPath, onProgress);
258297
} catch (error: unknown) {
259-
trackRenderError({ fps: options.fps, quality: options.quality, docker: false });
298+
trackRenderError({
299+
fps: options.fps,
300+
quality: options.quality,
301+
docker: false,
302+
});
260303
const message = error instanceof Error ? error.message : String(error);
261304
errorBox("Render failed", message, "Try --docker for containerized rendering");
262305
process.exit(1);
@@ -267,7 +310,7 @@ async function renderLocal(
267310
durationMs: elapsed,
268311
fps: options.fps,
269312
quality: options.quality,
270-
workers: options.workers ?? 4,
313+
workers: options.workers,
271314
docker: false,
272315
gpu: options.gpu,
273316
});

0 commit comments

Comments
 (0)