feat: publish av-tools 0.1.0
This commit is contained in:
242
tests/browser/performance.spec.ts
Normal file
242
tests/browser/performance.spec.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import { expect, test, type Browser, type Page } from '@playwright/test';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { PRIMARY_FIXTURE } from './browser-helpers';
|
||||
|
||||
interface BenchmarkResult {
|
||||
readonly mode: 'single-thread' | 'multithread';
|
||||
readonly source: 'small' | 'medium';
|
||||
readonly sourceDescription: string;
|
||||
readonly cacheCondition: string;
|
||||
readonly initMs: number;
|
||||
readonly probeMs: number;
|
||||
readonly conversionMs: number;
|
||||
readonly outputReadMs: number;
|
||||
readonly cleanupMs: number;
|
||||
readonly exportEndToEndMs: number;
|
||||
readonly outputBytes: number;
|
||||
readonly hardwareConcurrency: number;
|
||||
readonly crossOriginIsolated: boolean;
|
||||
readonly inputBytes: number;
|
||||
readonly usedJsHeapBytes?: number;
|
||||
}
|
||||
|
||||
test.describe('opt-in pinned-core performance record', () => {
|
||||
test.skip(
|
||||
process.env.AV_BENCHMARK !== '1',
|
||||
'Run with AV_BENCHMARK=1 through npm run benchmark:browser.'
|
||||
);
|
||||
|
||||
test('records comparable fresh-context ST and MT end-to-end timings', async ({
|
||||
browser,
|
||||
}, testInfo) => {
|
||||
test.setTimeout(600_000);
|
||||
const medium = createMediumFixture();
|
||||
const results: BenchmarkResult[] = [];
|
||||
try {
|
||||
results.push(
|
||||
await runMode(browser, 'single-thread', PRIMARY_FIXTURE, 'small')
|
||||
);
|
||||
results.push(
|
||||
await runMode(browser, 'multithread', PRIMARY_FIXTURE, 'small')
|
||||
);
|
||||
results.push(
|
||||
await runMode(browser, 'single-thread', medium.filePath, 'medium')
|
||||
);
|
||||
results.push(
|
||||
await runMode(browser, 'multithread', medium.filePath, 'medium')
|
||||
);
|
||||
await testInfo.attach('av-tools-browser-benchmark.json', {
|
||||
body: Buffer.from(`${JSON.stringify(results, null, 2)}\n`),
|
||||
contentType: 'application/json',
|
||||
});
|
||||
process.stdout.write(`AV_BENCHMARK_RESULT ${JSON.stringify(results)}\n`);
|
||||
} finally {
|
||||
medium.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function runMode(
|
||||
browser: Browser,
|
||||
mode: BenchmarkResult['mode'],
|
||||
fixturePath: string,
|
||||
source: BenchmarkResult['source']
|
||||
): Promise<BenchmarkResult> {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
try {
|
||||
page.on('dialog', (dialog) => void dialog.accept());
|
||||
await page.addInitScript(() => {
|
||||
Reflect.deleteProperty(globalThis, 'showSaveFilePicker');
|
||||
});
|
||||
await page.goto('http://127.0.0.1:4173/');
|
||||
const environment = await page.evaluate(() => ({
|
||||
hardwareConcurrency: navigator.hardwareConcurrency,
|
||||
crossOriginIsolated: globalThis.crossOriginIsolated,
|
||||
}));
|
||||
if (mode === 'multithread') {
|
||||
expect(environment.crossOriginIsolated).toBe(true);
|
||||
}
|
||||
|
||||
await page.getByText('Engine & browser').click();
|
||||
await page
|
||||
.getByLabel('Engine mode')
|
||||
.selectOption(
|
||||
mode === 'single-thread' ? 'force-single-thread' : 'prefer-multithread'
|
||||
);
|
||||
const initStarted = await monotonicNow(page);
|
||||
await page.getByRole('button', { name: 'Initialize engine' }).click();
|
||||
await expect(
|
||||
page.locator('.engine-pill').filter({
|
||||
hasText:
|
||||
mode === 'single-thread'
|
||||
? 'Single-threaded compatibility mode'
|
||||
: 'Multithreaded',
|
||||
})
|
||||
).toBeVisible({ timeout: 120_000 });
|
||||
const initMs = (await monotonicNow(page)) - initStarted;
|
||||
|
||||
const probeStarted = await monotonicNow(page);
|
||||
await mediaInput(page).setInputFiles(fixturePath);
|
||||
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
|
||||
'Ready',
|
||||
{ timeout: 120_000 }
|
||||
);
|
||||
const probeMs = (await monotonicNow(page)) - probeStarted;
|
||||
|
||||
const exportStarted = await monotonicNow(page);
|
||||
await page.getByRole('button', { name: 'Convert', exact: true }).click();
|
||||
const result = page.locator('.result-card').last();
|
||||
await expect(result).toContainText(
|
||||
/created · verified|completed locally/u,
|
||||
{
|
||||
timeout: 180_000,
|
||||
}
|
||||
);
|
||||
const exportEndToEndMs = (await monotonicNow(page)) - exportStarted;
|
||||
const reportDownload = page.waitForEvent('download');
|
||||
await page
|
||||
.getByRole('button', { name: 'Export validation report' })
|
||||
.click();
|
||||
const reportPath = await (await reportDownload).path();
|
||||
expect(reportPath).not.toBeNull();
|
||||
const report = JSON.parse(
|
||||
readFileSync(reportPath as string, 'utf8')
|
||||
) as PerformanceReport;
|
||||
const outputBytes = await page
|
||||
.locator('.quick-preview video')
|
||||
.evaluate(async (element) => {
|
||||
const response = await fetch((element as HTMLVideoElement).src);
|
||||
return (await response.arrayBuffer()).byteLength;
|
||||
});
|
||||
const usedJsHeapBytes = await page.evaluate(() => {
|
||||
const memory = (
|
||||
performance as Performance & {
|
||||
memory?: { usedJSHeapSize?: number };
|
||||
}
|
||||
).memory;
|
||||
return memory?.usedJSHeapSize;
|
||||
});
|
||||
|
||||
const benchmarkSample: BenchmarkResult = {
|
||||
mode,
|
||||
source,
|
||||
sourceDescription:
|
||||
source === 'small'
|
||||
? '2 s, 160×90 H.264/AAC synthetic pattern'
|
||||
: '6 s, 640×360 H.264/AAC synthetic loop',
|
||||
cacheCondition:
|
||||
'fresh browser context; browser HTTP cache may be warm after the first run',
|
||||
initMs: rounded(initMs),
|
||||
probeMs: rounded(probeMs),
|
||||
conversionMs: rounded(report.execution.conversionMilliseconds),
|
||||
outputReadMs: rounded(report.execution.outputReadMilliseconds),
|
||||
cleanupMs: rounded(report.execution.cleanupMilliseconds),
|
||||
exportEndToEndMs: rounded(exportEndToEndMs),
|
||||
outputBytes,
|
||||
hardwareConcurrency: environment.hardwareConcurrency,
|
||||
crossOriginIsolated: environment.crossOriginIsolated,
|
||||
inputBytes: statSync(fixturePath).size,
|
||||
...(usedJsHeapBytes === undefined ? {} : { usedJsHeapBytes }),
|
||||
};
|
||||
process.stdout.write(
|
||||
`AV_BENCHMARK_SAMPLE ${JSON.stringify(benchmarkSample)}\n`
|
||||
);
|
||||
return benchmarkSample;
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
interface PerformanceReport {
|
||||
readonly execution: {
|
||||
readonly conversionMilliseconds: number;
|
||||
readonly outputReadMilliseconds: number;
|
||||
readonly cleanupMilliseconds: number;
|
||||
};
|
||||
}
|
||||
|
||||
function createMediumFixture(): {
|
||||
readonly filePath: string;
|
||||
readonly dispose: () => void;
|
||||
} {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'av-tools-benchmark-'));
|
||||
const filePath = join(directory, 'medium-pattern.mp4');
|
||||
try {
|
||||
execFileSync(
|
||||
'ffmpeg',
|
||||
[
|
||||
'-hide_banner',
|
||||
'-loglevel',
|
||||
'error',
|
||||
'-y',
|
||||
'-stream_loop',
|
||||
'2',
|
||||
'-i',
|
||||
PRIMARY_FIXTURE,
|
||||
'-t',
|
||||
'6',
|
||||
'-vf',
|
||||
'scale=640:360',
|
||||
'-r',
|
||||
'24',
|
||||
'-c:v',
|
||||
'libx264',
|
||||
'-preset',
|
||||
'ultrafast',
|
||||
'-crf',
|
||||
'30',
|
||||
'-c:a',
|
||||
'aac',
|
||||
'-b:a',
|
||||
'64k',
|
||||
filePath,
|
||||
],
|
||||
{ stdio: 'pipe' }
|
||||
);
|
||||
} catch (error) {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
filePath,
|
||||
dispose: () => rmSync(directory, { recursive: true, force: true }),
|
||||
};
|
||||
}
|
||||
|
||||
function mediaInput(page: Page) {
|
||||
return page.locator('input[type="file"][accept*="audio"]').first();
|
||||
}
|
||||
|
||||
async function monotonicNow(page: Page): Promise<number> {
|
||||
return page.evaluate(() => performance.now());
|
||||
}
|
||||
|
||||
function rounded(value: number): number {
|
||||
return Math.round(value * 10) / 10;
|
||||
}
|
||||
Reference in New Issue
Block a user