From 3109b1846002b27da5c210e256c73060d9eff734 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sat, 4 Apr 2026 13:07:34 +1100 Subject: [PATCH] add export backpressure tuning --- src/lib/exporter/exportTuning.test.ts | 83 ++++++++++++ src/lib/exporter/exportTuning.ts | 188 ++++++++++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 src/lib/exporter/exportTuning.test.ts create mode 100644 src/lib/exporter/exportTuning.ts diff --git a/src/lib/exporter/exportTuning.test.ts b/src/lib/exporter/exportTuning.test.ts new file mode 100644 index 00000000..8af95de9 --- /dev/null +++ b/src/lib/exporter/exportTuning.test.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/src/lib/exporter/exportTuning.ts b/src/lib/exporter/exportTuning.ts new file mode 100644 index 00000000..9d6fed3e --- /dev/null +++ b/src/lib/exporter/exportTuning.ts @@ -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 = { + fast: 1.25, + balanced: 2, + quality: 2.4, +}; + +const MIN_QUEUE_LIMIT: Record = { + fast: 36, + balanced: 72, + quality: 96, +}; + +const MAX_QUEUE_LIMIT: Record = { + fast: 96, + balanced: 120, + quality: 180, +}; + +const KEYFRAME_INTERVAL_SECONDS: Record = { + 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, + }; +} \ No newline at end of file