refactor(core): allow multiple profilers (#60548)

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

PR Close #60548
This commit is contained in:
Pawel Kozlowski
2025-03-25 11:12:31 +01:00
committed by Alex Rickabaugh
parent fe9b79b615
commit 64f6fc0095
13 changed files with 102 additions and 11 deletions
+29 -10
View File
@@ -8,21 +8,40 @@
import {type Profiler} from './profiler_types';
let profilerCallback: Profiler | null = null;
const profilerCallbacks: Profiler[] = [];
const NOOP_PROFILER_REMOVAL = () => {};
function removeProfiler(profiler: Profiler) {
const profilerIdx = profilerCallbacks.indexOf(profiler);
if (profilerIdx !== -1) {
profilerCallbacks.splice(profilerIdx, 1);
}
}
/**
* Sets the callback function which will be invoked before and after performing certain actions at
* runtime (for example, before and after running change detection).
* Adds a callback function which will be invoked before and after performing certain actions at
* runtime (for example, before and after running change detection). 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.
* @param profiler function provided by the caller or null value to disable all profilers.
* @returns a cleanup function that, when invoked, removes a given profiler callback.
*/
export const setProfiler = (profiler: Profiler | null) => {
profilerCallback = profiler;
};
export function setProfiler(profiler: Profiler | null): () => void {
if (profiler !== null) {
if (!profilerCallbacks.includes(profiler)) {
profilerCallbacks.push(profiler);
}
return () => removeProfiler(profiler);
} else {
profilerCallbacks.length = 0;
return NOOP_PROFILER_REMOVAL;
}
}
/**
* Profiler function which wraps user code executed by the runtime.
@@ -32,10 +51,10 @@ export const setProfiler = (profiler: Profiler | null) => {
* @param eventFn function associated with event.
* For example a template function, lifecycle hook, or output listener.
* The value depends on the execution context
* @returns
*/
export const profiler: Profiler = function (event, instance = null, eventFn) {
if (profilerCallback != null /* both `null` and `undefined` */) {
export const profiler: Profiler = function (event, instance = null, eventFn): void {
for (let i = 0; i < profilerCallbacks.length; i++) {
const profilerCallback = profilerCallbacks[i];
profilerCallback(event, instance, eventFn);
}
};
+52 -1
View File
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {setProfiler} from '../../src/render3/profiler';
import {setProfiler, profiler} from '../../src/render3/profiler';
import {ProfilerEvent} from '../../src/render3/profiler_types';
import {TestBed} from '../../testing';
@@ -540,3 +540,54 @@ describe('profiler', () => {
});
});
});
describe('profiler activation and removal', () => {
it('should allow adding and removing multiple profilers', () => {
const events: string[] = [];
const r1 = setProfiler((e) => events.push('P1: ' + e));
const r2 = setProfiler((e) => events.push('P2: ' + e));
profiler(ProfilerEvent.TemplateCreateStart);
expect(events).toEqual(['P1: 0', 'P2: 0']);
r1();
profiler(ProfilerEvent.TemplateCreateEnd);
expect(events).toEqual(['P1: 0', 'P2: 0', 'P2: 1']);
r2();
profiler(ProfilerEvent.TemplateCreateStart);
expect(events).toEqual(['P1: 0', 'P2: 0', 'P2: 1']);
});
it('should not add / remove the same profiler twice', () => {
const events: string[] = [];
const p1 = (e: ProfilerEvent) => events.push('P1: ' + e);
const r1 = setProfiler(p1);
const r2 = setProfiler(p1);
profiler(ProfilerEvent.TemplateCreateStart);
expect(events).toEqual(['P1: 0']);
r1();
profiler(ProfilerEvent.TemplateCreateStart);
expect(events).toEqual(['P1: 0']);
// subsequent removals should be noop
r1();
r2();
});
it('should clear all profilers when passing null', () => {
const events: string[] = [];
setProfiler((e) => events.push('P1: ' + e));
setProfiler((e) => events.push('P2: ' + e));
profiler(ProfilerEvent.TemplateCreateStart);
expect(events).toEqual(['P1: 0', 'P2: 0']);
// clear all profilers
setProfiler(null);
profiler(ProfilerEvent.TemplateCreateEnd);
expect(events).toEqual(['P1: 0', 'P2: 0']);
});
});
@@ -429,6 +429,8 @@
"producerMarkClean",
"producerRemoveLiveConsumerAtIndex",
"producerUpdateValueVersion",
"profiler",
"profilerCallbacks",
"refreshContentQueries",
"refreshView",
"rememberChangeHistoryAndInvokeOnChangesHook",
@@ -455,6 +455,8 @@
"producerMarkClean",
"producerRemoveLiveConsumerAtIndex",
"producerUpdateValueVersion",
"profiler",
"profilerCallbacks",
"refreshContentQueries",
"refreshView",
"rememberChangeHistoryAndInvokeOnChangesHook",
@@ -369,6 +369,8 @@
"producerMarkClean",
"producerRemoveLiveConsumerAtIndex",
"producerUpdateValueVersion",
"profiler",
"profilerCallbacks",
"refreshContentQueries",
"refreshView",
"rememberChangeHistoryAndInvokeOnChangesHook",
@@ -433,6 +433,7 @@
"producerRemoveLiveConsumerAtIndex",
"producerUpdateValueVersion",
"profiler",
"profilerCallbacks",
"refreshContentQueries",
"refreshView",
"registerPostOrderHooks",
@@ -552,6 +552,8 @@
"producerRemoveLiveConsumerAtIndex",
"producerUpdateValueVersion",
"producerUpdatesAllowed",
"profiler",
"profilerCallbacks",
"providerToFactory",
"readableStreamLikeToAsyncGenerator",
"refreshContentQueries",
@@ -541,6 +541,8 @@
"producerRemoveLiveConsumerAtIndex",
"producerUpdateValueVersion",
"producerUpdatesAllowed",
"profiler",
"profilerCallbacks",
"providerToFactory",
"readableStreamLikeToAsyncGenerator",
"refreshContentQueries",
@@ -297,6 +297,8 @@
"producerMarkClean",
"producerRemoveLiveConsumerAtIndex",
"producerUpdateValueVersion",
"profiler",
"profilerCallbacks",
"refreshContentQueries",
"refreshView",
"rememberChangeHistoryAndInvokeOnChangesHook",
@@ -396,6 +396,8 @@
"producerMarkClean",
"producerRemoveLiveConsumerAtIndex",
"producerUpdateValueVersion",
"profiler",
"profilerCallbacks",
"readableStreamLikeToAsyncGenerator",
"refreshContentQueries",
"refreshView",
@@ -633,6 +633,8 @@
"producerNotifyConsumers",
"producerRemoveLiveConsumerAtIndex",
"producerUpdateValueVersion",
"profiler",
"profilerCallbacks",
"readableStreamLikeToAsyncGenerator",
"redirectIfUrlTree",
"redirectingNavigationError",
@@ -329,6 +329,8 @@
"producerMarkClean",
"producerRemoveLiveConsumerAtIndex",
"producerUpdateValueVersion",
"profiler",
"profilerCallbacks",
"refreshContentQueries",
"refreshView",
"rememberChangeHistoryAndInvokeOnChangesHook",
@@ -442,6 +442,8 @@
"producerMarkClean",
"producerRemoveLiveConsumerAtIndex",
"producerUpdateValueVersion",
"profiler",
"profilerCallbacks",
"refreshContentQueries",
"refreshView",
"registerPostOrderHooks",