Skip to content

Commit 2afc7f5

Browse files
committed
perf(@angular/build): bound aggregate worker allocation across dominant files in i18n inliner
Previously, MAX_DOMINANT_WORKERS (4) was applied independently per dominant file. When multiple large bundles qualified as dominant in the same window (e.g., 3 bundles of 3 MB each), each bundle claimed up to 4 worker threads, allocating up to 12 heavy tasks simultaneously. This could saturate the entire worker pool with large AST and sourcemap remappings, starving smaller chunks and spiking peak RSS. The target worker count per dominant file now divides MAX_DOMINANT_WORKERS by the number of dominant files in the window. This preserves the existing 4-worker allocation when a single dominant file is present, while bounding aggregate dominant concurrency across the pool when multiple large bundles exist.
1 parent 9a2b4a0 commit 2afc7f5

1 file changed

Lines changed: 18 additions & 5 deletions

File tree

‎packages/angular/build/src/tools/i18n/i18n-inliner.ts‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,12 @@ const SMALL_FILE_FLOOR_BYTES = 100 * 1024;
6666

6767
/**
6868
* Ratio of the maximum file size in a window to consider a file "dominant".
69-
* Files within 70% of the largest file are sharded across all workers for maximum concurrency.
69+
* Files within 70% of the largest file are sharded across bounded workers for concurrency.
7070
*/
7171
const DOMINANT_FILE_RATIO = 0.7;
7272

7373
/**
74-
* Maximum number of worker threads to allocate concurrently for a single dominant file.
74+
* Maximum number of worker threads to allocate concurrently across dominant files.
7575
* Capping concurrency on dominant files (which hold large AST and sourcemap trees)
7676
* prevents V8 heap and RSS explosion on machines with high CPU core counts.
7777
*/
@@ -520,6 +520,20 @@ export class I18nInliner {
520520
// while small files act as gap fillers near the window barrier to prevent tail stragglers.
521521
sortedFiles.sort((a, b) => b.fileSize - a.fileSize);
522522

523+
const dominantThreshold = Math.max(SMALL_FILE_FLOOR_BYTES, maxFileSize * DOMINANT_FILE_RATIO);
524+
let dominantCount = 0;
525+
for (const { fileSize } of sortedFiles) {
526+
if (fileSize >= dominantThreshold) {
527+
dominantCount++;
528+
} else {
529+
break;
530+
}
531+
}
532+
const dominantTargetWorkers =
533+
dominantCount > 0
534+
? Math.max(1, Math.min(workerCount - 1, Math.floor(MAX_DOMINANT_WORKERS / dominantCount)))
535+
: 1;
536+
523537
const workerTasks: Promise<void>[] = [];
524538

525539
for (const { filename, entries, codeFile, fileSize } of sortedFiles) {
@@ -535,11 +549,10 @@ export class I18nInliner {
535549
} else if (fileSize < SMALL_FILE_FLOOR_BYTES) {
536550
// Small chunks (< 100 KB): process all locales in 1 batch to eliminate IPC overhead
537551
localesPerBatch = entries.length;
538-
} else if (fileSize >= maxFileSize * DOMINANT_FILE_RATIO) {
552+
} else if (fileSize >= dominantThreshold) {
539553
// Dominant file(s) with multiple files: shard across bounded workers while always reserving
540554
// at least one worker to process smaller chunks in parallel without starvation.
541-
const targetWorkers = Math.max(1, Math.min(workerCount - 1, MAX_DOMINANT_WORKERS));
542-
localesPerBatch = Math.max(1, Math.ceil(entries.length / targetWorkers));
555+
localesPerBatch = Math.max(1, Math.ceil(entries.length / dominantTargetWorkers));
543556
} else if (uncachedByFile.size >= workerCount) {
544557
// Worker pool is already saturated by file-level concurrency;
545558
// process all locales in a single batch to avoid duplicate parsing and IPC overhead.

0 commit comments

Comments
 (0)