Skip to content

Commit 44e5f5e

Browse files
authored
Add fiber summary tooltip to devtools profiling (facebook#18048)
* Add tooltip component * Separate logic of ProfilerWhatChanged to a component * Add hovered Fiber info tooltip component * Add flame graph chart tooltip * Add commit ranked list tooltip * Fix flow issues * Minor improvement in filter * Fix flickering issue * Resolved issues on useCallbacks and mouse event listeners * Fix lints * Remove unnecessary useCallback
1 parent 2512c30 commit 44e5f5e

13 files changed

+527
-176
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.Component {
2+
margin-bottom: 1rem;
3+
}
4+
5+
.Item {
6+
margin-top: 0.25rem;
7+
}
8+
9+
.Key {
10+
font-family: var(--font-family-monospace);
11+
font-size: var(--font-size-monospace-small);
12+
line-height: 1;
13+
}
14+
15+
.Key:first-of-type::before {
16+
content: ' (';
17+
}
18+
19+
.Key::after {
20+
content: ', ';
21+
}
22+
23+
.Key:last-of-type::after {
24+
content: ')';
25+
}
26+
27+
.Label {
28+
font-weight: bold;
29+
margin-bottom: 0.5rem;
30+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import React, {useContext} from 'react';
11+
import {ProfilerContext} from '../Profiler/ProfilerContext';
12+
import {StoreContext} from '../context';
13+
14+
import styles from './ProfilerWhatChanged.css';
15+
16+
type ProfilerWhatChangedProps = {|
17+
fiberID: number,
18+
|};
19+
20+
export default function ProfilerWhatChanged({
21+
fiberID,
22+
}: ProfilerWhatChangedProps) {
23+
const {profilerStore} = useContext(StoreContext);
24+
const {rootID, selectedCommitIndex} = useContext(ProfilerContext);
25+
26+
// TRICKY
27+
// Handle edge case where no commit is selected because of a min-duration filter update.
28+
// If the commit index is null, suspending for data below would throw an error.
29+
// TODO (ProfilerContext) This check should not be necessary.
30+
if (selectedCommitIndex === null) {
31+
return null;
32+
}
33+
34+
const {changeDescriptions} = profilerStore.getCommitData(
35+
((rootID: any): number),
36+
selectedCommitIndex,
37+
);
38+
39+
if (changeDescriptions === null) {
40+
return null;
41+
}
42+
43+
const changeDescription = changeDescriptions.get(fiberID);
44+
if (changeDescription == null) {
45+
return null;
46+
}
47+
48+
if (changeDescription.isFirstMount) {
49+
return (
50+
<div className={styles.Component}>
51+
<label className={styles.Label}>Why did this render?</label>
52+
<div className={styles.Item}>
53+
This is the first time the component rendered.
54+
</div>
55+
</div>
56+
);
57+
}
58+
59+
const changes = [];
60+
61+
if (changeDescription.context === true) {
62+
changes.push(
63+
<div key="context" className={styles.Item}>
64+
• Context changed
65+
</div>,
66+
);
67+
} else if (
68+
typeof changeDescription.context === 'object' &&
69+
changeDescription.context !== null &&
70+
changeDescription.context.length !== 0
71+
) {
72+
changes.push(
73+
<div key="context" className={styles.Item}>
74+
• Context changed:
75+
{changeDescription.context.map(key => (
76+
<span key={key} className={styles.Key}>
77+
{key}
78+
</span>
79+
))}
80+
</div>,
81+
);
82+
}
83+
84+
if (changeDescription.didHooksChange) {
85+
changes.push(
86+
<div key="hooks" className={styles.Item}>
87+
• Hooks changed
88+
</div>,
89+
);
90+
}
91+
92+
if (
93+
changeDescription.props !== null &&
94+
changeDescription.props.length !== 0
95+
) {
96+
changes.push(
97+
<div key="props" className={styles.Item}>
98+
• Props changed:
99+
{changeDescription.props.map(key => (
100+
<span key={key} className={styles.Key}>
101+
{key}
102+
</span>
103+
))}
104+
</div>,
105+
);
106+
}
107+
108+
if (
109+
changeDescription.state !== null &&
110+
changeDescription.state.length !== 0
111+
) {
112+
changes.push(
113+
<div key="state" className={styles.Item}>
114+
• State changed:
115+
{changeDescription.state.map(key => (
116+
<span key={key} className={styles.Key}>
117+
{key}
118+
</span>
119+
))}
120+
</div>,
121+
);
122+
}
123+
124+
if (changes.length === 0) {
125+
changes.push(
126+
<div key="nothing" className={styles.Item}>
127+
The parent component rendered.
128+
</div>,
129+
);
130+
}
131+
132+
return (
133+
<div className={styles.Component}>
134+
<label className={styles.Label}>Why did this render?</label>
135+
{changes}
136+
</div>
137+
);
138+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.Tooltip {
2+
position: absolute;
3+
pointer-events: none;
4+
border: none;
5+
border-radius: 0.25rem;
6+
padding: 0.25rem 0.5rem;
7+
font-family: var(--font-family-sans);
8+
font-size: 12px;
9+
background-color: var(--color-tooltip-background);
10+
color: var(--color-tooltip-text);
11+
opacity: 1;
12+
/* Make sure this is above the DevTools, which are above the Overlay */
13+
z-index: 10000002;
14+
}
15+
16+
.Tooltip.hidden {
17+
opacity: 0;
18+
}
19+
20+
21+
.Container {
22+
width: -moz-max-content;
23+
width: -webkit-max-content;
24+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/** @flow */
2+
3+
import React, {useRef} from 'react';
4+
5+
import styles from './Tooltip.css';
6+
7+
const initialTooltipState = {height: 0, mouseX: 0, mouseY: 0, width: 0};
8+
9+
export default function Tooltip({children, label}: any) {
10+
const containerRef = useRef(null);
11+
const tooltipRef = useRef(null);
12+
13+
// update the position of the tooltip based on current mouse position
14+
const updateTooltipPosition = (event: SyntheticMouseEvent<*>) => {
15+
const element = tooltipRef.current;
16+
if (element != null) {
17+
// first find the mouse position
18+
const mousePosition = getMousePosition(containerRef.current, event);
19+
// use the mouse position to find the position of tooltip
20+
const {left, top} = getTooltipPosition(element, mousePosition);
21+
// update tooltip position
22+
element.style.left = left;
23+
element.style.top = top;
24+
}
25+
};
26+
27+
const onMouseMove = (event: SyntheticMouseEvent<*>) => {
28+
updateTooltipPosition(event);
29+
};
30+
31+
const tooltipClassName = label === null ? styles.hidden : '';
32+
33+
return (
34+
<div
35+
className={styles.Container}
36+
onMouseMove={onMouseMove}
37+
ref={containerRef}>
38+
<div ref={tooltipRef} className={`${styles.Tooltip} ${tooltipClassName}`}>
39+
{label}
40+
</div>
41+
{children}
42+
</div>
43+
);
44+
}
45+
46+
// Method used to find the position of the tooltip based on current mouse position
47+
function getTooltipPosition(element, mousePosition) {
48+
const {height, mouseX, mouseY, width} = mousePosition;
49+
const TOOLTIP_OFFSET_X = 5;
50+
const TOOLTIP_OFFSET_Y = 15;
51+
let top = 0;
52+
let left = 0;
53+
54+
// Let's check the vertical position.
55+
if (mouseY + TOOLTIP_OFFSET_Y + element.offsetHeight >= height) {
56+
// The tooltip doesn't fit below the mouse cursor (which is our
57+
// default strategy). Therefore we try to position it either above the
58+
// mouse cursor or finally aligned with the window's top edge.
59+
if (mouseY - TOOLTIP_OFFSET_Y - element.offsetHeight > 0) {
60+
// We position the tooltip above the mouse cursor if it fits there.
61+
top = `${mouseY - element.offsetHeight - TOOLTIP_OFFSET_Y}px`;
62+
} else {
63+
// Otherwise we align the tooltip with the window's top edge.
64+
top = '0px';
65+
}
66+
} else {
67+
top = `${mouseY + TOOLTIP_OFFSET_Y}px`;
68+
}
69+
70+
// Now let's check the horizontal position.
71+
if (mouseX + TOOLTIP_OFFSET_X + element.offsetWidth >= width) {
72+
// The tooltip doesn't fit at the right of the mouse cursor (which is
73+
// our default strategy). Therefore we try to position it either at the
74+
// left of the mouse cursor or finally aligned with the window's left
75+
// edge.
76+
if (mouseX - TOOLTIP_OFFSET_X - element.offsetWidth > 0) {
77+
// We position the tooltip at the left of the mouse cursor if it fits
78+
// there.
79+
left = `${mouseX - element.offsetWidth - TOOLTIP_OFFSET_X}px`;
80+
} else {
81+
// Otherwise, align the tooltip with the window's left edge.
82+
left = '0px';
83+
}
84+
} else {
85+
left = `${mouseX + TOOLTIP_OFFSET_X * 2}px`;
86+
}
87+
88+
return {left, top};
89+
}
90+
91+
// method used to find the current mouse position inside the container
92+
function getMousePosition(
93+
relativeContainer,
94+
mouseEvent: SyntheticMouseEvent<*>,
95+
) {
96+
if (relativeContainer !== null) {
97+
const {height, top, width} = relativeContainer.getBoundingClientRect();
98+
99+
const mouseX = mouseEvent.clientX;
100+
const mouseY = mouseEvent.clientY - top;
101+
102+
return {height, mouseX, mouseY, width};
103+
} else {
104+
return initialTooltipState;
105+
}
106+
}

packages/react-devtools-shared/src/devtools/views/Profiler/ChartNode.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ type Props = {|
1818
label: string,
1919
onClick: (event: SyntheticMouseEvent<*>) => mixed,
2020
onDoubleClick?: (event: SyntheticMouseEvent<*>) => mixed,
21+
onMouseEnter: (event: SyntheticMouseEvent<*>) => mixed,
22+
onMouseLeave: (event: SyntheticMouseEvent<*>) => mixed,
2123
placeLabelAboveNode?: boolean,
2224
textStyle?: Object,
2325
width: number,
@@ -33,6 +35,8 @@ export default function ChartNode({
3335
isDimmed = false,
3436
label,
3537
onClick,
38+
onMouseEnter,
39+
onMouseLeave,
3640
onDoubleClick,
3741
textStyle,
3842
width,
@@ -41,12 +45,13 @@ export default function ChartNode({
4145
}: Props) {
4246
return (
4347
<g className={styles.Group} transform={`translate(${x},${y})`}>
44-
<title>{label}</title>
4548
<rect
4649
width={width}
4750
height={height}
4851
fill={color}
4952
onClick={onClick}
53+
onMouseEnter={onMouseEnter}
54+
onMouseLeave={onMouseLeave}
5055
onDoubleClick={onDoubleClick}
5156
className={styles.Rect}
5257
style={{

0 commit comments

Comments
 (0)