feat(producer): lower parallel-DE router floor to 700 frames + power-state telemetry

HF_DE_PARALLEL_MIN_FRAMES default 2000 -> 700, re-calibrated by a controlled
crossover sweep (fixed content-per-frame, three synthetic profiles x
{350..3000f} x {single,par2,par3} x 3 reps, resolved worker counts and capture
modes verified per run): par3 beats single at EVERY size in every profile —
+17-21% at 700f rising to +28-34% at 3000f. That includes a
24-sub-composition profile built specifically to reproduce the 'workers
re-pay init' failure the original 2000 floor guarded against (92k tweens,
~2.5s pollSubCompositionTimelines per worker): workers initialize
concurrently, so duplicated init costs CPU, not wall-clock, and the comp
still parallelizes +19% at 700f. Below ~700f the win thins toward +10%
while paying three hardware-GPU browsers, so a floor remains. par2 loses to
par3 in every cell of every profile — the router's existing 3-worker pin is
confirmed, not changed. Harness:
plans/drawelement-fast-capture/de-crossover-bench.sh (docs repo).

Also adds on_battery / low_power_mode to render_complete and render_error.
The DE fleet is macOS laptops, and bench sweeps on an M4 Pro caught the SAME
render flipping between ~9.6 and ~17.2 ms/frame power-management regimes
with no existing telemetry signal to segment by — the router soak reading
this change needs that dimension to interpret perf on the machines users
actually render on. Sampled per event (volatile), pmset-based, darwin-only,
null-safe on failure.

Router stays default-off behind HF_DE_PARALLEL_ROUTER; this tunes what it
will do when the soak clears it to flip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-27 22:51:28 -07:00
co-authored by Claude Opus 5
parent 97ec7db5cc
commit 3da31e399b
4 changed files with 143 additions and 8 deletions
+16
View File
@@ -3,6 +3,20 @@ import type { SubTimelineWaitOutcome } from "@hyperframes/engine";
import { FEEDBACK_RATING_SCALE } from "../utils/feedbackRating.js";
import { flush, trackEvent } from "./client.js";
import { readConfig } from "./config.js";
import { getPowerState } from "./system.js";
// Power state is volatile (a laptop docks/undocks mid-session), so it is
// sampled per render event rather than cached with SystemMeta. Attached to
// render_complete AND render_error: the DE fleet is macOS laptops whose
// power management shifts render perf ~1.8x with no other telemetry signal,
// and perf/soak analysis needs to segment by it (see getPowerState).
function powerStateFields(): { on_battery?: boolean; low_power_mode?: boolean } {
const power = getPowerState();
return {
on_battery: power.on_battery ?? undefined,
low_power_mode: power.low_power_mode ?? undefined,
};
}
// run_id is attached only when the orchestrator set HYPERFRAMES_RUN_ID — an
// absent property, never null/"" (PostHog treats those as real values).
@@ -277,6 +291,7 @@ export function trackRenderComplete(
de_blank_recaptures: props.deBlankRecaptures,
de_boundary_frames: props.deBoundaryFrames,
de_ncpr_fallbacks: props.deNcprFallbacks,
...powerStateFields(),
source: props.source ?? "cli",
composition_duration_ms: props.compositionDurationMs,
composition_width: props.compositionWidth,
@@ -356,6 +371,7 @@ export function trackRenderError(
elapsed_ms: props.elapsedMs,
peak_memory_mb: props.peakMemoryMb,
memory_free_mb: props.memoryFreeMb,
...powerStateFields(),
...renderObservabilityEventProperties(props),
},
props.distinctId,
+51
View File
@@ -110,3 +110,54 @@ describe("getAvailableMemoryMb", () => {
expect(getAvailableMemoryMb()).toBe(6144);
});
});
describe("power state (laptop fleet segmentation)", () => {
it("parsePmsetPowerSource reads battery vs AC", async () => {
const { parsePmsetPowerSource } = await import("./system.js");
expect(parsePmsetPowerSource("Now drawing from 'Battery Power'\n -InternalBattery-0")).toBe(
true,
);
expect(parsePmsetPowerSource("Now drawing from 'AC Power'\n -InternalBattery-0")).toBe(false);
expect(parsePmsetPowerSource("garbage output")).toBe(null);
});
it("getPowerState samples pmset on darwin", async () => {
vi.doMock("node:os", async () => ({
...(await vi.importActual<typeof import("node:os")>("node:os")),
platform: () => "darwin",
}));
vi.doMock("node:child_process", async () => ({
...(await vi.importActual<typeof import("node:child_process")>("node:child_process")),
execSync: (cmd: string) =>
cmd === "pmset -g batt"
? "Now drawing from 'Battery Power'\n"
: "SleepDisabled 0\n lowpowermode 1\n",
}));
const { getPowerState } = await import("./system.js");
expect(getPowerState()).toEqual({ on_battery: true, low_power_mode: true });
});
it("getPowerState returns nulls off-darwin (no guessing)", async () => {
vi.doMock("node:os", async () => ({
...(await vi.importActual<typeof import("node:os")>("node:os")),
platform: () => "linux",
}));
const { getPowerState } = await import("./system.js");
expect(getPowerState()).toEqual({ on_battery: null, low_power_mode: null });
});
it("getPowerState survives pmset failure with nulls", async () => {
vi.doMock("node:os", async () => ({
...(await vi.importActual<typeof import("node:os")>("node:os")),
platform: () => "darwin",
}));
vi.doMock("node:child_process", async () => ({
...(await vi.importActual<typeof import("node:child_process")>("node:child_process")),
execSync: () => {
throw new Error("pmset: command failed");
},
}));
const { getPowerState } = await import("./system.js");
expect(getPowerState()).toEqual({ on_battery: null, low_power_mode: null });
});
});
+54
View File
@@ -184,6 +184,60 @@ export function getFreeDiskMb(path: string = "."): number | null {
}
}
export interface PowerState {
/** true = running on battery, false = external power, null = undetectable. */
on_battery: boolean | null;
/** macOS Low Power Mode; null off-darwin or undetectable. */
low_power_mode: boolean | null;
}
/**
* Parse `pmset -g batt` output for the power source line. Exported for tests.
* Example first line: `Now drawing from 'Battery Power'`.
*/
export function parsePmsetPowerSource(raw: string): boolean | null {
const m = raw.match(/Now drawing from '([^']+)'/);
if (!m) return null;
return m[1] === "Battery Power";
}
/**
* Sample the machine's power state. Volatile — sample at event time, never
* cache alongside SystemMeta.
*
* Why this exists: the DE fast path only engages on macOS + hardware GPU, so
* the render fleet is overwhelmingly laptops, and laptop perf is
* power-managed — bench sweeps on an M4 Pro showed the SAME render flipping
* between ~9.6 and ~17.2 ms/frame regimes with no load/thermal signal to
* explain it. Without a power-state dimension on render telemetry those
* regimes are indistinguishable noise; with it, perf distributions (and the
* DE parallel-router soak) can be segmented by the machine state real users
* actually render in.
*/
export function getPowerState(): PowerState {
if (platform() !== "darwin") {
// Linux laptops exist but the DE fleet is darwin; don't guess elsewhere.
return { on_battery: null, low_power_mode: null };
}
let on_battery: boolean | null = null;
let low_power_mode: boolean | null = null;
try {
on_battery = parsePmsetPowerSource(
execSync("pmset -g batt", { encoding: "utf-8", timeout: 2000 }),
);
} catch {
// pmset missing/slow — leave null rather than fail telemetry.
}
try {
const raw = execSync("pmset -g", { encoding: "utf-8", timeout: 2000 });
const m = raw.match(/lowpowermode\s+(\d)/);
if (m) low_power_mode = m[1] === "1";
} catch {
// Same: absence of the reading is itself acceptable.
}
return { on_battery, low_power_mode };
}
/**
* Get available memory in MB, accounting for OS-level page caching.
*
@@ -1305,9 +1305,14 @@ export function resolveInversionRetryPlan(args: {
* but the decision itself stays gated behind its own flag pending the
* telemetry soak (revert rate, de_verify_min_db distribution) on real wild
* traffic — there is currently none, since nothing routes here by default.
* Takes priority over the single-worker inversion when both would fire (a
* higher minFrames than HF_DE_SINGLE_MIN_FRAMES is the intended shape: this
* only picks up the long tail the inversion's own benchmark didn't cover).
* Takes priority over the single-worker inversion when both would fire.
* Re-calibrated 2026-07-27: a controlled crossover sweep (three content
* profiles including a genuinely init-expensive 24-sub-composition comp;
* worker counts and capture modes verified per run) found par3 > single at
* every size from 350f up in every profile — workers init concurrently, so
* per-worker init duplication costs CPU, not wall-clock. minFrames therefore
* dropped below the inversion's threshold (700 vs 900): where both fire,
* parallel wins over the inversion's single-worker pick (+1721% at 700f).
*/
export function shouldPreferParallelDrawElement(args: {
workerCount: number;
@@ -2310,18 +2315,27 @@ async function executeRenderPipeline(input: {
process.env.HF_DE_PARALLEL_STREAM === "true",
});
// DE parallel-router eligibility — see shouldPreferParallelDrawElement.
// Default-off (HF_DE_PARALLEL_ROUTER); HF_DE_PARALLEL_MIN_FRAMES defaults
// higher than the single-worker inversion's threshold since it targets
// the long tail the inversion's own benchmark didn't cover.
// Default-off (HF_DE_PARALLEL_ROUTER); HF_DE_PARALLEL_MIN_FRAMES default
// 700, re-calibrated 2026-07-27 from the original safe-high 2000. A
// controlled frame-count sweep (fixed content-per-frame, three synthetic
// profiles × {350..3000f} × {single,par2,par3} × 3 reps, worker counts +
// capture modes verified per run) showed par3 beating single at EVERY
// size in every profile — +1721% at 700f rising to +2834% at 3000f —
// including a 24-sub-composition profile built to reproduce the
// "workers re-pay init" failure (92k tweens, ~2.5s
// pollSubCompositionTimelines per worker): workers init CONCURRENTLY, so
// duplicated init costs CPU, not wall-clock. Below ~700f the win thins
// toward ~+10% while still paying 3 hardware-GPU browsers, so the floor
// stays. Harness: plans/drawelement-fast-capture/de-crossover-bench.sh.
const deParallelRouterEnabled = process.env.HF_DE_PARALLEL_ROUTER === "true";
const deParallelMinFramesRaw = process.env.HF_DE_PARALLEL_MIN_FRAMES;
const deParallelMinFramesNum =
deParallelMinFramesRaw === undefined || deParallelMinFramesRaw.trim() === ""
? 2000
? 700
: Number(deParallelMinFramesRaw);
const deParallelMinFrames = Number.isFinite(deParallelMinFramesNum)
? deParallelMinFramesNum
: 2000;
: 700;
// RAM floor default 24 GB: the wild black-slab report was a 16 GB
// machine; every clean routed cohort in telemetry so far is >=24 GB.
// HF_DE_PARALLEL_MIN_MEM_MB overrides (0 disables the guard).