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
							
								
									4faac78e32
								
							
						
					
					
						commit
						ba95b79a21
					
				| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user