mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 23:35:43 +00:00
add export backpressure tuning
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user