diff --git a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts index 5ce1130d093a..f80f3c78afca 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -20,7 +20,11 @@ import type { import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; -import { maxWorkers, useTypeChecking } from '../../../utils/environment-options'; +import { + hasCustomMaxWorkers, + maxWorkers, + useTypeChecking, +} from '../../../utils/environment-options'; import { calculateHash, initializeHash } from '../../../utils/hash'; import { AngularHostOptions } from '../../angular/angular-host'; import { AngularCompilation, DiagnosticModes } from '../../angular/compilation'; @@ -97,6 +101,10 @@ export function createCompilerPlugin( }); } } + // During bundling, esbuild runs its own multi-threaded Go process across all available cores. + // Unless explicitly configured via NG_BUILD_MAX_WORKERS, cap transformation concurrency to at + // most 4 to prevent CPU contention during bundling. + const maxTransformWorkers = hasCustomMaxWorkers ? maxWorkers : Math.min(4, maxWorkers); const javascriptTransformer = new JavaScriptTransformer( { sourcemap: !!pluginOptions.sourcemap, @@ -104,7 +112,7 @@ export function createCompilerPlugin( advancedOptimizations: pluginOptions.advancedOptimizations, jit: pluginOptions.jit || pluginOptions.includeTestMetadata, }, - maxWorkers, + maxTransformWorkers, cacheStore?.createCache('jstransformer'), ); diff --git a/packages/angular/build/src/utils/environment-options.ts b/packages/angular/build/src/utils/environment-options.ts index f008673a1552..1e225d6b7386 100644 --- a/packages/angular/build/src/utils/environment-options.ts +++ b/packages/angular/build/src/utils/environment-options.ts @@ -108,24 +108,28 @@ export const allowMinify = debugOptimize.minify; */ export const useRolldownChunks = parseTristate(process.env['NG_BUILD_CHUNKS_ROLLDOWN']) ?? true; +const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS']; + +let customMaxWorkers: number | undefined; +if (isPresent(maxWorkersVariable)) { + const parsed = +maxWorkersVariable; + if (Number.isInteger(parsed) && parsed >= 1) { + customMaxWorkers = parsed; + } +} + /** - * Some environments, like CircleCI which use Docker report a number of CPUs by the host and not the count of available. - * This cause `Error: Call retries were exceeded` errors when trying to use them. - * - * @see https://github.com/nodejs/node/issues/28762 - * @see https://github.com/webpack-contrib/terser-webpack-plugin/issues/143 - * @see https://ithub.com/angular/angular-cli/issues/16860#issuecomment-588828079 - * + * Whether the maximum number of workers was explicitly configured via the + * `NG_BUILD_MAX_WORKERS` environment variable. */ -const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS']; +export const hasCustomMaxWorkers = customMaxWorkers !== undefined; /** * The maximum number of workers to use for parallel processing. * This can be controlled by the `NG_BUILD_MAX_WORKERS` environment variable. + * When not set, defaults to available parallelism minus one to ensure the main thread is not starved. */ -export const maxWorkers = isPresent(maxWorkersVariable) - ? +maxWorkersVariable - : Math.min(4, Math.max(availableParallelism() - 1, 1)); +export const maxWorkers = customMaxWorkers ?? Math.max(availableParallelism() - 1, 1); /** * When `NG_BUILD_PARALLEL_TS` is set to `0` or `false`, parallel TypeScript compilation is disabled. diff --git a/packages/angular/build/src/utils/environment-options_spec.ts b/packages/angular/build/src/utils/environment-options_spec.ts new file mode 100644 index 000000000000..0d2ac7fa0c61 --- /dev/null +++ b/packages/angular/build/src/utils/environment-options_spec.ts @@ -0,0 +1,116 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { availableParallelism } from 'node:os'; + +describe('environment options - maxWorkers', () => { + const originalEnvValue = process.env['NG_BUILD_MAX_WORKERS']; + + function loadEnvironmentOptions(): typeof import('./environment-options') { + delete require.cache[require.resolve('./environment-options')]; + + return require('./environment-options'); + } + + afterEach(() => { + if (originalEnvValue !== undefined) { + process.env['NG_BUILD_MAX_WORKERS'] = originalEnvValue; + } else { + delete process.env['NG_BUILD_MAX_WORKERS']; + } + delete require.cache[require.resolve('./environment-options')]; + }); + + it('defaults maxWorkers to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is unset', () => { + delete process.env['NG_BUILD_MAX_WORKERS']; + const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions(); + + expect(hasCustomMaxWorkers).toBeFalse(); + expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1)); + }); + + it('uses configured positive integer when NG_BUILD_MAX_WORKERS is set', () => { + process.env['NG_BUILD_MAX_WORKERS'] = '8'; + const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions(); + + expect(hasCustomMaxWorkers).toBeTrue(); + expect(maxWorkers).toBe(8); + }); + + it('allows maxWorkers greater than 4 when explicitly configured', () => { + process.env['NG_BUILD_MAX_WORKERS'] = '32'; + const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions(); + + expect(hasCustomMaxWorkers).toBeTrue(); + expect(maxWorkers).toBe(32); + }); + + it('supports maxWorkers set to 1', () => { + process.env['NG_BUILD_MAX_WORKERS'] = '1'; + const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions(); + + expect(hasCustomMaxWorkers).toBeTrue(); + expect(maxWorkers).toBe(1); + }); + + it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is 0', () => { + process.env['NG_BUILD_MAX_WORKERS'] = '0'; + const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions(); + + expect(hasCustomMaxWorkers).toBeFalse(); + expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1)); + }); + + it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is negative', () => { + process.env['NG_BUILD_MAX_WORKERS'] = '-4'; + const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions(); + + expect(hasCustomMaxWorkers).toBeFalse(); + expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1)); + }); + + it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is not a number', () => { + process.env['NG_BUILD_MAX_WORKERS'] = 'invalid'; + const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions(); + + expect(hasCustomMaxWorkers).toBeFalse(); + expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1)); + }); + + it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is a float', () => { + process.env['NG_BUILD_MAX_WORKERS'] = '2.5'; + const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions(); + + expect(hasCustomMaxWorkers).toBeFalse(); + expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1)); + }); + + it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is an empty string', () => { + process.env['NG_BUILD_MAX_WORKERS'] = ''; + const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions(); + + expect(hasCustomMaxWorkers).toBeFalse(); + expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1)); + }); + + it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is whitespace only', () => { + process.env['NG_BUILD_MAX_WORKERS'] = ' '; + const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions(); + + expect(hasCustomMaxWorkers).toBeFalse(); + expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1)); + }); + + it('parses positive integers with surrounding whitespace', () => { + process.env['NG_BUILD_MAX_WORKERS'] = ' 8 '; + const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions(); + + expect(hasCustomMaxWorkers).toBeTrue(); + expect(maxWorkers).toBe(8); + }); +});