diff --git a/packages/core/src/di/contextual.ts b/packages/core/src/di/contextual.ts index a722a1ec2f8..4e1775c6737 100644 --- a/packages/core/src/di/contextual.ts +++ b/packages/core/src/di/contextual.ts @@ -7,6 +7,7 @@ */ import {RuntimeError, RuntimeErrorCode} from '../errors'; +import {InjectorProfilerContext, setInjectorProfilerContext} from '../render3/debug/injector_profiler'; import type {Injector} from './injector'; import {getCurrentInjector, setCurrentInjector} from './injector_compatibility'; import {getInjectImplementation, setInjectImplementation} from './inject_switch'; @@ -31,12 +32,17 @@ export function runInInjectionContext(injector: Injector, fn: () => Ret injector.assertNotDestroyed(); } + let prevInjectorProfilerContext: InjectorProfilerContext; + if (ngDevMode) { + prevInjectorProfilerContext = setInjectorProfilerContext({injector, token: null}); + } const prevInjector = setCurrentInjector(injector); const previousInjectImplementation = setInjectImplementation(undefined); try { return fn(); } finally { setCurrentInjector(prevInjector); + ngDevMode && setInjectorProfilerContext(prevInjectorProfilerContext!); setInjectImplementation(previousInjectImplementation); } } diff --git a/packages/core/src/di/injector_compatibility.ts b/packages/core/src/di/injector_compatibility.ts index 3fba9f1e6ac..9537bffd2af 100644 --- a/packages/core/src/di/injector_compatibility.ts +++ b/packages/core/src/di/injector_compatibility.ts @@ -10,6 +10,7 @@ import '../util/ng_dev_mode'; import {RuntimeError, RuntimeErrorCode} from '../errors'; import {Type} from '../interface/type'; +import {emitInjectEvent} from '../render3/debug/injector_profiler'; import {stringify} from '../util/stringify'; import {resolveForwardRef} from './forward_ref'; @@ -65,7 +66,10 @@ export function injectInjectorOnly(token: ProviderToken, flags = InjectFla } else if (_currentInjector === null) { return injectRootLimpMode(token, undefined, flags); } else { - return _currentInjector.get(token, flags & InjectFlags.Optional ? null : undefined, flags); + const value = + _currentInjector.get(token, flags & InjectFlags.Optional ? null : undefined, flags); + ngDevMode && emitInjectEvent(token as Type, value, flags); + return value; } } diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index ecb4825a52d..cbcec4c60d1 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -11,6 +11,7 @@ import '../util/ng_dev_mode'; import {RuntimeError, RuntimeErrorCode} from '../errors'; import {OnDestroy} from '../interface/lifecycle_hooks'; import {Type} from '../interface/type'; +import {emitInstanceCreatedByInjectorEvent, emitProviderConfiguredEvent, InjectorProfilerContext, runInInjectorProfilerContext, setInjectorProfilerContext} from '../render3/debug/injector_profiler'; import {FactoryFn, getFactoryDef} from '../render3/definition_factory'; import {throwCyclicDependencyError, throwInvalidProviderError, throwMixedMultiProviderError} from '../render3/errors_di'; import {NG_ENV_ID} from '../render3/fields'; @@ -225,11 +226,18 @@ export class R3Injector extends EnvironmentInjector { const previousInjector = setCurrentInjector(this); const previousInjectImplementation = setInjectImplementation(undefined); + + let prevInjectContext: InjectorProfilerContext|undefined; + if (ngDevMode) { + prevInjectContext = setInjectorProfilerContext({injector: this, token: null}); + } + try { return fn(); } finally { setCurrentInjector(previousInjector); setInjectImplementation(previousInjectImplementation); + ngDevMode && setInjectorProfilerContext(prevInjectContext!); } } @@ -245,6 +253,10 @@ export class R3Injector extends EnvironmentInjector { flags = convertToBitFlags(flags) as InjectFlags; // Set the injection context. + let prevInjectContext: InjectorProfilerContext; + if (ngDevMode) { + prevInjectContext = setInjectorProfilerContext({injector: this, token: token as Type}); + } const previousInjector = setCurrentInjector(this); const previousInjectImplementation = setInjectImplementation(undefined); try { @@ -298,6 +310,7 @@ export class R3Injector extends EnvironmentInjector { // Lastly, restore the previous injection context. setInjectImplementation(previousInjectImplementation); setCurrentInjector(previousInjector); + ngDevMode && setInjectorProfilerContext(prevInjectContext!); } } @@ -305,6 +318,11 @@ export class R3Injector extends EnvironmentInjector { resolveInjectorInitializers() { const previousInjector = setCurrentInjector(this); const previousInjectImplementation = setInjectImplementation(undefined); + let prevInjectContext: InjectorProfilerContext|undefined; + if (ngDevMode) { + prevInjectContext = setInjectorProfilerContext({injector: this, token: null}); + } + try { const initializers = this.get(ENVIRONMENT_INITIALIZER.multi, EMPTY_ARRAY, InjectFlags.Self); if (ngDevMode && !Array.isArray(initializers)) { @@ -321,6 +339,7 @@ export class R3Injector extends EnvironmentInjector { } finally { setCurrentInjector(previousInjector); setInjectImplementation(previousInjectImplementation); + ngDevMode && setInjectorProfilerContext(prevInjectContext!); } } @@ -353,6 +372,18 @@ export class R3Injector extends EnvironmentInjector { // Construct a `Record` for the provider. const record = providerToRecord(provider); + if (ngDevMode) { + runInInjectorProfilerContext(this, token, () => { + // Emit InjectorProfilerEventType.Create if provider is a value provider because + // these are the only providers that do not go through the value hydration logic + // where this event would normally be emitted from. + if (isValueProvider(provider)) { + emitInstanceCreatedByInjectorEvent(provider.useValue); + } + + emitProviderConfiguredEvent(provider); + }); + } if (!isTypeProvider(provider) && provider.multi === true) { // If the provider indicates that it's a multi-provider, process it specially. @@ -384,7 +415,15 @@ export class R3Injector extends EnvironmentInjector { throwCyclicDependencyError(stringify(token)); } else if (record.value === NOT_YET) { record.value = CIRCULAR; - record.value = record.factory!(); + + if (ngDevMode) { + runInInjectorProfilerContext(this, token as Type, () => { + record.value = record.factory!(); + emitInstanceCreatedByInjectorEvent(record.value); + }); + } else { + record.value = record.factory!(); + } } if (typeof record.value === 'object' && record.value && hasOnDestroy(record.value)) { this._ngOnDestroyHooks.add(record.value); diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index ad4c369bc58..84f4d8ff0f5 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -87,8 +87,8 @@ function getNamespace(elementName: string): string|null { * Injector that looks up a value using a specific injector, before falling back to the module * injector. Used primarily when creating components or embedded views dynamically. */ -class ChainedInjector implements Injector { - constructor(private injector: Injector, private parentInjector: Injector) {} +export class ChainedInjector implements Injector { + constructor(public injector: Injector, public parentInjector: Injector) {} get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags|InjectOptions): T { flags = convertToBitFlags(flags); diff --git a/packages/core/src/render3/debug/framework_injector_profiler.ts b/packages/core/src/render3/debug/framework_injector_profiler.ts new file mode 100644 index 00000000000..1469d5730ac --- /dev/null +++ b/packages/core/src/render3/debug/framework_injector_profiler.ts @@ -0,0 +1,246 @@ +/** + * @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 {Injector} from '../../di/injector'; +import {EnvironmentInjector} from '../../di/r3_injector'; +import {Type} from '../../interface/type'; +import {throwError} from '../../util/assert'; +import {getComponentDef} from '../definition'; +import {getNodeInjectorLView, NodeInjector} from '../di'; +import {LView} from '../interfaces/view'; + +import {InjectedService, InjectorCreatedInstance, InjectorProfilerContext, InjectorProfilerEvent, InjectorProfilerEventType, ProviderRecord, setInjectorProfiler} from './injector_profiler'; + +/** + * These are the data structures that our framework injector profiler will fill with data in order + * to support DI debugging APIs. + * + * resolverToTokenToDependencies: Maps an injector to a Map of tokens to an Array of + * dependencies. Injector -> Token -> Dependencies This is used to support the + * getDependenciesFromInjectable API, which takes in an injector and a token and returns it's + * dependencies. + * + * resolverToProviders: Maps a DI resolver (an Injector or an LView) to the providers configured + * within it This is used to support the getInjectorProviders API, which takes in an injector and + * returns the providers that it was configured with. + * + * standaloneInjectorToComponent: Maps the injector of a standalone component to the standalone + * component that it is associated with. Used in the getInjectorProviders API, specificially in the + * discovery of import paths for each provider. This is necessary because the imports array of a + * standalone component is processed and configured in its standalone injector, but exists within + * the component's definition. Because getInjectorProviders takes in an injector, if that injector + * is the injector of a standalone component, we need to be able to discover the place where the + * imports array is located (the component) in order to flatten the imports array within it to + * discover all of it's providers. + * + * + * All of these data structures are instantiated with WeakMaps. This will ensure that the presence + * of any object in the keys of these maps does not prevent the garbage collector from collecting + * those objects. Because of this property of WeakMaps, these data structures will never be the + * source of a memory leak. + * + * An example of this advantage: When components are destroyed, we don't need to do + * any additional work to remove that component from our mappings. + * + */ +class DIDebugData { + resolverToTokenToDependencies = + new WeakMap, InjectedService[]>>(); + resolverToProviders = new WeakMap(); + standaloneInjectorToComponent = new WeakMap>(); + + reset() { + this.resolverToTokenToDependencies = + new WeakMap, InjectedService[]>>(); + this.resolverToProviders = new WeakMap(); + this.standaloneInjectorToComponent = new WeakMap>(); + } +} + +let frameworkDIDebugData = new DIDebugData(); + +export function getFrameworkDIDebugData(): DIDebugData { + return frameworkDIDebugData; +} + +/** + * Initalize default handling of injector events. This handling parses events + * as they are emitted and constructs the data structures necessary to support + * some of debug APIs. + * + * See handleInjectEvent, handleCreateEvent and handleProviderConfiguredEvent + * for descriptions of each handler + * + * Supported APIs: + * - getDependenciesFromInjectable + * - getInjectorProviders + */ +export function setupFrameworkInjectorProfiler(): void { + frameworkDIDebugData.reset(); + setInjectorProfiler( + (injectorProfilerEvent) => handleInjectorProfilerEvent(injectorProfilerEvent)); +} + +function handleInjectorProfilerEvent(injectorProfilerEvent: InjectorProfilerEvent): void { + const {context, type} = injectorProfilerEvent; + + if (type === InjectorProfilerEventType.Inject) { + handleInjectEvent(context, injectorProfilerEvent.service); + } else if (type === InjectorProfilerEventType.InstanceCreatedByInjector) { + handleInstanceCreatedByInjectorEvent(context, injectorProfilerEvent.instance); + } else if (type === InjectorProfilerEventType.ProviderConfigured) { + handleProviderConfiguredEvent(context, injectorProfilerEvent.providerRecord); + } +} + +/** + * + * Stores the injected service in frameworkDIDebugData.resolverToTokenToDependencies + * based on it's injector and token. + * + * @param context InjectorProfilerContext the injection context that this event occurred in. + * @param data InjectedService the service associated with this inject event. + * + */ +function handleInjectEvent(context: InjectorProfilerContext, data: InjectedService) { + const diResolver = getDIResolver(context.injector); + if (diResolver === null) { + throwError('An Inject event must be run within an injection context.'); + } + + const diResolverToInstantiatedToken = frameworkDIDebugData.resolverToTokenToDependencies; + + if (!diResolverToInstantiatedToken.has(diResolver)) { + diResolverToInstantiatedToken.set(diResolver, new WeakMap, InjectedService[]>()); + } + + // if token is a primitive type, ignore this event. We do this because we cannot keep track of + // non-primitive tokens in WeakMaps since they are not garbage collectable. + if (!canBeHeldWeakly(context.token)) { + return; + } + + const instantiatedTokenToDependencies = diResolverToInstantiatedToken.get(diResolver)!; + if (!instantiatedTokenToDependencies.has(context.token!)) { + instantiatedTokenToDependencies.set(context.token!, []); + } + + const {token, value, flags} = data; + instantiatedTokenToDependencies.get(context.token!)!.push({token, value, flags}); +} + +/** + * + * If the created instance is an instance of a standalone component, maps the injector to that + * standalone component in frameworkDIDebugData.standaloneInjectorToComponent + * + * @param context InjectorProfilerContext the injection context that this event occurred in. + * @param data InjectorCreatedInstance an object containing the instance that was just created + * + */ +function handleInstanceCreatedByInjectorEvent( + context: InjectorProfilerContext, data: InjectorCreatedInstance): void { + const {value} = data; + + if (getDIResolver(context.injector) === null) { + throwError('An InjectorCreatedInstance event must be run within an injection context.'); + } + + // if our value is an instance of a standalone component, map the injector of that standalone + // component to the component class. Otherwise, this event is a noop. + let standaloneComponent: Type|undefined = undefined; + if (typeof value === 'object') { + standaloneComponent = value?.constructor as Type; + } + if (standaloneComponent === undefined || !isStandaloneComponent(standaloneComponent)) { + return; + } + + const environmentInjector: EnvironmentInjector|null = + context.injector.get(EnvironmentInjector, null, {optional: true}); + // Standalone components should have an environment injector. If one cannot be + // found we may be in a test case for low level functionality that did not explictly + // setup this injector. In those cases, we simply ignore this event. + if (environmentInjector === null) { + return; + } + + const {standaloneInjectorToComponent} = frameworkDIDebugData; + + // If our injector has already been mapped, as is the case + // when a standalone component imports another standalone component, + // we consider the original component (the component doing the importing) + // as the component connected to our injector. + if (standaloneInjectorToComponent.has(environmentInjector)) { + return; + } + // If our injector hasn't been mapped, then we map it to the standalone component + standaloneInjectorToComponent.set(environmentInjector, standaloneComponent); +} + +function isStandaloneComponent(value: Type): boolean { + const def = getComponentDef(value); + return !!def?.standalone; +} + +/** + * + * Stores the emitted ProviderRecords from the InjectorProfilerEventType.ProviderConfigured + * event in frameworkDIDebugData.resolverToProviders + * + * @param context InjectorProfilerContext the injection context that this event occurred in. + * @param data ProviderRecord an object containing the instance that was just created + * + */ +function handleProviderConfiguredEvent( + context: InjectorProfilerContext, data: ProviderRecord): void { + const {resolverToProviders} = frameworkDIDebugData; + + const diResolver = getDIResolver(context?.injector); + if (diResolver === null) { + throwError('A ProviderConfigured event must be run within an injection context.'); + } + + if (!resolverToProviders.has(diResolver)) { + resolverToProviders.set(diResolver, []); + } + + resolverToProviders.get(diResolver)!.push(data); +} + +function getDIResolver(injector: Injector|undefined): Injector|LView|null { + let diResolver: Injector|LView|null = null; + + if (injector === undefined) { + return diResolver; + } + + // We use the LView as the diResolver for NodeInjectors because they + // do not persist anywhere in the framework. They are simply wrappers around an LView and a TNode + // that do persist. Because of this, we rely on the LView of the NodeInjector in order to use + // as a concrete key to represent this injector. If we get the same LView back later, we know + // we're looking at the same injector. + if (injector instanceof NodeInjector) { + diResolver = getNodeInjectorLView(injector); + } + // Other injectors can be used a keys for a map because their instances + // persist + else { + diResolver = injector; + } + + return diResolver; +} + +// inspired by +// https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-canbeheldweakly +function canBeHeldWeakly(value: any): boolean { + // we check for value !== null here because typeof null === 'object + return value !== null && + (typeof value === 'object' || typeof value === 'function' || typeof value === 'symbol'); +} diff --git a/packages/core/src/render3/debug/injector_profiler.ts b/packages/core/src/render3/debug/injector_profiler.ts new file mode 100644 index 00000000000..d6ad45cf3c6 --- /dev/null +++ b/packages/core/src/render3/debug/injector_profiler.ts @@ -0,0 +1,254 @@ +/** + * @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 {resolveForwardRef} from '../../di/forward_ref'; +import {InjectionToken} from '../../di/injection_token'; +import type {Injector} from '../../di/injector'; +import {InjectFlags, InjectOptions, InternalInjectFlags} from '../../di/interface/injector'; +import type {SingleProvider} from '../../di/provider_collection'; +import {Type} from '../../interface/type'; +import {throwError} from '../../util/assert'; + +/** + * An enum describing the types of events that can be emitted from the injector profiler + */ +export const enum InjectorProfilerEventType { + /** + * Emits when a service is injected. + */ + Inject, + + /** + * Emits when an Angular class instance is created by an injector. + */ + InstanceCreatedByInjector, + + /** + * Emits when an injector configures a provider. + */ + ProviderConfigured +} + +/** + * An object that defines an injection context for the injector profiler. + */ +export interface InjectorProfilerContext { + /** + * The Injector that service is being injected into. + * - Example: if ModuleA --provides--> ServiceA --injects--> ServiceB + * then inject(ServiceB) in ServiceA has ModuleA as an injector context + */ + injector: Injector; + + /** + * The class where the constructor that is calling `inject` is located + * - Example: if ModuleA --provides--> ServiceA --injects--> ServiceB + * then inject(ServiceB) in ServiceA has ServiceA as a construction context + */ + token: Type|null; +} + + +export interface InjectedServiceEvent { + type: InjectorProfilerEventType.Inject; + context: InjectorProfilerContext; + service: InjectedService; +} + +export interface InjectorCreatedInstanceEvent { + type: InjectorProfilerEventType.InstanceCreatedByInjector; + context: InjectorProfilerContext; + instance: InjectorCreatedInstance; +} + +export interface ProviderConfiguredEvent { + type: InjectorProfilerEventType.ProviderConfigured; + context: InjectorProfilerContext; + providerRecord: ProviderRecord; +} + +/** + * An object representing an event that is emitted through the injector profiler + */ + +export type InjectorProfilerEvent = + InjectedServiceEvent|InjectorCreatedInstanceEvent|ProviderConfiguredEvent; + +/** + * An object that contains information about a provider that has been configured + */ +export interface ProviderRecord { + /** + * DI token that this provider is configuring + */ + token: Type; + + /** + * Determines if provider is configured as view provider. + */ + isViewProvider: boolean; + + /** + * The raw provider associated with this ProviderRecord. + */ + provider: SingleProvider; + + /** + * The path of DI containers that were followed to import this provider + */ + importPath?: Type[]; +} + +/** + * An object that contains information about a value that has been constructed within an injector + */ +export interface InjectorCreatedInstance { + /** + * Value of the created instance + */ + value: unknown; +} + +/** + * An object that contains information a service that has been injected within an + * InjectorProfilerContext + */ +export interface InjectedService { + /** + * DI token of the Service that is injected + */ + token?: Type|InjectionToken; + + /** + * Value of the injected service + */ + value: unknown; + + /** + * Flags that this service was injected with + */ + flags?: InternalInjectFlags|InjectFlags|InjectOptions; + + /** + * Injector that this service was provided in. + */ + providedIn?: Injector; +} + +export interface InjectorProfiler { + (event: InjectorProfilerEvent): void; +} + +let _injectorProfilerContext: InjectorProfilerContext; +export function getInjectorProfilerContext() { + !ngDevMode && throwError('getInjectorProfilerContext should never be called in production mode'); + return _injectorProfilerContext; +} + +export function setInjectorProfilerContext(context: InjectorProfilerContext) { + !ngDevMode && throwError('setInjectorProfilerContext should never be called in production mode'); + + const previous = _injectorProfilerContext; + _injectorProfilerContext = context; + return previous; +} + +let injectorProfilerCallback: InjectorProfiler|null = null; + +/** + * Sets the callback function which will be invoked during certain DI events within the + * runtime (for example: injecting services, creating injectable instances, configuring providers) + * + * Warning: this function is *INTERNAL* and should not be relied upon in application's code. + * The contract of the function might be changed in any release and/or the function can be removed + * completely. + * + * @param profiler function provided by the caller or null value to disable profiling. + */ +export const setInjectorProfiler = (injectorProfiler: InjectorProfiler|null) => { + !ngDevMode && throwError('setInjectorProfiler should never be called in production mode'); + injectorProfilerCallback = injectorProfiler; +}; + +/** + * Injector profiler function which emits on DI events executed by the runtime. + * + * @param event InjectorProfilerEvent corresponding to the DI event being emitted + */ +function injectorProfiler(event: InjectorProfilerEvent): void { + !ngDevMode && throwError('Injector profiler should never be called in production mode'); + + if (injectorProfilerCallback != null /* both `null` and `undefined` */) { + injectorProfilerCallback!(event); + } +} + +/** + * Emits an InjectorProfilerEventType.ProviderConfigured to the injector profiler. The data in the + * emitted event includes the raw provider, as well as the token that provider is providing. + * + * @param provider A provider object + */ +export function emitProviderConfiguredEvent( + provider: SingleProvider, isViewProvider: boolean = false): void { + !ngDevMode && throwError('Injector profiler should never be called in production mode'); + + injectorProfiler({ + type: InjectorProfilerEventType.ProviderConfigured, + context: getInjectorProfilerContext(), + providerRecord: { + token: typeof provider === 'function' ? provider : resolveForwardRef(provider.provide), + provider, + isViewProvider + } + }); +} + +/** + * Emits an event to the injector profiler with the instance that was created. Note that + * the injector associated with this emission can be accessed by using getDebugInjectContext() + * + * @param instance an object created by an injector + */ +export function emitInstanceCreatedByInjectorEvent(instance: unknown): void { + !ngDevMode && throwError('Injector profiler should never be called in production mode'); + + injectorProfiler({ + type: InjectorProfilerEventType.InstanceCreatedByInjector, + context: getInjectorProfilerContext(), + instance: {value: instance} + }); +} + +/** + * @param token DI token associated with injected service + * @param value the instance of the injected service (i.e the result of `inject(token)`) + * @param flags the flags that the token was injected with + */ +export function emitInjectEvent(token: Type, value: unknown, flags: InjectFlags): void { + !ngDevMode && throwError('Injector profiler should never be called in production mode'); + + injectorProfiler({ + type: InjectorProfilerEventType.Inject, + context: getInjectorProfilerContext(), + service: {token, value, flags} + }); +} + +export function runInInjectorProfilerContext( + injector: Injector, token: Type, callback: () => void): void { + !ngDevMode && + throwError('runInInjectorProfilerContext should never be called in production mode'); + + const prevInjectContext = setInjectorProfilerContext({injector, token}); + try { + callback(); + } finally { + setInjectorProfilerContext(prevInjectContext); + } +} diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 96c905c638c..fb2349f3e82 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -18,11 +18,12 @@ import {assertDefined, assertEqual, assertIndexInRange} from '../util/assert'; import {noSideEffects} from '../util/closure'; import {assertDirectiveDef, assertNodeInjector, assertTNodeForLView} from './assert'; +import {emitInstanceCreatedByInjectorEvent, InjectorProfilerContext, runInInjectorProfilerContext, setInjectorProfilerContext} from './debug/injector_profiler'; import {getFactoryDef} from './definition_factory'; import {throwCyclicDependencyError, throwProviderNotFoundError} from './errors_di'; import {NG_ELEMENT_ID, NG_FACTORY_DEF} from './fields'; import {registerPreOrderHooks} from './hooks'; -import {DirectiveDef} from './interfaces/definition'; +import {ComponentDef, DirectiveDef} from './interfaces/definition'; import {isFactory, NO_PARENT_INJECTOR, NodeInjectorFactory, NodeInjectorOffset, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector'; import {AttributeMarker, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TNode, TNodeProviderIndexes, TNodeType} from './interfaces/node'; import {isComponentDef, isComponentHost} from './interfaces/type_checks'; @@ -446,7 +447,22 @@ function lookupTokenUsingNodeInjector( lookupTokenUsingModuleInjector(lView, token, flags, notFoundValue); } try { - const value = bloomHash(flags); + let value: unknown; + + if (ngDevMode) { + runInInjectorProfilerContext( + new NodeInjector(getCurrentTNode() as TElementNode, getLView()), token as Type, + () => { + value = bloomHash(flags); + + if (value != null) { + emitInstanceCreatedByInjectorEvent(value); + } + }); + } else { + value = bloomHash(flags); + } + if (value == null && !(flags & InjectFlags.Optional)) { throwProviderNotFoundError(token); } else { @@ -618,6 +634,19 @@ export function getNodeInjectable( } const previousIncludeViewProviders = setIncludeViewProviders(factory.canSeeViewProviders); factory.resolving = true; + + let prevInjectContext: InjectorProfilerContext|undefined; + if (ngDevMode) { + // tData indexes mirror the concrete instances in its corresponding LView. + // lView[index] here is either the injectable instace itself or a factory, + // therefore tData[index] is the constructor of that injectable or a + // definition object that contains the constructor in a `.type` field. + const token = + (tData[index] as (DirectiveDef| ComponentDef)).type || tData[index]; + const injector = new NodeInjector(tNode, lView); + prevInjectContext = setInjectorProfilerContext({injector, token}); + } + const previousInjectImplementation = factory.injectImpl ? setInjectImplementation(factory.injectImpl) : null; const success = enterDI(lView, tNode, InjectFlags.Default); @@ -627,6 +656,9 @@ export function getNodeInjectable( 'Because flags do not contain \`SkipSelf\' we expect this to always succeed.'); try { value = lView[index] = factory.factory(undefined, tData, lView, tNode); + + ngDevMode && emitInstanceCreatedByInjectorEvent(value); + // This code path is hit for both directives and providers. // For perf reasons, we want to avoid searching for hooks on providers. // It does no harm to try (the hooks just won't exist), but the extra @@ -638,6 +670,8 @@ export function getNodeInjectable( registerPreOrderHooks(index, tData[index] as DirectiveDef, tView); } } finally { + ngDevMode && setInjectorProfilerContext(prevInjectContext!); + previousInjectImplementation !== null && setInjectImplementation(previousInjectImplementation); setIncludeViewProviders(previousIncludeViewProviders); @@ -703,6 +737,16 @@ function shouldSearchParent(flags: InjectFlags, isFirstHostTNode: boolean): bool return !(flags & InjectFlags.Self) && !(flags & InjectFlags.Host && isFirstHostTNode); } +export function getNodeInjectorLView(nodeInjector: NodeInjector): LView { + return (nodeInjector as any)._lView as LView; +} + +export function getNodeInjectorTNode(nodeInjector: NodeInjector): TElementNode|TContainerNode| + TElementContainerNode|null { + return (nodeInjector as any)._tNode as TElementNode | TContainerNode | TElementContainerNode | + null; +} + export class NodeInjector implements Injector { constructor( private _tNode: TElementNode|TContainerNode|TElementContainerNode|null, diff --git a/packages/core/src/render3/di_setup.ts b/packages/core/src/render3/di_setup.ts index 3d9b30a078b..b56b92bd4ea 100644 --- a/packages/core/src/render3/di_setup.ts +++ b/packages/core/src/render3/di_setup.ts @@ -9,11 +9,12 @@ import {resolveForwardRef} from '../di/forward_ref'; import {ClassProvider, Provider} from '../di/interface/provider'; -import {isClassProvider, isTypeProvider} from '../di/provider_collection'; +import {isClassProvider, isTypeProvider, SingleProvider} from '../di/provider_collection'; import {providerToFactory} from '../di/r3_injector'; import {assertDefined} from '../util/assert'; -import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from './di'; +import {emitProviderConfiguredEvent, runInInjectorProfilerContext} from './debug/injector_profiler'; +import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode, NodeInjector} from './di'; import {ɵɵdirectiveInject} from './instructions/all'; import {DirectiveDef} from './interfaces/definition'; import {NodeInjectorFactory} from './interfaces/injector'; @@ -74,10 +75,18 @@ function resolveProvider( } else { const tView = getTView(); const lView = getLView(); - let token: any = isTypeProvider(provider) ? provider : resolveForwardRef(provider.provide); - let providerFactory: () => any = providerToFactory(provider); - const tNode = getCurrentTNode()!; + let token: any = isTypeProvider(provider) ? provider : resolveForwardRef(provider.provide); + + const providerFactory = providerToFactory(provider); + if (ngDevMode) { + const injector = + new NodeInjector(tNode as TElementNode | TContainerNode | TElementContainerNode, lView); + runInInjectorProfilerContext(injector, token, () => { + emitProviderConfiguredEvent(provider as SingleProvider, isViewProvider); + }); + } + const beginIndex = tNode.providerIndexes & TNodeProviderIndexes.ProvidersStartIndexMask; const endIndex = tNode.directiveStart; const cptViewProvidersCount = diff --git a/packages/core/src/render3/instructions/di.ts b/packages/core/src/render3/instructions/di.ts index 71578e17e8c..81870eeb1d8 100644 --- a/packages/core/src/render3/instructions/di.ts +++ b/packages/core/src/render3/instructions/di.ts @@ -9,6 +9,8 @@ import {InjectFlags, resolveForwardRef} from '../../di'; import {assertInjectImplementationNotEqual} from '../../di/inject_switch'; import {ɵɵinject} from '../../di/injector_compatibility'; import {ProviderToken} from '../../di/provider_token'; +import {Type} from '../../interface/type'; +import {emitInjectEvent} from '../debug/injector_profiler'; import {getOrCreateInjectable} from '../di'; import {TDirectiveHostNode} from '../interfaces/node'; import {getCurrentTNode, getLView} from '../state'; @@ -49,8 +51,10 @@ export function ɵɵdirectiveInject(token: ProviderToken, flags = InjectFl return ɵɵinject(token, flags); } const tNode = getCurrentTNode(); - return getOrCreateInjectable( - tNode as TDirectiveHostNode, lView, resolveForwardRef(token), flags); + const value = + getOrCreateInjectable(tNode as TDirectiveHostNode, lView, resolveForwardRef(token), flags); + ngDevMode && emitInjectEvent(token as Type, value, flags); + return value; } /** diff --git a/packages/core/src/render3/pipe.ts b/packages/core/src/render3/pipe.ts index 86679a7d771..53fafcbfc44 100644 --- a/packages/core/src/render3/pipe.ts +++ b/packages/core/src/render3/pipe.ts @@ -11,14 +11,16 @@ import {setInjectImplementation} from '../di/inject_switch'; import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors'; import {Type} from '../interface/type'; +import {InjectorProfilerContext, setInjectorProfilerContext} from './debug/injector_profiler'; import {getFactoryDef} from './definition_factory'; -import {setIncludeViewProviders} from './di'; +import {NodeInjector, setIncludeViewProviders} from './di'; import {store, ɵɵdirectiveInject} from './instructions/all'; import {isHostComponentStandalone} from './instructions/element_validation'; import {PipeDef, PipeDefList} from './interfaces/definition'; +import {TTextNode} from './interfaces/node'; import {CONTEXT, DECLARATION_COMPONENT_VIEW, HEADER_OFFSET, LView, TVIEW} from './interfaces/view'; import {pureFunction1Internal, pureFunction2Internal, pureFunction3Internal, pureFunction4Internal, pureFunctionVInternal} from './pure_function'; -import {getBindingRoot, getLView, getTView} from './state'; +import {getBindingRoot, getCurrentTNode, getLView, getTView} from './state'; import {load} from './util/view_utils'; @@ -50,6 +52,14 @@ export function ɵɵpipe(index: number, pipeName: string): any { } const pipeFactory = pipeDef.factory || (pipeDef.factory = getFactoryDef(pipeDef.type, true)); + + let previousInjectorProfilerContext: InjectorProfilerContext; + if (ngDevMode) { + previousInjectorProfilerContext = setInjectorProfilerContext({ + injector: new NodeInjector(getCurrentTNode() as TTextNode, getLView()), + token: pipeDef.type + }); + } const previousInjectImplementation = setInjectImplementation(ɵɵdirectiveInject); try { // DI for pipes is supposed to behave like directives when placed on a component @@ -63,6 +73,7 @@ export function ɵɵpipe(index: number, pipeName: string): any { // we have to restore the injector implementation in finally, just in case the creation of the // pipe throws an error. setInjectImplementation(previousInjectImplementation); + ngDevMode && setInjectorProfilerContext(previousInjectorProfilerContext!); } }