refactor(compiler-cli): replace the `IncrementalDriver` with a new design (#41475)

This commit replaces the `IncrementalDriver` abstraction which powered
incremental compilation in the compiler with a new `IncrementalCompilation`
design. Principally, it separates two concerns which were tied together in
the previous implementation:

1. Tracking the reusable state of a compilation at any given point that
   could be reused in a subsequent future compilation.

2. Making use of a prior compilation's state to accelerate the current one.

The new abstraction adds explicit tracking and types to deal with both of
these concerns separately, which greatly reduces the complexity of the state
tracking that `IncrementalDriver` used to perform.

PR Close #41475
This commit is contained in:
Alex Rickabaugh 2021-04-06 17:59:20 -04:00 committed by Zach Arend
parent fab1a6468e
commit 94ec0af582
13 changed files with 516 additions and 452 deletions

View File

@ -85,7 +85,7 @@ export class NgccTraitCompiler extends TraitCompiler {
} }
class NoIncrementalBuild implements IncrementalBuild<any, any> { class NoIncrementalBuild implements IncrementalBuild<any, any> {
priorWorkFor(sf: ts.SourceFile): any[]|null { priorAnalysisFor(sf: ts.SourceFile): any[]|null {
return null; return null;
} }

View File

@ -15,7 +15,7 @@ import {COMPILER_ERRORS_WITH_GUIDES, ERROR_DETAILS_PAGE_BASE_URL, ErrorCode, ngE
import {checkForPrivateExports, ReferenceGraph} from '../../entry_point'; import {checkForPrivateExports, ReferenceGraph} from '../../entry_point';
import {AbsoluteFsPath, LogicalFileSystem, resolve} from '../../file_system'; import {AbsoluteFsPath, LogicalFileSystem, resolve} from '../../file_system';
import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports'; import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports';
import {IncrementalBuildStrategy, IncrementalDriver} from '../../incremental'; import {IncrementalBuildStrategy, IncrementalCompilation, IncrementalState} from '../../incremental';
import {SemanticSymbol} from '../../incremental/semantic_graph'; import {SemanticSymbol} from '../../incremental/semantic_graph';
import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer'; import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer';
import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, ResourceRegistry} from '../../metadata'; import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, ResourceRegistry} from '../../metadata';
@ -89,11 +89,10 @@ export interface FreshCompilationTicket {
export interface IncrementalTypeScriptCompilationTicket { export interface IncrementalTypeScriptCompilationTicket {
kind: CompilationTicketKind.IncrementalTypeScript; kind: CompilationTicketKind.IncrementalTypeScript;
options: NgCompilerOptions; options: NgCompilerOptions;
oldProgram: ts.Program;
newProgram: ts.Program; newProgram: ts.Program;
incrementalBuildStrategy: IncrementalBuildStrategy; incrementalBuildStrategy: IncrementalBuildStrategy;
incrementalCompilation: IncrementalCompilation;
programDriver: ProgramDriver; programDriver: ProgramDriver;
newDriver: IncrementalDriver;
enableTemplateTypeChecker: boolean; enableTemplateTypeChecker: boolean;
usePoisonedData: boolean; usePoisonedData: boolean;
perfRecorder: ActivePerfRecorder; perfRecorder: ActivePerfRecorder;
@ -143,10 +142,11 @@ export function freshCompilationTicket(
export function incrementalFromCompilerTicket( export function incrementalFromCompilerTicket(
oldCompiler: NgCompiler, newProgram: ts.Program, oldCompiler: NgCompiler, newProgram: ts.Program,
incrementalBuildStrategy: IncrementalBuildStrategy, programDriver: ProgramDriver, incrementalBuildStrategy: IncrementalBuildStrategy, programDriver: ProgramDriver,
modifiedResourceFiles: Set<string>, perfRecorder: ActivePerfRecorder|null): CompilationTicket { modifiedResourceFiles: Set<AbsoluteFsPath>,
perfRecorder: ActivePerfRecorder|null): CompilationTicket {
const oldProgram = oldCompiler.getCurrentProgram(); const oldProgram = oldCompiler.getCurrentProgram();
const oldDriver = oldCompiler.incrementalStrategy.getIncrementalDriver(oldProgram); const oldState = oldCompiler.incrementalStrategy.getIncrementalState(oldProgram);
if (oldDriver === null) { if (oldState === null) {
// No incremental step is possible here, since no IncrementalDriver was found for the old // No incremental step is possible here, since no IncrementalDriver was found for the old
// program. // program.
return freshCompilationTicket( return freshCompilationTicket(
@ -158,8 +158,8 @@ export function incrementalFromCompilerTicket(
perfRecorder = ActivePerfRecorder.zeroedToNow(); perfRecorder = ActivePerfRecorder.zeroedToNow();
} }
const newDriver = IncrementalDriver.reconcile( const incrementalCompilation = IncrementalCompilation.incremental(
oldProgram, oldDriver, newProgram, modifiedResourceFiles, perfRecorder); newProgram, oldProgram, oldState, modifiedResourceFiles, perfRecorder);
return { return {
kind: CompilationTicketKind.IncrementalTypeScript, kind: CompilationTicketKind.IncrementalTypeScript,
@ -167,9 +167,8 @@ export function incrementalFromCompilerTicket(
usePoisonedData: oldCompiler.usePoisonedData, usePoisonedData: oldCompiler.usePoisonedData,
options: oldCompiler.options, options: oldCompiler.options,
incrementalBuildStrategy, incrementalBuildStrategy,
incrementalCompilation,
programDriver, programDriver,
newDriver,
oldProgram,
newProgram, newProgram,
perfRecorder, perfRecorder,
}; };
@ -179,24 +178,23 @@ export function incrementalFromCompilerTicket(
* Create a `CompilationTicket` directly from an old `ts.Program` and associated Angular compilation * Create a `CompilationTicket` directly from an old `ts.Program` and associated Angular compilation
* state, along with a new `ts.Program`. * state, along with a new `ts.Program`.
*/ */
export function incrementalFromDriverTicket( export function incrementalFromStateTicket(
oldProgram: ts.Program, oldDriver: IncrementalDriver, newProgram: ts.Program, oldProgram: ts.Program, oldState: IncrementalState, newProgram: ts.Program,
options: NgCompilerOptions, incrementalBuildStrategy: IncrementalBuildStrategy, options: NgCompilerOptions, incrementalBuildStrategy: IncrementalBuildStrategy,
programDriver: ProgramDriver, modifiedResourceFiles: Set<string>, programDriver: ProgramDriver, modifiedResourceFiles: Set<AbsoluteFsPath>,
perfRecorder: ActivePerfRecorder|null, enableTemplateTypeChecker: boolean, perfRecorder: ActivePerfRecorder|null, enableTemplateTypeChecker: boolean,
usePoisonedData: boolean): CompilationTicket { usePoisonedData: boolean): CompilationTicket {
if (perfRecorder === null) { if (perfRecorder === null) {
perfRecorder = ActivePerfRecorder.zeroedToNow(); perfRecorder = ActivePerfRecorder.zeroedToNow();
} }
const newDriver = IncrementalDriver.reconcile( const incrementalCompilation = IncrementalCompilation.incremental(
oldProgram, oldDriver, newProgram, modifiedResourceFiles, perfRecorder); newProgram, oldProgram, oldState, modifiedResourceFiles, perfRecorder);
return { return {
kind: CompilationTicketKind.IncrementalTypeScript, kind: CompilationTicketKind.IncrementalTypeScript,
oldProgram,
newProgram, newProgram,
options, options,
incrementalBuildStrategy, incrementalBuildStrategy,
newDriver, incrementalCompilation,
programDriver, programDriver,
enableTemplateTypeChecker, enableTemplateTypeChecker,
usePoisonedData, usePoisonedData,
@ -284,7 +282,7 @@ export class NgCompiler {
ticket.tsProgram, ticket.tsProgram,
ticket.programDriver, ticket.programDriver,
ticket.incrementalBuildStrategy, ticket.incrementalBuildStrategy,
IncrementalDriver.fresh(ticket.tsProgram), IncrementalCompilation.fresh(ticket.tsProgram),
ticket.enableTemplateTypeChecker, ticket.enableTemplateTypeChecker,
ticket.usePoisonedData, ticket.usePoisonedData,
ticket.perfRecorder, ticket.perfRecorder,
@ -296,7 +294,7 @@ export class NgCompiler {
ticket.newProgram, ticket.newProgram,
ticket.programDriver, ticket.programDriver,
ticket.incrementalBuildStrategy, ticket.incrementalBuildStrategy,
ticket.newDriver, ticket.incrementalCompilation,
ticket.enableTemplateTypeChecker, ticket.enableTemplateTypeChecker,
ticket.usePoisonedData, ticket.usePoisonedData,
ticket.perfRecorder, ticket.perfRecorder,
@ -314,7 +312,7 @@ export class NgCompiler {
private inputProgram: ts.Program, private inputProgram: ts.Program,
readonly programDriver: ProgramDriver, readonly programDriver: ProgramDriver,
readonly incrementalStrategy: IncrementalBuildStrategy, readonly incrementalStrategy: IncrementalBuildStrategy,
readonly incrementalDriver: IncrementalDriver, readonly incrementalCompilation: IncrementalCompilation,
readonly enableTemplateTypeChecker: boolean, readonly enableTemplateTypeChecker: boolean,
readonly usePoisonedData: boolean, readonly usePoisonedData: boolean,
private livePerfRecorder: ActivePerfRecorder, private livePerfRecorder: ActivePerfRecorder,
@ -343,7 +341,7 @@ export class NgCompiler {
this.resourceManager = new AdapterResourceLoader(adapter, this.options); this.resourceManager = new AdapterResourceLoader(adapter, this.options);
this.cycleAnalyzer = new CycleAnalyzer( this.cycleAnalyzer = new CycleAnalyzer(
new ImportGraph(inputProgram.getTypeChecker(), this.delegatingPerfRecorder)); new ImportGraph(inputProgram.getTypeChecker(), this.delegatingPerfRecorder));
this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, inputProgram); this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, inputProgram);
this.ignoreForDiagnostics = this.ignoreForDiagnostics =
new Set(inputProgram.getSourceFiles().filter(sf => this.adapter.isShim(sf))); new Set(inputProgram.getSourceFiles().filter(sf => this.adapter.isShim(sf)));
@ -367,6 +365,16 @@ export class NgCompiler {
return this.livePerfRecorder; return this.livePerfRecorder;
} }
/**
* Exposes the `IncrementalCompilation` under an old property name that the CLI uses, avoiding a
* chicken-and-egg problem with the rename to `incrementalCompilation`.
*
* TODO(alxhub): remove when the CLI uses the new name.
*/
get incrementalDriver(): IncrementalCompilation {
return this.incrementalCompilation;
}
private updateWithChangedResources( private updateWithChangedResources(
changedResources: Set<string>, perfRecorder: ActivePerfRecorder): void { changedResources: Set<string>, perfRecorder: ActivePerfRecorder): void {
this.livePerfRecorder = perfRecorder; this.livePerfRecorder = perfRecorder;
@ -411,7 +419,7 @@ export class NgCompiler {
getResourceDependencies(file: ts.SourceFile): string[] { getResourceDependencies(file: ts.SourceFile): string[] {
this.ensureAnalyzed(); this.ensureAnalyzed();
return this.incrementalDriver.depGraph.getResourceDependencies(file); return this.incrementalCompilation.depGraph.getResourceDependencies(file);
} }
/** /**
@ -684,7 +692,7 @@ export class NgCompiler {
// At this point, analysis is complete and the compiler can now calculate which files need to // At this point, analysis is complete and the compiler can now calculate which files need to
// be emitted, so do that. // be emitted, so do that.
this.incrementalDriver.recordSuccessfulAnalysis(traitCompiler); this.incrementalCompilation.recordSuccessfulAnalysis(traitCompiler);
this.perfRecorder.memory(PerfCheckpoint.Resolve); this.perfRecorder.memory(PerfCheckpoint.Resolve);
}); });
@ -829,7 +837,7 @@ export class NgCompiler {
} }
const program = this.programDriver.getProgram(); const program = this.programDriver.getProgram();
this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, program); this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, program);
this.currentProgram = program; this.currentProgram = program;
return diagnostics; return diagnostics;
@ -846,7 +854,7 @@ export class NgCompiler {
} }
const program = this.programDriver.getProgram(); const program = this.programDriver.getProgram();
this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, program); this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, program);
this.currentProgram = program; this.currentProgram = program;
return diagnostics; return diagnostics;
@ -935,7 +943,8 @@ export class NgCompiler {
aliasingHost = new UnifiedModulesAliasingHost(this.adapter.unifiedModulesHost); aliasingHost = new UnifiedModulesAliasingHost(this.adapter.unifiedModulesHost);
} }
const evaluator = new PartialEvaluator(reflector, checker, this.incrementalDriver.depGraph); const evaluator =
new PartialEvaluator(reflector, checker, this.incrementalCompilation.depGraph);
const dtsReader = new DtsMetadataReader(checker, reflector); const dtsReader = new DtsMetadataReader(checker, reflector);
const localMetaRegistry = new LocalMetadataRegistry(); const localMetaRegistry = new LocalMetadataRegistry();
const localMetaReader: MetadataReader = localMetaRegistry; const localMetaReader: MetadataReader = localMetaRegistry;
@ -943,7 +952,7 @@ export class NgCompiler {
const scopeRegistry = const scopeRegistry =
new LocalModuleScopeRegistry(localMetaReader, depScopeReader, refEmitter, aliasingHost); new LocalModuleScopeRegistry(localMetaReader, depScopeReader, refEmitter, aliasingHost);
const scopeReader: ComponentScopeReader = scopeRegistry; const scopeReader: ComponentScopeReader = scopeRegistry;
const semanticDepGraphUpdater = this.incrementalDriver.getSemanticDepGraphUpdater(); const semanticDepGraphUpdater = this.incrementalCompilation.semanticDepGraphUpdater;
const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]); const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]);
const injectableRegistry = new InjectableClassRegistry(reflector); const injectableRegistry = new InjectableClassRegistry(reflector);
@ -992,8 +1001,9 @@ export class NgCompiler {
this.options.i18nUseExternalIds !== false, this.options.i18nUseExternalIds !== false,
this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData, this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData,
this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer, this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer,
cycleHandlingStrategy, refEmitter, this.incrementalDriver.depGraph, injectableRegistry, cycleHandlingStrategy, refEmitter, this.incrementalCompilation.depGraph,
semanticDepGraphUpdater, this.closureCompilerEnabled, this.delegatingPerfRecorder), injectableRegistry, semanticDepGraphUpdater, this.closureCompilerEnabled,
this.delegatingPerfRecorder),
// TODO(alxhub): understand why the cast here is necessary (something to do with `null` // TODO(alxhub): understand why the cast here is necessary (something to do with `null`
// not being assignable to `unknown` when wrapped in `Readonly`). // not being assignable to `unknown` when wrapped in `Readonly`).
@ -1020,7 +1030,7 @@ export class NgCompiler {
]; ];
const traitCompiler = new TraitCompiler( const traitCompiler = new TraitCompiler(
handlers, reflector, this.delegatingPerfRecorder, this.incrementalDriver, handlers, reflector, this.delegatingPerfRecorder, this.incrementalCompilation,
this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms, this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms,
semanticDepGraphUpdater); semanticDepGraphUpdater);
@ -1028,13 +1038,13 @@ export class NgCompiler {
// happens, they need to be tracked by the `NgCompiler`. // happens, they need to be tracked by the `NgCompiler`.
const notifyingDriver = const notifyingDriver =
new NotifyingProgramDriverWrapper(this.programDriver, (program: ts.Program) => { new NotifyingProgramDriverWrapper(this.programDriver, (program: ts.Program) => {
this.incrementalStrategy.setIncrementalDriver(this.incrementalDriver, program); this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, program);
this.currentProgram = program; this.currentProgram = program;
}); });
const templateTypeChecker = new TemplateTypeCheckerImpl( const templateTypeChecker = new TemplateTypeCheckerImpl(
this.inputProgram, notifyingDriver, traitCompiler, this.getTypeCheckingConfig(), refEmitter, this.inputProgram, notifyingDriver, traitCompiler, this.getTypeCheckingConfig(), refEmitter,
reflector, this.adapter, this.incrementalDriver, scopeRegistry, typeCheckScopeRegistry, reflector, this.adapter, this.incrementalCompilation, scopeRegistry, typeCheckScopeRegistry,
this.delegatingPerfRecorder); this.delegatingPerfRecorder);
return { return {

View File

@ -21,7 +21,7 @@ export interface IncrementalBuild<AnalysisT, FileTypeCheckDataT> {
/** /**
* Retrieve the prior analysis work, if any, done for the given source file. * Retrieve the prior analysis work, if any, done for the given source file.
*/ */
priorWorkFor(sf: ts.SourceFile): AnalysisT[]|null; priorAnalysisFor(sf: ts.SourceFile): AnalysisT[]|null;
/** /**
* Retrieve the prior type-checking work, if any, that's been done for the given source file. * Retrieve the prior type-checking work, if any, that's been done for the given source file.

View File

@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
export {IncrementalCompilation} from './src/incremental';
export {NOOP_INCREMENTAL_BUILD} from './src/noop'; export {NOOP_INCREMENTAL_BUILD} from './src/noop';
export {IncrementalDriver} from './src/state'; export {AnalyzedIncrementalState, DeltaIncrementalState, FreshIncrementalState, IncrementalState, IncrementalStateKind} from './src/state';
export * from './src/strategy'; export * from './src/strategy';

View File

@ -8,7 +8,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../file_system'; import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system';
import {DependencyTracker} from '../api'; import {DependencyTracker} from '../api';
/** /**
@ -28,7 +28,7 @@ export class FileDependencyGraph<T extends {fileName: string} = ts.SourceFile> i
private nodes = new Map<T, FileNode>(); private nodes = new Map<T, FileNode>();
addDependency(from: T, on: T): void { addDependency(from: T, on: T): void {
this.nodeFor(from).dependsOn.add(on.fileName); this.nodeFor(from).dependsOn.add(absoluteFromSourceFile(on));
} }
addResourceDependency(from: T, resource: AbsoluteFsPath): void { addResourceDependency(from: T, resource: AbsoluteFsPath): void {
@ -67,15 +67,17 @@ export class FileDependencyGraph<T extends {fileName: string} = ts.SourceFile> i
* P(n) = the physically changed files from build n - 1 to build n. * P(n) = the physically changed files from build n - 1 to build n.
*/ */
updateWithPhysicalChanges( updateWithPhysicalChanges(
previous: FileDependencyGraph<T>, changedTsPaths: Set<string>, deletedTsPaths: Set<string>, previous: FileDependencyGraph<T>, changedTsPaths: Set<AbsoluteFsPath>,
changedResources: Set<AbsoluteFsPath>): Set<string> { deletedTsPaths: Set<AbsoluteFsPath>,
const logicallyChanged = new Set<string>(); changedResources: Set<AbsoluteFsPath>): Set<AbsoluteFsPath> {
const logicallyChanged = new Set<AbsoluteFsPath>();
for (const sf of previous.nodes.keys()) { for (const sf of previous.nodes.keys()) {
const sfPath = absoluteFromSourceFile(sf);
const node = previous.nodeFor(sf); const node = previous.nodeFor(sf);
if (isLogicallyChanged(sf, node, changedTsPaths, deletedTsPaths, changedResources)) { if (isLogicallyChanged(sf, node, changedTsPaths, deletedTsPaths, changedResources)) {
logicallyChanged.add(sf.fileName); logicallyChanged.add(sfPath);
} else if (!deletedTsPaths.has(sf.fileName)) { } else if (!deletedTsPaths.has(sfPath)) {
this.nodes.set(sf, { this.nodes.set(sf, {
dependsOn: new Set(node.dependsOn), dependsOn: new Set(node.dependsOn),
usesResources: new Set(node.usesResources), usesResources: new Set(node.usesResources),
@ -90,7 +92,7 @@ export class FileDependencyGraph<T extends {fileName: string} = ts.SourceFile> i
private nodeFor(sf: T): FileNode { private nodeFor(sf: T): FileNode {
if (!this.nodes.has(sf)) { if (!this.nodes.has(sf)) {
this.nodes.set(sf, { this.nodes.set(sf, {
dependsOn: new Set<string>(), dependsOn: new Set<AbsoluteFsPath>(),
usesResources: new Set<AbsoluteFsPath>(), usesResources: new Set<AbsoluteFsPath>(),
failedAnalysis: false, failedAnalysis: false,
}); });
@ -104,7 +106,8 @@ export class FileDependencyGraph<T extends {fileName: string} = ts.SourceFile> i
* changed files and resources. * changed files and resources.
*/ */
function isLogicallyChanged<T extends {fileName: string}>( function isLogicallyChanged<T extends {fileName: string}>(
sf: T, node: FileNode, changedTsPaths: ReadonlySet<string>, deletedTsPaths: ReadonlySet<string>, sf: T, node: FileNode, changedTsPaths: ReadonlySet<AbsoluteFsPath>,
deletedTsPaths: ReadonlySet<AbsoluteFsPath>,
changedResources: ReadonlySet<AbsoluteFsPath>): boolean { changedResources: ReadonlySet<AbsoluteFsPath>): boolean {
// A file is assumed to have logically changed if its dependencies could not be determined // A file is assumed to have logically changed if its dependencies could not be determined
// accurately. // accurately.
@ -112,8 +115,10 @@ function isLogicallyChanged<T extends {fileName: string}>(
return true; return true;
} }
const sfPath = absoluteFromSourceFile(sf);
// A file is logically changed if it has physically changed itself (including being deleted). // A file is logically changed if it has physically changed itself (including being deleted).
if (changedTsPaths.has(sf.fileName) || deletedTsPaths.has(sf.fileName)) { if (changedTsPaths.has(sfPath) || deletedTsPaths.has(sfPath)) {
return true; return true;
} }
@ -134,7 +139,7 @@ function isLogicallyChanged<T extends {fileName: string}>(
} }
interface FileNode { interface FileNode {
dependsOn: Set<string>; dependsOn: Set<AbsoluteFsPath>;
usesResources: Set<AbsoluteFsPath>; usesResources: Set<AbsoluteFsPath>;
failedAnalysis: boolean; failedAnalysis: boolean;
} }

View File

@ -0,0 +1,348 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {absoluteFromSourceFile, AbsoluteFsPath, resolve} from '../../file_system';
import {PerfPhase, PerfRecorder} from '../../perf';
import {ClassRecord, TraitCompiler} from '../../transform';
import {FileTypeCheckingData} from '../../typecheck';
import {toUnredirectedSourceFile} from '../../util/src/typescript';
import {IncrementalBuild} from '../api';
import {SemanticDepGraphUpdater} from '../semantic_graph';
import {FileDependencyGraph} from './dependency_tracking';
import {AnalyzedIncrementalState, DeltaIncrementalState, IncrementalState, IncrementalStateKind} from './state';
/**
* Information about the previous compilation being used as a starting point for the current one,
* including the delta of files which have logically changed and need to be reanalyzed.
*/
interface IncrementalStep {
priorState: AnalyzedIncrementalState;
logicallyChangedTsFiles: Set<AbsoluteFsPath>;
}
/**
* Discriminant of the `Phase` type union.
*/
enum PhaseKind {
Analysis,
TypeCheckAndEmit,
}
/**
* An incremental compilation undergoing analysis, and building a semantic dependency graph.
*/
interface AnalysisPhase {
kind: PhaseKind.Analysis;
semanticDepGraphUpdater: SemanticDepGraphUpdater;
}
/**
* An incremental compilation that completed analysis and is undergoing template type-checking and
* emit.
*/
interface TypeCheckAndEmitPhase {
kind: PhaseKind.TypeCheckAndEmit;
needsEmit: Set<AbsoluteFsPath>;
needsTypeCheckEmit: Set<AbsoluteFsPath>;
}
/**
* Represents the current phase of a compilation.
*/
type Phase = AnalysisPhase|TypeCheckAndEmitPhase;
/**
* Manages the incremental portion of an Angular compilation, allowing for reuse of a prior
* compilation if available, and producing an output state for reuse of the current compilation in a
* future one.
*/
export class IncrementalCompilation implements IncrementalBuild<ClassRecord, FileTypeCheckingData> {
private phase: Phase;
/**
* `IncrementalState` of this compilation if it were to be reused in a subsequent incremental
* compilation at the current moment.
*
* Exposed via the `state` read-only getter.
*/
private _state: IncrementalState;
private constructor(
state: IncrementalState, readonly depGraph: FileDependencyGraph,
private step: IncrementalStep|null) {
this._state = state;
// The compilation begins in analysis phase.
this.phase = {
kind: PhaseKind.Analysis,
semanticDepGraphUpdater:
new SemanticDepGraphUpdater(step !== null ? step.priorState.semanticDepGraph : null),
};
}
/**
* Begin a fresh `IncrementalCompilation`.
*/
static fresh(program: ts.Program): IncrementalCompilation {
const state: IncrementalState = {
kind: IncrementalStateKind.Fresh,
};
return new IncrementalCompilation(state, new FileDependencyGraph(), /* reuse */ null);
}
static incremental(
program: ts.Program, oldProgram: ts.Program, oldState: IncrementalState,
modifiedResourceFiles: Set<AbsoluteFsPath>|null, perf: PerfRecorder): IncrementalCompilation {
return perf.inPhase(PerfPhase.Reconciliation, () => {
let priorAnalysis: AnalyzedIncrementalState;
const physicallyChangedTsFiles = new Set<AbsoluteFsPath>();
const changedResourceFiles = new Set<AbsoluteFsPath>(modifiedResourceFiles ?? []);
switch (oldState.kind) {
case IncrementalStateKind.Fresh:
// Since this line of program has never been successfully analyzed to begin with, treat
// this as a fresh compilation.
return IncrementalCompilation.fresh(program);
case IncrementalStateKind.Analyzed:
// The most recent program was analyzed successfully, so we can use that as our prior
// state and don't need to consider any other deltas except changes in the most recent
// program.
priorAnalysis = oldState;
break;
case IncrementalStateKind.Delta:
// There is an ancestor program which was analyzed successfully and can be used as a
// starting point, but we need to determine what's changed since that program.
priorAnalysis = oldState.lastAnalyzedState;
for (const sfPath of oldState.physicallyChangedTsFiles) {
physicallyChangedTsFiles.add(sfPath);
}
for (const resourcePath of oldState.changedResourceFiles) {
changedResourceFiles.add(resourcePath);
}
break;
}
const oldFilesArray = oldProgram.getSourceFiles().map(sf => toUnredirectedSourceFile(sf));
const oldFiles = new Set(oldFilesArray);
const deletedTsFiles = new Set(oldFilesArray.map(sf => absoluteFromSourceFile(sf)));
for (const possiblyRedirectedNewFile of program.getSourceFiles()) {
const sf = toUnredirectedSourceFile(possiblyRedirectedNewFile);
const sfPath = absoluteFromSourceFile(sf);
// Since we're seeing a file in the incoming program with this name, it can't have been
// deleted.
deletedTsFiles.delete(sfPath);
if (oldFiles.has(sf)) {
// This file hasn't changed.
continue;
}
// Bail out if a .d.ts file changes - the semantic dep graph is not able to process such
// changes correctly yet.
if (sf.isDeclarationFile) {
return IncrementalCompilation.fresh(program);
}
// The file has changed physically, so record it.
physicallyChangedTsFiles.add(sfPath);
}
// Remove any files that have been deleted from the list of physical changes.
for (const deletedFileName of deletedTsFiles) {
physicallyChangedTsFiles.delete(resolve(deletedFileName));
}
// Use the prior dependency graph to project physical changes into a set of logically changed
// files.
const depGraph = new FileDependencyGraph();
const logicallyChangedTsFiles = depGraph.updateWithPhysicalChanges(
priorAnalysis.depGraph, physicallyChangedTsFiles, deletedTsFiles, changedResourceFiles);
// Physically changed files aren't necessarily counted as logically changed by the dependency
// graph (files do not have edges to themselves), so add them to the logical changes
// explicitly.
for (const sfPath of physicallyChangedTsFiles) {
logicallyChangedTsFiles.add(sfPath);
}
// Start off in a `DeltaIncrementalState` as a delta against the previous successful analysis,
// until this compilation completes its own analysis.
const state: DeltaIncrementalState = {
kind: IncrementalStateKind.Delta,
physicallyChangedTsFiles,
changedResourceFiles,
lastAnalyzedState: priorAnalysis,
};
return new IncrementalCompilation(state, depGraph, {
priorState: priorAnalysis,
logicallyChangedTsFiles,
});
});
}
get state(): IncrementalState {
return this._state;
}
get semanticDepGraphUpdater(): SemanticDepGraphUpdater {
if (this.phase.kind !== PhaseKind.Analysis) {
throw new Error(
`AssertionError: Cannot update the SemanticDepGraph after analysis completes`);
}
return this.phase.semanticDepGraphUpdater;
}
recordSuccessfulAnalysis(traitCompiler: TraitCompiler): void {
if (this.phase.kind !== PhaseKind.Analysis) {
throw new Error(`AssertionError: Incremental compilation in phase ${
PhaseKind[this.phase.kind]}, expected Analysis`);
}
const {needsEmit, needsTypeCheckEmit, newGraph} = this.phase.semanticDepGraphUpdater.finalize();
// Determine the set of files which have already been emitted.
let emitted: Set<AbsoluteFsPath>;
if (this.step === null) {
// Since there is no prior compilation, no files have yet been emitted.
emitted = new Set();
} else {
// Begin with the files emitted by the prior successful compilation, but remove those which we
// know need to bee re-emitted.
emitted = new Set(this.step.priorState.emitted);
// Files need re-emitted if they've logically changed.
for (const sfPath of this.step.logicallyChangedTsFiles) {
emitted.delete(sfPath);
}
// Files need re-emitted if they've semantically changed.
for (const sfPath of needsEmit) {
emitted.delete(sfPath);
}
}
// Transition to a successfully analyzed compilation. At this point, a subsequent compilation
// could use this state as a starting point.
this._state = {
kind: IncrementalStateKind.Analyzed,
depGraph: this.depGraph,
semanticDepGraph: newGraph,
traitCompiler,
typeCheckResults: null,
emitted,
};
// We now enter the type-check and emit phase of compilation.
this.phase = {
kind: PhaseKind.TypeCheckAndEmit,
needsEmit,
needsTypeCheckEmit,
};
}
recordSuccessfulTypeCheck(results: Map<AbsoluteFsPath, FileTypeCheckingData>): void {
if (this._state.kind !== IncrementalStateKind.Analyzed) {
throw new Error(`AssertionError: Expected successfully analyzed compilation.`);
} else if (this.phase.kind !== PhaseKind.TypeCheckAndEmit) {
throw new Error(`AssertionError: Incremental compilation in phase ${
PhaseKind[this.phase.kind]}, expected TypeCheck`);
}
this._state.typeCheckResults = results;
}
recordSuccessfulEmit(sf: ts.SourceFile): void {
if (this._state.kind !== IncrementalStateKind.Analyzed) {
throw new Error(`AssertionError: Expected successfully analyzed compilation.`);
}
this._state.emitted.add(absoluteFromSourceFile(sf));
}
priorAnalysisFor(sf: ts.SourceFile): ClassRecord[]|null {
if (this.step === null) {
return null;
}
const sfPath = absoluteFromSourceFile(sf);
// If the file has logically changed, its previous analysis cannot be reused.
if (this.step.logicallyChangedTsFiles.has(sfPath)) {
return null;
}
return this.step.priorState.traitCompiler.recordsFor(sf);
}
priorTypeCheckingResultsFor(sf: ts.SourceFile): FileTypeCheckingData|null {
if (this.phase.kind !== PhaseKind.TypeCheckAndEmit) {
throw new Error(`AssertionError: Expected successfully analyzed compilation.`);
}
if (this.step === null) {
return null;
}
const sfPath = absoluteFromSourceFile(sf);
// If the file has logically changed, or its template type-checking results have semantically
// changed, then past type-checking results cannot be reused.
if (this.step.logicallyChangedTsFiles.has(sfPath) ||
this.phase.needsTypeCheckEmit.has(sfPath)) {
return null;
}
// Past results also cannot be reused if they're not available.
if (this.step.priorState.typeCheckResults === null ||
!this.step.priorState.typeCheckResults.has(sfPath)) {
return null;
}
const priorResults = this.step.priorState.typeCheckResults.get(sfPath)!;
// If the past results relied on inlining, they're not safe for reuse.
if (priorResults.hasInlines) {
return null;
}
return priorResults;
}
safeToSkipEmit(sf: ts.SourceFile): boolean {
// If this is a fresh compilation, it's never safe to skip an emit.
if (this.step === null) {
return false;
}
const sfPath = absoluteFromSourceFile(sf);
// If the file has itself logically changed, it must be emitted.
if (this.step.logicallyChangedTsFiles.has(sfPath)) {
return false;
}
if (this.phase.kind !== PhaseKind.TypeCheckAndEmit) {
throw new Error(
`AssertionError: Expected successful analysis before attempting to emit files`);
}
// If during analysis it was determined that this file has semantically changed, it must be
// emitted.
if (this.phase.needsEmit.has(sfPath)) {
return false;
}
// Generally it should be safe to assume here that the file was previously emitted by the last
// successful compilation. However, as a defense-in-depth against incorrectness, we explicitly
// check that the last emit included this file, and re-emit it otherwise.
return this.step.priorState.emitted.has(sfPath);
}
}

View File

@ -9,7 +9,7 @@
import {IncrementalBuild} from '../api'; import {IncrementalBuild} from '../api';
export const NOOP_INCREMENTAL_BUILD: IncrementalBuild<any, any> = { export const NOOP_INCREMENTAL_BUILD: IncrementalBuild<any, any> = {
priorWorkFor: () => null, priorAnalysisFor: () => null,
priorTypeCheckingResultsFor: () => null, priorTypeCheckingResultsFor: () => null,
recordSuccessfulTypeCheck: () => {}, recordSuccessfulTypeCheck: () => {},
}; };

View File

@ -6,409 +6,101 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../file_system';
import {TraitCompiler} from '../../transform';
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'; import {FileTypeCheckingData} from '../../typecheck/src/checker';
import {toUnredirectedSourceFile} from '../../util/src/typescript'; import {SemanticDepGraph} from '../semantic_graph';
import {IncrementalBuild} from '../api';
import {SemanticDepGraph, SemanticDepGraphUpdater} from '../semantic_graph';
import {FileDependencyGraph} from './dependency_tracking'; import {FileDependencyGraph} from './dependency_tracking';
/** /**
* Drives an incremental build, by tracking changes and determining which files need to be emitted. * Discriminant of the `IncrementalState` union.
*/ */
export class IncrementalDriver implements IncrementalBuild<ClassRecord, FileTypeCheckingData> { export enum IncrementalStateKind {
/** Fresh,
* State of the current build. Delta,
*
* This transitions as the compilation progresses.
*/
private state: BuildState;
private constructor(
state: PendingBuildState, readonly depGraph: FileDependencyGraph,
private logicalChanges: Set<string>|null) {
this.state = state;
}
/**
* Construct an `IncrementalDriver` with a starting state that incorporates the results of a
* previous build.
*
* The previous build's `BuildState` is reconciled with the new program's changes, and the results
* are merged into the new build's `PendingBuildState`.
*/
static reconcile(
oldProgram: ts.Program, oldDriver: IncrementalDriver, newProgram: ts.Program,
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 {
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().map(toUnredirectedSourceFile));
// 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 possiblyRedirectedNewFile of newProgram.getSourceFiles()) {
const newFile = toUnredirectedSourceFile(possiblyRedirectedNewFile);
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 {
// Initialize the set of files which need to be emitted to the set of all TS files in the
// program.
const tsFiles = tsOnlyFiles(program);
const state: PendingBuildState = {
kind: BuildStateKind.Pending,
pendingEmit: new Set<string>(tsFiles.map(sf => sf.fileName)),
pendingTypeCheckEmit: new Set<string>(tsFiles.map(sf => sf.fileName)),
changedResourcePaths: new Set<AbsoluteFsPath>(),
changedTsPaths: new Set<string>(),
lastGood: null,
semanticDepGraphUpdater: new SemanticDepGraphUpdater(/* priorGraph */ null),
};
return new IncrementalDriver(state, new FileDependencyGraph(), /* logicalChanges */ null);
}
getSemanticDepGraphUpdater(): SemanticDepGraphUpdater {
if (this.state.kind !== BuildStateKind.Pending) {
throw new Error('Semantic dependency updater is only available when pending analysis');
}
return this.state.semanticDepGraphUpdater;
}
recordSuccessfulAnalysis(traitCompiler: TraitCompiler): void {
if (this.state.kind !== BuildStateKind.Pending) {
// Changes have already been incorporated.
return;
}
const {needsEmit, needsTypeCheckEmit, newGraph} = this.state.semanticDepGraphUpdater.finalize();
const pendingEmit = this.state.pendingEmit;
for (const path of needsEmit) {
pendingEmit.add(path);
}
const pendingTypeCheckEmit = this.state.pendingTypeCheckEmit;
for (const path of needsTypeCheckEmit) {
pendingTypeCheckEmit.add(path);
}
// Update the state to an `AnalyzedBuildState`.
this.state = {
kind: BuildStateKind.Analyzed,
pendingEmit,
pendingTypeCheckEmit,
// Since this compilation was successfully analyzed, update the "last good" artifacts to the
// ones from the current compilation.
lastGood: {
depGraph: this.depGraph,
semanticDepGraph: newGraph,
traitCompiler: traitCompiler,
typeCheckingResults: null,
},
priorTypeCheckingResults:
this.state.lastGood !== null ? this.state.lastGood.typeCheckingResults : null,
};
}
recordSuccessfulTypeCheck(results: Map<AbsoluteFsPath, FileTypeCheckingData>): void {
if (this.state.lastGood === null || this.state.kind !== BuildStateKind.Analyzed) {
return;
}
this.state.lastGood.typeCheckingResults = results;
// Delete the files for which type-check code was generated from the set of pending type-check
// files.
for (const fileName of results.keys()) {
this.state.pendingTypeCheckEmit.delete(fileName);
}
}
recordSuccessfulEmit(sf: ts.SourceFile): void {
this.state.pendingEmit.delete(sf.fileName);
}
safeToSkipEmit(sf: ts.SourceFile): boolean {
return !this.state.pendingEmit.has(sf.fileName);
}
priorWorkFor(sf: ts.SourceFile): ClassRecord[]|null {
if (this.state.lastGood === null || this.logicalChanges === null) {
// There is no previous good build, so no prior work exists.
return null;
} else if (this.logicalChanges.has(sf.fileName)) {
// Prior work might exist, but would be stale as the file in question has logically changed.
return null;
} else {
// Prior work might exist, and if it does then it's usable!
return this.state.lastGood.traitCompiler.recordsFor(sf);
}
}
priorTypeCheckingResultsFor(sf: ts.SourceFile): FileTypeCheckingData|null {
if (this.state.kind !== BuildStateKind.Analyzed ||
this.state.priorTypeCheckingResults === null || this.logicalChanges === null) {
return null;
}
if (this.logicalChanges.has(sf.fileName) || this.state.pendingTypeCheckEmit.has(sf.fileName)) {
return null;
}
const fileName = absoluteFromSourceFile(sf);
if (!this.state.priorTypeCheckingResults.has(fileName)) {
return null;
}
const data = this.state.priorTypeCheckingResults.get(fileName)!;
if (data.hasInlines) {
return null;
}
return data;
}
}
type BuildState = PendingBuildState|AnalyzedBuildState;
enum BuildStateKind {
Pending,
Analyzed, Analyzed,
} }
interface BaseBuildState { /**
kind: BuildStateKind; * Placeholder state for a fresh compilation that has never been successfully analyzed.
*/
/** export interface FreshIncrementalState {
* The heart of incremental builds. This `Set` tracks the set of files which need to be emitted kind: IncrementalStateKind.Fresh;
* during the current compilation.
*
* This starts out as the set of files which are still pending from the previous program (or the
* full set of .ts files on a fresh build).
*
* After analysis, it's updated to include any files which might have changed and need a re-emit
* as a result of incremental changes.
*
* If an emit happens, any written files are removed from the `Set`, as they're no longer
* pending.
*
* Thus, after compilation `pendingEmit` should be empty (on a successful build) or contain the
* files which still need to be emitted but have not yet been (due to errors).
*
* `pendingEmit` is tracked as as `Set<string>` instead of a `Set<ts.SourceFile>`, because the
* contents of the file are not important here, only whether or not the current version of it
* needs to be emitted. The `string`s here are TS file paths.
*
* See the README.md for more information on this algorithm.
*/
pendingEmit: Set<string>;
/**
* Similar to `pendingEmit`, but then for representing the set of files for which the type-check
* file should be regenerated. It behaves identically with respect to errored compilations as
* `pendingEmit`.
*/
pendingTypeCheckEmit: Set<string>;
/**
* Specific aspects of the last compilation which successfully completed analysis, if any.
*/
lastGood: {
/**
* The dependency graph from the last successfully analyzed build.
*
* This is used to determine the logical impact of physical file changes.
*/
depGraph: FileDependencyGraph;
/**
* The semantic dependency graph from the last successfully analyzed build.
*
* This is used to perform in-depth comparison of Angular decorated classes, to determine
* which files have to be re-emitted and/or re-type-checked.
*/
semanticDepGraph: SemanticDepGraph;
/**
* The `TraitCompiler` from the last successfully analyzed build.
*
* This is used to extract "prior work" which might be reusable in this compilation.
*/
traitCompiler: TraitCompiler;
/**
* Type checking results which will be passed onto the next build.
*/
typeCheckingResults: Map<AbsoluteFsPath, FileTypeCheckingData>| null;
}|null;
} }
/** /**
* State of a build before the Angular analysis phase completes. * State captured from a compilation that completed analysis successfully, that can serve as a
* starting point for a future incremental build.
*/ */
interface PendingBuildState extends BaseBuildState { export interface AnalyzedIncrementalState {
kind: BuildStateKind.Pending; kind: IncrementalStateKind.Analyzed;
/** /**
* Set of files which are known to need an emit. * Dependency graph extracted from the build, to be used to determine the logical impact of
* physical file changes.
*/
depGraph: FileDependencyGraph;
/**
* The semantic dependency graph from the build.
* *
* Before the compiler's analysis phase completes, `pendingEmit` only contains files that were * This is used to perform in-depth comparison of Angular decorated classes, to determine
* still pending after the previous build. * which files have to be re-emitted and/or re-type-checked.
*/ */
pendingEmit: Set<string>; semanticDepGraph: SemanticDepGraph;
/** /**
* Set of TypeScript file paths which have changed since the last successfully analyzed build. * `TraitCompiler` which contains records of all analyzed classes within the build.
*/ */
changedTsPaths: Set<string>; traitCompiler: TraitCompiler;
/** /**
* Set of resource file paths which have changed since the last successfully analyzed build. * All generated template type-checking files produced as part of this compilation, or `null` if
* type-checking was not (yet) performed.
*/ */
changedResourcePaths: Set<AbsoluteFsPath>; typeCheckResults: Map<AbsoluteFsPath, FileTypeCheckingData>|null;
/** /**
* In a pending state, the semantic dependency graph is available to the compilation to register * Cumulative set of source file paths which were definitively emitted by this compilation or
* the incremental symbols into. * carried forward from a prior one.
*/ */
semanticDepGraphUpdater: SemanticDepGraphUpdater; emitted: Set<AbsoluteFsPath>;
} }
interface AnalyzedBuildState extends BaseBuildState { /**
kind: BuildStateKind.Analyzed; * Incremental state for a compilation that has not been successfully analyzed, but that can be
* based on a previous compilation which was.
*
* This is the state produced by an incremeental compilation until its own analysis succeeds. If
* analysis fails, this state carries forward information about which files have changed since the
* last successful build (the `lastAnalyzedState`), so that the next incremental build can consider
* the total delta between the `lastAnalyzedState` and the current program in its incremental
* analysis.
*/
export interface DeltaIncrementalState {
kind: IncrementalStateKind.Delta;
/** /**
* Set of files which are known to need an emit. * If available, the `AnalyzedIncrementalState` for the most recent ancestor of the current
* * program which was successfully analyzed.
* After analysis completes (that is, the state transitions to `AnalyzedBuildState`), the
* `pendingEmit` set takes into account any on-disk changes made since the last successfully
* analyzed build.
*/ */
pendingEmit: Set<string>; lastAnalyzedState: AnalyzedIncrementalState;
/** /**
* Type checking results from the previous compilation, which can be reused in this one. * Set of file paths which have changed since the `lastAnalyzedState` compilation.
*/ */
priorTypeCheckingResults: Map<AbsoluteFsPath, FileTypeCheckingData>|null; physicallyChangedTsFiles: Set<AbsoluteFsPath>;
/**
* Set of resource file paths which have changed since the `lastAnalyzedState` compilation.
*/
changedResourceFiles: Set<AbsoluteFsPath>;
} }
function tsOnlyFiles(program: ts.Program): ReadonlyArray<ts.SourceFile> { /**
return program.getSourceFiles().filter(sf => !sf.isDeclarationFile); * State produced by a compilation that's usable as the starting point for a subsequent compilation.
} *
* Discriminated by the `IncrementalStateKind` enum.
*/
export type IncrementalState = AnalyzedIncrementalState|DeltaIncrementalState|FreshIncrementalState;

View File

@ -7,7 +7,7 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {IncrementalDriver} from './state'; import {IncrementalState} from './state';
/** /**
* Strategy used to manage the association between a `ts.Program` and the `IncrementalDriver` which * Strategy used to manage the association between a `ts.Program` and the `IncrementalDriver` which
@ -17,13 +17,13 @@ export interface IncrementalBuildStrategy {
/** /**
* Determine the Angular `IncrementalDriver` for the given `ts.Program`, if one is available. * Determine the Angular `IncrementalDriver` for the given `ts.Program`, if one is available.
*/ */
getIncrementalDriver(program: ts.Program): IncrementalDriver|null; getIncrementalState(program: ts.Program): IncrementalState|null;
/** /**
* Associate the given `IncrementalDriver` with the given `ts.Program` and make it available to * Associate the given `IncrementalDriver` with the given `ts.Program` and make it available to
* future compilations. * future compilations.
*/ */
setIncrementalDriver(driver: IncrementalDriver, program: ts.Program): void; setIncrementalState(driver: IncrementalState, program: ts.Program): void;
/** /**
* Convert this `IncrementalBuildStrategy` into a possibly new instance to be used in the next * Convert this `IncrementalBuildStrategy` into a possibly new instance to be used in the next
@ -37,11 +37,11 @@ export interface IncrementalBuildStrategy {
* incremental data. * incremental data.
*/ */
export class NoopIncrementalBuildStrategy implements IncrementalBuildStrategy { export class NoopIncrementalBuildStrategy implements IncrementalBuildStrategy {
getIncrementalDriver(): null { getIncrementalState(): null {
return null; return null;
} }
setIncrementalDriver(): void {} setIncrementalState(): void {}
toNextBuildStrategy(): IncrementalBuildStrategy { toNextBuildStrategy(): IncrementalBuildStrategy {
return this; return this;
@ -52,22 +52,22 @@ export class NoopIncrementalBuildStrategy implements IncrementalBuildStrategy {
* Tracks an `IncrementalDriver` within the strategy itself. * Tracks an `IncrementalDriver` within the strategy itself.
*/ */
export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy { export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy {
private driver: IncrementalDriver|null = null; private state: IncrementalState|null = null;
private isSet: boolean = false; private isSet: boolean = false;
getIncrementalDriver(): IncrementalDriver|null { getIncrementalState(): IncrementalState|null {
return this.driver; return this.state;
} }
setIncrementalDriver(driver: IncrementalDriver): void { setIncrementalState(state: IncrementalState): void {
this.driver = driver; this.state = state;
this.isSet = true; this.isSet = true;
} }
toNextBuildStrategy(): TrackedIncrementalBuildStrategy { toNextBuildStrategy(): TrackedIncrementalBuildStrategy {
const strategy = new TrackedIncrementalBuildStrategy(); const strategy = new TrackedIncrementalBuildStrategy();
// Only reuse a driver that was explicitly set via `setIncrementalDriver`. // Only reuse state that was explicitly set via `setIncrementalState`.
strategy.driver = this.isSet ? this.driver : null; strategy.state = this.isSet ? this.state : null;
return strategy; return strategy;
} }
} }
@ -77,16 +77,16 @@ export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy
* program under `SYM_INCREMENTAL_DRIVER`. * program under `SYM_INCREMENTAL_DRIVER`.
*/ */
export class PatchedProgramIncrementalBuildStrategy implements IncrementalBuildStrategy { export class PatchedProgramIncrementalBuildStrategy implements IncrementalBuildStrategy {
getIncrementalDriver(program: ts.Program): IncrementalDriver|null { getIncrementalState(program: ts.Program): IncrementalState|null {
const driver = (program as any)[SYM_INCREMENTAL_DRIVER]; const state = (program as MayHaveIncrementalState)[SYM_INCREMENTAL_STATE];
if (driver === undefined || !(driver instanceof IncrementalDriver)) { if (state === undefined) {
return null; return null;
} }
return driver; return state;
} }
setIncrementalDriver(driver: IncrementalDriver, program: ts.Program): void { setIncrementalState(state: IncrementalState, program: ts.Program): void {
(program as any)[SYM_INCREMENTAL_DRIVER] = driver; (program as MayHaveIncrementalState)[SYM_INCREMENTAL_STATE] = state;
} }
toNextBuildStrategy(): IncrementalBuildStrategy { toNextBuildStrategy(): IncrementalBuildStrategy {
@ -108,4 +108,8 @@ export class PatchedProgramIncrementalBuildStrategy implements IncrementalBuildS
* support this behind the API of passing an old `ts.Program`, the `IncrementalDriver` is stored on * support this behind the API of passing an old `ts.Program`, the `IncrementalDriver` is stored on
* the `ts.Program` under this symbol. * the `ts.Program` under this symbol.
*/ */
const SYM_INCREMENTAL_DRIVER = Symbol('NgIncrementalDriver'); const SYM_INCREMENTAL_STATE = Symbol('NgIncrementalState');
interface MayHaveIncrementalState {
[SYM_INCREMENTAL_STATE]?: IncrementalState;
}

View File

@ -253,7 +253,7 @@ export class NgtscProgram implements api.Program {
continue; continue;
} }
this.compiler.incrementalDriver.recordSuccessfulEmit(writtenSf); this.compiler.incrementalCompilation.recordSuccessfulEmit(writtenSf);
} }
} }
this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
@ -274,7 +274,7 @@ export class NgtscProgram implements api.Program {
continue; continue;
} }
if (this.compiler.incrementalDriver.safeToSkipEmit(targetSourceFile)) { if (this.compiler.incrementalCompilation.safeToSkipEmit(targetSourceFile)) {
this.compiler.perfRecorder.eventCount(PerfEvent.EmitSkipSourceFile); this.compiler.perfRecorder.eventCount(PerfEvent.EmitSkipSourceFile);
continue; continue;
} }

View File

@ -118,7 +118,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
// type of 'void', so `undefined` is used instead. // type of 'void', so `undefined` is used instead.
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
const priorWork = this.incrementalBuild.priorWorkFor(sf); const priorWork = this.incrementalBuild.priorAnalysisFor(sf);
if (priorWork !== null) { if (priorWork !== null) {
for (const priorRecord of priorWork) { for (const priorRecord of priorWork) {
this.adopt(priorRecord); this.adopt(priorRecord);

View File

@ -8,9 +8,9 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {CompilationTicket, freshCompilationTicket, incrementalFromDriverTicket, NgCompiler, NgCompilerHost} from './core'; import {CompilationTicket, freshCompilationTicket, incrementalFromStateTicket, NgCompiler, NgCompilerHost} from './core';
import {NgCompilerOptions, UnifiedModulesHost} from './core/api'; import {NgCompilerOptions, UnifiedModulesHost} from './core/api';
import {NodeJSFileSystem, setFileSystem} from './file_system'; import {AbsoluteFsPath, NodeJSFileSystem, resolve, setFileSystem} from './file_system';
import {PatchedProgramIncrementalBuildStrategy} from './incremental'; import {PatchedProgramIncrementalBuildStrategy} from './incremental';
import {ActivePerfRecorder, PerfPhase} from './perf'; import {ActivePerfRecorder, PerfPhase} from './perf';
import {TsCreateProgramDriver} from './program_driver'; import {TsCreateProgramDriver} from './program_driver';
@ -109,25 +109,24 @@ export class NgTscPlugin implements TscPlugin {
const programDriver = new TsCreateProgramDriver( const programDriver = new TsCreateProgramDriver(
program, this.host, this.options, this.host.shimExtensionPrefixes); program, this.host, this.options, this.host.shimExtensionPrefixes);
const strategy = new PatchedProgramIncrementalBuildStrategy(); const strategy = new PatchedProgramIncrementalBuildStrategy();
const oldDriver = oldProgram !== undefined ? strategy.getIncrementalDriver(oldProgram) : null; const oldState = oldProgram !== undefined ? strategy.getIncrementalState(oldProgram) : null;
let ticket: CompilationTicket; let ticket: CompilationTicket;
let modifiedResourceFiles: Set<string>|undefined = undefined; const modifiedResourceFiles = new Set<AbsoluteFsPath>();
if (this.host.getModifiedResourceFiles !== undefined) { if (this.host.getModifiedResourceFiles !== undefined) {
modifiedResourceFiles = this.host.getModifiedResourceFiles(); for (const resourceFile of this.host.getModifiedResourceFiles() ?? []) {
} modifiedResourceFiles.add(resolve(resourceFile));
if (modifiedResourceFiles === undefined) { }
modifiedResourceFiles = new Set<string>();
} }
if (oldProgram === undefined || oldDriver === null) { if (oldProgram === undefined || oldState === null) {
ticket = freshCompilationTicket( ticket = freshCompilationTicket(
program, this.options, strategy, programDriver, perfRecorder, program, this.options, strategy, programDriver, perfRecorder,
/* enableTemplateTypeChecker */ false, /* usePoisonedData */ false); /* enableTemplateTypeChecker */ false, /* usePoisonedData */ false);
} else { } else {
strategy.toNextBuildStrategy().getIncrementalDriver(oldProgram); strategy.toNextBuildStrategy().getIncrementalState(oldProgram);
ticket = incrementalFromDriverTicket( ticket = incrementalFromStateTicket(
oldProgram, oldDriver, program, this.options, strategy, programDriver, oldProgram, oldState, program, this.options, strategy, programDriver,
modifiedResourceFiles, perfRecorder, false, false); modifiedResourceFiles, perfRecorder, false, false);
} }
this._compiler = NgCompiler.fromTicket(ticket, this.host); this._compiler = NgCompiler.fromTicket(ticket, this.host);

View File

@ -8,6 +8,7 @@
import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler, resourceChangeTicket} from '@angular/compiler-cli/src/ngtsc/core'; import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler, resourceChangeTicket} from '@angular/compiler-cli/src/ngtsc/core';
import {NgCompilerOptions} from '@angular/compiler-cli/src/ngtsc/core/api'; import {NgCompilerOptions} from '@angular/compiler-cli/src/ngtsc/core/api';
import {AbsoluteFsPath, resolve} from '@angular/compiler-cli/src/ngtsc/file_system';
import {TrackedIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental'; import {TrackedIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental';
import {ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver'; import {ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver';
@ -34,7 +35,10 @@ export class CompilerFactory {
getOrCreate(): NgCompiler { getOrCreate(): NgCompiler {
const program = this.programStrategy.getProgram(); const program = this.programStrategy.getProgram();
const modifiedResourceFiles = this.adapter.getModifiedResourceFiles() ?? new Set(); const modifiedResourceFiles = new Set<AbsoluteFsPath>();
for (const fileName of this.adapter.getModifiedResourceFiles() ?? []) {
modifiedResourceFiles.add(resolve(fileName));
}
if (this.compiler !== null && program === this.compiler.getCurrentProgram()) { if (this.compiler !== null && program === this.compiler.getCurrentProgram()) {
if (modifiedResourceFiles.size > 0) { if (modifiedResourceFiles.size > 0) {