mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
refactor: split large files (electron, lib, hooks, scripts)
This commit is contained in:
+106
-909
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,253 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const MODERN_BACKEND_SWEEP = ["auto", "webcodecs", "breeze"];
|
||||
const VARIANT_PRESETS = {
|
||||
adaptive: { name: "adaptive" },
|
||||
baseline: { name: "baseline", maxEncodeQueue: 120, maxDecodeQueue: 10, maxPendingFrames: 24 },
|
||||
tuned: { name: "tuned", maxEncodeQueue: 240, maxDecodeQueue: 12, maxPendingFrames: 32 },
|
||||
};
|
||||
|
||||
function parsePositiveInteger(rawValue, label) {
|
||||
const parsed = Number.parseInt(rawValue, 10);
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||
throw new Error(`${label} must be a positive integer`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseEvenInteger(rawValue, label) {
|
||||
const parsed = parsePositiveInteger(rawValue, label);
|
||||
if (parsed % 2 !== 0) {
|
||||
throw new Error(`${label} must be even`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseExportPipeline(rawValue) {
|
||||
if (rawValue === null || rawValue === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rawValue === "legacy" || rawValue === "modern") {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
throw new Error("RECORDLY_BENCH_EXPORT_PIPELINE must be 'legacy' or 'modern'");
|
||||
}
|
||||
|
||||
function parseExportBackend(rawValue) {
|
||||
if (rawValue === null || rawValue === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rawValue === "auto" || rawValue === "webcodecs" || rawValue === "breeze") {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
throw new Error("RECORDLY_BENCH_EXPORT_BACKEND must be 'auto', 'webcodecs', or 'breeze'");
|
||||
}
|
||||
|
||||
function parseExportBackendList(rawValue) {
|
||||
if (rawValue === null || rawValue === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rawValue === "all") {
|
||||
return [...MODERN_BACKEND_SWEEP];
|
||||
}
|
||||
|
||||
const values = rawValue
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0)
|
||||
.map((value) => parseExportBackend(value))
|
||||
.filter((value) => value !== null);
|
||||
|
||||
if (values.length === 0) {
|
||||
throw new Error(
|
||||
"RECORDLY_BENCH_EXPORT_BACKENDS must include at least one of: auto, webcodecs, breeze",
|
||||
);
|
||||
}
|
||||
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function parseBenchmarkVariantList(rawValue) {
|
||||
if (rawValue === null || rawValue === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const values = rawValue
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0);
|
||||
|
||||
if (values.length === 0) {
|
||||
throw new Error(
|
||||
"RECORDLY_BENCH_EXPORT_VARIANTS must include at least one of: adaptive, baseline, tuned",
|
||||
);
|
||||
}
|
||||
|
||||
for (const value of values) {
|
||||
if (!(value in VARIANT_PRESETS)) {
|
||||
throw new Error(
|
||||
"RECORDLY_BENCH_EXPORT_VARIANTS must include only: adaptive, baseline, tuned",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function parseExportEncodingMode(rawValue) {
|
||||
if (rawValue === null || rawValue === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rawValue === "fast" || rawValue === "balanced" || rawValue === "quality") {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
throw new Error("RECORDLY_BENCH_EXPORT_ENCODING_MODE must be 'fast', 'balanced', or 'quality'");
|
||||
}
|
||||
|
||||
function parseExportShadowIntensity(rawValue) {
|
||||
if (rawValue === null || rawValue === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseFloat(rawValue);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
throw new Error("RECORDLY_BENCH_EXPORT_SHADOW_INTENSITY must be a non-negative number");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function parseExportWebcamSize(rawValue) {
|
||||
if (rawValue === null || rawValue === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseFloat(rawValue);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 100) {
|
||||
throw new Error("RECORDLY_BENCH_EXPORT_WEBCAM_SIZE must be a number between 0 and 100");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function createBenchmarkConfig(env = process.env) {
|
||||
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||
const mainEntry = path.join(repoRoot, "dist-electron", "main.js");
|
||||
const rendererEntry = path.join(repoRoot, "dist", "index.html");
|
||||
const width = parseEvenInteger(env.RECORDLY_BENCH_EXPORT_WIDTH ?? "1280", "Width");
|
||||
const height = parseEvenInteger(env.RECORDLY_BENCH_EXPORT_HEIGHT ?? "720", "Height");
|
||||
const frameRate = parsePositiveInteger(env.RECORDLY_BENCH_EXPORT_FPS ?? "60", "Frame rate");
|
||||
const durationSeconds = parsePositiveInteger(
|
||||
env.RECORDLY_BENCH_EXPORT_DURATION ?? "15",
|
||||
"Duration",
|
||||
);
|
||||
const timeoutMs = parsePositiveInteger(
|
||||
env.RECORDLY_BENCH_EXPORT_TIMEOUT_MS ?? "180000",
|
||||
"Timeout",
|
||||
);
|
||||
const runsPerVariant = parsePositiveInteger(env.RECORDLY_BENCH_EXPORT_RUNS ?? "2", "Runs");
|
||||
const useNativeExport = env.RECORDLY_BENCH_EXPORT_USE_NATIVE === "1";
|
||||
const useWebcamOverlay = env.RECORDLY_BENCH_EXPORT_ENABLE_WEBCAM === "1";
|
||||
const exportEncodingMode = parseExportEncodingMode(
|
||||
env.RECORDLY_BENCH_EXPORT_ENCODING_MODE ?? null,
|
||||
);
|
||||
const exportShadowIntensity = parseExportShadowIntensity(
|
||||
env.RECORDLY_BENCH_EXPORT_SHADOW_INTENSITY ?? null,
|
||||
);
|
||||
const webcamWidth = parseEvenInteger(
|
||||
env.RECORDLY_BENCH_EXPORT_WEBCAM_WIDTH ?? "640",
|
||||
"Webcam width",
|
||||
);
|
||||
const webcamHeight = parseEvenInteger(
|
||||
env.RECORDLY_BENCH_EXPORT_WEBCAM_HEIGHT ?? "360",
|
||||
"Webcam height",
|
||||
);
|
||||
const webcamShadowIntensity = parseExportShadowIntensity(
|
||||
env.RECORDLY_BENCH_EXPORT_WEBCAM_SHADOW ?? null,
|
||||
);
|
||||
const webcamSize = parseExportWebcamSize(env.RECORDLY_BENCH_EXPORT_WEBCAM_SIZE ?? null);
|
||||
const exportPipeline = parseExportPipeline(env.RECORDLY_BENCH_EXPORT_PIPELINE ?? null);
|
||||
const exportBackend = parseExportBackend(env.RECORDLY_BENCH_EXPORT_BACKEND ?? null);
|
||||
const exportBackendList = parseExportBackendList(env.RECORDLY_BENCH_EXPORT_BACKENDS ?? null);
|
||||
const variantNameList = parseBenchmarkVariantList(
|
||||
env.RECORDLY_BENCH_EXPORT_VARIANTS ?? null,
|
||||
);
|
||||
|
||||
return {
|
||||
repoRoot,
|
||||
mainEntry,
|
||||
rendererEntry,
|
||||
width,
|
||||
height,
|
||||
frameRate,
|
||||
durationSeconds,
|
||||
timeoutMs,
|
||||
runsPerVariant,
|
||||
useNativeExport,
|
||||
useWebcamOverlay,
|
||||
exportEncodingMode,
|
||||
exportShadowIntensity,
|
||||
webcamWidth,
|
||||
webcamHeight,
|
||||
webcamShadowIntensity,
|
||||
webcamSize,
|
||||
exportPipeline,
|
||||
exportBackend,
|
||||
exportBackendList,
|
||||
variants: variantNameList
|
||||
? variantNameList.map((variantName) => VARIANT_PRESETS[variantName])
|
||||
: [VARIANT_PRESETS.baseline, VARIANT_PRESETS.tuned],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildBenchmarkRequests(config) {
|
||||
if (config.exportBackendList) {
|
||||
return config.exportBackendList.map((backend) => ({
|
||||
pipeline: config.exportPipeline,
|
||||
backend,
|
||||
label: backend,
|
||||
slug: backend,
|
||||
}));
|
||||
}
|
||||
|
||||
if (config.exportBackend) {
|
||||
return [
|
||||
{
|
||||
pipeline: config.exportPipeline,
|
||||
backend: config.exportBackend,
|
||||
label: config.exportBackend,
|
||||
slug: config.exportBackend,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (config.exportPipeline === "modern") {
|
||||
return MODERN_BACKEND_SWEEP.map((backend) => ({
|
||||
pipeline: config.exportPipeline,
|
||||
backend,
|
||||
label: backend,
|
||||
slug: backend,
|
||||
}));
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
pipeline: config.exportPipeline,
|
||||
backend: null,
|
||||
label: "default",
|
||||
slug: "default",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export async function ensureBuildArtifacts(config) {
|
||||
await fs.access(config.mainEntry);
|
||||
await fs.access(config.rendererEntry);
|
||||
}
|
||||
|
||||
export async function createFixtureVideo(ffmpegPath, targetPath, options) {
|
||||
const {
|
||||
durationSeconds,
|
||||
frameRate,
|
||||
fixtureWidth,
|
||||
fixtureHeight,
|
||||
includeAudio = true,
|
||||
videoFilter = `testsrc2=size=${fixtureWidth}x${fixtureHeight}:rate=${frameRate}`,
|
||||
} = options;
|
||||
const args = ["-y", "-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", videoFilter];
|
||||
|
||||
if (includeAudio) {
|
||||
args.push(
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"sine=frequency=880:sample_rate=48000",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"128k",
|
||||
);
|
||||
} else {
|
||||
args.push("-an");
|
||||
}
|
||||
|
||||
args.push(
|
||||
"-t",
|
||||
String(durationSeconds),
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"veryfast",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
targetPath,
|
||||
);
|
||||
|
||||
await execFileAsync(ffmpegPath, args, {
|
||||
timeout: 60_000,
|
||||
maxBuffer: 20 * 1024 * 1024,
|
||||
});
|
||||
}
|
||||
|
||||
function parseDurationSeconds(ffmpegOutput) {
|
||||
const match = ffmpegOutput.match(/Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)/i);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
Number.parseInt(match[1], 10) * 3600 +
|
||||
Number.parseInt(match[2], 10) * 60 +
|
||||
Number.parseFloat(match[3])
|
||||
);
|
||||
}
|
||||
|
||||
export async function inspectOutput(ffmpegPath, targetPath) {
|
||||
try {
|
||||
const { stderr } = await execFileAsync(
|
||||
ffmpegPath,
|
||||
["-hide_banner", "-i", targetPath, "-f", "null", "-"],
|
||||
{
|
||||
timeout: 30_000,
|
||||
maxBuffer: 20 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
return parseDurationSeconds(stderr);
|
||||
} catch (error) {
|
||||
return parseDurationSeconds(String(error?.stderr ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
export async function readSmokeExportReport(outputPath) {
|
||||
const reportPath = `${outputPath}.report.json`;
|
||||
|
||||
try {
|
||||
const reportContent = await fs.readFile(reportPath, "utf8");
|
||||
return {
|
||||
reportPath,
|
||||
report: JSON.parse(reportContent),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
function formatTableCell(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0 ? value.join(", ") : "-";
|
||||
}
|
||||
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "-";
|
||||
}
|
||||
|
||||
return String(value).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function printTable(title, columns, rows) {
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formattedRows = rows.map((row) =>
|
||||
columns.map((column) => formatTableCell(column.getValue(row))),
|
||||
);
|
||||
const widths = columns.map((column, columnIndex) => {
|
||||
const headerWidth = column.header.length;
|
||||
const rowWidth = Math.max(...formattedRows.map((row) => row[columnIndex].length));
|
||||
return Math.max(headerWidth, rowWidth);
|
||||
});
|
||||
const divider = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
||||
|
||||
console.log(`[benchmark-export-queues] ${title}`);
|
||||
console.log(
|
||||
`| ${columns
|
||||
.map((column, columnIndex) => column.header.padEnd(widths[columnIndex]))
|
||||
.join(" | ")} |`,
|
||||
);
|
||||
console.log(divider);
|
||||
for (const row of formattedRows) {
|
||||
console.log(
|
||||
`| ${row.map((value, columnIndex) => value.padEnd(widths[columnIndex])).join(" | ")} |`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function formatMs(value) {
|
||||
return typeof value === "number" && Number.isFinite(value) ? `${Math.round(value)} ms` : "-";
|
||||
}
|
||||
|
||||
function formatDeltaMs(value) {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const roundedValue = Math.round(value);
|
||||
return `${roundedValue > 0 ? "+" : ""}${roundedValue} ms`;
|
||||
}
|
||||
|
||||
function formatPercent(value) {
|
||||
return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(1)}%` : "-";
|
||||
}
|
||||
|
||||
function formatSeconds(value) {
|
||||
return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(2)} s` : "-";
|
||||
}
|
||||
|
||||
function formatMegabytes(value) {
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
? `${(value / (1024 * 1024)).toFixed(2)} MB`
|
||||
: "-";
|
||||
}
|
||||
|
||||
function formatBoolean(value) {
|
||||
return value ? "Yes" : "No";
|
||||
}
|
||||
|
||||
export function calculateDelta(referenceValue, nextValue) {
|
||||
if (
|
||||
typeof referenceValue !== "number" ||
|
||||
!Number.isFinite(referenceValue) ||
|
||||
typeof nextValue !== "number" ||
|
||||
!Number.isFinite(nextValue)
|
||||
) {
|
||||
return { deltaMs: null, deltaPercent: null };
|
||||
}
|
||||
|
||||
return {
|
||||
deltaMs: nextValue - referenceValue,
|
||||
deltaPercent:
|
||||
referenceValue > 0 ? ((nextValue - referenceValue) / referenceValue) * 100 : null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRequestedConfigRows(config, benchmarkRequests) {
|
||||
const rows = [
|
||||
{ key: "Width", value: config.width },
|
||||
{ key: "Height", value: config.height },
|
||||
{ key: "Frame rate", value: `${config.frameRate} FPS` },
|
||||
{ key: "Duration", value: `${config.durationSeconds} s` },
|
||||
{ key: "Timeout", value: formatMs(config.timeoutMs) },
|
||||
{ key: "Runs per variant", value: config.runsPerVariant },
|
||||
{ key: "Pipeline", value: config.exportPipeline ?? "default" },
|
||||
{ key: "Requested backends", value: benchmarkRequests.map((request) => request.label) },
|
||||
{ key: "Backend sweep", value: formatBoolean(benchmarkRequests.length > 1) },
|
||||
{ key: "Encoding mode", value: config.exportEncodingMode ?? "default" },
|
||||
{ key: "Shadow intensity", value: config.exportShadowIntensity ?? "default" },
|
||||
{ key: "Webcam enabled", value: formatBoolean(config.useWebcamOverlay) },
|
||||
{ key: "Experimental native override", value: formatBoolean(config.useNativeExport) },
|
||||
];
|
||||
|
||||
if (config.useWebcamOverlay) {
|
||||
rows.push(
|
||||
{ key: "Webcam width", value: config.webcamWidth },
|
||||
{ key: "Webcam height", value: config.webcamHeight },
|
||||
{ key: "Webcam shadow", value: config.webcamShadowIntensity ?? "default" },
|
||||
{ key: "Webcam size", value: config.webcamSize ?? "default" },
|
||||
);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function printRequestedConfigTable(config, benchmarkRequests) {
|
||||
printTable(
|
||||
"Requested config",
|
||||
[
|
||||
{ header: "Setting", getValue: (row) => row.key },
|
||||
{ header: "Value", getValue: (row) => row.value },
|
||||
],
|
||||
buildRequestedConfigRows(config, benchmarkRequests),
|
||||
);
|
||||
}
|
||||
|
||||
function buildTimingTableRows(benchmarkResults) {
|
||||
return benchmarkResults.flatMap((result) =>
|
||||
result.summaries.map((summary) => ({
|
||||
backend: result.request.backend ?? "default",
|
||||
pipeline: result.request.pipeline ?? "default",
|
||||
variant: summary.variant.name,
|
||||
averageElapsedMs: summary.averageElapsedMs,
|
||||
medianElapsedMs: summary.medianElapsedMs,
|
||||
averageSmokeElapsedMs: summary.averageSmokeElapsedMs,
|
||||
minElapsedMs: summary.minElapsedMs,
|
||||
maxElapsedMs: summary.maxElapsedMs,
|
||||
averageOutputDurationSeconds: summary.averageOutputDurationSeconds,
|
||||
averageSizeBytes: summary.averageSizeBytes,
|
||||
webcamEnabled: summary.webcamEnabled,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export function printTimingSummaryTable(benchmarkResults) {
|
||||
printTable(
|
||||
"Timing summary",
|
||||
[
|
||||
{ header: "Pipeline", getValue: (row) => row.pipeline },
|
||||
{ header: "Backend", getValue: (row) => row.backend },
|
||||
{ header: "Variant", getValue: (row) => row.variant },
|
||||
{ header: "Avg total", getValue: (row) => formatMs(row.averageElapsedMs) },
|
||||
{ header: "Median total", getValue: (row) => formatMs(row.medianElapsedMs) },
|
||||
{ header: "Avg export", getValue: (row) => formatMs(row.averageSmokeElapsedMs) },
|
||||
{ header: "Min", getValue: (row) => formatMs(row.minElapsedMs) },
|
||||
{ header: "Max", getValue: (row) => formatMs(row.maxElapsedMs) },
|
||||
{
|
||||
header: "Avg output",
|
||||
getValue: (row) => formatSeconds(row.averageOutputDurationSeconds),
|
||||
},
|
||||
{ header: "Avg size", getValue: (row) => formatMegabytes(row.averageSizeBytes) },
|
||||
{ header: "Webcam", getValue: (row) => formatBoolean(row.webcamEnabled) },
|
||||
],
|
||||
buildTimingTableRows(benchmarkResults),
|
||||
);
|
||||
}
|
||||
|
||||
function buildBackendDetailTableRows(benchmarkResults) {
|
||||
return benchmarkResults.flatMap((result) =>
|
||||
result.summaries.map((summary) => ({
|
||||
backend: result.request.backend ?? "default",
|
||||
pipeline: result.request.pipeline ?? "default",
|
||||
variant: summary.variant.name,
|
||||
encodeQueue: summary.variant.maxEncodeQueue,
|
||||
decodeQueue: summary.variant.maxDecodeQueue,
|
||||
pendingFrames: summary.variant.maxPendingFrames,
|
||||
observedRenderBackends: summary.observedRenderBackends,
|
||||
observedEncodeBackends: summary.observedEncodeBackends,
|
||||
observedEncoders: summary.observedEncoders,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export function printBackendDetailTable(benchmarkResults) {
|
||||
printTable(
|
||||
"Observed backends",
|
||||
[
|
||||
{ header: "Pipeline", getValue: (row) => row.pipeline },
|
||||
{ header: "Backend", getValue: (row) => row.backend },
|
||||
{ header: "Variant", getValue: (row) => row.variant },
|
||||
{ header: "Encode Q", getValue: (row) => row.encodeQueue },
|
||||
{ header: "Decode Q", getValue: (row) => row.decodeQueue },
|
||||
{ header: "Pending", getValue: (row) => row.pendingFrames },
|
||||
{ header: "Render", getValue: (row) => row.observedRenderBackends },
|
||||
{ header: "Encode", getValue: (row) => row.observedEncodeBackends },
|
||||
{ header: "Encoder", getValue: (row) => row.observedEncoders },
|
||||
],
|
||||
buildBackendDetailTableRows(benchmarkResults),
|
||||
);
|
||||
}
|
||||
|
||||
function buildDeltaTableRows(benchmarkResults) {
|
||||
return benchmarkResults
|
||||
.map((result) => {
|
||||
const baseline = result.summaries.find((summary) => summary.variant.name === "baseline");
|
||||
const tuned = result.summaries.find((summary) => summary.variant.name === "tuned");
|
||||
if (!baseline || !tuned) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const averageDelta = calculateDelta(baseline.averageElapsedMs, tuned.averageElapsedMs);
|
||||
const medianDelta = calculateDelta(baseline.medianElapsedMs, tuned.medianElapsedMs);
|
||||
const exportDelta = calculateDelta(
|
||||
baseline.averageSmokeElapsedMs,
|
||||
tuned.averageSmokeElapsedMs,
|
||||
);
|
||||
|
||||
return {
|
||||
pipeline: result.request.pipeline ?? "default",
|
||||
backend: result.request.backend ?? "default",
|
||||
averageDeltaMs: averageDelta.deltaMs,
|
||||
averageDeltaPercent: averageDelta.deltaPercent,
|
||||
medianDeltaMs: medianDelta.deltaMs,
|
||||
medianDeltaPercent: medianDelta.deltaPercent,
|
||||
exportDeltaMs: exportDelta.deltaMs,
|
||||
exportDeltaPercent: exportDelta.deltaPercent,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function printDeltaTable(benchmarkResults) {
|
||||
printTable(
|
||||
"Tuned vs baseline",
|
||||
[
|
||||
{ header: "Pipeline", getValue: (row) => row.pipeline },
|
||||
{ header: "Backend", getValue: (row) => row.backend },
|
||||
{
|
||||
header: "Avg delta",
|
||||
getValue: (row) =>
|
||||
`${formatDeltaMs(row.averageDeltaMs)} (${formatPercent(row.averageDeltaPercent)})`,
|
||||
},
|
||||
{
|
||||
header: "Median delta",
|
||||
getValue: (row) =>
|
||||
`${formatDeltaMs(row.medianDeltaMs)} (${formatPercent(row.medianDeltaPercent)})`,
|
||||
},
|
||||
{
|
||||
header: "Export delta",
|
||||
getValue: (row) =>
|
||||
`${formatDeltaMs(row.exportDeltaMs)} (${formatPercent(row.exportDeltaPercent)})`,
|
||||
},
|
||||
],
|
||||
buildDeltaTableRows(benchmarkResults),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { inspectOutput, readSmokeExportReport } from "./fixtures.mjs";
|
||||
|
||||
function collectUniqueStrings(values) {
|
||||
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
|
||||
}
|
||||
|
||||
function summarizeSmokeProgress(progressSamples) {
|
||||
if (!Array.isArray(progressSamples) || progressSamples.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const extractingSamples = progressSamples.filter(
|
||||
(sample) =>
|
||||
sample?.phase === "extracting" &&
|
||||
typeof sample?.currentFrame === "number" &&
|
||||
sample.currentFrame > 1,
|
||||
);
|
||||
const fpsSource = extractingSamples.length > 0 ? extractingSamples : progressSamples;
|
||||
const renderFpsSamples = fpsSource
|
||||
.map((sample) => sample?.renderFps)
|
||||
.filter((value) => typeof value === "number" && Number.isFinite(value));
|
||||
const firstSample = progressSamples[0] ?? null;
|
||||
const lastSample = progressSamples.at(-1) ?? null;
|
||||
const firstExtractingSample = extractingSamples[0] ?? null;
|
||||
const lastExtractingSample = extractingSamples.at(-1) ?? null;
|
||||
|
||||
return {
|
||||
samples: progressSamples.length,
|
||||
extractingSamples: extractingSamples.length,
|
||||
firstElapsedMs: typeof firstSample?.elapsedMs === "number" ? firstSample.elapsedMs : null,
|
||||
lastElapsedMs: typeof lastSample?.elapsedMs === "number" ? lastSample.elapsedMs : null,
|
||||
firstExtractingElapsedMs:
|
||||
typeof firstExtractingSample?.elapsedMs === "number"
|
||||
? firstExtractingSample.elapsedMs
|
||||
: null,
|
||||
lastExtractingElapsedMs:
|
||||
typeof lastExtractingSample?.elapsedMs === "number"
|
||||
? lastExtractingSample.elapsedMs
|
||||
: null,
|
||||
firstRenderFps: renderFpsSamples[0] ?? null,
|
||||
lastRenderFps: renderFpsSamples.at(-1) ?? null,
|
||||
minRenderFps: renderFpsSamples.length > 0 ? Math.min(...renderFpsSamples) : null,
|
||||
maxRenderFps: renderFpsSamples.length > 0 ? Math.max(...renderFpsSamples) : null,
|
||||
};
|
||||
}
|
||||
|
||||
async function runVariant(electronPath, ffmpegPath, inputPath, webcamInputPath, benchmarkRequest, variant, runIndex, config) {
|
||||
const outputPath = path.join(
|
||||
path.dirname(inputPath),
|
||||
`${benchmarkRequest.slug}-${variant.name}-${runIndex + 1}-${Date.now()}.mp4`,
|
||||
);
|
||||
const startedAt = performance.now();
|
||||
const runLabel = `${benchmarkRequest.label}/${variant.name}#${runIndex + 1}`;
|
||||
const child = spawn(electronPath, [config.repoRoot], {
|
||||
cwd: config.repoRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
RECORDLY_SMOKE_EXPORT: "1",
|
||||
RECORDLY_SMOKE_EXPORT_INPUT: inputPath,
|
||||
RECORDLY_SMOKE_EXPORT_OUTPUT: outputPath,
|
||||
...(config.useNativeExport ? { RECORDLY_SMOKE_EXPORT_USE_NATIVE: "1" } : {}),
|
||||
...(config.exportEncodingMode
|
||||
? { RECORDLY_SMOKE_EXPORT_ENCODING_MODE: config.exportEncodingMode }
|
||||
: {}),
|
||||
...(config.exportShadowIntensity !== null
|
||||
? { RECORDLY_SMOKE_EXPORT_SHADOW_INTENSITY: String(config.exportShadowIntensity) }
|
||||
: {}),
|
||||
...(webcamInputPath ? { RECORDLY_SMOKE_EXPORT_WEBCAM_INPUT: webcamInputPath } : {}),
|
||||
...(config.webcamShadowIntensity !== null
|
||||
? { RECORDLY_SMOKE_EXPORT_WEBCAM_SHADOW: String(config.webcamShadowIntensity) }
|
||||
: {}),
|
||||
...(config.webcamSize !== null
|
||||
? { RECORDLY_SMOKE_EXPORT_WEBCAM_SIZE: String(config.webcamSize) }
|
||||
: {}),
|
||||
...(benchmarkRequest.pipeline
|
||||
? { RECORDLY_SMOKE_EXPORT_PIPELINE: benchmarkRequest.pipeline }
|
||||
: {}),
|
||||
...(benchmarkRequest.backend
|
||||
? { RECORDLY_SMOKE_EXPORT_BACKEND: benchmarkRequest.backend }
|
||||
: {}),
|
||||
...(typeof variant.maxEncodeQueue === "number"
|
||||
? { RECORDLY_SMOKE_EXPORT_MAX_ENCODE_QUEUE: String(variant.maxEncodeQueue) }
|
||||
: {}),
|
||||
...(typeof variant.maxDecodeQueue === "number"
|
||||
? { RECORDLY_SMOKE_EXPORT_MAX_DECODE_QUEUE: String(variant.maxDecodeQueue) }
|
||||
: {}),
|
||||
...(typeof variant.maxPendingFrames === "number"
|
||||
? { RECORDLY_SMOKE_EXPORT_MAX_PENDING_FRAMES: String(variant.maxPendingFrames) }
|
||||
: {}),
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let combinedOutput = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
const text = chunk.toString();
|
||||
combinedOutput += text;
|
||||
process.stdout.write(`[${runLabel}] ${text}`);
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
const text = chunk.toString();
|
||||
combinedOutput += text;
|
||||
process.stderr.write(`[${runLabel}] ${text}`);
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
}, config.timeoutMs);
|
||||
|
||||
const [exitCode, signal] = await once(child, "close");
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (exitCode !== 0) {
|
||||
const signalText = signal ? ` (signal ${signal})` : "";
|
||||
throw new Error(
|
||||
`${variant.name} run ${runIndex + 1} failed with code ${exitCode ?? "unknown"}${signalText}\n${combinedOutput.trim()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const smokeExportReport = await readSmokeExportReport(outputPath);
|
||||
let outputStats;
|
||||
try {
|
||||
outputStats = await fs.stat(outputPath);
|
||||
} catch (error) {
|
||||
const reportSuffix = smokeExportReport ? `\n${JSON.stringify(smokeExportReport.report)}` : "";
|
||||
throw new Error(
|
||||
`${variant.name} run ${runIndex + 1} did not produce an output file: ${error instanceof Error ? error.message : String(error)}${reportSuffix}`,
|
||||
);
|
||||
}
|
||||
if (outputStats.size <= 0) {
|
||||
const reportSuffix = smokeExportReport ? `\n${JSON.stringify(smokeExportReport.report)}` : "";
|
||||
throw new Error(
|
||||
`${variant.name} run ${runIndex + 1} produced an empty output file${reportSuffix}`,
|
||||
);
|
||||
}
|
||||
|
||||
const elapsedMs = Math.round(performance.now() - startedAt);
|
||||
const outputDuration = await inspectOutput(ffmpegPath, outputPath);
|
||||
|
||||
return {
|
||||
elapsedMs,
|
||||
outputPath,
|
||||
sizeBytes: outputStats.size,
|
||||
outputDuration,
|
||||
webcamEnabled: !!webcamInputPath,
|
||||
smokeExportReport: smokeExportReport?.report ?? null,
|
||||
smokeProgressSummary: summarizeSmokeProgress(smokeExportReport?.report?.progressSamples),
|
||||
};
|
||||
}
|
||||
|
||||
function average(values) {
|
||||
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
||||
}
|
||||
|
||||
function median(values) {
|
||||
if (values.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const sorted = [...values].sort((left, right) => left - right);
|
||||
const middleIndex = Math.floor(sorted.length / 2);
|
||||
if (sorted.length % 2 === 0) {
|
||||
return (sorted[middleIndex - 1] + sorted[middleIndex]) / 2;
|
||||
}
|
||||
|
||||
return sorted[middleIndex];
|
||||
}
|
||||
|
||||
function summarizeVariantRuns(runs) {
|
||||
const elapsedValues = runs.map((run) => run.elapsedMs);
|
||||
const sizeValues = runs.map((run) => run.sizeBytes);
|
||||
const outputDurationValues = runs
|
||||
.map((run) => run.outputDuration)
|
||||
.filter((value) => typeof value === "number" && Number.isFinite(value));
|
||||
const smokeElapsedValues = runs
|
||||
.map((run) => run.smokeExportReport?.elapsedMs)
|
||||
.filter((value) => typeof value === "number" && Number.isFinite(value));
|
||||
|
||||
return {
|
||||
averageElapsedMs: Math.round(average(elapsedValues)),
|
||||
medianElapsedMs: Math.round(median(elapsedValues)),
|
||||
minElapsedMs: Math.min(...elapsedValues),
|
||||
maxElapsedMs: Math.max(...elapsedValues),
|
||||
averageSizeBytes: Math.round(average(sizeValues)),
|
||||
averageOutputDurationSeconds:
|
||||
outputDurationValues.length > 0 ? average(outputDurationValues) : null,
|
||||
averageSmokeElapsedMs:
|
||||
smokeElapsedValues.length > 0 ? Math.round(average(smokeElapsedValues)) : null,
|
||||
observedRenderBackends: collectUniqueStrings(
|
||||
runs.map((run) => run.smokeExportReport?.metrics?.renderBackend),
|
||||
),
|
||||
observedEncodeBackends: collectUniqueStrings(
|
||||
runs.map((run) => run.smokeExportReport?.metrics?.encodeBackend),
|
||||
),
|
||||
observedEncoders: collectUniqueStrings(
|
||||
runs.map((run) => run.smokeExportReport?.metrics?.encoderName),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export async function runBenchmarkRequest(electronPath, ffmpegPath, inputPath, webcamInputPath, benchmarkRequest, config) {
|
||||
const summaries = [];
|
||||
for (const variant of config.variants) {
|
||||
const runs = [];
|
||||
for (let index = 0; index < config.runsPerVariant; index += 1) {
|
||||
console.log(
|
||||
`[benchmark-export-queues] Running ${benchmarkRequest.label}/${variant.name} (${index + 1}/${config.runsPerVariant}) with encode=${variant.maxEncodeQueue ?? "auto"} decode=${variant.maxDecodeQueue ?? "auto"} pending=${variant.maxPendingFrames ?? "auto"}`,
|
||||
);
|
||||
runs.push(
|
||||
await runVariant(
|
||||
electronPath,
|
||||
ffmpegPath,
|
||||
inputPath,
|
||||
webcamInputPath,
|
||||
benchmarkRequest,
|
||||
variant,
|
||||
index,
|
||||
config,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const runSummary = summarizeVariantRuns(runs);
|
||||
summaries.push({
|
||||
variant,
|
||||
runs,
|
||||
...runSummary,
|
||||
webcamEnabled: config.useWebcamOverlay,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
request: benchmarkRequest,
|
||||
summaries,
|
||||
};
|
||||
}
|
||||
@@ -25,19 +25,24 @@ function getTargetConfigs() {
|
||||
|
||||
const helpers = [
|
||||
{
|
||||
source: "ScreenCaptureKitRecorder.swift",
|
||||
sources: [
|
||||
"ScreenCaptureKitRecorder.swift",
|
||||
"ScreenCaptureKitRecorder/ScreenCaptureRecorder.swift",
|
||||
"ScreenCaptureKitRecorder/ScreenCaptureRecorder+Stream.swift",
|
||||
"ScreenCaptureKitRecorder/RecorderService.swift",
|
||||
],
|
||||
output: "recordly-screencapturekit-helper",
|
||||
},
|
||||
{
|
||||
source: "ScreenCaptureKitWindowList.swift",
|
||||
sources: ["ScreenCaptureKitWindowList.swift"],
|
||||
output: "recordly-window-list",
|
||||
},
|
||||
{
|
||||
source: "SystemCursorAssets.swift",
|
||||
sources: ["SystemCursorAssets.swift"],
|
||||
output: "recordly-system-cursors",
|
||||
},
|
||||
{
|
||||
source: "NativeCursorMonitor.swift",
|
||||
sources: ["NativeCursorMonitor.swift"],
|
||||
output: "recordly-native-cursor-monitor",
|
||||
},
|
||||
];
|
||||
@@ -53,12 +58,12 @@ for (const target of getTargetConfigs()) {
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
|
||||
for (const helper of helpers) {
|
||||
const sourcePath = path.join(nativeRoot, helper.source);
|
||||
const sourcePaths = helper.sources.map((source) => path.join(nativeRoot, source));
|
||||
const outputPath = path.join(outputDir, helper.output);
|
||||
|
||||
const result = spawnSync(
|
||||
"swiftc",
|
||||
["-O", "-target", target.swiftTarget, sourcePath, "-o", outputPath],
|
||||
["-O", "-target", target.swiftTarget, ...sourcePaths, "-o", outputPath],
|
||||
{
|
||||
encoding: "utf8",
|
||||
timeout: 120000,
|
||||
@@ -67,7 +72,7 @@ for (const target of getTargetConfigs()) {
|
||||
|
||||
if (result.status !== 0) {
|
||||
const details = [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
||||
throw new Error(details || `Failed to compile ${helper.source} for ${target.archTag}`);
|
||||
throw new Error(details || `Failed to compile ${helper.sources[0]} for ${target.archTag}`);
|
||||
}
|
||||
|
||||
await chmod(outputPath, 0o755);
|
||||
|
||||
Reference in New Issue
Block a user