perf(compiler-cli): optimize computation of type-check scope information (#38539)
When type-checking a component, the declaring NgModule scope is used to create a directive matcher that contains flattened directive metadata, i.e. the metadata of a directive and its base classes. This computation is done for all components, whereas the type-check scope is constant per NgModule. Additionally, the flattening of metadata is constant per directive instance so doesn't necessarily have to be recomputed for each component. This commit introduces a `TypeCheckScopes` class that is responsible for flattening directives and computing the scope per NgModule. It caches the computed results as appropriate to avoid repeated computation. PR Close #38539
This commit is contained in:
parent
077f51685a
commit
297c060ae7
|
@ -16,7 +16,6 @@ import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from
|
|||
import {DependencyTracker} from '../../incremental/api';
|
||||
import {IndexingContext} from '../../indexer';
|
||||
import {ClassPropertyMapping, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
|
||||
import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance';
|
||||
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
|
||||
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
|
||||
import {ComponentScopeReader, LocalModuleScopeRegistry} from '../../scope';
|
||||
|
@ -31,6 +30,7 @@ import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagno
|
|||
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
|
||||
import {compileNgFactoryDefField} from './factory';
|
||||
import {generateSetClassMetadataCall} from './metadata';
|
||||
import {TypeCheckScopes} from './typecheck_scopes';
|
||||
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
|
||||
|
||||
const EMPTY_MAP = new Map<string, Expression>();
|
||||
|
@ -95,6 +95,7 @@ export class ComponentDecoratorHandler implements
|
|||
|
||||
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
|
||||
private elementSchemaRegistry = new DomElementSchemaRegistry();
|
||||
private typeCheckScopes = new TypeCheckScopes(this.scopeReader, this.metaReader);
|
||||
|
||||
/**
|
||||
* During the asynchronous preanalyze phase, it's necessary to parse the template to extract
|
||||
|
@ -423,36 +424,15 @@ export class ComponentDecoratorHandler implements
|
|||
return;
|
||||
}
|
||||
|
||||
const matcher = new SelectorMatcher<DirectiveMeta>();
|
||||
const pipes = new Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>();
|
||||
let schemas: SchemaMetadata[] = [];
|
||||
|
||||
const scope = this.scopeReader.getScopeForComponent(node);
|
||||
const scope = this.typeCheckScopes.getTypeCheckScope(node);
|
||||
if (scope === 'error') {
|
||||
// Don't type-check components that had errors in their scopes.
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope !== null) {
|
||||
for (const meta of scope.compilation.directives) {
|
||||
if (meta.selector !== null) {
|
||||
const extMeta = flattenInheritedDirectiveMetadata(this.metaReader, meta.ref);
|
||||
matcher.addSelectables(CssSelector.parse(meta.selector), extMeta);
|
||||
}
|
||||
}
|
||||
for (const {name, ref} of scope.compilation.pipes) {
|
||||
if (!ts.isClassDeclaration(ref.node)) {
|
||||
throw new Error(`Unexpected non-class declaration ${
|
||||
ts.SyntaxKind[ref.node.kind]} for pipe ${ref.debugName}`);
|
||||
}
|
||||
pipes.set(name, ref as Reference<ClassDeclaration<ts.ClassDeclaration>>);
|
||||
}
|
||||
schemas = scope.schemas;
|
||||
}
|
||||
|
||||
const binder = new R3TargetBinder(matcher);
|
||||
const binder = new R3TargetBinder(scope.matcher);
|
||||
ctx.addTemplate(
|
||||
new Reference(node), binder, meta.template.diagNodes, pipes, schemas,
|
||||
new Reference(node), binder, meta.template.diagNodes, scope.pipes, scope.schemas,
|
||||
meta.template.sourceMapping, meta.template.file);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* @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 {CssSelector, SchemaMetadata, SelectorMatcher} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
import {DirectiveMeta, flattenInheritedDirectiveMetadata, MetadataReader} from '../../metadata';
|
||||
import {ClassDeclaration} from '../../reflection';
|
||||
import {ComponentScopeReader} from '../../scope';
|
||||
|
||||
/**
|
||||
* The scope that is used for type-check code generation of a component template.
|
||||
*/
|
||||
export interface TypeCheckScope {
|
||||
/**
|
||||
* A `SelectorMatcher` instance that contains the flattened directive metadata of all directives
|
||||
* that are in the compilation scope of the declaring NgModule.
|
||||
*/
|
||||
matcher: SelectorMatcher<DirectiveMeta>;
|
||||
|
||||
/**
|
||||
* The pipes that are available in the compilation scope.
|
||||
*/
|
||||
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>;
|
||||
|
||||
/**
|
||||
* The schemas that are used in this scope.
|
||||
*/
|
||||
schemas: SchemaMetadata[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes scope information to be used in template type checking.
|
||||
*/
|
||||
export class TypeCheckScopes {
|
||||
/**
|
||||
* Cache of flattened directive metadata. Because flattened metadata is scope-invariant it's
|
||||
* cached individually, such that all scopes refer to the same flattened metadata.
|
||||
*/
|
||||
private flattenedDirectiveMetaCache = new Map<ClassDeclaration, DirectiveMeta>();
|
||||
|
||||
/**
|
||||
* Cache of the computed type check scope per NgModule declaration.
|
||||
*/
|
||||
private scopeCache = new Map<ClassDeclaration, TypeCheckScope>();
|
||||
|
||||
constructor(private scopeReader: ComponentScopeReader, private metaReader: MetadataReader) {}
|
||||
|
||||
/**
|
||||
* Computes the type-check scope information for the component declaration. If the NgModule
|
||||
* contains an error, then 'error' is returned. If the component is not declared in any NgModule,
|
||||
* an empty type-check scope is returned.
|
||||
*/
|
||||
getTypeCheckScope(node: ClassDeclaration): TypeCheckScope|'error' {
|
||||
const matcher = new SelectorMatcher<DirectiveMeta>();
|
||||
const pipes = new Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>();
|
||||
|
||||
const scope = this.scopeReader.getScopeForComponent(node);
|
||||
if (scope === null) {
|
||||
return {matcher, pipes, schemas: []};
|
||||
} else if (scope === 'error') {
|
||||
return scope;
|
||||
}
|
||||
|
||||
if (this.scopeCache.has(scope.ngModule)) {
|
||||
return this.scopeCache.get(scope.ngModule)!;
|
||||
}
|
||||
|
||||
for (const meta of scope.compilation.directives) {
|
||||
if (meta.selector !== null) {
|
||||
const extMeta = this.getInheritedDirectiveMetadata(meta.ref);
|
||||
matcher.addSelectables(CssSelector.parse(meta.selector), extMeta);
|
||||
}
|
||||
}
|
||||
|
||||
for (const {name, ref} of scope.compilation.pipes) {
|
||||
if (!ts.isClassDeclaration(ref.node)) {
|
||||
throw new Error(`Unexpected non-class declaration ${
|
||||
ts.SyntaxKind[ref.node.kind]} for pipe ${ref.debugName}`);
|
||||
}
|
||||
pipes.set(name, ref as Reference<ClassDeclaration<ts.ClassDeclaration>>);
|
||||
}
|
||||
|
||||
const typeCheckScope: TypeCheckScope = {matcher, pipes, schemas: scope.schemas};
|
||||
this.scopeCache.set(scope.ngModule, typeCheckScope);
|
||||
return typeCheckScope;
|
||||
}
|
||||
|
||||
private getInheritedDirectiveMetadata(ref: Reference<ClassDeclaration>): DirectiveMeta {
|
||||
const clazz = ref.node;
|
||||
if (this.flattenedDirectiveMetaCache.has(clazz)) {
|
||||
return this.flattenedDirectiveMetaCache.get(clazz)!;
|
||||
}
|
||||
|
||||
const meta = flattenInheritedDirectiveMetadata(this.metaReader, ref);
|
||||
this.flattenedDirectiveMetaCache.set(clazz, meta);
|
||||
return meta;
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
export * from './src/api';
|
||||
export {DtsMetadataReader} from './src/dts';
|
||||
export {flattenInheritedDirectiveMetadata} from './src/inheritance';
|
||||
export {CompoundMetadataRegistry, LocalMetadataRegistry, InjectableClassRegistry} from './src/registry';
|
||||
export {extractDirectiveTypeCheckMeta, CompoundMetadataReader} from './src/util';
|
||||
export {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, InputOrOutput} from './src/property_mapping';
|
||||
|
|
|
@ -26,6 +26,9 @@ export function flattenInheritedDirectiveMetadata(
|
|||
if (topMeta === null) {
|
||||
throw new Error(`Metadata not found for directive: ${dir.debugName}`);
|
||||
}
|
||||
if (topMeta.baseClass === null) {
|
||||
return topMeta;
|
||||
}
|
||||
|
||||
const coercedInputFields = new Set<ClassPropertyName>();
|
||||
const undeclaredInputFields = new Set<ClassPropertyName>();
|
||||
|
|
|
@ -26,6 +26,7 @@ export interface LocalNgModuleData {
|
|||
}
|
||||
|
||||
export interface LocalModuleScope extends ExportScope {
|
||||
ngModule: ClassDeclaration;
|
||||
compilation: ScopeData;
|
||||
reexports: Reexport[]|null;
|
||||
schemas: SchemaMetadata[];
|
||||
|
@ -433,7 +434,8 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
|
|||
}
|
||||
|
||||
// Finally, produce the `LocalModuleScope` with both the compilation and export scopes.
|
||||
const scope = {
|
||||
const scope: LocalModuleScope = {
|
||||
ngModule: ngModule.ref.node,
|
||||
compilation: {
|
||||
directives: Array.from(compilationDirectives.values()),
|
||||
pipes: Array.from(compilationPipes.values()),
|
||||
|
|
Loading…
Reference in New Issue