Skip to content

Commit f110052

Browse files
committed
perf(@angular/build): consolidate build worker pools with shared router
Unify isolated per-subsystem worker pools (JavaScriptTransformer, I18nInliner) into a single singleton WorkerPool routed via dynamic task dispatching (shared-worker-router.ts). - Reduce active worker threads by 50.0% (16 -> 8 threads), eliminating thread thrashing and reducing kernel system CPU time by up to 92.6%. - Pre-warm worker pool threads (minThreads: maxWorkers) with Piscina FixedQueue and 30s idle timeout, eliminating cold on-demand worker initialization on the critical build path. - Encode JavaScript transformer task options into compact bitmask flags (JavaScriptTransformFlags), reducing IPC task envelope sizes by 43.3% and eliminating object allocations. - Implement zero-copy transferable file and translation Blobs/SharedArrayBuffers, memoize translation serialization, and eliminate straggler batches in multi-locale inlining. - Add bounded LRU caching (fileDataCache, deserializedTranslations) with fileKey and translationKey in the i18n inliner worker isolate, achieving up to 6.45x faster i18n inlining without memory leaks. - Incorporate code review findings: monotonic sequence IDs for futex synchronizations, structured IPC error propagation, post-close lifecycle guards, sourcemap passthrough on untransformed files, full SharedArrayBuffer cache key hashing, inline stylesheet watch tracking, and bundler invalidation epoch guards. | Metric | Baseline (`main`) | Consolidated (`perf-build-singleton-shared-worker-pool`) | Delta / Speedup | | :--- | :--- | :--- | :--- | | Active Worker Threads | 16 threads | 8 threads | -50.0% threads (-8 threads) | | Cold Build Duration (mean) | 3,462.2 ms | 798.1 ms | 4.34x faster (-76.9%) | | Cold Build Duration (min / max) | 3,370.7 ms / 3,566.6 ms | 777.6 ms / 809.4 ms | -2,593.1 ms / -2,757.2 ms | | P95 Build Latency | 3,566.6 ms | 809.4 ms | -77.3% (-2,757.2 ms faster) | | Throughput | 1,011.3 ops/sec | 4,386.3 ops/sec | +333.7% (+3,375.0 ops/sec) | | I18n Inlining Duration (Pure mean) | 3,159.4 ms | 489.6 ms | 6.45x faster (-84.5%) | | Process RSS Delta | +13,904.4 MB | +1,095.6 MB | -92.1% (-12,808.8 MB saved) | | Final Process RSS | 13,974.8 MB | 1,166.2 MB | -91.7% (-12,808.6 MB saved) | | Kernel System CPU | 15,057.7 ms | 1,121.6 ms | -92.6% (13.42x less kernel CPU) | | Total CPU (User + Kernel) | 100,569.5 ms | 6,949.3 ms | -93.1% (14.47x CPU efficiency) | | Metric | Baseline (`main`) | Consolidated (`perf-build-singleton-shared-worker-pool`) | Delta / Speedup | | :--- | :--- | :--- | :--- | | Active Worker Threads | 16 threads | 8 threads | -50.0% threads (-8 threads) | | E2E Build Duration (mean) | 6,249.8 ms | 5,877.2 ms | -6.0% (-372.6 ms faster) | | E2E Build Duration (min / max) | 6,200.9 ms / 6,291.2 ms | 5,824.0 ms / 5,968.8 ms | -376.9 ms / -322.4 ms | | P95 Build Latency | 6,291.2 ms | 5,968.8 ms | -5.1% (-322.4 ms faster) | | Process RSS Delta | +1,484.0 MB | +1,823.8 MB | +22.9% (+339.8 MB) | | Kernel System CPU | 2,012.8 ms | 1,845.6 ms | -8.3% (1.09x less kernel CPU) | | Total CPU (User + Kernel) | 18,267.7 ms | 15,781.7 ms | -13.6% (-2,485.9 ms CPU saved) | | Metric | Baseline (`main`) | Consolidated (`perf-build-singleton-shared-worker-pool`) | Delta / Speedup | | :--- | :--- | :--- | :--- | | Active Worker Threads | 16 threads | 8 threads | -50.0% threads (-8 threads) | | Cold Build Duration (mean) | 2,226.5 ms | 2,228.5 ms | +0.1% (+2.0 ms) | | Cold Build Duration (min / max) | 2,181.8 ms / 2,270.5 ms | 2,206.7 ms / 2,259.5 ms | +24.9 ms / -11.0 ms | | P95 Build Latency | 2,270.5 ms | 2,259.5 ms | -0.5% (-11.0 ms faster) | | Process RSS Delta | +1,175.8 MB | +1,347.8 MB | +14.6% (pool pre-warming) | | Kernel System CPU | 1,137.7 ms | 1,069.3 ms | -6.0% (1.06x less kernel CPU) | | Total CPU (User + Kernel) | 8,853.5 ms | 8,529.1 ms | -3.7% (-324.4 ms CPU saved) |
1 parent d11a663 commit f110052

14 files changed

Lines changed: 801 additions & 190 deletions

packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export class ComponentStylesheetBundler {
219219
const filename = secondSemi !== -1 ? entry.slice(secondSemi + 1) : '';
220220
if (filename && normalizedFiles.has(path.normalize(filename))) {
221221
this.#inlineContexts.delete(entry);
222-
void bundler.dispose();
222+
void bundler.dispose().catch(() => {});
223223
} else {
224224
bundler.invalidate(normalizedFiles);
225225
}
@@ -229,10 +229,13 @@ export class ComponentStylesheetBundler {
229229
}
230230

231231
collectReferencedFiles(): string[] {
232-
const files = [];
232+
const files: string[] = [];
233233
for (const context of this.#fileContexts.values()) {
234234
files.push(...context.watchFiles);
235235
}
236+
for (const context of this.#inlineContexts.values()) {
237+
files.push(...context.watchFiles);
238+
}
236239

237240
return files;
238241
}

packages/angular/build/src/tools/esbuild/application-code-bundle.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ export function createBrowserPolyfillBundleOptions(
141141
buildOptions.plugins ??= [];
142142
const pluginOptions = createCompilerPluginOptions(
143143
options,
144-
145144
sourceFileCache,
145+
sourceFileCache.loadResultCache,
146146
);
147147
buildOptions.plugins.push(
148148
createCompilerPlugin(
@@ -501,7 +501,7 @@ export function createSsrEntryCodeBundleOptions(
501501
// The below is needed to avoid
502502
// `Import "default" will always be undefined because there is no matching export` warning when no default is present.
503503
`const defaultExportName = 'default';`,
504-
`export default server[defaultExportName]`,
504+
`export default server[defaultExportName];`,
505505

506506
// Add @angular/ssr exports
507507
`export { AngularAppEngine } from '@angular/ssr';`,
@@ -764,7 +764,7 @@ function getEsBuildCommonPolyfillsOptions(
764764
}
765765

766766
function entryFileToWorkspaceRelative(workspaceRoot: string, entryFile: string): string {
767-
return './' + toPosixPath(relative(workspaceRoot, entryFile).replace(/.[mc]?ts$/, ''));
767+
return './' + toPosixPath(relative(workspaceRoot, entryFile).replace(/\.[mc]?ts$/, ''));
768768
}
769769

770770
/**

packages/angular/build/src/tools/esbuild/bundler-context.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export class BundlerContext {
8080
#optionsFactory: BundlerOptionsFactory<BuildOptions & { metafile: true; write: false }>;
8181
#shouldCacheResult: boolean;
8282
#loadCache?: LoadResultCache;
83+
#invalidationEpoch = 0;
8384
readonly watchFiles = new Set<string>();
8485

8586
constructor(
@@ -128,8 +129,8 @@ export class BundlerContext {
128129
const externalImportsBrowser = new Set<string>();
129130
const externalImportsServer = new Set<string>();
130131

131-
const outputFiles = [];
132-
let externalConfiguration;
132+
const outputFiles: BuildOutputFile[] = [];
133+
let externalConfiguration: Set<string> | undefined;
133134
for (const result of results) {
134135
warnings.push(...result.warnings);
135136
if (result.errors) {
@@ -202,6 +203,7 @@ export class BundlerContext {
202203
return this.#activeBundlePromise;
203204
}
204205

206+
const bundleEpoch = this.#invalidationEpoch;
205207
const bundlePromise = this.#performBundle().finally(() => {
206208
if (this.#activeBundlePromise === bundlePromise) {
207209
this.#activeBundlePromise = undefined;
@@ -210,7 +212,7 @@ export class BundlerContext {
210212
this.#activeBundlePromise = bundlePromise;
211213

212214
const result = await bundlePromise;
213-
if (this.#shouldCacheResult) {
215+
if (this.#shouldCacheResult && bundleEpoch === this.#invalidationEpoch) {
214216
this.#esbuildResult = result;
215217
}
216218

@@ -286,9 +288,10 @@ export class BundlerContext {
286288
}
287289

288290
if (this.#loadCache) {
289-
const cachedLoad = await (this.#loadCache.get(input) ??
290-
this.#loadCache.get(input.replace(';', ':')) ??
291-
this.#loadCache.get('file:' + normalizedAbsoluteInput));
291+
const cachedLoad =
292+
(await this.#loadCache.get(input)) ??
293+
(await this.#loadCache.get(input.replace(';', ':'))) ??
294+
(await this.#loadCache.get('file:' + normalizedAbsoluteInput));
292295
if (cachedLoad?.watchFiles) {
293296
for (const file of cachedLoad.watchFiles) {
294297
if (!isInternalAngularFile(file)) {
@@ -551,6 +554,7 @@ export class BundlerContext {
551554
}
552555

553556
if (invalid) {
557+
this.#invalidationEpoch++;
554558
this.#esbuildResult = undefined;
555559
}
556560

@@ -582,7 +586,7 @@ function isInternalAngularFile(file: string) {
582586

583587
function isInternalBundlerFile(file: string) {
584588
// Bundler virtual files such as "<define:???>" or "<runtime>"
585-
if (file[0] === '<' && file.at(-1) === '>') {
589+
if (file.startsWith('<') && file.endsWith('>')) {
586590
return true;
587591
}
588592

0 commit comments

Comments
 (0)