diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index 69297df81f..c9636ed9cc 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 129cbb02b8..60a07b34a0 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 1e193edab7..9f9be2ae98 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 0000000000..53af9b1c4f --- /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 0000000000..73022ce250 --- /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 0000000000..93a74db5b6 --- /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 0000000000..c41b842523 --- /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 0000000000..b5fa2fab74 --- /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 0000000000..01e4c9c680 --- /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 0000000000..e8faef34ac --- /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 d2d9dfd7b9..f63a9f6815 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 8ea057933a..15b98f1461 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 938e377f92..cc01306d9b 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 65c7faab6a..6400fc6872 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