Skip to content

Commit 641f70b

Browse files
Merge pull request #265 from pmndrs/feat/retained-buffer-uploads
fix(three): avoid redundant retained buffer uploads
2 parents 8deacc5 + 3c7d20d commit 641f70b

15 files changed

Lines changed: 870 additions & 4 deletions

‎.agents/docs/attestations/2026-10-09-glyph-95b35aa7.md‎

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.agents/docs/attestations/2026-10-09-glyph-b0d68260.md‎

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.agents/docs/attestations/2026-10-09-glyph-benchmarks-0c28a343.md‎

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.agents/docs/attestations/2026-10-09-glyph-benchmarks-3568aecc.md‎

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.agents/docs/attestations/2026-10-09-glyph-benchmarks-8050b562.md‎

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.agents/docs/log/2026-10-09-retained-three-buffer-uploads.md‎

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.agents/docs/packages/benchmarks.md‎

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.agents/docs/packages/glyph.md‎

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎benches/retained-upload.html‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" href="data:," />
6+
<title>Retained buffer upload proof</title>
7+
</head>
8+
<body style="margin: 0; background: black">
9+
<canvas id="canvas" width="512" height="512"></canvas>
10+
<script type="module" src="/src/retained-upload-proof.ts"></script>
11+
</body>
12+
</html>
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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

Comments
 (0)