From b21c6e5ac58cf41f8702b35864d9e35be54d569d Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Wed, 26 Mar 2025 13:33:39 +0100 Subject: [PATCH] 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 --- .../src/render3/debug/injector_profiler.ts | 40 +++++++--- .../test/acceptance/injector_profiler_spec.ts | 75 ++++++++++++++++++- 2 files changed, 102 insertions(+), 13 deletions(-) diff --git a/packages/core/src/render3/debug/injector_profiler.ts b/packages/core/src/render3/debug/injector_profiler.ts index 509c2763014..955c437ed81 100644 --- a/packages/core/src/render3/debug/injector_profiler.ts +++ b/packages/core/src/render3/debug/injector_profiler.ts @@ -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); } } diff --git a/packages/core/test/acceptance/injector_profiler_spec.ts b/packages/core/test/acceptance/injector_profiler_spec.ts index f8f4046582a..f3770accf76 100644 --- a/packages/core/test/acceptance/injector_profiler_spec.ts +++ b/packages/core/test/acceptance/injector_profiler_spec.ts @@ -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 {}