Skip to content

Commit e36e6c8

Browse files
gkalpakmatsko
authored andcommitted
perf(ngcc): process tasks in parallel in async mode (#32427)
`ngcc` supports both synchronous and asynchronous execution. The default mode when using `ngcc` programmatically (which is how `@angular/cli` is using it) is synchronous. When running `ngcc` from the command line (i.e. via the `ivy-ngcc` script), it runs in async mode. Previously, the work would be executed in the same way in both modes. This commit improves the performance of `ngcc` in async mode by processing tasks in parallel on multiple processes. It uses the Node.js built-in [`cluster` module](https://nodejs.org/api/cluster.html) to launch a cluster of Node.js processes and take advantage of multi-core systems. Preliminary comparisons indicate a 1.8x to 2.6x speed improvement when processing the angular.io app (apparently depending on the OS, number of available cores, system load, etc.). Further investigation is needed to better understand these numbers and identify potential areas of improvement. Inspired by/Based on @alxhub's prototype: alxhub/angular@cb631bdb1 Original design doc: https://hackmd.io/uYG9CJrFQZ-6FtKqpnYJAA?view Jira issue: [FW-1460](https://angular-team.atlassian.net/browse/FW-1460) PR Close #32427
1 parent f4e4bb2 commit e36e6c8

File tree

17 files changed

+1174
-19
lines changed

17 files changed

+1174
-19
lines changed

integration/ngcc/test.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ grep "FocusMonitor.decorators =" node_modules/@angular/cdk/bundles/cdk-a11y.umd.
8282
ivy-ngcc -l debug | grep 'Skipping'
8383
if [[ $? != 0 ]]; then exit 1; fi
8484

85+
# Does it process the tasks in parallel?
86+
ivy-ngcc -l debug | grep 'Running ngcc on ClusterExecutor'
87+
if [[ $? != 0 ]]; then exit 1; fi
88+
8589
# Check that running it with logging level error outputs nothing
8690
ivy-ngcc -l error | grep '.' && exit 1
8791

packages/compiler-cli/ngcc/src/dependencies/dependency_resolver.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,17 @@ export interface DependencyDiagnostics {
6363
export type PartiallyOrderedEntryPoints = PartiallyOrderedList<EntryPoint>;
6464

6565
/**
66-
* A list of entry-points, sorted by their dependencies.
66+
* A list of entry-points, sorted by their dependencies, and the dependency graph.
6767
*
6868
* The `entryPoints` array will be ordered so that no entry point depends upon an entry point that
6969
* appears later in the array.
7070
*
71-
* Some entry points or their dependencies may be have been ignored. These are captured for
71+
* Some entry points or their dependencies may have been ignored. These are captured for
7272
* diagnostic purposes in `invalidEntryPoints` and `ignoredDependencies` respectively.
7373
*/
7474
export interface SortedEntryPointsInfo extends DependencyDiagnostics {
7575
entryPoints: PartiallyOrderedEntryPoints;
76+
graph: DepGraph<EntryPoint>;
7677
}
7778

7879
/**
@@ -109,6 +110,7 @@ export class DependencyResolver {
109110
return {
110111
entryPoints: (sortedEntryPointNodes as PartiallyOrderedList<string>)
111112
.map(path => graph.getNodeData(path)),
113+
graph,
112114
invalidEntryPoints,
113115
ignoredDependencies,
114116
};

packages/compiler-cli/ngcc/src/execution/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point';
9+
import {EntryPoint, EntryPointJsonProperty, JsonObject} from '../packages/entry_point';
1010
import {PartiallyOrderedList} from '../utils';
1111

1212

@@ -49,7 +49,7 @@ export interface Executor {
4949
export type PartiallyOrderedTasks = PartiallyOrderedList<Task>;
5050

5151
/** Represents a unit of work: processing a specific format property of an entry-point. */
52-
export interface Task {
52+
export interface Task extends JsonObject {
5353
/** The `EntryPoint` which needs to be processed as part of the task. */
5454
entryPoint: EntryPoint;
5555

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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.io/license
7+
*/
8+
9+
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
10+
import {JsonObject} from '../../packages/entry_point';
11+
import {PackageJsonChange} from '../../writing/package_json_updater';
12+
import {Task, TaskProcessingOutcome} from '../api';
13+
14+
15+
/** A message reporting that an unrecoverable error occurred. */
16+
export interface ErrorMessage extends JsonObject {
17+
type: 'error';
18+
error: string;
19+
}
20+
21+
/** A message requesting the processing of a task. */
22+
export interface ProcessTaskMessage extends JsonObject {
23+
type: 'process-task';
24+
task: Task;
25+
}
26+
27+
/**
28+
* A message reporting the result of processing the currently assigned task.
29+
*
30+
* NOTE: To avoid the communication overhead, the task is not included in the message. Instead, the
31+
* master is responsible for keeping a mapping of workers to their currently assigned tasks.
32+
*/
33+
export interface TaskCompletedMessage extends JsonObject {
34+
type: 'task-completed';
35+
outcome: TaskProcessingOutcome;
36+
}
37+
38+
/** A message requesting the update of a `package.json` file. */
39+
export interface UpdatePackageJsonMessage extends JsonObject {
40+
type: 'update-package-json';
41+
packageJsonPath: AbsoluteFsPath;
42+
changes: PackageJsonChange[];
43+
}
44+
45+
/** The type of messages sent from cluster workers to the cluster master. */
46+
export type MessageFromWorker = ErrorMessage | TaskCompletedMessage | UpdatePackageJsonMessage;
47+
48+
/** The type of messages sent from the cluster master to cluster workers. */
49+
export type MessageToWorker = ProcessTaskMessage;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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.io/license
7+
*/
8+
9+
/// <reference types="node" />
10+
11+
import * as cluster from 'cluster';
12+
13+
import {Logger} from '../../logging/logger';
14+
import {PackageJsonUpdater} from '../../writing/package_json_updater';
15+
import {AnalyzeEntryPointsFn, CreateCompileFn, Executor} from '../api';
16+
17+
import {ClusterMaster} from './master';
18+
import {ClusterWorker} from './worker';
19+
20+
21+
/**
22+
* An `Executor` that processes tasks in parallel (on multiple processes) and completes
23+
* asynchronously.
24+
*/
25+
export class ClusterExecutor implements Executor {
26+
constructor(
27+
private workerCount: number, private logger: Logger,
28+
private pkgJsonUpdater: PackageJsonUpdater) {}
29+
30+
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn):
31+
Promise<void> {
32+
if (cluster.isMaster) {
33+
this.logger.debug(
34+
`Running ngcc on ${this.constructor.name} (using ${this.workerCount} worker processes).`);
35+
36+
// This process is the cluster master.
37+
const master =
38+
new ClusterMaster(this.workerCount, this.logger, this.pkgJsonUpdater, analyzeEntryPoints);
39+
return master.run();
40+
} else {
41+
// This process is a cluster worker.
42+
const worker = new ClusterWorker(createCompileFn);
43+
return worker.run();
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)