Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: AOT compilation optimizations (patch) #38843

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
88 changes: 38 additions & 50 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 Expand Up @@ -495,45 +475,58 @@ export class ComponentDecoratorHandler implements
// Set up the R3TargetBinder, as well as a 'directives' array and a 'pipes' map that are later
// fed to the TemplateDefinitionBuilder. First, a SelectorMatcher is constructed to match
// directives that are in scope.
const matcher = new SelectorMatcher<DirectiveMeta&{expression: Expression}>();
const directives: {selector: string, expression: Expression}[] = [];
type MatchedDirective = DirectiveMeta&{selector: string};
const matcher = new SelectorMatcher<MatchedDirective>();

for (const dir of scope.compilation.directives) {
const {ref, selector} = dir;
if (selector !== null) {
const expression = this.refEmitter.emit(ref, context);
directives.push({selector, expression});
matcher.addSelectables(CssSelector.parse(selector), {...dir, expression});
if (dir.selector !== null) {
matcher.addSelectables(CssSelector.parse(dir.selector), dir as MatchedDirective);
}
}
const pipes = new Map<string, Expression>();
const pipes = new Map<string, Reference<ClassDeclaration>>();
for (const pipe of scope.compilation.pipes) {
pipes.set(pipe.name, this.refEmitter.emit(pipe.ref, context));
pipes.set(pipe.name, pipe.ref);
}

// Next, the component template AST is bound using the R3TargetBinder. This produces an
// Next, the component template AST is bound using the R3TargetBinder. This produces a
// BoundTarget, which is similar to a ts.TypeChecker.
const binder = new R3TargetBinder(matcher);
const bound = binder.bind({template: metadata.template.nodes});

// The BoundTarget knows which directives and pipes matched the template.
const usedDirectives = bound.getUsedDirectives();
const usedPipes = bound.getUsedPipes().map(name => pipes.get(name)!);
const usedDirectives = bound.getUsedDirectives().map(directive => {
return {
selector: directive.selector,
expression: this.refEmitter.emit(directive.ref, context),
};
});

const usedPipes: {pipeName: string, expression: Expression}[] = [];
for (const pipeName of bound.getUsedPipes()) {
if (!pipes.has(pipeName)) {
continue;
}
const pipe = pipes.get(pipeName)!;
usedPipes.push({
pipeName,
expression: this.refEmitter.emit(pipe, context),
});
}

// Scan through the directives/pipes actually used in the template and check whether any
// import which needs to be generated would create a cycle.
const cycleDetected =
usedDirectives.some(dir => this._isCyclicImport(dir.expression, context)) ||
usedPipes.some(pipe => this._isCyclicImport(pipe, context));
usedPipes.some(pipe => this._isCyclicImport(pipe.expression, context));

if (!cycleDetected) {
// No cycle was detected. Record the imports that need to be created in the cycle detector
// so that future cyclic import checks consider their production.
for (const {expression} of usedDirectives) {
this._recordSyntheticImport(expression, context);
}
for (const pipe of usedPipes) {
this._recordSyntheticImport(pipe, context);
for (const {expression} of usedPipes) {
this._recordSyntheticImport(expression, context);
}

// Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures.
Expand All @@ -542,16 +535,11 @@ export class ComponentDecoratorHandler implements
const wrapDirectivesAndPipesInClosure =
usedDirectives.some(
dir => isExpressionForwardReference(dir.expression, node.name, context)) ||
usedPipes.some(pipe => isExpressionForwardReference(pipe, node.name, context));

// Actual compilation still uses the full scope, not the narrowed scope determined by
// R3TargetBinder. This is a hedge against potential issues with the R3TargetBinder - right
// now the TemplateDefinitionBuilder is the "source of truth" for which directives/pipes are
// actually used (though the two should agree perfectly).
//
// TODO(alxhub): switch TemplateDefinitionBuilder over to using R3TargetBinder directly.
data.directives = directives;
data.pipes = pipes;
usedPipes.some(
pipe => isExpressionForwardReference(pipe.expression, node.name, context));

data.directives = usedDirectives;
data.pipes = new Map(usedPipes.map(pipe => [pipe.pipeName, pipe.expression]));
data.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure;
} else {
// Declaring the directiveDefs/pipeDefs arrays directly would require imports that would
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