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 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 @@ -30,7 +30,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 @@ -67,6 +67,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 @@ -205,24 +209,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 @@ -240,11 +226,12 @@ export default function Tree(props: Props) {
if (selectedElementID !== null) {
highlightNativeElement(selectedElementID);
} else {
bridge.send('clearNativeElementHighlight');
clearNativeElementHighlight();
}
}
}, [
bridge,
clearNativeElementHighlight,
isNavigatingWithKeyboard,
highlightNativeElement,
searchIndex,
Expand All @@ -270,9 +257,7 @@ export default function Tree(props: Props) {
setIsNavigatingWithKeyboard(false);
}, []);

const handleMouseLeave = useCallback(() => {
bridge.send('clearNativeElementHighlight');
}, [bridge]);
const handleMouseLeave = 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,6 +15,7 @@ type Props = {|
color: string,
height: number,
isDimmed?: boolean,
isHovered?: boolean,
label: string,
onClick: (event: SyntheticMouseEvent<*>) => mixed,
onDoubleClick?: (event: SyntheticMouseEvent<*>) => mixed,
Expand All @@ -33,29 +34,37 @@ export default function ChartNode({
color,
height,
isDimmed = false,
isHovered = false,
label,
onClick,
onDoubleClick,
onMouseEnter,
onMouseLeave,
onDoubleClick,
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})`}>
<rect
width={width}
height={height}
fill={color}
onClick={onClick}
onDoubleClick={onDoubleClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onDoubleClick={onDoubleClick}
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 @@ -16,6 +16,7 @@ import NoCommitData from './NoCommitData';
import CommitFlamegraphListItem from './CommitFlamegraphListItem';
import HoveredFiberInfo from './HoveredFiberInfo';
import {scale} from './utils';
import {useNativeElementHighlighter} from '../hooks';
import {StoreContext} from '../context';
import {SettingsContext} from '../Settings/SettingsContext';
import Tooltip from './Tooltip';
Expand All @@ -28,7 +29,9 @@ import type {CommitTree} from './types';

export type ItemData = {|
chartData: ChartData,
hoverFiber: (fiberData: TooltipFiberData | null) => void,
isHovered: boolean,
onElementMouseEnter: (fiberData: TooltipFiberData) => void,
onElementMouseLeave: () => void,
scaleX: (value: number, fallbackValue: number) => number,
selectedChartNode: ChartNode | null,
selectedChartNodeIndex: number,
Expand Down Expand Up @@ -96,9 +99,13 @@ type Props = {|
|};

function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
const [hoveredFiberData, hoverFiber] = useState<number | null>(null);
const [hoveredFiberData, setHoveredFiberData] = useState<number | null>(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

This Flow type looks wrong. (Looks like it's been wrong for a while though FWIW- which is a bit concerning.)

We aren't storing a number in this state, but an object with id and name props (TooltipFiberData).

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

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

const handleElementMouseEnter = ({id, name}) => {
highlightNativeElement(id); // Highlight last hovered element.
setHoveredFiberData({id, name}); // Set hovered fiber data for tooltip
};

const handleElementMouseLeave = () => {
bvaughn marked this conversation as resolved.
Show resolved Hide resolved
clearNativeElementHighlight(); // clear highlighting of element on mouse leave
setHoveredFiberData(null); // clear hovered fiber data for tooltip
};

const itemData = useMemo<ItemData>(
() => ({
chartData,
hoverFiber,
isHovered: hoveredFiberData && hoveredFiberData.id,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this check not just hoveredFiberData !== null?

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 All @@ -140,7 +159,7 @@ function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
}),
[
chartData,
hoverFiber,
setHoveredFiberData,
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. The dependencies array has setHoveredFiberData but the memoized value references hoveredFiberData.

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

selectedChartNode,
selectedChartNodeIndex,
selectFiber,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ type Props = {
function CommitFlamegraphListItem({data, index, style}: Props) {
const {
chartData,
hoverFiber,
isHovered,
onElementMouseEnter,
onElementMouseLeave,
scaleX,
selectedChartNode,
selectedChartNodeIndex,
selectFiber,
width,
} = data;

const {renderPathNodes, maxSelfDuration, rows} = chartData;

const {lineHeight} = useContext(SettingsContext);
Expand All @@ -49,11 +52,11 @@ function CommitFlamegraphListItem({data, index, style}: Props) {

const handleMouseEnter = (nodeData: ChartNodeType) => {
const {id, name} = nodeData;
hoverFiber({id, name});
onElementMouseEnter({id, name});
};

const handleMouseLeave = () => {
hoverFiber(null);
onElementMouseLeave();
};

// List items are absolutely positioned using the CSS "top" attribute.
Expand Down Expand Up @@ -114,6 +117,7 @@ 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)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import HoveredFiberInfo from './HoveredFiberInfo';
import {scale} from './utils';
import {StoreContext} from '../context';
import {SettingsContext} from '../Settings/SettingsContext';
import {useNativeElementHighlighter} from '../hooks';
import Tooltip from './Tooltip';

import styles from './CommitRanked.css';
Expand All @@ -28,7 +29,9 @@ import type {CommitTree} from './types';

export type ItemData = {|
chartData: ChartData,
hoverFiber: (fiberData: TooltipFiberData | null) => void,
isHovered: boolean,
onElementMouseEnter: (fiberData: TooltipFiberData) => void,
onElementMouseLeave: () => void,
scaleX: (value: number, fallbackValue: number) => number,
selectedFiberID: number | null,
selectedFiberIndex: number,
Expand Down Expand Up @@ -94,19 +97,35 @@ type Props = {|
|};

function CommitRanked({chartData, commitTree, height, width}: Props) {
const [hoveredFiberData, hoverFiber] = useState<number | null>(null);
const [hoveredFiberData, setHoveredFiberData] = useState<number | null>(null);
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 hook state type.

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

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

const handleElementMouseEnter = ({id, name}) => {
highlightNativeElement(id); // Highlight last hovered element.
setHoveredFiberData({id, name}); // Set hovered fiber data for tooltip
};

const handleElementMouseLeave = () => {
bvaughn marked this conversation as resolved.
Show resolved Hide resolved
clearNativeElementHighlight(); // clear highlighting of element on mouse leave
setHoveredFiberData(null); // clear hovered fiber data for tooltip
};

const itemData = useMemo<ItemData>(
() => ({
chartData,
hoverFiber,
isHovered: hoveredFiberData && hoveredFiberData.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 comment as above wrt hoveredFiberData !== null

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 @@ -27,7 +27,9 @@ type Props = {
function CommitRankedListItem({data, index, style}: Props) {
const {
chartData,
hoverFiber,
isHovered,
onElementMouseEnter,
onElementMouseLeave,
scaleX,
selectedFiberIndex,
selectFiber,
Expand All @@ -49,11 +51,11 @@ function CommitRankedListItem({data, index, style}: Props) {

const handleMouseEnter = () => {
const {id, name} = node;
hoverFiber({id, name});
onElementMouseEnter({id, name});
};

const handleMouseLeave = () => {
hoverFiber(null);
onElementMouseLeave();
};

// List items are absolutely positioned using the CSS "top" attribute.
Expand All @@ -67,6 +69,7 @@ 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}
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};
}