From b252aef0cf65f86f4c920f7585940b2c5b927d89 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 26 Oct 2026 00:51:28 -0700 Subject: [PATCH] perf(build): cap Rolldown threads on large hosts; build pairing web client in the background (#33828) Rolldown defaults to 1.5x physical cores Tokio workers plus a Rayon pool per logical CPU (24+24 here). Capping both at 8 on >8-core hosts cuts electron-vite build CPU 24% with byte-identical output. pnpm dev no longer blocks on a stale pairing web build; it stages into out/.web-next and swaps on success. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- config/scripts/rolldown-thread-limit.mjs | 16 +++++ config/scripts/rolldown-thread-limit.test.mjs | 17 +++++ config/scripts/run-electron-vite-build.mjs | 3 +- config/scripts/run-electron-vite-dev.mjs | 66 +++++++++++++++---- config/scripts/run-vite-web-build.mjs | 3 +- .../startup/run-electron-vite-dev-web.test.ts | 2 + 6 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 config/scripts/rolldown-thread-limit.mjs create mode 100644 config/scripts/rolldown-thread-limit.test.mjs diff --git a/config/scripts/rolldown-thread-limit.mjs b/config/scripts/rolldown-thread-limit.mjs new file mode 100644 index 00000000000..703fcf5033b --- /dev/null +++ b/config/scripts/rolldown-thread-limit.mjs @@ -0,0 +1,16 @@ +import os from 'node:os' + +// Measured on 24 threads: Rolldown's default 24+24 workers cost ~25% more CPU than 8 for no wall-time gain. +const maxRolldownThreads = '8' + +/** Caps Rolldown's Tokio and Rayon pools on large hosts unless the caller already chose sizes. */ +export function withRolldownThreadLimit(env, availableParallelism = os.availableParallelism()) { + if (availableParallelism <= Number(maxRolldownThreads)) { + return env + } + return { + ...env, + ROLLDOWN_WORKER_THREADS: env.ROLLDOWN_WORKER_THREADS ?? maxRolldownThreads, + RAYON_NUM_THREADS: env.RAYON_NUM_THREADS ?? maxRolldownThreads + } +} diff --git a/config/scripts/rolldown-thread-limit.test.mjs b/config/scripts/rolldown-thread-limit.test.mjs new file mode 100644 index 00000000000..77bbe26edd8 --- /dev/null +++ b/config/scripts/rolldown-thread-limit.test.mjs @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest' +import { withRolldownThreadLimit } from './rolldown-thread-limit.mjs' + +describe('withRolldownThreadLimit', () => { + it('caps both pools on large hosts', () => { + expect(withRolldownThreadLimit({ A: '1' }, 24)).toEqual({ + A: '1', + ROLLDOWN_WORKER_THREADS: '8', + RAYON_NUM_THREADS: '8' + }) + }) + + it('keeps Rolldown defaults on small hosts and explicit sizes everywhere', () => { + expect(withRolldownThreadLimit({}, 8)).toEqual({}) + expect(withRolldownThreadLimit({ RAYON_NUM_THREADS: '2' }, 24).RAYON_NUM_THREADS).toBe('2') + }) +}) diff --git a/config/scripts/run-electron-vite-build.mjs b/config/scripts/run-electron-vite-build.mjs index 6390ab7081a..39d2338c021 100644 --- a/config/scripts/run-electron-vite-build.mjs +++ b/config/scripts/run-electron-vite-build.mjs @@ -3,6 +3,7 @@ import fs from 'node:fs' import { createRequire } from 'node:module' import path from 'node:path' import { appendBuildOldSpaceOption } from './node-old-space-limit.mjs' +import { withRolldownThreadLimit } from './rolldown-thread-limit.mjs' import { RENDERER_BUILD_DIR, verifyRendererBootGraph } from './renderer-boot-graph.mjs' const require = createRequire(import.meta.url) @@ -15,7 +16,7 @@ const nodeOptions = appendBuildOldSpaceOption(process.env.NODE_OPTIONS) const child = spawn(process.execPath, [electronViteCli, 'build', ...process.argv.slice(2)], { stdio: 'inherit', env: { - ...process.env, + ...withRolldownThreadLimit(process.env), NODE_OPTIONS: nodeOptions } }) diff --git a/config/scripts/run-electron-vite-dev.mjs b/config/scripts/run-electron-vite-dev.mjs index ddca39aa654..a97a879648e 100644 --- a/config/scripts/run-electron-vite-dev.mjs +++ b/config/scripts/run-electron-vite-dev.mjs @@ -7,12 +7,14 @@ import { readdirSync, readFileSync, readlinkSync, + renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs' import { createRequire } from 'node:module' +import os from 'node:os' import path from 'node:path' import { prepareDevCliTerminalWrappers } from './dev-cli-terminal-wrapper.mjs' @@ -36,6 +38,7 @@ import { isDevWebClientFresh, recordDevWebClientInputs } from './dev-web-client-freshness.mjs' +import { withRolldownThreadLimit } from './rolldown-thread-limit.mjs' import { copyPrivateTree } from './space-sharing-copy.mjs' import { DEV_BUNDLE_ID, @@ -513,17 +516,53 @@ function prepareDevWebClient() { return } const inputs = captureDevWebClientInputs(repoRoot) - console.error('[orca-dev] Building web client for pairing...') - execFileSync( + console.error('[orca-dev] Building web client for pairing in the background...') + // Pairing reads out/web per request, so a stale bundle must not delay Electron startup. + // Stage then swap, so an interrupted build never leaves out/web empty. + const stagingDir = path.join(repoRoot, 'out', '.web-next') + const build = spawn( process.execPath, - [viteCli, 'build', '--config', path.join(repoRoot, 'vite.web.config.ts')], - { - cwd: repoRoot, - stdio: 'inherit', - env: process.env - } + [ + viteCli, + 'build', + '--config', + path.join(repoRoot, 'vite.web.config.ts'), + '--outDir', + stagingDir, + '--logLevel', + 'warn' + ], + { cwd: repoRoot, stdio: 'inherit', env: withRolldownThreadLimit(process.env) } ) - recordDevWebClientInputs(repoRoot, inputs) + webClientBuild = build + try { + os.setPriority(build.pid, os.constants.priority.PRIORITY_BELOW_NORMAL) + } catch { + // Priority is best effort; the build still runs at normal priority. + } + build.once('error', (error) => { + console.error(`[orca-dev] Web client build failed to start: ${error.message}`) + }) + build.once('exit', (code, signal) => { + webClientBuild = null + if (code !== 0) { + if (!isShuttingDown) { + console.error( + `[orca-dev] Web client build failed (${signal ?? code}); pairing serves the previous bundle` + ) + } + return + } + try { + const outDir = path.dirname(getDevWebClientIndexPath()) + rmSync(outDir, { recursive: true, force: true }) + renameSync(stagingDir, outDir) + recordDevWebClientInputs(repoRoot, inputs) + console.error('[orca-dev] Web client for pairing is ready') + } catch (error) { + console.error(`[orca-dev] Could not install the web client build: ${error.message}`) + } + }) } if (!isHelpOrVersion && process.env.ORCA_DEV_INSTANCE_LABEL) { @@ -542,7 +581,6 @@ if (!isHelpOrVersion) { forwardedExtras = ports.forwardedExtras announceDevInstance({ repoRoot, profile: devProfile, ...ports }) } -prepareDevWebClient() const forwardedArgs = ['dev', ...forwardedRaw, ...forwardedExtras] const child = spawn(process.execPath, [electronViteCli, ...forwardedArgs], { stdio: 'inherit', @@ -551,13 +589,16 @@ const child = spawn(process.execPath, [electronViteCli, ...forwardedArgs], { detached: process.platform !== 'win32' }) +let webClientBuild = null +let isShuttingDown = false + child.once('spawn', () => { if (!isHelpOrVersion) { startDevStorageReclaim({ repoRoot }) } + prepareDevWebClient() }) -let isShuttingDown = false let forcedKillTimer = null function signalExitCode(signal) { @@ -600,6 +641,7 @@ function beginShutdown(signal) { } isShuttingDown = true + webClientBuild?.kill() terminateChild(signal) forcedKillTimer = setTimeout(() => { terminateChild('SIGKILL') @@ -626,6 +668,8 @@ child.on('exit', (code, signal) => { if (forcedKillTimer) { clearTimeout(forcedKillTimer) } + // A half-written out/web must not outlive the session that started it. + webClientBuild?.kill() if (isShuttingDown) { process.exit(signalExitCode(signal ?? 'SIGINT')) diff --git a/config/scripts/run-vite-web-build.mjs b/config/scripts/run-vite-web-build.mjs index 6552d3bc582..b7c23451682 100644 --- a/config/scripts/run-vite-web-build.mjs +++ b/config/scripts/run-vite-web-build.mjs @@ -2,6 +2,7 @@ import { spawn } from 'node:child_process' import { createRequire } from 'node:module' import path from 'node:path' import { appendBuildOldSpaceOption } from './node-old-space-limit.mjs' +import { withRolldownThreadLimit } from './rolldown-thread-limit.mjs' const require = createRequire(import.meta.url) const vitePackageJson = require.resolve('vite/package.json') @@ -16,7 +17,7 @@ const child = spawn( { stdio: 'inherit', env: { - ...process.env, + ...withRolldownThreadLimit(process.env), NODE_OPTIONS: nodeOptions } } diff --git a/src/main/startup/run-electron-vite-dev-web.test.ts b/src/main/startup/run-electron-vite-dev-web.test.ts index b96ca1f7c0e..8fc33fba3ce 100644 --- a/src/main/startup/run-electron-vite-dev-web.test.ts +++ b/src/main/startup/run-electron-vite-dev-web.test.ts @@ -230,6 +230,8 @@ describe('run-electron-vite-dev web client prepare', () => { } }) + // The pairing build runs beside electron-vite instead of before it. + await waitFor(() => existsSync(viteFile)) expect(readFileSync(viteFile, 'utf8')).toContain('build') const trackedPids = trackPidFile(pidFile)