From ebede67433691e720ee5385ec7cc429f8901e311 Mon Sep 17 00:00:00 2001 From: JoostK Date: Wed, 19 Aug 2020 22:40:54 +0200 Subject: [PATCH] perf(compiler-cli): optimize computation of type-check scope information (#38843) 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 #38843 --- .../src/ngtsc/annotations/src/component.ts | 30 +---- .../ngtsc/annotations/src/typecheck_scopes.ts | 105 ++++++++++++++++++ .../compiler-cli/src/ngtsc/metadata/index.ts | 1 + .../src/ngtsc/metadata/src/inheritance.ts | 3 + .../compiler-cli/src/ngtsc/scope/src/local.ts | 4 +- 5 files changed, 117 insertions(+), 26 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/annotations/src/typecheck_scopes.ts diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 846a6e8d8c67f..da9edb9bcc82a 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -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, makeDuplicateDeclarationError, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util'; const EMPTY_MAP = new Map(); @@ -95,6 +95,7 @@ export class ComponentDecoratorHandler implements private literalCache = new Map(); 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(); - const pipes = new Map>>(); - 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>); - } - 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); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/typecheck_scopes.ts b/packages/compiler-cli/src/ngtsc/annotations/src/typecheck_scopes.ts new file mode 100644 index 0000000000000..aada9e0df4e55 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/annotations/src/typecheck_scopes.ts @@ -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; + + /** + * The pipes that are available in the compilation scope. + */ + pipes: Map>>; + + /** + * 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(); + + /** + * Cache of the computed type check scope per NgModule declaration. + */ + private scopeCache = new Map(); + + 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(); + const pipes = new Map>>(); + + 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>); + } + + const typeCheckScope: TypeCheckScope = {matcher, pipes, schemas: scope.schemas}; + this.scopeCache.set(scope.ngModule, typeCheckScope); + return typeCheckScope; + } + + private getInheritedDirectiveMetadata(ref: Reference): 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; + } +} diff --git a/packages/compiler-cli/src/ngtsc/metadata/index.ts b/packages/compiler-cli/src/ngtsc/metadata/index.ts index 2f857eee0bc40..9ee4e1a977ac4 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/index.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/index.ts @@ -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'; diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts b/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts index f5271f4351d32..b923de6e2a589 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts @@ -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(); const undeclaredInputFields = new Set(); diff --git a/packages/compiler-cli/src/ngtsc/scope/src/local.ts b/packages/compiler-cli/src/ngtsc/scope/src/local.ts index 985058a157d98..da7f3aa9397c1 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/local.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/local.ts @@ -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()),