main
js 229 lines 6.77 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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 * as React from 'react';
11 import {
12 useCallback,
13 useContext,
14 useLayoutEffect,
15 useMemo,
16 useRef,
17 useState,
18 } from 'react';
19 import AutoSizer from 'react-virtualized-auto-sizer';
20 import {FixedSizeList} from 'react-window';
21 import {ProfilerContext} from './ProfilerContext';
22 import NoCommitData from './NoCommitData';
23 import CommitRankedListItem from './CommitRankedListItem';
24 import HoveredFiberInfo from './HoveredFiberInfo';
25 import {scale} from './utils';
26 import {createRegExp} from '../utils';
27 import {StoreContext} from '../context';
28 import {SettingsContext} from '../Settings/SettingsContext';
29 import {useHighlightHostInstance} from '../hooks';
30 import Tooltip from './Tooltip';
31
32 import styles from './CommitRanked.css';
33
34 import type {TooltipFiberData} from './HoveredFiberInfo';
35 import type {ChartData} from './RankedChartBuilder';
36 import type {CommitTree} from './types';
37
38 export type ItemData = {
39 chartData: ChartData,
40 currentSearchMatchID: number | null,
41 matchedFiberIDs: Set<number>,
42 onElementMouseEnter: (fiberData: TooltipFiberData) => void,
43 onElementMouseLeave: () => void,
44 scaleX: (value: number, fallbackValue: number) => number,
45 searchRegExp: RegExp | null,
46 selectedFiberID: number | null,
47 selectedFiberIndex: number,
48 selectFiber: (id: number | null, name: string | null) => void,
49 width: number,
50 };
51
52 export default function CommitRankedAutoSizer(_: {}): React.Node {
53 const {profilerStore} = useContext(StoreContext);
54 const {rootID, selectedCommitIndex, selectFiber} =
55 useContext(ProfilerContext);
56 const {profilingCache} = profilerStore;
57
58 const deselectCurrentFiber = useCallback(
59 (event: $FlowFixMe) => {
60 event.stopPropagation();
61 selectFiber(null, null);
62 },
63 [selectFiber],
64 );
65
66 let commitTree: CommitTree | null = null;
67 let chartData: ChartData | null = null;
68 if (selectedCommitIndex !== null) {
69 commitTree = profilingCache.getCommitTree({
70 commitIndex: selectedCommitIndex,
71 rootID: rootID as any as number,
72 });
73
74 chartData = profilingCache.getRankedChartData({
75 commitIndex: selectedCommitIndex,
76 commitTree,
77 rootID: rootID as any as number,
78 });
79 }
80
81 if (commitTree != null && chartData != null && chartData.nodes.length > 0) {
82 return (
83 <div className={styles.Container} onClick={deselectCurrentFiber}>
84 <AutoSizer>
85 {({height, width}) => (
86 <CommitRanked
87 chartData={chartData as any as ChartData}
88 commitTree={commitTree as any as CommitTree}
89 height={height}
90 width={width}
91 />
92 )}
93 </AutoSizer>
94 </div>
95 );
96 } else {
97 return <NoCommitData />;
98 }
99 }
100
101 type Props = {
102 chartData: ChartData,
103 commitTree: CommitTree,
104 height: number,
105 width: number,
106 };
107
108 function CommitRanked({chartData, commitTree, height, width}: Props) {
109 const [hoveredFiberData, setHoveredFiberData] =
110 useState<TooltipFiberData | null>(null);
111 const {lineHeight} = useContext(SettingsContext);
112 const {selectedFiberID, selectFiber, searchText, searchResults, searchIndex} =
113 useContext(ProfilerContext);
114 const {highlightHostInstance, clearHighlightHostInstance} =
115 useHighlightHostInstance();
116
117 const selectedFiberIndex = useMemo(
118 () => getNodeIndex(chartData, selectedFiberID),
119 [chartData, selectedFiberID],
120 );
121
122 // Search highlighting (see CommitFlamegraph for details).
123 const searchRegExp = useMemo(
124 () => (searchText === '' ? null : createRegExp(searchText)),
125 [searchText],
126 );
127 const matchedFiberIDs = useMemo(
128 () => new Set(searchResults.map(result => result.id)),
129 [searchResults],
130 );
131 const currentSearchMatchID =
132 searchIndex >= 0 && searchIndex < searchResults.length
133 ? searchResults[searchIndex].id
134 : null;
135
136 const handleElementMouseEnter = useCallback(
137 ({id, name}: $FlowFixMe) => {
138 highlightHostInstance(id); // Highlight last hovered element.
139 setHoveredFiberData({id, name}); // Set hovered fiber data for tooltip
140 },
141 [highlightHostInstance],
142 );
143
144 const handleElementMouseLeave = useCallback(() => {
145 clearHighlightHostInstance(); // clear highlighting of element on mouse leave
146 setHoveredFiberData(null); // clear hovered fiber data for tooltip
147 }, [clearHighlightHostInstance]);
148
149 const itemData = useMemo<ItemData>(
150 () => ({
151 chartData,
152 currentSearchMatchID,
153 matchedFiberIDs,
154 onElementMouseEnter: handleElementMouseEnter,
155 onElementMouseLeave: handleElementMouseLeave,
156 scaleX: scale(0, chartData.nodes[selectedFiberIndex].value, 0, width),
157 searchRegExp,
158 selectedFiberID,
159 selectedFiberIndex,
160 selectFiber,
161 width,
162 }),
163 [
164 chartData,
165 currentSearchMatchID,
166 matchedFiberIDs,
167 handleElementMouseEnter,
168 handleElementMouseLeave,
169 searchRegExp,
170 selectedFiberID,
171 selectedFiberIndex,
172 selectFiber,
173 width,
174 ],
175 );
176
177 // Tooltip used to show summary of fiber info on hover
178 const tooltipLabel = useMemo(
179 () =>
180 hoveredFiberData !== null ? (
181 <HoveredFiberInfo fiberData={hoveredFiberData} />
182 ) : null,
183 [hoveredFiberData],
184 );
185
186 // Scroll the selected fiber's row into view when the selection changes (e.g.
187 // when navigating between search results). Selection is driven externally
188 // (search nav in ProfilerContext, or a node click) and selectedFiberIndex is
189 // derived here — no local event handler sets it — so we sync the imperative
190 // scroll in a layout effect, which runs before paint to avoid a frame where
191 // the scroll position lags the selection.
192 const listRef = useRef<FixedSizeList | null>(null);
193 const itemIsSelected = selectedFiberID !== null;
194 useLayoutEffect(() => {
195 // selectedFiberIndex falls back to 0 when nothing is selected, so only
196 // scroll when a fiber is actually selected.
197 if (itemIsSelected && listRef.current !== null) {
198 listRef.current.scrollToItem(selectedFiberIndex, 'smart');
199 }
200 }, [itemIsSelected, selectedFiberIndex]);
201
202 return (
203 <Tooltip label={tooltipLabel}>
204 <FixedSizeList
205 height={height}
206 innerElementType="svg"
207 itemCount={chartData.nodes.length}
208 itemData={itemData}
209 itemSize={lineHeight}
210 ref={listRef}
211 width={width}>
212 {CommitRankedListItem}
213 </FixedSizeList>
214 </Tooltip>
215 );
216 }
217
218 const getNodeIndex = (chartData: ChartData, id: number | null): number => {
219 if (id === null) {
220 return 0;
221 }
222 const {nodes} = chartData;
223 for (let index = 0; index < nodes.length; index++) {
224 if (nodes[index].id === id) {
225 return index;
226 }
227 }
228 return 0;
229 };