main
js 261 lines 7.95 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 forwardRef,
13 useCallback,
14 useContext,
15 useLayoutEffect,
16 useMemo,
17 useRef,
18 useState,
19 } from 'react';
20 import AutoSizer from 'react-virtualized-auto-sizer';
21 import {FixedSizeList} from 'react-window';
22 import {ProfilerContext} from './ProfilerContext';
23 import NoCommitData from './NoCommitData';
24 import CommitFlamegraphListItem from './CommitFlamegraphListItem';
25 import HoveredFiberInfo from './HoveredFiberInfo';
26 import {scale} from './utils';
27 import {createRegExp} from '../utils';
28 import {useHighlightHostInstance} from '../hooks';
29 import {StoreContext} from '../context';
30 import {SettingsContext} from '../Settings/SettingsContext';
31 import Tooltip from './Tooltip';
32
33 import styles from './CommitFlamegraph.css';
34
35 import type {TooltipFiberData} from './HoveredFiberInfo';
36 import type {ChartData, ChartNode} from './FlamegraphChartBuilder';
37 import type {CommitTree} from './types';
38
39 export type ItemData = {
40 chartData: ChartData,
41 currentSearchMatchID: number | null,
42 matchedFiberIDs: Set<number>,
43 onElementMouseEnter: (fiberData: TooltipFiberData) => void,
44 onElementMouseLeave: () => void,
45 scaleX: (value: number, fallbackValue: number) => number,
46 searchRegExp: RegExp | null,
47 selectedChartNode: ChartNode | null,
48 selectedChartNodeIndex: number,
49 selectFiber: (id: number | null, name: string | null) => void,
50 width: number,
51 };
52
53 export default function CommitFlamegraphAutoSizer(_: {}): React.Node {
54 const {profilerStore} = useContext(StoreContext);
55 const {rootID, selectedCommitIndex, selectFiber} =
56 useContext(ProfilerContext);
57 const {profilingCache} = profilerStore;
58
59 const deselectCurrentFiber = useCallback(
60 (event: $FlowFixMe) => {
61 event.stopPropagation();
62 selectFiber(null, null);
63 },
64 [selectFiber],
65 );
66
67 let commitTree: CommitTree | null = null;
68 let chartData: ChartData | null = null;
69 if (selectedCommitIndex !== null) {
70 commitTree = profilingCache.getCommitTree({
71 commitIndex: selectedCommitIndex,
72 rootID: rootID as any as number,
73 });
74
75 chartData = profilingCache.getFlamegraphChartData({
76 commitIndex: selectedCommitIndex,
77 commitTree,
78 rootID: rootID as any as number,
79 });
80 }
81
82 if (commitTree != null && chartData != null && chartData.depth > 0) {
83 return (
84 <div className={styles.Container} onClick={deselectCurrentFiber}>
85 <AutoSizer>
86 {({height, width}) => (
87 // Force Flow types to avoid checking for `null` here because there's no static proof that
88 // by the time this render prop function is called, the values of the `let` variables have not changed.
89 <CommitFlamegraph
90 chartData={chartData as any as ChartData}
91 commitTree={commitTree as any as CommitTree}
92 height={height}
93 width={width}
94 />
95 )}
96 </AutoSizer>
97 </div>
98 );
99 } else {
100 return <NoCommitData />;
101 }
102 }
103
104 type Props = {
105 chartData: ChartData,
106 commitTree: CommitTree,
107 height: number,
108 width: number,
109 };
110
111 function CommitFlamegraph({chartData, commitTree, height, width}: Props) {
112 const [hoveredFiberData, setHoveredFiberData] =
113 useState<TooltipFiberData | null>(null);
114 const {lineHeight} = useContext(SettingsContext);
115 const {selectFiber, selectedFiberID, searchText, searchResults, searchIndex} =
116 useContext(ProfilerContext);
117 const {highlightHostInstance, clearHighlightHostInstance} =
118 useHighlightHostInstance();
119
120 // Search highlighting: the regexp to highlight, the set of matching fibers,
121 // and the id of the current match (highlighted more prominently).
122 const searchRegExp = useMemo(
123 () => (searchText === '' ? null : createRegExp(searchText)),
124 [searchText],
125 );
126 const matchedFiberIDs = useMemo(
127 () => new Set(searchResults.map(result => result.id)),
128 [searchResults],
129 );
130 const currentSearchMatchID =
131 searchIndex >= 0 && searchIndex < searchResults.length
132 ? searchResults[searchIndex].id
133 : null;
134
135 const selectedChartNodeIndex = useMemo<number>(() => {
136 if (selectedFiberID === null) {
137 return 0;
138 }
139 // The selected node might not be in the tree for this commit,
140 // so it's important that we have a fallback plan.
141 const depth = chartData.idToDepthMap.get(selectedFiberID);
142 return depth !== undefined ? depth - 1 : 0;
143 }, [chartData, selectedFiberID]);
144
145 const selectedChartNode = useMemo(() => {
146 if (selectedFiberID !== null) {
147 return (
148 chartData.rows[selectedChartNodeIndex].find(
149 chartNode => chartNode.id === selectedFiberID,
150 ) || null
151 );
152 }
153 return null;
154 }, [chartData, selectedFiberID, selectedChartNodeIndex]);
155
156 const handleElementMouseEnter = useCallback(
157 ({id, name}: $FlowFixMe) => {
158 highlightHostInstance(id); // Highlight last hovered element.
159 setHoveredFiberData({id, name}); // Set hovered fiber data for tooltip
160 },
161 [highlightHostInstance],
162 );
163
164 const handleElementMouseLeave = useCallback(() => {
165 clearHighlightHostInstance(); // clear highlighting of element on mouse leave
166 setHoveredFiberData(null); // clear hovered fiber data for tooltip
167 }, [clearHighlightHostInstance]);
168
169 const itemData = useMemo<ItemData>(
170 () => ({
171 chartData,
172 currentSearchMatchID,
173 matchedFiberIDs,
174 onElementMouseEnter: handleElementMouseEnter,
175 onElementMouseLeave: handleElementMouseLeave,
176 scaleX: scale(
177 0,
178 selectedChartNode !== null
179 ? selectedChartNode.treeBaseDuration
180 : chartData.baseDuration,
181 0,
182 width,
183 ),
184 searchRegExp,
185 selectedChartNode,
186 selectedChartNodeIndex,
187 selectFiber,
188 width,
189 }),
190 [
191 chartData,
192 currentSearchMatchID,
193 matchedFiberIDs,
194 handleElementMouseEnter,
195 handleElementMouseLeave,
196 searchRegExp,
197 selectedChartNode,
198 selectedChartNodeIndex,
199 selectFiber,
200 width,
201 ],
202 );
203
204 // Tooltip used to show summary of fiber info on hover
205 const tooltipLabel = useMemo(
206 () =>
207 hoveredFiberData !== null ? (
208 <HoveredFiberInfo fiberData={hoveredFiberData} />
209 ) : null,
210 [hoveredFiberData],
211 );
212
213 // Scroll the selected fiber's row into view when the selection changes (e.g.
214 // when navigating between search results). Selection is driven externally
215 // (search nav in ProfilerContext, or a node click) and selectedChartNodeIndex
216 // is derived here — no local event handler sets it — so we sync the imperative
217 // scroll in a layout effect, which runs before paint to avoid a frame where
218 // the scroll position lags the selection.
219 const listRef = useRef<FixedSizeList | null>(null);
220 const itemIsSelected = selectedFiberID !== null;
221 useLayoutEffect(() => {
222 // selectedChartNodeIndex falls back to 0 when nothing is selected, so only
223 // scroll when a fiber is actually selected.
224 if (itemIsSelected && listRef.current !== null) {
225 listRef.current.scrollToItem(selectedChartNodeIndex, 'smart');
226 }
227 }, [itemIsSelected, selectedChartNodeIndex]);
228
229 return (
230 <Tooltip label={tooltipLabel}>
231 <FixedSizeList
232 height={height}
233 innerElementType={InnerElementType}
234 itemCount={chartData.depth}
235 itemData={itemData}
236 itemSize={lineHeight}
237 ref={listRef}
238 width={width}>
239 {CommitFlamegraphListItem}
240 </FixedSizeList>
241 </Tooltip>
242 );
243 }
244
245 const InnerElementType = forwardRef(({children, ...rest}, ref) => (
246 <svg ref={ref} {...rest}>
247 <defs>
248 <pattern
249 id="didNotRenderPattern"
250 patternUnits="userSpaceOnUse"
251 width="4"
252 height="4">
253 <path
254 d="M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2"
255 className={styles.PatternPath}
256 />
257 </pattern>
258 </defs>
259 {children}
260 </svg>
261 ));