Skip to content

Commit 0e2b033

Browse files
committed
perf(@angular/build): avoid web worker channel overhead in library parallel compilation
Skip creating and transferring `webWorkerChannel` (`MessageChannel`) and `webWorkerSignal` (`SharedArrayBuffer`) in `ParallelCompilation.initialize()` when `buildType === 'library'`, as library builds do not bundle web workers via `processWebWorker` and previously left ports open until the next build if `emitAffectedFiles()` was skipped due to stylesheet errors. Also replace `crypto.randomUUID()` per component stylesheet request in `parallel-worker.ts` with an incrementing numeric request counter.
1 parent 0fd4404 commit 0e2b033

3 files changed

Lines changed: 96 additions & 24 deletions

File tree

‎packages/angular/build/src/tools/angular/compilation/parallel-compilation.ts‎

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import type { PartialMessage } from 'esbuild';
1010
import { createRequire } from 'node:module';
11-
import { MessageChannel } from 'node:worker_threads';
11+
import { MessageChannel, type MessagePort } from 'node:worker_threads';
1212
import { mergeCumulativeDurations } from '../../../utils/profiling';
1313
import { WorkerPool } from '../../../utils/worker-pool';
1414
import type { AngularHostOptions } from '../angular-host';
@@ -67,23 +67,30 @@ export class ParallelCompilation extends AngularCompilation {
6767
);
6868

6969
this.#webWorkerChannel?.port1.close();
70+
this.#webWorkerChannel = undefined;
7071

71-
// The web worker processing is a synchronous operation and uses shared memory combined with
72-
// the Atomics API to block execution here until a response is received.
73-
const webWorkerChannel = new MessageChannel();
74-
this.#webWorkerChannel = webWorkerChannel;
75-
const webWorkerSignal = new Int32Array(new SharedArrayBuffer(4));
76-
webWorkerChannel.port1.on('message', ({ workerFile, containingFile }) => {
77-
try {
78-
const workerCodeFile = hostOptions.processWebWorker(workerFile, containingFile);
79-
webWorkerChannel.port1.postMessage({ workerCodeFile });
80-
} catch (error) {
81-
webWorkerChannel.port1.postMessage({ error });
82-
} finally {
83-
Atomics.store(webWorkerSignal, 0, 1);
84-
Atomics.notify(webWorkerSignal, 0);
85-
}
86-
});
72+
let webWorkerPort: MessagePort | undefined;
73+
let webWorkerSignal: Int32Array | undefined;
74+
if (buildType !== 'library') {
75+
// The web worker processing is a synchronous operation and uses shared memory combined with
76+
// the Atomics API to block execution here until a response is received.
77+
const webWorkerChannel = new MessageChannel();
78+
this.#webWorkerChannel = webWorkerChannel;
79+
const signal = new Int32Array(new SharedArrayBuffer(4));
80+
webWorkerPort = webWorkerChannel.port2;
81+
webWorkerSignal = signal;
82+
webWorkerChannel.port1.on('message', ({ workerFile, containingFile }) => {
83+
try {
84+
const workerCodeFile = hostOptions.processWebWorker(workerFile, containingFile);
85+
webWorkerChannel.port1.postMessage({ workerCodeFile });
86+
} catch (error) {
87+
webWorkerChannel.port1.postMessage({ error });
88+
} finally {
89+
Atomics.store(signal, 0, 1);
90+
Atomics.notify(signal, 0);
91+
}
92+
});
93+
}
8794

8895
let success = false;
8996
try {
@@ -97,12 +104,14 @@ export class ParallelCompilation extends AngularCompilation {
97104
compilerOptionOverrides,
98105
buildType,
99106
stylesheetPort: stylesheetChannel.port2,
100-
webWorkerPort: webWorkerChannel.port2,
107+
webWorkerPort,
101108
webWorkerSignal,
102109
},
103110
{
104111
name: 'initialize',
105-
transferList: [stylesheetChannel.port2, webWorkerChannel.port2],
112+
transferList: webWorkerPort
113+
? [stylesheetChannel.port2, webWorkerPort]
114+
: [stylesheetChannel.port2],
106115
},
107116
);
108117
success = true;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { MessageChannel } from 'node:worker_threads';
10+
import { AotCompilation } from './aot-compilation';
11+
import { initialize } from './parallel-worker';
12+
13+
describe('parallel-worker', () => {
14+
it('uses incrementing numeric request IDs for transformStylesheet and handles omitted webWorkerPort in library builds', async () => {
15+
const receivedRequestIds: unknown[] = [];
16+
let workerResult: string | undefined;
17+
18+
spyOn(AotCompilation.prototype, 'initialize').and.callFake(async (_tsconfig, hostOptions) => {
19+
const [style1, style2] = await Promise.all([
20+
hostOptions.transformStylesheet('body {}', '/src/a.ts', '/src/a.css', 0, 'CompA'),
21+
hostOptions.transformStylesheet('div {}', '/src/b.ts', '/src/b.css', 1, 'CompB'),
22+
]);
23+
expect(style1).toBe('transformed:body {}');
24+
expect(style2).toBe('transformed:div {}');
25+
26+
workerResult = hostOptions.processWebWorker('./my.worker', '/src/a.ts');
27+
28+
return {
29+
compilerOptions: {},
30+
referencedFiles: ['/src/a.ts'],
31+
};
32+
});
33+
34+
const stylesheetChannel = new MessageChannel();
35+
stylesheetChannel.port1.on('message', ({ requestId, data }) => {
36+
receivedRequestIds.push(requestId);
37+
stylesheetChannel.port1.postMessage({
38+
requestId,
39+
value: `transformed:${data}`,
40+
});
41+
});
42+
43+
try {
44+
const result = await initialize({
45+
jit: false,
46+
browserOnlyBuild: false,
47+
tsconfig: '/src/tsconfig.lib.json',
48+
buildType: 'library',
49+
stylesheetPort: stylesheetChannel.port2,
50+
});
51+
52+
expect(receivedRequestIds).toEqual([1, 2]);
53+
expect(workerResult).toBe('./my.worker');
54+
expect(result.referencedFiles).toEqual(['/src/a.ts']);
55+
} finally {
56+
stylesheetChannel.port1.close();
57+
}
58+
});
59+
});

‎packages/angular/build/src/tools/angular/compilation/parallel-worker.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import type { PartialMessage } from 'esbuild';
1010
import assert from 'node:assert';
11-
import { randomUUID } from 'node:crypto';
1211
import { type MessagePort, receiveMessageOnPort } from 'node:worker_threads';
1312
import { initializeHash } from '../../../utils/hash';
1413
import { getAndClearCumulativeDurations } from '../../../utils/profiling';
@@ -29,8 +28,8 @@ export interface InitRequest {
2928
compilerOptionOverrides?: CompilerOptionOverrides;
3029
buildType?: 'application' | 'library';
3130
stylesheetPort: MessagePort;
32-
webWorkerPort: MessagePort;
33-
webWorkerSignal: Int32Array;
31+
webWorkerPort?: MessagePort;
32+
webWorkerSignal?: Int32Array;
3433
}
3534

3635
let compilation: AngularCompilation | undefined;
@@ -52,8 +51,9 @@ export async function initialize(request: InitRequest): Promise<AngularCompilati
5251
? new JitCompilation(request.browserOnlyBuild)
5352
: new AotCompilation(request.browserOnlyBuild);
5453

54+
let nextRequestId = 0;
5555
const stylesheetRequests = new Map<
56-
string,
56+
number,
5757
[(value: string) => void, (reason: Error) => void]
5858
>();
5959
request.stylesheetPort.on('message', ({ requestId, value, error }) => {
@@ -81,7 +81,7 @@ export async function initialize(request: InitRequest): Promise<AngularCompilati
8181
fileReplacements: request.fileReplacements,
8282
modifiedFiles: currentModifiedFiles,
8383
transformStylesheet(data, containingFile, stylesheetFile, order, className) {
84-
const requestId = randomUUID();
84+
const requestId = ++nextRequestId;
8585
const resultPromise = new Promise<string>((resolve, reject) =>
8686
stylesheetRequests.set(requestId, [resolve, reject]),
8787
);
@@ -98,6 +98,10 @@ export async function initialize(request: InitRequest): Promise<AngularCompilati
9898
return resultPromise;
9999
},
100100
processWebWorker(workerFile, containingFile) {
101+
if (!request.webWorkerPort || !request.webWorkerSignal) {
102+
return workerFile;
103+
}
104+
101105
Atomics.store(request.webWorkerSignal, 0, 0);
102106
request.webWorkerPort.postMessage({ workerFile, containingFile });
103107

0 commit comments

Comments
 (0)