@samitouri / QOS-React-1 / commits / e7d213dfb0

feat[react-devtools]: display forget badge for components in profiling session (#29014)

# Summary - `compiledWithForget` field for nodes is now propagated from the backend to frontend profiler stores - Corresponding node with such field will have a `✨` prefix displayed before its displayName <img width="1728" alt="Screenshot 2024-05-07 at 15 05 37" src="https://github.com/facebook/react/assets/28902667/fe044d40-52cb-4169-867d-5a2d72e3275b"> - Badges are now displayed on the right panel when some fiber is selected in a specific commit <img width="1728" alt="Screenshot 2024-05-07 at 15 05 50" src="https://github.com/facebook/react/assets/28902667/297ba5ca-404d-4172-b9bf-bfed7978afe5"> - Badges are also displayed when user hovers over some node in the tree <img width="1728" alt="Screenshot 2024-05-07 at 15 25 22" src="https://github.com/facebook/react/assets/28902667/bee47884-61d1-46b6-a483-717fc148893a">

Ruslan Lesiutin committed May 7, 2024 at 16:39 UTC e7d213dfb0760dc9f6506fca6d6cfbac708a40e2
16 files changed +170 -68
packages/react-devtools-shared/src/__tests__/profilingCharts-test.js
+18 -18
@@ -124,8 +124,8 @@ describe('profiling charts', () => {
124 "actualDuration": 0,
125 "didRender": true,
126 "id": 5,
127 - "label": "Memo(Child) key="third" (<0.1ms of <0.1ms)",
128 - "name": "Memo(Child)",
127 + "label": "Child (Memo) key="third" (<0.1ms of <0.1ms)",
128 + "name": "Child",
129 "offset": 15,
130 "selfDuration": 0,
131 "treeBaseDuration": 0,
@@ -134,8 +134,8 @@ describe('profiling charts', () => {
134 "actualDuration": 2,
135 "didRender": true,
136 "id": 4,
137 - "label": "Memo(Child) key="second" (2ms of 2ms)",
138 - "name": "Memo(Child)",
137 + "label": "Child (Memo) key="second" (2ms of 2ms)",
138 + "name": "Child",
139 "offset": 13,
140 "selfDuration": 2,
141 "treeBaseDuration": 2,
@@ -144,8 +144,8 @@ describe('profiling charts', () => {
144 "actualDuration": 3,
145 "didRender": true,
146 "id": 3,
147 - "label": "Memo(Child) key="first" (3ms of 3ms)",
148 - "name": "Memo(Child)",
147 + "label": "Child (Memo) key="first" (3ms of 3ms)",
148 + "name": "Child",
149 "offset": 10,
150 "selfDuration": 3,
151 "treeBaseDuration": 3,
@@ -175,8 +175,8 @@ describe('profiling charts', () => {
175 "actualDuration": 0,
176 "didRender": false,
177 "id": 5,
178 - "label": "Memo(Child) key="third"",
179 - "name": "Memo(Child)",
178 + "label": "Child (Memo) key="third"",
179 + "name": "Child",
180 "offset": 15,
181 "selfDuration": 0,
182 "treeBaseDuration": 0,
@@ -185,8 +185,8 @@ describe('profiling charts', () => {
185 "actualDuration": 0,
186 "didRender": false,
187 "id": 4,
188 - "label": "Memo(Child) key="second"",
189 - "name": "Memo(Child)",
188 + "label": "Child (Memo) key="second"",
189 + "name": "Child",
190 "offset": 13,
191 "selfDuration": 0,
192 "treeBaseDuration": 2,
@@ -195,8 +195,8 @@ describe('profiling charts', () => {
195 "actualDuration": 0,
196 "didRender": false,
197 "id": 3,
198 - "label": "Memo(Child) key="first"",
199 - "name": "Memo(Child)",
198 + "label": "Child (Memo) key="first"",
199 + "name": "Child",
200 "offset": 10,
201 "selfDuration": 0,
202 "treeBaseDuration": 3,
@@ -264,20 +264,20 @@ describe('profiling charts', () => {
264 },
265 {
266 "id": 3,
267 - "label": "Memo(Child) (Memo) key="first" (3ms)",
268 - "name": "Memo(Child)",
267 + "label": "Child (Memo) key="first" (3ms)",
268 + "name": "Child",
269 "value": 3,
270 },
271 {
272 "id": 4,
273 - "label": "Memo(Child) (Memo) key="second" (2ms)",
274 - "name": "Memo(Child)",
273 + "label": "Child (Memo) key="second" (2ms)",
274 + "name": "Child",
275 "value": 2,
276 },
277 {
278 "id": 5,
279 - "label": "Memo(Child) (Memo) key="third" (<0.1ms)",
280 - "name": "Memo(Child)",
279 + "label": "Child (Memo) key="third" (<0.1ms)",
280 + "name": "Child",
281 "value": 0,
282 },
283 ]
packages/react-devtools-shared/src/devtools/ProfilerStore.js
+1
@@ -214,6 +214,7 @@ export default class ProfilerStore extends EventEmitter<{
214 hocDisplayNames: element.hocDisplayNames,
215 key: element.key,
216 type: element.type,
217 + compiledWithForget: element.compiledWithForget,
218 };
219 profilingSnapshots.set(elementID, snapshotNode);
220
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.css
-1
@@ -1,5 +1,4 @@
1 .Root {
2 - padding: 0.25rem;
2 user-select: none;
3 display: inline-flex;
4 }
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js
+12 -5
@@ -7,8 +7,6 @@
7 * @flow
8 */
9
10 -import type {Element} from 'react-devtools-shared/src/frontend/types';
11 -
10 import * as React from 'react';
11
12 import Badge from './Badge';
@@ -17,11 +15,20 @@ import ForgetBadge from './ForgetBadge';
15 import styles from './InspectedElementBadges.css';
16
17 type Props = {
20 - element: Element,
18 + hocDisplayNames: null | Array<string>,
19 + compiledWithForget: boolean,
20 };
21
23 -export default function InspectedElementBadges({element}: Props): React.Node {
24 - const {hocDisplayNames, compiledWithForget} = element;
22 +export default function InspectedElementBadges({
23 + hocDisplayNames,
24 + compiledWithForget,
25 +}: Props): React.Node {
26 + if (
27 + !compiledWithForget &&
28 + (hocDisplayNames == null || hocDisplayNames.length === 0)
29 + ) {
30 + return null;
31 + }
32
33 return (
34 <div className={styles.Root}>
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.css
+4
@@ -25,6 +25,10 @@
25 line-height: var(--line-height-data);
26 }
27
28 +.InspectedElementBadgesContainer {
29 + padding: 0.25rem;
30 +}
31 +
32 .Owner {
33 border-radius: 0.25rem;
34 padding: 0.125rem 0.25rem;
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+6 -1
@@ -85,7 +85,12 @@ export default function InspectedElementView({
85 return (
86 <Fragment>
87 <div className={styles.InspectedElement}>
88 - <InspectedElementBadges element={element} />
88 + <div className={styles.InspectedElementBadgesContainer}>
89 + <InspectedElementBadges
90 + hocDisplayNames={element.hocDisplayNames}
91 + compiledWithForget={element.compiledWithForget}
92 + />
93 + </div>
94
95 <InspectedElementPropsTree
96 bridge={bridge}
packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js
+12 -3
@@ -17,7 +17,10 @@ import {
17 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
18 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
19 } from 'react-devtools-shared/src/constants';
20 -import {utfDecodeStringWithRanges} from 'react-devtools-shared/src/utils';
20 +import {
21 + parseElementDisplayNameFromBackend,
22 + utfDecodeStringWithRanges,
23 +} from 'react-devtools-shared/src/utils';
24 import {ElementTypeRoot} from 'react-devtools-shared/src/frontend/types';
25 import ProfilerStore from 'react-devtools-shared/src/devtools/ProfilerStore';
26
@@ -133,6 +136,7 @@ function recursivelyInitializeTree(
136 id,
137 ): any): number),
138 type: node.type,
139 + compiledWithForget: node.compiledWithForget,
140 });
141
142 node.children.forEach(childID =>
@@ -214,6 +218,7 @@ function updateTree(
218 parentID: 0,
219 treeBaseDuration: 0, // This will be updated by a subsequent operation
220 type,
221 + compiledWithForget: false,
222 };
223
224 nodes.set(id, node);
@@ -241,15 +246,19 @@ function updateTree(
246 const parentNode = getClonedNode(parentID);
247 parentNode.children = parentNode.children.concat(id);
248
249 + const {formattedDisplayName, hocDisplayNames, compiledWithForget} =
250 + parseElementDisplayNameFromBackend(displayName, type);
251 +
252 const node: CommitTreeNode = {
253 children: [],
246 - displayName,
247 - hocDisplayNames: null,
254 + displayName: formattedDisplayName,
255 + hocDisplayNames: hocDisplayNames,
256 id,
257 key,
258 parentID,
259 treeBaseDuration: 0, // This will be updated by a subsequent operation
260 type,
261 + compiledWithForget,
262 };
263
264 nodes.set(id, node);
packages/react-devtools-shared/src/devtools/views/Profiler/FlamegraphChartBuilder.js
+11 -3
@@ -75,8 +75,14 @@ export function getChartData({
75 throw Error(`Could not find node with id "${id}" in commit tree`);
76 }
77
78 - const {children, displayName, hocDisplayNames, key, treeBaseDuration} =
79 - node;
78 + const {
79 + children,
80 + displayName,
81 + hocDisplayNames,
82 + key,
83 + treeBaseDuration,
84 + compiledWithForget,
85 + } = node;
86
87 const actualDuration = fiberActualDurations.get(id) || 0;
88 const selfDuration = fiberSelfDurations.get(id) || 0;
@@ -86,11 +92,13 @@ export function getChartData({
92 const maybeKey = key !== null ? ` key="${key}"` : '';
93
94 let maybeBadge = '';
95 + const maybeForgetBadge = compiledWithForget ? '✨ ' : '';
96 +
97 if (hocDisplayNames !== null && hocDisplayNames.length > 0) {
98 maybeBadge = ` (${hocDisplayNames[0]})`;
99 }
100
93 - let label = `${name}${maybeBadge}${maybeKey}`;
101 + let label = `${maybeForgetBadge}${name}${maybeBadge}${maybeKey}`;
102 if (didRender) {
103 label += ` (${formatDuration(selfDuration)}ms of ${formatDuration(
104 actualDuration,
packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.css
+14 -3
@@ -1,9 +1,19 @@
1 .Toolbar {
2 padding: 0.25rem 0;
3 - margin-bottom: 0.25rem;
3 flex: 0 0 auto;
4 display: flex;
6 - align-items: center;
5 + flex-direction: column;
6 + gap: 0.25rem;
7 +}
8 +
9 +.BadgesContainer {
10 + display: flex;
11 + flex-direction: column;
12 + gap: 0.25rem;
13 +}
14 +
15 +.BadgesContainer:not(:empty) {
16 + padding-bottom: 0.25rem;
17 border-bottom: 1px solid var(--color-border);
18 }
19
@@ -20,10 +30,11 @@
30 white-space: nowrap;
31 overflow-x: hidden;
32 text-overflow: ellipsis;
33 + padding-bottom: 0.25rem;
34 + border-bottom: 1px solid var(--color-border);
35 }
36
37 .CurrentCommit {
26 - margin: 0.25rem 0;
38 display: block;
39 width: 100%;
40 text-align: left;
packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js
+37 -7
@@ -9,6 +9,8 @@
9
10 import * as React from 'react';
11 import {Fragment, useContext} from 'react';
12 +
13 +import InspectedElementBadges from '../Components/InspectedElementBadges';
14 import {ProfilerContext} from './ProfilerContext';
15 import {formatDuration} from './utils';
16 import WhatChanged from './WhatChanged';
@@ -34,11 +36,21 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
36 const {id, name} = fiberData;
37 const {profilingCache} = profilerStore;
38
39 + if (rootID === null || selectedCommitIndex === null) {
40 + return null;
41 + }
42 +
43 const commitIndices = profilingCache.getFiberCommits({
38 - fiberID: ((id: any): number),
39 - rootID: ((rootID: any): number),
44 + fiberID: id,
45 + rootID,
46 });
47
48 + const {nodes} = profilingCache.getCommitTree({
49 + rootID,
50 + commitIndex: selectedCommitIndex,
51 + });
52 + const node = nodes.get(id);
53 +
54 let renderDurationInfo = null;
55 let i = 0;
56 for (i = 0; i < commitIndices.length; i++) {
@@ -51,7 +63,8 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
63
64 renderDurationInfo = (
65 <div key={commitIndex} className={styles.CurrentCommit}>
54 - {formatDuration(selfDuration)}ms of {formatDuration(actualDuration)}ms
66 + <strong>Duration:</strong> {formatDuration(selfDuration)}ms of{' '}
67 + {formatDuration(actualDuration)}ms
68 </div>
69 );
70
@@ -63,10 +76,27 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
76 <Fragment>
77 <div className={styles.Toolbar}>
78 <div className={styles.Component}>{name}</div>
66 - </div>
67 - <div className={styles.Content}>
68 - {renderDurationInfo || <div>Did not render.</div>}
69 - <WhatChanged fiberID={((id: any): number)} />
79 +
80 + {node != null && (
81 + <div className={styles.BadgesContainer}>
82 + <InspectedElementBadges
83 + hocDisplayNames={node.hocDisplayNames}
84 + compiledWithForget={node.compiledWithForget}
85 + />
86 +
87 + {node.compiledWithForget && (
88 + <div>
89 + ✨ This component has been auto-memoized by the React Compiler.
90 + </div>
91 + )}
92 + </div>
93 + )}
94 +
95 + <div className={styles.Content}>
96 + {renderDurationInfo || <div>Did not render.</div>}
97 +
98 + <WhatChanged fiberID={id} />
99 + </div>
100 </div>
101 </Fragment>
102 );
packages/react-devtools-shared/src/devtools/views/Profiler/RankedChartBuilder.js
+3 -2
@@ -61,7 +61,7 @@ export function getChartData({
61 throw Error(`Could not find node with id "${id}" in commit tree`);
62 }
63
64 - const {displayName, key, parentID, type} = node;
64 + const {displayName, key, parentID, type, compiledWithForget} = node;
65
66 // Don't show the root node in this chart.
67 if (parentID === 0) {
@@ -72,6 +72,7 @@ export function getChartData({
72
73 const name = displayName || 'Anonymous';
74 const maybeKey = key !== null ? ` key="${key}"` : '';
75 + const maybeForgetBadge = compiledWithForget ? '✨ ' : '';
76
77 let maybeBadge = '';
78 if (type === ElementTypeForwardRef) {
@@ -80,7 +81,7 @@ export function getChartData({
81 maybeBadge = ' (Memo)';
82 }
83
83 - const label = `${name}${maybeBadge}${maybeKey} (${formatDuration(
84 + const label = `${maybeForgetBadge}${name}${maybeBadge}${maybeKey} (${formatDuration(
85 selfDuration,
86 )}ms)`;
87 chartNodes.push({
packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css
+3
@@ -11,6 +11,9 @@
11 padding: 0.5rem;
12 user-select: none;
13 overflow-y: auto;
14 + display: flex;
15 + flex-direction: column;
16 + gap: 0.5rem;
17 }
18
19 .Component {
packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js
+40 -19
@@ -9,18 +9,18 @@
9
10 import * as React from 'react';
11 import {Fragment, useContext, useEffect, useRef} from 'react';
12 +
13 import WhatChanged from './WhatChanged';
14 import {ProfilerContext} from './ProfilerContext';
15 import {formatDuration, formatTime} from './utils';
16 import {StoreContext} from '../context';
17 import Button from '../Button';
18 import ButtonIcon from '../ButtonIcon';
19 +import InspectedElementBadges from '../Components/InspectedElementBadges';
20
21 import styles from './SidebarSelectedFiberInfo.css';
22
21 -export type Props = {};
22 -
23 -export default function SidebarSelectedFiberInfo(_: Props): React.Node {
23 +export default function SidebarSelectedFiberInfo(): React.Node {
24 const {profilerStore} = useContext(StoreContext);
25 const {
26 rootID,
@@ -33,11 +33,36 @@ export default function SidebarSelectedFiberInfo(_: Props): React.Node {
33 const {profilingCache} = profilerStore;
34 const selectedListItemRef = useRef<HTMLElement | null>(null);
35
36 + useEffect(() => {
37 + const selectedElement = selectedListItemRef.current;
38 + if (
39 + selectedElement !== null &&
40 + // $FlowFixMe[method-unbinding]
41 + typeof selectedElement.scrollIntoView === 'function'
42 + ) {
43 + selectedElement.scrollIntoView({block: 'nearest', inline: 'nearest'});
44 + }
45 + }, [selectedCommitIndex]);
46 +
47 + if (
48 + selectedFiberID === null ||
49 + rootID === null ||
50 + selectedCommitIndex === null
51 + ) {
52 + return null;
53 + }
54 +
55 const commitIndices = profilingCache.getFiberCommits({
37 - fiberID: ((selectedFiberID: any): number),
38 - rootID: ((rootID: any): number),
56 + fiberID: selectedFiberID,
57 + rootID: rootID,
58 });
59
60 + const {nodes} = profilingCache.getCommitTree({
61 + rootID,
62 + commitIndex: selectedCommitIndex,
63 + });
64 + const node = nodes.get(selectedFiberID);
65 +
66 // $FlowFixMe[missing-local-annot]
67 const handleKeyDown = event => {
68 switch (event.key) {
@@ -64,17 +89,6 @@ export default function SidebarSelectedFiberInfo(_: Props): React.Node {
89 }
90 };
91
67 - useEffect(() => {
68 - const selectedElement = selectedListItemRef.current;
69 - if (
70 - selectedElement !== null &&
71 - // $FlowFixMe[method-unbinding]
72 - typeof selectedElement.scrollIntoView === 'function'
73 - ) {
74 - selectedElement.scrollIntoView({block: 'nearest', inline: 'nearest'});
75 - }
76 - }, [selectedCommitIndex]);
77 -
92 const listItems = [];
93 let i = 0;
94 for (i = 0; i < commitIndices.length; i++) {
@@ -114,11 +128,18 @@ export default function SidebarSelectedFiberInfo(_: Props): React.Node {
128 </Button>
129 </div>
130 <div className={styles.Content} onKeyDown={handleKeyDown} tabIndex={0}>
131 + {node != null && (
132 + <InspectedElementBadges
133 + hocDisplayNames={node.hocDisplayNames}
134 + compiledWithForget={node.compiledWithForget}
135 + />
136 + )}
137 <WhatChanged fiberID={((selectedFiberID: any): number)} />
138 {listItems.length > 0 && (
119 - <Fragment>
120 - <label className={styles.Label}>Rendered at</label>: {listItems}
121 - </Fragment>
139 + <div>
140 + <label className={styles.Label}>Rendered at: </label>
141 + {listItems}
142 + </div>
143 )}
144 {listItems.length === 0 && (
145 <div>Did not render during this profiling session.</div>
packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.css
-4
@@ -1,7 +1,3 @@
1 -.Component {
2 - margin-bottom: 0.5rem;
3 -}
4 -
1 .Item {
2 margin-top: 0.25rem;
3 }
packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js
+3 -2
@@ -9,7 +9,8 @@
9
10 import * as React from 'react';
11 import {useContext} from 'react';
12 -import {ProfilerContext} from '../Profiler/ProfilerContext';
12 +
13 +import {ProfilerContext} from './ProfilerContext';
14 import {StoreContext} from '../context';
15
16 import styles from './WhatChanged.css';
@@ -152,7 +153,7 @@ export default function WhatChanged({fiberID}: Props): React.Node {
153 }
154
155 return (
155 - <div className={styles.Component}>
156 + <div>
157 <label className={styles.Label}>Why did this render?</label>
158 {changes}
159 </div>
packages/react-devtools-shared/src/devtools/views/Profiler/types.js
+6
@@ -25,6 +25,9 @@ export type CommitTreeNode = {
25 parentID: number,
26 treeBaseDuration: number,
27 type: ElementType,
28 + // If component is compiled with Forget, the backend will send its name as Forget(...)
29 + // Later, on the frontend side, we will strip HOC names and Forget prefix.
30 + compiledWithForget: boolean,
31 };
32
33 export type CommitTree = {
@@ -39,6 +42,9 @@ export type SnapshotNode = {
42 hocDisplayNames: Array<string> | null,
43 key: number | string | null,
44 type: ElementType,
45 + // If component is compiled with Forget, the backend will send its name as Forget(...)
46 + // Later, on the frontend side, we will strip HOC names and Forget prefix.
47 + compiledWithForget: boolean,
48 };
49
50 export type ChangeDescription = {