Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ describe('ClientEventSubscriber', () => {
expect(messageBusMock.on).toHaveBeenCalledWith('inspectorEnd', jasmine.any(Function));
expect(messageBusMock.on).toHaveBeenCalledWith('createHighlightOverlay', jasmine.any(Function));
expect(messageBusMock.on).toHaveBeenCalledWith('removeHighlightOverlay', jasmine.any(Function));
expect(messageBusMock.on).toHaveBeenCalledWith(
'createRenderScanOverlay',
jasmine.any(Function),
);
expect(messageBusMock.on).toHaveBeenCalledWith(
'removeRenderScanOverlay',
jasmine.any(Function),
);
expect(messageBusMock.on).toHaveBeenCalledWith('createHydrationOverlay', jasmine.any(Function));
expect(messageBusMock.on).toHaveBeenCalledWith('removeHydrationOverlay', jasmine.any(Function));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const subscribeToClientEvents = (
},
): void => {
const inspector: InspectorRef = {ref: null};
const profilerState = {isProfiling: false};

messageBus.on('shutdown', shutdownCallback(messageBus));

Expand All @@ -83,10 +84,12 @@ export const subscribeToClientEvents = (

messageBus.on('queryNgAvailability', checkForAngularCallback(messageBus));

messageBus.on('startProfiling', startProfilingCallback(messageBus));
messageBus.on('stopProfiling', stopProfilingCallback(messageBus));
messageBus.on('startProfiling', startProfilingCallback(messageBus, profilerState));
messageBus.on('stopProfiling', stopProfilingCallback(messageBus, profilerState));

messageBus.on('setSelectedComponent', selectedComponentCallback(inspector));
messageBus.on('setSelectedComponent', selectedComponentCallback(inspector, profilerState));
messageBus.on('createRenderScanOverlay', renderScanOverlayCallback(inspector));
messageBus.on('removeRenderScanOverlay', removeRenderScanOverlayCallback());

messageBus.on('getNestedProperties', getNestedPropertiesCallback(messageBus));
messageBus.on('getRoutes', getRoutesCallback(messageBus));
Expand Down Expand Up @@ -202,22 +205,40 @@ export const viewSourceFromRouter = (constructName: string, type: RoutePropertyT
return getRouterCallableConstructRef(router.config, type, constructName);
};

const startProfilingCallback = (messageBus: MessageBus<Events>) => () =>
startProfiling((frame: ProfilerFrame) => {
messageBus.emit('sendProfilerChunk', [frame]);
});
const startProfilingCallback =
(messageBus: MessageBus<Events>, profilerState: {isProfiling: boolean}) => () => {
profilerState.isProfiling = true;
startProfiling((frame: ProfilerFrame) => {
messageBus.emit('sendProfilerChunk', [frame]);
});
};

const stopProfilingCallback =
(messageBus: MessageBus<Events>, profilerState: {isProfiling: boolean}) => () => {
profilerState.isProfiling = false;
messageBus.emit('profilerResults', [stopProfiling()]);
};

const selectedComponentCallback =
(inspector: InspectorRef, profilerState: {isProfiling: boolean}) =>
(position: ElementPosition) => {
if (profilerState.isProfiling) {
return;
}
const node = queryDirectiveForest(
position,
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
);
setConsoleReference({node, position});
inspector.ref?.highlightByPosition(position);
};

const stopProfilingCallback = (messageBus: MessageBus<Events>) => () => {
messageBus.emit('profilerResults', [stopProfiling()]);
const renderScanOverlayCallback = (inspector: InspectorRef) => (positions: ElementPosition[]) => {
inspector.ref?.highlightByPositions(positions);
};

const selectedComponentCallback = (inspector: InspectorRef) => (position: ElementPosition) => {
const node = queryDirectiveForest(
position,
initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(),
);
setConsoleReference({node, position});
inspector.ref?.highlightByPosition(position);
const removeRenderScanOverlayCallback = () => () => {
unHighlight();
};

const getNestedPropertiesCallback =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
findComponentAndHost,
highlightHydrationElement,
highlightSelectedElement,
highlightSelectedElementsForProfiler,
removeHydrationHighlights,
unHighlight,
unHighlightProfiler,
} from '../highlighter';
import {initializeOrGetDirectiveForestHooks} from '../hooks';
import {ComponentTreeNode} from '../interfaces';
Expand Down Expand Up @@ -109,6 +111,20 @@ export class ComponentInspector {
}
}

highlightByPositions(positions: ElementPosition[]): void {
const forest: ComponentTreeNode[] = initializeOrGetDirectiveForestHooks().getDirectiveForest();
const elementsToHighlight = positions
.map((position) => findNodeInForest(position, forest))
.filter((element): element is HTMLElement => element !== null);

if (elementsToHighlight.length > 0) {
highlightSelectedElementsForProfiler(elementsToHighlight);
return;
}

unHighlightProfiler();
}

highlightHydrationNodes(): void {
const forest: ComponentTreeNode[] = initializeOrGetDirectiveForestHooks().getDirectiveForest();

Expand Down
81 changes: 63 additions & 18 deletions devtools/projects/ng-devtools-backend/src/lib/highlighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ import {HydrationStatus} from '../../../protocol';
import {ngDebugClient} from './ng-debug-api/ng-debug-api';

let hydrationOverlayItems: HTMLElement[] = [];
let selectedElementOverlay: HTMLElement | null = null;
let profilerOverlays: HTMLElement[] = [];
let selectedElementOverlays: HTMLElement[] = [];
let selectedElement: Node | null = null;

const DEV_TOOLS_HIGHLIGHT_NODE_ID = '____ngDevToolsHighlight';
const PROFILER_OVERLAY_COLOR: RgbColor = [115, 97, 230];
const DEFAULT_OVERLAY_FILL_ALPHA = 0.35;
const PROFILER_OVERLAY_FILL_ALPHA = 0.1;

const OVERLAY_CONTENT_MARGIN = 4;
const MINIMAL_OVERLAY_CONTENT_SIZE = {
Expand All @@ -35,10 +39,18 @@ const HYDRATION_SVG = `
const HYDRATION_SKIPPED_SVG = `<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><rect fill="none" height="24" width="24"/><path d="M21.19,21.19L2.81,2.81L1.39,4.22l4.2,4.2c-1,1.31-1.6,2.94-1.6,4.7C4,17.48,7.58,21,12,21c1.75,0,3.36-0.56,4.67-1.5 l3.1,3.1L21.19,21.19z M12,19c-3.31,0-6-2.63-6-5.87c0-1.19,0.36-2.32,1.02-3.28L12,14.83V19z M8.38,5.56L12,2l5.65,5.56l0,0 C19.1,8.99,20,10.96,20,13.13c0,1.18-0.27,2.29-0.74,3.3L12,9.17V4.81L9.8,6.97L8.38,5.56z"/></svg>`;
const HYDRATION_ERROR_SVG = `<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M11 15h2v2h-2v-2zm0-8h2v6h-2V7zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>`;

function createOverlay(color: RgbColor): {overlay: HTMLElement; overlayContent: HTMLElement} {
function createOverlay(
color: RgbColor,
fillAlpha: number = DEFAULT_OVERLAY_FILL_ALPHA,
showBorder: boolean = false,
): {overlay: HTMLElement; overlayContent: HTMLElement} {
const overlay = document.createElement('div');
overlay.className = 'ng-devtools-overlay';
overlay.style.backgroundColor = toCSSColor(...color, 0.35);
overlay.style.backgroundColor = toCSSColor(...color, fillAlpha);
if (showBorder) {
overlay.style.border = `1px solid ${toCSSColor(...color)}`;
overlay.style.boxSizing = 'border-box';
}
overlay.style.position = 'absolute';
overlay.style.zIndex = '2147483647';
overlay.style.pointerEvents = 'none';
Expand Down Expand Up @@ -87,9 +99,40 @@ export function highlightSelectedElement(el: Node): void {
if (el === selectedElement) {
return;
}
unHighlight();
selectedElementOverlay = addHighlightForElement(el);
selectedElement = el;
highlightSelectedElements([el]);
}

export function highlightSelectedElementsForProfiler(elements: Node[]): void {
unHighlightProfiler();
profilerOverlays = elements
.map((element) =>
addHighlightForElement(
element,
PROFILER_OVERLAY_COLOR,
undefined,
PROFILER_OVERLAY_FILL_ALPHA,
true,
true,
),
)
.filter((overlay): overlay is HTMLElement => overlay !== null);
}

export function unHighlightProfiler(): void {
profilerOverlays.forEach((overlay) => {
if (inDoc(overlay)) {
document.body.removeChild(overlay);
}
});
profilerOverlays = [];
}

export function highlightSelectedElements(elements: Node[]): void {
clearSelectedElementHighlights();
selectedElement = null;
selectedElementOverlays = elements
.map((element) => addHighlightForElement(element))
.filter((overlay): overlay is HTMLElement => overlay !== null);
}

export function highlightHydrationElement(el: Node, status: HydrationStatus) {
Expand All @@ -108,19 +151,18 @@ export function highlightHydrationElement(el: Node, status: HydrationStatus) {
}

export function unHighlight(): void {
if (!selectedElementOverlay) {
return;
}

for (const node of document.body.childNodes) {
if (node === selectedElementOverlay) {
document.body.removeChild(selectedElementOverlay);
clearSelectedElementHighlights();
}

break;
function clearSelectedElementHighlights(): void {
selectedElementOverlays.forEach((overlay) => {
if (inDoc(overlay)) {
document.body.removeChild(overlay);
}
}
});

selectedElementOverlay = null;
selectedElementOverlays = [];
selectedElement = null;
}

export function removeHydrationHighlights(): void {
Expand All @@ -145,6 +187,9 @@ function addHighlightForElement(
el: Node,
color: RgbColor = COLORS.blue,
overlayType?: NonNullable<HydrationStatus>['status'],
fillAlpha: number = DEFAULT_OVERLAY_FILL_ALPHA,
showLabel: boolean = true,
showBorder: boolean = false,
): HTMLElement | null {
const cmp = findComponentAndHost(el).component;
const rect = getComponentRect(el);
Expand All @@ -153,7 +198,7 @@ function addHighlightForElement(
return null;
}

const {overlay, overlayContent} = createOverlay(color);
const {overlay, overlayContent} = createOverlay(color, fillAlpha, showBorder);
if (!rect) return null;

const content: Node[] = [];
Expand All @@ -169,7 +214,7 @@ function addHighlightForElement(
const svg = createOverlaySvgElement(overlayType!);
content.push(svg);
}
} else if (componentName) {
} else if (showLabel && componentName) {
const middleText = document.createTextNode(componentName);
const pre = document.createElement('span');
pre.innerText = `<`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,21 @@ export class DirectiveExplorerComponent {

handleNodeSelection(node: IndexedNode | null): void {
if (node) {
const isSameNode = this._clickedElement && sameDirectives(this._clickedElement, node);
const isSamePosition =
this._clickedElement && String(this._clickedElement.position) === String(node.position);

if (isSameNode && isSamePosition) {
this._clickedElement = node;
this.currentSelectedElement.set(node);
return;
}

// We want to guarantee that we're not reusing any of the previous properties.
// That's possible if the user has selected an NgForOf and after that
// they select another NgForOf instance. In this case, we don't want to diff the props
// we want to render from scratch.
if (this._clickedElement && !sameDirectives(this._clickedElement, node)) {
if (this._clickedElement && !isSameNode) {
this._propResolver.clearProperties();
}
this._clickedElement = node;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
</button>

<p class="instructions">
Interact to preview change detection. Clicking stop ends this Profiler recording.
Interact to preview change detection. The inspected app will be highlighted live while
recording. Clicking stop ends this Profiler recording.
</p>

<mat-progress-bar color="accent" mode="indeterminate" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {MatDialog} from '@angular/material/dialog';
import {MatIcon} from '@angular/material/icon';
import {MatTooltip} from '@angular/material/tooltip';
import {MatProgressBar} from '@angular/material/progress-bar';
import {Events, MessageBus, ProfilerFrame} from '../../../../../protocol';
import {ElementPosition, Events, MessageBus, ProfilerFrame} from '../../../../../protocol';
import {Subject} from 'rxjs';

import {FileApiService} from './file-api-service';
Expand Down Expand Up @@ -94,17 +94,20 @@ export class ProfilerComponent {
this._messageBus.on('sendProfilerChunk', (chunkOfRecords: ProfilerFrame) => {
this.stream.next([chunkOfRecords]);
this._buffer.push(chunkOfRecords);
this._highlightRenderScanOverlay(chunkOfRecords);
});
}

startRecording(): void {
this._messageBus.emit('removeRenderScanOverlay');
this.state.set('recording');
this._messageBus.emit('startProfiling');
}

stopRecording(): void {
this.state.set('visualizing');
this._messageBus.emit('stopProfiling');
this._messageBus.emit('removeRenderScanOverlay');
this.stream.complete();
}

Expand All @@ -121,8 +124,49 @@ export class ProfilerComponent {
}

discardRecording(): void {
this._messageBus.emit('removeRenderScanOverlay');
this.stream = new Subject<ProfilerFrame[]>();
this.state.set('idle');
this._buffer = [];
}

private _highlightRenderScanOverlay(frame: ProfilerFrame): void {
const positions = collectRenderScanPositions(frame.directives);
if (!positions.length) {
this._messageBus.emit('removeRenderScanOverlay');
return;
}

this._messageBus.emit('createRenderScanOverlay', [positions]);
}
}

function collectRenderScanPositions(
elements: ProfilerFrame['directives'],
path: ElementPosition = [],
positions: ElementPosition[] = [],
): ElementPosition[] {
elements.forEach((element, index) => {
if (!element) {
return;
}

const position = [...path, index];
if (didRunChangeDetection(element)) {
positions.push(position);
}

collectRenderScanPositions(element.children, position, positions);
});

return positions;
}

function didRunChangeDetection(profile: ProfilerFrame['directives'][number]): boolean {
const components = profile.directives.filter((directive) => directive.isComponent);
if (!components.length) {
return false;
}

return components.some((component) => component.changeDetection !== undefined);
}
2 changes: 2 additions & 0 deletions devtools/projects/protocol/src/lib/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,8 @@ export interface Events {
highlightComponent: (id: number) => void;
selectComponent: (id: number) => void;
removeComponentHighlight: () => void;
createRenderScanOverlay: (positions: ElementPosition[]) => void;
removeRenderScanOverlay: () => void;

enableTimingAPI: () => void;
disableTimingAPI: () => void;
Expand Down
Loading