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 {ReactContext} from 'shared/ReactTypes';
    
  11. 
    
  12. import * as React from 'react';
    
  13. import {createContext, useCallback, useContext, useMemo, useState} from 'react';
    
  14. import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
    
  15. import {useLocalStorage, useSubscription} from '../hooks';
    
  16. import {
    
  17.   TreeDispatcherContext,
    
  18.   TreeStateContext,
    
  19. } from '../Components/TreeContext';
    
  20. import {StoreContext} from '../context';
    
  21. import {logEvent} from 'react-devtools-shared/src/Logger';
    
  22. 
    
  23. import type {ProfilingDataFrontend} from './types';
    
  24. 
    
  25. export type TabID = 'flame-chart' | 'ranked-chart' | 'timeline';
    
  26. 
    
  27. export type Context = {
    
  28.   // Which tab is selected in the Profiler UI?
    
  29.   selectedTabID: TabID,
    
  30.   selectTab(id: TabID): void,
    
  31. 
    
  32.   // Store subscription based values.
    
  33.   // The isProfiling value may be modified by the record button in the Profiler toolbar,
    
  34.   // or from the backend itself (after a reload-and-profile action).
    
  35.   // It is synced between the backend and frontend via a Store subscription.
    
  36.   didRecordCommits: boolean,
    
  37.   isProcessingData: boolean,
    
  38.   isProfiling: boolean,
    
  39.   profilingData: ProfilingDataFrontend | null,
    
  40.   startProfiling(): void,
    
  41.   stopProfiling(): void,
    
  42.   supportsProfiling: boolean,
    
  43. 
    
  44.   // Which root should profiling data be shown for?
    
  45.   // This value should be initialized to either:
    
  46.   // 1. The selected root in the Components tree (if it has any profiling data) or
    
  47.   // 2. The first root in the list with profiling data.
    
  48.   rootID: number | null,
    
  49.   setRootID: (id: number) => void,
    
  50. 
    
  51.   // Controls whether commits are filtered by duration.
    
  52.   // This value is controlled by a filter toggle UI in the Profiler toolbar.
    
  53.   // It impacts the commit selector UI as well as the fiber commits bar chart.
    
  54.   isCommitFilterEnabled: boolean,
    
  55.   setIsCommitFilterEnabled: (value: boolean) => void,
    
  56.   minCommitDuration: number,
    
  57.   setMinCommitDuration: (value: number) => void,
    
  58. 
    
  59.   // Which commit is currently selected in the commit selector UI.
    
  60.   // Note that this is the index of the commit in all commits (non-filtered) that were profiled.
    
  61.   // This value is controlled by the commit selector UI in the Profiler toolbar.
    
  62.   // It impacts the flame graph and ranked charts.
    
  63.   selectedCommitIndex: number | null,
    
  64.   selectCommitIndex: (value: number | null) => void,
    
  65. 
    
  66.   // Which fiber is currently selected in the Ranked or Flamegraph charts?
    
  67.   selectedFiberID: number | null,
    
  68.   selectedFiberName: string | null,
    
  69.   selectFiber: (id: number | null, name: string | null) => void,
    
  70. };
    
  71. 
    
  72. const ProfilerContext: ReactContext<Context> = createContext<Context>(
    
  73.   ((null: any): Context),
    
  74. );
    
  75. ProfilerContext.displayName = 'ProfilerContext';
    
  76. 
    
  77. type StoreProfilingState = {
    
  78.   didRecordCommits: boolean,
    
  79.   isProcessingData: boolean,
    
  80.   isProfiling: boolean,
    
  81.   profilingData: ProfilingDataFrontend | null,
    
  82.   supportsProfiling: boolean,
    
  83. };
    
  84. 
    
  85. type Props = {
    
  86.   children: React$Node,
    
  87. };
    
  88. 
    
  89. function ProfilerContextController({children}: Props): React.Node {
    
  90.   const store = useContext(StoreContext);
    
  91.   const {selectedElementID} = useContext(TreeStateContext);
    
  92.   const dispatch = useContext(TreeDispatcherContext);
    
  93. 
    
  94.   const {profilerStore} = store;
    
  95. 
    
  96.   const subscription = useMemo(
    
  97.     () => ({
    
  98.       getCurrentValue: () => ({
    
  99.         didRecordCommits: profilerStore.didRecordCommits,
    
  100.         isProcessingData: profilerStore.isProcessingData,
    
  101.         isProfiling: profilerStore.isProfiling,
    
  102.         profilingData: profilerStore.profilingData,
    
  103.         supportsProfiling: store.rootSupportsBasicProfiling,
    
  104.       }),
    
  105.       subscribe: (callback: Function) => {
    
  106.         profilerStore.addListener('profilingData', callback);
    
  107.         profilerStore.addListener('isProcessingData', callback);
    
  108.         profilerStore.addListener('isProfiling', callback);
    
  109.         store.addListener('rootSupportsBasicProfiling', callback);
    
  110.         return () => {
    
  111.           profilerStore.removeListener('profilingData', callback);
    
  112.           profilerStore.removeListener('isProcessingData', callback);
    
  113.           profilerStore.removeListener('isProfiling', callback);
    
  114.           store.removeListener('rootSupportsBasicProfiling', callback);
    
  115.         };
    
  116.       },
    
  117.     }),
    
  118.     [profilerStore, store],
    
  119.   );
    
  120.   const {
    
  121.     didRecordCommits,
    
  122.     isProcessingData,
    
  123.     isProfiling,
    
  124.     profilingData,
    
  125.     supportsProfiling,
    
  126.   } = useSubscription<StoreProfilingState>(subscription);
    
  127. 
    
  128.   const [prevProfilingData, setPrevProfilingData] =
    
  129.     useState<ProfilingDataFrontend | null>(null);
    
  130.   const [rootID, setRootID] = useState<number | null>(null);
    
  131.   const [selectedFiberID, selectFiberID] = useState<number | null>(null);
    
  132.   const [selectedFiberName, selectFiberName] = useState<string | null>(null);
    
  133. 
    
  134.   const selectFiber = useCallback(
    
  135.     (id: number | null, name: string | null) => {
    
  136.       selectFiberID(id);
    
  137.       selectFiberName(name);
    
  138. 
    
  139.       // Sync selection to the Components tab for convenience.
    
  140.       // Keep in mind that profiling data may be from a previous session.
    
  141.       // If data has been imported, we should skip the selection sync.
    
  142.       if (
    
  143.         id !== null &&
    
  144.         profilingData !== null &&
    
  145.         profilingData.imported === false
    
  146.       ) {
    
  147.         // We should still check to see if this element is still in the store.
    
  148.         // It may have been removed during profiling.
    
  149.         if (store.containsElement(id)) {
    
  150.           dispatch({
    
  151.             type: 'SELECT_ELEMENT_BY_ID',
    
  152.             payload: id,
    
  153.           });
    
  154.         }
    
  155.       }
    
  156.     },
    
  157.     [dispatch, selectFiberID, selectFiberName, store, profilingData],
    
  158.   );
    
  159. 
    
  160.   const setRootIDAndClearFiber = useCallback(
    
  161.     (id: number | null) => {
    
  162.       selectFiber(null, null);
    
  163.       setRootID(id);
    
  164.     },
    
  165.     [setRootID, selectFiber],
    
  166.   );
    
  167. 
    
  168.   if (prevProfilingData !== profilingData) {
    
  169.     batchedUpdates(() => {
    
  170.       setPrevProfilingData(profilingData);
    
  171. 
    
  172.       const dataForRoots =
    
  173.         profilingData !== null ? profilingData.dataForRoots : null;
    
  174.       if (dataForRoots != null) {
    
  175.         const firstRootID = dataForRoots.keys().next().value || null;
    
  176. 
    
  177.         if (rootID === null || !dataForRoots.has(rootID)) {
    
  178.           let selectedElementRootID = null;
    
  179.           if (selectedElementID !== null) {
    
  180.             selectedElementRootID =
    
  181.               store.getRootIDForElement(selectedElementID);
    
  182.           }
    
  183.           if (
    
  184.             selectedElementRootID !== null &&
    
  185.             dataForRoots.has(selectedElementRootID)
    
  186.           ) {
    
  187.             setRootIDAndClearFiber(selectedElementRootID);
    
  188.           } else {
    
  189.             setRootIDAndClearFiber(firstRootID);
    
  190.           }
    
  191.         }
    
  192.       }
    
  193.     });
    
  194.   }
    
  195. 
    
  196.   const [isCommitFilterEnabled, setIsCommitFilterEnabled] =
    
  197.     useLocalStorage<boolean>('React::DevTools::isCommitFilterEnabled', false);
    
  198.   const [minCommitDuration, setMinCommitDuration] = useLocalStorage<number>(
    
  199.     'minCommitDuration',
    
  200.     0,
    
  201.   );
    
  202. 
    
  203.   const [selectedCommitIndex, selectCommitIndex] = useState<number | null>(
    
  204.     null,
    
  205.   );
    
  206.   const [selectedTabID, selectTab] = useLocalStorage<TabID>(
    
  207.     'React::DevTools::Profiler::defaultTab',
    
  208.     'flame-chart',
    
  209.     value => {
    
  210.       logEvent({
    
  211.         event_name: 'profiler-tab-changed',
    
  212.         metadata: {
    
  213.           tabId: value,
    
  214.         },
    
  215.       });
    
  216.     },
    
  217.   );
    
  218. 
    
  219.   const startProfiling = useCallback(() => {
    
  220.     logEvent({
    
  221.       event_name: 'profiling-start',
    
  222.       metadata: {current_tab: selectedTabID},
    
  223.     });
    
  224.     store.profilerStore.startProfiling();
    
  225.   }, [store, selectedTabID]);
    
  226.   const stopProfiling = useCallback(
    
  227.     () => store.profilerStore.stopProfiling(),
    
  228.     [store],
    
  229.   );
    
  230. 
    
  231.   if (isProfiling) {
    
  232.     batchedUpdates(() => {
    
  233.       if (selectedCommitIndex !== null) {
    
  234.         selectCommitIndex(null);
    
  235.       }
    
  236.       if (selectedFiberID !== null) {
    
  237.         selectFiberID(null);
    
  238.         selectFiberName(null);
    
  239.       }
    
  240.     });
    
  241.   }
    
  242. 
    
  243.   const value = useMemo(
    
  244.     () => ({
    
  245.       selectedTabID,
    
  246.       selectTab,
    
  247. 
    
  248.       didRecordCommits,
    
  249.       isProcessingData,
    
  250.       isProfiling,
    
  251.       profilingData,
    
  252.       startProfiling,
    
  253.       stopProfiling,
    
  254.       supportsProfiling,
    
  255. 
    
  256.       rootID,
    
  257.       setRootID: setRootIDAndClearFiber,
    
  258. 
    
  259.       isCommitFilterEnabled,
    
  260.       setIsCommitFilterEnabled,
    
  261.       minCommitDuration,
    
  262.       setMinCommitDuration,
    
  263. 
    
  264.       selectedCommitIndex,
    
  265.       selectCommitIndex,
    
  266. 
    
  267.       selectedFiberID,
    
  268.       selectedFiberName,
    
  269.       selectFiber,
    
  270.     }),
    
  271.     [
    
  272.       selectedTabID,
    
  273.       selectTab,
    
  274. 
    
  275.       didRecordCommits,
    
  276.       isProcessingData,
    
  277.       isProfiling,
    
  278.       profilingData,
    
  279.       startProfiling,
    
  280.       stopProfiling,
    
  281.       supportsProfiling,
    
  282. 
    
  283.       rootID,
    
  284.       setRootID,
    
  285.       setRootIDAndClearFiber,
    
  286. 
    
  287.       isCommitFilterEnabled,
    
  288.       setIsCommitFilterEnabled,
    
  289.       minCommitDuration,
    
  290.       setMinCommitDuration,
    
  291. 
    
  292.       selectedCommitIndex,
    
  293.       selectCommitIndex,
    
  294. 
    
  295.       selectedFiberID,
    
  296.       selectedFiberName,
    
  297.       selectFiber,
    
  298.     ],
    
  299.   );
    
  300. 
    
  301.   return (
    
  302.     <ProfilerContext.Provider value={value}>
    
  303.       {children}
    
  304.     </ProfilerContext.Provider>
    
  305.   );
    
  306. }
    
  307. 
    
  308. export {ProfilerContext, ProfilerContextController};