diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index ba7f9fc66f6..78a82a2d7c0 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -12,6 +12,7 @@ import {RuntimeError, RuntimeErrorCode} from '../errors'; import {OnDestroy} from '../interface/lifecycle_hooks'; import {Type} from '../interface/type'; import { + emitInjectorToCreateInstanceEvent, emitInstanceCreatedByInjectorEvent, emitProviderConfiguredEvent, InjectorProfilerContext, @@ -434,6 +435,7 @@ export class R3Injector extends EnvironmentInjector { // 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)) { + emitInjectorToCreateInstanceEvent(token); emitInstanceCreatedByInjectorEvent(provider.useValue); } @@ -478,6 +480,7 @@ export class R3Injector extends EnvironmentInjector { if (ngDevMode) { runInInjectorProfilerContext(this, token as Type, () => { + emitInjectorToCreateInstanceEvent(token); record.value = record.factory!(); emitInstanceCreatedByInjectorEvent(record.value); }); diff --git a/packages/core/src/render3/debug/framework_injector_profiler.ts b/packages/core/src/render3/debug/framework_injector_profiler.ts index 1db31e60a17..728ec17b24c 100644 --- a/packages/core/src/render3/debug/framework_injector_profiler.ts +++ b/packages/core/src/render3/debug/framework_injector_profiler.ts @@ -10,7 +10,7 @@ import {Injector} from '../../di/injector'; import {EnvironmentInjector} from '../../di/r3_injector'; import {Type} from '../../interface/type'; import {assertDefined, throwError} from '../../util/assert'; -import {assertTNode, assertTNodeForLView} from '../assert'; +import {assertTNodeForLView} from '../assert'; import {getComponentDef} from '../def_getters'; import {getNodeInjectorLView, getNodeInjectorTNode, NodeInjector} from '../di'; import {TNode} from '../interfaces/node'; @@ -220,6 +220,12 @@ function handleInstanceCreatedByInjectorEvent( ): void { const {value} = data; + // It might happen that a DI token is requested but there is no corresponding value. + // The InstanceCreatedByInjectorEvent will be still emitted in this case (to mirror the InjectorToCreateInstanceEvent) but we don't want to do any particular processing for those situations. + if (data.value == null) { + return; + } + if (getDIResolver(context.injector) === null) { throwError('An InjectorCreatedInstance event must be run within an injection context.'); } diff --git a/packages/core/src/render3/debug/injector_profiler.ts b/packages/core/src/render3/debug/injector_profiler.ts index de3ad1b1929..11076334ec4 100644 --- a/packages/core/src/render3/debug/injector_profiler.ts +++ b/packages/core/src/render3/debug/injector_profiler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import type {FactoryProvider} from '../../di'; +import type {FactoryProvider, ProviderToken} from '../../di'; import {resolveForwardRef} from '../../di/forward_ref'; import {InjectionToken} from '../../di/injection_token'; import type {Injector} from '../../di/injector'; @@ -41,6 +41,11 @@ export const enum InjectorProfilerEventType { * Emits when an effect is created. */ EffectCreated, + + /** + * Emits when an Angular DI system is about to create an instance corresponding to a given token. + */ + InjectorToCreateInstanceEvent, } /** @@ -68,6 +73,12 @@ export interface InjectedServiceEvent { service: InjectedService; } +export interface InjectorToCreateInstanceEvent { + type: InjectorProfilerEventType.InjectorToCreateInstanceEvent; + context: InjectorProfilerContext; + token: ProviderToken; +} + export interface InjectorCreatedInstanceEvent { type: InjectorProfilerEventType.InstanceCreatedByInjector; context: InjectorProfilerContext; @@ -92,6 +103,7 @@ export interface EffectCreatedEvent { export type InjectorProfilerEvent = | InjectedServiceEvent + | InjectorToCreateInstanceEvent | InjectorCreatedInstanceEvent | ProviderConfiguredEvent | EffectCreatedEvent; @@ -254,6 +266,22 @@ export function emitProviderConfiguredEvent( }); } +/** + * Emits an event to the injector profiler when an instance corresponding to a given token is about to be created be an injector. Note that + * the injector associated with this emission can be accessed by using getDebugInjectContext() + * + * @param instance an object created by an injector + */ +export function emitInjectorToCreateInstanceEvent(token: ProviderToken): void { + !ngDevMode && throwError('Injector profiler should never be called in production mode'); + + injectorProfiler({ + type: InjectorProfilerEventType.InjectorToCreateInstanceEvent, + context: getInjectorProfilerContext(), + token: token, + }); +} + /** * 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() diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 4f1c0f568ba..b29026cadaa 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -19,6 +19,7 @@ import {noSideEffects} from '../util/closure'; import {assertDirectiveDef, assertNodeInjector, assertTNodeForLView} from './assert'; import { + emitInjectorToCreateInstanceEvent, emitInstanceCreatedByInjectorEvent, InjectorProfilerContext, runInInjectorProfilerContext, @@ -521,11 +522,9 @@ function lookupTokenUsingNodeInjector( new NodeInjector(getCurrentTNode() as TElementNode, getLView()), token as Type, () => { + emitInjectorToCreateInstanceEvent(token); value = bloomHash(flags); - - if (value != null) { - emitInstanceCreatedByInjectorEvent(value); - } + emitInstanceCreatedByInjectorEvent(value); }, ); } else { @@ -737,14 +736,15 @@ export function getNodeInjectable( const previousIncludeViewProviders = setIncludeViewProviders(factory.canSeeViewProviders); factory.resolving = true; + // tData indexes mirror the concrete instances in its corresponding LView. + // lView[index] here is either the injectable instance 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]; + 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}); } @@ -760,6 +760,8 @@ export function getNodeInjectable( "Because flags do not contain `SkipSelf' we expect this to always succeed.", ); try { + ngDevMode && emitInjectorToCreateInstanceEvent(token); + value = lView[index] = factory.factory(undefined, tData, lView, tNode); ngDevMode && emitInstanceCreatedByInjectorEvent(value); diff --git a/packages/core/test/acceptance/injector_profiler_spec.ts b/packages/core/test/acceptance/injector_profiler_spec.ts index 40371929bc0..0c7b3738133 100644 --- a/packages/core/test/acceptance/injector_profiler_spec.ts +++ b/packages/core/test/acceptance/injector_profiler_spec.ts @@ -37,6 +37,7 @@ import {setupFrameworkInjectorProfiler} from '@angular/core/src/render3/debug/fr import { getInjectorProfilerContext, InjectedServiceEvent, + InjectorToCreateInstanceEvent, InjectorCreatedInstanceEvent, InjectorProfilerEvent, InjectorProfilerEventType, @@ -57,6 +58,7 @@ import {Router, RouterModule, RouterOutlet} from '@angular/router'; describe('setProfiler', () => { let injectEvents: InjectedServiceEvent[] = []; + let aboutToCreateEvents: InjectorToCreateInstanceEvent[] = []; let createEvents: InjectorCreatedInstanceEvent[] = []; let providerConfiguredEvents: ProviderConfiguredEvent[] = []; @@ -69,6 +71,7 @@ describe('setProfiler', () => { beforeEach(() => { injectEvents = []; + aboutToCreateEvents = []; createEvents = []; providerConfiguredEvents = []; @@ -80,20 +83,22 @@ describe('setProfiler', () => { context: getInjectorProfilerContext(), type, }); - } - if (type === InjectorProfilerEventType.InstanceCreatedByInjector) { + } else if (type === InjectorProfilerEventType.InstanceCreatedByInjector) { createEvents.push({ instance: injectorProfilerEvent.instance, context: getInjectorProfilerContext(), type, }); - } - if (type === InjectorProfilerEventType.ProviderConfigured) { + } else if (type === InjectorProfilerEventType.ProviderConfigured) { providerConfiguredEvents.push({ providerRecord: injectorProfilerEvent.providerRecord, context: getInjectorProfilerContext(), type, }); + } else if (type === InjectorProfilerEventType.InjectorToCreateInstanceEvent) { + aboutToCreateEvents.push(injectorProfilerEvent); + } else { + throw new Error('Unexpected event type: ' + type); } }); }); @@ -139,6 +144,11 @@ describe('setProfiler', () => { createEvents, (event) => event.instance.value === myComp, ); + const componentAboutToCreateEvent = searchForProfilerEvent( + aboutToCreateEvents, + (event) => event.token === MyComponent, + ); + expect(componentAboutToCreateEvent).toBeDefined(); expect(componentCreateEvent).toBeTruthy(); });