perf(engine): a --gpu render probes the GPU encoders once, not twice (#6638)

* perf(engine,cli): a machine's GPU encoder probe runs once, not on every render

* refactor(engine): one shared probe per process, no answer kept on disk
This commit is contained in:
Miguel Ángel
2026-10-28 08:44:28 +00:00
committed by GitHub
parent e6476c2cd5
commit 8e63faf846
6 changed files with 37 additions and 8 deletions
+15
View File
@@ -19,6 +19,16 @@ vi.mock("./linuxDeps.js", async (importOriginal) => ({
vi.mock("../utils/cancellableProcess.js", () => ({ runCancellableProcess: vi.fn() }));
const mockRun = vi.mocked(runCancellableProcess);
// The render's own GPU probe and the engine's must be one answer, so the encoder check asks the shared one.
const gpuProbes = vi.hoisted(() => ({
shared: vi.fn(async () => "nvenc"),
fresh: vi.fn(async () => "nvenc"),
}));
vi.mock("@hyperframes/engine", () => ({
getCachedGpuEncoder: gpuProbes.shared,
detectGpuEncoder: gpuProbes.fresh,
}));
afterEach(() => {
vi.clearAllMocks();
delete process.env.HYPERFRAMES_FFMPEG_PATH;
@@ -111,6 +121,11 @@ describe("resolveH264EncoderMode", () => {
).rejects.toThrow(H264EncoderUnavailableError);
expect(probeGpu).toHaveBeenCalledOnce();
mockRun.mockResolvedValue({ stdout: " V....D h264_nvenc H.264\n" } as never);
expect(await detectH264EncoderModeForRender("/custom/ffmpeg", true, signal)).toBe("gpu");
expect(gpuProbes.shared).toHaveBeenCalledOnce();
expect(gpuProbes.fresh).not.toHaveBeenCalled();
const timeout = new Error("ETIMEDOUT");
mockRun.mockRejectedValueOnce(timeout);
await expect(
+2 -2
View File
@@ -1,4 +1,4 @@
import { detectGpuEncoder } from "@hyperframes/engine";
import { getCachedGpuEncoder } from "@hyperframes/engine";
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
import { detectLinuxDistro, ffmpegInstallCommand } from "./linuxDeps.js";
import { runCancellableProcess } from "../utils/cancellableProcess.js";
@@ -40,7 +40,7 @@ export async function detectH264EncoderModeForRender(
ffmpegPath: string,
gpuRequested: boolean,
signal: AbortSignal,
probeGpu: () => Promise<unknown> = detectGpuEncoder,
probeGpu: () => Promise<unknown> = getCachedGpuEncoder,
): Promise<H264EncoderMode> {
const result = await runCancellableProcess(ffmpegPath, ["-hide_banner", "-encoders"], {
signal,
+1
View File
@@ -189,6 +189,7 @@ export {
applyFaststart,
packageHls,
detectGpuEncoder,
getCachedGpuEncoder,
ENCODER_PRESETS,
getEncoderPreset,
PRORES_PROFILES,
+1 -1
View File
@@ -186,7 +186,7 @@ export function getEncoderPreset(
}
// Re-export GPU utilities so existing consumers that import from chunkEncoder still work.
export { detectGpuEncoder, type GpuEncoder } from "../utils/gpuEncoder.js";
export { detectGpuEncoder, getCachedGpuEncoder, type GpuEncoder } from "../utils/gpuEncoder.js";
/** The `lockGopForChunkConcat` / `gopSize` pair, shared by every encoder entry point. */
export interface LockedGopOptions {
@@ -1,10 +1,12 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
getCachedGpuEncoder,
getCompiledGpuEncoders,
getGpuEncoderName,
getProbeArgs,
mapPresetForGpuEncoder,
resetCachedGpuEncoder,
selectUsableGpuEncoder,
} from "./gpuEncoder.js";
@@ -146,3 +148,12 @@ describe("getProbeArgs", () => {
expect(args[args.indexOf("-allow_sw") + 1]).toBe("1");
});
});
describe("getCachedGpuEncoder", () => {
afterEach(() => resetCachedGpuEncoder());
it("is one probe however many callers ask while it runs", () => {
const first = getCachedGpuEncoder();
expect(getCachedGpuEncoder()).toBe(first);
});
});
+7 -5
View File
@@ -80,15 +80,17 @@ export async function detectGpuEncoder(): Promise<GpuEncoder> {
return selectUsableGpuEncoder(candidates, canUseGpuEncoder).catch(() => null);
}
let cachedGpuEncoder: GpuEncoder | undefined = undefined;
let cachedGpuEncoder: Promise<GpuEncoder> | undefined = undefined;
export async function getCachedGpuEncoder(): Promise<GpuEncoder> {
if (cachedGpuEncoder === undefined) {
cachedGpuEncoder = await detectGpuEncoder();
}
export function getCachedGpuEncoder(): Promise<GpuEncoder> {
cachedGpuEncoder ??= detectGpuEncoder();
return cachedGpuEncoder;
}
export function resetCachedGpuEncoder() {
cachedGpuEncoder = undefined;
}
export function getGpuEncoderName(encoder: GpuEncoder, codec: "h264" | "h265"): string {
if (!encoder) return codec === "h264" ? "libx264" : "libx265";
switch (encoder) {