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 {Fragment, useContext, useMemo} from 'react';
    
  12. import Button from '../Button';
    
  13. import ButtonIcon from '../ButtonIcon';
    
  14. import {ProfilerContext} from './ProfilerContext';
    
  15. import SnapshotCommitList from './SnapshotCommitList';
    
  16. import {maxBarWidth} from './constants';
    
  17. import {StoreContext} from '../context';
    
  18. 
    
  19. import styles from './SnapshotSelector.css';
    
  20. 
    
  21. export type Props = {};
    
  22. 
    
  23. export default function SnapshotSelector(_: Props): React.Node {
    
  24.   const {
    
  25.     isCommitFilterEnabled,
    
  26.     minCommitDuration,
    
  27.     rootID,
    
  28.     selectedCommitIndex,
    
  29.     selectCommitIndex,
    
  30.   } = useContext(ProfilerContext);
    
  31. 
    
  32.   const {profilerStore} = useContext(StoreContext);
    
  33.   const {commitData} = profilerStore.getDataForRoot(((rootID: any): number));
    
  34. 
    
  35.   const totalDurations: Array<number> = [];
    
  36.   const commitTimes: Array<number> = [];
    
  37.   commitData.forEach(commitDatum => {
    
  38.     totalDurations.push(
    
  39.       commitDatum.duration +
    
  40.         (commitDatum.effectDuration || 0) +
    
  41.         (commitDatum.passiveEffectDuration || 0),
    
  42.     );
    
  43.     commitTimes.push(commitDatum.timestamp);
    
  44.   });
    
  45. 
    
  46.   const filteredCommitIndices = useMemo(
    
  47.     () =>
    
  48.       commitData.reduce((reduced: $FlowFixMe, commitDatum, index) => {
    
  49.         if (
    
  50.           !isCommitFilterEnabled ||
    
  51.           commitDatum.duration >= minCommitDuration
    
  52.         ) {
    
  53.           reduced.push(index);
    
  54.         }
    
  55.         return reduced;
    
  56.       }, []),
    
  57.     [commitData, isCommitFilterEnabled, minCommitDuration],
    
  58.   );
    
  59. 
    
  60.   const numFilteredCommits = filteredCommitIndices.length;
    
  61. 
    
  62.   // Map the (unfiltered) selected commit index to an index within the filtered data.
    
  63.   const selectedFilteredCommitIndex = useMemo(() => {
    
  64.     if (selectedCommitIndex !== null) {
    
  65.       for (let i = 0; i < filteredCommitIndices.length; i++) {
    
  66.         if (filteredCommitIndices[i] === selectedCommitIndex) {
    
  67.           return i;
    
  68.         }
    
  69.       }
    
  70.     }
    
  71.     return null;
    
  72.   }, [filteredCommitIndices, selectedCommitIndex]);
    
  73. 
    
  74.   // TODO (ProfilerContext) This should be managed by the context controller (reducer).
    
  75.   // It doesn't currently know about the filtered commits though (since it doesn't suspend).
    
  76.   // Maybe this component should pass filteredCommitIndices up?
    
  77.   if (selectedFilteredCommitIndex === null) {
    
  78.     if (numFilteredCommits > 0) {
    
  79.       selectCommitIndex(0);
    
  80.     } else {
    
  81.       selectCommitIndex(null);
    
  82.     }
    
  83.   } else if (selectedFilteredCommitIndex >= numFilteredCommits) {
    
  84.     selectCommitIndex(numFilteredCommits === 0 ? null : numFilteredCommits - 1);
    
  85.   }
    
  86. 
    
  87.   let label = null;
    
  88.   if (numFilteredCommits > 0) {
    
  89.     // $FlowFixMe[missing-local-annot]
    
  90.     const handleCommitInputChange = event => {
    
  91.       const value = parseInt(event.currentTarget.value, 10);
    
  92.       if (!isNaN(value)) {
    
  93.         const filteredIndex = Math.min(
    
  94.           Math.max(value - 1, 0),
    
  95. 
    
  96.           // Snashots are shown to the user as 1-based
    
  97.           // but the indices within the profiler data array ar 0-based.
    
  98.           numFilteredCommits - 1,
    
  99.         );
    
  100.         selectCommitIndex(filteredCommitIndices[filteredIndex]);
    
  101.       }
    
  102.     };
    
  103. 
    
  104.     // $FlowFixMe[missing-local-annot]
    
  105.     const handleClick = event => {
    
  106.       event.currentTarget.select();
    
  107.     };
    
  108. 
    
  109.     // $FlowFixMe[missing-local-annot]
    
  110.     const handleKeyDown = event => {
    
  111.       switch (event.key) {
    
  112.         case 'ArrowDown':
    
  113.           viewPrevCommit();
    
  114.           event.stopPropagation();
    
  115.           break;
    
  116.         case 'ArrowUp':
    
  117.           viewNextCommit();
    
  118.           event.stopPropagation();
    
  119.           break;
    
  120.         default:
    
  121.           break;
    
  122.       }
    
  123.     };
    
  124. 
    
  125.     const input = (
    
  126.       <input
    
  127.         className={styles.Input}
    
  128.         data-testname="SnapshotSelector-Input"
    
  129.         type="text"
    
  130.         inputMode="numeric"
    
  131.         pattern="[0-9]*"
    
  132.         value={
    
  133.           // $FlowFixMe[unsafe-addition] addition with possible null/undefined value
    
  134.           selectedFilteredCommitIndex + 1
    
  135.         }
    
  136.         size={`${numFilteredCommits}`.length}
    
  137.         onChange={handleCommitInputChange}
    
  138.         onClick={handleClick}
    
  139.         onKeyDown={handleKeyDown}
    
  140.       />
    
  141.     );
    
  142. 
    
  143.     label = (
    
  144.       <Fragment>
    
  145.         {input} / {numFilteredCommits}
    
  146.       </Fragment>
    
  147.     );
    
  148.   }
    
  149. 
    
  150.   const viewNextCommit = () => {
    
  151.     let nextCommitIndex = ((selectedFilteredCommitIndex: any): number) + 1;
    
  152.     if (nextCommitIndex === filteredCommitIndices.length) {
    
  153.       nextCommitIndex = 0;
    
  154.     }
    
  155.     selectCommitIndex(filteredCommitIndices[nextCommitIndex]);
    
  156.   };
    
  157.   const viewPrevCommit = () => {
    
  158.     let nextCommitIndex = ((selectedFilteredCommitIndex: any): number) - 1;
    
  159.     if (nextCommitIndex < 0) {
    
  160.       nextCommitIndex = filteredCommitIndices.length - 1;
    
  161.     }
    
  162.     selectCommitIndex(filteredCommitIndices[nextCommitIndex]);
    
  163.   };
    
  164. 
    
  165.   // $FlowFixMe[missing-local-annot]
    
  166.   const handleKeyDown = event => {
    
  167.     switch (event.key) {
    
  168.       case 'ArrowLeft':
    
  169.         viewPrevCommit();
    
  170.         event.stopPropagation();
    
  171.         break;
    
  172.       case 'ArrowRight':
    
  173.         viewNextCommit();
    
  174.         event.stopPropagation();
    
  175.         break;
    
  176.       default:
    
  177.         break;
    
  178.     }
    
  179.   };
    
  180. 
    
  181.   if (commitData.length === 0) {
    
  182.     return null;
    
  183.   }
    
  184. 
    
  185.   return (
    
  186.     <Fragment>
    
  187.       <span
    
  188.         className={styles.IndexLabel}
    
  189.         data-testname="SnapshotSelector-Label">
    
  190.         {label}
    
  191.       </span>
    
  192.       <Button
    
  193.         className={styles.Button}
    
  194.         data-testname="SnapshotSelector-PreviousButton"
    
  195.         disabled={numFilteredCommits === 0}
    
  196.         onClick={viewPrevCommit}
    
  197.         title="Select previous commit">
    
  198.         <ButtonIcon type="previous" />
    
  199.       </Button>
    
  200.       <div
    
  201.         className={styles.Commits}
    
  202.         onKeyDown={handleKeyDown}
    
  203.         style={{
    
  204.           flex: numFilteredCommits > 0 ? '1 1 auto' : '0 0 auto',
    
  205.           maxWidth:
    
  206.             numFilteredCommits > 0
    
  207.               ? numFilteredCommits * maxBarWidth
    
  208.               : undefined,
    
  209.         }}
    
  210.         tabIndex={0}>
    
  211.         {numFilteredCommits > 0 && (
    
  212.           <SnapshotCommitList
    
  213.             commitData={commitData}
    
  214.             commitTimes={commitTimes}
    
  215.             filteredCommitIndices={filteredCommitIndices}
    
  216.             selectedCommitIndex={selectedCommitIndex}
    
  217.             selectedFilteredCommitIndex={selectedFilteredCommitIndex}
    
  218.             selectCommitIndex={selectCommitIndex}
    
  219.             totalDurations={totalDurations}
    
  220.           />
    
  221.         )}
    
  222.         {numFilteredCommits === 0 && (
    
  223.           <div className={styles.NoCommits}>No commits</div>
    
  224.         )}
    
  225.       </div>
    
  226.       <Button
    
  227.         className={styles.Button}
    
  228.         data-testname="SnapshotSelector-NextButton"
    
  229.         disabled={numFilteredCommits === 0}
    
  230.         onClick={viewNextCommit}
    
  231.         title="Select next commit">
    
  232.         <ButtonIcon type="next" />
    
  233.       </Button>
    
  234.     </Fragment>
    
  235.   );
    
  236. }