mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 07:16:02 +00:00
241 lines
8.2 KiB
JavaScript
241 lines
8.2 KiB
JavaScript
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,
|
|
};
|
|
} |