Skip to content

Commit a67ec1d

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.
1 parent 8235b43 commit a67ec1d

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)