Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
perf(compiler-cli): optimize computation of type-check scope informat…
…ion (#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
  • Loading branch information
JoostK authored and AndrewKushnir committed Sep 14, 2020
1 parent 5658405 commit ebede67
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 26 deletions.
30 changes: 5 additions & 25 deletions packages/compiler-cli/src/ngtsc/annotations/src/component.ts
Expand Up @@ -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';
Expand All @@ -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<string, Expression>();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

Expand Down
105 changes: 105 additions & 0 deletions 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<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;
}
}
1 change: 1 addition & 0 deletions packages/compiler-cli/src/ngtsc/metadata/index.ts
Expand Up @@ -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';
3 changes: 3 additions & 0 deletions packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts
Expand Up @@ -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>();
Expand Down
4 changes: 3 additions & 1 deletion packages/compiler-cli/src/ngtsc/scope/src/local.ts
Expand Up @@ -26,6 +26,7 @@ export interface LocalNgModuleData {
}

export interface LocalModuleScope extends ExportScope {
ngModule: ClassDeclaration;
compilation: ScopeData;
reexports: Reexport[]|null;
schemas: SchemaMetadata[];
Expand Down Expand Up @@ -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()),
Expand Down

0 comments on commit ebede67

Please sign in to comment.