perf(compiler-cli): introduce fast path for resource-only updates (#40561)
This commit adds a new `IncrementalResourceCompilationTicket` which reuses an existing `NgCompiler` instance and updates it to optimally process template-only and style-only changes. Performing this update involves both instructing `DecoratorHandler`s to react to the resource changes, as well as invalidating `TemplateTypeChecker` state for the component(s) in question. That way, querying the `TemplateTypeChecker` will trigger new TCB generation for the changed template(s). PR Close #40561
This commit is contained in:
parent
52aeb5326d
commit
be979c907b
@ -71,6 +71,16 @@ export interface ComponentAnalysisData {
|
|||||||
|
|
||||||
resources: ComponentResources;
|
resources: ComponentResources;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The literal `styleUrls` extracted from the decorator, if present.
|
||||||
|
*/
|
||||||
|
styleUrls: string[]|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline stylesheets extracted from the decorator, if present.
|
||||||
|
*/
|
||||||
|
inlineStyles: string[]|null;
|
||||||
|
|
||||||
isPoisoned: boolean;
|
isPoisoned: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,9 +285,11 @@ export class ComponentDecoratorHandler implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let inlineStyles: string[]|null = null;
|
||||||
if (component.has('styles')) {
|
if (component.has('styles')) {
|
||||||
const litStyles = parseFieldArrayValue(component, 'styles', this.evaluator);
|
const litStyles = parseFieldArrayValue(component, 'styles', this.evaluator);
|
||||||
if (litStyles !== null) {
|
if (litStyles !== null) {
|
||||||
|
inlineStyles = [...litStyles];
|
||||||
if (styles === null) {
|
if (styles === null) {
|
||||||
styles = litStyles;
|
styles = litStyles;
|
||||||
} else {
|
} else {
|
||||||
@ -333,6 +345,8 @@ export class ComponentDecoratorHandler implements
|
|||||||
template,
|
template,
|
||||||
providersRequiringFactory,
|
providersRequiringFactory,
|
||||||
viewProvidersRequiringFactory,
|
viewProvidersRequiringFactory,
|
||||||
|
inlineStyles,
|
||||||
|
styleUrls,
|
||||||
resources: {
|
resources: {
|
||||||
styles: styleResources,
|
styles: styleResources,
|
||||||
template: templateResource,
|
template: templateResource,
|
||||||
@ -581,6 +595,37 @@ export class ComponentDecoratorHandler implements
|
|||||||
return {data};
|
return {data};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateResources(node: ClassDeclaration, analysis: ComponentAnalysisData): void {
|
||||||
|
const containingFile = node.getSourceFile().fileName;
|
||||||
|
|
||||||
|
// If the template is external, re-parse it.
|
||||||
|
const templateDecl = analysis.template.declaration;
|
||||||
|
if (!templateDecl.isInline) {
|
||||||
|
analysis.template = this.extractTemplate(node, templateDecl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update any external stylesheets and rebuild the combined 'styles' list.
|
||||||
|
// TODO(alxhub): write tests for styles when the primary compiler uses the updateResources path
|
||||||
|
let styles: string[] = [];
|
||||||
|
if (analysis.styleUrls !== null) {
|
||||||
|
for (const styleUrl of analysis.styleUrls) {
|
||||||
|
const resolvedStyleUrl = this.resourceLoader.resolve(styleUrl, containingFile);
|
||||||
|
const styleText = this.resourceLoader.load(resolvedStyleUrl);
|
||||||
|
styles.push(styleText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (analysis.inlineStyles !== null) {
|
||||||
|
for (const styleText of analysis.inlineStyles) {
|
||||||
|
styles.push(styleText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const styleText of analysis.template.styles) {
|
||||||
|
styles.push(styleText);
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis.meta.styles = styles;
|
||||||
|
}
|
||||||
|
|
||||||
compileFull(
|
compileFull(
|
||||||
node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>,
|
node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>,
|
||||||
resolution: Readonly<ComponentResolutionData>, pool: ConstantPool): CompileResult[] {
|
resolution: Readonly<ComponentResolutionData>, pool: ConstantPool): CompileResult[] {
|
||||||
|
@ -63,6 +63,7 @@ interface LazyCompilationState {
|
|||||||
export enum CompilationTicketKind {
|
export enum CompilationTicketKind {
|
||||||
Fresh,
|
Fresh,
|
||||||
IncrementalTypeScript,
|
IncrementalTypeScript,
|
||||||
|
IncrementalResource,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,6 +94,12 @@ export interface IncrementalTypeScriptCompilationTicket {
|
|||||||
usePoisonedData: boolean;
|
usePoisonedData: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IncrementalResourceCompilationTicket {
|
||||||
|
kind: CompilationTicketKind.IncrementalResource;
|
||||||
|
compiler: NgCompiler;
|
||||||
|
modifiedResourceFiles: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A request to begin Angular compilation, either starting from scratch or from a known prior state.
|
* A request to begin Angular compilation, either starting from scratch or from a known prior state.
|
||||||
*
|
*
|
||||||
@ -100,7 +107,8 @@ export interface IncrementalTypeScriptCompilationTicket {
|
|||||||
* Angular compiler. They abstract the starting state of compilation and allow `NgCompiler` to be
|
* Angular compiler. They abstract the starting state of compilation and allow `NgCompiler` to be
|
||||||
* managed independently of any incremental compilation lifecycle.
|
* managed independently of any incremental compilation lifecycle.
|
||||||
*/
|
*/
|
||||||
export type CompilationTicket = FreshCompilationTicket|IncrementalTypeScriptCompilationTicket;
|
export type CompilationTicket = FreshCompilationTicket|IncrementalTypeScriptCompilationTicket|
|
||||||
|
IncrementalResourceCompilationTicket;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a `CompilationTicket` for a brand new compilation, using no prior state.
|
* Create a `CompilationTicket` for a brand new compilation, using no prior state.
|
||||||
@ -180,6 +188,15 @@ export function incrementalFromDriverTicket(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resourceChangeTicket(compiler: NgCompiler, modifiedResourceFiles: Set<string>):
|
||||||
|
IncrementalResourceCompilationTicket {
|
||||||
|
return {
|
||||||
|
kind: CompilationTicketKind.IncrementalResource,
|
||||||
|
compiler,
|
||||||
|
modifiedResourceFiles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The heart of the Angular Ivy compiler.
|
* The heart of the Angular Ivy compiler.
|
||||||
@ -260,6 +277,10 @@ export class NgCompiler {
|
|||||||
ticket.usePoisonedData,
|
ticket.usePoisonedData,
|
||||||
perfRecorder,
|
perfRecorder,
|
||||||
);
|
);
|
||||||
|
case CompilationTicketKind.IncrementalResource:
|
||||||
|
const compiler = ticket.compiler;
|
||||||
|
compiler.updateWithChangedResources(ticket.modifiedResourceFiles);
|
||||||
|
return compiler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,6 +327,36 @@ export class NgCompiler {
|
|||||||
this.ignoreForEmit = this.adapter.ignoreForEmit;
|
this.ignoreForEmit = this.adapter.ignoreForEmit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resourceManager.invalidate();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const clazz of classesToUpdate) {
|
||||||
|
this.compilation.traitCompiler.updateResources(clazz);
|
||||||
|
if (!ts.isClassDeclaration(clazz)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.compilation.templateTypeChecker.invalidateClass(clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the resource dependencies of a file.
|
* Get the resource dependencies of a file.
|
||||||
*
|
*
|
||||||
|
@ -17,7 +17,7 @@ import {OptimizeFor, TypeCheckingProgramStrategy} from '../../typecheck/api';
|
|||||||
|
|
||||||
import {NgCompilerOptions} from '../api';
|
import {NgCompilerOptions} from '../api';
|
||||||
|
|
||||||
import {freshCompilationTicket, NgCompiler} from '../src/compiler';
|
import {freshCompilationTicket, NgCompiler, resourceChangeTicket} from '../src/compiler';
|
||||||
import {NgCompilerHost} from '../src/host';
|
import {NgCompilerHost} from '../src/host';
|
||||||
|
|
||||||
function makeFreshCompiler(
|
function makeFreshCompiler(
|
||||||
@ -111,6 +111,7 @@ runInEachFileSystem(() => {
|
|||||||
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
|
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
|
||||||
const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA');
|
const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA');
|
||||||
const CmpC = getClass(getSourceFileOrError(program, cmpCFile), 'CmpC');
|
const CmpC = getClass(getSourceFileOrError(program, cmpCFile), 'CmpC');
|
||||||
|
|
||||||
const compiler = makeFreshCompiler(
|
const compiler = makeFreshCompiler(
|
||||||
host, options, program, new ReusedProgramStrategy(program, host, options, []),
|
host, options, program, new ReusedProgramStrategy(program, host, options, []),
|
||||||
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
|
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
|
||||||
@ -278,6 +279,53 @@ runInEachFileSystem(() => {
|
|||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('resource-only changes', () => {
|
||||||
|
it('should reuse the full compilation state for a resource-only change', () => {
|
||||||
|
const COMPONENT = _('/cmp.ts');
|
||||||
|
const TEMPLATE = _('/template.html');
|
||||||
|
fs.writeFile(COMPONENT, `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
@Component({
|
||||||
|
selector: 'test-cmp',
|
||||||
|
templateUrl: './template.html',
|
||||||
|
})
|
||||||
|
export class Cmp {}
|
||||||
|
`);
|
||||||
|
fs.writeFile(TEMPLATE, `<h1>Resource</h1>`);
|
||||||
|
|
||||||
|
const options: NgCompilerOptions = {
|
||||||
|
strictTemplates: true,
|
||||||
|
};
|
||||||
|
const baseHost = new NgtscCompilerHost(getFileSystem(), options);
|
||||||
|
const host = NgCompilerHost.wrap(baseHost, [COMPONENT], options, /* oldProgram */ null);
|
||||||
|
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
|
||||||
|
const compilerA = makeFreshCompiler(
|
||||||
|
host, options, program, new ReusedProgramStrategy(program, host, options, []),
|
||||||
|
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
|
||||||
|
/* usePoisonedData */ false);
|
||||||
|
|
||||||
|
const componentSf = getSourceFileOrError(program, COMPONENT);
|
||||||
|
|
||||||
|
// There should be no diagnostics for the component.
|
||||||
|
expect(compilerA.getDiagnosticsForFile(componentSf, OptimizeFor.WholeProgram).length)
|
||||||
|
.toBe(0);
|
||||||
|
|
||||||
|
// Change the resource file and introduce an error.
|
||||||
|
fs.writeFile(TEMPLATE, `<h1>Resource</h2>`);
|
||||||
|
|
||||||
|
// Perform a resource-only incremental step.
|
||||||
|
const resourceTicket = resourceChangeTicket(compilerA, new Set([TEMPLATE]));
|
||||||
|
const compilerB = NgCompiler.fromTicket(resourceTicket, host);
|
||||||
|
|
||||||
|
// A resource-only update should reuse the same compiler instance.
|
||||||
|
expect(compilerB).toBe(compilerA);
|
||||||
|
|
||||||
|
// The new template error should be reported in component diagnostics.
|
||||||
|
expect(compilerB.getDiagnosticsForFile(componentSf, OptimizeFor.WholeProgram).length)
|
||||||
|
.toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -113,6 +113,13 @@ export class AdapterResourceLoader implements ResourceLoader {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the entire resource cache.
|
||||||
|
*/
|
||||||
|
invalidate(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to resolve `url` in the context of `fromFile`, while respecting the rootDirs
|
* Attempt to resolve `url` in the context of `fromFile`, while respecting the rootDirs
|
||||||
* option from the tsconfig. First, normalize the file name.
|
* option from the tsconfig. First, normalize the file name.
|
||||||
|
@ -128,6 +128,12 @@ export interface DecoratorHandler<D, A, R> {
|
|||||||
analyze(node: ClassDeclaration, metadata: Readonly<D>, handlerFlags?: HandlerFlags):
|
analyze(node: ClassDeclaration, metadata: Readonly<D>, handlerFlags?: HandlerFlags):
|
||||||
AnalysisOutput<A>;
|
AnalysisOutput<A>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React to a change in a resource file by updating the `analysis` or `resolution`, under the
|
||||||
|
* assumption that nothing in the TypeScript code has changed.
|
||||||
|
*/
|
||||||
|
updateResources?(node: ClassDeclaration, analysis: A, resolution: R): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Post-process the analysis of a decorator/class combination and record any necessary information
|
* Post-process the analysis of a decorator/class combination and record any necessary information
|
||||||
* in the larger compilation.
|
* in the larger compilation.
|
||||||
|
@ -453,6 +453,20 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateResources(clazz: DeclarationNode): void {
|
||||||
|
if (!this.reflector.isClass(clazz) || !this.classes.has(clazz)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const record = this.classes.get(clazz)!;
|
||||||
|
for (const trait of record.traits) {
|
||||||
|
if (trait.state !== TraitState.Resolved || trait.handler.updateResources === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
trait.handler.updateResources(clazz, trait.analysis, trait.resolution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
compile(clazz: DeclarationNode, constantPool: ConstantPool): CompileResult[]|null {
|
compile(clazz: DeclarationNode, constantPool: ConstantPool): CompileResult[]|null {
|
||||||
const original = ts.getOriginalNode(clazz) as typeof clazz;
|
const original = ts.getOriginalNode(clazz) as typeof clazz;
|
||||||
if (!this.reflector.isClass(clazz) || !this.reflector.isClass(original) ||
|
if (!this.reflector.isClass(clazz) || !this.reflector.isClass(original) ||
|
||||||
|
@ -165,6 +165,12 @@ export interface TemplateTypeChecker {
|
|||||||
* Retrieve the type checking engine's metadata for the given directive class, if available.
|
* Retrieve the type checking engine's metadata for the given directive class, if available.
|
||||||
*/
|
*/
|
||||||
getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta|null;
|
getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the `TemplateTypeChecker`'s state for the given class, so that it will be recomputed on
|
||||||
|
* the next request.
|
||||||
|
*/
|
||||||
|
invalidateClass(clazz: ts.ClassDeclaration): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -318,6 +318,25 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
|||||||
return engine.getExpressionCompletionLocation(ast);
|
return engine.getExpressionCompletionLocation(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidateClass(clazz: ts.ClassDeclaration): void {
|
||||||
|
this.completionCache.delete(clazz);
|
||||||
|
this.symbolBuilderCache.delete(clazz);
|
||||||
|
this.scopeCache.delete(clazz);
|
||||||
|
this.elementTagCache.delete(clazz);
|
||||||
|
|
||||||
|
const sf = clazz.getSourceFile();
|
||||||
|
const sfPath = absoluteFromSourceFile(sf);
|
||||||
|
const shimPath = this.typeCheckingStrategy.shimPathForComponent(clazz);
|
||||||
|
const fileData = this.getFileData(sfPath);
|
||||||
|
const templateId = fileData.sourceManager.getTemplateId(clazz);
|
||||||
|
|
||||||
|
fileData.shimData.delete(shimPath);
|
||||||
|
fileData.isComplete = false;
|
||||||
|
fileData.templateOverrides?.delete(templateId);
|
||||||
|
|
||||||
|
this.isComplete = false;
|
||||||
|
}
|
||||||
|
|
||||||
private getOrCreateCompletionEngine(component: ts.ClassDeclaration): CompletionEngine|null {
|
private getOrCreateCompletionEngine(component: ts.ClassDeclaration): CompletionEngine|null {
|
||||||
if (this.completionCache.has(component)) {
|
if (this.completionCache.has(component)) {
|
||||||
return this.completionCache.get(component)!;
|
return this.completionCache.get(component)!;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user