Skip to content

Commit b21c6e5

Browse files
refactor(core): allow multiple DI profilers (angular#60562)
This commit changes the DI profiler infrastructure to allow multiple profilers running at the same time. PR Close angular#60562
1 parent bae7b75 commit b21c6e5

File tree

2 files changed

+102
-13
lines changed

2 files changed

+102
-13
lines changed

packages/core/src/render3/debug/injector_profiler.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,33 +194,55 @@ export function setInjectorProfilerContext(context: InjectorProfilerContext) {
194194
return previous;
195195
}
196196

197-
let injectorProfilerCallback: InjectorProfiler | null = null;
197+
const injectorProfilerCallbacks: InjectorProfiler[] = [];
198+
199+
const NOOP_PROFILER_REMOVAL = () => {};
200+
201+
function removeProfiler(profiler: InjectorProfiler) {
202+
const profilerIdx = injectorProfilerCallbacks.indexOf(profiler);
203+
if (profilerIdx !== -1) {
204+
injectorProfilerCallbacks.splice(profilerIdx, 1);
205+
}
206+
}
198207

199208
/**
200-
* Sets the callback function which will be invoked during certain DI events within the
201-
* runtime (for example: injecting services, creating injectable instances, configuring providers)
209+
* Adds a callback function which will be invoked during certain DI events within the
210+
* runtime (for example: injecting services, creating injectable instances, configuring providers).
211+
* Multiple profiler callbacks can be set: in this case profiling events are
212+
* reported to every registered callback.
202213
*
203214
* Warning: this function is *INTERNAL* and should not be relied upon in application's code.
204215
* The contract of the function might be changed in any release and/or the function can be removed
205216
* completely.
206217
*
207218
* @param profiler function provided by the caller or null value to disable profiling.
219+
* @returns a cleanup function that, when invoked, removes a given profiler callback.
208220
*/
209-
export const setInjectorProfiler = (injectorProfiler: InjectorProfiler | null) => {
221+
export function setInjectorProfiler(injectorProfiler: InjectorProfiler | null): () => void {
210222
!ngDevMode && throwError('setInjectorProfiler should never be called in production mode');
211-
injectorProfilerCallback = injectorProfiler;
212-
};
223+
224+
if (injectorProfiler !== null) {
225+
if (!injectorProfilerCallbacks.includes(injectorProfiler)) {
226+
injectorProfilerCallbacks.push(injectorProfiler);
227+
}
228+
return () => removeProfiler(injectorProfiler);
229+
} else {
230+
injectorProfilerCallbacks.length = 0;
231+
return NOOP_PROFILER_REMOVAL;
232+
}
233+
}
213234

214235
/**
215236
* Injector profiler function which emits on DI events executed by the runtime.
216237
*
217238
* @param event InjectorProfilerEvent corresponding to the DI event being emitted
218239
*/
219-
function injectorProfiler(event: InjectorProfilerEvent): void {
240+
export function injectorProfiler(event: InjectorProfilerEvent): void {
220241
!ngDevMode && throwError('Injector profiler should never be called in production mode');
221242

222-
if (injectorProfilerCallback != null /* both `null` and `undefined` */) {
223-
injectorProfilerCallback!(event);
243+
for (let i = 0; i < injectorProfilerCallbacks.length; i++) {
244+
const injectorProfilerCallback = injectorProfilerCallbacks[i];
245+
injectorProfilerCallback(event);
224246
}
225247
}
226248

packages/core/test/acceptance/injector_profiler_spec.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
afterRender,
1212
ClassProvider,
1313
Component,
14+
createEnvironmentInjector,
1415
Directive,
1516
ElementRef,
1617
inject,
@@ -43,6 +44,8 @@ import {
4344
InjectorProfilerEventType,
4445
ProviderConfiguredEvent,
4546
setInjectorProfiler,
47+
injectorProfiler,
48+
InjectorProfilerContext,
4649
} from '../../src/render3/debug/injector_profiler';
4750
import {getNodeInjectorLView, NodeInjector} from '../../src/render3/di';
4851
import {
@@ -103,7 +106,7 @@ describe('setProfiler', () => {
103106
});
104107
});
105108

106-
afterAll(() => setInjectorProfiler(null));
109+
afterEach(() => setInjectorProfiler(null));
107110

108111
it('should emit DI events when a component contains a provider and injects it', () => {
109112
class MyService {}
@@ -382,6 +385,70 @@ describe('setProfiler', () => {
382385
});
383386
});
384387

388+
describe('profiler activation and removal', () => {
389+
class SomeClass {}
390+
391+
const fakeContext: InjectorProfilerContext = {
392+
injector: Injector.create({providers: []}),
393+
token: SomeClass,
394+
};
395+
396+
const fakeEvent: InjectorCreatedInstanceEvent = {
397+
type: InjectorProfilerEventType.InstanceCreatedByInjector,
398+
context: fakeContext,
399+
instance: {value: new SomeClass()},
400+
};
401+
402+
it('should allow adding and removing multiple profilers', () => {
403+
const events: string[] = [];
404+
const r1 = setInjectorProfiler((e) => events.push('P1: ' + e.type));
405+
const r2 = setInjectorProfiler((e) => events.push('P2: ' + e.type));
406+
407+
injectorProfiler(fakeEvent);
408+
expect(events).toEqual(['P1: 1', 'P2: 1']);
409+
410+
r1();
411+
injectorProfiler(fakeEvent);
412+
expect(events).toEqual(['P1: 1', 'P2: 1', 'P2: 1']);
413+
414+
r2();
415+
injectorProfiler(fakeEvent);
416+
expect(events).toEqual(['P1: 1', 'P2: 1', 'P2: 1']);
417+
});
418+
419+
it('should not add / remove the same profiler twice', () => {
420+
const events: string[] = [];
421+
const p1 = (e: InjectorProfilerEvent) => events.push('P1: ' + e.type);
422+
const r1 = setInjectorProfiler(p1);
423+
const r2 = setInjectorProfiler(p1);
424+
425+
injectorProfiler(fakeEvent);
426+
expect(events).toEqual(['P1: 1']);
427+
428+
r1();
429+
injectorProfiler(fakeEvent);
430+
expect(events).toEqual(['P1: 1']);
431+
432+
// subsequent removals should be noop
433+
r1();
434+
r2();
435+
});
436+
437+
it('should clear all profilers when passing null', () => {
438+
const events: string[] = [];
439+
setInjectorProfiler((e) => events.push('P1: ' + e.type));
440+
setInjectorProfiler((e) => events.push('P2: ' + e.type));
441+
442+
injectorProfiler(fakeEvent);
443+
expect(events).toEqual(['P1: 1', 'P2: 1']);
444+
445+
// clear all profilers
446+
setInjectorProfiler(null);
447+
injectorProfiler(fakeEvent);
448+
expect(events).toEqual(['P1: 1', 'P2: 1']);
449+
});
450+
});
451+
385452
describe('getInjectorMetadata', () => {
386453
it('should be able to determine injector type and name', fakeAsync(() => {
387454
class MyServiceA {}
@@ -487,7 +554,7 @@ describe('getInjectorMetadata', () => {
487554

488555
describe('getInjectorProviders', () => {
489556
beforeEach(() => setupFrameworkInjectorProfiler());
490-
afterAll(() => setInjectorProfiler(null));
557+
afterEach(() => setInjectorProfiler(null));
491558

492559
it('should be able to get the providers from a components injector', () => {
493560
class MyService {}
@@ -953,7 +1020,7 @@ describe('getInjectorProviders', () => {
9531020

9541021
describe('getDependenciesFromInjectable', () => {
9551022
beforeEach(() => setupFrameworkInjectorProfiler());
956-
afterAll(() => setInjectorProfiler(null));
1023+
afterEach(() => setInjectorProfiler(null));
9571024

9581025
it('should be able to determine which injector dependencies come from', fakeAsync(() => {
9591026
class MyService {}
@@ -1244,7 +1311,7 @@ describe('getDependenciesFromInjectable', () => {
12441311

12451312
describe('getInjectorResolutionPath', () => {
12461313
beforeEach(() => setupFrameworkInjectorProfiler());
1247-
afterAll(() => setInjectorProfiler(null));
1314+
afterEach(() => setInjectorProfiler(null));
12481315

12491316
it('should be able to inspect injector hierarchy structure', fakeAsync(() => {
12501317
class MyServiceA {}

0 commit comments

Comments
 (0)