Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 18 additions & 19 deletions packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,26 +276,25 @@ export async function inlineFileBatch(
map = rawMap ? (JSON.parse(rawMap) as SourceMapInput) : undefined;
}

const results = await Promise.all(
Array.from(request.locales, async ([locale, translation]) => {
const result = await inlineLocalize(
code,
map,
metadata,
locale,
await loadTranslation(locale, translation),
request.filename,
request.missingTranslation,
);
const results = [];
for (const [locale, translation] of request.locales) {
const result = await inlineLocalize(
code,
map,
metadata,
locale,
await loadTranslation(locale, translation),
request.filename,
request.missingTranslation,
);

return {
locale,
code: result.code,
map: result.map,
messages: result.diagnostics,
};
}),
);
results.push({
locale,
code: result.code,
map: result.map,
messages: result.diagnostics,
});
}

return {
file: request.filename,
Expand Down
18 changes: 14 additions & 4 deletions packages/angular/build/src/tools/i18n/i18n-inliner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ const SMALL_FILE_FLOOR_BYTES = 100 * 1024;
*/
const DOMINANT_FILE_RATIO = 0.7;

/**
* Maximum number of worker threads to allocate concurrently for a single dominant file.
* Capping concurrency on dominant files (which hold large AST and sourcemap trees)
* prevents V8 heap and RSS explosion on machines with high CPU core counts.
*/
const MAX_DOMINANT_WORKERS = 4;

/**
* Serializes the translation messages for a locale for transfer to an inliner Worker.
*
Expand Down Expand Up @@ -522,14 +529,17 @@ export class I18nInliner {

let localesPerBatch: number;
if (uncachedByFile.size === 1) {
// Single file in window: shard across all workers to avoid idle threads
localesPerBatch = Math.max(1, Math.ceil(entries.length / workerCount));
// Single file in window: shard across bounded workers to balance throughput and peak memory
const targetWorkers = Math.min(workerCount, MAX_DOMINANT_WORKERS);
localesPerBatch = Math.max(1, Math.ceil(entries.length / targetWorkers));
} else if (fileSize < SMALL_FILE_FLOOR_BYTES) {
// Small chunks (< 100 KB): process all locales in 1 batch to eliminate IPC overhead
localesPerBatch = entries.length;
} else if (fileSize >= maxFileSize * DOMINANT_FILE_RATIO) {
// Dominant file(s): shard across all workers for maximum multi-core parallelism
localesPerBatch = Math.max(1, Math.ceil(entries.length / workerCount));
// Dominant file(s) with multiple files: shard across bounded workers while always reserving
// at least one worker to process smaller chunks in parallel without starvation.
const targetWorkers = Math.max(1, Math.min(workerCount - 1, MAX_DOMINANT_WORKERS));
localesPerBatch = Math.max(1, Math.ceil(entries.length / targetWorkers));
} else {
// Intermediate files: moderate sharding
localesPerBatch = Math.max(1, Math.ceil(entries.length / 2));
Expand Down
87 changes: 87 additions & 0 deletions scripts/benchmarks/i18n/scenarios.mts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,92 @@ export function createMonolithicScenario(options: ScenarioFactoryOptions = {}):
};
}

/**
* 5. Multi-Dominant Scenario:
* 3 dominant bundles (3 MB each, e.g. main.js, polyfills.js, admin-portal.js) + 15 small chunks (40 KB),
* 8 locales, sourcemaps ON.
* Tests contention and worker distribution when multiple large files qualify as dominant in the same window.
*/
export function createMultiDominantScenario(
options: ScenarioFactoryOptions = {},
): BenchmarkScenario {
let workload: GeneratedWorkload | undefined;

return {
name: 'multi-dominant',
description:
'Multi-Dominant: 3 large bundles (3 MB each) + 15 chunks (40 KB), 8 locales, sourcemaps ON',
get inputSizeBytes() {
return workload?.totalInputSizeBytes ?? 0;
},
get localeCount() {
return DEFAULT_LOCALES_8.length;
},
async setup() {
await initializeFixtures();
const files: BuildOutputFile[] = [];
const totalMessages = 3000;
const dominantCount = 3;
const dominantSize = 3 * 1024 * 1024;
const dominantMessages = 600; // 600 each x 3 = 1800 messages

for (let d = 0; d < dominantCount; d++) {
const { codeFile, mapFile } = generateSyntheticBundle({
filename: `dominant_${d}.js`,
targetByteSize: dominantSize,
messageCount: dominantMessages,
withSourceMap: true,
messageIdOffset: d * dominantMessages,
});
files.push(codeFile);
if (mapFile) {
files.push(mapFile);
}
}

const chunkCount = 15;
const chunkSizeBytes = 40 * 1024;
const remainingMessages = totalMessages - dominantCount * dominantMessages;
const messagesPerChunk = Math.max(1, Math.floor(remainingMessages / chunkCount));

for (let i = 0; i < chunkCount; i++) {
const { codeFile, mapFile } = generateSyntheticBundle({
filename: `chunk_${i}.js`,
targetByteSize: chunkSizeBytes,
messageCount: messagesPerChunk,
withSourceMap: true,
messageIdOffset: dominantCount * dominantMessages + i * messagesPerChunk,
});
files.push(codeFile);
if (mapFile) {
files.push(mapFile);
}
}

const locales = generateTranslations(DEFAULT_LOCALES_8, totalMessages);
workload = {
files,
locales,
totalInputSizeBytes: calculateInputSizeBytes(files),
};
},
async run() {
if (!workload) {
return;
}
const inliner = new I18nInliner({
missingTranslation: 'warning',
maxConcurrency: options.concurrency,
});
try {
await inliner.inlineAll(workload.files, workload.locales);
} finally {
await inliner.close();
}
},
};
}

/**
* Helper to prime the persistent cache in an isolated process.
*/
Expand Down Expand Up @@ -462,6 +548,7 @@ export function getAllScenarios(options: ScenarioFactoryOptions = {}): Benchmark
createEnterpriseScenario(options),
createLargeEnterpriseScenario(options),
createMonolithicScenario(options),
createMultiDominantScenario(options),
createPersistentCacheWarmScenario(options),
];
}
Expand Down
Loading