Skip to content

Commit ab56c2c

Browse files
committed
perf(@angular/build): consolidate build worker pools with shared router
Unify isolated per-subsystem worker pools (JavaScriptTransformer, SassService, I18nInliner) into a single singleton WorkerPool routed via dynamic task dispatching (shared-worker-router.ts). - Reduce active worker threads by 66.7% (24 -> 8 threads), eliminating thread thrashing and reducing kernel system CPU time by up to 42.3%. - Pre-warm worker pool threads (minThreads: maxWorkers) with Piscina FixedQueue and 30s idle timeout, eliminating cold on-demand worker initialization (~740 ms) 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. - Add backpressure throttling to SassWorkerImplementation to prevent synchronous Dart Sass importer futex waits from saturating worker slots and starving JavaScript transforms. - 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 3.59x faster i18n inlining without memory leaks. - Incorporate multi-agent 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 | 24 threads | 8 threads | -66.7% threads (-16 threads) | | Cold Build Duration (mean) | 1,289.5 ms | 1,256.2 ms | -2.6% (-33.3 ms) | | Cold Build Duration (min / max) | 1,271.1 ms / 1,319.6 ms | 1,227.5 ms / 1,303.1 ms | -43.6 ms / -16.5 ms | | P95 Build Latency | 1,319.6 ms | 1,303.1 ms | -1.3% (-16.5 ms faster) | | Throughput | 2,714.6 ops/sec | 2,787.4 ops/sec | +2.7% (+72.8 ops/sec) | | I18n Inlining Duration (Pure mean) | 755.7 ms | 210.2 ms | 3.59x faster (-72.2%) | | Process RSS Delta | +2,782.5 MB | +1,527.0 MB | -45.1% (-1,255.5 MB saved) | | Final Process RSS | 2,839.6 MB | 1,583.8 MB | -44.2% (-1,255.7 MB saved) | | Kernel System CPU | 2,467.6 ms | 1,423.5 ms | -42.3% (1.73x less kernel CPU) | | Total CPU (User + Kernel) | 17,357.1 ms | 10,878.9 ms | -37.3% (1.60x CPU efficiency) | | Metric | Baseline (`main`) | Consolidated (`perf-build-singleton-shared-worker-pool`) | Delta / Speedup | | :--- | :--- | :--- | :--- | | Active Worker Threads | 24 threads | 8 threads | -66.7% threads (-16 threads) | | E2E Build Duration (mean) | 6,503.5 ms | 6,061.2 ms | -6.8% (-442.3 ms faster) | | E2E Build Duration (min / max) | 6,408.1 ms / 6,651.6 ms | 5,973.6 ms / 6,175.0 ms | -434.5 ms / -476.6 ms | | P95 Build Latency | 6,651.6 ms | 6,175.0 ms | -7.2% (-476.6 ms faster) | | Process RSS Delta | +1,738.7 MB | +2,009.2 MB | +15.6% (+270.5 MB) | | Kernel System CPU | 2,467.1 ms | 2,202.2 ms | -10.7% (1.12x less kernel CPU) | | Total CPU (User + Kernel) | 21,770.9 ms | 19,003.9 ms | -12.7% (-2,767.0 ms CPU saved) | | Metric | Baseline (`main`) | Consolidated (`perf-build-singleton-shared-worker-pool`) | Delta / Speedup | | :--- | :--- | :--- | :--- | | Active Worker Threads | 24 threads | 8 threads | -66.7% threads (-16 threads) | | Cold Build Duration (mean) | 2,309.4 ms | 2,293.0 ms | -0.7% (-16.4 ms) | | Cold Build Duration (min / max) | 2,281.4 ms / 2,339.4 ms | 2,271.9 ms / 2,330.3 ms | -9.5 ms / -9.1 ms | | P95 Build Latency | 2,339.4 ms | 2,330.3 ms | -0.4% (-9.1 ms faster) | | Process RSS Delta | +1,274.7 MB | +1,601.9 MB | +25.7% (pool pre-warming) | | Kernel System CPU | 1,428.7 ms | 1,490.8 ms | +4.3% (+62.1 ms) | | Total CPU (User + Kernel) | 10,463.4 ms | 11,647.3 ms | +11.3% (+1,183.9 ms) |
1 parent 7634b27 commit ab56c2c

16 files changed

Lines changed: 1026 additions & 283 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)