mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
@@ -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
@@ -1,5 +1,4 @@
|
||||
.Root {
|
||||
padding: 0.25rem;
|
||||
user-select: none;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
+12
-5
@@ -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;
|
||||
|
||||
+6
-1
@@ -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}
|
||||
|
||||
+12
-3
@@ -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);
|
||||
|
||||
+11
-3
@@ -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;
|
||||
|
||||
+37
-7
@@ -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>
|
||||
);
|
||||
|
||||
+3
-2
@@ -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({
|
||||
|
||||
+3
@@ -11,6 +11,9 @@
|
||||
padding: 0.5rem;
|
||||
user-select: none;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.Component {
|
||||
|
||||
+40
-19
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user