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">
This commit is contained in:
Ruslan Lesiutin
2024-05-07 16:39:01 +01:00
committed by GitHub
parent c32ff0f4f1
commit e7d213dfb0
16 changed files with 170 additions and 68 deletions
@@ -124,8 +124,8 @@ describe('profiling charts', () => {
"actualDuration": 0,
"didRender": true,
"id": 5,
"label": "Memo(Child) key="third" (<0.1ms of <0.1ms)",
"name": "Memo(Child)",
"label": "Child (Memo) key="third" (<0.1ms of <0.1ms)",
"name": "Child",
"offset": 15,
"selfDuration": 0,
"treeBaseDuration": 0,
@@ -134,8 +134,8 @@ describe('profiling charts', () => {
"actualDuration": 2,
"didRender": true,
"id": 4,
"label": "Memo(Child) key="second" (2ms of 2ms)",
"name": "Memo(Child)",
"label": "Child (Memo) key="second" (2ms of 2ms)",
"name": "Child",
"offset": 13,
"selfDuration": 2,
"treeBaseDuration": 2,
@@ -144,8 +144,8 @@ describe('profiling charts', () => {
"actualDuration": 3,
"didRender": true,
"id": 3,
"label": "Memo(Child) key="first" (3ms of 3ms)",
"name": "Memo(Child)",
"label": "Child (Memo) key="first" (3ms of 3ms)",
"name": "Child",
"offset": 10,
"selfDuration": 3,
"treeBaseDuration": 3,
@@ -175,8 +175,8 @@ describe('profiling charts', () => {
"actualDuration": 0,
"didRender": false,
"id": 5,
"label": "Memo(Child) key="third"",
"name": "Memo(Child)",
"label": "Child (Memo) key="third"",
"name": "Child",
"offset": 15,
"selfDuration": 0,
"treeBaseDuration": 0,
@@ -185,8 +185,8 @@ describe('profiling charts', () => {
"actualDuration": 0,
"didRender": false,
"id": 4,
"label": "Memo(Child) key="second"",
"name": "Memo(Child)",
"label": "Child (Memo) key="second"",
"name": "Child",
"offset": 13,
"selfDuration": 0,
"treeBaseDuration": 2,
@@ -195,8 +195,8 @@ describe('profiling charts', () => {
"actualDuration": 0,
"didRender": false,
"id": 3,
"label": "Memo(Child) key="first"",
"name": "Memo(Child)",
"label": "Child (Memo) key="first"",
"name": "Child",
"offset": 10,
"selfDuration": 0,
"treeBaseDuration": 3,
@@ -264,20 +264,20 @@ describe('profiling charts', () => {
},
{
"id": 3,
"label": "Memo(Child) (Memo) key="first" (3ms)",
"name": "Memo(Child)",
"label": "Child (Memo) key="first" (3ms)",
"name": "Child",
"value": 3,
},
{
"id": 4,
"label": "Memo(Child) (Memo) key="second" (2ms)",
"name": "Memo(Child)",
"label": "Child (Memo) key="second" (2ms)",
"name": "Child",
"value": 2,
},
{
"id": 5,
"label": "Memo(Child) (Memo) key="third" (<0.1ms)",
"name": "Memo(Child)",
"label": "Child (Memo) key="third" (<0.1ms)",
"name": "Child",
"value": 0,
},
]
@@ -214,6 +214,7 @@ export default class ProfilerStore extends EventEmitter<{
hocDisplayNames: element.hocDisplayNames,
key: element.key,
type: element.type,
compiledWithForget: element.compiledWithForget,
};
profilingSnapshots.set(elementID, snapshotNode);
@@ -1,5 +1,4 @@
.Root {
padding: 0.25rem;
user-select: none;
display: inline-flex;
}
@@ -7,8 +7,6 @@
* @flow
*/
import type {Element} from 'react-devtools-shared/src/frontend/types';
import * as React from 'react';
import Badge from './Badge';
@@ -17,11 +15,20 @@ import ForgetBadge from './ForgetBadge';
import styles from './InspectedElementBadges.css';
type Props = {
element: Element,
hocDisplayNames: null | Array<string>,
compiledWithForget: boolean,
};
export default function InspectedElementBadges({element}: Props): React.Node {
const {hocDisplayNames, compiledWithForget} = element;
export default function InspectedElementBadges({
hocDisplayNames,
compiledWithForget,
}: Props): React.Node {
if (
!compiledWithForget &&
(hocDisplayNames == null || hocDisplayNames.length === 0)
) {
return null;
}
return (
<div className={styles.Root}>
@@ -25,6 +25,10 @@
line-height: var(--line-height-data);
}
.InspectedElementBadgesContainer {
padding: 0.25rem;
}
.Owner {
border-radius: 0.25rem;
padding: 0.125rem 0.25rem;
@@ -85,7 +85,12 @@ export default function InspectedElementView({
return (
<Fragment>
<div className={styles.InspectedElement}>
<InspectedElementBadges element={element} />
<div className={styles.InspectedElementBadgesContainer}>
<InspectedElementBadges
hocDisplayNames={element.hocDisplayNames}
compiledWithForget={element.compiledWithForget}
/>
</div>
<InspectedElementPropsTree
bridge={bridge}
@@ -17,7 +17,10 @@ import {
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
} from 'react-devtools-shared/src/constants';
import {utfDecodeStringWithRanges} from 'react-devtools-shared/src/utils';
import {
parseElementDisplayNameFromBackend,
utfDecodeStringWithRanges,
} from 'react-devtools-shared/src/utils';
import {ElementTypeRoot} from 'react-devtools-shared/src/frontend/types';
import ProfilerStore from 'react-devtools-shared/src/devtools/ProfilerStore';
@@ -133,6 +136,7 @@ function recursivelyInitializeTree(
id,
): any): number),
type: node.type,
compiledWithForget: node.compiledWithForget,
});
node.children.forEach(childID =>
@@ -214,6 +218,7 @@ function updateTree(
parentID: 0,
treeBaseDuration: 0, // This will be updated by a subsequent operation
type,
compiledWithForget: false,
};
nodes.set(id, node);
@@ -241,15 +246,19 @@ function updateTree(
const parentNode = getClonedNode(parentID);
parentNode.children = parentNode.children.concat(id);
const {formattedDisplayName, hocDisplayNames, compiledWithForget} =
parseElementDisplayNameFromBackend(displayName, type);
const node: CommitTreeNode = {
children: [],
displayName,
hocDisplayNames: null,
displayName: formattedDisplayName,
hocDisplayNames: hocDisplayNames,
id,
key,
parentID,
treeBaseDuration: 0, // This will be updated by a subsequent operation
type,
compiledWithForget,
};
nodes.set(id, node);
@@ -75,8 +75,14 @@ export function getChartData({
throw Error(`Could not find node with id "${id}" in commit tree`);
}
const {children, displayName, hocDisplayNames, key, treeBaseDuration} =
node;
const {
children,
displayName,
hocDisplayNames,
key,
treeBaseDuration,
compiledWithForget,
} = node;
const actualDuration = fiberActualDurations.get(id) || 0;
const selfDuration = fiberSelfDurations.get(id) || 0;
@@ -86,11 +92,13 @@ export function getChartData({
const maybeKey = key !== null ? ` key="${key}"` : '';
let maybeBadge = '';
const maybeForgetBadge = compiledWithForget ? '✨ ' : '';
if (hocDisplayNames !== null && hocDisplayNames.length > 0) {
maybeBadge = ` (${hocDisplayNames[0]})`;
}
let label = `${name}${maybeBadge}${maybeKey}`;
let label = `${maybeForgetBadge}${name}${maybeBadge}${maybeKey}`;
if (didRender) {
label += ` (${formatDuration(selfDuration)}ms of ${formatDuration(
actualDuration,
@@ -1,9 +1,19 @@
.Toolbar {
padding: 0.25rem 0;
margin-bottom: 0.25rem;
flex: 0 0 auto;
display: flex;
align-items: center;
flex-direction: column;
gap: 0.25rem;
}
.BadgesContainer {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.BadgesContainer:not(:empty) {
padding-bottom: 0.25rem;
border-bottom: 1px solid var(--color-border);
}
@@ -20,10 +30,11 @@
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
padding-bottom: 0.25rem;
border-bottom: 1px solid var(--color-border);
}
.CurrentCommit {
margin: 0.25rem 0;
display: block;
width: 100%;
text-align: left;
@@ -9,6 +9,8 @@
import * as React from 'react';
import {Fragment, useContext} from 'react';
import InspectedElementBadges from '../Components/InspectedElementBadges';
import {ProfilerContext} from './ProfilerContext';
import {formatDuration} from './utils';
import WhatChanged from './WhatChanged';
@@ -34,11 +36,21 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
const {id, name} = fiberData;
const {profilingCache} = profilerStore;
if (rootID === null || selectedCommitIndex === null) {
return null;
}
const commitIndices = profilingCache.getFiberCommits({
fiberID: ((id: any): number),
rootID: ((rootID: any): number),
fiberID: id,
rootID,
});
const {nodes} = profilingCache.getCommitTree({
rootID,
commitIndex: selectedCommitIndex,
});
const node = nodes.get(id);
let renderDurationInfo = null;
let i = 0;
for (i = 0; i < commitIndices.length; i++) {
@@ -51,7 +63,8 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
renderDurationInfo = (
<div key={commitIndex} className={styles.CurrentCommit}>
{formatDuration(selfDuration)}ms of {formatDuration(actualDuration)}ms
<strong>Duration:</strong> {formatDuration(selfDuration)}ms of{' '}
{formatDuration(actualDuration)}ms
</div>
);
@@ -63,10 +76,27 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
<Fragment>
<div className={styles.Toolbar}>
<div className={styles.Component}>{name}</div>
</div>
<div className={styles.Content}>
{renderDurationInfo || <div>Did not render.</div>}
<WhatChanged fiberID={((id: any): number)} />
{node != null && (
<div className={styles.BadgesContainer}>
<InspectedElementBadges
hocDisplayNames={node.hocDisplayNames}
compiledWithForget={node.compiledWithForget}
/>
{node.compiledWithForget && (
<div>
This component has been auto-memoized by the React Compiler.
</div>
)}
</div>
)}
<div className={styles.Content}>
{renderDurationInfo || <div>Did not render.</div>}
<WhatChanged fiberID={id} />
</div>
</div>
</Fragment>
);
@@ -61,7 +61,7 @@ export function getChartData({
throw Error(`Could not find node with id "${id}" in commit tree`);
}
const {displayName, key, parentID, type} = node;
const {displayName, key, parentID, type, compiledWithForget} = node;
// Don't show the root node in this chart.
if (parentID === 0) {
@@ -72,6 +72,7 @@ export function getChartData({
const name = displayName || 'Anonymous';
const maybeKey = key !== null ? ` key="${key}"` : '';
const maybeForgetBadge = compiledWithForget ? '✨ ' : '';
let maybeBadge = '';
if (type === ElementTypeForwardRef) {
@@ -80,7 +81,7 @@ export function getChartData({
maybeBadge = ' (Memo)';
}
const label = `${name}${maybeBadge}${maybeKey} (${formatDuration(
const label = `${maybeForgetBadge}${name}${maybeBadge}${maybeKey} (${formatDuration(
selfDuration,
)}ms)`;
chartNodes.push({
@@ -11,6 +11,9 @@
padding: 0.5rem;
user-select: none;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.Component {
@@ -9,18 +9,18 @@
import * as React from 'react';
import {Fragment, useContext, useEffect, useRef} from 'react';
import WhatChanged from './WhatChanged';
import {ProfilerContext} from './ProfilerContext';
import {formatDuration, formatTime} from './utils';
import {StoreContext} from '../context';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import InspectedElementBadges from '../Components/InspectedElementBadges';
import styles from './SidebarSelectedFiberInfo.css';
export type Props = {};
export default function SidebarSelectedFiberInfo(_: Props): React.Node {
export default function SidebarSelectedFiberInfo(): React.Node {
const {profilerStore} = useContext(StoreContext);
const {
rootID,
@@ -33,11 +33,36 @@ export default function SidebarSelectedFiberInfo(_: Props): React.Node {
const {profilingCache} = profilerStore;
const selectedListItemRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const selectedElement = selectedListItemRef.current;
if (
selectedElement !== null &&
// $FlowFixMe[method-unbinding]
typeof selectedElement.scrollIntoView === 'function'
) {
selectedElement.scrollIntoView({block: 'nearest', inline: 'nearest'});
}
}, [selectedCommitIndex]);
if (
selectedFiberID === null ||
rootID === null ||
selectedCommitIndex === null
) {
return null;
}
const commitIndices = profilingCache.getFiberCommits({
fiberID: ((selectedFiberID: any): number),
rootID: ((rootID: any): number),
fiberID: selectedFiberID,
rootID: rootID,
});
const {nodes} = profilingCache.getCommitTree({
rootID,
commitIndex: selectedCommitIndex,
});
const node = nodes.get(selectedFiberID);
// $FlowFixMe[missing-local-annot]
const handleKeyDown = event => {
switch (event.key) {
@@ -64,17 +89,6 @@ export default function SidebarSelectedFiberInfo(_: Props): React.Node {
}
};
useEffect(() => {
const selectedElement = selectedListItemRef.current;
if (
selectedElement !== null &&
// $FlowFixMe[method-unbinding]
typeof selectedElement.scrollIntoView === 'function'
) {
selectedElement.scrollIntoView({block: 'nearest', inline: 'nearest'});
}
}, [selectedCommitIndex]);
const listItems = [];
let i = 0;
for (i = 0; i < commitIndices.length; i++) {
@@ -114,11 +128,18 @@ export default function SidebarSelectedFiberInfo(_: Props): React.Node {
</Button>
</div>
<div className={styles.Content} onKeyDown={handleKeyDown} tabIndex={0}>
{node != null && (
<InspectedElementBadges
hocDisplayNames={node.hocDisplayNames}
compiledWithForget={node.compiledWithForget}
/>
)}
<WhatChanged fiberID={((selectedFiberID: any): number)} />
{listItems.length > 0 && (
<Fragment>
<label className={styles.Label}>Rendered at</label>: {listItems}
</Fragment>
<div>
<label className={styles.Label}>Rendered at: </label>
{listItems}
</div>
)}
{listItems.length === 0 && (
<div>Did not render during this profiling session.</div>
@@ -1,7 +1,3 @@
.Component {
margin-bottom: 0.5rem;
}
.Item {
margin-top: 0.25rem;
}
@@ -9,7 +9,8 @@
import * as React from 'react';
import {useContext} from 'react';
import {ProfilerContext} from '../Profiler/ProfilerContext';
import {ProfilerContext} from './ProfilerContext';
import {StoreContext} from '../context';
import styles from './WhatChanged.css';
@@ -152,7 +153,7 @@ export default function WhatChanged({fiberID}: Props): React.Node {
}
return (
<div className={styles.Component}>
<div>
<label className={styles.Label}>Why did this render?</label>
{changes}
</div>
@@ -25,6 +25,9 @@ export type CommitTreeNode = {
parentID: number,
treeBaseDuration: number,
type: ElementType,
// If component is compiled with Forget, the backend will send its name as Forget(...)
// Later, on the frontend side, we will strip HOC names and Forget prefix.
compiledWithForget: boolean,
};
export type CommitTree = {
@@ -39,6 +42,9 @@ export type SnapshotNode = {
hocDisplayNames: Array<string> | null,
key: number | string | null,
type: ElementType,
// If component is compiled with Forget, the backend will send its name as Forget(...)
// Later, on the frontend side, we will strip HOC names and Forget prefix.
compiledWithForget: boolean,
};
export type ChangeDescription = {