perf(core): make DI decorators tree-shakable when used for `useFactory` deps config (#40145)

This commit updates the logic that calculates `useFactory` function arguments to avoid relying on `instanceof`
checks (thus always retaining symbols) and relies on flags that DI decorators contain (as a monkey-patched property).

Another perf benefit is having less megamorphic reads while calculating args for the `useFactory` call: we used to
check whether a token has `ngMetadataName` property 4 times (in worst case), now we have just 1 megamorphic read in
all cases.

Closes #40143.

PR Close #40145
This commit is contained in:
Andrew Kushnir 2020-12-15 18:47:57 -08:00 committed by atscott
parent 25788106e5
commit 6cff877f4f
9 changed files with 136 additions and 52 deletions

View File

@ -838,11 +838,11 @@ export declare interface RendererType2 {
} }
export declare class ResolvedReflectiveFactory { export declare class ResolvedReflectiveFactory {
dependencies: ɵangular_packages_core_core_d[]; dependencies: ɵangular_packages_core_core_e[];
factory: Function; factory: Function;
constructor( constructor(
factory: Function, factory: Function,
dependencies: ɵangular_packages_core_core_d[]); dependencies: ɵangular_packages_core_core_e[]);
} }
export declare interface ResolvedReflectiveProvider { export declare interface ResolvedReflectiveProvider {

View File

@ -16,14 +16,20 @@ import {resolveForwardRef} from './forward_ref';
import {getInjectImplementation, injectRootLimpMode} from './inject_switch'; import {getInjectImplementation, injectRootLimpMode} from './inject_switch';
import {InjectionToken} from './injection_token'; import {InjectionToken} from './injection_token';
import {Injector} from './injector'; import {Injector} from './injector';
import {InjectFlags} from './interface/injector'; import {DecoratorFlags, InjectFlags, InternalInjectFlags} from './interface/injector';
import {ValueProvider} from './interface/provider'; import {ValueProvider} from './interface/provider';
import {Host, Inject, Optional, Self, SkipSelf} from './metadata';
const _THROW_IF_NOT_FOUND = {}; const _THROW_IF_NOT_FOUND = {};
export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND; export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND;
/*
* Name of a property (that we patch onto DI decorator), which is used as an annotation of which
* InjectFlag this decorator represents. This allows to avoid direct references to the DI decorators
* in the code, thus making them tree-shakable.
*/
const DI_DECORATOR_FLAG = '__NG_DI_FLAG__';
export const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath'; export const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath';
const NG_TOKEN_PATH = 'ngTokenPath'; const NG_TOKEN_PATH = 'ngTokenPath';
const NEW_LINE = /\n/gm; const NEW_LINE = /\n/gm;
@ -145,17 +151,14 @@ export function injectArgs(types: (Type<any>|InjectionToken<any>|any[])[]): any[
for (let j = 0; j < arg.length; j++) { for (let j = 0; j < arg.length; j++) {
const meta = arg[j]; const meta = arg[j];
if (meta instanceof Optional || meta.ngMetadataName === 'Optional' || meta === Optional) { const flag = getInjectFlag(meta);
flags |= InjectFlags.Optional; if (typeof flag === 'number') {
} else if ( // Special case when we handle @Inject decorator.
meta instanceof SkipSelf || meta.ngMetadataName === 'SkipSelf' || meta === SkipSelf) { if (flag === DecoratorFlags.Inject) {
flags |= InjectFlags.SkipSelf;
} else if (meta instanceof Self || meta.ngMetadataName === 'Self' || meta === Self) {
flags |= InjectFlags.Self;
} else if (meta instanceof Host || meta.ngMetadataName === 'Host' || meta === Host) {
flags |= InjectFlags.Host;
} else if (meta instanceof Inject || meta === Inject) {
type = meta.token; type = meta.token;
} else {
flags |= flag;
}
} else { } else {
type = meta; type = meta;
} }
@ -169,6 +172,30 @@ export function injectArgs(types: (Type<any>|InjectionToken<any>|any[])[]): any[
return args; return args;
} }
/**
* Attaches a given InjectFlag to a given decorator using monkey-patching.
* Since DI decorators can be used in providers `deps` array (when provider is configured using
* `useFactory`) without initialization (e.g. `Host`) and as an instance (e.g. `new Host()`), we
* attach the flag to make it available both as a static property and as a field on decorator
* instance.
*
* @param decorator Provided DI decorator.
* @param flag InjectFlag that should be applied.
*/
export function attachInjectFlag(decorator: any, flag: InternalInjectFlags|DecoratorFlags): any {
decorator[DI_DECORATOR_FLAG] = flag;
decorator.prototype[DI_DECORATOR_FLAG] = flag;
return decorator;
}
/**
* Reads monkey-patched property that contains InjectFlag attached to a decorator.
*
* @param token Token that may contain monkey-patched DI flags property.
*/
export function getInjectFlag(token: any): number|undefined {
return token[DI_DECORATOR_FLAG];
}
export function catchInjectorError( export function catchInjectorError(
e: any, token: any, injectorErrorName: string, source: string|null): never { e: any, token: any, injectorErrorName: string, source: string|null): never {

View File

@ -7,25 +7,67 @@
*/ */
/**
* Special flag indicating that a decorator is of type `Inject`. It's used to make `Inject`
* decorator tree-shakable (so we don't have to rely on the `instanceof` checks).
* Note: this flag is not included into the `InjectFlags` since it's an internal-only API.
*/
export const enum DecoratorFlags {
Inject = -1
}
/** /**
* Injection flags for DI. * Injection flags for DI.
* *
* @publicApi * @publicApi
*/ */
export enum InjectFlags { export enum InjectFlags {
// TODO(alxhub): make this 'const' when ngc no longer writes exports of it into ngfactory files. // TODO(alxhub): make this 'const' (and remove `InternalInjectFlags` enum) when ngc no longer
// writes exports of it into ngfactory files.
/** Check self and check parent injector if needed */ /** Check self and check parent injector if needed */
Default = 0b0000, Default = 0b0000,
/** /**
* Specifies that an injector should retrieve a dependency from any injector until reaching the * Specifies that an injector should retrieve a dependency from any injector until reaching the
* host element of the current component. (Only used with Element Injector) * host element of the current component. (Only used with Element Injector)
*/ */
Host = 0b0001, Host = 0b0001,
/** Don't ascend to ancestors of the node requesting injection. */ /** Don't ascend to ancestors of the node requesting injection. */
Self = 0b0010, Self = 0b0010,
/** Skip the node that is requesting injection. */ /** Skip the node that is requesting injection. */
SkipSelf = 0b0100, SkipSelf = 0b0100,
/** Inject `defaultValue` instead if token not found. */
Optional = 0b1000,
}
/**
* This enum is an exact copy of the `InjectFlags` enum above, but the difference is that this is a
* const enum, so actual enum values would be inlined in generated code. The `InjectFlags` enum can
* be turned into a const enum when ViewEngine is removed (see TODO at the `InjectFlags` enum
* above). The benefit of inlining is that we can use these flags at the top level without affecting
* tree-shaking (see "no-toplevel-property-access" tslint rule for more info).
* Keep this enum in sync with `InjectFlags` enum above.
*/
export const enum InternalInjectFlags {
/** Check self and check parent injector if needed */
Default = 0b0000,
/**
* Specifies that an injector should retrieve a dependency from any injector until reaching the
* host element of the current component. (Only used with Element Injector)
*/
Host = 0b0001,
/** Don't ascend to ancestors of the node requesting injection. */
Self = 0b0010,
/** Skip the node that is requesting injection. */
SkipSelf = 0b0100,
/** Inject `defaultValue` instead if token not found. */ /** Inject `defaultValue` instead if token not found. */
Optional = 0b1000, Optional = 0b1000,
} }

View File

@ -8,6 +8,9 @@
import {makeParamDecorator} from '../util/decorators'; import {makeParamDecorator} from '../util/decorators';
import {attachInjectFlag} from './injector_compatibility';
import {DecoratorFlags, InternalInjectFlags} from './interface/injector';
/** /**
* Type of the Inject decorator / constructor function. * Type of the Inject decorator / constructor function.
@ -54,8 +57,10 @@ export interface Inject {
* @Annotation * @Annotation
* @publicApi * @publicApi
*/ */
export const Inject: InjectDecorator = makeParamDecorator('Inject', (token: any) => ({token})); export const Inject: InjectDecorator = attachInjectFlag(
// Disable tslint because `DecoratorFlags` is a const enum which gets inlined.
// tslint:disable-next-line: no-toplevel-property-access
makeParamDecorator('Inject', (token: any) => ({token})), DecoratorFlags.Inject);
/** /**
* Type of the Optional decorator / constructor function. * Type of the Optional decorator / constructor function.
@ -97,7 +102,10 @@ export interface Optional {}
* @Annotation * @Annotation
* @publicApi * @publicApi
*/ */
export const Optional: OptionalDecorator = makeParamDecorator('Optional'); export const Optional: OptionalDecorator =
// Disable tslint because `InternalInjectFlags` is a const enum which gets inlined.
// tslint:disable-next-line: no-toplevel-property-access
attachInjectFlag(makeParamDecorator('Optional'), InternalInjectFlags.Optional);
/** /**
* Type of the Self decorator / constructor function. * Type of the Self decorator / constructor function.
@ -142,7 +150,10 @@ export interface Self {}
* @Annotation * @Annotation
* @publicApi * @publicApi
*/ */
export const Self: SelfDecorator = makeParamDecorator('Self'); export const Self: SelfDecorator =
// Disable tslint because `InternalInjectFlags` is a const enum which gets inlined.
// tslint:disable-next-line: no-toplevel-property-access
attachInjectFlag(makeParamDecorator('Self'), InternalInjectFlags.Self);
/** /**
@ -187,7 +198,10 @@ export interface SkipSelf {}
* @Annotation * @Annotation
* @publicApi * @publicApi
*/ */
export const SkipSelf: SkipSelfDecorator = makeParamDecorator('SkipSelf'); export const SkipSelf: SkipSelfDecorator =
// Disable tslint because `InternalInjectFlags` is a const enum which gets inlined.
// tslint:disable-next-line: no-toplevel-property-access
attachInjectFlag(makeParamDecorator('SkipSelf'), InternalInjectFlags.SkipSelf);
/** /**
* Type of the `Host` decorator / constructor function. * Type of the `Host` decorator / constructor function.
@ -227,4 +241,7 @@ export interface Host {}
* @Annotation * @Annotation
* @publicApi * @publicApi
*/ */
export const Host: HostDecorator = makeParamDecorator('Host'); export const Host: HostDecorator =
// Disable tslint because `InternalInjectFlags` is a const enum which gets inlined.
// tslint:disable-next-line: no-toplevel-property-access
attachInjectFlag(makeParamDecorator('Host'), InternalInjectFlags.Host);

View File

@ -236,9 +236,6 @@
{ {
"name": "FormsModule" "name": "FormsModule"
}, },
{
"name": "Host"
},
{ {
"name": "INJECTOR" "name": "INJECTOR"
}, },
@ -569,9 +566,6 @@
{ {
"name": "SelectMultipleControlValueAccessor" "name": "SelectMultipleControlValueAccessor"
}, },
{
"name": "Self"
},
{ {
"name": "ShadowDomRenderer" "name": "ShadowDomRenderer"
}, },
@ -743,6 +737,9 @@
{ {
"name": "applyView" "name": "applyView"
}, },
{
"name": "attachInjectFlag"
},
{ {
"name": "attachPatchData" "name": "attachPatchData"
}, },

View File

@ -5,18 +5,12 @@
{ {
"name": "EMPTY_ARRAY" "name": "EMPTY_ARRAY"
}, },
{
"name": "Host"
},
{ {
"name": "INJECTOR" "name": "INJECTOR"
}, },
{ {
"name": "INJECTOR_SCOPE" "name": "INJECTOR_SCOPE"
}, },
{
"name": "Inject"
},
{ {
"name": "InjectFlags" "name": "InjectFlags"
}, },
@ -50,21 +44,12 @@
{ {
"name": "NullInjector" "name": "NullInjector"
}, },
{
"name": "Optional"
},
{ {
"name": "R3Injector" "name": "R3Injector"
}, },
{ {
"name": "ScopedService" "name": "ScopedService"
}, },
{
"name": "Self"
},
{
"name": "SkipSelf"
},
{ {
"name": "THROW_IF_NOT_FOUND" "name": "THROW_IF_NOT_FOUND"
}, },
@ -113,9 +98,6 @@
{ {
"name": "isValueProvider" "name": "isValueProvider"
}, },
{
"name": "makeParamDecorator"
},
{ {
"name": "makeRecord" "name": "makeRecord"
}, },

View File

@ -290,9 +290,6 @@
{ {
"name": "HashLocationStrategy" "name": "HashLocationStrategy"
}, },
{
"name": "Host"
},
{ {
"name": "INITIAL_VALUE" "name": "INITIAL_VALUE"
}, },
@ -731,9 +728,6 @@
{ {
"name": "SecurityContext" "name": "SecurityContext"
}, },
{
"name": "Self"
},
{ {
"name": "ShadowDomRenderer" "name": "ShadowDomRenderer"
}, },
@ -971,6 +965,9 @@
{ {
"name": "applyView" "name": "applyView"
}, },
{
"name": "attachInjectFlag"
},
{ {
"name": "attachPatchData" "name": "attachPatchData"
}, },

View File

@ -221,6 +221,9 @@
{ {
"name": "assertTemplate" "name": "assertTemplate"
}, },
{
"name": "attachInjectFlag"
},
{ {
"name": "attachPatchData" "name": "attachPatchData"
}, },

View File

@ -0,0 +1,19 @@
/**
* @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 {InjectFlags, InternalInjectFlags} from '../../src/di/interface/injector';
describe('InjectFlags', () => {
it('should always match InternalInjectFlags', () => {
expect(InjectFlags.Default).toEqual(InternalInjectFlags.Default as number);
expect(InjectFlags.Host).toEqual(InternalInjectFlags.Host as number);
expect(InjectFlags.Self).toEqual(InternalInjectFlags.Self as number);
expect(InjectFlags.SkipSelf).toEqual(InternalInjectFlags.SkipSelf as number);
expect(InjectFlags.Optional).toEqual(InternalInjectFlags.Optional as number);
});
});