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
This commit is contained in:
Alex Rickabaugh 2021-03-15 15:23:03 -07:00
parent dd82c8e9f4
commit 48fec08c95
44 changed files with 1201 additions and 651 deletions

View File

@ -16,6 +16,7 @@ ts_library(
],
deps = [
"//packages/compiler-cli",
"//packages/compiler-cli/src/ngtsc/perf",
"@npm//@bazel/typescript",
"@npm//@types/node",
"@npm//tsickle",

View File

@ -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.

View File

@ -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());

View File

@ -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<unknown, unknown, SemanticSymbol|null,unknown>,
// 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[] = [

View File

@ -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",

View File

@ -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<Decorator, ts.ObjectLiteralExpression>();
private elementSchemaRegistry = new DomElementSchemaRegistry();
@ -309,6 +310,7 @@ export class ComponentDecoratorHandler implements
analyze(
node: ClassDeclaration, decorator: Readonly<Decorator>,
flags: HandlerFlags = HandlerFlags.NONE): AnalysisOutput<ComponentAnalysisData> {
this.perf.eventCount(PerfEvent.AnalyzeComponent);
const containingFile = node.getSourceFile().fileName;
this.literalCache.delete(decorator);

View File

@ -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);

View File

@ -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<Decorator>):
AnalysisOutput<InjectableHandlerData> {
this.perf.eventCount(PerfEvent.AnalyzeInjectable);
const meta = extractInjectableMetadata(node, decorator, this.reflector);
const decorators = this.reflector.getDecoratorsOfDeclaration(node);

View File

@ -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<Decorator>):
AnalysisOutput<NgModuleAnalysis> {
this.perf.eventCount(PerfEvent.AnalyzeNgModule);
const name = node.name.text;
if (decorator.args === null || decorator.args.length > 1) {
throw new FatalDiagnosticError(

View File

@ -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<Decorator>):
AnalysisOutput<PipeHandlerData> {
this.perf.eventCount(PerfEvent.AnalyzePipe);
const name = clazz.name.text;
const type = wrapTypeReference(this.reflector, clazz);
const internalType = new WrappedNodeExpr(this.reflector.getInternalNameOfClass(clazz));

View File

@ -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",

View File

@ -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};
}

View File

@ -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);

View File

@ -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) {

View File

@ -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 =

View File

@ -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.
*/

View File

@ -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<string>;
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<string>): CompilationTicket {
typeCheckingProgramStrategy: TypeCheckingProgramStrategy, modifiedResourceFiles: Set<string>,
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<string>,
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<ts.SourceFile>;
readonly ignoreForEmit: Set<ts.SourceFile>;
/**
* `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<string>): 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<string>, perfRecorder: ActivePerfRecorder): void {
this.livePerfRecorder = perfRecorder;
this.delegatingPerfRecorder.target = perfRecorder;
const classesToUpdate = new Set<DeclarationNode>();
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<DeclarationNode>();
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<void>[] = [];
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<void>[] = [];
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<DecoratorHandler<unknown, unknown, SemanticSymbol | null,unknown>>,
// 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,

View File

@ -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);
}

View File

@ -9,6 +9,7 @@ ts_library(
]),
module_name = "@angular/compiler-cli/src/ngtsc/cycles",
deps = [
"//packages/compiler-cli/src/ngtsc/perf",
"@npm//typescript",
],
)

View File

@ -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<ts.SourceFile, Set<ts.SourceFile>>();
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<ts.SourceFile> {
const imports = new Set<ts.SourceFile>();
// 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<ts.SourceFile>();
// 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;
});
}
}

View File

@ -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",
],

View File

@ -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)),
};
}
});

View File

@ -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),
};
}

View File

@ -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",

View File

@ -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<ClassRecord, FileType
*/
static reconcile(
oldProgram: ts.Program, oldDriver: IncrementalDriver, newProgram: ts.Program,
modifiedResourceFiles: Set<string>|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<AbsoluteFsPath>(),
changedTsPaths: new Set<string>(),
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<ts.SourceFile>(oldProgram.getSourceFiles());
// Assume all the old files were deleted to begin with. Only TS files are tracked.
const deletedTsPaths = new Set<string>(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<string>|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<string>|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<AbsoluteFsPath>(),
changedTsPaths: new Set<string>(),
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<ts.SourceFile>(oldProgram.getSourceFiles());
// Assume all the old files were deleted to begin with. Only TS files are tracked.
const deletedTsPaths = new Set<string>(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<string>|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 {

View File

@ -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",
],

View File

@ -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';

View File

@ -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<T>(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;
}

View File

@ -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<T>(phase: PerfPhase, fn: () => T): T {
return fn();
}
reset(): void {}
}
export const NOOP_PERF_RECORDER: PerfRecorder = new NoopPerfRecorder();

View File

@ -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
*/
/// <reference types="node" />
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<string, number>;
phases: Record<string, number>;
memory: Record<string, number>;
}
/**
* 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<T>(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<T>(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();
}
}

View File

@ -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
*/
/// <reference types="node" />
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,
}

View File

@ -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<string>, 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<ts.SourceFile>|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<ts.SourceFile>|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<DeclarationNode, IndexedComponent> {

View File

@ -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<unknown>;
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<unknown>` 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)) {

View File

@ -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<ts.SourceFile> {
const recordWrappedNodeExpr = createRecorderFn(defaultImportRecorder);
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
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));
};
};
}

View File

@ -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<ts.SourceFile>,
ignoreForEmit: Set<ts.SourceFile>,
} {
// 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;
}
}

View File

@ -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",

View File

@ -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<ts.CompilerHost, 'getCanonicalFileName'>,
private priorBuild: IncrementalBuild<unknown, FileTypeCheckingData>,
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 {

View File

@ -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<ts.CompilerHost, 'getCanonicalFileName'>,
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.

View File

@ -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",

View File

@ -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,

View File

@ -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(

View File

@ -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",

View File

@ -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;

View File

@ -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.RenameInfoSuccess, 'kind'|'kindModifiers'>|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<string, ts.RenameLocation> = 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<string, ts.RenameLocation> = 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):