diff --git a/packages/core/src/render3/profiler.ts b/packages/core/src/render3/profiler.ts index 1d7d1a0e60e..6eccaad76b6 100644 --- a/packages/core/src/render3/profiler.ts +++ b/packages/core/src/render3/profiler.ts @@ -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); } }; diff --git a/packages/core/test/acceptance/profiler_spec.ts b/packages/core/test/acceptance/profiler_spec.ts index 96eae05aca7..cb526dcc397 100644 --- a/packages/core/test/acceptance/profiler_spec.ts +++ b/packages/core/test/acceptance/profiler_spec.ts @@ -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']); + }); +}); diff --git a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json index 4ce3b5e3258..dc3dfccc32f 100644 --- a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json @@ -429,6 +429,8 @@ "producerMarkClean", "producerRemoveLiveConsumerAtIndex", "producerUpdateValueVersion", + "profiler", + "profilerCallbacks", "refreshContentQueries", "refreshView", "rememberChangeHistoryAndInvokeOnChangesHook", diff --git a/packages/core/test/bundling/animations/bundle.golden_symbols.json b/packages/core/test/bundling/animations/bundle.golden_symbols.json index 94bdfef9e3c..58e3c3e607a 100644 --- a/packages/core/test/bundling/animations/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations/bundle.golden_symbols.json @@ -455,6 +455,8 @@ "producerMarkClean", "producerRemoveLiveConsumerAtIndex", "producerUpdateValueVersion", + "profiler", + "profilerCallbacks", "refreshContentQueries", "refreshView", "rememberChangeHistoryAndInvokeOnChangesHook", diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index abb7e6dbd28..49612fb5f9c 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -369,6 +369,8 @@ "producerMarkClean", "producerRemoveLiveConsumerAtIndex", "producerUpdateValueVersion", + "profiler", + "profilerCallbacks", "refreshContentQueries", "refreshView", "rememberChangeHistoryAndInvokeOnChangesHook", diff --git a/packages/core/test/bundling/defer/bundle.golden_symbols.json b/packages/core/test/bundling/defer/bundle.golden_symbols.json index 6a2ea8cee53..70e29c735f8 100644 --- a/packages/core/test/bundling/defer/bundle.golden_symbols.json +++ b/packages/core/test/bundling/defer/bundle.golden_symbols.json @@ -433,6 +433,7 @@ "producerRemoveLiveConsumerAtIndex", "producerUpdateValueVersion", "profiler", + "profilerCallbacks", "refreshContentQueries", "refreshView", "registerPostOrderHooks", diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index 955ae05958d..fa6f34bc0cc 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -552,6 +552,8 @@ "producerRemoveLiveConsumerAtIndex", "producerUpdateValueVersion", "producerUpdatesAllowed", + "profiler", + "profilerCallbacks", "providerToFactory", "readableStreamLikeToAsyncGenerator", "refreshContentQueries", diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index bb018a0ffe3..ad7e1ba5b93 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -541,6 +541,8 @@ "producerRemoveLiveConsumerAtIndex", "producerUpdateValueVersion", "producerUpdatesAllowed", + "profiler", + "profilerCallbacks", "providerToFactory", "readableStreamLikeToAsyncGenerator", "refreshContentQueries", diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 14c74ae79a9..593ffecee29 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -297,6 +297,8 @@ "producerMarkClean", "producerRemoveLiveConsumerAtIndex", "producerUpdateValueVersion", + "profiler", + "profilerCallbacks", "refreshContentQueries", "refreshView", "rememberChangeHistoryAndInvokeOnChangesHook", diff --git a/packages/core/test/bundling/hydration/bundle.golden_symbols.json b/packages/core/test/bundling/hydration/bundle.golden_symbols.json index 5e1bc628c5e..e4a27e14287 100644 --- a/packages/core/test/bundling/hydration/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hydration/bundle.golden_symbols.json @@ -396,6 +396,8 @@ "producerMarkClean", "producerRemoveLiveConsumerAtIndex", "producerUpdateValueVersion", + "profiler", + "profilerCallbacks", "readableStreamLikeToAsyncGenerator", "refreshContentQueries", "refreshView", diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index 78f110f17ae..5c27c8547f1 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -633,6 +633,8 @@ "producerNotifyConsumers", "producerRemoveLiveConsumerAtIndex", "producerUpdateValueVersion", + "profiler", + "profilerCallbacks", "readableStreamLikeToAsyncGenerator", "redirectIfUrlTree", "redirectingNavigationError", diff --git a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json index b6fb1cd5638..7ea0d0e5f76 100644 --- a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json +++ b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json @@ -329,6 +329,8 @@ "producerMarkClean", "producerRemoveLiveConsumerAtIndex", "producerUpdateValueVersion", + "profiler", + "profilerCallbacks", "refreshContentQueries", "refreshView", "rememberChangeHistoryAndInvokeOnChangesHook", diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 8887ccc61c8..1e3a4a7ba58 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -442,6 +442,8 @@ "producerMarkClean", "producerRemoveLiveConsumerAtIndex", "producerUpdateValueVersion", + "profiler", + "profilerCallbacks", "refreshContentQueries", "refreshView", "registerPostOrderHooks",