From 761aa23cb781edfc7bb446fd17addef3fe618d52 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Mon, 18 Mar 2019 11:16:38 -0700 Subject: [PATCH 1/3] feat(ivy): performance trace mechanism for ngtsc This commit adds a `tracePerformance` option for tsconfig.json. When specified, it causes a JSON file with timing information from the ngtsc compiler to be emitted at the specified path. This tracing system is used to instrument the analysis/emit phases of compilation, and will be useful in debugging future integration work with @angular/cli. See ngtsc/perf/README.md for more details. --- packages/compiler-cli/BUILD.bazel | 1 + packages/compiler-cli/ngcc/BUILD.bazel | 1 + .../ngcc/src/analysis/decoration_analyzer.ts | 4 +- .../compiler-cli/src/ngtsc/perf/BUILD.bazel | 15 +++ .../compiler-cli/src/ngtsc/perf/README.md | 21 ++++ packages/compiler-cli/src/ngtsc/perf/index.ts | 11 ++ .../compiler-cli/src/ngtsc/perf/src/api.ts | 18 +++ .../compiler-cli/src/ngtsc/perf/src/clock.ts | 21 ++++ .../compiler-cli/src/ngtsc/perf/src/noop.ts | 20 ++++ .../src/ngtsc/perf/src/tracking.ts | 110 ++++++++++++++++++ packages/compiler-cli/src/ngtsc/program.ts | 47 +++++++- .../src/ngtsc/transform/BUILD.bazel | 1 + .../src/ngtsc/transform/src/compilation.ts | 13 ++- packages/compiler-cli/src/transformers/api.ts | 11 ++ 14 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/perf/BUILD.bazel create mode 100644 packages/compiler-cli/src/ngtsc/perf/README.md create mode 100644 packages/compiler-cli/src/ngtsc/perf/index.ts create mode 100644 packages/compiler-cli/src/ngtsc/perf/src/api.ts create mode 100644 packages/compiler-cli/src/ngtsc/perf/src/clock.ts create mode 100644 packages/compiler-cli/src/ngtsc/perf/src/noop.ts create mode 100644 packages/compiler-cli/src/ngtsc/perf/src/tracking.ts diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index 69297df81f401..c9636ed9cc6fb 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -30,6 +30,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/path", + "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/routing", "//packages/compiler-cli/src/ngtsc/scope", diff --git a/packages/compiler-cli/ngcc/BUILD.bazel b/packages/compiler-cli/ngcc/BUILD.bazel index 129cbb02b8ea4..60a07b34a0aba 100644 --- a/packages/compiler-cli/ngcc/BUILD.bazel +++ b/packages/compiler-cli/ngcc/BUILD.bazel @@ -16,6 +16,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/path", + "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/transform", diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index 1e193edab7909..9f9be2ae98d3b 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {ConstantPool} from '@angular/compiler'; +import {NOOP_PERF_RECORDER} from '@angular/compiler-cli/src/ngtsc/perf'; import * as path from 'canonical-path'; import * as fs from 'fs'; import * as ts from 'typescript'; @@ -90,7 +91,8 @@ export class DecorationAnalyzer { this.reflectionHost, this.evaluator, this.scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore), new InjectableDecoratorHandler( - this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, /* strictCtorDeps */ false), + this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, + /* strictCtorDeps */ false), new NgModuleDecoratorHandler( this.reflectionHost, this.evaluator, this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER), diff --git a/packages/compiler-cli/src/ngtsc/perf/BUILD.bazel b/packages/compiler-cli/src/ngtsc/perf/BUILD.bazel new file mode 100644 index 0000000000000..53af9b1c4ff24 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/perf/BUILD.bazel @@ -0,0 +1,15 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "perf", + srcs = ["index.ts"] + glob([ + "src/*.ts", + ]), + deps = [ + "//packages:types", + "@npm//@types/node", + "@npm//typescript", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/perf/README.md b/packages/compiler-cli/src/ngtsc/perf/README.md new file mode 100644 index 0000000000000..73022ce2504d2 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/perf/README.md @@ -0,0 +1,21 @@ +# What is the `perf` package? + +This package contains utilities for collecting performance information from around the compiler and for producing ngtsc performance traces. + +This feature is currently undocumented and not exposed to users as the trace file format is still unstable. + +# What is a performance trace? + +A performance trace is a JSON file which logs events within ngtsc with microsecond precision. It tracks the phase of compilation, the file and possibly symbol being compiled, and other metadata explaining what the compiler was doing during the event. + +Traces track two specific kinds of events: marks and spans. A mark is a single event with a timestamp, while a span is two events (start and end) that cover a section of work in the compiler. Analyzing a file is a span event, while a decision (such as deciding not to emit a particular file) is a mark event. + +# Enabling performance traces + +Performance traces are enabled via the undocumented `tracePerformance` option in `angularCompilerOptions` inside the tsconfig.json file. This option takes a string path relative to the current directory, which will be used to write the trace JSON file. + +## In-Memory TS Host Tracing + +By default, the trace file will be written with the `fs` package directly. However, ngtsc supports in-memory compilation using a `ts.CompilerHost` for all operations. In the event that tracing is required when using an in-memory filesystem, a `ts:` prefix can be added to the value of `tracePerformance`, which will cause the trace JSON file to be written with the TS host's `writeFile` method instead. + +This is not done by default as `@angular/cli` does not allow writing arbitrary JSON files via its host. diff --git a/packages/compiler-cli/src/ngtsc/perf/index.ts b/packages/compiler-cli/src/ngtsc/perf/index.ts new file mode 100644 index 0000000000000..93a74db5b66b9 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/perf/index.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export {PerfRecorder} from './src/api'; +export {NOOP_PERF_RECORDER} from './src/noop'; +export {PerfTracker} from './src/tracking'; diff --git a/packages/compiler-cli/src/ngtsc/perf/src/api.ts b/packages/compiler-cli/src/ngtsc/perf/src/api.ts new file mode 100644 index 0000000000000..c41b8425239db --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/perf/src/api.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +export interface PerfRecorder { + readonly enabled: boolean; + + mark(name: string, node?: ts.SourceFile|ts.Declaration, category?: string, detail?: string): void; + start(name: string, node?: ts.SourceFile|ts.Declaration, category?: string, detail?: string): + number; + stop(span: number): void; +} diff --git a/packages/compiler-cli/src/ngtsc/perf/src/clock.ts b/packages/compiler-cli/src/ngtsc/perf/src/clock.ts new file mode 100644 index 0000000000000..b5fa2fab74608 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/perf/src/clock.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// This file uses 'process' +/// + +export type HrTime = [number, number]; + +export function mark(): HrTime { + return process.hrtime(); +} + +export function timeSinceInMicros(mark: HrTime): number { + const delta = process.hrtime(mark); + return (delta[0] * 1000000) + Math.floor(delta[1] / 1000); +} diff --git a/packages/compiler-cli/src/ngtsc/perf/src/noop.ts b/packages/compiler-cli/src/ngtsc/perf/src/noop.ts new file mode 100644 index 0000000000000..01e4c9c68063f --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/perf/src/noop.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {PerfRecorder} from './api'; + +export const NOOP_PERF_RECORDER: PerfRecorder = { + enabled: false, + mark: (name: string, node: ts.SourceFile | ts.Declaration, category?: string, detail?: string): + void => {}, + start: (name: string, node: ts.SourceFile | ts.Declaration, category?: string, detail?: string): + number => { return 0;}, + stop: (span: number | false): void => {}, +}; diff --git a/packages/compiler-cli/src/ngtsc/perf/src/tracking.ts b/packages/compiler-cli/src/ngtsc/perf/src/tracking.ts new file mode 100644 index 0000000000000..e8faef34acb71 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/perf/src/tracking.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/// +import * as fs from 'fs'; +import * as path from 'path'; + +import * as ts from 'typescript'; + +import {PerfRecorder} from './api'; +import {HrTime, mark, timeSinceInMicros} from './clock'; + +export class PerfTracker implements PerfRecorder { + private nextSpanId = 1; + private log: PerfLogEvent[] = []; + + readonly enabled = true; + + private constructor(private zeroTime: HrTime) {} + + static zeroedToNow(): PerfTracker { return new PerfTracker(mark()); } + + mark(name: string, node?: ts.SourceFile|ts.Declaration, category?: string, detail?: string): + void { + const msg = this.makeLogMessage(PerfLogEventType.MARK, name, node, category, detail, undefined); + this.log.push(msg); + } + + start(name: string, node?: ts.SourceFile|ts.Declaration, category?: string, detail?: string): + number { + const span = this.nextSpanId++; + const msg = this.makeLogMessage(PerfLogEventType.SPAN_OPEN, name, node, category, detail, span); + this.log.push(msg); + return span; + } + + stop(span: number): void { + this.log.push({ + type: PerfLogEventType.SPAN_CLOSE, + span, + stamp: timeSinceInMicros(this.zeroTime), + }); + } + + private makeLogMessage( + type: PerfLogEventType, name: string, node: ts.SourceFile|ts.Declaration|undefined, + category: string|undefined, detail: string|undefined, span: number|undefined): PerfLogEvent { + const msg: PerfLogEvent = { + type, + name, + stamp: timeSinceInMicros(this.zeroTime), + }; + if (category !== undefined) { + msg.category = category; + } + if (detail !== undefined) { + msg.detail = detail; + } + if (span !== undefined) { + msg.span = span; + } + if (node !== undefined) { + msg.file = node.getSourceFile().fileName; + if (!ts.isSourceFile(node)) { + const name = ts.getNameOfDeclaration(node); + if (name !== undefined && ts.isIdentifier(name)) { + msg.declaration = name.text; + } + } + } + return msg; + } + + asJson(): unknown { return this.log; } + + serializeToFile(target: string, host: ts.CompilerHost): void { + const json = JSON.stringify(this.log, null, 2); + + if (target.startsWith('ts:')) { + target = target.substr('ts:'.length); + const outFile = path.posix.resolve(host.getCurrentDirectory(), target); + host.writeFile(outFile, json, false); + } else { + const outFile = path.posix.resolve(host.getCurrentDirectory(), target); + fs.writeFileSync(outFile, json); + } + } +} + +export interface PerfLogEvent { + name?: string; + span?: number; + file?: string; + declaration?: string; + type: PerfLogEventType; + category?: string; + detail?: string; + stamp: number; +} + +export enum PerfLogEventType { + SPAN_OPEN, + SPAN_CLOSE, + MARK, +} diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index d2d9dfd7b910a..f63a9f68151af 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -20,6 +20,7 @@ import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatInde import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, DefaultImportTracker, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports'; import {PartialEvaluator} from './partial_evaluator'; import {AbsoluteFsPath, LogicalFileSystem} from './path'; +import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf'; import {TypeScriptReflectionHost} from './reflection'; import {HostResourceLoader} from './resource_loader'; import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing'; @@ -57,10 +58,17 @@ export class NgtscProgram implements api.Program { private refEmitter: ReferenceEmitter|null = null; private fileToModuleHost: FileToModuleHost|null = null; private defaultImportTracker: DefaultImportTracker; + private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER; + private perfTracker: PerfTracker|null = null; constructor( rootNames: ReadonlyArray, private options: api.CompilerOptions, host: api.CompilerHost, oldProgram?: api.Program) { + if (shouldEnablePerfTracing(options)) { + this.perfTracker = PerfTracker.zeroedToNow(); + this.perfRecorder = this.perfTracker; + } + this.rootDirs = getRootDirs(host, options); this.closureCompilerEnabled = !!options.annotateForClosureCompiler; this.resourceManager = new HostResourceLoader(host, options); @@ -187,10 +195,23 @@ export class NgtscProgram implements api.Program { if (this.compilation === undefined) { this.compilation = this.makeCompilation(); } + const analyzeSpan = this.perfRecorder.start('analyze'); await Promise.all(this.tsProgram.getSourceFiles() .filter(file => !file.fileName.endsWith('.d.ts')) - .map(file => this.compilation !.analyzeAsync(file)) + .map(file => { + + const analyzeFileSpan = this.perfRecorder.start('analyzeFile', file); + let analysisPromise = this.compilation !.analyzeAsync(file); + if (analysisPromise === undefined) { + this.perfRecorder.stop(analyzeFileSpan); + } else if (this.perfRecorder.enabled) { + analysisPromise = analysisPromise.then( + () => this.perfRecorder.stop(analyzeFileSpan)); + } + return analysisPromise; + }) .filter((result): result is Promise => result !== undefined)); + this.perfRecorder.stop(analyzeSpan); this.compilation.resolve(); } @@ -245,10 +266,16 @@ export class NgtscProgram implements api.Program { private ensureAnalyzed(): IvyCompilation { if (this.compilation === undefined) { + const analyzeSpan = this.perfRecorder.start('analyze'); this.compilation = this.makeCompilation(); this.tsProgram.getSourceFiles() .filter(file => !file.fileName.endsWith('.d.ts')) - .forEach(file => this.compilation !.analyzeSync(file)); + .forEach(file => { + const analyzeFileSpan = this.perfRecorder.start('analyzeFile', file); + this.compilation !.analyzeSync(file); + this.perfRecorder.stop(analyzeFileSpan); + }); + this.perfRecorder.stop(analyzeSpan); this.compilation.resolve(); } return this.compilation; @@ -298,12 +325,14 @@ export class NgtscProgram implements api.Program { beforeTransforms.push(...customTransforms.beforeTs); } + const emitSpan = this.perfRecorder.start('emit'); const emitResults: ts.EmitResult[] = []; for (const targetSourceFile of this.tsProgram.getSourceFiles()) { if (targetSourceFile.isDeclarationFile) { continue; } + const fileEmitSpan = this.perfRecorder.start('emitFile', targetSourceFile); emitResults.push(emitCallback({ targetSourceFile, program: this.tsProgram, @@ -316,6 +345,12 @@ export class NgtscProgram implements api.Program { afterDeclarations: afterDeclarationsTransforms, }, })); + this.perfRecorder.stop(fileEmitSpan); + } + this.perfRecorder.stop(emitSpan); + + if (this.perfTracker !== null && this.options.tracePerformance !== undefined) { + this.perfTracker.serializeToFile(this.options.tracePerformance, this.host); } // Run the emit, including a custom transformer that will downlevel the Ivy decorators in code. @@ -405,7 +440,8 @@ export class NgtscProgram implements api.Program { ]; return new IvyCompilation( - handlers, checker, this.reflector, this.importRewriter, this.sourceToFactorySymbols); + handlers, checker, this.reflector, this.importRewriter, this.perfRecorder, + this.sourceToFactorySymbols); } private get reflector(): TypeScriptReflectionHost { @@ -455,6 +491,7 @@ function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult { emitSkipped = emitSkipped || er.emitSkipped; emittedFiles.push(...(er.emittedFiles || [])); } + return {diagnostics, emitSkipped, emittedFiles}; } @@ -519,3 +556,7 @@ export class ReferenceGraphAdapter implements ReferencesRegistry { } } } + +function shouldEnablePerfTracing(options: api.CompilerOptions): boolean { + return options.tracePerformance !== undefined; +} diff --git a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel index 8ea057933ad94..15b98f1461515 100644 --- a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( "//packages/compiler", "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/translator", "//packages/compiler-cli/src/ngtsc/typecheck", diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index 938e377f92ce6..cc01306d9bad6 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -11,6 +11,7 @@ import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ImportRewriter} from '../../imports'; +import {PerfRecorder} from '../../perf'; import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration, reflectNameOfDeclaration} from '../../reflection'; import {TypeCheckContext} from '../../typecheck'; import {getSourceFile} from '../../util/src/typescript'; @@ -75,7 +76,7 @@ export class IvyCompilation { constructor( private handlers: DecoratorHandler[], private checker: ts.TypeChecker, private reflector: ReflectionHost, private importRewriter: ImportRewriter, - private sourceToFactorySymbols: Map>|null) {} + private perf: PerfRecorder, private sourceToFactorySymbols: Map>|null) {} get exportStatements(): Map> { return this.reexportMap; } @@ -182,6 +183,7 @@ export class IvyCompilation { for (const match of ivyClass.matchedHandlers) { // The analyze() function will run the analysis phase of the handler. const analyze = () => { + const analyzeClassSpan = this.perf.start('analyzeClass', node); try { match.analyzed = match.handler.analyze(node, match.detected.metadata); @@ -201,6 +203,8 @@ export class IvyCompilation { } else { throw err; } + } finally { + this.perf.stop(analyzeClassSpan); } }; @@ -243,10 +247,12 @@ export class IvyCompilation { } resolve(): void { + const resolveSpan = this.perf.start('resolve'); this.ivyClasses.forEach((ivyClass, node) => { for (const match of ivyClass.matchedHandlers) { if (match.handler.resolve !== undefined && match.analyzed !== null && match.analyzed.analysis !== undefined) { + const resolveClassSpan = this.perf.start('resolveClass', node); try { const res = match.handler.resolve(node, match.analyzed.analysis); if (res.reexports !== undefined) { @@ -268,10 +274,13 @@ export class IvyCompilation { } else { throw err; } + } finally { + this.perf.stop(resolveClassSpan); } } } }); + this.perf.stop(resolveSpan); } typeCheck(context: TypeCheckContext): void { @@ -305,8 +314,10 @@ export class IvyCompilation { continue; } + const compileSpan = this.perf.start('compileClass', original); const compileMatchRes = match.handler.compile(node as ClassDeclaration, match.analyzed.analysis, constantPool); + this.perf.stop(compileSpan); if (!Array.isArray(compileMatchRes)) { res.push(compileMatchRes); } else { diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 65c7faab6a2d8..6400fc6872f01 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -205,6 +205,17 @@ export interface CompilerOptions extends ts.CompilerOptions { /** @internal */ collectAllErrors?: boolean; + /** An option to enable ngtsc's internal performance tracing. + * + * This should be a path to a JSON file where trace information will be written. An optional 'ts:' + * prefix will cause the trace to be written via the TS host instead of directly to the filesystem + * (not all hosts support this mode of operation). + * + * This is currently not exposed to users as the trace format is still unstable. + * + * @internal */ + tracePerformance?: string; + /** * Whether NGC should generate re-exports for external symbols which are referenced * in Angular metadata (e.g. @Component, @Inject, @ViewChild). This can be enabled in From 9309fba4b9cf0b567868b8059e00fbc0bbec2ac9 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Mon, 18 Mar 2019 11:21:29 -0700 Subject: [PATCH 2/3] test(ivy): support multiple compilations in the ngtsc test env This commit adds support for compiling the same program repeatedly in a way that's similar to how incremental builds work in a tool such as the CLI. * support is added to the compiler entrypoint for reuse of the Program object between compilations. This is the basis of the compiler's incremental compilation model. * support is added to wrap the CompilerHost the compiler creates and cache ts.SourceFiles in between compilations. * support is added to track when files are emitted, for assertion purposes. * an 'exclude' section is added to the base tsconfig to prevent .d.ts outputs from the first compilation from becoming inputs to any subsequent compilations. --- packages/compiler-cli/src/main.ts | 16 +- .../src/transformers/compiler_host.ts | 13 +- packages/compiler-cli/test/ngtsc/env.ts | 137 ++++++++++++++++-- 3 files changed, 145 insertions(+), 21 deletions(-) diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index ff8461708329f..2c77b31adcd6b 100644 --- a/packages/compiler-cli/src/main.ts +++ b/packages/compiler-cli/src/main.ts @@ -22,7 +22,9 @@ import {performWatchCompilation, createPerformWatchHost} from './perform_watch' export function main( args: string[], consoleError: (s: string) => void = console.error, - config?: NgcParsedConfiguration, customTransformers?: api.CustomTransformers): number { + config?: NgcParsedConfiguration, customTransformers?: api.CustomTransformers, programReuse?: { + program: api.Program | undefined, + }): number { let {project, rootNames, options, errors: configErrors, watch, emitFlags} = config || readNgcCommandLineAndConfiguration(args); if (configErrors.length) { @@ -32,12 +34,22 @@ export function main( const result = watchMode(project, options, consoleError); return reportErrorsAndExit(result.firstCompileResult, options, consoleError); } - const {diagnostics: compileDiags} = performCompilation({ + + let oldProgram: api.Program|undefined; + if (programReuse !== undefined) { + oldProgram = programReuse.program; + } + + const {diagnostics: compileDiags, program} = performCompilation({ rootNames, options, emitFlags, + oldProgram, emitCallback: createEmitCallback(options), customTransformers }); + if (programReuse !== undefined) { + programReuse.program = program; + } return reportErrorsAndExit(compileDiags, options, consoleError); } diff --git a/packages/compiler-cli/src/transformers/compiler_host.ts b/packages/compiler-cli/src/transformers/compiler_host.ts index 988e4a486e89f..a4f16c3548f9b 100644 --- a/packages/compiler-cli/src/transformers/compiler_host.ts +++ b/packages/compiler-cli/src/transformers/compiler_host.ts @@ -21,19 +21,18 @@ const NODE_MODULES_PACKAGE_NAME = /node_modules\/((\w|-|\.)+|(@(\w|-|\.)+\/(\w|- const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; const CSS_PREPROCESSOR_EXT = /(\.scss|\.less|\.styl)$/; -let augmentHostForTest: {[name: string]: Function}|null = null; +let wrapHostForTest: ((host: ts.CompilerHost) => ts.CompilerHost)|null = null; -export function setAugmentHostForTest(augmentation: {[name: string]: Function} | null): void { - augmentHostForTest = augmentation; +export function setWrapHostForTest(wrapFn: ((host: ts.CompilerHost) => ts.CompilerHost) | null): + void { + wrapHostForTest = wrapFn; } export function createCompilerHost( {options, tsHost = ts.createCompilerHost(options, true)}: {options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost { - if (augmentHostForTest !== null) { - for (const name of Object.keys(augmentHostForTest)) { - (tsHost as any)[name] = augmentHostForTest[name]; - } + if (wrapHostForTest !== null) { + tsHost = wrapHostForTest(tsHost); } return tsHost; } diff --git a/packages/compiler-cli/test/ngtsc/env.ts b/packages/compiler-cli/test/ngtsc/env.ts index 6ce40e673c26a..3cdc5d7dc2526 100644 --- a/packages/compiler-cli/test/ngtsc/env.ts +++ b/packages/compiler-cli/test/ngtsc/env.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {CustomTransformers} from '@angular/compiler-cli'; -import {setAugmentHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host'; +import {CustomTransformers, Program} from '@angular/compiler-cli'; +import {setWrapHostForTest} from '@angular/compiler-cli/src/transformers/compiler_host'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; @@ -37,6 +37,9 @@ function setupFakeCore(support: TestSupport): void { * TypeScript code. */ export class NgtscTestEnvironment { + private multiCompileHostExt: MultiCompileHostExt|null = null; + private oldProgram: Program|null = null; + private constructor(private support: TestSupport, readonly outDir: string) {} get basePath(): string { return this.support.basePath; } @@ -50,7 +53,7 @@ export class NgtscTestEnvironment { process.chdir(support.basePath); setupFakeCore(support); - setAugmentHostForTest(null); + setWrapHostForTest(null); const env = new NgtscTestEnvironment(support, outDir); @@ -74,7 +77,10 @@ export class NgtscTestEnvironment { }, "angularCompilerOptions": { "enableIvy": true - } + }, + "exclude": [ + "built" + ] }`); return env; @@ -98,7 +104,47 @@ export class NgtscTestEnvironment { return fs.readFileSync(modulePath, 'utf8'); } - write(fileName: string, content: string) { this.support.write(fileName, content); } + enableMultipleCompilations(): void { + this.multiCompileHostExt = new MultiCompileHostExt(); + setWrapHostForTest(makeWrapHost(this.multiCompileHostExt)); + } + + flushWrittenFileTracking(): void { + if (this.multiCompileHostExt === null) { + throw new Error(`Not tracking written files - call enableMultipleCompilations()`); + } + this.multiCompileHostExt.flushWrittenFileTracking(); + } + + getFilesWrittenSinceLastFlush(): Set { + if (this.multiCompileHostExt === null) { + throw new Error(`Not tracking written files - call enableMultipleCompilations()`); + } + const outDir = path.join(this.support.basePath, 'built'); + const writtenFiles = new Set(); + this.multiCompileHostExt.getFilesWrittenSinceLastFlush().forEach(rawFile => { + if (rawFile.startsWith(outDir)) { + writtenFiles.add(rawFile.substr(outDir.length)); + } + }); + return writtenFiles; + } + + write(fileName: string, content: string) { + if (this.multiCompileHostExt !== null) { + const absFilePath = path.resolve(this.support.basePath, fileName); + this.multiCompileHostExt.invalidate(absFilePath); + } + this.support.write(fileName, content); + } + + invalidateCachedFile(fileName: string): void { + if (this.multiCompileHostExt === null) { + throw new Error(`Not caching files - call enableMultipleCompilations()`); + } + const fullFile = path.join(this.support.basePath, fileName); + this.multiCompileHostExt.invalidate(fullFile); + } tsconfig(extraOpts: {[key: string]: string | boolean} = {}, extraRootDirs?: string[]): void { const tsconfig: {[key: string]: any} = { @@ -113,12 +159,7 @@ export class NgtscTestEnvironment { this.write('tsconfig.json', JSON.stringify(tsconfig, null, 2)); if (extraOpts['_useHostForImportGeneration'] === true) { - const cwd = process.cwd(); - setAugmentHostForTest({ - fileNameToModuleName: (importedFilePath: string) => { - return 'root' + importedFilePath.substr(cwd.length).replace(/(\.d)?.ts$/, ''); - } - }); + setWrapHostForTest(makeWrapHost(new FileNameToModuleNameHost())); } } @@ -127,9 +168,19 @@ export class NgtscTestEnvironment { */ driveMain(customTransformers?: CustomTransformers): void { const errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error); - const exitCode = main(['-p', this.basePath], errorSpy, undefined, customTransformers); + let reuseProgram: {program: Program | undefined}|undefined = undefined; + if (this.multiCompileHostExt !== null) { + reuseProgram = { + program: this.oldProgram || undefined, + }; + } + const exitCode = + main(['-p', this.basePath], errorSpy, undefined, customTransformers, reuseProgram); expect(errorSpy).not.toHaveBeenCalled(); expect(exitCode).toBe(0); + if (this.multiCompileHostExt !== null) { + this.oldProgram = reuseProgram !.program !; + } } /** @@ -147,3 +198,65 @@ export class NgtscTestEnvironment { return program.listLazyRoutes(entryPoint); } } + +class AugmentedCompilerHost { + delegate !: ts.CompilerHost; +} + +class FileNameToModuleNameHost extends AugmentedCompilerHost { + // CWD must be initialized lazily as `this.delegate` is not set until later. + private cwd: string|null = null; + fileNameToModuleName(importedFilePath: string): string { + if (this.cwd === null) { + this.cwd = this.delegate.getCurrentDirectory(); + } + return 'root' + importedFilePath.substr(this.cwd.length).replace(/(\.d)?.ts$/, ''); + } +} + +class MultiCompileHostExt extends AugmentedCompilerHost implements Partial { + private cache = new Map(); + private writtenFiles = new Set(); + + getSourceFile( + fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void, + shouldCreateNewSourceFile?: boolean): ts.SourceFile|undefined { + if (this.cache.has(fileName)) { + return this.cache.get(fileName) !; + } + const sf = + this.delegate.getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile); + if (sf !== undefined) { + this.cache.set(sf.fileName, sf); + } + return sf; + } + + flushWrittenFileTracking(): void { this.writtenFiles.clear(); } + + writeFile( + fileName: string, data: string, writeByteOrderMark: boolean, + onError: ((message: string) => void)|undefined, + sourceFiles?: ReadonlyArray): void { + this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); + this.writtenFiles.add(fileName); + } + + getFilesWrittenSinceLastFlush(): Set { return this.writtenFiles; } + + invalidate(fileName: string): void { this.cache.delete(fileName); } +} + +function makeWrapHost(wrapped: AugmentedCompilerHost): (host: ts.CompilerHost) => ts.CompilerHost { + return (delegate) => { + wrapped.delegate = delegate; + return new Proxy(delegate, { + get: (target: ts.CompilerHost, name: string): any => { + if ((wrapped as any)[name] !== undefined) { + return (wrapped as any)[name] !.bind(wrapped); + } + return (target as any)[name]; + } + }); + }; +} From 3492fc0ecb6d08458d0e4d9dcef35539bd7e24b9 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Mon, 18 Mar 2019 12:25:26 -0700 Subject: [PATCH 3/3] perf(ivy): basic incremental compilation for ngtsc This commit introduces a mechanism for incremental compilation to the ngtsc compiler. Previously, incremental information was used in the construction of the ts.Program for subsequent compilations, but was not used in ngtsc itself. This commit adds an IncrementalState class, which tracks state between ngtsc compilations. Currently, this supports skipping the TypeScript emit step when the compiler can prove the contents of emit have not changed. This is implemented for @Injectables as well as for files which don't contain any Angular decorated types. These are the only files which can be proven to be safe today. See ngtsc/incremental/README.md for more details. --- packages/compiler-cli/BUILD.bazel | 1 + .../src/ngtsc/annotations/src/injectable.ts | 2 + .../src/ngtsc/incremental/BUILD.bazel | 13 +++ .../src/ngtsc/incremental/index.ts | 9 ++ .../src/ngtsc/incremental/src/README.md | 43 ++++++++++ .../src/ngtsc/incremental/src/state.ts | 86 +++++++++++++++++++ packages/compiler-cli/src/ngtsc/program.ts | 17 +++- .../src/ngtsc/transform/BUILD.bazel | 1 + .../src/ngtsc/transform/src/api.ts | 1 + .../src/ngtsc/transform/src/compilation.ts | 25 +++++- .../test/ngtsc/incremental_spec.ts | 47 ++++++++++ 11 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/incremental/BUILD.bazel create mode 100644 packages/compiler-cli/src/ngtsc/incremental/index.ts create mode 100644 packages/compiler-cli/src/ngtsc/incremental/src/README.md create mode 100644 packages/compiler-cli/src/ngtsc/incremental/src/state.ts create mode 100644 packages/compiler-cli/test/ngtsc/incremental_spec.ts diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index c9636ed9cc6fb..d6aed05f7a0fc 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -28,6 +28,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/entry_point", "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/incremental", "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/perf", diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index 31edc426602d1..d113fa3b25025 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -50,6 +50,8 @@ export class InjectableDecoratorHandler implements analyze(node: ClassDeclaration, decorator: Decorator): AnalysisOutput { return { + // @Injectable()s cannot depend on other files for their compilation output. + allowSkipAnalysisAndEmit: true, analysis: { meta: extractInjectableMetadata( node, decorator, this.reflector, this.defaultImportRecorder, this.isCore, diff --git a/packages/compiler-cli/src/ngtsc/incremental/BUILD.bazel b/packages/compiler-cli/src/ngtsc/incremental/BUILD.bazel new file mode 100644 index 0000000000000..adeb92659a718 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/incremental/BUILD.bazel @@ -0,0 +1,13 @@ +load("//tools:defaults.bzl", "ts_library") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "incremental", + srcs = ["index.ts"] + glob([ + "src/**/*.ts", + ]), + deps = [ + "@npm//typescript", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/incremental/index.ts b/packages/compiler-cli/src/ngtsc/incremental/index.ts new file mode 100644 index 0000000000000..3de91807fc564 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/incremental/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export {IncrementalState} from './src/state'; \ No newline at end of file diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/README.md b/packages/compiler-cli/src/ngtsc/incremental/src/README.md new file mode 100644 index 0000000000000..5aec7a09ab260 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/incremental/src/README.md @@ -0,0 +1,43 @@ +# What is the `incremental` package? + +This package contains logic related to incremental compilation in ngtsc. + +In particular, it tracks metadata for `ts.SourceFile`s in between compilations, so the compiler can make intelligent decisions about when to skip certain operations and rely on previously analyzed data. + +# How does incremental compilation work? + +The initial compilation is no different from a standalone compilation; the compiler is unaware that incremental compilation will be utilized. + +When an `NgtscProgram` is created for a _subsequent_ compilation, it is initialized with the `NgtscProgram` from the previous compilation. It is therefore able to take advantage of any information present in the previous compilation to optimize the next one. + +This information is leveraged in two major ways: + +1) The previous `ts.Program` itself is used to create the next `ts.Program`, allowing TypeScript internally to leverage information from the previous compile in much the same way. + +2) An `IncrementalState` instance is constructed from the previous compilation's `IncrementalState` and its `ts.Program`. + +After this initialization, the `IncrementalState` contains the knowledge from the previous compilation which will be used to optimize the next one. + +# What optimizations can be made? + +Currently, ngtsc makes a decision to skip the emit of a file if it can prove that the contents of the file will not have changed. To prove this, two conditions must be true. + +* The input file itself must not have changed since the previous compilation. + +* As a result of analyzing the file, no dependencies must exist where the output of compilation could vary depending on the contents of any other file. + +The second condition is challenging, as Angular allows statically evaluated expressions in lots of contexts that could result in changes from file to file. For example, the `name` of an `@Pipe` could be a reference to a constant in a different file. + +Therefore, only two types of files meet these conditions and can be optimized today: + +* Files with no Angular decorated classes at all. + +* Files with only `@Injectable`s. + +# What optimizations are possible in the future? + +There is plenty of room for improvement here, with diminishing returns for the work involved. + +* The compiler could track the dependencies of each file being compiled, and know whether an `@Pipe` gets its name from a second file, for example. This is sufficient to skip the analysis and emit of more files when none of the dependencies have changed. + +* The compiler could also perform analysis on files which _have_ changed dependencies, and skip emit if the analysis indicates nothing has changed which would affect the file being emitted. diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/state.ts b/packages/compiler-cli/src/ngtsc/incremental/src/state.ts new file mode 100644 index 0000000000000..ab80199d3aca4 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/incremental/src/state.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +/** + * Accumulates state between compilations. + */ +export class IncrementalState { + private constructor( + private unchangedFiles: Set, + private metadata: Map) {} + + static reconcile(previousState: IncrementalState, oldProgram: ts.Program, newProgram: ts.Program): + IncrementalState { + const unchangedFiles = new Set(); + const metadata = new Map(); + + // Compute the set of files that's unchanged. + const oldFiles = new Set(); + for (const oldFile of oldProgram.getSourceFiles()) { + if (!oldFile.isDeclarationFile) { + oldFiles.add(oldFile); + } + } + + // Look for files in the new program which haven't changed. + for (const newFile of newProgram.getSourceFiles()) { + if (oldFiles.has(newFile)) { + unchangedFiles.add(newFile); + + // Copy over metadata for the unchanged file if available. + if (previousState.metadata.has(newFile)) { + metadata.set(newFile, previousState.metadata.get(newFile) !); + } + } + } + + return new IncrementalState(unchangedFiles, metadata); + } + + static fresh(): IncrementalState { + return new IncrementalState(new Set(), new Map()); + } + + safeToSkipEmit(sf: ts.SourceFile): boolean { + if (!this.unchangedFiles.has(sf)) { + // The file has changed since the last run, and must be re-emitted. + return false; + } + + // The file hasn't changed since the last emit. Whether or not it's safe to emit depends on + // what metadata was gathered about the file. + + if (!this.metadata.has(sf)) { + // The file has no metadata from the previous or current compilations, so it must be emitted. + return false; + } + + const meta = this.metadata.get(sf) !; + + // Check if this file was explicitly marked as safe. This would only be done if every + // `DecoratorHandler` agreed that the file didn't depend on any other file's contents. + if (meta.safeToSkipEmitIfUnchanged) { + return true; + } + + // The file wasn't explicitly marked as safe to skip emitting, so require an emit. + return false; + } + + markFileAsSafeToSkipEmitIfUnchanged(sf: ts.SourceFile): void { + this.metadata.set(sf, { + safeToSkipEmitIfUnchanged: true, + }); + } +} + +interface FileMetadata { + safeToSkipEmitIfUnchanged: boolean; +} diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index f63a9f68151af..2698cb4515abd 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -18,6 +18,7 @@ import {CycleAnalyzer, ImportGraph} from './cycles'; import {ErrorCode, ngErrorCode} from './diagnostics'; import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point'; import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, DefaultImportTracker, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports'; +import {IncrementalState} from './incremental'; import {PartialEvaluator} from './partial_evaluator'; import {AbsoluteFsPath, LogicalFileSystem} from './path'; import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf'; @@ -60,6 +61,7 @@ export class NgtscProgram implements api.Program { private defaultImportTracker: DefaultImportTracker; private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER; private perfTracker: PerfTracker|null = null; + private incrementalState: IncrementalState; constructor( rootNames: ReadonlyArray, private options: api.CompilerOptions, @@ -143,6 +145,13 @@ export class NgtscProgram implements api.Program { this.moduleResolver = new ModuleResolver(this.tsProgram, options, this.host); this.cycleAnalyzer = new CycleAnalyzer(new ImportGraph(this.moduleResolver)); this.defaultImportTracker = new DefaultImportTracker(); + if (oldProgram === undefined) { + this.incrementalState = IncrementalState.fresh(); + } else { + const oldNgtscProgram = oldProgram as NgtscProgram; + this.incrementalState = IncrementalState.reconcile( + oldNgtscProgram.incrementalState, oldNgtscProgram.tsProgram, this.tsProgram); + } } getTsProgram(): ts.Program { return this.tsProgram; } @@ -332,6 +341,10 @@ export class NgtscProgram implements api.Program { continue; } + if (this.incrementalState.safeToSkipEmit(targetSourceFile)) { + continue; + } + const fileEmitSpan = this.perfRecorder.start('emitFile', targetSourceFile); emitResults.push(emitCallback({ targetSourceFile, @@ -440,8 +453,8 @@ export class NgtscProgram implements api.Program { ]; return new IvyCompilation( - handlers, checker, this.reflector, this.importRewriter, this.perfRecorder, - this.sourceToFactorySymbols); + handlers, checker, this.reflector, this.importRewriter, this.incrementalState, + this.perfRecorder, this.sourceToFactorySymbols); } private get reflector(): TypeScriptReflectionHost { diff --git a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel index 15b98f1461515..e45022d9ecb63 100644 --- a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( "//packages/compiler", "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/incremental", "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/translator", diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index 76a627818ad42..e8f8f7a4ea5b5 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -110,6 +110,7 @@ export interface AnalysisOutput { diagnostics?: ts.Diagnostic[]; factorySymbolName?: string; typeCheck?: boolean; + allowSkipAnalysisAndEmit?: boolean; } /** diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index cc01306d9bad6..51e978c4bd530 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -11,6 +11,7 @@ import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ImportRewriter} from '../../imports'; +import {IncrementalState} from '../../incremental'; import {PerfRecorder} from '../../perf'; import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration, reflectNameOfDeclaration} from '../../reflection'; import {TypeCheckContext} from '../../typecheck'; @@ -76,7 +77,8 @@ export class IvyCompilation { constructor( private handlers: DecoratorHandler[], private checker: ts.TypeChecker, private reflector: ReflectionHost, private importRewriter: ImportRewriter, - private perf: PerfRecorder, private sourceToFactorySymbols: Map>|null) {} + private incrementalState: IncrementalState, private perf: PerfRecorder, + private sourceToFactorySymbols: Map>|null) {} get exportStatements(): Map> { return this.reexportMap; } @@ -170,6 +172,10 @@ export class IvyCompilation { private analyze(sf: ts.SourceFile, preanalyze: boolean): Promise|undefined { const promises: Promise[] = []; + // This flag begins as true for the file. If even one handler is matched and does not explicitly + // state that analysis/emit can be skipped, then the flag will be set to false. + let allowSkipAnalysisAndEmit = true; + const analyzeClass = (node: ClassDeclaration): void => { const ivyClass = this.detectHandlersForClass(node); @@ -197,6 +203,11 @@ export class IvyCompilation { this.sourceToFactorySymbols.get(sf.fileName) !.add(match.analyzed.factorySymbolName); } + // Update the allowSkipAnalysisAndEmit flag - it will only remain true if match.analyzed + // also explicitly specifies a value of true for the flag. + allowSkipAnalysisAndEmit = + allowSkipAnalysisAndEmit && (!!match.analyzed.allowSkipAnalysisAndEmit); + } catch (err) { if (err instanceof FatalDiagnosticError) { this._diagnostics.push(err.toDiagnostic()); @@ -239,9 +250,19 @@ export class IvyCompilation { visit(sf); + const updateIncrementalState = () => { + if (allowSkipAnalysisAndEmit) { + this.incrementalState.markFileAsSafeToSkipEmitIfUnchanged(sf); + } + }; + if (preanalyze && promises.length > 0) { - return Promise.all(promises).then(() => undefined); + return Promise.all(promises).then(() => { + updateIncrementalState(); + return undefined; + }); } else { + updateIncrementalState(); return undefined; } } diff --git a/packages/compiler-cli/test/ngtsc/incremental_spec.ts b/packages/compiler-cli/test/ngtsc/incremental_spec.ts new file mode 100644 index 0000000000000..5092ea4d867c1 --- /dev/null +++ b/packages/compiler-cli/test/ngtsc/incremental_spec.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NgtscTestEnvironment} from './env'; + +describe('ngtsc incremental compilation', () => { + let env !: NgtscTestEnvironment; + beforeEach(() => { + env = NgtscTestEnvironment.setup(); + env.enableMultipleCompilations(); + env.tsconfig(); + }); + + it('should compile incrementally', () => { + env.write('service.ts', ` + import {Injectable} from '@angular/core'; + + @Injectable() + export class Service {} + `); + env.write('test.ts', ` + import {Component} from '@angular/core'; + import {Service} from './service'; + + @Component({selector: 'cmp', template: 'cmp'}) + export class Cmp { + constructor(service: Service) {} + } + `); + env.driveMain(); + env.flushWrittenFileTracking(); + + // Pretend a change was made to test.ts. + env.invalidateCachedFile('test.ts'); + env.driveMain(); + const written = env.getFilesWrittenSinceLastFlush(); + + // The component should be recompiled, but not the service. + expect(written).toContain('/test.js'); + expect(written).not.toContain('/service.js'); + }); +}); \ No newline at end of file