Run the check browser on the GPU instead of rasterising in software
The harness forced software WebGL (--disable-gpu --use-gl=swiftshader) on every host. Measured here on an idle editor page over 20 s: that costs 373% of a core at 8 fps, while the Intel HD 630 via ANGLE/Vulkan costs 69% at 56 fps. The full suite now runs in ~124 s. Note the app is not rendering 60 fps as previously assumed - in software it only reaches 8, and a single frame costs more than any throttle interval would save, which is why capping the framerate had measured no effect. --use-angle=gl and --use-gl=egl look cheaper still but get no WebGL context at all, so the app never renders. They are not usable and are named in the comment as such. There is deliberately no software fallback: a host without a usable GPU should fail loudly rather than drag the machine to a standstill. None of the 267 checks reads the rendered image anyway - they read window.__gm.locate() and the debug hook, and the screenshots assert nothing - but Pixi needs WebGL2 to run at all. The browser launch moves out of harness.mjs into tools/lib/browser.mjs, which also supports GM_REMOTE=user@host via an SSH-forwarded CDP port. That path is unfinished: the tunnel degraded until the suite crawled at ~25 s per check, and forcing software WebGL on a desktop with a real card brought it to its knees. README says so plainly rather than advertising it. Also: the jump-run HUD check asserted a fixed "Coins 1/10", which fails on a faster host where the same walk sweeps up two coins. It now asserts the HUD against the count the run actually reached - the real guarantee is that HUD and state agree. Verified: npm run check green (404 tests), 267 e2e ALL PASSED and 24 jump-run checks ALL PASSED against the GPU flags. The fallback removal leaves the flag set on this host identical (same flags, different order), so it was not re-run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3d038e11cb
commit
32a40ee34b
5 files changed
+180
-29
No files matched your search
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Where the headless browser for a check run lives.
|
||||
*
|
||||
* Local by default. Set `GM_REMOTE=user@host` to run it on another machine instead: an SSH
|
||||
* session forwards the CDP port back to 127.0.0.1, so everything downstream – waitForChrome,
|
||||
* /json/new, the WebSocket – stays unchanged. The checks load the public base URL, so the
|
||||
* remote side needs nothing but a browser and internet.
|
||||
*
|
||||
* Why bother: even with the GPU in use an idle editor page keeps rendering frames that never
|
||||
* change, and this machine has 8 GB with most of the swap already gone.
|
||||
*/
|
||||
import { spawn, execFileSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const LAUNCHER = join(HERE, '..', 'remote', 'gm-browser.ps1');
|
||||
|
||||
const FLAGS = [
|
||||
'--headless',
|
||||
'--hide-scrollbars',
|
||||
/**
|
||||
* Measured on this host (idle editor page, 20 s): software WebGL costs 373% of a core at 8 fps,
|
||||
* the Intel HD 630 via ANGLE/Vulkan costs 69% at 56 fps. There is deliberately no software
|
||||
* fallback — a host without a usable GPU should fail loudly rather than melt down. `--use-angle=gl`
|
||||
* and `--use-gl=egl` look even cheaper but get no WebGL context at all — do not use them.
|
||||
*/
|
||||
'--use-angle=vulkan',
|
||||
'--ignore-gpu-blocklist',
|
||||
'--enable-gpu',
|
||||
'--enable-features=Vulkan',
|
||||
// None of this serves a headless check, and its stderr was full of GCM sign-in retries.
|
||||
'--disable-background-networking',
|
||||
'--disable-sync',
|
||||
'--disable-extensions',
|
||||
'--no-first-run',
|
||||
'--mute-audio',
|
||||
];
|
||||
|
||||
function sshArgs(key) {
|
||||
return [
|
||||
'-i',
|
||||
key,
|
||||
'-o',
|
||||
'IdentitiesOnly=yes',
|
||||
'-o',
|
||||
'BatchMode=yes',
|
||||
'-o',
|
||||
'StrictHostKeyChecking=accept-new',
|
||||
'-o',
|
||||
'ConnectTimeout=15',
|
||||
// A dead peer must surface as a closed socket, not as a run that waits forever.
|
||||
'-o',
|
||||
'ServerAliveInterval=10',
|
||||
'-o',
|
||||
'ServerAliveCountMax=3',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the browser and returns its process plus a `stop()` that ends it the way that host
|
||||
* needs. `proc.stderr` carries the browser's own last words either way.
|
||||
*/
|
||||
export function launchBrowser({ port, width, height }) {
|
||||
const remote = process.env.GM_REMOTE;
|
||||
if (!remote) {
|
||||
const proc = spawn(
|
||||
'chromium',
|
||||
// --no-sandbox: this container runs as root, where chromium refuses to start otherwise.
|
||||
[
|
||||
...FLAGS,
|
||||
'--no-sandbox',
|
||||
`--remote-debugging-port=${port}`,
|
||||
`--window-size=${width},${height}`,
|
||||
'about:blank',
|
||||
],
|
||||
{ stdio: ['ignore', 'ignore', 'pipe'] },
|
||||
);
|
||||
return { proc, stop: () => proc.kill('SIGTERM'), where: 'local chromium' };
|
||||
}
|
||||
|
||||
const key = process.env.GM_REMOTE_KEY ?? join(homedir(), '.ssh', 'gm-win.key');
|
||||
// Copied every run: a stale launcher on the remote is a debugging session nobody enjoys.
|
||||
execFileSync('scp', [...sshArgs(key), LAUNCHER, `${remote}:gm-browser.ps1`], { stdio: 'pipe' });
|
||||
|
||||
const proc = spawn(
|
||||
'ssh',
|
||||
[
|
||||
...sshArgs(key),
|
||||
'-o',
|
||||
'ExitOnForwardFailure=yes',
|
||||
'-L',
|
||||
`${port}:127.0.0.1:${port}`,
|
||||
remote,
|
||||
`powershell -NoProfile -ExecutionPolicy Bypass -File gm-browser.ps1 -Port ${port} -Size ${width},${height}`,
|
||||
],
|
||||
// stdin stays open on purpose: closing it is what tells the launcher to kill the browser.
|
||||
{ stdio: ['pipe', 'ignore', 'pipe'] },
|
||||
);
|
||||
|
||||
return { proc, stop: () => proc.stdin.end(), where: `remote browser on ${remote}` };
|
||||
}
|
||||
Reference in new issue
Block a user