|
| 1 | +/* @workflow { "name": "benchmark:retained-uploads", "summary": "Prove retained Three storage uploads and profile an ASCII-rich Text on WebGPU and WebGL2.", "requirements": "Built Glyph, project Chromium, authenticated Inter Bitmap fixture, WebGPU, WebGL2, and Darwin or Linux advisory-lock tooling. Pass --usage=dynamic only for a baseline revision.", "writes": "No repository files; JSON evidence to stdout." } */ |
| 2 | +import { spawnSync } from 'node:child_process'; |
| 3 | +import { realpathSync } from 'node:fs'; |
| 4 | +import { resolve as resolvePath } from 'node:path'; |
| 5 | +import { fileURLToPath } from 'node:url'; |
| 6 | +import { createServer, type ViteDevServer } from 'vite'; |
| 7 | + |
| 8 | +import { launchProjectChromium } from './support/project-chromium.mts'; |
| 9 | + |
| 10 | +interface RetainedUploadResult { |
| 11 | + readonly backend: 'webgpu' | 'webgl2'; |
| 12 | + readonly expectedUsage: 'dynamic' | 'stream'; |
| 13 | + readonly idle: Readonly<{ |
| 14 | + readonly uploads: Readonly<{ readonly attributeUpdateCalls: number }>; |
| 15 | + }>; |
| 16 | +} |
| 17 | + |
| 18 | +const performanceLockPath = '/private/tmp/glyph-perf-measurement.lock'; |
| 19 | +const scriptPath = fileURLToPath(import.meta.url); |
| 20 | +const lockHeld = process.argv.includes('--performance-lock-held'); |
| 21 | +const usage = process.argv.includes('--usage=dynamic') ? 'dynamic' : 'stream'; |
| 22 | +const root = fileURLToPath(new URL('..', import.meta.url)); |
| 23 | +const glyphPackageRoot = |
| 24 | + process.env.GLYPH_RETAINED_UPLOAD_PACKAGE_ROOT === undefined |
| 25 | + ? undefined |
| 26 | + : realpathSync(resolvePath(process.env.GLYPH_RETAINED_UPLOAD_PACKAGE_ROOT)); |
| 27 | +const port = process.env.PORT === undefined ? 0 : Number(process.env.PORT); |
| 28 | +if (!Number.isSafeInteger(port) || port < 0 || port > 65_535) throw new Error('PORT must be a valid TCP port'); |
| 29 | + |
| 30 | +if (!lockHeld) { |
| 31 | + runWithPerformanceLock(); |
| 32 | +} else { |
| 33 | + await run(); |
| 34 | +} |
| 35 | + |
| 36 | +function runWithPerformanceLock(): void { |
| 37 | + const command = process.platform === 'darwin' ? '/usr/bin/lockf' : process.platform === 'linux' ? 'flock' : undefined; |
| 38 | + if (command === undefined) throw new Error('retained upload timing requires lockf on Darwin or flock on Linux'); |
| 39 | + const forwarded = process.argv.slice(2).filter((argument) => argument !== '--performance-lock-held'); |
| 40 | + const commandArguments = |
| 41 | + process.platform === 'darwin' |
| 42 | + ? ['-t', '0', performanceLockPath, process.execPath, scriptPath, '--performance-lock-held', ...forwarded] |
| 43 | + : [ |
| 44 | + '--nonblock', |
| 45 | + '--conflict-exit-code', |
| 46 | + '75', |
| 47 | + performanceLockPath, |
| 48 | + process.execPath, |
| 49 | + scriptPath, |
| 50 | + '--performance-lock-held', |
| 51 | + ...forwarded, |
| 52 | + ]; |
| 53 | + const result = spawnSync(command, commandArguments, { cwd: root, env: process.env, stdio: 'inherit' }); |
| 54 | + if (result.error !== undefined) throw result.error; |
| 55 | + if (result.status === 75) throw new Error(`retained upload timing could not acquire ${performanceLockPath}`); |
| 56 | + if (result.status !== 0) throw new Error(`retained upload timing failed under ${performanceLockPath}`); |
| 57 | +} |
| 58 | + |
| 59 | +async function run(): Promise<void> { |
| 60 | + let browser: Awaited<ReturnType<typeof launchProjectChromium>> | undefined; |
| 61 | + let server: ViteDevServer | undefined; |
| 62 | + try { |
| 63 | + server = await createServer({ |
| 64 | + root, |
| 65 | + logLevel: 'warn', |
| 66 | + optimizeDeps: { force: true }, |
| 67 | + resolve: { |
| 68 | + dedupe: ['three'], |
| 69 | + ...(glyphPackageRoot === undefined |
| 70 | + ? {} |
| 71 | + : { |
| 72 | + alias: [ |
| 73 | + { find: /^@pmndrs\/glyph\/three$/u, replacement: `${glyphPackageRoot}/dist/three.js` }, |
| 74 | + { find: /^@pmndrs\/glyph$/u, replacement: `${glyphPackageRoot}/dist/index.js` }, |
| 75 | + ], |
| 76 | + }), |
| 77 | + }, |
| 78 | + server: { |
| 79 | + host: process.env.HOST ?? '127.0.0.1', |
| 80 | + port, |
| 81 | + strictPort: port !== 0, |
| 82 | + ...(glyphPackageRoot === undefined ? {} : { fs: { allow: [root, glyphPackageRoot] } }), |
| 83 | + }, |
| 84 | + }); |
| 85 | + await server.listen(); |
| 86 | + const address = server.httpServer?.address(); |
| 87 | + if (address === null || address === undefined || typeof address === 'string') { |
| 88 | + throw new Error('Vite did not publish its loopback TCP address'); |
| 89 | + } |
| 90 | + const origin = process.env.PORTLESS_URL ?? `http://127.0.0.1:${String(address.port)}`; |
| 91 | + browser = await launchProjectChromium({ |
| 92 | + headless: true, |
| 93 | + args: ['--enable-gpu', '--ignore-gpu-blocklist', '--enable-unsafe-webgpu'], |
| 94 | + }); |
| 95 | + const results: RetainedUploadResult[] = []; |
| 96 | + for (const backend of ['webgpu', 'webgl2'] as const) { |
| 97 | + const page = await browser.newPage({ viewport: { width: 512, height: 512 }, deviceScaleFactor: 1 }); |
| 98 | + const errors: string[] = []; |
| 99 | + page.on('console', (message) => { |
| 100 | + if (message.type() === 'error') errors.push(message.text()); |
| 101 | + }); |
| 102 | + page.on('pageerror', (error) => errors.push(error.message)); |
| 103 | + await page.goto(`${origin}/retained-upload.html?backend=${backend}&usage=${usage}`, { |
| 104 | + waitUntil: 'domcontentloaded', |
| 105 | + }); |
| 106 | + await page.waitForFunction( |
| 107 | + () => |
| 108 | + (window as typeof window & { retainedUploadReady?: Promise<RetainedUploadResult> }).retainedUploadReady !== |
| 109 | + undefined, |
| 110 | + ); |
| 111 | + const result = await page.evaluate( |
| 112 | + () => (window as typeof window & { retainedUploadReady: Promise<RetainedUploadResult> }).retainedUploadReady, |
| 113 | + ); |
| 114 | + if (errors.length !== 0) throw new Error(`${backend} browser errors: ${errors.join(' | ')}`); |
| 115 | + if (result.backend !== backend) throw new Error(`expected ${backend}, received ${result.backend}`); |
| 116 | + results.push(result); |
| 117 | + await page.close(); |
| 118 | + } |
| 119 | + process.stdout.write( |
| 120 | + `${JSON.stringify( |
| 121 | + { |
| 122 | + schemaVersion: 1, |
| 123 | + environment: { |
| 124 | + browser: browser.version(), |
| 125 | + three: '0.185.1', |
| 126 | + glyph: glyphPackageRoot ?? 'workspace-source', |
| 127 | + }, |
| 128 | + workload: { |
| 129 | + model: 'one retained Text inside one TextGroup', |
| 130 | + initialColorSpans: 240, |
| 131 | + initialGlyphs: 240 * 12, |
| 132 | + idleFrames: 30, |
| 133 | + }, |
| 134 | + usage, |
| 135 | + results, |
| 136 | + limitations: [ |
| 137 | + 'Bytes are counted at Three common Attributes upload decisions and WebGL2 PBO texture updates; ordinary textures, uniforms, and swap-chain traffic are excluded.', |
| 138 | + 'CPU render timings measure synchronous renderer submission in one browser task, not display presentation cadence or GPU execution.', |
| 139 | + 'Headless project Chromium proves real backend execution and readback but is not labeled as a hardware-GPU timing result.', |
| 140 | + ], |
| 141 | + }, |
| 142 | + null, |
| 143 | + 2, |
| 144 | + )}\n`, |
| 145 | + ); |
| 146 | + } finally { |
| 147 | + try { |
| 148 | + await browser?.close(); |
| 149 | + } finally { |
| 150 | + await server?.close(); |
| 151 | + } |
| 152 | + } |
| 153 | +} |
0 commit comments