mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
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:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -189,6 +189,7 @@ export {
|
||||
applyFaststart,
|
||||
packageHls,
|
||||
detectGpuEncoder,
|
||||
getCachedGpuEncoder,
|
||||
ENCODER_PRESETS,
|
||||
getEncoderPreset,
|
||||
PRORES_PROFILES,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user