mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
This commit runs tslint --fix with the angular/angular tslint configuration on the files inside the devtools codebase. Notably, the file-header rule in `tslint.json` was missing a default attribute. This commit adds that default attribute and sets it to the license header that is present in all files in this repo. After running tslint --fix with this default added, this commit added the license header to all files in the devtools directory. Note for the reviewer: the automatically added license headers were added as comments with the "/*!" prefix. Since we want these comments removed in builds, and the rest of the codebase uses "/**", a simple find and replace was performed on the devtools directory to change these prefixes to "/**".
342 lines
11 KiB
TypeScript
342 lines
11 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {DirectiveProfile, ElementPosition, ElementProfile, LifecycleProfile, ProfilerFrame} from 'protocol';
|
|
|
|
import {getDirectiveName} from '../highlighter';
|
|
import {ComponentTreeNode} from '../interfaces';
|
|
import {isCustomElement, runOutsideAngular} from '../utils';
|
|
|
|
import {initializeOrGetDirectiveForestHooks} from '.';
|
|
import {DirectiveForestHooks} from './hooks';
|
|
import {Hooks} from './profiler';
|
|
|
|
let inProgress = false;
|
|
let inChangeDetection = false;
|
|
let eventMap: Map<any, DirectiveProfile>;
|
|
let frameDuration = 0;
|
|
let hooks: Partial<Hooks> = {};
|
|
|
|
export const start = (onFrame: (frame: ProfilerFrame) => void): void => {
|
|
if (inProgress) {
|
|
throw new Error('Recording already in progress');
|
|
}
|
|
eventMap = new Map<any, DirectiveProfile>();
|
|
inProgress = true;
|
|
hooks = getHooks(onFrame);
|
|
initializeOrGetDirectiveForestHooks().profiler.subscribe(hooks);
|
|
};
|
|
|
|
export const stop = (): ProfilerFrame => {
|
|
const directiveForestHooks = initializeOrGetDirectiveForestHooks();
|
|
const result = flushBuffer(directiveForestHooks);
|
|
initializeOrGetDirectiveForestHooks().profiler.unsubscribe(hooks);
|
|
hooks = {};
|
|
inProgress = false;
|
|
return result;
|
|
};
|
|
|
|
const startEvent = (map: Record<string, number>, directive: any, label: string) => {
|
|
const name = getDirectiveName(directive);
|
|
const key = `${name}#${label}`;
|
|
map[key] = performance.now();
|
|
};
|
|
|
|
const getEventStart = (map: Record<string, number>, directive: any, label: string) => {
|
|
const name = getDirectiveName(directive);
|
|
const key = `${name}#${label}`;
|
|
return map[key];
|
|
};
|
|
|
|
const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial<Hooks> => {
|
|
const timeStartMap: Record<string, number> = {};
|
|
return {
|
|
// We flush here because it's possible the current node to overwrite
|
|
// an existing removed node.
|
|
onCreate(
|
|
directive: any, node: Node, _: number, isComponent: boolean, position: ElementPosition):
|
|
void {
|
|
eventMap.set(directive, {
|
|
isElement: isCustomElement(node),
|
|
name: getDirectiveName(directive),
|
|
isComponent,
|
|
lifecycle: {},
|
|
outputs: {},
|
|
});
|
|
},
|
|
onChangeDetectionStart(component: any, node: Node): void {
|
|
startEvent(timeStartMap, component, 'changeDetection');
|
|
if (!inChangeDetection) {
|
|
inChangeDetection = true;
|
|
const source = getChangeDetectionSource();
|
|
runOutsideAngular(() => {
|
|
Promise.resolve().then(() => {
|
|
inChangeDetection = false;
|
|
onFrame(flushBuffer(initializeOrGetDirectiveForestHooks(), source));
|
|
});
|
|
});
|
|
}
|
|
if (!eventMap.has(component)) {
|
|
eventMap.set(component, {
|
|
isElement: isCustomElement(node),
|
|
name: getDirectiveName(component),
|
|
isComponent: true,
|
|
changeDetection: 0,
|
|
lifecycle: {},
|
|
outputs: {},
|
|
});
|
|
}
|
|
},
|
|
onChangeDetectionEnd(component: any): void {
|
|
const profile = eventMap.get(component);
|
|
|
|
if (profile) {
|
|
let current = profile.changeDetection;
|
|
if (current === undefined) {
|
|
current = 0;
|
|
}
|
|
const startTimestamp = getEventStart(timeStartMap, component, 'changeDetection');
|
|
if (startTimestamp === undefined) {
|
|
return;
|
|
}
|
|
const duration = performance.now() - startTimestamp;
|
|
profile.changeDetection = current + duration;
|
|
frameDuration += duration;
|
|
} else {
|
|
console.warn('Could not find profile for', component);
|
|
}
|
|
},
|
|
onDestroy(directive: any, node: Node, _: number, isComponent: boolean, __: ElementPosition):
|
|
void {
|
|
// Make sure we reflect such directives in the report.
|
|
if (!eventMap.has(directive)) {
|
|
eventMap.set(directive, {
|
|
isElement: isComponent && isCustomElement(node),
|
|
name: getDirectiveName(directive),
|
|
isComponent,
|
|
lifecycle: {},
|
|
outputs: {},
|
|
});
|
|
}
|
|
},
|
|
onLifecycleHookStart(
|
|
directive: any, hookName: keyof LifecycleProfile, node: Node, __: number,
|
|
isComponent: boolean): void {
|
|
startEvent(timeStartMap, directive, hookName);
|
|
if (!eventMap.has(directive)) {
|
|
eventMap.set(directive, {
|
|
isElement: isCustomElement(node),
|
|
name: getDirectiveName(directive),
|
|
isComponent,
|
|
lifecycle: {},
|
|
outputs: {},
|
|
});
|
|
}
|
|
},
|
|
onLifecycleHookEnd(
|
|
directive: any, hookName: keyof LifecycleProfile, _: Node, __: number, ___: boolean): void {
|
|
const dir = eventMap.get(directive);
|
|
const startTimestamp = getEventStart(timeStartMap, directive, hookName);
|
|
if (startTimestamp === undefined) {
|
|
return;
|
|
}
|
|
if (!dir) {
|
|
console.warn('Could not find directive in onLifecycleHook callback', directive, hookName);
|
|
return;
|
|
}
|
|
const duration = performance.now() - startTimestamp;
|
|
dir.lifecycle[hookName] = (dir.lifecycle[hookName] || 0) + duration;
|
|
frameDuration += duration;
|
|
},
|
|
onOutputStart(componentOrDirective: any, outputName: string, node: Node, isComponent: boolean):
|
|
void {
|
|
startEvent(timeStartMap, componentOrDirective, outputName);
|
|
if (!eventMap.has(componentOrDirective)) {
|
|
eventMap.set(componentOrDirective, {
|
|
isElement: isCustomElement(node),
|
|
name: getDirectiveName(componentOrDirective),
|
|
isComponent,
|
|
lifecycle: {},
|
|
outputs: {},
|
|
});
|
|
}
|
|
},
|
|
onOutputEnd(componentOrDirective: any, outputName: string): void {
|
|
const name = outputName;
|
|
const entry = eventMap.get(componentOrDirective);
|
|
const startTimestamp = getEventStart(timeStartMap, componentOrDirective, name);
|
|
if (startTimestamp === undefined) {
|
|
return;
|
|
}
|
|
if (!entry) {
|
|
console.warn(
|
|
'Could not find directive or component in onOutputEnd callback', componentOrDirective,
|
|
outputName);
|
|
return;
|
|
}
|
|
const duration = performance.now() - startTimestamp;
|
|
entry.outputs[name] = (entry.outputs[name] || 0) + duration;
|
|
frameDuration += duration;
|
|
},
|
|
};
|
|
};
|
|
|
|
const insertOrMerge = (lastFrame: ElementProfile, profile: DirectiveProfile) => {
|
|
let exists = false;
|
|
lastFrame.directives.forEach((d) => {
|
|
if (d.name === profile.name) {
|
|
exists = true;
|
|
let current = d.changeDetection;
|
|
if (current === undefined) {
|
|
current = 0;
|
|
}
|
|
d.changeDetection = current + (profile.changeDetection ?? 0);
|
|
for (const key of Object.keys(profile.lifecycle)) {
|
|
if (!d.lifecycle[key]) {
|
|
d.lifecycle[key] = 0;
|
|
}
|
|
d.lifecycle[key] += profile.lifecycle[key];
|
|
}
|
|
for (const key of Object.keys(profile.outputs)) {
|
|
if (!d.outputs[key]) {
|
|
d.outputs[key] = 0;
|
|
}
|
|
d.outputs[key] += profile.outputs[key];
|
|
}
|
|
}
|
|
});
|
|
if (!exists) {
|
|
lastFrame.directives.push(profile);
|
|
}
|
|
};
|
|
|
|
const insertElementProfile =
|
|
(frames: ElementProfile[], position: ElementPosition, profile?: DirectiveProfile) => {
|
|
if (!profile) {
|
|
return;
|
|
}
|
|
const original = frames;
|
|
for (let i = 0; i < position.length - 1; i++) {
|
|
const pos = position[i];
|
|
if (!frames[pos]) {
|
|
// TODO(mgechev): consider how to ensure we don't hit this case
|
|
console.warn('Unable to find parent node for', profile, original);
|
|
return;
|
|
}
|
|
frames = frames[pos].children;
|
|
}
|
|
const lastIdx = position[position.length - 1];
|
|
let lastFrame: ElementProfile = {
|
|
children: [],
|
|
directives: [],
|
|
};
|
|
if (frames[lastIdx]) {
|
|
lastFrame = frames[lastIdx];
|
|
} else {
|
|
frames[lastIdx] = lastFrame;
|
|
}
|
|
insertOrMerge(lastFrame, profile);
|
|
};
|
|
|
|
const prepareInitialFrame = (source: string, duration: number) => {
|
|
const frame: ProfilerFrame = {
|
|
source,
|
|
duration,
|
|
directives: [],
|
|
};
|
|
const directiveForestHooks = initializeOrGetDirectiveForestHooks();
|
|
const directiveForest = directiveForestHooks.getIndexedDirectiveForest();
|
|
const traverse = (node: ComponentTreeNode, children = frame.directives) => {
|
|
let position: ElementPosition|undefined;
|
|
if (node.component) {
|
|
position = directiveForestHooks.getDirectivePosition(node.component.instance);
|
|
} else {
|
|
position = directiveForestHooks.getDirectivePosition(node.directives[0].instance);
|
|
}
|
|
if (position === undefined) {
|
|
return;
|
|
}
|
|
const directives = node.directives.map((d) => {
|
|
return {
|
|
isComponent: false,
|
|
isElement: false,
|
|
name: getDirectiveName(d.instance),
|
|
outputs: {},
|
|
lifecycle: {},
|
|
};
|
|
});
|
|
if (node.component) {
|
|
directives.push({
|
|
isElement: node.component.isElement,
|
|
isComponent: true,
|
|
lifecycle: {},
|
|
outputs: {},
|
|
name: getDirectiveName(node.component.instance),
|
|
});
|
|
}
|
|
const result = {
|
|
children: [],
|
|
directives,
|
|
};
|
|
children[position[position.length - 1]] = result;
|
|
node.children.forEach((n) => traverse(n, result.children));
|
|
};
|
|
directiveForest.forEach((n) => traverse(n));
|
|
return frame;
|
|
};
|
|
|
|
const flushBuffer = (directiveForestHooks: DirectiveForestHooks, source: string = '') => {
|
|
const items = Array.from(eventMap.keys());
|
|
const positions: ElementPosition[] = [];
|
|
const positionDirective = new Map<ElementPosition, any>();
|
|
items.forEach((dir) => {
|
|
const position = directiveForestHooks.getDirectivePosition(dir);
|
|
if (position === undefined) {
|
|
return;
|
|
}
|
|
positions.push(position);
|
|
positionDirective.set(position, dir);
|
|
});
|
|
positions.sort(lexicographicOrder);
|
|
|
|
const result = prepareInitialFrame(source, frameDuration);
|
|
frameDuration = 0;
|
|
|
|
positions.forEach((position) => {
|
|
const dir = positionDirective.get(position);
|
|
insertElementProfile(result.directives, position, eventMap.get(dir));
|
|
});
|
|
eventMap = new Map<any, DirectiveProfile>();
|
|
return result;
|
|
};
|
|
|
|
const getChangeDetectionSource = () => {
|
|
const zone = (window as any).Zone;
|
|
if (!zone || !zone.currentTask) {
|
|
return '';
|
|
}
|
|
return zone.currentTask.source;
|
|
};
|
|
|
|
const lexicographicOrder = (a: ElementPosition, b: ElementPosition) => {
|
|
if (a.length < b.length) {
|
|
return -1;
|
|
}
|
|
if (a.length > b.length) {
|
|
return 1;
|
|
}
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (a[i] < b[i]) {
|
|
return -1;
|
|
}
|
|
if (a[i] > b[i]) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
};
|