@samitouri / QOS-React / commits / 44e5f5e645

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 -}