feat(core): introduce new DI profiling event (#60158)

This change introduces a new DI profiler event:
InjectorToCreateInstanceEvent. This new event allows
us to measure DI tokens instantiation time.

PR Close #60158
This commit is contained in:
Pawel Kozlowski
2025-02-28 11:28:25 +01:00
committed by Miles Malerba
parent d92fba084b
commit 809b5b4596
5 changed files with 65 additions and 16 deletions
+3
View File
@@ -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<T>, () => {
emitInjectorToCreateInstanceEvent(token);
record.value = record.factory!();
emitInstanceCreatedByInjectorEvent(record.value);
});
@@ -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.');
}
@@ -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<unknown>;
}
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<unknown>): 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()
+12 -10
View File
@@ -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<T>(
new NodeInjector(getCurrentTNode() as TElementNode, getLView()),
token as Type<T>,
() => {
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<unknown> | ComponentDef<unknown>).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<unknown> | ComponentDef<unknown>).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);
@@ -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<InjectorToCreateInstanceEvent>(
aboutToCreateEvents,
(event) => event.token === MyComponent,
);
expect(componentAboutToCreateEvent).toBeDefined();
expect(componentCreateEvent).toBeTruthy();
});