From e7d213dfb0760dc9f6506fca6d6cfbac708a40e2 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Tue, 7 May 2024 16:39:01 +0100 Subject: [PATCH] feat[react-devtools]: display forget badge for components in profiling session (#29014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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 Screenshot 2024-05-07 at 15 05 37 - Badges are now displayed on the right panel when some fiber is selected in a specific commit Screenshot 2024-05-07 at 15 05 50 - Badges are also displayed when user hovers over some node in the tree Screenshot 2024-05-07 at 15 25 22 --- .../src/__tests__/profilingCharts-test.js | 36 +++++------ .../src/devtools/ProfilerStore.js | 1 + .../Components/InspectedElementBadges.css | 1 - .../Components/InspectedElementBadges.js | 17 ++++-- .../views/Components/InspectedElementView.css | 4 ++ .../views/Components/InspectedElementView.js | 7 ++- .../views/Profiler/CommitTreeBuilder.js | 15 ++++- .../views/Profiler/FlamegraphChartBuilder.js | 14 ++++- .../views/Profiler/HoveredFiberInfo.css | 17 +++++- .../views/Profiler/HoveredFiberInfo.js | 44 +++++++++++--- .../views/Profiler/RankedChartBuilder.js | 5 +- .../Profiler/SidebarSelectedFiberInfo.css | 3 + .../Profiler/SidebarSelectedFiberInfo.js | 59 +++++++++++++------ .../devtools/views/Profiler/WhatChanged.css | 4 -- .../devtools/views/Profiler/WhatChanged.js | 5 +- .../src/devtools/views/Profiler/types.js | 6 ++ 16 files changed, 170 insertions(+), 68 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js b/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js index 41d9093feeb54..e54af61d62098 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCharts-test.js @@ -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, }, ] diff --git a/packages/react-devtools-shared/src/devtools/ProfilerStore.js b/packages/react-devtools-shared/src/devtools/ProfilerStore.js index 4f530939f0fc3..46b2edcab4d32 100644 --- a/packages/react-devtools-shared/src/devtools/ProfilerStore.js +++ b/packages/react-devtools-shared/src/devtools/ProfilerStore.js @@ -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); diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.css index 11f6a23d3b472..5c8c977fb8105 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.css @@ -1,5 +1,4 @@ .Root { - padding: 0.25rem; user-select: none; display: inline-flex; } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js index e7ddfb2a216f5..9a069b484b5ed 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementBadges.js @@ -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, + 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 (
diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.css index ce1bf1e81c4c5..17a22a309a74a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.css @@ -25,6 +25,10 @@ line-height: var(--line-height-data); } +.InspectedElementBadgesContainer { + padding: 0.25rem; +} + .Owner { border-radius: 0.25rem; padding: 0.125rem 0.25rem; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js index 1c8f5864a823a..82e4ef47fc28f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js @@ -85,7 +85,12 @@ export default function InspectedElementView({ return (
- +
+ +
@@ -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); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/FlamegraphChartBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/FlamegraphChartBuilder.js index d64bb74648162..53f9f49657793 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/FlamegraphChartBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/FlamegraphChartBuilder.js @@ -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, diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.css b/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.css index c7fd8bbaba249..f16e99067ab9d 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.css +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.css @@ -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; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js b/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js index a5f565a01af1d..51f82038d3489 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js @@ -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 = (
- {formatDuration(selfDuration)}ms of {formatDuration(actualDuration)}ms + Duration: {formatDuration(selfDuration)}ms of{' '} + {formatDuration(actualDuration)}ms
); @@ -63,10 +76,27 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
{name}
-
-
- {renderDurationInfo ||
Did not render.
} - + + {node != null && ( +
+ + + {node.compiledWithForget && ( +
+ ✨ This component has been auto-memoized by the React Compiler. +
+ )} +
+ )} + +
+ {renderDurationInfo ||
Did not render.
} + + +
); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/RankedChartBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/RankedChartBuilder.js index f5938674b7b23..8cec8c6b924f6 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/RankedChartBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/RankedChartBuilder.js @@ -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({ diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css index 090658d64214d..2992247eb8f28 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css @@ -11,6 +11,9 @@ padding: 0.5rem; user-select: none; overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0.5rem; } .Component { diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js index 205a3aefa46fa..2eca7b0fdad3d 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js @@ -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(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 {
+ {node != null && ( + + )} {listItems.length > 0 && ( - - : {listItems} - +
+ + {listItems} +
)} {listItems.length === 0 && (
Did not render during this profiling session.
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.css b/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.css index 246dbfe9bac17..974b5e1a6a694 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.css +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.css @@ -1,7 +1,3 @@ -.Component { - margin-bottom: 0.5rem; -} - .Item { margin-top: 0.25rem; } diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js b/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js index 8ecae81af571c..52fc756a063d1 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js @@ -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 ( -
+
{changes}
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/types.js b/packages/react-devtools-shared/src/devtools/views/Profiler/types.js index 0df849e3fb0a4..34f24122e073a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/types.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/types.js @@ -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 | 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 = {