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 {
dependencies: ɵangular_packages_core_core_d[];
dependencies: ɵangular_packages_core_core_e[];
factory: Function;
constructor(
factory: Function,
dependencies: ɵangular_packages_core_core_d[]);
dependencies: ɵangular_packages_core_core_e[]);
}
export declare interface ResolvedReflectiveProvider {

View File

@ -16,14 +16,20 @@ import {resolveForwardRef} from './forward_ref';
import {getInjectImplementation, injectRootLimpMode} from './inject_switch';
import {InjectionToken} from './injection_token';
import {Injector} from './injector';
import {InjectFlags} from './interface/injector';
import {DecoratorFlags, InjectFlags, InternalInjectFlags} from './interface/injector';
import {ValueProvider} from './interface/provider';
import {Host, Inject, Optional, Self, SkipSelf} from './metadata';
const _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';
const NG_TOKEN_PATH = 'ngTokenPath';
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++) {
const meta = arg[j];
if (meta instanceof Optional || meta.ngMetadataName === 'Optional' || meta === Optional) {
flags |= InjectFlags.Optional;
} else if (
meta instanceof SkipSelf || meta.ngMetadataName === 'SkipSelf' || meta === SkipSelf) {
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) {
const flag = getInjectFlag(meta);
if (typeof flag === 'number') {
// Special case when we handle @Inject decorator.
if (flag === DecoratorFlags.Inject) {
type = meta.token;
} else {
flags |= flag;
}
} else {
type = meta;
}
@ -169,6 +172,30 @@ export function injectArgs(types: (Type<any>|InjectionToken<any>|any[])[]): any[
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(
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.
*
* @publicApi
*/
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 */
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. */
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. */
Optional = 0b1000,
}

View File

@ -8,6 +8,9 @@
import {makeParamDecorator} from '../util/decorators';
import {attachInjectFlag} from './injector_compatibility';
import {DecoratorFlags, InternalInjectFlags} from './interface/injector';
/**
* Type of the Inject decorator / constructor function.
@ -54,8 +57,10 @@ export interface Inject {
* @Annotation
* @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.
@ -97,7 +102,10 @@ export interface Optional {}
* @Annotation
* @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.
@ -142,7 +150,10 @@ export interface Self {}
* @Annotation
* @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
* @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.
@ -227,4 +241,7 @@ export interface Host {}
* @Annotation
* @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": "Host"
},
{
"name": "INJECTOR"
},
@ -569,9 +566,6 @@
{
"name": "SelectMultipleControlValueAccessor"
},
{
"name": "Self"
},
{
"name": "ShadowDomRenderer"
},
@ -743,6 +737,9 @@
{
"name": "applyView"
},
{
"name": "attachInjectFlag"
},
{
"name": "attachPatchData"
},

View File

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

View File

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

View File

@ -221,6 +221,9 @@
{
"name": "assertTemplate"
},
{
"name": "attachInjectFlag"
},
{
"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);
});
});