feat(ivy): avoid unnecessary recompilations in TestBed (#29294)

Prior to this change, we always recompile all Components/Directives/Pipes even if they were AOT-compiled and had no overrides. This is causing problems in case we try to recompile a Component with "templateUrl" or "styleUrls" (which were already resolved in case of AOT) and generally this unnecessary work that TestBed was doing is not required. This commit adds extra logic to check whether a Component/Directive/Pipe already have compiled NG def (like ngComponentDef) and whether there are no overrides present - in this case recompilation is skipped. Recompilation is also skipped in case a Component/Directive has only Provider overrides - in this situation providers resolver function is patched to reflect overrides. Provider overrides are very common in g3, thus this code path ensures no full recompilation.

PR Close #29294
This commit is contained in:
Andrew Kushnir
2019-03-11 10:35:25 -07:00
committed by Matias Niemelä
parent 86aba1e8f3
commit 0244a2433e
11 changed files with 207 additions and 60 deletions

View File

@ -365,3 +365,9 @@ export interface ClassProvider extends ClassSansProvider {
*/
export type Provider = TypeProvider | ValueProvider | ClassProvider | ConstructorProvider |
ExistingProvider | FactoryProvider | any[];
/**
* Describes a function that is used to process provider list (for example in case of provider
* overrides).
*/
export type ProcessProvidersFunction = (providers: Provider[]) => Provider[];

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Type} from '../interface/type';
import {Component} from './directives';
@ -42,9 +43,9 @@ import {Component} from './directives';
* contents of the resolved URL. Browser's `fetch()` method is a good default implementation.
*/
export function resolveComponentResources(
resourceResolver: (url: string) => (Promise<string|{text(): Promise<string>}>)): Promise<null> {
resourceResolver: (url: string) => (Promise<string|{text(): Promise<string>}>)): Promise<void> {
// Store all promises which are fetching the resources.
const urlFetches: Promise<string>[] = [];
const componentResolved: Promise<void>[] = [];
// Cache so that we don't fetch the same resource more than once.
const urlMap = new Map<string, Promise<string>>();
@ -53,50 +54,68 @@ export function resolveComponentResources(
if (!promise) {
const resp = resourceResolver(url);
urlMap.set(url, promise = resp.then(unwrapResponse));
urlFetches.push(promise);
}
return promise;
}
componentResourceResolutionQueue.forEach((component: Component) => {
componentResourceResolutionQueue.forEach((component: Component, type: Type<any>) => {
const promises: Promise<void>[] = [];
if (component.templateUrl) {
cachedResourceResolve(component.templateUrl).then((template) => {
promises.push(cachedResourceResolve(component.templateUrl).then((template) => {
component.template = template;
});
}));
}
const styleUrls = component.styleUrls;
const styles = component.styles || (component.styles = []);
const styleOffset = component.styles.length;
styleUrls && styleUrls.forEach((styleUrl, index) => {
styles.push(''); // pre-allocate array.
cachedResourceResolve(styleUrl).then((style) => {
promises.push(cachedResourceResolve(styleUrl).then((style) => {
styles[styleOffset + index] = style;
styleUrls.splice(styleUrls.indexOf(styleUrl), 1);
if (styleUrls.length == 0) {
component.styleUrls = undefined;
}
});
}));
});
const fullyResolved = Promise.all(promises).then(() => componentDefResolved(type));
componentResolved.push(fullyResolved);
});
clearResolutionOfComponentResourcesQueue();
return Promise.all(urlFetches).then(() => null);
return Promise.all(componentResolved).then(() => undefined);
}
const componentResourceResolutionQueue: Set<Component> = new Set();
let componentResourceResolutionQueue = new Map<Type<any>, Component>();
export function maybeQueueResolutionOfComponentResources(metadata: Component) {
// Track when existing ngComponentDef for a Type is waiting on resources.
const componentDefPendingResolution = new Set<Type<any>>();
export function maybeQueueResolutionOfComponentResources(type: Type<any>, metadata: Component) {
if (componentNeedsResolution(metadata)) {
componentResourceResolutionQueue.add(metadata);
componentResourceResolutionQueue.set(type, metadata);
componentDefPendingResolution.add(type);
}
}
export function isComponentDefPendingResolution(type: Type<any>): boolean {
return componentDefPendingResolution.has(type);
}
export function componentNeedsResolution(component: Component): boolean {
return !!(
(component.templateUrl && !component.template) ||
component.styleUrls && component.styleUrls.length);
}
export function clearResolutionOfComponentResourcesQueue() {
componentResourceResolutionQueue.clear();
export function clearResolutionOfComponentResourcesQueue(): Map<Type<any>, Component> {
const old = componentResourceResolutionQueue;
componentResourceResolutionQueue = new Map();
return old;
}
export function restoreComponentResolutionQueue(queue: Map<Type<any>, Component>): void {
componentDefPendingResolution.clear();
queue.forEach((_, type) => componentDefPendingResolution.add(type));
componentResourceResolutionQueue = queue;
}
export function isComponentResourceResolutionQueueEmpty() {
@ -106,3 +125,7 @@ export function isComponentResourceResolutionQueueEmpty() {
function unwrapResponse(response: string | {text(): Promise<string>}): string|Promise<string> {
return typeof response == 'string' ? response : response.text();
}
function componentDefResolved(type: Type<any>): void {
componentDefPendingResolution.delete(type);
}

View File

@ -5,7 +5,7 @@
* 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 {Provider} from '../../di/interface/provider';
import {ProcessProvidersFunction, Provider} from '../../di/interface/provider';
import {providersResolver} from '../di_setup';
import {DirectiveDef} from '../interfaces/definition';
@ -39,7 +39,12 @@ import {DirectiveDef} from '../interfaces/definition';
*/
export function ProvidersFeature<T>(providers: Provider[], viewProviders: Provider[] = []) {
return (definition: DirectiveDef<T>) => {
definition.providersResolver = (def: DirectiveDef<T>) =>
providersResolver(def, providers, viewProviders);
definition.providersResolver =
(def: DirectiveDef<T>, processProvidersFn?: ProcessProvidersFunction) => {
return providersResolver(
def, //
processProvidersFn ? processProvidersFn(providers) : providers, //
viewProviders);
};
};
}

View File

@ -7,6 +7,7 @@
*/
import {SchemaMetadata, ViewEncapsulation} from '../../core';
import {ProcessProvidersFunction} from '../../di/interface/provider';
import {Type} from '../../interface/type';
import {CssSelectorList} from './projection';
@ -138,7 +139,9 @@ export interface DirectiveDef<T> extends BaseDef<T> {
type: Type<T>;
/** Function that resolves providers and publishes them into the DI system. */
providersResolver: (<U extends T>(def: DirectiveDef<U>) => void)|null;
providersResolver:
(<U extends T>(def: DirectiveDef<U>, processProvidersFn?: ProcessProvidersFunction) =>
void)|null;
/** The selectors that will be used to match nodes to this directive. */
readonly selectors: CssSelectorList;

View File

@ -37,7 +37,7 @@ import {flushModuleScopingQueueAsMuchAsPossible, patchComponentDefWithScope, tra
export function compileComponent(type: Type<any>, metadata: Component): void {
let ngComponentDef: any = null;
// Metadata may have resources which need to be resolved.
maybeQueueResolutionOfComponentResources(metadata);
maybeQueueResolutionOfComponentResources(type, metadata);
Object.defineProperty(type, NG_COMPONENT_DEF, {
get: () => {
const compiler = getCompilerFacade();

View File

@ -357,6 +357,12 @@ export function patchComponentDefWithScope<C>(
componentDef.pipeDefs = () =>
Array.from(transitiveScopes.compilation.pipes).map(pipe => getPipeDef(pipe) !);
componentDef.schemas = transitiveScopes.schemas;
// Since we avoid Components/Directives/Pipes recompiling in case there are no overrides, we
// may face a problem where previously compiled defs available to a given Component/Directive
// are cached in TView and may become stale (in case any of these defs gets recompiled). In
// order to avoid this problem, we force fresh TView to be created.
componentDef.template.ngPrivateData = undefined;
}
/**