From 48fec08c95f363c2906b2a2315e783793ccaedc5 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Mon, 15 Mar 2021 15:23:03 -0700 Subject: [PATCH] perf(compiler-cli): refactor the performance tracing infrastructure (#41125) ngtsc has an internal performance tracing package, which previously has not really seen much use. It used to track performance statistics on a very granular basis (microseconds per actual class analysis, for example). This had two problems: * it produced voluminous amounts of data, complicating the analysis of such results and providing dubious value. * it added nontrivial overhead to compilation when used (which also affected the very performance of the operations being measured). This commit replaces the old system with a streamlined performance tracing setup which is lightweight and designed to be always-on. The new system tracks 3 metrics: * time taken by various phases and operations within the compiler * events (counters) which measure the shape and size of the compilation * memory usage measured at various points of the compilation process If the compiler option `tracePerformance` is set, the compiler will serialize these metrics to a JSON file at that location after compilation is complete. PR Close #41125 --- packages/bazel/src/ngc-wrapped/BUILD.bazel | 1 + packages/bazel/src/ngc-wrapped/index.ts | 12 + packages/compiler-cli/index.ts | 1 + .../ngcc/src/analysis/decoration_analyzer.ts | 17 +- .../src/ngtsc/annotations/BUILD.bazel | 1 + .../src/ngtsc/annotations/src/component.ts | 4 +- .../src/ngtsc/annotations/src/directive.ts | 5 +- .../src/ngtsc/annotations/src/injectable.ts | 5 +- .../src/ngtsc/annotations/src/ng_module.ts | 6 +- .../src/ngtsc/annotations/src/pipe.ts | 6 +- .../src/ngtsc/annotations/test/BUILD.bazel | 1 + .../ngtsc/annotations/test/component_spec.ts | 4 +- .../ngtsc/annotations/test/directive_spec.ts | 3 +- .../ngtsc/annotations/test/injectable_spec.ts | 3 +- .../ngtsc/annotations/test/ng_module_spec.ts | 4 +- .../src/ngtsc/core/api/src/options.ts | 5 +- .../src/ngtsc/core/src/compiler.ts | 238 +++++++------ .../src/ngtsc/core/test/compiler_test.ts | 4 +- .../compiler-cli/src/ngtsc/cycles/BUILD.bazel | 1 + .../src/ngtsc/cycles/src/imports.ts | 42 ++- .../src/ngtsc/cycles/test/BUILD.bazel | 1 + .../src/ngtsc/cycles/test/analyzer_spec.ts | 3 +- .../src/ngtsc/cycles/test/imports_spec.ts | 3 +- .../src/ngtsc/incremental/BUILD.bazel | 1 + .../src/ngtsc/incremental/src/state.ts | 215 ++++++------ .../compiler-cli/src/ngtsc/perf/BUILD.bazel | 2 - packages/compiler-cli/src/ngtsc/perf/index.ts | 4 +- .../compiler-cli/src/ngtsc/perf/src/api.ts | 328 +++++++++++++++++- .../compiler-cli/src/ngtsc/perf/src/noop.ts | 28 +- .../src/ngtsc/perf/src/recorder.ts | 154 ++++++++ .../src/ngtsc/perf/src/tracking.ts | 110 ------ packages/compiler-cli/src/ngtsc/program.ts | 217 ++++++------ .../src/ngtsc/transform/src/compilation.ts | 11 +- .../src/ngtsc/transform/src/transform.ts | 11 +- packages/compiler-cli/src/ngtsc/tsc_plugin.ts | 18 +- .../src/ngtsc/typecheck/BUILD.bazel | 1 + .../src/ngtsc/typecheck/src/checker.ts | 196 ++++++----- .../src/ngtsc/typecheck/src/context.ts | 5 +- .../src/ngtsc/typecheck/test/BUILD.bazel | 1 + .../src/ngtsc/typecheck/test/test_utils.ts | 3 +- .../typecheck/test/type_constructor_spec.ts | 7 +- packages/language-service/ivy/BUILD.bazel | 1 + .../language-service/ivy/compiler_factory.ts | 10 +- packages/language-service/ivy/references.ts | 159 +++++---- 44 files changed, 1201 insertions(+), 651 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/perf/src/recorder.ts delete mode 100644 packages/compiler-cli/src/ngtsc/perf/src/tracking.ts diff --git a/packages/bazel/src/ngc-wrapped/BUILD.bazel b/packages/bazel/src/ngc-wrapped/BUILD.bazel index 41b26525a6..198caa6604 100644 --- a/packages/bazel/src/ngc-wrapped/BUILD.bazel +++ b/packages/bazel/src/ngc-wrapped/BUILD.bazel @@ -16,6 +16,7 @@ ts_library( ], deps = [ "//packages/compiler-cli", + "//packages/compiler-cli/src/ngtsc/perf", "@npm//@bazel/typescript", "@npm//@types/node", "@npm//tsickle", diff --git a/packages/bazel/src/ngc-wrapped/index.ts b/packages/bazel/src/ngc-wrapped/index.ts index 44dd4b1100..477a1a38d2 100644 --- a/packages/bazel/src/ngc-wrapped/index.ts +++ b/packages/bazel/src/ngc-wrapped/index.ts @@ -7,6 +7,7 @@ */ import * as ng from '@angular/compiler-cli'; +import {PerfPhase} from '@angular/compiler-cli/src/ngtsc/perf'; import {BazelOptions, CachedFileLoader, CompilerHost, constructManifest, debug, FileCache, FileLoader, parseTsconfig, resolveNormalizedPath, runAsWorker, runWorkerLoop, UncachedFileLoader} from '@bazel/typescript'; import * as fs from 'fs'; import * as path from 'path'; @@ -515,6 +516,12 @@ function gatherDiagnosticsForInputsOnly( options: ng.CompilerOptions, bazelOpts: BazelOptions, ngProgram: ng.Program): (ng.Diagnostic|ts.Diagnostic)[] { const tsProgram = ngProgram.getTsProgram(); + + // For the Ivy compiler, track the amount of time spent fetching TypeScript diagnostics. + let previousPhase = PerfPhase.Unaccounted; + if (ngProgram instanceof ng.NgtscProgram) { + previousPhase = ngProgram.compiler.perfRecorder.phase(PerfPhase.TypeScriptDiagnostics); + } const diagnostics: (ng.Diagnostic|ts.Diagnostic)[] = []; // These checks mirror ts.getPreEmitDiagnostics, with the important // exception of avoiding b/30708240, which is that if you call @@ -529,6 +536,11 @@ function gatherDiagnosticsForInputsOnly( diagnostics.push(...tsProgram.getSyntacticDiagnostics(sf)); diagnostics.push(...tsProgram.getSemanticDiagnostics(sf)); } + + if (ngProgram instanceof ng.NgtscProgram) { + ngProgram.compiler.perfRecorder.phase(previousPhase); + } + if (!diagnostics.length) { // only gather the angular diagnostics if we have no diagnostics // in any other files. diff --git a/packages/compiler-cli/index.ts b/packages/compiler-cli/index.ts index e74cedd96b..1247ebc2f4 100644 --- a/packages/compiler-cli/index.ts +++ b/packages/compiler-cli/index.ts @@ -22,5 +22,6 @@ export {CompilerOptions as AngularCompilerOptions} from './src/transformers/api' export {ngToTsDiagnostic} from './src/transformers/util'; export {NgTscPlugin} from './src/ngtsc/tsc_plugin'; +export {NgtscProgram} from './src/ngtsc/program'; setFileSystem(new NodeJSFileSystem()); diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index b2fb42c113..54f2b08dc4 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 ts from 'typescript'; import {ParsedConfiguration} from '../../..'; @@ -89,7 +90,7 @@ export class DecorationAnalyzer { fullRegistry = new CompoundMetadataRegistry([this.metaRegistry, this.scopeRegistry]); evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker, /* dependencyTracker */ null); - importGraph = new ImportGraph(this.typeChecker); + importGraph = new ImportGraph(this.typeChecker, NOOP_PERF_RECORDER); cycleAnalyzer = new CycleAnalyzer(this.importGraph); injectableRegistry = new InjectableClassRegistry(this.reflectionHost); typeCheckScopeRegistry = new TypeCheckScopeRegistry(this.scopeRegistry, this.fullMetaReader); @@ -104,7 +105,8 @@ export class DecorationAnalyzer { /* i18nNormalizeLineEndingsInICUs */ false, this.moduleResolver, this.cycleAnalyzer, CycleHandlingStrategy.UseRemoteScoping, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER, NOOP_DEPENDENCY_TRACKER, this.injectableRegistry, - /* semanticDepGraphUpdater */ null, !!this.compilerOptions.annotateForClosureCompiler), + /* semanticDepGraphUpdater */ null, !!this.compilerOptions.annotateForClosureCompiler, + NOOP_PERF_RECORDER), // See the note in ngtsc about why this cast is needed. // clang-format off @@ -117,23 +119,26 @@ export class DecorationAnalyzer { // version 10, undecorated classes that use Angular features are no longer handled // in ngtsc, but we want to ensure compatibility in ngcc for outdated libraries that // have not migrated to explicit decorators. See: https://hackmd.io/@alx/ryfYYuvzH. - /* compileUndecoratedClassesWithAngularFeatures */ true + /* compileUndecoratedClassesWithAngularFeatures */ true, + NOOP_PERF_RECORDER ) as DecoratorHandler, // clang-format on // Pipe handler must be before injectable handler in list so pipe factories are printed // before injectable factories (so injectable factories can delegate to them) new PipeDecoratorHandler( this.reflectionHost, this.evaluator, this.metaRegistry, this.scopeRegistry, - NOOP_DEFAULT_IMPORT_RECORDER, this.injectableRegistry, this.isCore), + NOOP_DEFAULT_IMPORT_RECORDER, this.injectableRegistry, this.isCore, NOOP_PERF_RECORDER), new InjectableDecoratorHandler( this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, - /* strictCtorDeps */ false, this.injectableRegistry, /* errorOnDuplicateProv */ false), + /* strictCtorDeps */ false, this.injectableRegistry, NOOP_PERF_RECORDER, + /* errorOnDuplicateProv */ false), new NgModuleDecoratorHandler( this.reflectionHost, this.evaluator, this.fullMetaReader, this.fullRegistry, this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null, this.refEmitter, /* factoryTracker */ null, NOOP_DEFAULT_IMPORT_RECORDER, - !!this.compilerOptions.annotateForClosureCompiler, this.injectableRegistry), + !!this.compilerOptions.annotateForClosureCompiler, this.injectableRegistry, + NOOP_PERF_RECORDER), ]; compiler = new NgccTraitCompiler(this.handlers, this.reflectionHost); migrations: Migration[] = [ diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel index 002755aad5..b10a14584e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -18,6 +18,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/indexer", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//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/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index de37e283d2..597d70d3d0 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -18,6 +18,7 @@ import {extractSemanticTypeParameters, isArrayEqual, isReferenceEqual, SemanticD import {IndexingContext} from '../../indexer'; import {ClassPropertyMapping, ComponentResources, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, Resource, ResourceRegistry} from '../../metadata'; import {EnumValue, PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; +import {PerfEvent, PerfRecorder} from '../../perf'; import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {ComponentScopeReader, LocalModuleScopeRegistry, TypeCheckScopeRegistry} from '../../scope'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform'; @@ -208,7 +209,7 @@ export class ComponentDecoratorHandler implements private depTracker: DependencyTracker|null, private injectableRegistry: InjectableClassRegistry, private semanticDepGraphUpdater: SemanticDepGraphUpdater|null, - private annotateForClosureCompiler: boolean) {} + private annotateForClosureCompiler: boolean, private perf: PerfRecorder) {} private literalCache = new Map(); private elementSchemaRegistry = new DomElementSchemaRegistry(); @@ -309,6 +310,7 @@ export class ComponentDecoratorHandler implements analyze( node: ClassDeclaration, decorator: Readonly, flags: HandlerFlags = HandlerFlags.NONE): AnalysisOutput { + this.perf.eventCount(PerfEvent.AnalyzeComponent); const containingFile = node.getSourceFile().fileName; this.literalCache.delete(decorator); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index fd03d1b92e..86cc55d0d3 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -16,6 +16,7 @@ import {areTypeParametersEqual, extractSemanticTypeParameters, isArrayEqual, isS import {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, TemplateGuardMeta} from '../../metadata'; import {extractDirectiveTypeCheckMeta} from '../../metadata/src/util'; import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator'; +import {PerfEvent, PerfRecorder} from '../../perf'; import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {LocalModuleScopeRegistry} from '../../scope'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform'; @@ -180,7 +181,7 @@ export class DirectiveDecoratorHandler implements private injectableRegistry: InjectableClassRegistry, private isCore: boolean, private semanticDepGraphUpdater: SemanticDepGraphUpdater|null, private annotateForClosureCompiler: boolean, - private compileUndecoratedClassesWithAngularFeatures: boolean) {} + private compileUndecoratedClassesWithAngularFeatures: boolean, private perf: PerfRecorder) {} readonly precedence = HandlerPrecedence.PRIMARY; readonly name = DirectiveDecoratorHandler.name; @@ -211,6 +212,8 @@ export class DirectiveDecoratorHandler implements return {diagnostics: [getUndecoratedClassWithAngularFeaturesDiagnostic(node)]}; } + this.perf.eventCount(PerfEvent.AnalyzeDirective); + const directiveResult = extractDirectiveMetadata( node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore, flags, this.annotateForClosureCompiler); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index 4e689a6961..0f420934e0 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -12,6 +12,7 @@ import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {DefaultImportRecorder} from '../../imports'; import {InjectableClassRegistry} from '../../metadata'; +import {PerfEvent, PerfRecorder} from '../../perf'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; @@ -34,7 +35,7 @@ export class InjectableDecoratorHandler implements constructor( private reflector: ReflectionHost, private defaultImportRecorder: DefaultImportRecorder, private isCore: boolean, private strictCtorDeps: boolean, - private injectableRegistry: InjectableClassRegistry, + private injectableRegistry: InjectableClassRegistry, private perf: PerfRecorder, /** * What to do if the injectable already contains a ɵprov property. * @@ -64,6 +65,8 @@ export class InjectableDecoratorHandler implements analyze(node: ClassDeclaration, decorator: Readonly): AnalysisOutput { + this.perf.eventCount(PerfEvent.AnalyzeInjectable); + const meta = extractInjectableMetadata(node, decorator, this.reflector); const decorators = this.reflector.getDecoratorsOfDeclaration(node); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 52aac200bb..ac8f3562cc 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -14,6 +14,7 @@ import {DefaultImportRecorder, Reference, ReferenceEmitter} from '../../imports' import {isArrayEqual, isReferenceEqual, isSymbolEqual, SemanticReference, SemanticSymbol} from '../../incremental/semantic_graph'; import {InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata'; import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; +import {PerfEvent, PerfRecorder} from '../../perf'; import {ClassDeclaration, Decorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection'; import {NgModuleRouteAnalyzer} from '../../routing'; import {LocalModuleScopeRegistry, ScopeData} from '../../scope'; @@ -131,7 +132,8 @@ export class NgModuleDecoratorHandler implements private factoryTracker: FactoryTracker|null, private defaultImportRecorder: DefaultImportRecorder, private annotateForClosureCompiler: boolean, - private injectableRegistry: InjectableClassRegistry, private localeId?: string) {} + private injectableRegistry: InjectableClassRegistry, private perf: PerfRecorder, + private localeId?: string) {} readonly precedence = HandlerPrecedence.PRIMARY; readonly name = NgModuleDecoratorHandler.name; @@ -154,6 +156,8 @@ export class NgModuleDecoratorHandler implements analyze(node: ClassDeclaration, decorator: Readonly): AnalysisOutput { + this.perf.eventCount(PerfEvent.AnalyzeNgModule); + const name = node.name.text; if (decorator.args === null || decorator.args.length > 1) { throw new FatalDiagnosticError( diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index a19b5bae59..102aa1bd47 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -14,6 +14,7 @@ import {DefaultImportRecorder, Reference} from '../../imports'; import {SemanticSymbol} from '../../incremental/semantic_graph'; import {InjectableClassRegistry, MetadataRegistry} from '../../metadata'; import {PartialEvaluator} from '../../partial_evaluator'; +import {PerfEvent, PerfRecorder} from '../../perf'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {LocalModuleScopeRegistry} from '../../scope'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform'; @@ -55,7 +56,8 @@ export class PipeDecoratorHandler implements private reflector: ReflectionHost, private evaluator: PartialEvaluator, private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry, private defaultImportRecorder: DefaultImportRecorder, - private injectableRegistry: InjectableClassRegistry, private isCore: boolean) {} + private injectableRegistry: InjectableClassRegistry, private isCore: boolean, + private perf: PerfRecorder) {} readonly precedence = HandlerPrecedence.PRIMARY; readonly name = PipeDecoratorHandler.name; @@ -78,6 +80,8 @@ export class PipeDecoratorHandler implements analyze(clazz: ClassDeclaration, decorator: Readonly): AnalysisOutput { + this.perf.eventCount(PerfEvent.AnalyzePipe); + const name = clazz.name.text; const type = wrapTypeReference(this.reflector, clazz); const internalType = new WrappedNodeExpr(this.reflector.getInternalNameOfClass(clazz)); diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel index ba62f6e73c..88a4280c2e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel @@ -20,6 +20,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/incremental:api", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/testing", diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts index da3f93532d..ccd010802a 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -16,6 +16,7 @@ import {runInEachFileSystem} from '../../file_system/testing'; import {ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; import {CompoundMetadataReader, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, ResourceRegistry} from '../../metadata'; import {PartialEvaluator} from '../../partial_evaluator'; +import {NOOP_PERF_RECORDER} from '../../perf'; import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../scope'; import {getDeclaration, makeProgram} from '../../testing'; @@ -41,7 +42,7 @@ function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.Compil const evaluator = new PartialEvaluator(reflectionHost, checker, /* dependencyTracker */ null); const moduleResolver = new ModuleResolver(program, options, host, /* moduleResolutionCache */ null); - const importGraph = new ImportGraph(checker); + const importGraph = new ImportGraph(checker, NOOP_PERF_RECORDER); const cycleAnalyzer = new CycleAnalyzer(importGraph); const metaRegistry = new LocalMetadataRegistry(); const dtsReader = new DtsMetadataReader(checker, reflectionHost); @@ -80,6 +81,7 @@ function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.Compil injectableRegistry, /* semanticDepGraphUpdater */ null, /* annotateForClosureCompiler */ false, + NOOP_PERF_RECORDER, ); return {reflectionHost, handler}; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts index 294174542f..898a0a28eb 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts @@ -13,6 +13,7 @@ import {runInEachFileSystem} from '../../file_system/testing'; import {NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; import {DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../metadata'; import {PartialEvaluator} from '../../partial_evaluator'; +import {NOOP_PERF_RECORDER} from '../../perf'; import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; import {getDeclaration, makeProgram} from '../../testing'; @@ -171,7 +172,7 @@ runInEachFileSystem(() => { NOOP_DEFAULT_IMPORT_RECORDER, injectableRegistry, /*isCore*/ false, /*semanticDepGraphUpdater*/ null, /*annotateForClosureCompiler*/ false, - /*detectUndecoratedClassesWithAngularFeatures*/ false); + /*detectUndecoratedClassesWithAngularFeatures*/ false, NOOP_PERF_RECORDER); const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration); diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/injectable_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/injectable_spec.ts index eab3b82aa6..293fd25533 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/injectable_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/injectable_spec.ts @@ -10,6 +10,7 @@ import {absoluteFrom} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../imports'; import {InjectableClassRegistry} from '../../metadata'; +import {NOOP_PERF_RECORDER} from '../../perf'; import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing'; import {InjectableDecoratorHandler} from '../src/injectable'; @@ -70,7 +71,7 @@ function setupHandler(errorOnDuplicateProv: boolean) { const injectableRegistry = new InjectableClassRegistry(reflectionHost); const handler = new InjectableDecoratorHandler( reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, /* isCore */ false, - /* strictCtorDeps */ false, injectableRegistry, errorOnDuplicateProv); + /* strictCtorDeps */ false, injectableRegistry, NOOP_PERF_RECORDER, errorOnDuplicateProv); const TestClass = getDeclaration(program, ENTRY_FILE, 'TestClass', isNamedClassDeclaration); const ɵprov = reflectionHost.getMembersOfClass(TestClass).find(member => member.name === 'ɵprov'); if (ɵprov === undefined) { diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts index ff621536bb..5e0387b1db 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts @@ -14,6 +14,7 @@ import {runInEachFileSystem} from '../../file_system/testing'; import {LocalIdentifierStrategy, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; import {CompoundMetadataReader, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../metadata'; import {PartialEvaluator} from '../../partial_evaluator'; +import {NOOP_PERF_RECORDER} from '../../perf'; import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; import {getDeclaration, makeProgram} from '../../testing'; @@ -71,7 +72,8 @@ runInEachFileSystem(() => { const handler = new NgModuleDecoratorHandler( reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry, /* isCore */ false, /* routeAnalyzer */ null, refEmitter, /* factoryTracker */ null, - NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false, injectableRegistry); + NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false, injectableRegistry, + NOOP_PERF_RECORDER); const TestModule = getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); const detected = diff --git a/packages/compiler-cli/src/ngtsc/core/api/src/options.ts b/packages/compiler-cli/src/ngtsc/core/api/src/options.ts index fc86d664f1..661221d87a 100644 --- a/packages/compiler-cli/src/ngtsc/core/api/src/options.ts +++ b/packages/compiler-cli/src/ngtsc/core/api/src/options.ts @@ -28,9 +28,8 @@ export interface TestOnlyOptions { /** * 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 should be a path to a JSON file where trace information will be written. This is sensitive + * to the compiler's working directory, and should likely be an absolute path. * * This is currently not exposed to users as the trace format is still unstable. */ diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index 32cedf2641..f99ceb9ebb 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -21,7 +21,9 @@ import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, ResourceRegistry} from '../../metadata'; import {ModuleWithProvidersScanner} from '../../modulewithproviders'; import {PartialEvaluator} from '../../partial_evaluator'; -import {NOOP_PERF_RECORDER, PerfRecorder} from '../../perf'; +import {ActivePerfRecorder} from '../../perf'; +import {PerfCheckpoint, PerfEvent, PerfPhase} from '../../perf/src/api'; +import {DelegatingPerfRecorder} from '../../perf/src/recorder'; import {DeclarationNode, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {AdapterResourceLoader} from '../../resource'; import {entryPointKeyFor, NgModuleRouteAnalyzer} from '../../routing'; @@ -80,6 +82,7 @@ export interface FreshCompilationTicket { enableTemplateTypeChecker: boolean; usePoisonedData: boolean; tsProgram: ts.Program; + perfRecorder: ActivePerfRecorder; } /** @@ -95,12 +98,14 @@ export interface IncrementalTypeScriptCompilationTicket { newDriver: IncrementalDriver; enableTemplateTypeChecker: boolean; usePoisonedData: boolean; + perfRecorder: ActivePerfRecorder; } export interface IncrementalResourceCompilationTicket { kind: CompilationTicketKind.IncrementalResource; compiler: NgCompiler; modifiedResourceFiles: Set; + perfRecorder: ActivePerfRecorder; } /** @@ -119,8 +124,8 @@ export type CompilationTicket = FreshCompilationTicket|IncrementalTypeScriptComp export function freshCompilationTicket( tsProgram: ts.Program, options: NgCompilerOptions, incrementalBuildStrategy: IncrementalBuildStrategy, - typeCheckingProgramStrategy: TypeCheckingProgramStrategy, enableTemplateTypeChecker: boolean, - usePoisonedData: boolean): CompilationTicket { + typeCheckingProgramStrategy: TypeCheckingProgramStrategy, perfRecorder: ActivePerfRecorder|null, + enableTemplateTypeChecker: boolean, usePoisonedData: boolean): CompilationTicket { return { kind: CompilationTicketKind.Fresh, tsProgram, @@ -129,6 +134,7 @@ export function freshCompilationTicket( typeCheckingProgramStrategy, enableTemplateTypeChecker, usePoisonedData, + perfRecorder: perfRecorder ?? ActivePerfRecorder.zeroedToNow(), }; } @@ -139,8 +145,8 @@ export function freshCompilationTicket( export function incrementalFromCompilerTicket( oldCompiler: NgCompiler, newProgram: ts.Program, incrementalBuildStrategy: IncrementalBuildStrategy, - typeCheckingProgramStrategy: TypeCheckingProgramStrategy, - modifiedResourceFiles: Set): CompilationTicket { + typeCheckingProgramStrategy: TypeCheckingProgramStrategy, modifiedResourceFiles: Set, + perfRecorder: ActivePerfRecorder|null): CompilationTicket { const oldProgram = oldCompiler.getNextProgram(); const oldDriver = oldCompiler.incrementalStrategy.getIncrementalDriver(oldProgram); if (oldDriver === null) { @@ -148,11 +154,15 @@ export function incrementalFromCompilerTicket( // program. return freshCompilationTicket( newProgram, oldCompiler.options, incrementalBuildStrategy, typeCheckingProgramStrategy, - oldCompiler.enableTemplateTypeChecker, oldCompiler.usePoisonedData); + perfRecorder, oldCompiler.enableTemplateTypeChecker, oldCompiler.usePoisonedData); } - const newDriver = - IncrementalDriver.reconcile(oldProgram, oldDriver, newProgram, modifiedResourceFiles); + if (perfRecorder === null) { + perfRecorder = ActivePerfRecorder.zeroedToNow(); + } + + const newDriver = IncrementalDriver.reconcile( + oldProgram, oldDriver, newProgram, modifiedResourceFiles, perfRecorder); return { kind: CompilationTicketKind.IncrementalTypeScript, @@ -164,6 +174,7 @@ export function incrementalFromCompilerTicket( newDriver, oldProgram, newProgram, + perfRecorder, }; } @@ -175,9 +186,14 @@ export function incrementalFromDriverTicket( oldProgram: ts.Program, oldDriver: IncrementalDriver, newProgram: ts.Program, options: NgCompilerOptions, incrementalBuildStrategy: IncrementalBuildStrategy, typeCheckingProgramStrategy: TypeCheckingProgramStrategy, modifiedResourceFiles: Set, - enableTemplateTypeChecker: boolean, usePoisonedData: boolean): CompilationTicket { - const newDriver = - IncrementalDriver.reconcile(oldProgram, oldDriver, newProgram, modifiedResourceFiles); + perfRecorder: ActivePerfRecorder|null, enableTemplateTypeChecker: boolean, + usePoisonedData: boolean): CompilationTicket { + if (perfRecorder === null) { + perfRecorder = ActivePerfRecorder.zeroedToNow(); + } + + const newDriver = IncrementalDriver.reconcile( + oldProgram, oldDriver, newProgram, modifiedResourceFiles, perfRecorder); return { kind: CompilationTicketKind.IncrementalTypeScript, oldProgram, @@ -188,6 +204,7 @@ export function incrementalFromDriverTicket( typeCheckingProgramStrategy, enableTemplateTypeChecker, usePoisonedData, + perfRecorder, }; } @@ -197,6 +214,7 @@ export function resourceChangeTicket(compiler: NgCompiler, modifiedResourceFiles kind: CompilationTicketKind.IncrementalResource, compiler, modifiedResourceFiles, + perfRecorder: ActivePerfRecorder.zeroedToNow(), }; } @@ -245,16 +263,23 @@ export class NgCompiler { readonly ignoreForDiagnostics: Set; readonly ignoreForEmit: Set; + /** + * `NgCompiler` can be reused for multiple compilations (for resource-only changes), and each + * new compilation uses a fresh `PerfRecorder`. Thus, classes created with a lifespan of the + * `NgCompiler` use a `DelegatingPerfRecorder` so the `PerfRecorder` they write to can be updated + * with each fresh compilation. + */ + private delegatingPerfRecorder = new DelegatingPerfRecorder(this.perfRecorder); + /** * Convert a `CompilationTicket` into an `NgCompiler` instance for the requested compilation. * * Depending on the nature of the compilation request, the `NgCompiler` instance may be reused * from a previous compilation and updated with any changes, it may be a new instance which - * incrementally reuses state from a previous compilation, or it may represent a fresh compilation - * entirely. + * incrementally reuses state from a previous compilation, or it may represent a fresh + * compilation entirely. */ - static fromTicket( - ticket: CompilationTicket, adapter: NgCompilerAdapter, perfRecorder?: PerfRecorder) { + static fromTicket(ticket: CompilationTicket, adapter: NgCompilerAdapter) { switch (ticket.kind) { case CompilationTicketKind.Fresh: return new NgCompiler( @@ -266,7 +291,7 @@ export class NgCompiler { IncrementalDriver.fresh(ticket.tsProgram), ticket.enableTemplateTypeChecker, ticket.usePoisonedData, - perfRecorder, + ticket.perfRecorder, ); case CompilationTicketKind.IncrementalTypeScript: return new NgCompiler( @@ -278,11 +303,11 @@ export class NgCompiler { ticket.newDriver, ticket.enableTemplateTypeChecker, ticket.usePoisonedData, - perfRecorder, + ticket.perfRecorder, ); case CompilationTicketKind.IncrementalResource: const compiler = ticket.compiler; - compiler.updateWithChangedResources(ticket.modifiedResourceFiles); + compiler.updateWithChangedResources(ticket.modifiedResourceFiles, ticket.perfRecorder); return compiler; } } @@ -296,7 +321,7 @@ export class NgCompiler { readonly incrementalDriver: IncrementalDriver, readonly enableTemplateTypeChecker: boolean, readonly usePoisonedData: boolean, - private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER, + private livePerfRecorder: ActivePerfRecorder, ) { this.constructionDiagnostics.push(...this.adapter.constructionDiagnostics); const incompatibleTypeCheckOptionsDiagnostic = verifyCompatibleTypeCheckOptions(this.options); @@ -312,9 +337,7 @@ export class NgCompiler { const moduleResolutionCache = ts.createModuleResolutionCache( this.adapter.getCurrentDirectory(), - // Note: this used to be an arrow-function closure. However, JS engines like v8 have some - // strange behaviors with retaining the lexical scope of the closure. Even if this function - // doesn't retain a reference to `this`, if other closures in the constructor here reference + // doen't retain a reference to `this`, if other closures in the constructor here reference // `this` internally then a closure created here would retain them. This can cause major // memory leak issues since the `moduleResolutionCache` is a long-lived object and finds its // way into all kinds of places inside TS internal objects. @@ -322,42 +345,66 @@ export class NgCompiler { this.moduleResolver = new ModuleResolver(tsProgram, this.options, this.adapter, moduleResolutionCache); this.resourceManager = new AdapterResourceLoader(adapter, this.options); - this.cycleAnalyzer = new CycleAnalyzer(new ImportGraph(tsProgram.getTypeChecker())); + this.cycleAnalyzer = + new CycleAnalyzer(new ImportGraph(tsProgram.getTypeChecker(), this.delegatingPerfRecorder)); this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, tsProgram); this.ignoreForDiagnostics = new Set(tsProgram.getSourceFiles().filter(sf => this.adapter.isShim(sf))); this.ignoreForEmit = this.adapter.ignoreForEmit; + + let dtsFileCount = 0; + let nonDtsFileCount = 0; + for (const sf of tsProgram.getSourceFiles()) { + if (sf.isDeclarationFile) { + dtsFileCount++; + } else { + nonDtsFileCount++; + } + } + + livePerfRecorder.eventCount(PerfEvent.InputDtsFile, dtsFileCount); + livePerfRecorder.eventCount(PerfEvent.InputTsFile, nonDtsFileCount); } - private updateWithChangedResources(changedResources: Set): void { - if (this.compilation === null) { - // Analysis hasn't happened yet, so no update is necessary - any changes to resources will be - // captured by the inital analysis pass itself. - return; - } + get perfRecorder(): ActivePerfRecorder { + return this.livePerfRecorder; + } - this.resourceManager.invalidate(); + private updateWithChangedResources( + changedResources: Set, perfRecorder: ActivePerfRecorder): void { + this.livePerfRecorder = perfRecorder; + this.delegatingPerfRecorder.target = perfRecorder; - const classesToUpdate = new Set(); - for (const resourceFile of changedResources) { - for (const templateClass of this.getComponentsWithTemplateFile(resourceFile)) { - classesToUpdate.add(templateClass); + perfRecorder.inPhase(PerfPhase.ResourceUpdate, () => { + if (this.compilation === null) { + // Analysis hasn't happened yet, so no update is necessary - any changes to resources will + // be captured by the inital analysis pass itself. + return; } - for (const styleClass of this.getComponentsWithStyleFile(resourceFile)) { - classesToUpdate.add(styleClass); - } - } + this.resourceManager.invalidate(); - for (const clazz of classesToUpdate) { - this.compilation.traitCompiler.updateResources(clazz); - if (!ts.isClassDeclaration(clazz)) { - continue; + const classesToUpdate = new Set(); + for (const resourceFile of changedResources) { + for (const templateClass of this.getComponentsWithTemplateFile(resourceFile)) { + classesToUpdate.add(templateClass); + } + + for (const styleClass of this.getComponentsWithStyleFile(resourceFile)) { + classesToUpdate.add(styleClass); + } } - this.compilation.templateTypeChecker.invalidateClass(clazz); - } + for (const clazz of classesToUpdate) { + this.compilation.traitCompiler.updateResources(clazz); + if (!ts.isClassDeclaration(clazz)) { + continue; + } + + this.compilation.templateTypeChecker.invalidateClass(clazz); + } + }); } /** @@ -481,33 +528,28 @@ export class NgCompiler { if (this.compilation !== null) { return; } - this.compilation = this.makeCompilation(); - const analyzeSpan = this.perfRecorder.start('analyze'); - const promises: Promise[] = []; - for (const sf of this.tsProgram.getSourceFiles()) { - if (sf.isDeclarationFile) { - continue; + await this.perfRecorder.inPhase(PerfPhase.Analysis, async () => { + this.compilation = this.makeCompilation(); + + const promises: Promise[] = []; + for (const sf of this.tsProgram.getSourceFiles()) { + if (sf.isDeclarationFile) { + continue; + } + + let analysisPromise = this.compilation.traitCompiler.analyzeAsync(sf); + this.scanForMwp(sf); + if (analysisPromise !== undefined) { + promises.push(analysisPromise); + } } - const analyzeFileSpan = this.perfRecorder.start('analyzeFile', sf); - let analysisPromise = this.compilation.traitCompiler.analyzeAsync(sf); - this.scanForMwp(sf); - if (analysisPromise === undefined) { - this.perfRecorder.stop(analyzeFileSpan); - } else if (this.perfRecorder.enabled) { - analysisPromise = analysisPromise.then(() => this.perfRecorder.stop(analyzeFileSpan)); - } - if (analysisPromise !== undefined) { - promises.push(analysisPromise); - } - } + await Promise.all(promises); - await Promise.all(promises); - - this.perfRecorder.stop(analyzeSpan); - - this.resolveCompilation(this.compilation.traitCompiler); + this.perfRecorder.memory(PerfCheckpoint.Analysis); + this.resolveCompilation(this.compilation.traitCompiler); + }); } /** @@ -517,9 +559,7 @@ export class NgCompiler { */ listLazyRoutes(entryRoute?: string): LazyRoute[] { if (entryRoute) { - // Note: - // This resolution step is here to match the implementation of the old `AotCompilerHost` (see - // https://github.com/angular/angular/blob/50732e156/packages/compiler-cli/src/transformers/compiler_host.ts#L175-L188). + // htts://github.com/angular/angular/blob/50732e156/packages/compiler-cli/src/transformers/compiler_host.ts#L175-L188). // // `@angular/cli` will always call this API with an absolute path, so the resolution step is // not necessary, but keeping it backwards compatible in case someone else is using the API. @@ -573,7 +613,8 @@ export class NgCompiler { const before = [ ivyTransformFactory( compilation.traitCompiler, compilation.reflector, importRewriter, - compilation.defaultImportTracker, compilation.isCore, this.closureCompilerEnabled), + compilation.defaultImportTracker, this.delegatingPerfRecorder, compilation.isCore, + this.closureCompilerEnabled), aliasTransformFactory(compilation.traitCompiler.exportStatements), compilation.defaultImportTracker.importPreservingTransformer(), ]; @@ -618,28 +659,32 @@ export class NgCompiler { } private analyzeSync(): void { - const analyzeSpan = this.perfRecorder.start('analyze'); - this.compilation = this.makeCompilation(); - for (const sf of this.tsProgram.getSourceFiles()) { - if (sf.isDeclarationFile) { - continue; + this.perfRecorder.inPhase(PerfPhase.Analysis, () => { + this.compilation = this.makeCompilation(); + for (const sf of this.tsProgram.getSourceFiles()) { + if (sf.isDeclarationFile) { + continue; + } + this.compilation.traitCompiler.analyzeSync(sf); + this.scanForMwp(sf); } - const analyzeFileSpan = this.perfRecorder.start('analyzeFile', sf); - this.compilation.traitCompiler.analyzeSync(sf); - this.scanForMwp(sf); - this.perfRecorder.stop(analyzeFileSpan); - } - this.perfRecorder.stop(analyzeSpan); - this.resolveCompilation(this.compilation.traitCompiler); + this.perfRecorder.memory(PerfCheckpoint.Analysis); + + this.resolveCompilation(this.compilation.traitCompiler); + }); } private resolveCompilation(traitCompiler: TraitCompiler): void { - traitCompiler.resolve(); + this.perfRecorder.inPhase(PerfPhase.Resolve, () => { + traitCompiler.resolve(); - // At this point, analysis is complete and the compiler can now calculate which files need to - // be emitted, so do that. - this.incrementalDriver.recordSuccessfulAnalysis(traitCompiler); + // At this point, analysis is complete and the compiler can now calculate which files need to + // be emitted, so do that. + this.incrementalDriver.recordSuccessfulAnalysis(traitCompiler); + + this.perfRecorder.memory(PerfCheckpoint.Resolve); + }); } private get fullTemplateTypeCheck(): boolean { @@ -770,7 +815,6 @@ export class NgCompiler { const compilation = this.ensureAnalyzed(); // Get the diagnostics. - const typeCheckSpan = this.perfRecorder.start('typeCheckDiagnostics'); const diagnostics: ts.Diagnostic[] = []; for (const sf of this.tsProgram.getSourceFiles()) { if (sf.isDeclarationFile || this.adapter.isShim(sf)) { @@ -782,7 +826,6 @@ export class NgCompiler { } const program = this.typeCheckingProgramStrategy.getProgram(); - this.perfRecorder.stop(typeCheckSpan); this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, program); this.nextProgram = program; @@ -794,14 +837,12 @@ export class NgCompiler { const compilation = this.ensureAnalyzed(); // Get the diagnostics. - const typeCheckSpan = this.perfRecorder.start('typeCheckDiagnostics'); const diagnostics: ts.Diagnostic[] = []; if (!sf.isDeclarationFile && !this.adapter.isShim(sf)) { diagnostics.push(...compilation.templateTypeChecker.getDiagnosticsForFile(sf, optimizeFor)); } const program = this.typeCheckingProgramStrategy.getProgram(); - this.perfRecorder.stop(typeCheckSpan); this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, program); this.nextProgram = program; @@ -950,7 +991,8 @@ export class NgCompiler { this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData, this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer, cycleHandlingStrategy, refEmitter, defaultImportTracker, this.incrementalDriver.depGraph, - injectableRegistry, semanticDepGraphUpdater, this.closureCompilerEnabled), + injectableRegistry, semanticDepGraphUpdater, this.closureCompilerEnabled, + this.delegatingPerfRecorder), // TODO(alxhub): understand why the cast here is necessary (something to do with `null` // not being assignable to `unknown` when wrapped in `Readonly`). @@ -959,31 +1001,33 @@ export class NgCompiler { reflector, evaluator, metaRegistry, scopeRegistry, metaReader, defaultImportTracker, injectableRegistry, isCore, semanticDepGraphUpdater, this.closureCompilerEnabled, compileUndecoratedClassesWithAngularFeatures, + this.delegatingPerfRecorder, ) as Readonly>, // clang-format on // Pipe handler must be before injectable handler in list so pipe factories are printed // before injectable factories (so injectable factories can delegate to them) new PipeDecoratorHandler( reflector, evaluator, metaRegistry, scopeRegistry, defaultImportTracker, - injectableRegistry, isCore), + injectableRegistry, isCore, this.delegatingPerfRecorder), new InjectableDecoratorHandler( reflector, defaultImportTracker, isCore, this.options.strictInjectionParameters || false, - injectableRegistry), + injectableRegistry, this.delegatingPerfRecorder), new NgModuleDecoratorHandler( reflector, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry, isCore, routeAnalyzer, refEmitter, this.adapter.factoryTracker, defaultImportTracker, - this.closureCompilerEnabled, injectableRegistry, this.options.i18nInLocale), + this.closureCompilerEnabled, injectableRegistry, this.delegatingPerfRecorder, + this.options.i18nInLocale), ]; const traitCompiler = new TraitCompiler( - handlers, reflector, this.perfRecorder, this.incrementalDriver, + handlers, reflector, this.delegatingPerfRecorder, this.incrementalDriver, this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms, semanticDepGraphUpdater); const templateTypeChecker = new TemplateTypeCheckerImpl( this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler, this.getTypeCheckingConfig(), refEmitter, reflector, this.adapter, this.incrementalDriver, - scopeRegistry, typeCheckScopeRegistry); + scopeRegistry, typeCheckScopeRegistry, this.delegatingPerfRecorder); return { isCore, diff --git a/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts b/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts index 785499527d..e7f59a2103 100644 --- a/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts +++ b/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts @@ -25,8 +25,8 @@ function makeFreshCompiler( programStrategy: TypeCheckingProgramStrategy, incrementalStrategy: IncrementalBuildStrategy, enableTemplateTypeChecker: boolean, usePoisonedData: boolean): NgCompiler { const ticket = freshCompilationTicket( - program, options, incrementalStrategy, programStrategy, enableTemplateTypeChecker, - usePoisonedData); + program, options, incrementalStrategy, programStrategy, /* perfRecorder */ null, + enableTemplateTypeChecker, usePoisonedData); return NgCompiler.fromTicket(ticket, host); } diff --git a/packages/compiler-cli/src/ngtsc/cycles/BUILD.bazel b/packages/compiler-cli/src/ngtsc/cycles/BUILD.bazel index 78a4cbf391..b17665f899 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/cycles/BUILD.bazel @@ -9,6 +9,7 @@ ts_library( ]), module_name = "@angular/compiler-cli/src/ngtsc/cycles", deps = [ + "//packages/compiler-cli/src/ngtsc/perf", "@npm//typescript", ], ) diff --git a/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts b/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts index 1696cab7a2..a04f044965 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts @@ -8,6 +8,8 @@ import * as ts from 'typescript'; +import {PerfPhase, PerfRecorder} from '../../perf'; + /** * A cached graph of imports in the `ts.Program`. * @@ -17,7 +19,7 @@ import * as ts from 'typescript'; export class ImportGraph { private map = new Map>(); - constructor(private checker: ts.TypeChecker) {} + constructor(private checker: ts.TypeChecker, private perf: PerfRecorder) {} /** * List the direct (not transitive) imports of a given `ts.SourceFile`. @@ -99,26 +101,28 @@ export class ImportGraph { } private scanImports(sf: ts.SourceFile): Set { - const imports = new Set(); - // Look through the source file for import and export statements. - for (const stmt of sf.statements) { - if ((!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) || - stmt.moduleSpecifier === undefined) { - continue; - } + return this.perf.inPhase(PerfPhase.CycleDetection, () => { + const imports = new Set(); + // Look through the source file for import and export statements. + for (const stmt of sf.statements) { + if ((!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) || + stmt.moduleSpecifier === undefined) { + continue; + } - const symbol = this.checker.getSymbolAtLocation(stmt.moduleSpecifier); - if (symbol === undefined || symbol.valueDeclaration === undefined) { - // No symbol could be found to skip over this import/export. - continue; + const symbol = this.checker.getSymbolAtLocation(stmt.moduleSpecifier); + if (symbol === undefined || symbol.valueDeclaration === undefined) { + // No symbol could be found to skip over this import/export. + continue; + } + const moduleFile = symbol.valueDeclaration; + if (ts.isSourceFile(moduleFile) && isLocalFile(moduleFile)) { + // Record this local import. + imports.add(moduleFile); + } } - const moduleFile = symbol.valueDeclaration; - if (ts.isSourceFile(moduleFile) && isLocalFile(moduleFile)) { - // Record this local import. - imports.add(moduleFile); - } - } - return imports; + return imports; + }); } } diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/cycles/test/BUILD.bazel index 8ba04d7446..21b3aaae06 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/cycles/test/BUILD.bazel @@ -13,6 +13,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system/testing", + "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/testing", "@npm//typescript", ], diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts b/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts index 388a1d9b6b..85b058dd8f 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts @@ -8,6 +8,7 @@ import * as ts from 'typescript'; import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; +import {NOOP_PERF_RECORDER} from '../../perf'; import {Cycle, CycleAnalyzer} from '../src/analyzer'; import {ImportGraph} from '../src/imports'; import {importPath, makeProgramFromGraph} from './util'; @@ -75,7 +76,7 @@ runInEachFileSystem(() => { const {program} = makeProgramFromGraph(getFileSystem(), graph); return { program, - analyzer: new CycleAnalyzer(new ImportGraph(program.getTypeChecker())), + analyzer: new CycleAnalyzer(new ImportGraph(program.getTypeChecker(), NOOP_PERF_RECORDER)), }; } }); diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts b/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts index e4713f44f6..f88500643d 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts @@ -8,6 +8,7 @@ import * as ts from 'typescript'; import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; +import {NOOP_PERF_RECORDER} from '../../perf'; import {ImportGraph} from '../src/imports'; import {importPath, makeProgramFromGraph} from './util'; @@ -86,7 +87,7 @@ runInEachFileSystem(() => { const {program} = makeProgramFromGraph(getFileSystem(), graph); return { program, - graph: new ImportGraph(program.getTypeChecker()), + graph: new ImportGraph(program.getTypeChecker(), NOOP_PERF_RECORDER), }; } diff --git a/packages/compiler-cli/src/ngtsc/incremental/BUILD.bazel b/packages/compiler-cli/src/ngtsc/incremental/BUILD.bazel index 4b690eb29d..0e89dfbed2 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/incremental/BUILD.bazel @@ -14,6 +14,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/incremental/semantic_graph", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/partial_evaluator", + "//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/src/ngtsc/incremental/src/state.ts b/packages/compiler-cli/src/ngtsc/incremental/src/state.ts index d247aeaa6d..cd5f5fa431 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/src/state.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/src/state.ts @@ -9,6 +9,7 @@ import * as ts from 'typescript'; import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; +import {PerfEvent, PerfPhase, PerfRecorder} from '../../perf'; import {ClassDeclaration} from '../../reflection'; import {ClassRecord, TraitCompiler} from '../../transform'; import {FileTypeCheckingData} from '../../typecheck/src/checker'; @@ -43,118 +44,122 @@ export class IncrementalDriver implements IncrementalBuild|null): IncrementalDriver { - // Initialize the state of the current build based on the previous one. - let state: PendingBuildState; - if (oldDriver.state.kind === BuildStateKind.Pending) { - // The previous build never made it past the pending state. Reuse it as the starting state for - // this build. - state = oldDriver.state; - } else { - let priorGraph: SemanticDepGraph|null = null; - if (oldDriver.state.lastGood !== null) { - priorGraph = oldDriver.state.lastGood.semanticDepGraph; - } - - // The previous build was successfully analyzed. `pendingEmit` is the only state carried - // forward into this build. - state = { - kind: BuildStateKind.Pending, - pendingEmit: oldDriver.state.pendingEmit, - pendingTypeCheckEmit: oldDriver.state.pendingTypeCheckEmit, - changedResourcePaths: new Set(), - changedTsPaths: new Set(), - lastGood: oldDriver.state.lastGood, - semanticDepGraphUpdater: new SemanticDepGraphUpdater(priorGraph), - }; - } - - // Merge the freshly modified resource files with any prior ones. - if (modifiedResourceFiles !== null) { - for (const resFile of modifiedResourceFiles) { - state.changedResourcePaths.add(absoluteFrom(resFile)); - } - } - - // Next, process the files in the new program, with a couple of goals: - // 1) Determine which TS files have changed, if any, and merge them into `changedTsFiles`. - // 2) Produce a list of TS files which no longer exist in the program (they've been deleted - // since the previous compilation). These need to be removed from the state tracking to avoid - // leaking memory. - - // All files in the old program, for easy detection of changes. - const oldFiles = new Set(oldProgram.getSourceFiles()); - - // Assume all the old files were deleted to begin with. Only TS files are tracked. - const deletedTsPaths = new Set(tsOnlyFiles(oldProgram).map(sf => sf.fileName)); - - for (const newFile of newProgram.getSourceFiles()) { - if (!newFile.isDeclarationFile) { - // This file exists in the new program, so remove it from `deletedTsPaths`. - deletedTsPaths.delete(newFile.fileName); - } - - if (oldFiles.has(newFile)) { - // This file hasn't changed; no need to look at it further. - continue; - } - - // The file has changed since the last successful build. The appropriate reaction depends on - // what kind of file it is. - if (!newFile.isDeclarationFile) { - // It's a .ts file, so track it as a change. - state.changedTsPaths.add(newFile.fileName); + modifiedResourceFiles: Set|null, perf: PerfRecorder): IncrementalDriver { + return perf.inPhase(PerfPhase.Reconciliation, () => { + // Initialize the state of the current build based on the previous one. + let state: PendingBuildState; + if (oldDriver.state.kind === BuildStateKind.Pending) { + // The previous build never made it past the pending state. Reuse it as the starting state + // for this build. + state = oldDriver.state; } else { - // It's a .d.ts file. Currently the compiler does not do a great job of tracking - // dependencies on .d.ts files, so bail out of incremental builds here and do a full build. - // This usually only happens if something in node_modules changes. - return IncrementalDriver.fresh(newProgram); - } - } + let priorGraph: SemanticDepGraph|null = null; + if (oldDriver.state.lastGood !== null) { + priorGraph = oldDriver.state.lastGood.semanticDepGraph; + } - // The next step is to remove any deleted files from the state. - for (const filePath of deletedTsPaths) { - state.pendingEmit.delete(filePath); - state.pendingTypeCheckEmit.delete(filePath); - - // Even if the file doesn't exist in the current compilation, it still might have been changed - // in a previous one, so delete it from the set of changed TS files, just in case. - state.changedTsPaths.delete(filePath); - } - - // Now, changedTsPaths contains physically changed TS paths. Use the previous program's logical - // dependency graph to determine logically changed files. - const depGraph = new FileDependencyGraph(); - - // If a previous compilation exists, use its dependency graph to determine the set of logically - // changed files. - let logicalChanges: Set|null = null; - if (state.lastGood !== null) { - // Extract the set of logically changed files. At the same time, this operation populates the - // current (fresh) dependency graph with information about those files which have not - // logically changed. - logicalChanges = depGraph.updateWithPhysicalChanges( - state.lastGood.depGraph, state.changedTsPaths, deletedTsPaths, - state.changedResourcePaths); - for (const fileName of state.changedTsPaths) { - logicalChanges.add(fileName); + // The previous build was successfully analyzed. `pendingEmit` is the only state carried + // forward into this build. + state = { + kind: BuildStateKind.Pending, + pendingEmit: oldDriver.state.pendingEmit, + pendingTypeCheckEmit: oldDriver.state.pendingTypeCheckEmit, + changedResourcePaths: new Set(), + changedTsPaths: new Set(), + lastGood: oldDriver.state.lastGood, + semanticDepGraphUpdater: new SemanticDepGraphUpdater(priorGraph), + }; } - // Any logically changed files need to be re-emitted. Most of the time this would happen - // regardless because the new dependency graph would _also_ identify the file as stale. - // However there are edge cases such as removing a component from an NgModule without adding - // it to another one, where the previous graph identifies the file as logically changed, but - // the new graph (which does not have that edge) fails to identify that the file should be - // re-emitted. - for (const change of logicalChanges) { - state.pendingEmit.add(change); - state.pendingTypeCheckEmit.add(change); + // Merge the freshly modified resource files with any prior ones. + if (modifiedResourceFiles !== null) { + for (const resFile of modifiedResourceFiles) { + state.changedResourcePaths.add(absoluteFrom(resFile)); + } } - } - // `state` now reflects the initial pending state of the current compilation. + // Next, process the files in the new program, with a couple of goals: + // 1) Determine which TS files have changed, if any, and merge them into `changedTsFiles`. + // 2) Produce a list of TS files which no longer exist in the program (they've been deleted + // since the previous compilation). These need to be removed from the state tracking to + // avoid leaking memory. - return new IncrementalDriver(state, depGraph, logicalChanges); + // All files in the old program, for easy detection of changes. + const oldFiles = new Set(oldProgram.getSourceFiles()); + + // Assume all the old files were deleted to begin with. Only TS files are tracked. + const deletedTsPaths = new Set(tsOnlyFiles(oldProgram).map(sf => sf.fileName)); + + for (const newFile of newProgram.getSourceFiles()) { + if (!newFile.isDeclarationFile) { + // This file exists in the new program, so remove it from `deletedTsPaths`. + deletedTsPaths.delete(newFile.fileName); + } + + if (oldFiles.has(newFile)) { + // This file hasn't changed; no need to look at it further. + continue; + } + + // The file has changed since the last successful build. The appropriate reaction depends on + // what kind of file it is. + if (!newFile.isDeclarationFile) { + // It's a .ts file, so track it as a change. + state.changedTsPaths.add(newFile.fileName); + } else { + // It's a .d.ts file. Currently the compiler does not do a great job of tracking + // dependencies on .d.ts files, so bail out of incremental builds here and do a full + // build. This usually only happens if something in node_modules changes. + return IncrementalDriver.fresh(newProgram); + } + } + + // The next step is to remove any deleted files from the state. + for (const filePath of deletedTsPaths) { + state.pendingEmit.delete(filePath); + state.pendingTypeCheckEmit.delete(filePath); + + // Even if the file doesn't exist in the current compilation, it still might have been + // changed in a previous one, so delete it from the set of changed TS files, just in case. + state.changedTsPaths.delete(filePath); + } + + perf.eventCount(PerfEvent.SourceFilePhysicalChange, state.changedTsPaths.size); + + // Now, changedTsPaths contains physically changed TS paths. Use the previous program's + // logical dependency graph to determine logically changed files. + const depGraph = new FileDependencyGraph(); + + // If a previous compilation exists, use its dependency graph to determine the set of + // logically changed files. + let logicalChanges: Set|null = null; + if (state.lastGood !== null) { + // Extract the set of logically changed files. At the same time, this operation populates + // the current (fresh) dependency graph with information about those files which have not + // logically changed. + logicalChanges = depGraph.updateWithPhysicalChanges( + state.lastGood.depGraph, state.changedTsPaths, deletedTsPaths, + state.changedResourcePaths); + perf.eventCount(PerfEvent.SourceFileLogicalChange, logicalChanges.size); + for (const fileName of state.changedTsPaths) { + logicalChanges.add(fileName); + } + + // Any logically changed files need to be re-emitted. Most of the time this would happen + // regardless because the new dependency graph would _also_ identify the file as stale. + // However there are edge cases such as removing a component from an NgModule without adding + // it to another one, where the previous graph identifies the file as logically changed, but + // the new graph (which does not have that edge) fails to identify that the file should be + // re-emitted. + for (const change of logicalChanges) { + state.pendingEmit.add(change); + state.pendingTypeCheckEmit.add(change); + } + } + + // `state` now reflects the initial pending state of the current compilation. + return new IncrementalDriver(state, depGraph, logicalChanges); + }); } static fresh(program: ts.Program): IncrementalDriver { diff --git a/packages/compiler-cli/src/ngtsc/perf/BUILD.bazel b/packages/compiler-cli/src/ngtsc/perf/BUILD.bazel index f51e6b0a56..53af9b1c4f 100644 --- a/packages/compiler-cli/src/ngtsc/perf/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/perf/BUILD.bazel @@ -9,8 +9,6 @@ ts_library( ]), deps = [ "//packages:types", - "//packages/compiler-cli/src/ngtsc/file_system", - "//packages/compiler-cli/src/ngtsc/reflection", "@npm//@types/node", "@npm//typescript", ], diff --git a/packages/compiler-cli/src/ngtsc/perf/index.ts b/packages/compiler-cli/src/ngtsc/perf/index.ts index 9f5b7ce343..32b9762f9f 100644 --- a/packages/compiler-cli/src/ngtsc/perf/index.ts +++ b/packages/compiler-cli/src/ngtsc/perf/index.ts @@ -6,6 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -export {PerfRecorder} from './src/api'; +export * from './src/api'; export {NOOP_PERF_RECORDER} from './src/noop'; -export {PerfTracker} from './src/tracking'; +export {ActivePerfRecorder, DelegatingPerfRecorder} from './src/recorder'; diff --git a/packages/compiler-cli/src/ngtsc/perf/src/api.ts b/packages/compiler-cli/src/ngtsc/perf/src/api.ts index a717808324..851ada1ba7 100644 --- a/packages/compiler-cli/src/ngtsc/perf/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/perf/src/api.ts @@ -6,12 +6,328 @@ * found in the LICENSE file at https://angular.io/license */ -import {DeclarationNode} from '../../reflection'; +/** + * A phase of compilation for which time is tracked in a distinct bucket. + */ +export enum PerfPhase { + /** + * The "default" phase which tracks time not spent in any other phase. + */ + Unaccounted, -export interface PerfRecorder { - readonly enabled: boolean; + /** + * Time spent setting up the compiler, before a TypeScript program is created. + * + * This includes operations like configuring the `ts.CompilerHost` and any wrappers. + */ + Setup, - mark(name: string, node?: DeclarationNode, category?: string, detail?: string): void; - start(name: string, node?: DeclarationNode, category?: string, detail?: string): number; - stop(span: number): void; + /** + * Time spent in `ts.createProgram`, including reading and parsing `ts.SourceFile`s in the + * `ts.CompilerHost`. + * + * This might be an incremental program creation operation. + */ + TypeScriptProgramCreate, + + /** + * Time spent reconciling the contents of an old `ts.Program` with the new incremental one. + * + * Only present in incremental compilations. + */ + Reconciliation, + + /** + * Time spent updating an `NgCompiler` instance with a resource-only change. + * + * Only present in incremental compilations where the change was resource-only. + */ + ResourceUpdate, + + /** + * Time spent calculating the plain TypeScript diagnostics (structural and semantic). + */ + TypeScriptDiagnostics, + + /** + * Time spent in Angular analysis of individual classes in the program. + */ + Analysis, + + /** + * Time spent in Angular global analysis (synthesis of analysis information into a complete + * understanding of the program). + */ + Resolve, + + /** + * Time spent building the import graph of the program in order to perform cycle detection. + */ + CycleDetection, + + /** + * Time spent generating the text of Type Check Blocks in order to perform template type checking. + */ + TcbGeneration, + + /** + * Time spent updating the `ts.Program` with new Type Check Block code. + */ + TcbUpdateProgram, + + /** + * Time spent by TypeScript performing its emit operations, including downleveling and writing + * output files. + */ + TypeScriptEmit, + + /** + * Time spent by Angular performing code transformations of ASTs as they're about to be emitted. + * + * This includes the actual code generation step for templates, and occurs during the emit phase + * (but is tracked separately from `TypeScriptEmit` time). + */ + Compile, + + /** + * Time spent performing a `TemplateTypeChecker` autocompletion operation. + */ + TtcAutocompletion, + + /** + * Time spent computing template type-checking diagnostics. + */ + TtcDiagnostics, + + /** + * Time spent getting a `Symbol` from the `TemplateTypeChecker`. + */ + TtcSymbol, + + /** + * Time spent by the Angular Language Service calculating a "get references" or a renaming + * operation. + */ + LsReferencesAndRenames, + + /** + * Tracks the number of `PerfPhase`s, and must appear at the end of the list. + */ + LAST, +} + +/** + * Represents some occurrence during compilation, and is tracked with a counter. + */ +export enum PerfEvent { + /** + * Counts the number of `.d.ts` files in the program. + */ + InputDtsFile, + + /** + * Counts the number of non-`.d.ts` files in the program. + */ + InputTsFile, + + /** + * An `@Component` class was analyzed. + */ + AnalyzeComponent, + + /** + * An `@Directive` class was analyzed. + */ + AnalyzeDirective, + + /** + * An `@Injectable` class was analyzed. + */ + AnalyzeInjectable, + + /** + * An `@NgModule` class was analyzed. + */ + AnalyzeNgModule, + + /** + * An `@Pipe` class was analyzed. + */ + AnalyzePipe, + + /** + * A trait was analyzed. + * + * In theory, this should be the sum of the `Analyze` counters for each decorator type. + */ + TraitAnalyze, + + /** + * A trait had a prior analysis available from an incremental program, and did not need to be + * re-analyzed. + */ + TraitReuseAnalysis, + + /** + * A `ts.SourceFile` directly changed between the prior program and a new incremental compilation. + */ + SourceFilePhysicalChange, + + /** + * A `ts.SourceFile` did not physically changed, but according to the file dependency graph, has + * logically changed between the prior program and a new incremental compilation. + */ + SourceFileLogicalChange, + + /** + * A `ts.SourceFile` has not logically changed and all of its analysis results were thus available + * for reuse. + */ + SourceFileReuseAnalysis, + + /** + * A Type Check Block (TCB) was generated. + */ + GenerateTcb, + + /** + * A Type Check Block (TCB) could not be generated because inlining was disabled, and the block + * would've required inlining. + */ + SkipGenerateTcbNoInline, + + /** + * A `.ngtypecheck.ts` file could be reused from the previous program and did not need to be + * regenerated. + */ + ReuseTypeCheckFile, + + /** + * The template type-checking program required changes and had to be updated in an incremental + * step. + */ + UpdateTypeCheckProgram, + + /** + * The compiler was able to prove that a `ts.SourceFile` did not need to be re-emitted. + */ + EmitSkipSourceFile, + + /** + * A `ts.SourceFile` was emitted. + */ + EmitSourceFile, + + /** + * Tracks the number of `PrefEvent`s, and must appear at the end of the list. + */ + LAST, +} + +/** + * Represents a checkpoint during compilation at which the memory usage of the compiler should be + * recorded. + */ +export enum PerfCheckpoint { + /** + * The point at which the `PerfRecorder` was created, and ideally tracks memory used before any + * compilation structures are created. + */ + Initial, + + /** + * The point just after the `ts.Program` has been created. + */ + TypeScriptProgramCreate, + + /** + * The point just before Angular analysis starts. + * + * In the main usage pattern for the compiler, TypeScript diagnostics have been calculated at this + * point, so the `ts.TypeChecker` has fully ingested the current program, all `ts.Type` structures + * and `ts.Symbol`s have been created. + */ + PreAnalysis, + + /** + * The point just after Angular analysis completes. + */ + Analysis, + + /** + * The point just after Angular resolution is complete. + */ + Resolve, + + /** + * The point just after Type Check Blocks (TCBs) have been generated. + */ + TtcGeneration, + + /** + * The point just after the template type-checking program has been updated with any new TCBs. + */ + TtcUpdateProgram, + + /** + * The point just before emit begins. + * + * In the main usage pattern for the compiler, all template type-checking diagnostics have been + * requested at this point. + */ + PreEmit, + + /** + * The point just after the program has been fully emitted. + */ + Emit, + + /** + * Tracks the number of `PerfCheckpoint`s, and must appear at the end of the list. + */ + LAST, +} + +/** + * Records timing, memory, or counts at specific points in the compiler's operation. + */ +export interface PerfRecorder { + /** + * Set the current phase of compilation. + * + * Time spent in the previous phase will be accounted to that phase. The caller is responsible for + * exiting the phase when work that should be tracked within it is completed, and either returning + * to the previous phase or transitioning to the next one directly. + * + * In general, prefer using `inPhase()` to instrument a section of code, as it automatically + * handles entering and exiting the phase. `phase()` should only be used when the former API + * cannot be cleanly applied to a particular operation. + * + * @returns the previous phase + */ + phase(phase: PerfPhase): PerfPhase; + + /** + * Run `fn` in the given `PerfPhase` and return the result. + * + * Enters `phase` before executing the given `fn`, then exits the phase and returns the result. + * Prefer this API to `phase()` where possible. + */ + inPhase(phase: PerfPhase, fn: () => T): T; + + /** + * Record the memory usage of the compiler at the given checkpoint. + */ + memory(after: PerfCheckpoint): void; + + /** + * Record that a specific event has occurred, possibly more than once. + */ + eventCount(event: PerfEvent, incrementBy?: number): void; + + /** + * Return the `PerfRecorder` to an empty state (clear all tracked statistics) and reset the zero + * point to the current time. + */ + reset(): void; } diff --git a/packages/compiler-cli/src/ngtsc/perf/src/noop.ts b/packages/compiler-cli/src/ngtsc/perf/src/noop.ts index 73f4384dde..8d5c8e0a41 100644 --- a/packages/compiler-cli/src/ngtsc/perf/src/noop.ts +++ b/packages/compiler-cli/src/ngtsc/perf/src/noop.ts @@ -5,15 +5,23 @@ * 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 {DeclarationNode} from '../../reflection'; +import {PerfPhase, PerfRecorder} from './api'; -import {PerfRecorder} from './api'; +class NoopPerfRecorder implements PerfRecorder { + eventCount(): void {} -export const NOOP_PERF_RECORDER: PerfRecorder = { - enabled: false, - mark: (name: string, node: DeclarationNode, category?: string, detail?: string): void => {}, - start: (name: string, node: DeclarationNode, category?: string, detail?: string): number => { - return 0; - }, - stop: (span: number|false): void => {}, -}; + memory(): void {} + + phase(): PerfPhase { + return PerfPhase.Unaccounted; + } + + inPhase(phase: PerfPhase, fn: () => T): T { + return fn(); + } + + reset(): void {} +} + + +export const NOOP_PERF_RECORDER: PerfRecorder = new NoopPerfRecorder(); diff --git a/packages/compiler-cli/src/ngtsc/perf/src/recorder.ts b/packages/compiler-cli/src/ngtsc/perf/src/recorder.ts new file mode 100644 index 0000000000..b35bf79ae0 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/perf/src/recorder.ts @@ -0,0 +1,154 @@ +/** + * @license + * Copyright Google LLC 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 {PerfCheckpoint, PerfEvent, PerfPhase, PerfRecorder} from './api'; +import {HrTime, mark, timeSinceInMicros} from './clock'; + +/** + * Serializable performance data for the compilation, using string names. + */ +export interface PerfResults { + events: Record; + phases: Record; + memory: Record; +} + +/** + * A `PerfRecorder` that actively tracks performance statistics. + */ +export class ActivePerfRecorder implements PerfRecorder { + private counters: number[]; + private phaseTime: number[]; + private bytes: number[]; + + private currentPhase = PerfPhase.Unaccounted; + private currentPhaseEntered = this.zeroTime; + + /** + * Creates an `ActivePerfRecoder` with its zero point set to the current time. + */ + static zeroedToNow(): ActivePerfRecorder { + return new ActivePerfRecorder(mark()); + } + + private constructor(private zeroTime: HrTime) { + this.counters = Array(PerfEvent.LAST).fill(0); + this.phaseTime = Array(PerfPhase.LAST).fill(0); + this.bytes = Array(PerfCheckpoint.LAST).fill(0); + + // Take an initial memory snapshot before any other compilation work begins. + this.memory(PerfCheckpoint.Initial); + } + + reset(): void { + this.counters = Array(PerfEvent.LAST).fill(0); + this.phaseTime = Array(PerfPhase.LAST).fill(0); + this.bytes = Array(PerfCheckpoint.LAST).fill(0); + this.zeroTime = mark(); + this.currentPhase = PerfPhase.Unaccounted; + this.currentPhaseEntered = this.zeroTime; + } + + memory(after: PerfCheckpoint): void { + this.bytes[after] = process.memoryUsage().heapUsed; + } + + phase(phase: PerfPhase): PerfPhase { + const previous = this.currentPhase; + this.phaseTime[this.currentPhase] += timeSinceInMicros(this.currentPhaseEntered); + this.currentPhase = phase; + this.currentPhaseEntered = mark(); + return previous; + } + + inPhase(phase: PerfPhase, fn: () => T): T { + const previousPhase = this.phase(phase); + try { + return fn(); + } finally { + this.phase(previousPhase); + } + } + + eventCount(counter: PerfEvent, incrementBy: number = 1): void { + this.counters[counter] += incrementBy; + } + + /** + * Return the current performance metrics as a serializable object. + */ + finalize(): PerfResults { + // Track the last segment of time spent in `this.currentPhase` in the time array. + this.phase(PerfPhase.Unaccounted); + + const results: PerfResults = { + events: {}, + phases: {}, + memory: {}, + }; + + for (let i = 0; i < this.phaseTime.length; i++) { + if (this.phaseTime[i] > 0) { + results.phases[PerfPhase[i]] = this.phaseTime[i]; + } + } + + for (let i = 0; i < this.phaseTime.length; i++) { + if (this.counters[i] > 0) { + results.events[PerfEvent[i]] = this.counters[i]; + } + } + + for (let i = 0; i < this.bytes.length; i++) { + if (this.bytes[i] > 0) { + results.memory[PerfCheckpoint[i]] = this.bytes[i]; + } + } + + return results; + } +} + +/** + * A `PerfRecorder` that delegates to a target `PerfRecorder` which can be updated later. + * + * `DelegatingPerfRecorder` is useful when a compiler class that needs a `PerfRecorder` can outlive + * the current compilation. This is true for most compiler classes as resource-only changes reuse + * the same `NgCompiler` for a new compilation. + */ +export class DelegatingPerfRecorder implements PerfRecorder { + constructor(public target: PerfRecorder) {} + + eventCount(counter: PerfEvent, incrementBy?: number): void { + this.target.eventCount(counter, incrementBy); + } + + phase(phase: PerfPhase): PerfPhase { + return this.target.phase(phase); + } + + inPhase(phase: PerfPhase, fn: () => T): T { + // Note: this doesn't delegate to `this.target.inPhase` but instead is implemented manually here + // to avoid adding an additional frame of noise to the stack when debugging. + const previousPhase = this.target.phase(phase); + try { + return fn(); + } finally { + this.target.phase(previousPhase); + } + } + + memory(after: PerfCheckpoint): void { + this.target.memory(after); + } + + reset(): void { + this.target.reset(); + } +} diff --git a/packages/compiler-cli/src/ngtsc/perf/src/tracking.ts b/packages/compiler-cli/src/ngtsc/perf/src/tracking.ts deleted file mode 100644 index 732c8f0367..0000000000 --- a/packages/compiler-cli/src/ngtsc/perf/src/tracking.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 ts from 'typescript'; -import {resolve} from '../../file_system'; -import {DeclarationNode} from '../../reflection'; -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?: DeclarationNode, category?: string, detail?: string): void { - const msg = this.makeLogMessage(PerfLogEventType.MARK, name, node, category, detail, undefined); - this.log.push(msg); - } - - start(name: string, node?: DeclarationNode, 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: DeclarationNode|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 = resolve(host.getCurrentDirectory(), target); - host.writeFile(outFile, json, false); - } else { - const outFile = 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 28345f5810..1eb8ae18e7 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -14,10 +14,10 @@ import {verifySupportedTypeScriptVersion} from '../typescript_support'; import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler, NgCompilerHost} from './core'; import {NgCompilerOptions} from './core/api'; -import {absoluteFrom, AbsoluteFsPath} from './file_system'; +import {absoluteFrom, AbsoluteFsPath, getFileSystem} from './file_system'; import {TrackedIncrementalBuildStrategy} from './incremental'; import {IndexedComponent} from './indexer'; -import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf'; +import {ActivePerfRecorder, PerfCheckpoint as PerfCheckpoint, PerfEvent, PerfPhase} from './perf'; import {DeclarationNode} from './reflection'; import {retagAllTsFiles, untagAllTsFiles} from './shims'; import {ReusedProgramStrategy} from './typecheck'; @@ -54,22 +54,20 @@ export class NgtscProgram implements api.Program { private reuseTsProgram: ts.Program; private closureCompilerEnabled: boolean; private host: NgCompilerHost; - private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER; - private perfTracker: PerfTracker|null = null; private incrementalStrategy: TrackedIncrementalBuildStrategy; constructor( rootNames: ReadonlyArray, private options: NgCompilerOptions, delegateHost: api.CompilerHost, oldProgram?: NgtscProgram) { + const perfRecorder = ActivePerfRecorder.zeroedToNow(); + + perfRecorder.phase(PerfPhase.Setup); + // First, check whether the current TS version is supported. if (!options.disableTypeScriptVersionCheck) { verifySupportedTypeScriptVersion(); } - if (options.tracePerformance !== undefined) { - this.perfTracker = PerfTracker.zeroedToNow(); - this.perfRecorder = this.perfTracker; - } this.closureCompilerEnabled = !!options.annotateForClosureCompiler; const reuseProgram = oldProgram?.reuseTsProgram; @@ -83,9 +81,14 @@ export class NgtscProgram implements api.Program { retagAllTsFiles(reuseProgram); } - this.tsProgram = ts.createProgram(this.host.inputFiles, options, this.host, reuseProgram); + this.tsProgram = perfRecorder.inPhase( + PerfPhase.TypeScriptProgramCreate, + () => ts.createProgram(this.host.inputFiles, options, this.host, reuseProgram)); this.reuseTsProgram = this.tsProgram; + perfRecorder.phase(PerfPhase.Unaccounted); + perfRecorder.memory(PerfCheckpoint.TypeScriptProgramCreate); + this.host.postProgramCreationCleanup(); // Shim tagging has served its purpose, and tags can now be removed from all `ts.SourceFile`s in @@ -111,7 +114,7 @@ export class NgtscProgram implements api.Program { let ticket: CompilationTicket; if (oldProgram === undefined) { ticket = freshCompilationTicket( - this.tsProgram, options, this.incrementalStrategy, reusedProgramStrategy, + this.tsProgram, options, this.incrementalStrategy, reusedProgramStrategy, perfRecorder, /* enableTemplateTypeChecker */ false, /* usePoisonedData */ false); } else { ticket = incrementalFromCompilerTicket( @@ -120,12 +123,13 @@ export class NgtscProgram implements api.Program { this.incrementalStrategy, reusedProgramStrategy, modifiedResourceFiles, + perfRecorder, ); } // Create the NgCompiler which will drive the rest of the compilation. - this.compiler = NgCompiler.fromTicket(ticket, this.host, this.perfRecorder); + this.compiler = NgCompiler.fromTicket(ticket, this.host); } getTsProgram(): ts.Program { @@ -138,49 +142,59 @@ export class NgtscProgram implements api.Program { getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken| undefined): readonly ts.Diagnostic[] { - return this.tsProgram.getOptionsDiagnostics(cancellationToken); + return this.compiler.perfRecorder.inPhase( + PerfPhase.TypeScriptDiagnostics, + () => this.tsProgram.getOptionsDiagnostics(cancellationToken)); } getTsSyntacticDiagnostics( sourceFile?: ts.SourceFile|undefined, cancellationToken?: ts.CancellationToken|undefined): readonly ts.Diagnostic[] { - const ignoredFiles = this.compiler.ignoreForDiagnostics; - if (sourceFile !== undefined) { - if (ignoredFiles.has(sourceFile)) { - return []; - } - - return this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken); - } else { - const diagnostics: ts.Diagnostic[] = []; - for (const sf of this.tsProgram.getSourceFiles()) { - if (!ignoredFiles.has(sf)) { - diagnostics.push(...this.tsProgram.getSyntacticDiagnostics(sf, cancellationToken)); + return this.compiler.perfRecorder.inPhase(PerfPhase.TypeScriptDiagnostics, () => { + const ignoredFiles = this.compiler.ignoreForDiagnostics; + let res: readonly ts.Diagnostic[]; + if (sourceFile !== undefined) { + if (ignoredFiles.has(sourceFile)) { + return []; } + + res = this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken); + } else { + const diagnostics: ts.Diagnostic[] = []; + for (const sf of this.tsProgram.getSourceFiles()) { + if (!ignoredFiles.has(sf)) { + diagnostics.push(...this.tsProgram.getSyntacticDiagnostics(sf, cancellationToken)); + } + } + res = diagnostics; } - return diagnostics; - } + return res; + }); } getTsSemanticDiagnostics( sourceFile?: ts.SourceFile|undefined, cancellationToken?: ts.CancellationToken|undefined): readonly ts.Diagnostic[] { - const ignoredFiles = this.compiler.ignoreForDiagnostics; - if (sourceFile !== undefined) { - if (ignoredFiles.has(sourceFile)) { - return []; - } - - return this.tsProgram.getSemanticDiagnostics(sourceFile, cancellationToken); - } else { - const diagnostics: ts.Diagnostic[] = []; - for (const sf of this.tsProgram.getSourceFiles()) { - if (!ignoredFiles.has(sf)) { - diagnostics.push(...this.tsProgram.getSemanticDiagnostics(sf, cancellationToken)); + return this.compiler.perfRecorder.inPhase(PerfPhase.TypeScriptDiagnostics, () => { + const ignoredFiles = this.compiler.ignoreForDiagnostics; + let res: readonly ts.Diagnostic[]; + if (sourceFile !== undefined) { + if (ignoredFiles.has(sourceFile)) { + return []; } + + res = this.tsProgram.getSemanticDiagnostics(sourceFile, cancellationToken); + } else { + const diagnostics: ts.Diagnostic[] = []; + for (const sf of this.tsProgram.getSourceFiles()) { + if (!ignoredFiles.has(sf)) { + diagnostics.push(...this.tsProgram.getSemanticDiagnostics(sf, cancellationToken)); + } + } + res = diagnostics; } - return diagnostics; - } + return res; + }); } getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken| @@ -235,73 +249,82 @@ export class NgtscProgram implements api.Program { emitCallback?: api.TsEmitCallback | undefined; mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback | undefined; }|undefined): ts.EmitResult { - const {transformers} = this.compiler.prepareEmit(); - const ignoreFiles = this.compiler.ignoreForEmit; - const emitCallback = opts && opts.emitCallback || defaultEmitCallback; + this.compiler.perfRecorder.memory(PerfCheckpoint.PreEmit); - const writeFile: ts.WriteFileCallback = - (fileName: string, data: string, writeByteOrderMark: boolean, - onError: ((message: string) => void)|undefined, - sourceFiles: ReadonlyArray|undefined) => { - if (sourceFiles !== undefined) { - // Record successful writes for any `ts.SourceFile` (that's not a declaration file) - // that's an input to this write. - for (const writtenSf of sourceFiles) { - if (writtenSf.isDeclarationFile) { - continue; + const res = this.compiler.perfRecorder.inPhase(PerfPhase.TypeScriptEmit, () => { + const {transformers} = this.compiler.prepareEmit(); + const ignoreFiles = this.compiler.ignoreForEmit; + const emitCallback = opts && opts.emitCallback || defaultEmitCallback; + + const writeFile: ts.WriteFileCallback = + (fileName: string, data: string, writeByteOrderMark: boolean, + onError: ((message: string) => void)|undefined, + sourceFiles: ReadonlyArray|undefined) => { + if (sourceFiles !== undefined) { + // Record successful writes for any `ts.SourceFile` (that's not a declaration file) + // that's an input to this write. + for (const writtenSf of sourceFiles) { + if (writtenSf.isDeclarationFile) { + continue; + } + + this.compiler.incrementalDriver.recordSuccessfulEmit(writtenSf); } - - this.compiler.incrementalDriver.recordSuccessfulEmit(writtenSf); } - } - this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); - }; + this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); + }; - const customTransforms = opts && opts.customTransformers; - const beforeTransforms = transformers.before || []; - const afterDeclarationsTransforms = transformers.afterDeclarations; + const customTransforms = opts && opts.customTransformers; + const beforeTransforms = transformers.before || []; + const afterDeclarationsTransforms = transformers.afterDeclarations; - if (customTransforms !== undefined && customTransforms.beforeTs !== undefined) { - beforeTransforms.push(...customTransforms.beforeTs); - } - - const emitSpan = this.perfRecorder.start('emit'); - const emitResults: ts.EmitResult[] = []; - - for (const targetSourceFile of this.tsProgram.getSourceFiles()) { - if (targetSourceFile.isDeclarationFile || ignoreFiles.has(targetSourceFile)) { - continue; + if (customTransforms !== undefined && customTransforms.beforeTs !== undefined) { + beforeTransforms.push(...customTransforms.beforeTs); } - if (this.compiler.incrementalDriver.safeToSkipEmit(targetSourceFile)) { - continue; + const emitResults: ts.EmitResult[] = []; + + for (const targetSourceFile of this.tsProgram.getSourceFiles()) { + if (targetSourceFile.isDeclarationFile || ignoreFiles.has(targetSourceFile)) { + continue; + } + + if (this.compiler.incrementalDriver.safeToSkipEmit(targetSourceFile)) { + this.compiler.perfRecorder.eventCount(PerfEvent.EmitSkipSourceFile); + continue; + } + + this.compiler.perfRecorder.eventCount(PerfEvent.EmitSourceFile); + + emitResults.push(emitCallback({ + targetSourceFile, + program: this.tsProgram, + host: this.host, + options: this.options, + emitOnlyDtsFiles: false, + writeFile, + customTransformers: { + before: beforeTransforms, + after: customTransforms && customTransforms.afterTs, + afterDeclarations: afterDeclarationsTransforms, + } as any, + })); } - const fileEmitSpan = this.perfRecorder.start('emitFile', targetSourceFile); - emitResults.push(emitCallback({ - targetSourceFile, - program: this.tsProgram, - host: this.host, - options: this.options, - emitOnlyDtsFiles: false, - writeFile, - customTransformers: { - before: beforeTransforms, - after: customTransforms && customTransforms.afterTs, - afterDeclarations: afterDeclarationsTransforms, - } as any, - })); - this.perfRecorder.stop(fileEmitSpan); + this.compiler.perfRecorder.memory(PerfCheckpoint.Emit); + + // Run the emit, including a custom transformer that will downlevel the Ivy decorators in + // code. + return ((opts && opts.mergeEmitResultsCallback) || mergeEmitResults)(emitResults); + }); + + // Record performance analysis information to disk if we've been asked to do so. + if (this.options.tracePerformance !== undefined) { + const perf = this.compiler.perfRecorder.finalize(); + getFileSystem().writeFile( + getFileSystem().resolve(this.options.tracePerformance), JSON.stringify(perf, null, 2)); } - - 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. - return ((opts && opts.mergeEmitResultsCallback) || mergeEmitResults)(emitResults); + return res; } getIndexedComponents(): Map { diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index d3bae1c54a..d26059c9b6 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -13,7 +13,7 @@ import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {IncrementalBuild} from '../../incremental/api'; import {SemanticDepGraphUpdater, SemanticSymbol} from '../../incremental/semantic_graph'; import {IndexingContext} from '../../indexer'; -import {PerfRecorder} from '../../perf'; +import {PerfEvent, PerfRecorder} from '../../perf'; import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost} from '../../reflection'; import {ProgramTypeCheckAdapter, TypeCheckContext} from '../../typecheck/api'; import {getSourceFile, isExported} from '../../util/src/typescript'; @@ -124,6 +124,9 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { this.adopt(priorRecord); } + this.perf.eventCount(PerfEvent.SourceFileReuseAnalysis); + this.perf.eventCount(PerfEvent.TraitReuseAnalysis, priorWork.length); + // Skip the rest of analysis, as this file's prior traits are being reused. return; } @@ -359,6 +362,8 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { TraitState[trait.state]} (expected DETECTED)`); } + this.perf.eventCount(PerfEvent.TraitAnalyze); + // Attempt analysis. This could fail with a `FatalDiagnosticError`; catch it if it does. let result: AnalysisOutput; try { @@ -509,9 +514,6 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { continue; } - const compileSpan = this.perf.start('compileClass', original); - - // `trait.resolution` is non-null asserted here because TypeScript does not recognize that // `Readonly` is nullable (as `unknown` itself is nullable) due to the way that // `Readonly` works. @@ -526,7 +528,6 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { } const compileMatchRes = compileRes; - this.perf.stop(compileSpan); if (Array.isArray(compileMatchRes)) { for (const result of compileMatchRes) { if (!res.some(r => r.name === result.name)) { diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index 9dbeb1279f..c9a0d7bd10 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -10,6 +10,7 @@ import {ConstantPool} from '@angular/compiler'; import * as ts from 'typescript'; import {DefaultImportRecorder, ImportRewriter} from '../../imports'; +import {PerfPhase, PerfRecorder} from '../../perf'; import {Decorator, ReflectionHost} from '../../reflection'; import {ImportManager, RecordWrappedNodeExprFn, translateExpression, translateStatement, TranslatorOptions} from '../../translator'; import {visit, VisitListEntryResult, Visitor} from '../../util/src/visitor'; @@ -33,14 +34,16 @@ interface FileOverviewMeta { export function ivyTransformFactory( compilation: TraitCompiler, reflector: ReflectionHost, importRewriter: ImportRewriter, - defaultImportRecorder: DefaultImportRecorder, isCore: boolean, + defaultImportRecorder: DefaultImportRecorder, perf: PerfRecorder, isCore: boolean, isClosureCompilerEnabled: boolean): ts.TransformerFactory { const recordWrappedNodeExpr = createRecorderFn(defaultImportRecorder); return (context: ts.TransformationContext): ts.Transformer => { return (file: ts.SourceFile): ts.SourceFile => { - return transformIvySourceFile( - compilation, context, reflector, importRewriter, file, isCore, isClosureCompilerEnabled, - recordWrappedNodeExpr); + return perf.inPhase( + PerfPhase.Compile, + () => transformIvySourceFile( + compilation, context, reflector, importRewriter, file, isCore, + isClosureCompilerEnabled, recordWrappedNodeExpr)); }; }; } diff --git a/packages/compiler-cli/src/ngtsc/tsc_plugin.ts b/packages/compiler-cli/src/ngtsc/tsc_plugin.ts index e6ae2920ca..292c472a19 100644 --- a/packages/compiler-cli/src/ngtsc/tsc_plugin.ts +++ b/packages/compiler-cli/src/ngtsc/tsc_plugin.ts @@ -12,7 +12,7 @@ import {CompilationTicket, freshCompilationTicket, incrementalFromDriverTicket, import {NgCompilerOptions, UnifiedModulesHost} from './core/api'; import {NodeJSFileSystem, setFileSystem} from './file_system'; import {PatchedProgramIncrementalBuildStrategy} from './incremental'; -import {NOOP_PERF_RECORDER} from './perf'; +import {ActivePerfRecorder, NOOP_PERF_RECORDER, PerfPhase} from './perf'; import {untagAllTsFiles} from './shims'; import {OptimizeFor} from './typecheck/api'; import {ReusedProgramStrategy} from './typecheck/src/augmented_program'; @@ -94,6 +94,13 @@ export class NgTscPlugin implements TscPlugin { ignoreForDiagnostics: Set, ignoreForEmit: Set, } { + // TODO(alxhub): we provide a `PerfRecorder` to the compiler, but because we're not driving the + // compilation, the information captured within it is incomplete, and may not include timings + // for phases such as emit. + // + // Additionally, nothing actually captures the perf results here, so recording stats at all is + // somewhat moot for now :) + const perfRecorder = ActivePerfRecorder.zeroedToNow(); if (this.host === null || this.options === null) { throw new Error('Lifecycle error: setupCompilation() before wrapHost().'); } @@ -115,15 +122,15 @@ export class NgTscPlugin implements TscPlugin { if (oldProgram === undefined || oldDriver === null) { ticket = freshCompilationTicket( - program, this.options, strategy, typeCheckStrategy, + program, this.options, strategy, typeCheckStrategy, perfRecorder, /* enableTemplateTypeChecker */ false, /* usePoisonedData */ false); } else { strategy.toNextBuildStrategy().getIncrementalDriver(oldProgram); ticket = incrementalFromDriverTicket( oldProgram, oldDriver, program, this.options, strategy, typeCheckStrategy, - modifiedResourceFiles, false, false); + modifiedResourceFiles, perfRecorder, false, false); } - this._compiler = NgCompiler.fromTicket(ticket, this.host, NOOP_PERF_RECORDER); + this._compiler = NgCompiler.fromTicket(ticket, this.host); return { ignoreForDiagnostics: this._compiler.ignoreForDiagnostics, ignoreForEmit: this._compiler.ignoreForEmit, @@ -146,6 +153,9 @@ export class NgTscPlugin implements TscPlugin { } createTransformers(): ts.CustomTransformers { + // The plugin consumer doesn't know about our perf tracing system, so we consider the emit phase + // as beginning now. + this.compiler.perfRecorder.phase(PerfPhase.TypeScriptEmit); return this.compiler.prepareEmit().transformers; } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel index f1553346ed..a258bcd9be 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel @@ -15,6 +15,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/incremental:api", "//packages/compiler-cli/src/ngtsc/metadata", + "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/shims", diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index 2cbad7db17..92ba82b43f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -12,6 +12,7 @@ import * as ts from 'typescript'; import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system'; import {Reference, ReferenceEmitter} from '../../imports'; import {IncrementalBuild} from '../../incremental/api'; +import {PerfCheckpoint, PerfEvent, PerfPhase, PerfRecorder} from '../../perf'; import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost} from '../../reflection'; import {ComponentScopeReader, TypeCheckScopeRegistry} from '../../scope'; import {isShim} from '../../shims'; @@ -82,7 +83,8 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { private compilerHost: Pick, private priorBuild: IncrementalBuild, private readonly componentScopeReader: ComponentScopeReader, - private readonly typeCheckScopeRegistry: TypeCheckScopeRegistry) {} + private readonly typeCheckScopeRegistry: TypeCheckScopeRegistry, + private readonly perf: PerfRecorder) {} getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null { const {data} = this.getLatestComponentState(component); @@ -181,19 +183,60 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { break; } - const sfPath = absoluteFromSourceFile(sf); - const fileRecord = this.state.get(sfPath)!; + return this.perf.inPhase(PerfPhase.TtcDiagnostics, () => { + const sfPath = absoluteFromSourceFile(sf); + const fileRecord = this.state.get(sfPath)!; - const typeCheckProgram = this.typeCheckingStrategy.getProgram(); + const typeCheckProgram = this.typeCheckingStrategy.getProgram(); - const diagnostics: (ts.Diagnostic|null)[] = []; - if (fileRecord.hasInlines) { - const inlineSf = getSourceFileOrError(typeCheckProgram, sfPath); - diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(inlineSf).map( - diag => convertDiagnostic(diag, fileRecord.sourceManager))); - } + const diagnostics: (ts.Diagnostic|null)[] = []; + if (fileRecord.hasInlines) { + const inlineSf = getSourceFileOrError(typeCheckProgram, sfPath); + diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(inlineSf).map( + diag => convertDiagnostic(diag, fileRecord.sourceManager))); + } + + for (const [shimPath, shimRecord] of fileRecord.shimData) { + const shimSf = getSourceFileOrError(typeCheckProgram, shimPath); + diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(shimSf).map( + diag => convertDiagnostic(diag, fileRecord.sourceManager))); + diagnostics.push(...shimRecord.genesisDiagnostics); + + for (const templateData of shimRecord.templates.values()) { + diagnostics.push(...templateData.templateDiagnostics); + } + } + + return diagnostics.filter((diag: ts.Diagnostic|null): diag is ts.Diagnostic => diag !== null); + }); + } + + getDiagnosticsForComponent(component: ts.ClassDeclaration): ts.Diagnostic[] { + this.ensureShimForComponent(component); + + return this.perf.inPhase(PerfPhase.TtcDiagnostics, () => { + const sf = component.getSourceFile(); + const sfPath = absoluteFromSourceFile(sf); + const shimPath = this.typeCheckingStrategy.shimPathForComponent(component); + + const fileRecord = this.getFileData(sfPath); + + if (!fileRecord.shimData.has(shimPath)) { + return []; + } + + const templateId = fileRecord.sourceManager.getTemplateId(component); + const shimRecord = fileRecord.shimData.get(shimPath)!; + + const typeCheckProgram = this.typeCheckingStrategy.getProgram(); + + const diagnostics: (TemplateDiagnostic|null)[] = []; + if (shimRecord.hasInlines) { + const inlineSf = getSourceFileOrError(typeCheckProgram, sfPath); + diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(inlineSf).map( + diag => convertDiagnostic(diag, fileRecord.sourceManager))); + } - for (const [shimPath, shimRecord] of fileRecord.shimData) { const shimSf = getSourceFileOrError(typeCheckProgram, shimPath); diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(shimSf).map( diag => convertDiagnostic(diag, fileRecord.sourceManager))); @@ -202,48 +245,11 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { for (const templateData of shimRecord.templates.values()) { diagnostics.push(...templateData.templateDiagnostics); } - } - return diagnostics.filter((diag: ts.Diagnostic|null): diag is ts.Diagnostic => diag !== null); - } - - getDiagnosticsForComponent(component: ts.ClassDeclaration): ts.Diagnostic[] { - this.ensureShimForComponent(component); - - const sf = component.getSourceFile(); - const sfPath = absoluteFromSourceFile(sf); - const shimPath = this.typeCheckingStrategy.shimPathForComponent(component); - - const fileRecord = this.getFileData(sfPath); - - if (!fileRecord.shimData.has(shimPath)) { - return []; - } - - const templateId = fileRecord.sourceManager.getTemplateId(component); - const shimRecord = fileRecord.shimData.get(shimPath)!; - - const typeCheckProgram = this.typeCheckingStrategy.getProgram(); - - const diagnostics: (TemplateDiagnostic|null)[] = []; - if (shimRecord.hasInlines) { - const inlineSf = getSourceFileOrError(typeCheckProgram, sfPath); - diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(inlineSf).map( - diag => convertDiagnostic(diag, fileRecord.sourceManager))); - } - - const shimSf = getSourceFileOrError(typeCheckProgram, shimPath); - diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(shimSf).map( - diag => convertDiagnostic(diag, fileRecord.sourceManager))); - diagnostics.push(...shimRecord.genesisDiagnostics); - - for (const templateData of shimRecord.templates.values()) { - diagnostics.push(...templateData.templateDiagnostics); - } - - return diagnostics.filter( - (diag: TemplateDiagnostic|null): diag is TemplateDiagnostic => - diag !== null && diag.templateId === templateId); + return diagnostics.filter( + (diag: TemplateDiagnostic|null): diag is TemplateDiagnostic => + diag !== null && diag.templateId === templateId); + }); } getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node|null { @@ -256,7 +262,8 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { if (engine === null) { return null; } - return engine.getGlobalCompletions(context); + return this.perf.inPhase( + PerfPhase.TtcAutocompletion, () => engine.getGlobalCompletions(context)); } getExpressionCompletionLocation( @@ -266,7 +273,8 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { if (engine === null) { return null; } - return engine.getExpressionCompletionLocation(ast); + return this.perf.inPhase( + PerfPhase.TtcAutocompletion, () => engine.getExpressionCompletionLocation(ast)); } invalidateClass(clazz: ts.ClassDeclaration): void { @@ -318,6 +326,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return; } + this.perf.eventCount(PerfEvent.ReuseTypeCheckFile); this.state.set(sfPath, previousResults); } @@ -326,50 +335,55 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return; } - const host = new WholeProgramTypeCheckingHost(this); - const ctx = this.newContext(host); + this.perf.inPhase(PerfPhase.TcbGeneration, () => { + const host = new WholeProgramTypeCheckingHost(this); + const ctx = this.newContext(host); - for (const sf of this.originalProgram.getSourceFiles()) { - if (sf.isDeclarationFile || isShim(sf)) { - continue; + for (const sf of this.originalProgram.getSourceFiles()) { + if (sf.isDeclarationFile || isShim(sf)) { + continue; + } + + this.maybeAdoptPriorResultsForFile(sf); + + const sfPath = absoluteFromSourceFile(sf); + const fileData = this.getFileData(sfPath); + if (fileData.isComplete) { + continue; + } + + this.typeCheckAdapter.typeCheck(sf, ctx); + + fileData.isComplete = true; } + this.updateFromContext(ctx); + this.isComplete = true; + }); + } + + private ensureAllShimsForOneFile(sf: ts.SourceFile): void { + this.perf.inPhase(PerfPhase.TcbGeneration, () => { this.maybeAdoptPriorResultsForFile(sf); const sfPath = absoluteFromSourceFile(sf); + const fileData = this.getFileData(sfPath); if (fileData.isComplete) { - continue; + // All data for this file is present and accounted for already. + return; } + const host = + new SingleFileTypeCheckingHost(sfPath, fileData, this.typeCheckingStrategy, this); + const ctx = this.newContext(host); + this.typeCheckAdapter.typeCheck(sf, ctx); fileData.isComplete = true; - } - this.updateFromContext(ctx); - this.isComplete = true; - } - - private ensureAllShimsForOneFile(sf: ts.SourceFile): void { - this.maybeAdoptPriorResultsForFile(sf); - - const sfPath = absoluteFromSourceFile(sf); - - const fileData = this.getFileData(sfPath); - if (fileData.isComplete) { - // All data for this file is present and accounted for already. - return; - } - - const host = new SingleFileTypeCheckingHost(sfPath, fileData, this.typeCheckingStrategy, this); - const ctx = this.newContext(host); - - this.typeCheckAdapter.typeCheck(sf, ctx); - - fileData.isComplete = true; - - this.updateFromContext(ctx); + this.updateFromContext(ctx); + }); } private ensureShimForComponent(component: ts.ClassDeclaration): void { @@ -399,7 +413,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { InliningMode.Error; return new TypeCheckContextImpl( this.config, this.compilerHost, this.typeCheckingStrategy, this.refEmitter, this.reflector, - host, inlining); + host, inlining, this.perf); } /** @@ -428,8 +442,14 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { private updateFromContext(ctx: TypeCheckContextImpl): void { const updates = ctx.finalize(); - this.typeCheckingStrategy.updateFiles(updates, UpdateMode.Incremental); - this.priorBuild.recordSuccessfulTypeCheck(this.state); + return this.perf.inPhase(PerfPhase.TcbUpdateProgram, () => { + if (updates.size > 0) { + this.perf.eventCount(PerfEvent.UpdateTypeCheckProgram); + } + this.typeCheckingStrategy.updateFiles(updates, UpdateMode.Incremental); + this.priorBuild.recordSuccessfulTypeCheck(this.state); + this.perf.memory(PerfCheckpoint.TtcUpdateProgram); + }); } getFileData(path: AbsoluteFsPath): FileTypeCheckingData { @@ -450,7 +470,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { if (builder === null) { return null; } - return builder.getSymbol(node); + return this.perf.inPhase(PerfPhase.TtcSymbol, () => builder.getSymbol(node)); } private getOrCreateSymbolBuilder(component: ts.ClassDeclaration): SymbolBuilder|null { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index 42bc6b79e9..b06bdcfce3 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -12,6 +12,7 @@ import * as ts from 'typescript'; import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports'; +import {PerfEvent, PerfRecorder} from '../../perf'; import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ImportManager} from '../../translator'; import {ComponentToShimMappingStrategy, TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api'; @@ -179,7 +180,7 @@ export class TypeCheckContextImpl implements TypeCheckContext { private compilerHost: Pick, private componentMappingStrategy: ComponentToShimMappingStrategy, private refEmitter: ReferenceEmitter, private reflector: ReflectionHost, - private host: TypeCheckingHost, private inlining: InliningMode) { + private host: TypeCheckingHost, private inlining: InliningMode, private perf: PerfRecorder) { if (inlining === InliningMode.Error && config.useInlineTypeConstructors) { // We cannot use inlining for type checking since this environment does not support it. throw new Error(`AssertionError: invalid inlining configuration.`); @@ -273,6 +274,7 @@ export class TypeCheckContextImpl implements TypeCheckContext { shimData.oobRecorder.requiresInlineTcb(templateId, ref.node); // Checking this template would be unsupported, so don't try. + this.perf.eventCount(PerfEvent.SkipGenerateTcbNoInline); return; } @@ -282,6 +284,7 @@ export class TypeCheckContextImpl implements TypeCheckContext { pipes, schemas, }; + this.perf.eventCount(PerfEvent.GenerateTcb); if (tcbRequiresInline) { // This class didn't meet the requirements for external type checking, so generate an inline // TCB for the class. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel index 958d0c02fb..853ecffa1a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel @@ -17,6 +17,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/incremental", "//packages/compiler-cli/src/ngtsc/metadata", + "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/shims", diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts index 0c8083b357..71dc6a073e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts @@ -14,6 +14,7 @@ import {TestFile} from '../../file_system/testing'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reexport, Reference, ReferenceEmitter, RelativePathStrategy} from '../../imports'; import {NOOP_INCREMENTAL_BUILD} from '../../incremental'; import {ClassPropertyMapping, CompoundMetadataReader} from '../../metadata'; +import {NOOP_PERF_RECORDER} from '../../perf'; import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {ComponentScopeReader, LocalModuleScope, ScopeData, TypeCheckScopeRegistry} from '../../scope'; import {makeProgram} from '../../testing'; @@ -515,7 +516,7 @@ export function setup(targets: TypeCheckingTarget[], overrides: { const templateTypeChecker = new TemplateTypeCheckerImpl( program, programStrategy, checkAdapter, fullConfig, emitter, reflectionHost, host, - NOOP_INCREMENTAL_BUILD, fakeScopeReader, typeCheckScopeRegistry); + NOOP_INCREMENTAL_BUILD, fakeScopeReader, typeCheckScopeRegistry, NOOP_PERF_RECORDER); return { templateTypeChecker, program, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts index 4ac6638c0e..6cd0410e34 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts @@ -10,6 +10,7 @@ import * as ts from 'typescript'; import {absoluteFrom, AbsoluteFsPath, getFileSystem, getSourceFileOrError, LogicalFileSystem, NgtscCompilerHost} from '../../file_system'; import {runInEachFileSystem, TestFile} from '../../file_system/testing'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; +import {NOOP_PERF_RECORDER} from '../../perf'; import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing'; import {getRootDirs} from '../../util/src/typescript'; @@ -74,7 +75,7 @@ TestClass.ngTypeCtor({value: 'test'}); ]); const ctx = new TypeCheckContextImpl( ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost, - new TestTypeCheckingHost(), InliningMode.InlineOps); + new TestTypeCheckingHost(), InliningMode.InlineOps, NOOP_PERF_RECORDER); const TestClass = getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); const pendingFile = makePendingFile(); @@ -113,7 +114,7 @@ TestClass.ngTypeCtor({value: 'test'}); const pendingFile = makePendingFile(); const ctx = new TypeCheckContextImpl( ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost, - new TestTypeCheckingHost(), InliningMode.InlineOps); + new TestTypeCheckingHost(), InliningMode.InlineOps, NOOP_PERF_RECORDER); const TestClass = getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); ctx.addInlineTypeCtor( @@ -158,7 +159,7 @@ TestClass.ngTypeCtor({value: 'test'}); const pendingFile = makePendingFile(); const ctx = new TypeCheckContextImpl( ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost, - new TestTypeCheckingHost(), InliningMode.InlineOps); + new TestTypeCheckingHost(), InliningMode.InlineOps, NOOP_PERF_RECORDER); const TestClass = getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); ctx.addInlineTypeCtor( diff --git a/packages/language-service/ivy/BUILD.bazel b/packages/language-service/ivy/BUILD.bazel index e87b920172..314728e998 100644 --- a/packages/language-service/ivy/BUILD.bazel +++ b/packages/language-service/ivy/BUILD.bazel @@ -15,6 +15,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/incremental", "//packages/compiler-cli/src/ngtsc/metadata", + "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/shims", "//packages/compiler-cli/src/ngtsc/typecheck", diff --git a/packages/language-service/ivy/compiler_factory.ts b/packages/language-service/ivy/compiler_factory.ts index d243f0b164..2c634d1026 100644 --- a/packages/language-service/ivy/compiler_factory.ts +++ b/packages/language-service/ivy/compiler_factory.ts @@ -9,6 +9,7 @@ import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler, resourceChangeTicket} from '@angular/compiler-cli/src/ngtsc/core'; import {NgCompilerOptions} from '@angular/compiler-cli/src/ngtsc/core/api'; import {TrackedIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental'; +import {ActivePerfRecorder} from '@angular/compiler-cli/src/ngtsc/perf'; import {TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import * as ts from 'typescript/lib/tsserverlibrary'; @@ -44,6 +45,10 @@ export class CompilerFactory { // Only resource files have changed since the last NgCompiler was created. const ticket = resourceChangeTicket(this.compiler, modifiedResourceFiles); this.compiler = NgCompiler.fromTicket(ticket, this.adapter); + } else { + // The previous NgCompiler is being reused, but we still want to reset its performance + // tracker to capture only the operations that are needed to service the current request. + this.compiler.perfRecorder.reset(); } return this.compiler; @@ -52,11 +57,12 @@ export class CompilerFactory { let ticket: CompilationTicket; if (this.compiler === null || this.lastKnownProgram === null) { ticket = freshCompilationTicket( - program, this.options, this.incrementalStrategy, this.programStrategy, true, true); + program, this.options, this.incrementalStrategy, this.programStrategy, + /* perfRecorder */ null, true, true); } else { ticket = incrementalFromCompilerTicket( this.compiler, program, this.incrementalStrategy, this.programStrategy, - modifiedResourceFiles); + modifiedResourceFiles, /* perfRecorder */ null); } this.compiler = NgCompiler.fromTicket(ticket, this.adapter); this.lastKnownProgram = program; diff --git a/packages/language-service/ivy/references.ts b/packages/language-service/ivy/references.ts index 104d100e62..5881da4cd5 100644 --- a/packages/language-service/ivy/references.ts +++ b/packages/language-service/ivy/references.ts @@ -8,6 +8,7 @@ import {AbsoluteSourceSpan, AST, BindingPipe, LiteralPrimitive, MethodCall, ParseSourceSpan, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstNode, TmplAstReference, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler'; import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {PerfPhase} from '@angular/compiler-cli/src/ngtsc/perf'; import {DirectiveSymbol, ShimLocation, SymbolKind, TemplateTypeChecker, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import {ExpressionIdentifier, hasExpressionIdentifier} from '@angular/compiler-cli/src/ngtsc/typecheck/src/comments'; import * as ts from 'typescript'; @@ -66,46 +67,53 @@ export class ReferencesAndRenameBuilder { getRenameInfo(filePath: string, position: number): Omit|ts.RenameInfoFailure { - const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler); - // We could not get a template at position so we assume the request came from outside the - // template. - if (templateInfo === undefined) { - return this.tsLS.getRenameInfo(filePath, position); - } + return this.compiler.perfRecorder.inPhase(PerfPhase.LsReferencesAndRenames, () => { + const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler); + // We could not get a template at position so we assume the request came from outside the + // template. + if (templateInfo === undefined) { + return this.tsLS.getRenameInfo(filePath, position); + } - const allTargetDetails = this.getTargetDetailsAtTemplatePosition(templateInfo, position); - if (allTargetDetails === null) { - return {canRename: false, localizedErrorMessage: 'Could not find template node at position.'}; - } - const {templateTarget} = allTargetDetails[0]; - const templateTextAndSpan = getRenameTextAndSpanAtPosition(templateTarget, position); - if (templateTextAndSpan === null) { - return {canRename: false, localizedErrorMessage: 'Could not determine template node text.'}; - } - const {text, span} = templateTextAndSpan; - return { - canRename: true, - displayName: text, - fullDisplayName: text, - triggerSpan: span, - }; + const allTargetDetails = this.getTargetDetailsAtTemplatePosition(templateInfo, position); + if (allTargetDetails === null) { + return { + canRename: false, + localizedErrorMessage: 'Could not find template node at position.', + }; + } + const {templateTarget} = allTargetDetails[0]; + const templateTextAndSpan = getRenameTextAndSpanAtPosition(templateTarget, position); + if (templateTextAndSpan === null) { + return {canRename: false, localizedErrorMessage: 'Could not determine template node text.'}; + } + const {text, span} = templateTextAndSpan; + return { + canRename: true, + displayName: text, + fullDisplayName: text, + triggerSpan: span, + }; + }); } findRenameLocations(filePath: string, position: number): readonly ts.RenameLocation[]|undefined { this.ttc.generateAllTypeCheckBlocks(); - const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler); - // We could not get a template at position so we assume the request came from outside the - // template. - if (templateInfo === undefined) { - const requestNode = this.getTsNodeAtPosition(filePath, position); - if (requestNode === null) { - return undefined; + return this.compiler.perfRecorder.inPhase(PerfPhase.LsReferencesAndRenames, () => { + const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler); + // We could not get a template at position so we assume the request came from outside the + // template. + if (templateInfo === undefined) { + const requestNode = this.getTsNodeAtPosition(filePath, position); + if (requestNode === null) { + return undefined; + } + const requestOrigin: TypeScriptRequest = {kind: RequestKind.TypeScript, requestNode}; + return this.findRenameLocationsAtTypescriptPosition(filePath, position, requestOrigin); } - const requestOrigin: TypeScriptRequest = {kind: RequestKind.TypeScript, requestNode}; - return this.findRenameLocationsAtTypescriptPosition(filePath, position, requestOrigin); - } - return this.findRenameLocationsAtTemplatePosition(templateInfo, position); + return this.findRenameLocationsAtTemplatePosition(templateInfo, position); + }); } private findRenameLocationsAtTemplatePosition(templateInfo: TemplateInfo, position: number): @@ -148,55 +156,60 @@ export class ReferencesAndRenameBuilder { findRenameLocationsAtTypescriptPosition( filePath: string, position: number, requestOrigin: RequestOrigin): readonly ts.RenameLocation[]|undefined { - let originalNodeText: string; - if (requestOrigin.kind === RequestKind.TypeScript) { - originalNodeText = requestOrigin.requestNode.getText(); - } else { - const templateNodeText = - getRenameTextAndSpanAtPosition(requestOrigin.requestNode, requestOrigin.position); - if (templateNodeText === null) { + return this.compiler.perfRecorder.inPhase(PerfPhase.LsReferencesAndRenames, () => { + let originalNodeText: string; + if (requestOrigin.kind === RequestKind.TypeScript) { + originalNodeText = requestOrigin.requestNode.getText(); + } else { + const templateNodeText = + getRenameTextAndSpanAtPosition(requestOrigin.requestNode, requestOrigin.position); + if (templateNodeText === null) { + return undefined; + } + originalNodeText = templateNodeText.text; + } + + const locations = this.tsLS.findRenameLocations( + filePath, position, /*findInStrings*/ false, /*findInComments*/ false); + if (locations === undefined) { return undefined; } - originalNodeText = templateNodeText.text; - } - const locations = this.tsLS.findRenameLocations( - filePath, position, /*findInStrings*/ false, /*findInComments*/ false); - if (locations === undefined) { - return undefined; - } - - const entries: Map = new Map(); - for (const location of locations) { - // TODO(atscott): Determine if a file is a shim file in a more robust way and make the API - // available in an appropriate location. - if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(location.fileName))) { - const entry = this.convertToTemplateDocumentSpan(location, this.ttc, originalNodeText); - // There is no template node whose text matches the original rename request. Bail on - // renaming completely rather than providing incomplete results. - if (entry === null) { - return undefined; + const entries: Map = new Map(); + for (const location of locations) { + // TODO(atscott): Determine if a file is a shim file in a more robust way and make the API + // available in an appropriate location. + if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(location.fileName))) { + const entry = this.convertToTemplateDocumentSpan(location, this.ttc, originalNodeText); + // There is no template node whose text matches the original rename request. Bail on + // renaming completely rather than providing incomplete results. + if (entry === null) { + return undefined; + } + entries.set(createLocationKey(entry), entry); + } else { + // Ensure we only allow renaming a TS result with matching text + const refNode = this.getTsNodeAtPosition(location.fileName, location.textSpan.start); + if (refNode === null || refNode.getText() !== originalNodeText) { + return undefined; + } + entries.set(createLocationKey(location), location); } - entries.set(createLocationKey(entry), entry); - } else { - // Ensure we only allow renaming a TS result with matching text - const refNode = this.getTsNodeAtPosition(location.fileName, location.textSpan.start); - if (refNode === null || refNode.getText() !== originalNodeText) { - return undefined; - } - entries.set(createLocationKey(location), location); } - } - return Array.from(entries.values()); + return Array.from(entries.values()); + }); } getReferencesAtPosition(filePath: string, position: number): ts.ReferenceEntry[]|undefined { this.ttc.generateAllTypeCheckBlocks(); - const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler); - if (templateInfo === undefined) { - return this.getReferencesAtTypescriptPosition(filePath, position); - } - return this.getReferencesAtTemplatePosition(templateInfo, position); + + return this.compiler.perfRecorder.inPhase(PerfPhase.LsReferencesAndRenames, () => { + const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler); + if (templateInfo === undefined) { + return this.getReferencesAtTypescriptPosition(filePath, position); + } + return this.getReferencesAtTemplatePosition(templateInfo, position); + }); } private getReferencesAtTemplatePosition(templateInfo: TemplateInfo, position: number):