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 {ViewState} from './types';
    
  11. 
    
  12. import * as React from 'react';
    
  13. import {
    
  14.   Suspense,
    
  15.   useContext,
    
  16.   useDeferredValue,
    
  17.   useLayoutEffect,
    
  18.   useRef,
    
  19.   useState,
    
  20. } from 'react';
    
  21. import {SettingsContext} from 'react-devtools-shared/src/devtools/views/Settings/SettingsContext';
    
  22. import {ProfilerContext} from 'react-devtools-shared/src/devtools/views/Profiler/ProfilerContext';
    
  23. import NoProfilingData from 'react-devtools-shared/src/devtools/views/Profiler/NoProfilingData';
    
  24. import RecordingInProgress from 'react-devtools-shared/src/devtools/views/Profiler/RecordingInProgress';
    
  25. import {updateColorsToMatchTheme} from './content-views/constants';
    
  26. import {TimelineContext} from './TimelineContext';
    
  27. import CanvasPage from './CanvasPage';
    
  28. import {importFile} from './timelineCache';
    
  29. import TimelineSearchInput from './TimelineSearchInput';
    
  30. import TimelineNotSupported from './TimelineNotSupported';
    
  31. import {TimelineSearchContextController} from './TimelineSearchContext';
    
  32. 
    
  33. import styles from './Timeline.css';
    
  34. 
    
  35. export function Timeline(_: {}): React.Node {
    
  36.   const {file, inMemoryTimelineData, isTimelineSupported, setFile, viewState} =
    
  37.     useContext(TimelineContext);
    
  38.   const {didRecordCommits, isProfiling} = useContext(ProfilerContext);
    
  39. 
    
  40.   const ref = useRef(null);
    
  41. 
    
  42.   // HACK: Canvas rendering uses an imperative API,
    
  43.   // but DevTools colors are stored in CSS variables (see root.css and SettingsContext).
    
  44.   // When the theme changes, we need to trigger update the imperative colors and re-draw the Canvas.
    
  45.   const {theme} = useContext(SettingsContext);
    
  46.   // HACK: SettingsContext also uses a useLayoutEffect to update styles;
    
  47.   // make sure the theme context in SettingsContext updates before this code.
    
  48.   const deferredTheme = useDeferredValue(theme);
    
  49.   // HACK: Schedule a re-render of the Canvas once colors have been updated.
    
  50.   // The easiest way to guarangee this happens is to recreate the inner Canvas component.
    
  51.   const [key, setKey] = useState<string>(theme);
    
  52.   useLayoutEffect(() => {
    
  53.     const pollForTheme = () => {
    
  54.       if (updateColorsToMatchTheme(((ref.current: any): HTMLDivElement))) {
    
  55.         clearInterval(intervalID);
    
  56.         setKey(deferredTheme);
    
  57.       }
    
  58.     };
    
  59. 
    
  60.     const intervalID = setInterval(pollForTheme, 50);
    
  61. 
    
  62.     return () => {
    
  63.       clearInterval(intervalID);
    
  64.     };
    
  65.   }, [deferredTheme]);
    
  66. 
    
  67.   let content = null;
    
  68.   if (isProfiling) {
    
  69.     content = <RecordingInProgress />;
    
  70.   } else if (inMemoryTimelineData && inMemoryTimelineData.length > 0) {
    
  71.     // TODO (timeline) Support multiple renderers.
    
  72.     const timelineData = inMemoryTimelineData[0];
    
  73. 
    
  74.     content = (
    
  75.       <TimelineSearchContextController
    
  76.         profilerData={timelineData}
    
  77.         viewState={viewState}>
    
  78.         <TimelineSearchInput />
    
  79.         <CanvasPage profilerData={timelineData} viewState={viewState} />
    
  80.       </TimelineSearchContextController>
    
  81.     );
    
  82.   } else if (file) {
    
  83.     content = (
    
  84.       <Suspense fallback={<ProcessingData />}>
    
  85.         <FileLoader
    
  86.           file={file}
    
  87.           key={key}
    
  88.           onFileSelect={setFile}
    
  89.           viewState={viewState}
    
  90.         />
    
  91.       </Suspense>
    
  92.     );
    
  93.   } else if (didRecordCommits) {
    
  94.     content = <NoTimelineData />;
    
  95.   } else if (isTimelineSupported) {
    
  96.     content = <NoProfilingData />;
    
  97.   } else {
    
  98.     content = <TimelineNotSupported />;
    
  99.   }
    
  100. 
    
  101.   return (
    
  102.     <div className={styles.Content} ref={ref}>
    
  103.       {content}
    
  104.     </div>
    
  105.   );
    
  106. }
    
  107. 
    
  108. const ProcessingData = () => (
    
  109.   <div className={styles.EmptyStateContainer}>
    
  110.     <div className={styles.Header}>Processing data...</div>
    
  111.     <div className={styles.Row}>This should only take a minute.</div>
    
  112.   </div>
    
  113. );
    
  114. 
    
  115. // $FlowFixMe[missing-local-annot]
    
  116. const CouldNotLoadProfile = ({error, onFileSelect}) => (
    
  117.   <div className={styles.EmptyStateContainer}>
    
  118.     <div className={styles.Header}>Could not load profile</div>
    
  119.     {error.message && (
    
  120.       <div className={styles.Row}>
    
  121.         <div className={styles.ErrorMessage}>{error.message}</div>
    
  122.       </div>
    
  123.     )}
    
  124.     <div className={styles.Row}>
    
  125.       Try importing another Chrome performance profile.
    
  126.     </div>
    
  127.   </div>
    
  128. );
    
  129. 
    
  130. const NoTimelineData = () => (
    
  131.   <div className={styles.EmptyStateContainer}>
    
  132.     <div className={styles.Row}>
    
  133.       This current profile does not contain timeline data.
    
  134.     </div>
    
  135.   </div>
    
  136. );
    
  137. 
    
  138. const FileLoader = ({
    
  139.   file,
    
  140.   onFileSelect,
    
  141.   viewState,
    
  142. }: {
    
  143.   file: File | null,
    
  144.   onFileSelect: (file: File) => void,
    
  145.   viewState: ViewState,
    
  146. }) => {
    
  147.   if (file === null) {
    
  148.     return null;
    
  149.   }
    
  150. 
    
  151.   const dataOrError = importFile(file);
    
  152.   if (dataOrError instanceof Error) {
    
  153.     return (
    
  154.       <CouldNotLoadProfile error={dataOrError} onFileSelect={onFileSelect} />
    
  155.     );
    
  156.   }
    
  157. 
    
  158.   return (
    
  159.     <TimelineSearchContextController
    
  160.       profilerData={dataOrError}
    
  161.       viewState={viewState}>
    
  162.       <TimelineSearchInput />
    
  163.       <CanvasPage profilerData={dataOrError} viewState={viewState} />
    
  164.     </TimelineSearchContextController>
    
  165.   );
    
  166. };