Skip to content

Commit bcdcc09

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. (cherry picked from commit 7f4de14)
1 parent 5ae6a66 commit bcdcc09

3 files changed

Lines changed: 93 additions & 23 deletions

File tree

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

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,23 +66,28 @@ export class ParallelCompilation extends AngularCompilation {
6666
);
6767

6868
this.#webWorkerChannel?.port1.close();
69+
this.#webWorkerChannel = undefined;
6970

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

8792
let success = false;
8893
try {
@@ -95,12 +100,14 @@ export class ParallelCompilation extends AngularCompilation {
95100
browserOnlyBuild: this.browserOnlyBuild,
96101
compilerOptionOverrides,
97102
stylesheetPort: stylesheetChannel.port2,
98-
webWorkerPort: webWorkerChannel.port2,
103+
webWorkerPort: this.#webWorkerChannel?.port2,
99104
webWorkerSignal,
100105
},
101106
{
102107
name: 'initialize',
103-
transferList: [stylesheetChannel.port2, webWorkerChannel.port2],
108+
transferList: this.#webWorkerChannel
109+
? [stylesheetChannel.port2, this.#webWorkerChannel.port2]
110+
: [stylesheetChannel.port2],
104111
},
105112
);
106113
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';
@@ -28,8 +27,8 @@ export interface InitRequest {
2827
fileReplacements?: Record<string, string>;
2928
compilerOptionOverrides?: CompilerOptionOverrides;
3029
stylesheetPort: MessagePort;
31-
webWorkerPort: MessagePort;
32-
webWorkerSignal: Int32Array;
30+
webWorkerPort?: MessagePort;
31+
webWorkerSignal?: Int32Array;
3332
}
3433

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

53+
let nextRequestId = 0;
5454
const stylesheetRequests = new Map<
55-
string,
55+
number,
5656
[(value: string) => void, (reason: Error) => void]
5757
>();
5858
request.stylesheetPort.on('message', ({ requestId, value, error }) => {
@@ -80,7 +80,7 @@ export async function initialize(request: InitRequest): Promise<AngularCompilati
8080
fileReplacements: request.fileReplacements,
8181
modifiedFiles: currentModifiedFiles,
8282
transformStylesheet(data, containingFile, stylesheetFile, order, className) {
83-
const requestId = randomUUID();
83+
const requestId = ++nextRequestId;
8484
const resultPromise = new Promise<string>((resolve, reject) =>
8585
stylesheetRequests.set(requestId, [resolve, reject]),
8686
);
@@ -97,6 +97,10 @@ export async function initialize(request: InitRequest): Promise<AngularCompilati
9797
return resultPromise;
9898
},
9999
processWebWorker(workerFile, containingFile) {
100+
if (!request.webWorkerPort || !request.webWorkerSignal) {
101+
return workerFile;
102+
}
103+
100104
Atomics.store(request.webWorkerSignal, 0, 0);
101105
request.webWorkerPort.postMessage({ workerFile, containingFile });
102106

0 commit comments

Comments
 (0)