Skip to content

Commit c76b2d5

Browse files
committed
refactor(@angular/build): cap default i18n inlining concurrency to 8
Translation inlining and sourcemap remapping are CPU- and memory-intensive operations. When Piscina is initialized without explicit maxThreads, it defaults to floor(cpus * 1.5), spawning 18 to 48 worker threads on high-core machines. This excessive concurrency causes severe thread oversubscription, memory allocator lock contention, and high-core performance degradation under Promise.all barriers. This change introduces maxInlinerWorkers in environment options defaulting to min(8, availableParallelism()) while preserving explicit overrides via NG_BUILD_MAX_WORKERS. Concurrency passed to I18nInliner in the application builder is bounded to maxInlinerWorkers, capping standalone workers and shared worker pools while honoring shared pool thread limits when lower. (cherry picked from commit 93c118e)
1 parent 6ffd267 commit c76b2d5

3 files changed

Lines changed: 77 additions & 2 deletions

File tree

packages/angular/build/src/builders/application/i18n.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from '../../tools/esbuild/bundler-execution-result';
1818
import { BuildOutputFileType, InitialFileRecord } from '../../tools/esbuild/bundler-files';
1919
import { I18nInliner } from '../../tools/esbuild/i18n-inliner';
20-
import { maxWorkers } from '../../utils/environment-options';
20+
import { maxInlinerWorkers } from '../../utils/environment-options';
2121
import { loadTranslations } from '../../utils/i18n-options';
2222
import { createTranslationLoader } from '../../utils/load-translations';
2323
import { createProjectResolver } from '../../utils/resolve-project';
@@ -51,7 +51,9 @@ export async function inlineI18n(
5151
const inliner = new I18nInliner(
5252
{
5353
missingTranslation: i18nOptions.missingTranslationBehavior ?? 'warning',
54-
maxConcurrency: workerPool ? undefined : maxWorkers,
54+
maxConcurrency: workerPool
55+
? Math.min(workerPool.maxThreads, maxInlinerWorkers)
56+
: maxInlinerWorkers,
5557
persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined,
5658
localizeVersion: i18nOptions.localizeVersion,
5759
},

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,15 @@ export const maxWorkers = customMaxWorkers ?? Math.max(availableParallelism() -
141141
export const maxTransformWorkers =
142142
customMaxWorkers ?? Math.max(1, Math.min(6, Math.floor(availableParallelism() / 4)));
143143

144+
/**
145+
* The maximum number of workers to use for i18n translation inlining.
146+
* Translation inlining and sourcemap remapping are CPU- and memory-intensive operations.
147+
* To prevent thread oversubscription, memory allocator lock contention, and high-core
148+
* performance degradation, concurrency is capped at 8 unless overridden by
149+
* `NG_BUILD_MAX_WORKERS`.
150+
*/
151+
export const maxInlinerWorkers = customMaxWorkers ?? Math.min(8, availableParallelism());
152+
144153
/**
145154
* When `NG_BUILD_PARALLEL_TS` is set to `0` or `false`, parallel TypeScript compilation is disabled.
146155
*/

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,67 @@ describe('environment options - maxTransformWorkers', () => {
181181
expect(maxTransformWorkers).toBe(expected);
182182
});
183183
});
184+
185+
describe('environment options - maxInlinerWorkers', () => {
186+
const originalEnvValue = process.env['NG_BUILD_MAX_WORKERS'];
187+
188+
function loadEnvironmentOptions(): typeof import('./environment-options') {
189+
delete require.cache[require.resolve('./environment-options')];
190+
191+
return require('./environment-options');
192+
}
193+
194+
afterEach(() => {
195+
if (originalEnvValue !== undefined) {
196+
process.env['NG_BUILD_MAX_WORKERS'] = originalEnvValue;
197+
} else {
198+
delete process.env['NG_BUILD_MAX_WORKERS'];
199+
}
200+
delete require.cache[require.resolve('./environment-options')];
201+
});
202+
203+
it('defaults maxInlinerWorkers to min(8, availableParallelism()) when NG_BUILD_MAX_WORKERS is unset', () => {
204+
delete process.env['NG_BUILD_MAX_WORKERS'];
205+
const { maxInlinerWorkers } = loadEnvironmentOptions();
206+
207+
expect(maxInlinerWorkers).toBe(Math.min(8, availableParallelism()));
208+
});
209+
210+
it('uses configured positive integer when NG_BUILD_MAX_WORKERS is set', () => {
211+
process.env['NG_BUILD_MAX_WORKERS'] = '4';
212+
const { maxInlinerWorkers } = loadEnvironmentOptions();
213+
214+
expect(maxInlinerWorkers).toBe(4);
215+
});
216+
217+
it('allows maxInlinerWorkers greater than 8 when explicitly configured', () => {
218+
process.env['NG_BUILD_MAX_WORKERS'] = '16';
219+
const { maxInlinerWorkers } = loadEnvironmentOptions();
220+
221+
expect(maxInlinerWorkers).toBe(16);
222+
});
223+
224+
it('supports maxInlinerWorkers set to 1', () => {
225+
process.env['NG_BUILD_MAX_WORKERS'] = '1';
226+
const { maxInlinerWorkers } = loadEnvironmentOptions();
227+
228+
expect(maxInlinerWorkers).toBe(1);
229+
});
230+
231+
it('falls back to min(8, availableParallelism()) when NG_BUILD_MAX_WORKERS is 0 or negative', () => {
232+
process.env['NG_BUILD_MAX_WORKERS'] = '0';
233+
const { maxInlinerWorkers: zeroWorkers } = loadEnvironmentOptions();
234+
expect(zeroWorkers).toBe(Math.min(8, availableParallelism()));
235+
236+
process.env['NG_BUILD_MAX_WORKERS'] = '-4';
237+
const { maxInlinerWorkers: negativeWorkers } = loadEnvironmentOptions();
238+
expect(negativeWorkers).toBe(Math.min(8, availableParallelism()));
239+
});
240+
241+
it('falls back to min(8, availableParallelism()) when NG_BUILD_MAX_WORKERS is invalid', () => {
242+
process.env['NG_BUILD_MAX_WORKERS'] = 'invalid';
243+
const { maxInlinerWorkers } = loadEnvironmentOptions();
244+
245+
expect(maxInlinerWorkers).toBe(Math.min(8, availableParallelism()));
246+
});
247+
});

0 commit comments

Comments
 (0)