Skip to content

Commit ecffc35

Browse files
committed
perf(compiler-cli): perform template type-checking incrementally (angular#36211)
This optimization builds on a lot of prior work to finally make type- checking of templates incremental. Incrementality requires two main components: - the ability to reuse work from a prior compilation. - the ability to know when changes in the current program invalidate that prior work. Prior to this commit, on every type-checking pass the compiler would generate new .ngtypecheck files for each original input file in the program. 1. (Build #1 main program): empty .ngtypecheck files generated for each original input file. 2. (Build #1 type-check program): .ngtypecheck contents overridden for those which have corresponding components that need type-checked. 3. (Build #2 main program): throw away old .ngtypecheck files and generate new empty ones. 4. (Build #2 type-check program): same as step 2. With this commit, the `IncrementalDriver` now tracks template type-checking _metadata_ for each input file. The metadata contains information about source mappings for generated type-checking code, as well as some diagnostics which were discovered at type-check analysis time. The actual type-checking code is stored in the TypeScript AST for type-checking files, which is now re-used between programs as follows: 1. (Build #1 main program): empty .ngtypecheck files generated for each original input file. 2. (Build #1 type-check program): .ngtypecheck contents overridden for those which have corresponding components that need type-checked, and the metadata registered in the `IncrementalDriver`. 3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_ .ngtypecheck `ts.SourceFile` shims from build #1's type-check program in the construction of build #2's main program. Some of the contents of these files might be stale (if a component's template changed, for example), but wholesale reuse here prevents unnecessary changes in the contents of the program at this point and makes TypeScript's job a lot easier. 4. (Build #2 type-check program): For those input files which have not "logically changed" (meaning components within are semantically the same as they were before), the compiler will re-use the type-check file metadata from build #1, and _not_ generate a new .ngtypecheck shim. For components which have logically changed or where the previous .ngtypecheck contents cannot otherwise be reused, code generation happens as before. PR Close angular#36211
1 parent b861e9c commit ecffc35

File tree

18 files changed

+451
-72
lines changed

18 files changed

+451
-72
lines changed

Diff for: packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,12 @@ export class NgccTraitCompiler extends TraitCompiler {
8282
}
8383
}
8484

85-
class NoIncrementalBuild implements IncrementalBuild<any> {
85+
class NoIncrementalBuild implements IncrementalBuild<any, any> {
8686
priorWorkFor(sf: ts.SourceFile): any[]|null {
8787
return null;
8888
}
89+
90+
priorTypeCheckingResultsFor(): null {
91+
return null;
92+
}
8993
}

Diff for: packages/compiler-cli/src/ngtsc/core/src/compiler.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,6 @@ export class NgCompiler {
482482
}
483483

484484
private getTemplateDiagnostics(): ReadonlyArray<ts.Diagnostic> {
485-
const host = this.host;
486-
487485
// Skip template type-checking if it's disabled.
488486
if (this.options.ivyTemplateTypeCheck === false && !this.fullTemplateTypeCheck) {
489487
return [];
@@ -493,7 +491,8 @@ export class NgCompiler {
493491

494492
// Execute the typeCheck phase of each decorator in the program.
495493
const prepSpan = this.perfRecorder.start('typeCheckPrep');
496-
compilation.templateTypeChecker.refresh();
494+
const results = compilation.templateTypeChecker.refresh();
495+
this.incrementalDriver.recordSuccessfulTypeCheck(results.perFileData);
497496
this.perfRecorder.stop(prepSpan);
498497

499498
// Get the diagnostics.
@@ -728,7 +727,7 @@ export class NgCompiler {
728727

729728
const templateTypeChecker = new TemplateTypeChecker(
730729
this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler,
731-
this.getTypeCheckingConfig(), refEmitter, reflector);
730+
this.getTypeCheckingConfig(), refEmitter, reflector, this.incrementalDriver);
732731

733732
return {
734733
isCore,

Diff for: packages/compiler-cli/src/ngtsc/incremental/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ ts_library(
1616
"//packages/compiler-cli/src/ngtsc/reflection",
1717
"//packages/compiler-cli/src/ngtsc/scope",
1818
"//packages/compiler-cli/src/ngtsc/transform",
19+
"//packages/compiler-cli/src/ngtsc/typecheck",
1920
"//packages/compiler-cli/src/ngtsc/util",
2021
"@npm//typescript",
2122
],

Diff for: packages/compiler-cli/src/ngtsc/incremental/README.md

+17-10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@ If the file is logically unchanged, ngtsc will reuse the previous analysis and o
3333

3434
If the file is logically changed, ngtsc will re-analyze it.
3535

36+
## Reuse of template type-checking code
37+
38+
Generally speaking, the generation of a template type-checking "shim" for an input component file is a time-consuming operation. Such generation produces several outputs:
39+
40+
1) The text of the template type-checking shim file, which can later be fed to TypeScript for the production of raw diagnostics.
41+
2) Metadata regarding source mappings within the template type-checking shim, which can be used to convert the raw diagnostics into mapped template diagnostics.
42+
3) "Construction" diagnostics, which are diagnostics produced as a side effect of generation of the shim itself.
43+
44+
When a component file is logically unchanged, ngtsc attempts to reuse this generation work. As part of creating both the new emit program and template type-checking program, the `ts.SourceFile` of the shim for the component file is included directly and not re-generated.
45+
46+
At the same time, the metadata and construction diagnostics are passed via the incremental build system. When TS gets diagnostics for the shim file, this metadata is used to convert them into mapped template diagnostics for delivery to the user.
47+
48+
### Limitations on template type-checking reuse
49+
50+
In certain cases the template type-checking system is unable to use the existing shim code. If the component is logically changed, the shim is regenerated in case its contents may have changed. If generating the shim itself required the use of any "inline" code (type-checking code which needs to be inserted into the component file instead for some reason), it also becomes ineligible for reuse.
51+
3652
## Skipping emit
3753

3854
ngtsc makes a decision to skip the emit of a file if it can prove that the contents of the file will not have changed since the last good compilation. To prove this, two conditions must be true.
@@ -135,13 +151,4 @@ Currently the compiler does not distinguish these two cases, and conservatively
135151

136152
## Skipping template type-checking
137153

138-
For certain kinds of changes, it may be possible to avoid the cost of generating and checking template type-checking files. Several levels of this can be imagined.
139-
140-
For resource-only changes, only the component(s) which have changed resources need to be re-checked. No other components could be affected, so previously produced diagnostics are still valid.
141-
142-
For arbitrary source changes, things get a bit more complicated. A change to any .ts file could affect types anywhere in the program (think `declare global ...`). If a set of affected components can be determined (perhaps via the import graph that the cycle analyzer extracts?) and it can be proven that the change does not impact any global types (exactly how to do this is left as an exercise for the reader), then type-checking could be skipped for other components in the mix.
143-
144-
If the above is too complex, then certain kinds of type changes might allow for the reuse of the text of some template type-checking files, if it can be proven that none of the inputs to their generation have changed. This is useful for two very important reasons.
145-
146-
1) Generating (and subsequently parsing) the template type-checking files is expensive.
147-
2) Under ideal conditions, after an initial template type-checking program is created, it may be possible to reuse it for emit _and_ type-checking in subsequent builds. This would be a pretty advanced optimization but would save creation of a second `ts.Program` on each valid rebuild.
154+
Under ideal conditions, after an initial template type-checking program is created, it may be possible to reuse it for emit _and_ type-checking in subsequent builds. This would be a pretty advanced optimization but would save creation of a second `ts.Program` on each valid rebuild.

Diff for: packages/compiler-cli/src/ngtsc/incremental/api.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,19 @@ import {AbsoluteFsPath} from '../file_system';
1414
*
1515
* `W` is a generic type representing a unit of work. This is generic to avoid a cyclic dependency
1616
* between the incremental engine API definition and its consumer(s).
17+
* `T` is a generic type representing template type-checking data for a particular file, which is
18+
* generic for the same reason.
1719
*/
18-
export interface IncrementalBuild<W> {
20+
export interface IncrementalBuild<W, T> {
1921
/**
2022
* Retrieve the prior analysis work, if any, done for the given source file.
2123
*/
2224
priorWorkFor(sf: ts.SourceFile): W[]|null;
25+
26+
/**
27+
* Retrieve the prior type-checking work, if any, that's been done for the given source file.
28+
*/
29+
priorTypeCheckingResultsFor(sf: ts.SourceFile): T|null;
2330
}
2431

2532
/**

Diff for: packages/compiler-cli/src/ngtsc/incremental/src/noop.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {IncrementalBuild} from '../api';
1010

11-
export const NOOP_INCREMENTAL_BUILD: IncrementalBuild<any> = {
12-
priorWorkFor: () => null
11+
export const NOOP_INCREMENTAL_BUILD: IncrementalBuild<any, any> = {
12+
priorWorkFor: () => null,
13+
priorTypeCheckingResultsFor: () => null,
1314
};

Diff for: packages/compiler-cli/src/ngtsc/incremental/src/state.ts

+49-4
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@
88

99
import * as ts from 'typescript';
1010

11-
import {absoluteFrom, AbsoluteFsPath} from '../../file_system';
11+
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system';
1212
import {ClassRecord, TraitCompiler} from '../../transform';
13+
import {FileTypeCheckingData} from '../../typecheck/src/context';
1314
import {IncrementalBuild} from '../api';
1415

1516
import {FileDependencyGraph} from './dependency_tracking';
1617

1718
/**
1819
* Drives an incremental build, by tracking changes and determining which files need to be emitted.
1920
*/
20-
export class IncrementalDriver implements IncrementalBuild<ClassRecord> {
21+
export class IncrementalDriver implements IncrementalBuild<ClassRecord, FileTypeCheckingData> {
2122
/**
2223
* State of the current build.
2324
*
@@ -190,10 +191,21 @@ export class IncrementalDriver implements IncrementalBuild<ClassRecord> {
190191
lastGood: {
191192
depGraph: this.depGraph,
192193
traitCompiler: traitCompiler,
193-
}
194+
typeCheckingResults: null,
195+
},
196+
197+
priorTypeCheckingResults:
198+
this.state.lastGood !== null ? this.state.lastGood.typeCheckingResults : null,
194199
};
195200
}
196201

202+
recordSuccessfulTypeCheck(results: Map<AbsoluteFsPath, FileTypeCheckingData>): void {
203+
if (this.state.lastGood === null || this.state.kind !== BuildStateKind.Analyzed) {
204+
return;
205+
}
206+
this.state.lastGood.typeCheckingResults = results;
207+
}
208+
197209
recordSuccessfulEmit(sf: ts.SourceFile): void {
198210
this.state.pendingEmit.delete(sf.fileName);
199211
}
@@ -214,6 +226,28 @@ export class IncrementalDriver implements IncrementalBuild<ClassRecord> {
214226
return this.state.lastGood.traitCompiler.recordsFor(sf);
215227
}
216228
}
229+
230+
priorTypeCheckingResultsFor(sf: ts.SourceFile): FileTypeCheckingData|null {
231+
if (this.state.kind !== BuildStateKind.Analyzed ||
232+
this.state.priorTypeCheckingResults === null || this.logicalChanges === null) {
233+
return null;
234+
}
235+
236+
if (this.logicalChanges.has(sf.fileName)) {
237+
return null;
238+
}
239+
240+
const fileName = absoluteFromSourceFile(sf);
241+
if (!this.state.priorTypeCheckingResults.has(fileName)) {
242+
return null;
243+
}
244+
const data = this.state.priorTypeCheckingResults.get(fileName)!;
245+
if (data.hasInlines) {
246+
return null;
247+
}
248+
249+
return data;
250+
}
217251
}
218252

219253
type BuildState = PendingBuildState|AnalyzedBuildState;
@@ -236,7 +270,8 @@ interface BaseBuildState {
236270
* After analysis, it's updated to include any files which might have changed and need a re-emit
237271
* as a result of incremental changes.
238272
*
239-
* If an emit happens, any written files are removed from the `Set`, as they're no longer pending.
273+
* If an emit happens, any written files are removed from the `Set`, as they're no longer
274+
* pending.
240275
*
241276
* Thus, after compilation `pendingEmit` should be empty (on a successful build) or contain the
242277
* files which still need to be emitted but have not yet been (due to errors).
@@ -267,6 +302,11 @@ interface BaseBuildState {
267302
* This is used to extract "prior work" which might be reusable in this compilation.
268303
*/
269304
traitCompiler: TraitCompiler;
305+
306+
/**
307+
* Type checking results which will be passed onto the next build.
308+
*/
309+
typeCheckingResults: Map<AbsoluteFsPath, FileTypeCheckingData>| null;
270310
}|null;
271311
}
272312

@@ -306,6 +346,11 @@ interface AnalyzedBuildState extends BaseBuildState {
306346
* analyzed build.
307347
*/
308348
pendingEmit: Set<string>;
349+
350+
/**
351+
* Type checking results from the previous compilation, which can be reused in this one.
352+
*/
353+
priorTypeCheckingResults: Map<AbsoluteFsPath, FileTypeCheckingData>|null;
309354
}
310355

311356
function tsOnlyFiles(program: ts.Program): ReadonlyArray<ts.SourceFile> {

Diff for: packages/compiler-cli/src/ngtsc/shims/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
export {PerFileShimGenerator, TopLevelShimGenerator} from './api';
1212
export {ShimAdapter} from './src/adapter';
13-
export {isShim} from './src/expando';
13+
export {copyFileShimData, isShim} from './src/expando';
1414
export {FactoryGenerator, FactoryInfo, FactoryTracker, generatedFactoryTransform} from './src/factory_generator';
1515
export {ShimReferenceTagger} from './src/reference_tagger';
1616
export {SummaryGenerator} from './src/summary_generator';

Diff for: packages/compiler-cli/src/ngtsc/shims/src/expando.ts

+10
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,13 @@ export function isFileShimSourceFile(sf: ts.SourceFile): sf is NgFileShimSourceF
100100
export function isShim(sf: ts.SourceFile): boolean {
101101
return isExtended(sf) && (sf[NgExtension].fileShim !== null || sf[NgExtension].isTopLevelShim);
102102
}
103+
104+
/**
105+
* Copy any shim data from one `ts.SourceFile` to another.
106+
*/
107+
export function copyFileShimData(from: ts.SourceFile, to: ts.SourceFile): void {
108+
if (!isFileShimSourceFile(from)) {
109+
return;
110+
}
111+
sfExtensionData(to).fileShim = sfExtensionData(from).fileShim;
112+
}

Diff for: packages/compiler-cli/src/ngtsc/transform/src/compilation.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
8787
constructor(
8888
private handlers: DecoratorHandler<unknown, unknown, unknown>[],
8989
private reflector: ReflectionHost, private perf: PerfRecorder,
90-
private incrementalBuild: IncrementalBuild<ClassRecord>,
90+
private incrementalBuild: IncrementalBuild<ClassRecord, unknown>,
9191
private compileNonExportedClasses: boolean, private dtsTransforms: DtsTransformRegistry) {
9292
for (const handler of handlers) {
9393
this.handlersByName.set(handler.name, handler);
@@ -423,8 +423,16 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
423423
}
424424
}
425425

426-
typeCheck(ctx: TypeCheckContext): void {
427-
for (const clazz of this.classes.keys()) {
426+
/**
427+
* Generate type-checking code into the `TypeCheckContext` for any components within the given
428+
* `ts.SourceFile`.
429+
*/
430+
typeCheck(sf: ts.SourceFile, ctx: TypeCheckContext): void {
431+
if (!this.fileToClasses.has(sf)) {
432+
return;
433+
}
434+
435+
for (const clazz of this.fileToClasses.get(sf)!) {
428436
const record = this.classes.get(clazz)!;
429437
for (const trait of record.traits) {
430438
if (trait.state !== TraitState.RESOLVED) {

Diff for: packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ ts_library(
1111
"//packages/compiler-cli/src/ngtsc/diagnostics",
1212
"//packages/compiler-cli/src/ngtsc/file_system",
1313
"//packages/compiler-cli/src/ngtsc/imports",
14+
"//packages/compiler-cli/src/ngtsc/incremental:api",
1415
"//packages/compiler-cli/src/ngtsc/metadata",
1516
"//packages/compiler-cli/src/ngtsc/reflection",
1617
"//packages/compiler-cli/src/ngtsc/shims",

Diff for: packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts

+24-6
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,20 @@ import * as ts from 'typescript';
1010

1111
import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
1212
import {ReferenceEmitter} from '../../imports';
13+
import {IncrementalBuild} from '../../incremental/api';
1314
import {ReflectionHost} from '../../reflection';
15+
import {isShim} from '../../shims';
1416

1517
import {TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from './api';
16-
import {FileTypeCheckingData, TypeCheckContext} from './context';
18+
import {FileTypeCheckingData, TypeCheckContext, TypeCheckRequest} from './context';
1719
import {shouldReportDiagnostic, translateDiagnostic} from './diagnostics';
1820

1921
/**
2022
* Interface to trigger generation of type-checking code for a program given a new
2123
* `TypeCheckContext`.
2224
*/
2325
export interface ProgramTypeCheckAdapter {
24-
typeCheck(ctx: TypeCheckContext): void;
26+
typeCheck(sf: ts.SourceFile, ctx: TypeCheckContext): void;
2527
}
2628

2729
/**
@@ -36,26 +38,42 @@ export class TemplateTypeChecker {
3638
private originalProgram: ts.Program,
3739
private typeCheckingStrategy: TypeCheckingProgramStrategy,
3840
private typeCheckAdapter: ProgramTypeCheckAdapter, private config: TypeCheckingConfig,
39-
private refEmitter: ReferenceEmitter, private reflector: ReflectionHost) {}
41+
private refEmitter: ReferenceEmitter, private reflector: ReflectionHost,
42+
private priorBuild: IncrementalBuild<unknown, FileTypeCheckingData>) {}
4043

4144
/**
4245
* Reset the internal type-checking program by generating type-checking code from the user's
4346
* program.
4447
*/
45-
refresh(): void {
48+
refresh(): TypeCheckRequest {
4649
this.files.clear();
4750

4851
const ctx =
4952
new TypeCheckContext(this.config, this.originalProgram, this.refEmitter, this.reflector);
5053

5154
// Typecheck all the files.
52-
this.typeCheckAdapter.typeCheck(ctx);
55+
for (const sf of this.originalProgram.getSourceFiles()) {
56+
if (sf.isDeclarationFile || isShim(sf)) {
57+
continue;
58+
}
59+
60+
const previousResults = this.priorBuild.priorTypeCheckingResultsFor(sf);
61+
if (previousResults === null) {
62+
// Previous results were not available, so generate new type-checking code for this file.
63+
this.typeCheckAdapter.typeCheck(sf, ctx);
64+
} else {
65+
// Previous results were available, and can be adopted into the current build.
66+
ctx.adoptPriorResults(sf, previousResults);
67+
}
68+
}
5369

5470
const results = ctx.finalize();
5571
this.typeCheckingStrategy.updateFiles(results.updates, UpdateMode.Complete);
5672
for (const [file, fileData] of results.perFileData) {
57-
this.files.set(file, {...fileData});
73+
this.files.set(file, fileData);
5874
}
75+
76+
return results;
5977
}
6078

6179
/**

Diff for: packages/compiler-cli/src/ngtsc/typecheck/src/context.ts

+20
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,22 @@ export class TypeCheckContext {
128128
*/
129129
private typeCtorPending = new Set<ts.ClassDeclaration>();
130130

131+
/**
132+
* Map of data for file paths which was adopted from a prior compilation.
133+
*
134+
* This data allows the `TypeCheckContext` to generate a `TypeCheckRequest` which can interpret
135+
* diagnostics from type-checking shims included in the prior compilation.
136+
*/
137+
private adoptedFiles = new Map<AbsoluteFsPath, FileTypeCheckingData>();
138+
139+
/**
140+
* Record the `FileTypeCheckingData` from a previous program that's associated with a particular
141+
* source file.
142+
*/
143+
adoptPriorResults(sf: ts.SourceFile, data: FileTypeCheckingData): void {
144+
this.adoptedFiles.set(absoluteFromSourceFile(sf), data);
145+
}
146+
131147
/**
132148
* Record a template for the given component `node`, with a `SelectorMatcher` for directive
133149
* matching.
@@ -274,6 +290,10 @@ export class TypeCheckContext {
274290
});
275291
}
276292

293+
for (const [sfPath, fileData] of this.adoptedFiles.entries()) {
294+
results.perFileData.set(sfPath, fileData);
295+
}
296+
277297
return results;
278298
}
279299

0 commit comments

Comments
 (0)