| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { execSync } from 'node:child_process'; |
| import fs from 'node:fs'; |
| import http from 'node:http'; |
| import os from 'node:os'; |
| import path from 'node:path'; |
| import { fileURLToPath } from 'node:url'; |
|
|
| import { chromium, webkit } from 'playwright'; |
|
|
| const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| const ROOT = path.resolve(HERE, '..', '..', '..'); |
| const ORT_DIST = path.join(ROOT, 'node_modules', 'onnxruntime-web', 'dist'); |
|
|
| const arg = (name, fallback) => { |
| const i = process.argv.indexOf(`--${name}`); |
| return i === -1 ? fallback : process.argv[i + 1]; |
| }; |
|
|
| const MODELS_DIR = arg('models-dir', ''); |
| const INPUT_DIR = arg('input-dir', ''); |
| const MODELS = (arg('models', '') ?? '').split(',').filter(Boolean); |
| const CONFIG_IDS = (arg('configs', 'wasm-mt,wasm-st') ?? '').split(',').filter(Boolean); |
| const ENGINE = arg('engine', 'chrome'); |
| const REPEATS = Number(arg('repeats', 12)); |
| const WARMUPS = Number(arg('warmups', 2)); |
| const INPUT = arg('input', '000000000139.f32'); |
| const OUT_FILE = arg('out', path.join(HERE, 'results.json')); |
| const CELL_TIMEOUT_MS = Number(arg('cellTimeoutMs', 300_000)); |
| |
| const THREADS_OVERRIDE = arg('threads', ''); |
| const EM_OVERRIDE = arg('em', ''); |
|
|
| if (!MODELS_DIR || !INPUT_DIR) throw new Error('pass --models-dir and --input-dir'); |
|
|
| const ALL_CONFIGS = { |
| 'wasm-mt': { id: 'wasm-mt', threads: 'auto', isolated: true }, |
| 'wasm-st': { id: 'wasm-st', threads: '1', isolated: false }, |
| }; |
| const CONFIGS = CONFIG_IDS.map((id) => { |
| if (!ALL_CONFIGS[id]) throw new Error(`unknown config ${id}`); |
| return ALL_CONFIGS[id]; |
| }); |
|
|
| const CONTENT_TYPES = { |
| '.html': 'text/html; charset=utf-8', |
| '.mjs': 'text/javascript; charset=utf-8', |
| '.js': 'text/javascript; charset=utf-8', |
| '.wasm': 'application/wasm', |
| '.map': 'application/json', |
| '.onnx': 'application/octet-stream', |
| '.f32': 'application/octet-stream', |
| }; |
|
|
| const startServer = (isolated) => |
| new Promise((resolve) => { |
| const server = http.createServer((req, res) => { |
| const url = new URL(req.url, 'http://localhost'); |
| const pathname = decodeURIComponent(url.pathname); |
|
|
| let file; |
| if (pathname === '/' || pathname === '/harness.html') file = path.join(HERE, 'harness.html'); |
| else if (pathname === '/harness.mjs') file = path.join(HERE, 'harness.mjs'); |
| else if (pathname.startsWith('/ort/')) file = path.join(ORT_DIST, pathname.slice(5)); |
| else if (pathname.startsWith('/models/')) file = path.join(MODELS_DIR, pathname.slice(8)); |
| else if (pathname.startsWith('/input/')) file = path.join(INPUT_DIR, pathname.slice(7)); |
| else file = ''; |
|
|
| if (!file || !fs.existsSync(file) || fs.statSync(file).isDirectory()) { |
| if (process.env.BENCH_DEBUG) console.error(` [server 404] ${pathname}`); |
| res.writeHead(404).end('not found'); |
| return; |
| } |
| const headers = { 'Content-Type': CONTENT_TYPES[path.extname(file)] ?? 'application/octet-stream' }; |
| if (isolated) { |
| headers['Cross-Origin-Opener-Policy'] = 'same-origin'; |
| headers['Cross-Origin-Embedder-Policy'] = 'require-corp'; |
| headers['Cross-Origin-Resource-Policy'] = 'same-origin'; |
| } |
| res.writeHead(200, headers); |
| fs.createReadStream(file).pipe(res); |
| }); |
| server.listen(0, '127.0.0.1', () => resolve({ server, port: server.address().port })); |
| }); |
|
|
| |
| |
| |
| |
| const HEADLESS = process.env.BENCH_HEADLESS === '1'; |
| let udidCounter = 0; |
| |
| const launch = async () => { |
| if (ENGINE === 'webkit') return webkit.launch({ headless: HEADLESS }); |
| const userDataDir = path.join(os.tmpdir(), `ecseg-bench-${process.pid}-${udidCounter++}`); |
| fs.rmSync(userDataDir, { recursive: true, force: true }); |
| return chromium.launchPersistentContext(userDataDir, { |
| channel: 'chrome', |
| headless: HEADLESS, |
| args: ['--force-high-performance-gpu', '--no-first-run', '--no-default-browser-check'], |
| }); |
| }; |
|
|
| const hostInfo = () => { |
| let cpu = os.cpus()?.[0]?.model ?? 'unknown'; |
| let chip = ''; |
| try { chip = execSync('sysctl -n machdep.cpu.brand_string', { encoding: 'utf8' }).trim(); } catch {} |
| return { |
| platform: `${os.platform()} ${os.release()}`, |
| arch: os.arch(), |
| cpu: chip || cpu, |
| logicalCores: os.cpus()?.length ?? null, |
| totalMemGB: Math.round(os.totalmem() / 2 ** 30), |
| }; |
| }; |
|
|
| const runCell = async (browser, port, model, config) => { |
| const page = await browser.newPage(); |
| if (process.env.BENCH_DEBUG) { |
| page.on('console', (m) => console.error(` [page:${m.type()}] ${m.text()}`)); |
| page.on('pageerror', (e) => console.error(` [pageerror] ${e.message}`)); |
| page.on('requestfailed', (r) => console.error(` [reqfail] ${r.url()} ${r.failure()?.errorText}`)); |
| page.on('response', (r) => { if (r.status() >= 400) console.error(` [http ${r.status()}] ${r.url()}`); }); |
| } |
| try { |
| const threads = THREADS_OVERRIDE || config.threads; |
| const url = |
| `http://127.0.0.1:${port}/harness.html` + |
| `?model=${encodeURIComponent(model)}&input=${encodeURIComponent(INPUT)}` + |
| `&threads=${threads}&repeats=${REPEATS}&warmups=${WARMUPS}` + |
| (EM_OVERRIDE ? `&em=${EM_OVERRIDE}` : ''); |
| await page.goto(url, { waitUntil: 'load' }); |
|
|
| const payload = await Promise.race([ |
| page.evaluate(() => globalThis.benchmarkPromise), |
| new Promise((resolve) => |
| setTimeout( |
| () => |
| page |
| .evaluate(() => document.getElementById('log')?.textContent ?? 'unknown') |
| .catch(() => 'unknown') |
| .then((phase) => resolve({ ok: false, timedOut: true, stalledAt: phase })), |
| CELL_TIMEOUT_MS |
| ) |
| ), |
| ]); |
| return { model, config: config.id, ...payload }; |
| } finally { |
| await page.close(); |
| } |
| }; |
|
|
| const main = async () => { |
| const host = { ...hostInfo(), engine: ENGINE, ortWeb: '1.24.3', when: new Date().toISOString() }; |
| console.log(`ECSeg browser benchmark — ${ENGINE} — ${host.cpu} (${host.logicalCores} cores)`); |
|
|
| const cells = []; |
| for (const config of CONFIGS) { |
| const { server, port } = await startServer(config.isolated); |
| const browser = await launch(); |
| try { |
| for (const model of MODELS) { |
| process.stdout.write(` ${model.padEnd(34)} ${config.id.padEnd(8)} … `); |
| try { |
| const cell = await runCell(browser, port, model, config); |
| if (cell.ok === false) { |
| console.log(cell.timedOut ? `STALLED @ "${cell.stalledAt}"` : `FAILED: ${String(cell.error).split('\n')[0]}`); |
| } else { |
| const r = cell.result; |
| console.log( |
| `create ${Math.round(r.sessionCreateMs)}ms · cold ${Math.round(r.coldInferenceMs)}ms · ` + |
| `warm p50 ${Math.round(r.warm.p50)}ms (p90 ${Math.round(r.warm.p90)}) · inst ${r.fingerprint.numInstances}` + |
| (r.fingerprint.anyNaNInf ? ' · NaN!' : '') |
| ); |
| } |
| cells.push(cell); |
| } catch (e) { |
| console.log(`error: ${e.message}`); |
| cells.push({ model, config: config.id, ok: false, error: e.message }); |
| } |
| } |
| } finally { |
| await browser.close(); |
| server.close(); |
| } |
| } |
|
|
| fs.writeFileSync(OUT_FILE, JSON.stringify({ host, repeats: REPEATS, warmups: WARMUPS, input: INPUT, cells }, null, 2)); |
| console.log(`\n wrote ${OUT_FILE}`); |
| }; |
|
|
| await main(); |
|
|