From 2f5ab541eafba72bc0079a8650d0b96b0ddfde2f Mon Sep 17 00:00:00 2001 From: Jaime Burgos <73321943+SkyZeroZx@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:37:11 -0500 Subject: [PATCH] feat(core): enhance profiling with documentation URLs Enhances the Chrome DevTools performance profiling integration by adding links to relevant Angular documentation for lifecycle hooks and profiler events. --- .../debug/chrome_dev_tools_performance.ts | 59 +++++++++++++++++- .../chrome_dev_tools_performance_spec.ts | 61 ++++++++++++++++++- 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/packages/core/src/render3/debug/chrome_dev_tools_performance.ts b/packages/core/src/render3/debug/chrome_dev_tools_performance.ts index 80c65036703..3565d3c10c0 100644 --- a/packages/core/src/render3/debug/chrome_dev_tools_performance.ts +++ b/packages/core/src/render3/debug/chrome_dev_tools_performance.ts @@ -7,8 +7,9 @@ */ import {isTypeProvider} from '../../di/provider_collection'; -import {assertDefined, assertEqual} from '../../util/assert'; +import {assertDefined} from '../../util/assert'; import {performanceMarkFeature} from '../../util/performance'; +import {VERSION} from '../../version'; import {setProfiler} from '../profiler'; import {Profiler, ProfilerEvent} from '../../../primitives/devtools'; import {stringifyForError} from '../util/stringify_utils'; @@ -42,6 +43,10 @@ declare global { trackName?: string, trackGroup?: string, color?: DevToolsColor, + // Experimental: 7th argument for user-supplied detail data rendered in the DevTools performance panel. + // https://developer.mozilla.org/en-US/docs/Web/API/Console/timeStamp_static#browser_compatibility + // Requires: chrome://flags/#enable-devtools-deep-link-via-extensibility-api + detail?: object, ): void; } } @@ -53,6 +58,54 @@ let counter = 0; type stackEntry = [ProfilerEvent | ProfilerDIEvent, number]; const eventsStack: stackEntry[] = []; +function getBaseDocUrl(): string { + const full = VERSION.full; + const isPreRelease = + full.includes('-next') || full.includes('-rc') || full === '0.0.0-PLACEHOLDER'; + const prefix = isPreRelease ? 'next' : `v${VERSION.major}`; + return `https://${prefix}.angular.dev`; +} + +/** + * Returns documentation URL for lifecycle hooks. + * Extracts lifecycle hook name from the component method string and maps to Angular docs. + */ +function getLifecycleHookDocUrl(hookName: string): string | undefined { + // Extract lifecycle hook name (e.g., "MyComponent:ngOnInit" -> "ngOnInit") + const match = hookName.match(/:(ng\w+)$/); + if (!match) return undefined; + + const lifecycleHook = match[1].toLowerCase(); + const baseUrl = getBaseDocUrl(); + + return `${baseUrl}/guide/components/lifecycle#${lifecycleHook}`; +} + +/** + * Returns documentation URL for profiler events. + */ +function getProfilerEventDocUrl(event: ProfilerEvent | ProfilerDIEvent, entryName: string) { + const baseUrl = getBaseDocUrl(); + + switch (event) { + case ProfilerEvent.ChangeDetectionStart: + case ProfilerEvent.ChangeDetectionEnd: + case ProfilerEvent.ChangeDetectionSyncStart: + case ProfilerEvent.ChangeDetectionSyncEnd: + return `${baseUrl}/best-practices/runtime-performance`; + case ProfilerEvent.AfterRenderHooksStart: + case ProfilerEvent.AfterRenderHooksEnd: + return `${baseUrl}/guide/components/lifecycle#aftereveryrender-and-afternextrender`; + case ProfilerEvent.DeferBlockStateStart: + case ProfilerEvent.DeferBlockStateEnd: + return `${baseUrl}/guide/defer`; + case ProfilerEvent.LifecycleHookStart: + return getLifecycleHookDocUrl(entryName); + default: + return undefined; + } +} + /** * Enum mimicking ProfilerEvent. The idea is to have unique event identifiers for both DI and other profiling events. */ @@ -72,7 +125,6 @@ function measureEnd( color: DevToolsColor, ) { let top: stackEntry | undefined; - // The stack may be asymmetric when an end event for a prior start event is missing (e.g. when an exception // has occurred), unroll the stack until a matching item has been found in that case. do { @@ -80,6 +132,8 @@ function measureEnd( assertDefined(top, 'Profiling error: could not find start event entry ' + startEvent); } while (top[0] !== startEvent); + const docUrl = getProfilerEventDocUrl(startEvent, entryName); + console.timeStamp( entryName, 'Event_' + top[0] + '_' + top[1], @@ -87,6 +141,7 @@ function measureEnd( '\u{1F170}\uFE0F Angular', undefined, color, + docUrl ? {description: 'Documentation', url: docUrl} : undefined, ); } diff --git a/packages/core/test/acceptance/chrome_dev_tools_performance_spec.ts b/packages/core/test/acceptance/chrome_dev_tools_performance_spec.ts index bce6e7d24fe..92e3db7f0e7 100644 --- a/packages/core/test/acceptance/chrome_dev_tools_performance_spec.ts +++ b/packages/core/test/acceptance/chrome_dev_tools_performance_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Component, HostAttributeToken, inject, Inject} from '../../src/core'; +import {Component, HostAttributeToken, inject, Inject, OnInit} from '../../src/core'; import {enableProfiling} from '../../src/render3/debug/chrome_dev_tools_performance'; import {profiler} from '../../src/render3/profiler'; import {ProfilerEvent} from '../../primitives/devtools'; @@ -83,4 +83,63 @@ describe('Chrome DevTools Performance integration', () => { } }); }); + + describe('documentation URLs', () => { + it('should include documentation URL for lifecycle hooks', () => { + @Component({ + template: ``, + }) + class MyCmp implements OnInit { + ngOnInit() {} + } + + const timeStampSpy = spyOn(console, 'timeStamp'); + const stopProfiling = enableProfiling(); + + try { + const fixture = TestBed.createComponent(MyCmp); + fixture.detectChanges(); + + const lifecycleCall = timeStampSpy.calls + .all() + .find((call) => (call.args[0] as string)?.includes(':ngOnInit')); + + expect(lifecycleCall).toBeDefined(); + // The 7th argument (index 6) is the detail object + const detail = (lifecycleCall!.args as any[])[6] as {url: string; description: string}; + expect(detail).toBeDefined(); + expect(detail.url).toMatch(/guide\/components\/lifecycle#ngoninit/); + expect(detail.description).toBe('Documentation'); + } finally { + stopProfiling(); + } + }); + + it('should include documentation URL for change detection', () => { + @Component({ + template: ``, + }) + class MyCmp {} + + const timeStampSpy = spyOn(console, 'timeStamp'); + const stopProfiling = enableProfiling(); + + try { + const fixture = TestBed.createComponent(MyCmp); + fixture.detectChanges(); + + const cdCall = timeStampSpy.calls + .all() + .find((call) => call.args[0]?.startsWith?.('Change detection')); + + expect(cdCall).toBeDefined(); + const detail = (cdCall!.args as any[])[6] as {url: string; description: string}; + expect(detail).toBeDefined(); + expect(detail.url).toMatch(/best-practices\/runtime-performance/); + expect(detail.description).toBe('Documentation'); + } finally { + stopProfiling(); + } + }); + }); });