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: 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: 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={(listRef: any) /* Flow bug? */}
    
  296.           width={width}>
    
  297.           {SnapshotCommitListItem}
    
  298.         </FixedSizeList>
    
  299.       </div>
    
  300.     </Tooltip>
    
  301.   );
    
  302. }