Skip to content

Commit 1713a1e

Browse files
committed
refactor(@angular/build): support supplying worker pool to i18n inliner
The I18nInliner class previously created a dedicated WorkerPool instance on every construction and unconditionally destroyed it when closed. In watch mode and across build executions, this led to unnecessary worker thread lifecycle overhead and prevented reusing existing thread pools across post-bundle phases. This change updates the I18nInliner constructor to accept an optional workerPool argument directly as a second parameter. When provided, the supplied worker pool is used for all transformation task executions and is treated as an external borrowed dependency that is not destroyed when close is invoked. When omitted, the inliner continues to create and manage its own dedicated worker pool as before. The inlineI18n application builder helper is also updated to accept an optional workerPool parameter and forward it to the inliner constructor, enabling callers and future shared build orchestration to pass through a common worker pool.
1 parent 55583c4 commit 1713a1e

3 files changed

Lines changed: 102 additions & 12 deletions

File tree

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { maxWorkers } 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';
24+
import type { WorkerPool } from '../../utils/worker-pool';
2425
import { executePostBundleSteps } from './execute-post-bundle';
2526
import { NormalizedApplicationBuildOptions, getLocaleBaseHref } from './options';
2627

@@ -31,12 +32,14 @@ import { NormalizedApplicationBuildOptions, getLocaleBaseHref } from './options'
3132
* @param options The normalized application builder options used to create the build.
3233
* @param executionResult The result of an executed build.
3334
* @param initialFiles A map containing initial file information for the executed build.
35+
* @param workerPool An optional worker pool to use for running transformation tasks.
3436
*/
3537
export async function inlineI18n(
3638
metafile: Metafile,
3739
options: NormalizedApplicationBuildOptions,
3840
executionResult: ExecutionResult,
3941
initialFiles: Map<string, InitialFileRecord>,
42+
workerPool?: WorkerPool,
4043
): Promise<{
4144
errors: string[];
4245
warnings: string[];
@@ -45,12 +48,15 @@ export async function inlineI18n(
4548
const { i18nOptions, baseHref, cacheOptions } = options;
4649

4750
// Create the multi-threaded inliner with common options.
48-
const inliner = new I18nInliner({
49-
missingTranslation: i18nOptions.missingTranslationBehavior ?? 'warning',
50-
maxConcurrency: maxWorkers,
51-
persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined,
52-
localizeVersion: i18nOptions.localizeVersion,
53-
});
51+
const inliner = new I18nInliner(
52+
{
53+
missingTranslation: i18nOptions.missingTranslationBehavior ?? 'warning',
54+
maxConcurrency: maxWorkers,
55+
persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined,
56+
localizeVersion: i18nOptions.localizeVersion,
57+
},
58+
workerPool,
59+
);
5460

5561
const inlineResult: {
5662
errors: string[];

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ interface UncachedLocaleEntry {
187187
*/
188188
export class I18nInliner {
189189
#cacheInitFailed = false;
190-
#workerPool: WorkerPool;
190+
readonly #workerPool: WorkerPool;
191+
readonly #ownsWorkerPool: boolean;
191192
#cacheStore: PersistentCacheStore | undefined;
192193
#transformedFileCache: Cache<TransformedFileResult> | undefined;
193194
#translationCache: Cache<Uint8Array> | undefined;
@@ -197,20 +198,26 @@ export class I18nInliner {
197198
return this.options.maxConcurrency ?? (this.#workerPool.maxThreads || 1);
198199
}
199200

200-
constructor(private readonly options: I18nInlinerOptions) {
201+
constructor(
202+
private readonly options: I18nInlinerOptions,
203+
workerPool?: WorkerPool,
204+
) {
201205
if (
202206
options.maxConcurrency !== undefined &&
203207
(!Number.isInteger(options.maxConcurrency) || options.maxConcurrency < 1)
204208
) {
205209
throw new RangeError('options.maxConcurrency must be an integer greater than or equal to 1.');
206210
}
207211

212+
this.#ownsWorkerPool = !workerPool;
208213
// Piscina uses object spread against default options internally. Only define
209214
// maxThreads when specified to avoid overwriting Piscina's default thread count
210215
// with undefined.
211-
this.#workerPool = new WorkerPool({
212-
...(options.maxConcurrency !== undefined && { maxThreads: options.maxConcurrency }),
213-
});
216+
this.#workerPool =
217+
workerPool ??
218+
new WorkerPool({
219+
...(options.maxConcurrency !== undefined && { maxThreads: options.maxConcurrency }),
220+
});
214221
}
215222

216223
#partitionFiles(files: Iterable<BuildOutputFile>): {
@@ -662,7 +669,10 @@ export class I18nInliner {
662669
* @returns A void promise that resolves when closing is complete.
663670
*/
664671
async close(): Promise<void> {
665-
await Promise.allSettled([this.#cacheStore?.close(), this.#workerPool.destroy()]);
672+
await Promise.allSettled([
673+
this.#cacheStore?.close(),
674+
this.#ownsWorkerPool ? this.#workerPool.destroy() : undefined,
675+
]);
666676
}
667677

668678
/**

packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,4 +1092,78 @@ describe('I18nInliner', () => {
10921092
expect(maxThreadsSpy.calls.mostRecent().returnValue).toBeGreaterThanOrEqual(1);
10931093
});
10941094
});
1095+
1096+
describe('supplied workerPool', () => {
1097+
let externalPool: WorkerPool | undefined;
1098+
1099+
afterEach(async () => {
1100+
await externalPool?.destroy();
1101+
externalPool = undefined;
1102+
});
1103+
1104+
it('uses the supplied workerPool to execute transformation tasks', async () => {
1105+
externalPool = new WorkerPool({ maxThreads: 1 });
1106+
const runSpy = spyOn(externalPool, 'run').and.callThrough();
1107+
1108+
inliner = new I18nInliner({ missingTranslation: 'warning' }, externalPool);
1109+
1110+
const files = [browserFile('main.js', GREETING_SOURCE)];
1111+
const results = await inliner.inlineAll(files, [
1112+
{
1113+
locale: 'fr',
1114+
translation: { greeting: translationFor('Bonjour') },
1115+
},
1116+
]);
1117+
1118+
expect(results.size).toBe(1);
1119+
expect(findFile(results.get('fr')?.outputFiles ?? [], 'main.js').text).toContain('"Bonjour"');
1120+
expect(runSpy).toHaveBeenCalled();
1121+
});
1122+
1123+
it('does not destroy the supplied workerPool on close()', async () => {
1124+
externalPool = new WorkerPool({ maxThreads: 1 });
1125+
const destroySpy = spyOn(externalPool, 'destroy').and.callThrough();
1126+
1127+
inliner = new I18nInliner({ missingTranslation: 'warning' }, externalPool);
1128+
await inliner.close();
1129+
1130+
expect(destroySpy).not.toHaveBeenCalled();
1131+
});
1132+
1133+
it('destroys internally created workerPool on close()', async () => {
1134+
const destroySpy = spyOn(WorkerPool.prototype, 'destroy').and.callThrough();
1135+
1136+
inliner = new I18nInliner({ missingTranslation: 'warning', maxConcurrency: 1 });
1137+
await inliner.close();
1138+
1139+
expect(destroySpy).toHaveBeenCalled();
1140+
});
1141+
1142+
it('supports multiple sequential inlining passes with the same supplied workerPool', async () => {
1143+
externalPool = new WorkerPool({ maxThreads: 1 });
1144+
1145+
inliner = new I18nInliner({ missingTranslation: 'warning' }, externalPool);
1146+
1147+
const files1 = [browserFile('main.js', GREETING_SOURCE)];
1148+
const results1 = await inliner.inlineAll(files1, [
1149+
{
1150+
locale: 'fr',
1151+
translation: { greeting: translationFor('Bonjour') },
1152+
},
1153+
]);
1154+
expect(findFile(results1.get('fr')?.outputFiles ?? [], 'main.js').text).toContain(
1155+
'"Bonjour"',
1156+
);
1157+
1158+
// Second inlining pass on the same pool
1159+
const files2 = [browserFile('main.js', GREETING_SOURCE)];
1160+
const results2 = await inliner.inlineAll(files2, [
1161+
{
1162+
locale: 'es',
1163+
translation: { greeting: translationFor('Hola') },
1164+
},
1165+
]);
1166+
expect(findFile(results2.get('es')?.outputFiles ?? [], 'main.js').text).toContain('"Hola"');
1167+
});
1168+
});
10951169
});

0 commit comments

Comments
 (0)