mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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.
This commit is contained in:
parent
f9b74e90e3
commit
2f5ab541ea
2 changed files with 117 additions and 3 deletions
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue