Add fiber summary tooltip to devtools profiling (#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
Moji Izadmehr committed
Feb 19, 2020 at 18:44 UTC
44e5f5e6451cf192af5dee3aa1f3a87119fc231e
13 files changed
+527
-176
packages/react-devtools-shared/src/devtools/views/Components/ProfilerWhatChanged.css
new
+30
@@ -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
+}
packages/react-devtools-shared/src/devtools/views/Components/ProfilerWhatChanged.js
new
+138
@@ -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
+}
packages/react-devtools-shared/src/devtools/views/Components/Tooltip.css
new
+24
@@ -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
+}
packages/react-devtools-shared/src/devtools/views/Components/Tooltip.js
new
+106
@@ -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
+6
-1
@@ -18,6 +18,8 @@ type Props = {|
18
label: string,
19
onClick: (event: SyntheticMouseEvent<*>) => mixed,
20
onDoubleClick?: (event: SyntheticMouseEvent<*>) => mixed,
21
+ onMouseEnter: (event: SyntheticMouseEvent<*>) => mixed,
22
+ onMouseLeave: (event: SyntheticMouseEvent<*>) => mixed,
23
placeLabelAboveNode?: boolean,
24
textStyle?: Object,
25
width: number,
@@ -33,6 +35,8 @@ export default function ChartNode({
35
isDimmed = false,
36
label,
37
onClick,
38
+ onMouseEnter,
39
+ onMouseLeave,
40
onDoubleClick,
41
textStyle,
42
width,
@@ -41,12 +45,13 @@ export default function ChartNode({
45
}: Props) {
46
return (
47
<g className={styles.Group} transform={`translate(${x},${y})`}>
44
- <title>{label}</title>
48
<rect
49
width={width}
50
height={height}
51
fill={color}
52
onClick={onClick}
53
+ onMouseEnter={onMouseEnter}
54
+ onMouseLeave={onMouseLeave}
55
onDoubleClick={onDoubleClick}
56
className={styles.Rect}
57
style={{
packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraph.js
+41
-11
@@ -7,23 +7,33 @@
7
* @flow
8
*/
9
10
-import React, {forwardRef, useCallback, useContext, useMemo} from 'react';
10
+import React, {
11
+ forwardRef,
12
+ useCallback,
13
+ useContext,
14
+ useMemo,
15
+ useState,
16
+} from 'react';
17
import AutoSizer from 'react-virtualized-auto-sizer';
18
import {FixedSizeList} from 'react-window';
19
import {ProfilerContext} from './ProfilerContext';
20
import NoCommitData from './NoCommitData';
21
import CommitFlamegraphListItem from './CommitFlamegraphListItem';
22
+import HoveredFiberInfo from './HoveredFiberInfo';
23
import {scale} from './utils';
24
import {StoreContext} from '../context';
25
import {SettingsContext} from '../Settings/SettingsContext';
26
+import Tooltip from '../Components/Tooltip';
27
28
import styles from './CommitFlamegraph.css';
29
30
+import type {TooltipFiberData} from './HoveredFiberInfo';
31
import type {ChartData, ChartNode} from './FlamegraphChartBuilder';
32
import type {CommitTree} from './types';
33
34
export type ItemData = {|
35
chartData: ChartData,
36
+ hoverFiber: (fiberData: TooltipFiberData | null) => void,
37
scaleX: (value: number, fallbackValue: number) => number,
38
selectedChartNode: ChartNode | null,
39
selectedChartNodeIndex: number,
@@ -91,6 +101,7 @@ type Props = {|
101
|};
102
103
function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
104
+ const [hoveredFiberData, hoverFiber] = useState<number | null>(null);
105
const {lineHeight} = useContext(SettingsContext);
106
const {selectFiber, selectedFiberID} = useContext(ProfilerContext);
107
@@ -118,6 +129,7 @@ function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
129
const itemData = useMemo<ItemData>(
130
() => ({
131
chartData,
132
+ hoverFiber,
133
scaleX: scale(
134
0,
135
selectedChartNode !== null
@@ -131,19 +143,37 @@ function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
143
selectFiber,
144
width,
145
}),
134
- [chartData, selectedChartNode, selectedChartNodeIndex, selectFiber, width],
146
+ [
147
+ chartData,
148
+ hoverFiber,
149
+ selectedChartNode,
150
+ selectedChartNodeIndex,
151
+ selectFiber,
152
+ width,
153
+ ],
154
+ );
155
+
156
+ // Tooltip used to show summary of fiber info on hover
157
+ const tooltipLabel = useMemo(
158
+ () =>
159
+ hoveredFiberData !== null ? (
160
+ <HoveredFiberInfo fiberData={hoveredFiberData} />
161
+ ) : null,
162
+ [hoveredFiberData],
163
);
164
165
return (
138
- <FixedSizeList
139
- height={height}
140
- innerElementType={InnerElementType}
141
- itemCount={chartData.depth}
142
- itemData={itemData}
143
- itemSize={lineHeight}
144
- width={width}>
145
- {CommitFlamegraphListItem}
146
- </FixedSizeList>
166
+ <Tooltip label={tooltipLabel}>
167
+ <FixedSizeList
168
+ height={height}
169
+ innerElementType={InnerElementType}
170
+ itemCount={chartData.depth}
171
+ itemData={itemData}
172
+ itemSize={lineHeight}
173
+ width={width}>
174
+ {CommitFlamegraphListItem}
175
+ </FixedSizeList>
176
+ </Tooltip>
177
);
178
}
179
packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraphListItem.js
+14
@@ -14,6 +14,7 @@ import {getGradientColor} from './utils';
14
import ChartNode from './ChartNode';
15
import {SettingsContext} from '../Settings/SettingsContext';
16
17
+import type {ChartNode as ChartNodeType} from './FlamegraphChartBuilder';
18
import type {ItemData} from './CommitFlamegraph';
19
20
type Props = {
@@ -26,6 +27,7 @@ type Props = {
27
function CommitFlamegraphListItem({data, index, style}: Props) {
28
const {
29
chartData,
30
+ hoverFiber,
31
scaleX,
32
selectedChartNode,
33
selectedChartNodeIndex,
@@ -35,6 +37,7 @@ function CommitFlamegraphListItem({data, index, style}: Props) {
37
const {renderPathNodes, maxSelfDuration, rows} = chartData;
38
39
const {lineHeight} = useContext(SettingsContext);
40
+
41
const handleClick = useCallback(
42
(event: SyntheticMouseEvent<*>, id: number, name: string) => {
43
event.stopPropagation();
@@ -43,6 +46,15 @@ function CommitFlamegraphListItem({data, index, style}: Props) {
46
[selectFiber],
47
);
48
49
+ const handleMouseEnter = (nodeData: ChartNodeType) => {
50
+ const {id, name} = nodeData;
51
+ hoverFiber({id, name});
52
+ };
53
+
54
+ const handleMouseLeave = () => {
55
+ hoverFiber(null);
56
+ };
57
+
58
// List items are absolutely positioned using the CSS "top" attribute.
59
// The "left" value will always be 0.
60
// Since height is fixed, and width is based on the node's duration,
@@ -104,6 +116,8 @@ function CommitFlamegraphListItem({data, index, style}: Props) {
116
key={id}
117
label={label}
118
onClick={event => handleClick(event, id, name)}
119
+ onMouseEnter={() => handleMouseEnter(chartNode)}
120
+ onMouseLeave={handleMouseLeave}
121
textStyle={{color: textColor}}
122
width={nodeWidth}
123
x={nodeOffset - selectedNodeOffset}
packages/react-devtools-shared/src/devtools/views/Profiler/CommitRanked.js
+28
-10
@@ -7,23 +7,27 @@
7
* @flow
8
*/
9
10
-import React, {useCallback, useContext, useMemo} from 'react';
10
+import React, {useCallback, useContext, useMemo, useState} from 'react';
11
import AutoSizer from 'react-virtualized-auto-sizer';
12
import {FixedSizeList} from 'react-window';
13
import {ProfilerContext} from './ProfilerContext';
14
import NoCommitData from './NoCommitData';
15
import CommitRankedListItem from './CommitRankedListItem';
16
+import HoveredFiberInfo from './HoveredFiberInfo';
17
import {scale} from './utils';
18
import {StoreContext} from '../context';
19
import {SettingsContext} from '../Settings/SettingsContext';
20
+import Tooltip from '../Components/Tooltip';
21
22
import styles from './CommitRanked.css';
23
24
+import type {TooltipFiberData} from './HoveredFiberInfo';
25
import type {ChartData} from './RankedChartBuilder';
26
import type {CommitTree} from './types';
27
28
export type ItemData = {|
29
chartData: ChartData,
30
+ hoverFiber: (fiberData: TooltipFiberData | null) => void,
31
scaleX: (value: number, fallbackValue: number) => number,
32
selectedFiberID: number | null,
33
selectedFiberIndex: number,
@@ -89,6 +93,7 @@ type Props = {|
93
|};
94
95
function CommitRanked({chartData, commitTree, height, width}: Props) {
96
+ const [hoveredFiberData, hoverFiber] = useState<number | null>(null);
97
const {lineHeight} = useContext(SettingsContext);
98
const {selectedFiberID, selectFiber} = useContext(ProfilerContext);
99
@@ -100,6 +105,7 @@ function CommitRanked({chartData, commitTree, height, width}: Props) {
105
const itemData = useMemo<ItemData>(
106
() => ({
107
chartData,
108
+ hoverFiber,
109
scaleX: scale(0, chartData.nodes[selectedFiberIndex].value, 0, width),
110
selectedFiberID,
111
selectedFiberIndex,
@@ -109,16 +115,28 @@ function CommitRanked({chartData, commitTree, height, width}: Props) {
115
[chartData, selectedFiberID, selectedFiberIndex, selectFiber, width],
116
);
117
118
+ // Tooltip used to show summary of fiber info on hover
119
+ const tooltipLabel = useMemo(
120
+ () =>
121
+ hoveredFiberData !== null ? (
122
+ <HoveredFiberInfo fiberData={hoveredFiberData} />
123
+ ) : null,
124
+ [hoveredFiberData],
125
+ );
126
+
127
return (
113
- <FixedSizeList
114
- height={height}
115
- innerElementType="svg"
116
- itemCount={chartData.nodes.length}
117
- itemData={itemData}
118
- itemSize={lineHeight}
119
- width={width}>
120
- {CommitRankedListItem}
121
- </FixedSizeList>
128
+ <Tooltip label={tooltipLabel}>
129
+ <FixedSizeList
130
+ height={height}
131
+ innerElementType="svg"
132
+ itemCount={chartData.nodes.length}
133
+ itemData={itemData}
134
+ itemSize={lineHeight}
135
+ width={width}>
136
+ {CommitRankedListItem}
137
+ </FixedSizeList>
138
+ >
139
+ </Tooltip>
140
);
141
}
142
packages/react-devtools-shared/src/devtools/views/Profiler/CommitRankedListItem.js
+21
-2
@@ -24,7 +24,14 @@ type Props = {
24
};
25
26
function CommitRankedListItem({data, index, style}: Props) {
27
- const {chartData, scaleX, selectedFiberIndex, selectFiber, width} = data;
27
+ const {
28
+ chartData,
29
+ hoverFiber,
30
+ scaleX,
31
+ selectedFiberIndex,
32
+ selectFiber,
33
+ width,
34
+ } = data;
35
36
const node = chartData.nodes[index];
37
@@ -33,11 +40,21 @@ function CommitRankedListItem({data, index, style}: Props) {
40
const handleClick = useCallback(
41
event => {
42
event.stopPropagation();
36
- selectFiber(node.id, node.name);
43
+ const {id, name} = node;
44
+ selectFiber(id, name);
45
},
46
[node, selectFiber],
47
);
48
49
+ const handleMouseEnter = () => {
50
+ const {id, name} = node;
51
+ hoverFiber({id, name});
52
+ };
53
+
54
+ const handleMouseLeave = () => {
55
+ hoverFiber(null);
56
+ };
57
+
58
// List items are absolutely positioned using the CSS "top" attribute.
59
// The "left" value will always be 0.
60
// Since height is fixed, and width is based on the node's duration,
@@ -52,6 +69,8 @@ function CommitRankedListItem({data, index, style}: Props) {
69
key={node.id}
70
label={node.label}
71
onClick={handleClick}
72
+ onMouseEnter={handleMouseEnter}
73
+ onMouseLeave={handleMouseLeave}
74
width={Math.max(minBarWidth, scaleX(node.value, width))}
75
x={0}
76
y={top}
packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.css
new
+38
@@ -0,0 +1,38 @@
1
+.Toolbar {
2
+ height: 2.25rem;
3
+ padding: 0 0.5rem;
4
+ flex: 0 0 auto;
5
+ display: flex;
6
+ align-items: center;
7
+ border-bottom: 1px solid var(--color-border);
8
+}
9
+
10
+.Content {
11
+ padding: 0.5rem;
12
+ user-select: none;
13
+ overflow-y: auto;
14
+}
15
+
16
+.Component {
17
+ flex: 1;
18
+ font-weight: bold;
19
+ font-family: var(--font-family-monospace);
20
+ font-size: var(--font-size-monospace-normal);
21
+ white-space: nowrap;
22
+ overflow-x: hidden;
23
+ text-overflow: ellipsis;
24
+}
25
+
26
+.Label {
27
+ font-weight: bold;
28
+ margin-bottom: 0.5rem;
29
+}
30
+
31
+.CurrentCommit {
32
+ display: block;
33
+ width: 100%;
34
+ text-align: left;
35
+ background: none;
36
+ border: none;
37
+ padding: 0.25rem 0.5rem;
38
+}
packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js
new
+77
@@ -0,0 +1,77 @@
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, {Fragment, useContext} from 'react';
11
+import {ProfilerContext} from './ProfilerContext';
12
+import {formatDuration, formatTime} from './utils';
13
+import ProfilerWhatChanged from '../Components/ProfilerWhatChanged';
14
+import {StoreContext} from '../context';
15
+
16
+import styles from './HoveredFiberInfo.css';
17
+
18
+import type {ChartNode} from './FlamegraphChartBuilder';
19
+
20
+export type TooltipFiberData = {|
21
+ id: number,
22
+ name: string,
23
+|};
24
+
25
+export type Props = {
26
+ fiberData: ChartNode,
27
+};
28
+
29
+export default function HoveredFiberInfo({fiberData}: Props) {
30
+ const {profilerStore} = useContext(StoreContext);
31
+ const {rootID, selectedCommitIndex} = useContext(ProfilerContext);
32
+
33
+ const {id, name} = fiberData;
34
+ const {profilingCache} = profilerStore;
35
+
36
+ const commitIndices = profilingCache.getFiberCommits({
37
+ fiberID: ((id: any): number),
38
+ rootID: ((rootID: any): number),
39
+ });
40
+
41
+ let renderDurationInfo;
42
+ let i = 0;
43
+ for (i = 0; i < commitIndices.length; i++) {
44
+ const commitIndex = commitIndices[i];
45
+ if (selectedCommitIndex === commitIndex) {
46
+ const {duration, timestamp} = profilerStore.getCommitData(
47
+ ((rootID: any): number),
48
+ commitIndex,
49
+ );
50
+
51
+ renderDurationInfo = (
52
+ <Fragment>
53
+ <label className={styles.Label}>Rendered at:</label>
54
+ <div key={commitIndex} className={styles.CurrentCommit}>
55
+ {formatTime(timestamp)}s for {formatDuration(duration)}ms
56
+ </div>
57
+ </Fragment>
58
+ );
59
+
60
+ break;
61
+ }
62
+ }
63
+
64
+ return (
65
+ <Fragment>
66
+ <div className={styles.Toolbar}>
67
+ <div className={styles.Component}>{name}</div>
68
+ </div>
69
+ <div className={styles.Content}>
70
+ <ProfilerWhatChanged fiberID={((id: any): number)} />
71
+ {renderDurationInfo || (
72
+ <div>Did not render during this profiling session.</div>
73
+ )}
74
+ </div>
75
+ </Fragment>
76
+ );
77
+}
packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css
+2
-19
@@ -42,6 +42,7 @@
42
padding: 0.25rem 0.5rem;
43
color: var(--color-text);
44
}
45
+
46
.Commit:focus,
47
.Commit:hover {
48
outline: none;
@@ -52,25 +53,7 @@
53
background-color: var(--color-background-selected);
54
color: var(--color-text-selected);
55
}
56
+
57
.CurrentCommit:focus {
58
outline: none;
59
}
58
-
59
-.WhatChangedItem {
60
- margin-top: 0.25rem;
61
-}
62
-
63
-.WhatChangedKey {
64
- font-family: var(--font-family-monospace);
65
- font-size: var(--font-size-monospace-small);
66
- line-height: 1;
67
-}
68
-.WhatChangedKey:first-of-type::before {
69
- content: ' (';
70
-}
71
-.WhatChangedKey::after {
72
- content: ', ';
73
-}
74
-.WhatChangedKey:last-of-type::after {
75
- content: ')';
76
-}
packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js
+2
-133
@@ -8,7 +8,7 @@
8
*/
9
10
import React, {Fragment, useContext} from 'react';
11
-import ProfilerStore from 'react-devtools-shared/src/devtools/ProfilerStore';
11
+import ProfilerWhatChanged from '../Components/ProfilerWhatChanged';
12
import {ProfilerContext} from './ProfilerContext';
13
import {formatDuration, formatTime} from './utils';
14
import {StoreContext} from '../context';
@@ -75,12 +75,7 @@ export default function SidebarSelectedFiberInfo(_: Props) {
75
</Button>
76
</div>
77
<div className={styles.Content}>
78
- <WhatChanged
79
- commitIndex={((selectedCommitIndex: any): number)}
80
- fiberID={((selectedFiberID: any): number)}
81
- profilerStore={profilerStore}
82
- rootID={((rootID: any): number)}
83
- />
78
+ <ProfilerWhatChanged fiberID={((selectedFiberID: any): number)} />
79
{listItems.length > 0 && (
80
<Fragment>
81
<label className={styles.Label}>Rendered at</label>: {listItems}
@@ -93,129 +88,3 @@ export default function SidebarSelectedFiberInfo(_: Props) {
88
</Fragment>
89
);
90
}
96
-
97
-type WhatChangedProps = {|
98
- commitIndex: number | null,
99
- fiberID: number,
100
- profilerStore: ProfilerStore,
101
- rootID: number,
102
-|};
103
-
104
-function WhatChanged({
105
- commitIndex,
106
- fiberID,
107
- profilerStore,
108
- rootID,
109
-}: WhatChangedProps) {
110
- // TRICKY
111
- // Handle edge case where no commit is selected because of a min-duration filter update.
112
- // If the commit index is null, suspending for data below would throw an error.
113
- // TODO (ProfilerContext) This check should not be necessary.
114
- if (commitIndex === null) {
115
- return null;
116
- }
117
-
118
- const {changeDescriptions} = profilerStore.getCommitData(
119
- ((rootID: any): number),
120
- commitIndex,
121
- );
122
- if (changeDescriptions === null) {
123
- return null;
124
- }
125
-
126
- const changeDescription = changeDescriptions.get(fiberID);
127
- if (changeDescription == null) {
128
- return null;
129
- }
130
-
131
- if (changeDescription.isFirstMount) {
132
- return (
133
- <div className={styles.WhatChanged}>
134
- <label className={styles.Label}>Why did this render?</label>
135
- <div className={styles.WhatChangedItem}>
136
- This is the first time the component rendered.
137
- </div>
138
- </div>
139
- );
140
- }
141
-
142
- const changes = [];
143
-
144
- if (changeDescription.context === true) {
145
- changes.push(
146
- <div key="context" className={styles.WhatChangedItem}>
147
- • Context changed
148
- </div>,
149
- );
150
- } else if (
151
- typeof changeDescription.context === 'object' &&
152
- changeDescription.context !== null &&
153
- changeDescription.context.length !== 0
154
- ) {
155
- changes.push(
156
- <div key="context" className={styles.WhatChangedItem}>
157
- • Context changed:
158
- {changeDescription.context.map(key => (
159
- <span key={key} className={styles.WhatChangedKey}>
160
- {key}
161
- </span>
162
- ))}
163
- </div>,
164
- );
165
- }
166
-
167
- if (changeDescription.didHooksChange) {
168
- changes.push(
169
- <div key="hooks" className={styles.WhatChangedItem}>
170
- • Hooks changed
171
- </div>,
172
- );
173
- }
174
-
175
- if (
176
- changeDescription.props !== null &&
177
- changeDescription.props.length !== 0
178
- ) {
179
- changes.push(
180
- <div key="props" className={styles.WhatChangedItem}>
181
- • Props changed:
182
- {changeDescription.props.map(key => (
183
- <span key={key} className={styles.WhatChangedKey}>
184
- {key}
185
- </span>
186
- ))}
187
- </div>,
188
- );
189
- }
190
-
191
- if (
192
- changeDescription.state !== null &&
193
- changeDescription.state.length !== 0
194
- ) {
195
- changes.push(
196
- <div key="state" className={styles.WhatChangedItem}>
197
- • State changed:
198
- {changeDescription.state.map(key => (
199
- <span key={key} className={styles.WhatChangedKey}>
200
- {key}
201
- </span>
202
- ))}
203
- </div>,
204
- );
205
- }
206
-
207
- if (changes.length === 0) {
208
- changes.push(
209
- <div key="nothing" className={styles.WhatChangedItem}>
210
- The parent component rendered.
211
- </div>,
212
- );
213
- }
214
-
215
- return (
216
- <div className={styles.WhatChanged}>
217
- <label className={styles.Label}>Why did this render?</label>
218
- {changes}
219
- </div>
220
- );
221
-}