mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
test(embedded-mpv): commit the Linux frame-copy measurement probe
linux-frame-probe.mjs reproduces the RESULTS.md Linux rows: spawns the production helper, attaches the frame-reader addon to the announced shm generation, and reports new-frame fps, copy wall time, produce->copy age, torn reads and pixel spread. Usage documented in RESULTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c17666ec49
commit
0ea4190a29
2 files changed
+143
-3
No files matched your search
@@ -111,9 +111,10 @@ Windows/Linux.
|
||||
|
||||
Source: Linux port branch (headless-EGL `frame_helper_gl.h` backend), system
|
||||
libmpv 2.5.0 (mpv 0.40), Mesa 25.0 iris. Measured with the production helper
|
||||
binary + `embedded_mpv_frame_reader.node` in a Node probe loop (the spike
|
||||
viewer harness is macOS-only), so *age* here is produce→reader-copy and
|
||||
excludes the renderer texture upload. hwdec was NOT active — this machine
|
||||
binary + `embedded_mpv_frame_reader.node` in a Node probe loop
|
||||
(`linux-frame-probe.mjs` in this directory — the spike viewer harness is
|
||||
macOS-only), so *age* here is produce→reader-copy and excludes the renderer
|
||||
texture upload. hwdec was NOT active — this machine
|
||||
has no VAAPI driver installed (`intel-media-va-driver`), so HEVC rows are
|
||||
software decode; treat them as a decode-limited floor, not a pipeline
|
||||
ceiling.
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Linux frame-copy probe: spawns iptvnator_mpv_helper, attaches the
|
||||
* embedded_mpv_frame_reader addon to the announced shm generation, and
|
||||
* reports producer fps, copy latency (ageMs), copy wall time, torn reads
|
||||
* and pixel spread. Usage:
|
||||
* node frame-probe.mjs <url> <width> <height> <seconds> [--hwdec auto]
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const releaseDir = process.env.RELEASE_DIR;
|
||||
const [url, width = '1280', height = '720', seconds = '5', ...rest] =
|
||||
process.argv.slice(2);
|
||||
if (!url || !releaseDir) {
|
||||
console.error('usage: RELEASE_DIR=<native build dir> node frame-probe.mjs <url> [w] [h] [s]');
|
||||
process.exit(1);
|
||||
}
|
||||
const hwdecIdx = rest.indexOf('--hwdec');
|
||||
const hwdec = hwdecIdx >= 0 ? rest[hwdecIdx + 1] : null;
|
||||
|
||||
const reader = require(path.join(releaseDir, 'embedded_mpv_frame_reader.node'));
|
||||
const helperPath = path.join(releaseDir, 'iptvnator_mpv_helper');
|
||||
const shmBase = `/impv-probe-${process.pid}`;
|
||||
|
||||
const args = ['--shm-base', shmBase, '--width', width, '--height', height];
|
||||
if (hwdec) args.push('--hwdec', hwdec);
|
||||
const child = spawn(helperPath, args, { stdio: ['pipe', 'pipe', 'inherit'] });
|
||||
|
||||
let buffer = '';
|
||||
let currentSource = null;
|
||||
const generations = [];
|
||||
let statusLog = [];
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
let i;
|
||||
while ((i = buffer.indexOf('\n')) >= 0) {
|
||||
const line = buffer.slice(0, i);
|
||||
buffer = buffer.slice(i + 1);
|
||||
if (!line.trim()) continue;
|
||||
let ev;
|
||||
try { ev = JSON.parse(line); } catch { continue; }
|
||||
if (ev.event === 'shm') {
|
||||
generations.push(`${ev.name} ${ev.width}x${ev.height}`);
|
||||
currentSource = ev;
|
||||
} else if (ev.event === 'snapshot') {
|
||||
if (statusLog[statusLog.length - 1] !== ev.status) statusLog.push(ev.status);
|
||||
} else if (ev.event === 'fatal') {
|
||||
console.error('FATAL:', ev.error);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
await sleep(300);
|
||||
child.stdin.write(`load\turl=${url.replace(/%/g, '%25')}\n`);
|
||||
|
||||
// Wait for playback + a possibly-resized generation to settle.
|
||||
const settleDeadline = Date.now() + 10000;
|
||||
while (Date.now() < settleDeadline) {
|
||||
await sleep(100);
|
||||
if (statusLog.includes('playing') && currentSource) {
|
||||
await sleep(700); // allow the aspect-fit resize generation to land
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!currentSource) {
|
||||
console.error('no shm source announced');
|
||||
child.kill('SIGKILL');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const info = reader.open(currentSource.name);
|
||||
console.log(`attached ${currentSource.name}: ${info.width}x${info.height} stride=${info.stride} gen=${info.generation}`);
|
||||
|
||||
const frameBuf = new ArrayBuffer(info.frameBytes);
|
||||
let lastSeq = 0;
|
||||
let frames = 0;
|
||||
let torn = 0;
|
||||
const ages = [];
|
||||
const copyTimes = [];
|
||||
const durationMs = Number(seconds) * 1000;
|
||||
const start = Date.now();
|
||||
let pixelMin = 255, pixelMax = 0;
|
||||
|
||||
while (Date.now() - start < durationMs) {
|
||||
const seq = reader.latestSeq();
|
||||
if (seq !== lastSeq) {
|
||||
const t0 = process.hrtime.bigint();
|
||||
const result = reader.copyLatest(frameBuf);
|
||||
const t1 = process.hrtime.bigint();
|
||||
if (result) {
|
||||
frames += 1;
|
||||
lastSeq = result.seq;
|
||||
ages.push(result.ageMs);
|
||||
copyTimes.push(Number(t1 - t0) / 1e6);
|
||||
if (result.torn) torn += 1;
|
||||
}
|
||||
}
|
||||
await sleep(2);
|
||||
}
|
||||
const elapsed = (Date.now() - start) / 1000;
|
||||
|
||||
// pixel spread over the last frame
|
||||
const view = new Uint8Array(frameBuf);
|
||||
for (let i = 0; i < view.length; i += 4001 * 4) {
|
||||
for (let c = 0; c < 3; c++) {
|
||||
const v = view[i + c];
|
||||
if (v < pixelMin) pixelMin = v;
|
||||
if (v > pixelMax) pixelMax = v;
|
||||
}
|
||||
}
|
||||
|
||||
const pct = (arr, p) => {
|
||||
if (!arr.length) return NaN;
|
||||
const sorted = [...arr].sort((a, b) => a - b);
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
|
||||
};
|
||||
const avg = (arr) => arr.reduce((a, b) => a + b, 0) / (arr.length || 1);
|
||||
|
||||
console.log(`generations seen: ${generations.join(' | ')}`);
|
||||
console.log(`status transitions: ${statusLog.join(' -> ')}`);
|
||||
console.log(`producerAliveMs: ${reader.producerAliveMs().toFixed(1)}`);
|
||||
console.log(`new frames: ${frames} in ${elapsed.toFixed(1)}s = ${(frames / elapsed).toFixed(1)} fps`);
|
||||
console.log(`copy ms avg/p95: ${avg(copyTimes).toFixed(2)} / ${pct(copyTimes, 0.95).toFixed(2)}`);
|
||||
console.log(`age ms avg/p95: ${avg(ages).toFixed(2)} / ${pct(ages, 0.95).toFixed(2)}`);
|
||||
console.log(`torn: ${torn}`);
|
||||
console.log(`pixel spread (BGR sampled): min=${pixelMin} max=${pixelMax}`);
|
||||
|
||||
child.stdin.write('quit\n');
|
||||
child.stdin.end();
|
||||
await sleep(500);
|
||||
if (child.exitCode === null) child.kill('SIGKILL');
|
||||
reader.close();
|
||||
Reference in new issue
Block a user