add export backpressure tuning

This commit is contained in:
webadderall
2026-04-04 13:07:34 +11:00
parent 015d46245c
commit 3109b18460
2 changed files with 271 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
import { describe, expect, it } from "vitest";
import {
getExportBackpressureProfile,
getPreferredWebCodecsLatencyModes,
getWebCodecsEncodeQueueLimit,
getWebCodecsKeyFrameInterval,
} from "./exportTuning";
describe("exportTuning", () => {
it("prefers realtime latency for fast and balanced exports", () => {
expect(getPreferredWebCodecsLatencyModes("fast")).toEqual(["realtime", "quality"]);
expect(getPreferredWebCodecsLatencyModes("balanced")).toEqual(["realtime", "quality"]);
expect(getPreferredWebCodecsLatencyModes("quality")).toEqual(["quality", "realtime"]);
});
it("keeps queue depth bounded by encoding mode", () => {
expect(getWebCodecsEncodeQueueLimit(60, "fast")).toBe(75);
expect(getWebCodecsEncodeQueueLimit(60, "balanced")).toBe(120);
expect(getWebCodecsEncodeQueueLimit(60, "quality")).toBe(144);
expect(getWebCodecsEncodeQueueLimit(240, "fast")).toBe(96);
expect(getWebCodecsEncodeQueueLimit(12, "balanced")).toBe(72);
});
it("widens keyframe spacing for faster modes", () => {
expect(getWebCodecsKeyFrameInterval(60, "fast")).toBe(240);
expect(getWebCodecsKeyFrameInterval(60, "balanced")).toBe(180);
expect(getWebCodecsKeyFrameInterval(60, "quality")).toBe(150);
});
it("uses shallower decode buffers for Breeze than for WebCodecs", () => {
const webCodecsProfile = getExportBackpressureProfile({
encodeBackend: "webcodecs",
width: 1280,
height: 720,
frameRate: 60,
encodingMode: "balanced",
hardwareConcurrency: 8,
});
const breezeProfile = getExportBackpressureProfile({
encodeBackend: "ffmpeg",
width: 1280,
height: 720,
frameRate: 60,
encodingMode: "balanced",
hardwareConcurrency: 8,
});
expect(webCodecsProfile.name).toBe("webcodecs-balanced-plus");
expect(webCodecsProfile.maxDecodeQueue).toBe(12);
expect(webCodecsProfile.maxPendingFrames).toBe(32);
expect(breezeProfile.name).toBe("breeze-balanced-plus");
expect(breezeProfile.maxDecodeQueue).toBe(10);
expect(breezeProfile.maxPendingFrames).toBe(24);
expect(breezeProfile.maxInFlightNativeWrites).toBe(4);
});
it("falls back to conservative native settings on low-core or very heavy workloads", () => {
const breezeLowCoreProfile = getExportBackpressureProfile({
encodeBackend: "ffmpeg",
width: 1280,
height: 720,
frameRate: 60,
hardwareConcurrency: 4,
});
const breezeHeavyProfile = getExportBackpressureProfile({
encodeBackend: "ffmpeg",
width: 3840,
height: 2160,
frameRate: 60,
hardwareConcurrency: 12,
});
expect(breezeLowCoreProfile.name).toBe("breeze-conservative");
expect(breezeLowCoreProfile.maxDecodeQueue).toBe(6);
expect(breezeLowCoreProfile.maxPendingFrames).toBe(12);
expect(breezeLowCoreProfile.maxInFlightNativeWrites).toBe(1);
expect(breezeHeavyProfile.name).toBe("breeze-conservative");
expect(breezeHeavyProfile.maxDecodeQueue).toBe(6);
expect(breezeHeavyProfile.maxPendingFrames).toBe(12);
});
});
+188
View File
@@ -0,0 +1,188 @@
import type { ExportEncodeBackend, ExportEncodingMode } from "./types";
const DEFAULT_ENCODING_MODE: ExportEncodingMode = "balanced";
type WebCodecsLatencyMode = "quality" | "realtime";
const BASELINE_PIXELS_PER_SECOND = 1280 * 720 * 60;
const LATENCY_MODE_PREFERENCES: Record<
ExportEncodingMode,
readonly WebCodecsLatencyMode[]
> = {
fast: ["realtime", "quality"],
balanced: ["realtime", "quality"],
quality: ["quality", "realtime"],
};
const TARGET_QUEUE_SECONDS: Record<ExportEncodingMode, number> = {
fast: 1.25,
balanced: 2,
quality: 2.4,
};
const MIN_QUEUE_LIMIT: Record<ExportEncodingMode, number> = {
fast: 36,
balanced: 72,
quality: 96,
};
const MAX_QUEUE_LIMIT: Record<ExportEncodingMode, number> = {
fast: 96,
balanced: 120,
quality: 180,
};
const KEYFRAME_INTERVAL_SECONDS: Record<ExportEncodingMode, number> = {
fast: 4,
balanced: 3,
quality: 2.5,
};
function normalizeEncodingMode(encodingMode?: ExportEncodingMode): ExportEncodingMode {
return encodingMode ?? DEFAULT_ENCODING_MODE;
}
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
function getEffectiveHardwareConcurrency(hardwareConcurrency?: number): number {
if (typeof hardwareConcurrency === "number" && Number.isFinite(hardwareConcurrency)) {
return Math.max(1, Math.floor(hardwareConcurrency));
}
if (
typeof navigator !== "undefined" &&
typeof navigator.hardwareConcurrency === "number" &&
Number.isFinite(navigator.hardwareConcurrency)
) {
return Math.max(1, Math.floor(navigator.hardwareConcurrency));
}
return 8;
}
function getRelativePixelRate(width: number, height: number, frameRate: number): number {
return (Math.max(1, width) * Math.max(1, height) * Math.max(1, frameRate)) / BASELINE_PIXELS_PER_SECOND;
}
export interface ExportBackpressureProfile {
name: string;
maxEncodeQueue: number;
maxDecodeQueue: number;
maxPendingFrames: number;
maxInFlightNativeWrites: number;
}
interface ExportBackpressureProfileOptions {
encodeBackend: ExportEncodeBackend;
width: number;
height: number;
frameRate: number;
encodingMode?: ExportEncodingMode;
hardwareConcurrency?: number;
}
export function getPreferredWebCodecsLatencyModes(
encodingMode?: ExportEncodingMode,
): readonly WebCodecsLatencyMode[] {
return LATENCY_MODE_PREFERENCES[normalizeEncodingMode(encodingMode)];
}
export function getWebCodecsEncodeQueueLimit(
frameRate: number,
encodingMode?: ExportEncodingMode,
): number {
const resolvedEncodingMode = normalizeEncodingMode(encodingMode);
const targetLimit = Math.round(frameRate * TARGET_QUEUE_SECONDS[resolvedEncodingMode]);
return clamp(
targetLimit,
MIN_QUEUE_LIMIT[resolvedEncodingMode],
MAX_QUEUE_LIMIT[resolvedEncodingMode],
);
}
export function getWebCodecsKeyFrameInterval(
frameRate: number,
encodingMode?: ExportEncodingMode,
): number {
const resolvedEncodingMode = normalizeEncodingMode(encodingMode);
return Math.max(1, Math.round(frameRate * KEYFRAME_INTERVAL_SECONDS[resolvedEncodingMode]));
}
export function getExportBackpressureProfile(
options: ExportBackpressureProfileOptions,
): ExportBackpressureProfile {
const hardwareConcurrency = getEffectiveHardwareConcurrency(options.hardwareConcurrency);
const relativePixelRate = getRelativePixelRate(
options.width,
options.height,
options.frameRate,
);
const isLowCoreSystem = hardwareConcurrency <= 4;
const isHighCoreSystem = hardwareConcurrency >= 8;
const isHeavyWorkload = relativePixelRate >= 1.5;
const isExtremeWorkload = relativePixelRate >= 3;
const maxEncodeQueue = getWebCodecsEncodeQueueLimit(
options.frameRate,
options.encodingMode,
);
if (options.encodeBackend === "ffmpeg") {
if (isLowCoreSystem || isExtremeWorkload) {
return {
name: "breeze-conservative",
maxEncodeQueue,
maxDecodeQueue: 6,
maxPendingFrames: 12,
maxInFlightNativeWrites: 1,
};
}
if (isHighCoreSystem && !isHeavyWorkload) {
return {
name: "breeze-balanced-plus",
maxEncodeQueue,
maxDecodeQueue: 10,
maxPendingFrames: 24,
maxInFlightNativeWrites: 4,
};
}
return {
name: "breeze-balanced",
maxEncodeQueue,
maxDecodeQueue: 8,
maxPendingFrames: 16,
maxInFlightNativeWrites: 2,
};
}
if (isLowCoreSystem || isExtremeWorkload) {
return {
name: "webcodecs-conservative",
maxEncodeQueue,
maxDecodeQueue: 8,
maxPendingFrames: 20,
maxInFlightNativeWrites: 1,
};
}
if (isHighCoreSystem && !isHeavyWorkload) {
return {
name: "webcodecs-balanced-plus",
maxEncodeQueue,
maxDecodeQueue: 12,
maxPendingFrames: 32,
maxInFlightNativeWrites: 1,
};
}
return {
name: "webcodecs-balanced",
maxEncodeQueue,
maxDecodeQueue: 10,
maxPendingFrames: 24,
maxInFlightNativeWrites: 1,
};
}