@@ -23,11 +23,23 @@ import { encodeTranslationToBuffer } from './i18n-translation-encoder';
2323const LOCALIZE_KEYWORD = '$localize' ;
2424
2525/**
26- * The maximum number of locales to process concurrently in a single sliding window.
27- * This caps peak worker memory while maintaining multi-locale batching throughput.
26+ * The baseline number of locales to process concurrently in a single sliding window.
27+ * This caps peak worker memory on low-core machines while maintaining multi-locale batching throughput.
2828 */
2929const DEFAULT_LOCALE_WINDOW_SIZE = 8 ;
3030
31+ /**
32+ * Minimum byte size threshold for a file to be eligible for multi-batch sharding.
33+ * Files below this threshold (< 100 KB) are processed in a single batch to minimize IPC overhead.
34+ */
35+ const SMALL_FILE_FLOOR_BYTES = 100 * 1024 ;
36+
37+ /**
38+ * Ratio of the maximum file size in a window to consider a file "dominant".
39+ * Files within 70% of the largest file are sharded across all workers for maximum concurrency.
40+ */
41+ const DOMINANT_FILE_RATIO = 0.7 ;
42+
3143/**
3244 * Serializes the translation messages for a locale for transfer to an inliner Worker.
3345 *
@@ -271,11 +283,13 @@ export class I18nInliner {
271283 ( name ) => ! name . endsWith ( '.map' ) ,
272284 ) ;
273285
274- // Process locales in sliding windows to cap peak worker memory
275- for ( let i = 0 ; i < localeList . length ; i += DEFAULT_LOCALE_WINDOW_SIZE ) {
276- const windowLocales = localeList . slice ( i , i + DEFAULT_LOCALE_WINDOW_SIZE ) ;
286+ // Process locales in sliding windows to cap peak worker memory.
287+ // Ensure the window has at least enough locales to saturate all available workers on high-core machines.
288+ const windowSize = Math . max ( DEFAULT_LOCALE_WINDOW_SIZE , this . #workerPool. maxThreads || 1 ) ;
289+ for ( let i = 0 ; i < localeList . length ; i += windowSize ) {
290+ const windowLocales = localeList . slice ( i , i + windowSize ) ;
277291 const activeLocales = windowLocales . map ( ( item ) => item . locale ) ;
278- const isLastWindow = i + DEFAULT_LOCALE_WINDOW_SIZE >= localeList . length ;
292+ const isLastWindow = i + windowSize >= localeList . length ;
279293
280294 // Pre-calculate cache key bases and serialized Blobs for each locale in this window
281295 const localeCacheBases = new Map < string , string > ( ) ;
@@ -375,7 +389,6 @@ export class I18nInliner {
375389 if ( uncachedByFile . size > 0 ) {
376390 await this . #processUncachedBatches(
377391 uncachedByFile ,
378- windowLocales . length ,
379392 fileResultsByLocale ,
380393 activeLocales ,
381394 isLastWindow ,
@@ -441,24 +454,53 @@ export class I18nInliner {
441454
442455 async #processUncachedBatches(
443456 uncachedByFile : Map < string , UncachedLocaleEntry [ ] > ,
444- localeCount : number ,
445457 fileResultsByLocale : Map < string , Map < string , TransformedFileResult > > ,
446458 activeLocales ?: string [ ] ,
447459 isLastWindow = true ,
448460 ) : Promise < void > {
449461 const workerCount = this . #workerPool. maxThreads || 1 ;
450- const targetTaskCount = Math . max ( uncachedByFile . size , workerCount * 2 ) ;
451- const localesPerBatch = Math . max (
452- 1 ,
453- Math . ceil ( localeCount / ( targetTaskCount / ( uncachedByFile . size || 1 ) ) ) ,
454- ) ;
462+
463+ // Identify the heaviest file size in the current window to enable relative sizing heuristics
464+ let maxFileSize = 0 ;
465+ for ( const filename of uncachedByFile . keys ( ) ) {
466+ const size = this . #localizeFiles. get ( filename ) ?. contents . byteLength ?? 0 ;
467+ if ( size > maxFileSize ) {
468+ maxFileSize = size ;
469+ }
470+ }
471+
472+ // Sort files descending by byte size (Longest Processing Time First / LPT).
473+ // Heavy files (e.g. main.js) are queued first to saturate all worker threads immediately,
474+ // while small files act as gap fillers near the window barrier to prevent tail stragglers.
475+ const sortedFiles = Array . from ( uncachedByFile . entries ( ) ) . sort ( ( [ fileA ] , [ fileB ] ) => {
476+ const sizeA = this . #localizeFiles. get ( fileA ) ?. contents . byteLength ?? 0 ;
477+ const sizeB = this . #localizeFiles. get ( fileB ) ?. contents . byteLength ?? 0 ;
478+
479+ return sizeB - sizeA ;
480+ } ) ;
455481
456482 const workerTasks : Promise < void > [ ] = [ ] ;
457483
458- for ( const [ filename , entries ] of uncachedByFile ) {
484+ for ( const [ filename , entries ] of sortedFiles ) {
459485 const codeFile = this . #localizeFiles. get ( filename ) ;
460486 assert ( codeFile !== undefined , 'Localize file must exist: ' + filename ) ;
461487 const mapFile = this . #localizeFiles. get ( filename + '.map' ) ;
488+ const fileSize = codeFile . contents . byteLength ;
489+
490+ let localesPerBatch : number ;
491+ if ( uncachedByFile . size === 1 ) {
492+ // Single file in window: shard across all workers to avoid idle threads
493+ localesPerBatch = Math . max ( 1 , Math . ceil ( entries . length / workerCount ) ) ;
494+ } else if ( fileSize < SMALL_FILE_FLOOR_BYTES ) {
495+ // Small chunks (< 100 KB): process all locales in 1 batch to eliminate IPC overhead
496+ localesPerBatch = entries . length ;
497+ } else if ( fileSize >= maxFileSize * DOMINANT_FILE_RATIO ) {
498+ // Dominant file(s): shard across all workers for maximum multi-core parallelism
499+ localesPerBatch = Math . max ( 1 , Math . ceil ( entries . length / workerCount ) ) ;
500+ } else {
501+ // Intermediate files: moderate sharding
502+ localesPerBatch = Math . max ( 1 , Math . ceil ( entries . length / 2 ) ) ;
503+ }
462504
463505 const ephemeral = isLastWindow && entries . length <= localesPerBatch ;
464506 for ( let i = 0 ; i < entries . length ; i += localesPerBatch ) {
0 commit comments