From b296e2bd59c57a1c6affe5470c41f7ad02677534 Mon Sep 17 00:00:00 2001 From: Jeremy Elbourn Date: Fri, 15 Sep 2017 13:51:08 -0700 Subject: [PATCH] build: compile entry-points in parallel (#7094) --- tools/package-tools/build-package.ts | 53 ++++++++++++++----- tools/package-tools/secondary-entry-points.ts | 38 ++++++++++--- 2 files changed, 72 insertions(+), 19 deletions(-) diff --git a/tools/package-tools/build-package.ts b/tools/package-tools/build-package.ts index 616d8b0e68e..2ab4f5e94be 100644 --- a/tools/package-tools/build-package.ts +++ b/tools/package-tools/build-package.ts @@ -1,10 +1,11 @@ -import {main as ngc} from '@angular/tsc-wrapped'; +import {red} from 'chalk'; +import {spawn} from 'child_process'; import {readFileSync, writeFileSync} from 'fs'; import {sync as glob} from 'glob'; import {join} from 'path'; +import {PackageBundler} from './build-bundles'; import {buildConfig} from './build-config'; import {getSecondaryEntryPointsForPackage} from './secondary-entry-points'; -import {PackageBundler} from './build-bundles'; const {packagesDir, outputDir} = buildConfig; @@ -37,19 +38,23 @@ export class BuildPackage { /** Path to the tsconfig file, which will be used to build the tests. */ private readonly tsconfigTests: string; + private _secondaryEntryPoints: string[]; + private _secondaryEntryPointsByDepth: string[][]; + + /** Secondary entry-points partitioned by their build depth. */ + private get secondaryEntryPointsByDepth(): string[][] { + this.cacheSecondaryEntryPoints(); + return this._secondaryEntryPointsByDepth; + } + private readonly bundler = new PackageBundler(this); /** Secondary entry points for the package. */ get secondaryEntryPoints(): string[] { - if (!this._secondaryEntryPoints) { - this._secondaryEntryPoints = getSecondaryEntryPointsForPackage(this); - } - + this.cacheSecondaryEntryPoints(); return this._secondaryEntryPoints; } - private _secondaryEntryPoints: string[]; - constructor(public readonly name: string, public readonly dependencies: BuildPackage[] = []) { this.sourceDir = join(packagesDir, name); this.outputDir = join(outputDir, 'packages', name); @@ -62,9 +67,13 @@ export class BuildPackage { /** Compiles the package sources with all secondary entry points. */ async compile() { - // Walk through every secondary entry point and build the TypeScript sources sequentially. - for (const entryPoint of this.secondaryEntryPoints) { - await this._compileEntryPoint(buildTsconfigName, entryPoint); + // Compile all secondary entry-points with the same depth in parallel, and each separate depth + // group in sequence. This will look something like: + // Depth 0: coercion, platform, keycodes, bidi + // Depth 1: a11y, scrolling + // Depth 2: overlay + for (const entryPointGroup of this.secondaryEntryPointsByDepth) { + await Promise.all(entryPointGroup.map(p => this._compileEntryPoint(buildTsconfigName, p))); } // Compile the primary entry-point. @@ -86,8 +95,17 @@ export class BuildPackage { const entryPointPath = join(this.sourceDir, secondaryEntryPoint); const entryPointTsconfigPath = join(entryPointPath, tsconfigName); - await ngc(entryPointTsconfigPath, {basePath: entryPointPath}); - this.renamePrivateReExportsToBeUnique(secondaryEntryPoint); + return new Promise((resolve, reject) => { + const childProcess = spawn('./node_modules/.bin/ngc', ['-p', entryPointTsconfigPath]); + + // Pipe stdout and stderr from the child process. + childProcess.stdout.on('data', (data: any) => console.log(`${data}`)); + childProcess.stderr.on('data', (data: any) => console.error(red(`${data}`))); + + childProcess.on('exit', (exitCode: number) => exitCode === 0 ? resolve() : reject()); + }) + .catch(() => console.error(red(`Failed to compile ${secondaryEntryPoint}`))) + .then(() => this.renamePrivateReExportsToBeUnique(secondaryEntryPoint)); } /** Renames `ɵa`-style re-exports generated by Angular to be unique across compilation units. */ @@ -107,4 +125,13 @@ export class BuildPackage { }); } } + + /** Stores the secondary entry-points for this package if they haven't been computed already. */ + private cacheSecondaryEntryPoints() { + if (!this._secondaryEntryPoints) { + this._secondaryEntryPointsByDepth = getSecondaryEntryPointsForPackage(this); + this._secondaryEntryPoints = + this._secondaryEntryPointsByDepth.reduce((list, p) => list.concat(p), []); + } + } } diff --git a/tools/package-tools/secondary-entry-points.ts b/tools/package-tools/secondary-entry-points.ts index 0e6bcedea35..d94d1e251de 100644 --- a/tools/package-tools/secondary-entry-points.ts +++ b/tools/package-tools/secondary-entry-points.ts @@ -24,7 +24,7 @@ export function getSecondaryEntryPointsForPackage(pkg: BuildPackage) { .filter(d => existsSync(join(packageDir, d, 'tsconfig-build.json'))); // Create nodes that comprise the build graph. - const buildNodes: BuildNode[] = entryPoints.map(p => ({name: p, deps: []})); + const buildNodes: BuildNode[] = entryPoints.map(p => ({name: p, deps: [], depth: 0})); // Create a lookup for name -> build graph node. const nodeLookup = buildNodes.reduce((lookup, node) => { @@ -54,24 +54,27 @@ export function getSecondaryEntryPointsForPackage(pkg: BuildPackage) { // Concatenate the build order for each node into one global build order. // Duplicates are automatically omitted by getBuildOrder. - return buildNodes.reduce((order: string[], node) => { + const buildOrder = buildNodes.reduce((order: BuildNode[], node) => { return [...order, ...getBuildOrder(node)]; }, []); + + return partitionNodesByDepth(buildOrder).map(level => level.map(node => node.name)); } -/** Gets the build order for a given node with DFS. */ -function getBuildOrder(node: BuildNode): string[] { +/** Gets the build order for node with DFS. As a side-effect, sets the depth on visited nodes. */ +function getBuildOrder(node: BuildNode): BuildNode[] { if (node.visited) { return []; } - let buildOrder: string[] = []; + let buildOrder: BuildNode[] = []; for (const dep of node.deps) { buildOrder = [...buildOrder, ...getBuildOrder(dep)]; + node.depth = node.deps.reduce((maxDepth, d) => Math.max(d.depth + 1, maxDepth), -1); } node.visited = true; - return [...buildOrder, node.name]; + return [...buildOrder, node]; } /** Gets the names of all subdirectories for a given path. */ @@ -84,9 +87,32 @@ interface BuildNode { name: string; deps: BuildNode[]; visited?: boolean; + depth: number; } +/** + * Partitions nodes into groups by depth. For example, + * [{name: a11y, depth: 1}, {name: bidi, depth: 0}, {name: platform, depth: 0}] + * => + * [ [{name: bidi, depth: 0}, {name: platform, depth: 0}], [{name: a11y, depth: 1}] ] + */ +function partitionNodesByDepth(nodes: BuildNode[]): BuildNode[][] { + const result: BuildNode[][] = [[]]; + let lastDepth = 0; + + nodes.sort((a, b) => a.depth - b.depth).forEach(node => { + if (node.depth === lastDepth) { + result[result.length - 1].push(node); + } else { + result.push([node]); + lastDepth = node.depth; + } + }); + + return result; +} + /** Builds the command that will be executed to find all import statements for a package. */ function buildPackageImportStatementFindCommand(searchDirectory: string, packageName: string) { if (platform() === 'win32') {