Skip to content

Commit c05a270

Browse files
committed
refactor(@angular/build): remove arbitrary cap from maxWorkers and localize bundling concurrency
Previously, maxWorkers was clamped to a maximum of 4 globally across the entire build system. This limitation was originally introduced to mitigate memory pressure from Babel transforms, but subsequent optimizations such as the OXC linker migration, zero-copy shared memory for i18n translations, and bounded memory buffers have eliminated those memory constraints. Consequently, clamping maxWorkers artificially restricted post-bundle operations like translation inlining and route prerendering on high-core systems. This change decouples the global default of maxWorkers so that it scales with available parallelism minus one, ensuring the main thread is not starved while allowing parallel tasks to utilize full hardware capacity. When NG_BUILD_MAX_WORKERS is specified, it is safely parsed as a positive integer or falls back to the default available parallelism. To avoid CPU contention during bundling when esbuild concurrently executes its internal multi-threaded Go routine across all cores, transformation concurrency for JavaScriptTransformer is now locally capped to at most 4 unless NG_BUILD_MAX_WORKERS has been explicitly provided, tracked via the exported hasCustomMaxWorkers option.
1 parent 6ee559c commit c05a270

3 files changed

Lines changed: 141 additions & 13 deletions

File tree

packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import type {
2020
import assert from 'node:assert';
2121
import { readFile } from 'node:fs/promises';
2222
import * as path from 'node:path';
23-
import { maxWorkers, useTypeChecking } from '../../../utils/environment-options';
23+
import {
24+
hasCustomMaxWorkers,
25+
maxWorkers,
26+
useTypeChecking,
27+
} from '../../../utils/environment-options';
2428
import { calculateHash, initializeHash } from '../../../utils/hash';
2529
import { AngularHostOptions } from '../../angular/angular-host';
2630
import { AngularCompilation, DiagnosticModes } from '../../angular/compilation';
@@ -97,14 +101,18 @@ export function createCompilerPlugin(
97101
});
98102
}
99103
}
104+
// During bundling, esbuild runs its own multi-threaded Go process across all available cores.
105+
// Unless explicitly configured via NG_BUILD_MAX_WORKERS, cap transformation concurrency to at
106+
// most 4 to prevent CPU contention during bundling.
107+
const maxTransformWorkers = hasCustomMaxWorkers ? maxWorkers : Math.min(4, maxWorkers);
100108
const javascriptTransformer = new JavaScriptTransformer(
101109
{
102110
sourcemap: !!pluginOptions.sourcemap,
103111
thirdPartySourcemaps: pluginOptions.thirdPartySourcemaps,
104112
advancedOptimizations: pluginOptions.advancedOptimizations,
105113
jit: pluginOptions.jit || pluginOptions.includeTestMetadata,
106114
},
107-
maxWorkers,
115+
maxTransformWorkers,
108116
cacheStore?.createCache('jstransformer'),
109117
);
110118

packages/angular/build/src/utils/environment-options.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,24 +108,28 @@ export const allowMinify = debugOptimize.minify;
108108
*/
109109
export const useRolldownChunks = parseTristate(process.env['NG_BUILD_CHUNKS_ROLLDOWN']) ?? true;
110110

111+
const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS'];
112+
113+
let customMaxWorkers: number | undefined;
114+
if (isPresent(maxWorkersVariable)) {
115+
const parsed = +maxWorkersVariable;
116+
if (Number.isInteger(parsed) && parsed >= 1) {
117+
customMaxWorkers = parsed;
118+
}
119+
}
120+
111121
/**
112-
* Some environments, like CircleCI which use Docker report a number of CPUs by the host and not the count of available.
113-
* This cause `Error: Call retries were exceeded` errors when trying to use them.
114-
*
115-
* @see https://github.com/nodejs/node/issues/28762
116-
* @see https://github.com/webpack-contrib/terser-webpack-plugin/issues/143
117-
* @see https://ithub.com/angular/angular-cli/issues/16860#issuecomment-588828079
118-
*
122+
* Whether the maximum number of workers was explicitly configured via the
123+
* `NG_BUILD_MAX_WORKERS` environment variable.
119124
*/
120-
const maxWorkersVariable = process.env['NG_BUILD_MAX_WORKERS'];
125+
export const hasCustomMaxWorkers = customMaxWorkers !== undefined;
121126

122127
/**
123128
* The maximum number of workers to use for parallel processing.
124129
* This can be controlled by the `NG_BUILD_MAX_WORKERS` environment variable.
130+
* When not set, defaults to available parallelism minus one to ensure the main thread is not starved.
125131
*/
126-
export const maxWorkers = isPresent(maxWorkersVariable)
127-
? +maxWorkersVariable
128-
: Math.min(4, Math.max(availableParallelism() - 1, 1));
132+
export const maxWorkers = customMaxWorkers ?? Math.max(availableParallelism() - 1, 1);
129133

130134
/**
131135
* When `NG_BUILD_PARALLEL_TS` is set to `0` or `false`, parallel TypeScript compilation is disabled.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { availableParallelism } from 'node:os';
10+
11+
describe('environment options - maxWorkers', () => {
12+
const originalEnvValue = process.env['NG_BUILD_MAX_WORKERS'];
13+
14+
function loadEnvironmentOptions(): typeof import('./environment-options') {
15+
delete require.cache[require.resolve('./environment-options')];
16+
17+
return require('./environment-options');
18+
}
19+
20+
afterEach(() => {
21+
if (originalEnvValue !== undefined) {
22+
process.env['NG_BUILD_MAX_WORKERS'] = originalEnvValue;
23+
} else {
24+
delete process.env['NG_BUILD_MAX_WORKERS'];
25+
}
26+
delete require.cache[require.resolve('./environment-options')];
27+
});
28+
29+
it('defaults maxWorkers to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is unset', () => {
30+
delete process.env['NG_BUILD_MAX_WORKERS'];
31+
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();
32+
33+
expect(hasCustomMaxWorkers).toBeFalse();
34+
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
35+
});
36+
37+
it('uses configured positive integer when NG_BUILD_MAX_WORKERS is set', () => {
38+
process.env['NG_BUILD_MAX_WORKERS'] = '8';
39+
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();
40+
41+
expect(hasCustomMaxWorkers).toBeTrue();
42+
expect(maxWorkers).toBe(8);
43+
});
44+
45+
it('allows maxWorkers greater than 4 when explicitly configured', () => {
46+
process.env['NG_BUILD_MAX_WORKERS'] = '32';
47+
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();
48+
49+
expect(hasCustomMaxWorkers).toBeTrue();
50+
expect(maxWorkers).toBe(32);
51+
});
52+
53+
it('supports maxWorkers set to 1', () => {
54+
process.env['NG_BUILD_MAX_WORKERS'] = '1';
55+
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();
56+
57+
expect(hasCustomMaxWorkers).toBeTrue();
58+
expect(maxWorkers).toBe(1);
59+
});
60+
61+
it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is 0', () => {
62+
process.env['NG_BUILD_MAX_WORKERS'] = '0';
63+
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();
64+
65+
expect(hasCustomMaxWorkers).toBeFalse();
66+
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
67+
});
68+
69+
it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is negative', () => {
70+
process.env['NG_BUILD_MAX_WORKERS'] = '-4';
71+
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();
72+
73+
expect(hasCustomMaxWorkers).toBeFalse();
74+
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
75+
});
76+
77+
it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is not a number', () => {
78+
process.env['NG_BUILD_MAX_WORKERS'] = 'invalid';
79+
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();
80+
81+
expect(hasCustomMaxWorkers).toBeFalse();
82+
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
83+
});
84+
85+
it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is a float', () => {
86+
process.env['NG_BUILD_MAX_WORKERS'] = '2.5';
87+
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();
88+
89+
expect(hasCustomMaxWorkers).toBeFalse();
90+
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
91+
});
92+
93+
it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is an empty string', () => {
94+
process.env['NG_BUILD_MAX_WORKERS'] = '';
95+
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();
96+
97+
expect(hasCustomMaxWorkers).toBeFalse();
98+
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
99+
});
100+
101+
it('falls back to availableParallelism - 1 when NG_BUILD_MAX_WORKERS is whitespace only', () => {
102+
process.env['NG_BUILD_MAX_WORKERS'] = ' ';
103+
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();
104+
105+
expect(hasCustomMaxWorkers).toBeFalse();
106+
expect(maxWorkers).toBe(Math.max(availableParallelism() - 1, 1));
107+
});
108+
109+
it('parses positive integers with surrounding whitespace', () => {
110+
process.env['NG_BUILD_MAX_WORKERS'] = ' 8 ';
111+
const { maxWorkers, hasCustomMaxWorkers } = loadEnvironmentOptions();
112+
113+
expect(hasCustomMaxWorkers).toBeTrue();
114+
expect(maxWorkers).toBe(8);
115+
});
116+
});

0 commit comments

Comments
 (0)