main
js 306 lines 8.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 type {CommitDataFrontend} from './types';
11
12 import * as React from 'react';
13 import {useEffect, useMemo, useRef, useState} from 'react';
14 import AutoSizer from 'react-virtualized-auto-sizer';
15 import {FixedSizeList} from 'react-window';
16 import SnapshotCommitListItem from './SnapshotCommitListItem';
17 import {minBarWidth} from './constants';
18 import {formatDuration, formatTime} from './utils';
19 import Tooltip from './Tooltip';
20
21 import styles from './SnapshotCommitList.css';
22
23 export type ItemData = {
24 commitTimes: Array<number>,
25 filteredCommitIndices: Array<number>,
26 maxDuration: number,
27 selectedCommitIndex: number | null,
28 selectedFilteredCommitIndex: number | null,
29 selectCommitIndex: (index: number) => void,
30 setHoveredCommitIndex: (index: number) => void,
31 startCommitDrag: (newDragState: DragState) => void,
32 totalDurations: Array<number>,
33 };
34
35 type Props = {
36 commitData: $ReadOnlyArray<CommitDataFrontend>,
37 commitTimes: Array<number>,
38 filteredCommitIndices: Array<number>,
39 selectedCommitIndex: number | null,
40 selectedFilteredCommitIndex: number | null,
41 selectCommitIndex: (index: number) => void,
42 totalDurations: Array<number>,
43 };
44
45 export default function SnapshotCommitList({
46 commitData,
47 commitTimes,
48 filteredCommitIndices,
49 selectedCommitIndex,
50 selectedFilteredCommitIndex,
51 selectCommitIndex,
52 totalDurations,
53 }: Props): React.Node {
54 return (
55 <AutoSizer>
56 {({height, width}) => (
57 <List
58 commitData={commitData}
59 commitTimes={commitTimes}
60 height={height}
61 filteredCommitIndices={filteredCommitIndices}
62 selectedCommitIndex={selectedCommitIndex}
63 selectedFilteredCommitIndex={selectedFilteredCommitIndex}
64 selectCommitIndex={selectCommitIndex}
65 totalDurations={totalDurations}
66 width={width}
67 />
68 )}
69 </AutoSizer>
70 );
71 }
72
73 type ListProps = {
74 commitData: $ReadOnlyArray<CommitDataFrontend>,
75 commitTimes: Array<number>,
76 height: number,
77 filteredCommitIndices: Array<number>,
78 selectedCommitIndex: number | null,
79 selectedFilteredCommitIndex: number | null,
80 selectCommitIndex: (index: number) => void,
81 totalDurations: Array<number>,
82 width: number,
83 };
84
85 type DragState = {
86 commitIndex: number,
87 left: number,
88 sizeIncrement: number,
89 };
90
91 function List({
92 commitData,
93 selectedCommitIndex,
94 commitTimes,
95 height,
96 filteredCommitIndices,
97 selectedFilteredCommitIndex,
98 selectCommitIndex,
99 totalDurations,
100 width,
101 }: ListProps) {
102 // $FlowFixMe[incompatible-use]
103 const listRef = useRef<FixedSizeList<ItemData> | null>(null);
104 const divRef = useRef<HTMLDivElement | null>(null);
105 const prevCommitIndexRef = useRef<number | null>(null);
106
107 // Make sure a newly selected snapshot is fully visible within the list.
108 useEffect(() => {
109 if (selectedFilteredCommitIndex !== prevCommitIndexRef.current) {
110 prevCommitIndexRef.current = selectedFilteredCommitIndex;
111 if (selectedFilteredCommitIndex !== null && listRef.current !== null) {
112 listRef.current.scrollToItem(selectedFilteredCommitIndex);
113 }
114 }
115 }, [listRef, selectedFilteredCommitIndex]);
116
117 const itemSize = useMemo(
118 () => Math.max(minBarWidth, width / filteredCommitIndices.length),
119 [filteredCommitIndices, width],
120 );
121 const maxDuration = useMemo(
122 () => totalDurations.reduce((max, duration) => Math.max(max, duration), 0),
123 [totalDurations],
124 );
125
126 const maxCommitIndex = filteredCommitIndices.length - 1;
127
128 const [dragState, setDragState] = useState<DragState | null>(null);
129
130 const handleDragCommit = ({buttons, pageX}: any) => {
131 if (buttons === 0) {
132 setDragState(null);
133 return;
134 }
135
136 if (dragState !== null) {
137 const {commitIndex, left, sizeIncrement} = dragState;
138
139 let newCommitIndex = commitIndex;
140 let newCommitLeft = left;
141
142 if (pageX < newCommitLeft) {
143 while (pageX < newCommitLeft) {
144 newCommitLeft -= sizeIncrement;
145 newCommitIndex -= 1;
146 }
147 } else {
148 let newCommitRectRight = newCommitLeft + sizeIncrement;
149 while (pageX > newCommitRectRight) {
150 newCommitRectRight += sizeIncrement;
151 newCommitIndex += 1;
152 }
153 }
154
155 if (newCommitIndex < 0) {
156 newCommitIndex = 0;
157 } else if (newCommitIndex > maxCommitIndex) {
158 newCommitIndex = maxCommitIndex;
159 }
160
161 selectCommitIndex(newCommitIndex);
162 }
163 };
164
165 useEffect(() => {
166 if (dragState === null) {
167 return;
168 }
169
170 const element = divRef.current;
171 if (element !== null) {
172 const ownerDocument = element.ownerDocument;
173 ownerDocument.addEventListener('mousemove', handleDragCommit);
174 return () => {
175 ownerDocument.removeEventListener('mousemove', handleDragCommit);
176 };
177 }
178 }, [dragState]);
179
180 const [hoveredCommitIndex, setHoveredCommitIndex] = useState<number | null>(
181 null,
182 );
183
184 // Pass required contextual data down to the ListItem renderer.
185 const itemData = useMemo<ItemData>(
186 () => ({
187 commitTimes,
188 filteredCommitIndices,
189 maxDuration,
190 selectedCommitIndex,
191 selectedFilteredCommitIndex,
192 selectCommitIndex,
193 setHoveredCommitIndex,
194 startCommitDrag: setDragState,
195 totalDurations,
196 }),
197 [
198 commitTimes,
199 filteredCommitIndices,
200 maxDuration,
201 selectedCommitIndex,
202 selectedFilteredCommitIndex,
203 selectCommitIndex,
204 setHoveredCommitIndex,
205 totalDurations,
206 ],
207 );
208
209 let tooltipLabel = null;
210 if (hoveredCommitIndex !== null) {
211 const {
212 duration,
213 effectDuration,
214 passiveEffectDuration,
215 priorityLevel,
216 timestamp,
217 } = commitData[hoveredCommitIndex];
218
219 // Only some React versions include commit durations.
220 // Show a richer tooltip only for builds that have that info.
221 if (
222 effectDuration !== null ||
223 passiveEffectDuration !== null ||
224 priorityLevel !== null
225 ) {
226 tooltipLabel = (
227 <ul className={styles.TooltipList}>
228 {priorityLevel !== null && (
229 <li className={styles.TooltipListItem}>
230 <label className={styles.TooltipLabel}>Priority</label>
231 <span className={styles.TooltipValue}>{priorityLevel}</span>
232 </li>
233 )}
234 <li className={styles.TooltipListItem}>
235 <label className={styles.TooltipLabel}>Committed at</label>
236 <span className={styles.TooltipValue}>
237 {formatTime(timestamp)}s
238 </span>
239 </li>
240 <li className={styles.TooltipListItem}>
241 <div className={styles.DurationsWrapper}>
242 <label className={styles.TooltipLabel}>Durations</label>
243 <ul className={styles.DurationsList}>
244 <li className={styles.DurationsListItem}>
245 <label className={styles.DurationsLabel}>Render</label>
246 <span className={styles.DurationsValue}>
247 {formatDuration(duration)}ms
248 </span>
249 </li>
250 {effectDuration !== null && (
251 <li className={styles.DurationsListItem}>
252 <label className={styles.DurationsLabel}>
253 Layout effects
254 </label>
255 <span className={styles.DurationsValue}>
256 {formatDuration(effectDuration)}ms
257 </span>
258 </li>
259 )}
260 {passiveEffectDuration !== null && (
261 <li className={styles.DurationsListItem}>
262 <label className={styles.DurationsLabel}>
263 Passive effects
264 </label>
265 <span className={styles.DurationsValue}>
266 {formatDuration(passiveEffectDuration)}ms
267 </span>
268 </li>
269 )}
270 </ul>
271 </div>
272 </li>
273 </ul>
274 );
275 } else {
276 tooltipLabel = `${formatDuration(duration)}ms at ${formatTime(
277 timestamp,
278 )}s`;
279 }
280 }
281
282 return (
283 <Tooltip className={styles.Tooltip} label={tooltipLabel}>
284 <div
285 ref={divRef}
286 style={{height, width}}
287 onMouseLeave={() => setHoveredCommitIndex(null)}>
288 <FixedSizeList
289 className={styles.List}
290 layout="horizontal"
291 height={height}
292 itemCount={filteredCommitIndices.length}
293 itemData={itemData}
294 itemSize={itemSize}
295 ref={
296 listRef as any
297
298 /* Flow bug? */
299 }
300 width={width}>
301 {SnapshotCommitListItem}
302 </FixedSizeList>
303 </div>
304 </Tooltip>
305 );
306 }