refactor(core): allow multiple DI profilers (#60562)

This commit changes the DI profiler infrastructure to
allow multiple profilers running at the same time.

PR Close #60562
This commit is contained in:
Pawel Kozlowski
2025-03-26 13:33:39 +01:00
committed by Jessica Janiuk
parent bae7b75ff5
commit b21c6e5ac5
2 changed files with 102 additions and 13 deletions
@@ -194,33 +194,55 @@ export function setInjectorProfilerContext(context: InjectorProfilerContext) {
return previous;
}
let injectorProfilerCallback: InjectorProfiler | null = null;
const injectorProfilerCallbacks: InjectorProfiler[] = [];
const NOOP_PROFILER_REMOVAL = () => {};
function removeProfiler(profiler: InjectorProfiler) {
const profilerIdx = injectorProfilerCallbacks.indexOf(profiler);
if (profilerIdx !== -1) {
injectorProfilerCallbacks.splice(profilerIdx, 1);
}
}
/**
* Sets the callback function which will be invoked during certain DI events within the
* runtime (for example: injecting services, creating injectable instances, configuring providers)
* Adds a callback function which will be invoked during certain DI events within the
* runtime (for example: injecting services, creating injectable instances, configuring providers).
* Multiple profiler callbacks can be set: in this case profiling events are
* reported to every registered callback.
*
* 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.
* @returns a cleanup function that, when invoked, removes a given profiler callback.
*/
export const setInjectorProfiler = (injectorProfiler: InjectorProfiler | null) => {
export function setInjectorProfiler(injectorProfiler: InjectorProfiler | null): () => void {
!ngDevMode && throwError('setInjectorProfiler should never be called in production mode');
injectorProfilerCallback = injectorProfiler;
};
if (injectorProfiler !== null) {
if (!injectorProfilerCallbacks.includes(injectorProfiler)) {
injectorProfilerCallbacks.push(injectorProfiler);
}
return () => removeProfiler(injectorProfiler);
} else {
injectorProfilerCallbacks.length = 0;
return NOOP_PROFILER_REMOVAL;
}
}
/**
* 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 {
export 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);
for (let i = 0; i < injectorProfilerCallbacks.length; i++) {
const injectorProfilerCallback = injectorProfilerCallbacks[i];
injectorProfilerCallback(event);
}
}
@@ -11,6 +11,7 @@ import {
afterRender,
ClassProvider,
Component,
createEnvironmentInjector,
Directive,
ElementRef,
inject,
@@ -43,6 +44,8 @@ import {
InjectorProfilerEventType,
ProviderConfiguredEvent,
setInjectorProfiler,
injectorProfiler,
InjectorProfilerContext,
} from '../../src/render3/debug/injector_profiler';
import {getNodeInjectorLView, NodeInjector} from '../../src/render3/di';
import {
@@ -103,7 +106,7 @@ describe('setProfiler', () => {
});
});
afterAll(() => setInjectorProfiler(null));
afterEach(() => setInjectorProfiler(null));
it('should emit DI events when a component contains a provider and injects it', () => {
class MyService {}
@@ -382,6 +385,70 @@ describe('setProfiler', () => {
});
});
describe('profiler activation and removal', () => {
class SomeClass {}
const fakeContext: InjectorProfilerContext = {
injector: Injector.create({providers: []}),
token: SomeClass,
};
const fakeEvent: InjectorCreatedInstanceEvent = {
type: InjectorProfilerEventType.InstanceCreatedByInjector,
context: fakeContext,
instance: {value: new SomeClass()},
};
it('should allow adding and removing multiple profilers', () => {
const events: string[] = [];
const r1 = setInjectorProfiler((e) => events.push('P1: ' + e.type));
const r2 = setInjectorProfiler((e) => events.push('P2: ' + e.type));
injectorProfiler(fakeEvent);
expect(events).toEqual(['P1: 1', 'P2: 1']);
r1();
injectorProfiler(fakeEvent);
expect(events).toEqual(['P1: 1', 'P2: 1', 'P2: 1']);
r2();
injectorProfiler(fakeEvent);
expect(events).toEqual(['P1: 1', 'P2: 1', 'P2: 1']);
});
it('should not add / remove the same profiler twice', () => {
const events: string[] = [];
const p1 = (e: InjectorProfilerEvent) => events.push('P1: ' + e.type);
const r1 = setInjectorProfiler(p1);
const r2 = setInjectorProfiler(p1);
injectorProfiler(fakeEvent);
expect(events).toEqual(['P1: 1']);
r1();
injectorProfiler(fakeEvent);
expect(events).toEqual(['P1: 1']);
// subsequent removals should be noop
r1();
r2();
});
it('should clear all profilers when passing null', () => {
const events: string[] = [];
setInjectorProfiler((e) => events.push('P1: ' + e.type));
setInjectorProfiler((e) => events.push('P2: ' + e.type));
injectorProfiler(fakeEvent);
expect(events).toEqual(['P1: 1', 'P2: 1']);
// clear all profilers
setInjectorProfiler(null);
injectorProfiler(fakeEvent);
expect(events).toEqual(['P1: 1', 'P2: 1']);
});
});
describe('getInjectorMetadata', () => {
it('should be able to determine injector type and name', fakeAsync(() => {
class MyServiceA {}
@@ -487,7 +554,7 @@ describe('getInjectorMetadata', () => {
describe('getInjectorProviders', () => {
beforeEach(() => setupFrameworkInjectorProfiler());
afterAll(() => setInjectorProfiler(null));
afterEach(() => setInjectorProfiler(null));
it('should be able to get the providers from a components injector', () => {
class MyService {}
@@ -953,7 +1020,7 @@ describe('getInjectorProviders', () => {
describe('getDependenciesFromInjectable', () => {
beforeEach(() => setupFrameworkInjectorProfiler());
afterAll(() => setInjectorProfiler(null));
afterEach(() => setInjectorProfiler(null));
it('should be able to determine which injector dependencies come from', fakeAsync(() => {
class MyService {}
@@ -1244,7 +1311,7 @@ describe('getDependenciesFromInjectable', () => {
describe('getInjectorResolutionPath', () => {
beforeEach(() => setupFrameworkInjectorProfiler());
afterAll(() => setInjectorProfiler(null));
afterEach(() => setInjectorProfiler(null));
it('should be able to inspect injector hierarchy structure', fakeAsync(() => {
class MyServiceA {}