Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Component Highlighting to Profiler DevTools #17934

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 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 @@ -29,7 +29,7 @@ import SearchInput from './SearchInput';
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
import SelectedTreeHighlight from './SelectedTreeHighlight';
import TreeFocusedContext from './TreeFocusedContext';

import {useNativeElementHighlighter} from '../hooks';
import styles from './Tree.css';

// Never indent more than this number of pixels (even if we have the room).
Expand Down Expand Up @@ -66,6 +66,10 @@ export default function Tree(props: Props) {
const [treeFocused, setTreeFocused] = useState<boolean>(false);

const {lineHeight} = useContext(SettingsContext);
const {
highlightNativeElement,
clearNativeElementHighlight,
} = useNativeElementHighlighter();

// Make sure a newly selected element is visible in the list.
// This is helpful for things like the owners list and search.
Expand Down Expand Up @@ -204,24 +208,6 @@ export default function Tree(props: Props) {
[dispatch, selectedElementID],
);

const highlightNativeElement = useCallback(
(id: number) => {
const element = store.getElementByID(id);
const rendererID = store.getRendererIDForElement(id);
if (element !== null && rendererID !== null) {
bridge.send('highlightNativeElement', {
displayName: element.displayName,
hideAfterTimeout: false,
id,
openNativeElementsPanel: false,
rendererID,
scrollIntoView: false,
});
}
},
[store, bridge],
);

// If we switch the selected element while using the keyboard,
// start highlighting it in the DOM instead of the last hovered node.
const searchRef = useRef({searchIndex, searchResults});
Expand All @@ -239,11 +225,12 @@ export default function Tree(props: Props) {
if (selectedElementID !== null) {
highlightNativeElement(selectedElementID);
} else {
bridge.send('clearNativeElementHighlight');
clearNativeElementHighlight();
}
}
}, [
bridge,
clearNativeElementHighlight,
isNavigatingWithKeyboard,
highlightNativeElement,
searchIndex,
Expand All @@ -269,9 +256,9 @@ export default function Tree(props: Props) {
setIsNavigatingWithKeyboard(false);
}, []);

const handleMouseLeave = useCallback(() => {
bridge.send('clearNativeElementHighlight');
}, [bridge]);
const handleMouseLeave = () => {
M-Izadmehr marked this conversation as resolved.
Show resolved Hide resolved
clearNativeElementHighlight();
};

// Let react-window know to re-render any time the underlying tree data changes.
// This includes the owner context, since it controls a filtered view of the tree.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ type Props = {|
color: string,
height: number,
isDimmed?: boolean,
isHovered?: boolean,
label: string,
onClick: (event: SyntheticMouseEvent<*>) => mixed,
onDoubleClick?: (event: SyntheticMouseEvent<*>) => mixed,
onMouseEnter?: (event: SyntheticMouseEvent<*>) => mixed,
onMouseLeave?: (event: SyntheticMouseEvent<*>) => mixed,
placeLabelAboveNode?: boolean,
textStyle?: Object,
width: number,
Expand All @@ -31,14 +34,24 @@ export default function ChartNode({
color,
height,
isDimmed = false,
isHovered = false,
label,
onClick,
onDoubleClick,
onMouseEnter,
onMouseLeave,
textStyle,
width,
x,
y,
}: Props) {
let opacity = 1;
if (isHovered) {
opacity = 0.75;
} else if (isDimmed) {
opacity = 0.5;
}

return (
<g className={styles.Group} transform={`translate(${x},${y})`}>
<title>{label}</title>
Expand All @@ -48,9 +61,11 @@ export default function ChartNode({
fill={color}
onClick={onClick}
onDoubleClick={onDoubleClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={styles.Rect}
style={{
opacity: isDimmed ? 0.5 : 1,
opacity,
}}
/>
{width >= minWidthToDisplay && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {ProfilerContext} from './ProfilerContext';
import NoCommitData from './NoCommitData';
import CommitFlamegraphListItem from './CommitFlamegraphListItem';
import {scale} from './utils';
import {useNativeElementHighlighter} from '../hooks';
import {StoreContext} from '../context';
import {SettingsContext} from '../Settings/SettingsContext';

Expand All @@ -24,6 +25,8 @@ import type {CommitTree} from './types';

export type ItemData = {|
chartData: ChartData,
onElementMouseEnter: (id: number) => void,
onElementMouseLeave: () => void,
scaleX: (value: number, fallbackValue: number) => number,
selectedChartNode: ChartNode | null,
selectedChartNodeIndex: number,
Expand Down Expand Up @@ -93,6 +96,10 @@ type Props = {|
function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
const {lineHeight} = useContext(SettingsContext);
const {selectFiber, selectedFiberID} = useContext(ProfilerContext);
const {
highlightNativeElement,
clearNativeElementHighlight,
} = useNativeElementHighlighter();

const selectedChartNodeIndex = useMemo<number>(() => {
if (selectedFiberID === null) {
Expand All @@ -115,9 +122,21 @@ function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
return null;
}, [chartData, selectedFiberID, selectedChartNodeIndex]);

// Highlight last hovered element.
const handleElementMouseEnter = id => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary function wrapper. I'd suggest just passing highlightNativeElement directly (or if you think the naming is clearer, just alias, e.g. const handleElementMouseEnter = highlightNativeElement )

Copy link
Contributor Author

@M-Izadmehr M-Izadmehr Mar 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After merging the other PR, this method now does two actions:

  1. Adding the highlighting,
  2. Setting the fiberData used for the tooltip,
    So, I guess we can keep the wrapper.

highlightNativeElement(id);
};

// remove highlighting of element on mouse leave
const handleElementMouseLeave = () => {
bvaughn marked this conversation as resolved.
Show resolved Hide resolved
clearNativeElementHighlight();
};

const itemData = useMemo<ItemData>(
() => ({
chartData,
onElementMouseEnter: handleElementMouseEnter,
onElementMouseLeave: handleElementMouseLeave,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks wrong. Neither handleElementMouseEnter nor handleElementMouseLeave are mentioned in the dependencies array.

I wonder why our lint rule isn't complaining about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

scaleX: scale(
0,
selectedChartNode !== null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @flow
*/

import React, {Fragment, memo, useCallback, useContext} from 'react';
import React, {Fragment, memo, useCallback, useContext, useState} from 'react';
import {areEqual} from 'react-window';
import {barWidthThreshold} from './constants';
import {getGradientColor} from './utils';
Expand All @@ -24,8 +24,12 @@ type Props = {
};

function CommitFlamegraphListItem({data, index, style}: Props) {
const [isHovered, setIsHovered] = useState(false);

const {
chartData,
onElementMouseEnter,
onElementMouseLeave,
scaleX,
selectedChartNode,
selectedChartNodeIndex,
Expand All @@ -43,6 +47,19 @@ function CommitFlamegraphListItem({data, index, style}: Props) {
[selectFiber],
);

const handleMouseEnter = (id: number) => {
setIsHovered(true);

if (id !== null) {
onElementMouseEnter(id);
}
};

const handleMouseLeave = () => {
setIsHovered(false);
onElementMouseLeave();
};

// List items are absolutely positioned using the CSS "top" attribute.
// The "left" value will always be 0.
// Since height is fixed, and width is based on the node's duration,
Expand Down Expand Up @@ -101,9 +118,12 @@ function CommitFlamegraphListItem({data, index, style}: Props) {
color={color}
height={lineHeight}
isDimmed={index < selectedChartNodeIndex}
isHovered={isHovered}
key={id}
label={label}
onClick={event => handleClick(event, id, name)}
onMouseEnter={() => handleMouseEnter(id)}
onMouseLeave={handleMouseLeave}
textStyle={{color: textColor}}
width={nodeWidth}
x={nodeOffset - selectedNodeOffset}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import CommitRankedListItem from './CommitRankedListItem';
import {scale} from './utils';
import {StoreContext} from '../context';
import {SettingsContext} from '../Settings/SettingsContext';
import {useNativeElementHighlighter} from '../hooks';

import styles from './CommitRanked.css';

Expand All @@ -24,6 +25,8 @@ import type {CommitTree} from './types';

export type ItemData = {|
chartData: ChartData,
onElementMouseEnter: (id: number) => void,
onElementMouseLeave: () => void,
scaleX: (value: number, fallbackValue: number) => number,
selectedFiberID: number | null,
selectedFiberIndex: number,
Expand Down Expand Up @@ -91,15 +94,31 @@ type Props = {|
function CommitRanked({chartData, commitTree, height, width}: Props) {
const {lineHeight} = useContext(SettingsContext);
const {selectedFiberID, selectFiber} = useContext(ProfilerContext);
const {
highlightNativeElement,
clearNativeElementHighlight,
} = useNativeElementHighlighter();

const selectedFiberIndex = useMemo(
() => getNodeIndex(chartData, selectedFiberID),
[chartData, selectedFiberID],
);

// Highlight last hovered element.
const handleElementMouseEnter = id => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above comment:)

highlightNativeElement(id);
};

// remove highlighting of element on mouse leave
const handleElementMouseLeave = () => {
bvaughn marked this conversation as resolved.
Show resolved Hide resolved
clearNativeElementHighlight();
};

const itemData = useMemo<ItemData>(
() => ({
chartData,
onElementMouseEnter: handleElementMouseEnter,
onElementMouseLeave: handleElementMouseLeave,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above wrt missing dependencies.

scaleX: scale(0, chartData.nodes[selectedFiberIndex].value, 0, width),
selectedFiberID,
selectedFiberIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @flow
*/

import React, {memo, useCallback, useContext} from 'react';
import React, {memo, useCallback, useContext, useState} from 'react';
import {areEqual} from 'react-window';
import {minBarWidth} from './constants';
import {getGradientColor} from './utils';
Expand All @@ -24,7 +24,16 @@ type Props = {
};

function CommitRankedListItem({data, index, style}: Props) {
const {chartData, scaleX, selectedFiberIndex, selectFiber, width} = data;
const [isHovered, setIsHovered] = useState(false);
const {
chartData,
onElementMouseEnter,
onElementMouseLeave,
scaleX,
selectedFiberIndex,
selectFiber,
width,
} = data;

const node = chartData.nodes[index];

Expand All @@ -38,6 +47,19 @@ function CommitRankedListItem({data, index, style}: Props) {
[node, selectFiber],
);

const handleMouseEnter = (id: number) => {
setIsHovered(true);

if (id !== null) {
onElementMouseEnter(id);
}
};

const handleMouseLeave = () => {
setIsHovered(false);
onElementMouseLeave();
};

// List items are absolutely positioned using the CSS "top" attribute.
// The "left" value will always be 0.
// Since height is fixed, and width is based on the node's duration,
Expand All @@ -49,9 +71,12 @@ function CommitRankedListItem({data, index, style}: Props) {
color={getGradientColor(node.value / chartData.maxValue)}
height={lineHeight}
isDimmed={index < selectedFiberIndex}
isHovered={isHovered}
key={node.id}
label={node.label}
onClick={handleClick}
onMouseEnter={() => handleMouseEnter(node.id)}
onMouseLeave={handleMouseLeave}
width={Math.max(minBarWidth, scaleX(node.value, width))}
x={0}
y={top}
Expand Down
28 changes: 28 additions & 0 deletions packages/react-devtools-shared/src/devtools/views/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import {
useLayoutEffect,
useReducer,
useState,
useContext,
} from 'react';
import {
localStorageGetItem,
localStorageSetItem,
} from 'react-devtools-shared/src/storage';
import {sanitizeForParse, smartParse, smartStringify} from '../utils';
import {BridgeContext, StoreContext} from './context';

type ACTION_RESET = {|
type: 'RESET',
Expand Down Expand Up @@ -300,3 +302,29 @@ export function useSubscription<Value>({

return state.value;
}

// provides highlighting and clearing highlights of elements based on ID
export function useNativeElementHighlighter() {
const store = useContext(StoreContext);
const bridge = useContext(BridgeContext);

const highlightNativeElement = (id: number) => {
bvaughn marked this conversation as resolved.
Show resolved Hide resolved
const element = store.getElementByID(id);
const rendererID = store.getRendererIDForElement(id);
if (element !== null && rendererID !== null) {
bridge.send('highlightNativeElement', {
displayName: element.displayName,
hideAfterTimeout: false,
id,
openNativeElementsPanel: false,
rendererID,
scrollIntoView: false,
});
}
};

const clearNativeElementHighlight = () => {
bridge.send('clearNativeElementHighlight');
};
return {highlightNativeElement, clearNativeElementHighlight};
}