From ff5a89129fa79637270f2c73a6f65e9ae1c88505 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:50:44 -0800 Subject: [PATCH] Fix `pnpm bench` on macos --- bin/benchmark.mjs | 4 ++ bin/benchmark/run.mjs | 40 ++++++++++++++++--- package.json | 5 ++- patches/@tracerbench__core@8.0.1.patch | 15 +++++++ pnpm-lock.yaml | 9 ++++- .../benchmark-app/app/run-benchmark.js | 27 ++++++++++--- 6 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 patches/@tracerbench__core@8.0.1.patch diff --git a/bin/benchmark.mjs b/bin/benchmark.mjs index bf84459a61b..c7e734c71d5 100644 --- a/bin/benchmark.mjs +++ b/bin/benchmark.mjs @@ -16,6 +16,8 @@ Output directory: Options: --force delete cached directories before running --reuse reuse existing apps and tarballs, if available (by default only the control app/tarball is reused) + --no-headless run Chrome without headless mode (opens visible browser windows) + On macOS, benchmarks will run sequentially (one browser at a time) Notes: - This script runs \`pnpm install\` and \`node ./bin/build-for-publishing.js\` in both repos. @@ -27,11 +29,13 @@ Notes: const FORCE = hasFlag(process.argv, '--force'); const REUSE = hasFlag(process.argv, '--reuse'); +const HEADLESS = !hasFlag(process.argv, '--no-headless'); try { await runBenchmark({ force: FORCE, reuse: REUSE, + headless: HEADLESS, }); } catch (error) { console.error(error); diff --git a/bin/benchmark/run.mjs b/bin/benchmark/run.mjs index 9e8413c7379..a31d121e975 100644 --- a/bin/benchmark/run.mjs +++ b/bin/benchmark/run.mjs @@ -66,7 +66,7 @@ const EXPERIMENT_DIRS = { repo: REPO_ROOT, }; -export async function runBenchmark({ force = false, reuse = false } = {}) { +export async function runBenchmark({ force = false, reuse = false, headless = true } = {}) { await ensureDir(BENCH_ROOT); if (force) { @@ -119,7 +119,7 @@ export async function runBenchmark({ force = false, reuse = false } = {}) { }); try { - await bootAndRun(); + await bootAndRun({ headless }); } finally { console.log(`\n\tCleaning up servers with SIGKILL...`); @@ -127,7 +127,7 @@ export async function runBenchmark({ force = false, reuse = false } = {}) { } } -async function bootAndRun() { +async function bootAndRun({ headless = true } = {}) { const controlUrl = `http://127.0.0.1:${DEFAULT_CONTROL_PORT}`; const experimentUrl = `http://127.0.0.1:${DEFAULT_EXPERIMENT_PORT}`; const markersString = buildMarkersString(DEFAULT_MARKERS); @@ -164,14 +164,44 @@ async function bootAndRun() { '--experimentURL', experimentUrl, '--report', - '--headless', '--cpuThrottleRate', DEFAULT_THROTTLE, '--markers', markersString, '--debug', '--browserArgs', - `"--incognito,--disable-gpu,--mute-audio,--log-level=3,--headless=new"`, + [ + '--no-sandbox', + '--crash-dumps-dir=./tmp', + // Disable task throttling (also in TracerBench defaults, but explicit here for clarity) + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding', + // Disable caching and unnecessary subsystems + '--disable-dev-shm-usage', + '--disable-cache', + '--disable-v8-idle-tasks', + '--disable-breakpad', + '--disable-component-update', + '--disable-background-networking', + '--disable-notifications', + '--disable-hang-monitor', + '--safebrowsing-disable-auto-update', + '--ignore-certificate-errors', + '--v8-cache-options=none', + // Use the new headless mode to support multiple targets + ...(headless ? ['--headless=new'] : []), + // GPU: use software rendering via SwiftShader, but do NOT + // combine --disable-gpu with --use-gl or --disable-software-rasterizer + // as the contradictory flags cause use-after-free crashes on macOS + '--disable-gpu', + '--disable-gpu-compositing', + // Disable Chrome ML/TFLite features (suppresses XNNPACK/TFLite init) + '--disable-features=TranslateUI', + '--disable-features=UseChromiumML', + '--disable-features=UseTfLite', + '--disable-features=TensorFlowLite', + ].join(','), ]; await run('node', args, { cwd: EXPERIMENT_DIRS.app }); diff --git a/package.json b/package.json index 3d420d7b6c9..8914708785f 100644 --- a/package.json +++ b/package.json @@ -166,7 +166,10 @@ "@swc/core", "core-js", "esbuild" - ] + ], + "patchedDependencies": { + "@tracerbench/core@8.0.1": "patches/@tracerbench__core@8.0.1.patch" + } }, "peerDependencies": { "@glimmer/component": ">= 1.1.2" diff --git a/patches/@tracerbench__core@8.0.1.patch b/patches/@tracerbench__core@8.0.1.patch new file mode 100644 index 00000000000..fa9b0967701 --- /dev/null +++ b/patches/@tracerbench__core@8.0.1.patch @@ -0,0 +1,15 @@ +diff --git a/dist/create-trace-benchmark.js b/dist/create-trace-benchmark.js +index 5918e8f7665b3e796ef88283fc40c2b3286a564f..e1a8768964de8b3c16d38bfb6280d836b45aba0b 100644 +--- a/dist/create-trace-benchmark.js ++++ b/dist/create-trace-benchmark.js +@@ -45,9 +45,8 @@ function createTraceBenchmark(group, sampleTrace, options = {}) { + } + exports.default = createTraceBenchmark; + function getCategories(isTrial, options) { +- const categories = ['-*', ...defaultCategories]; ++ const categories = [...defaultCategories]; + if (isTrial) { +- categories.push(...captureAllDevtoolsTimelineCategories, ...captureCpuProfileCategories, captureCpuProfilesHiresCategory, captureFilmStripCategory, ...capturePaintProfileCategories); + if (options.additionalTrialCategories) { + categories.push(...options.additionalTrialCategories); + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b7d9a90722..dbb85d22795 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,11 @@ overrides: rollup: ^4.2.0 ember-cli-htmlbars: ^7.0.0 +patchedDependencies: + '@tracerbench/core@8.0.1': + hash: 94ed69d4e124c0c94f1c1e3332668ae5d3265509b12cc97dd634feee8ed7e846 + path: patches/@tracerbench__core@8.0.1.patch + importers: .: @@ -18023,7 +18028,7 @@ snapshots: '@tootallnate/quickjs-emscripten@0.23.0': {} - '@tracerbench/core@8.0.1': + '@tracerbench/core@8.0.1(patch_hash=94ed69d4e124c0c94f1c1e3332668ae5d3265509b12cc97dd634feee8ed7e846)': dependencies: '@tracerbench/har': 8.0.0 '@tracerbench/trace-event': 8.0.0 @@ -26769,7 +26774,7 @@ snapshots: '@oclif/parser': 3.8.17 '@oclif/plugin-help': 5.2.20(@swc/core@1.15.11)(@types/node@22.19.11)(typescript@5.9.3) '@oclif/plugin-warn-if-update-available': 2.1.1(@swc/core@1.15.11)(@types/node@22.19.11)(typescript@5.9.3) - '@tracerbench/core': 8.0.1 + '@tracerbench/core': 8.0.1(patch_hash=94ed69d4e124c0c94f1c1e3332668ae5d3265509b12cc97dd634feee8ed7e846) '@tracerbench/stats': 8.0.1 '@tracerbench/trace-event': 8.0.0 archiver: 5.3.2 diff --git a/smoke-tests/benchmark-app/app/run-benchmark.js b/smoke-tests/benchmark-app/app/run-benchmark.js index 7588c9e2c71..1836c39c7bd 100644 --- a/smoke-tests/benchmark-app/app/run-benchmark.js +++ b/smoke-tests/benchmark-app/app/run-benchmark.js @@ -33,6 +33,21 @@ export function waitForIdle() { }); } +/** + * After heavy DOM operations (e.g. rendering/clearing 5000 items), Chrome's + * internal trace-writer thread may fall behind. requestIdleCallback only waits + * for the *main* thread to be idle — the trace-writer is a separate thread. + * This explicit delay gives the trace writer time to flush its buffer so that + * subsequent performance.mark() events are not lost to packet drops. + */ +export function waitForTraceFlush() { + return new Promise((resolve) => { + requestIdleCallback(() => { + setTimeout(resolve, 100); + }); + }); +} + export function enforcePaintEvent() { const docElem = document.documentElement; @@ -64,12 +79,12 @@ async function renderBenchmark() { let resolveRender await measureRender('render', 'renderStart', 'renderEnd', () => { - requestIdleCallback(() => { + requestIdleCallback(() => { if (!resolveRender) return; resolveRender(); resolveRender = undefined; - }); + }); }); @@ -134,25 +149,25 @@ export async function runBenchmark() { emitDomClickEvent(ButtonSelectors.Clear); }); - await waitForIdle(); + await waitForTraceFlush(); await app('render5000Items1', () => { emitDomClickEvent(ButtonSelectors.Create5000); }); - await waitForIdle(); + await waitForTraceFlush(); await app('clearManyItems1', () => { emitDomClickEvent(ButtonSelectors.Clear); }); - await waitForIdle(); + await waitForTraceFlush(); await app('render5000Items2', () => { emitDomClickEvent(ButtonSelectors.Create5000); }); - await waitForIdle(); + await waitForTraceFlush(); await app('clearManyItems2', () => { emitDomClickEvent(ButtonSelectors.Clear);