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 EventEmitter from '../events';
    
  11. import {prepareProfilingDataFrontendFromBackendAndStore} from './views/Profiler/utils';
    
  12. import ProfilingCache from './ProfilingCache';
    
  13. import Store from './store';
    
  14. 
    
  15. import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
    
  16. import type {ProfilingDataBackend} from 'react-devtools-shared/src/backend/types';
    
  17. import type {
    
  18.   CommitDataFrontend,
    
  19.   ProfilingDataForRootFrontend,
    
  20.   ProfilingDataFrontend,
    
  21.   SnapshotNode,
    
  22. } from './views/Profiler/types';
    
  23. 
    
  24. export default class ProfilerStore extends EventEmitter<{
    
  25.   isProcessingData: [],
    
  26.   isProfiling: [],
    
  27.   profilingData: [],
    
  28. }> {
    
  29.   _bridge: FrontendBridge;
    
  30. 
    
  31.   // Suspense cache for lazily calculating derived profiling data.
    
  32.   _cache: ProfilingCache;
    
  33. 
    
  34.   // Temporary store of profiling data from the backend renderer(s).
    
  35.   // This data will be converted to the ProfilingDataFrontend format after being collected from all renderers.
    
  36.   _dataBackends: Array<ProfilingDataBackend> = [];
    
  37. 
    
  38.   // Data from the most recently completed profiling session,
    
  39.   // or data that has been imported from a previously exported session.
    
  40.   // This object contains all necessary data to drive the Profiler UI interface,
    
  41.   // even though some of it is lazily parsed/derived via the ProfilingCache.
    
  42.   _dataFrontend: ProfilingDataFrontend | null = null;
    
  43. 
    
  44.   // Snapshot of all attached renderer IDs.
    
  45.   // Once profiling is finished, this snapshot will be used to query renderers for profiling data.
    
  46.   //
    
  47.   // This map is initialized when profiling starts and updated when a new root is added while profiling;
    
  48.   // Upon completion, it is converted into the exportable ProfilingDataFrontend format.
    
  49.   _initialRendererIDs: Set<number> = new Set();
    
  50. 
    
  51.   // Snapshot of the state of the main Store (including all roots) when profiling started.
    
  52.   // Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling,
    
  53.   // to reconstruct the state of each root for each commit.
    
  54.   // It's okay to use a single root to store this information because node IDs are unique across all roots.
    
  55.   //
    
  56.   // This map is initialized when profiling starts and updated when a new root is added while profiling;
    
  57.   // Upon completion, it is converted into the exportable ProfilingDataFrontend format.
    
  58.   _initialSnapshotsByRootID: Map<number, Map<number, SnapshotNode>> = new Map();
    
  59. 
    
  60.   // Map of root (id) to a list of tree mutation that occur during profiling.
    
  61.   // Once profiling is finished, these mutations can be used, along with the initial tree snapshots,
    
  62.   // to reconstruct the state of each root for each commit.
    
  63.   //
    
  64.   // This map is only updated while profiling is in progress;
    
  65.   // Upon completion, it is converted into the exportable ProfilingDataFrontend format.
    
  66.   _inProgressOperationsByRootID: Map<number, Array<Array<number>>> = new Map();
    
  67. 
    
  68.   // The backend is currently profiling.
    
  69.   // When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
    
  70.   _isProfiling: boolean = false;
    
  71. 
    
  72.   // Tracks whether a specific renderer logged any profiling data during the most recent session.
    
  73.   _rendererIDsThatReportedProfilingData: Set<number> = new Set();
    
  74. 
    
  75.   // After profiling, data is requested from each attached renderer using this queue.
    
  76.   // So long as this queue is not empty, the store is retrieving and processing profiling data from the backend.
    
  77.   _rendererQueue: Set<number> = new Set();
    
  78. 
    
  79.   _store: Store;
    
  80. 
    
  81.   constructor(
    
  82.     bridge: FrontendBridge,
    
  83.     store: Store,
    
  84.     defaultIsProfiling: boolean,
    
  85.   ) {
    
  86.     super();
    
  87. 
    
  88.     this._bridge = bridge;
    
  89.     this._isProfiling = defaultIsProfiling;
    
  90.     this._store = store;
    
  91. 
    
  92.     bridge.addListener('operations', this.onBridgeOperations);
    
  93.     bridge.addListener('profilingData', this.onBridgeProfilingData);
    
  94.     bridge.addListener('profilingStatus', this.onProfilingStatus);
    
  95.     bridge.addListener('shutdown', this.onBridgeShutdown);
    
  96. 
    
  97.     // It's possible that profiling has already started (e.g. "reload and start profiling")
    
  98.     // so the frontend needs to ask the backend for its status after mounting.
    
  99.     bridge.send('getProfilingStatus');
    
  100. 
    
  101.     this._cache = new ProfilingCache(this);
    
  102.   }
    
  103. 
    
  104.   getCommitData(rootID: number, commitIndex: number): CommitDataFrontend {
    
  105.     if (this._dataFrontend !== null) {
    
  106.       const dataForRoot = this._dataFrontend.dataForRoots.get(rootID);
    
  107.       if (dataForRoot != null) {
    
  108.         const commitDatum = dataForRoot.commitData[commitIndex];
    
  109.         if (commitDatum != null) {
    
  110.           return commitDatum;
    
  111.         }
    
  112.       }
    
  113.     }
    
  114. 
    
  115.     throw Error(
    
  116.       `Could not find commit data for root "${rootID}" and commit "${commitIndex}"`,
    
  117.     );
    
  118.   }
    
  119. 
    
  120.   getDataForRoot(rootID: number): ProfilingDataForRootFrontend {
    
  121.     if (this._dataFrontend !== null) {
    
  122.       const dataForRoot = this._dataFrontend.dataForRoots.get(rootID);
    
  123.       if (dataForRoot != null) {
    
  124.         return dataForRoot;
    
  125.       }
    
  126.     }
    
  127. 
    
  128.     throw Error(`Could not find commit data for root "${rootID}"`);
    
  129.   }
    
  130. 
    
  131.   // Profiling data has been recorded for at least one root.
    
  132.   get didRecordCommits(): boolean {
    
  133.     return (
    
  134.       this._dataFrontend !== null && this._dataFrontend.dataForRoots.size > 0
    
  135.     );
    
  136.   }
    
  137. 
    
  138.   get isProcessingData(): boolean {
    
  139.     return this._rendererQueue.size > 0 || this._dataBackends.length > 0;
    
  140.   }
    
  141. 
    
  142.   get isProfiling(): boolean {
    
  143.     return this._isProfiling;
    
  144.   }
    
  145. 
    
  146.   get profilingCache(): ProfilingCache {
    
  147.     return this._cache;
    
  148.   }
    
  149. 
    
  150.   get profilingData(): ProfilingDataFrontend | null {
    
  151.     return this._dataFrontend;
    
  152.   }
    
  153.   set profilingData(value: ProfilingDataFrontend | null): void {
    
  154.     if (this._isProfiling) {
    
  155.       console.warn(
    
  156.         'Profiling data cannot be updated while profiling is in progress.',
    
  157.       );
    
  158.       return;
    
  159.     }
    
  160. 
    
  161.     this._dataBackends.splice(0);
    
  162.     this._dataFrontend = value;
    
  163.     this._initialRendererIDs.clear();
    
  164.     this._initialSnapshotsByRootID.clear();
    
  165.     this._inProgressOperationsByRootID.clear();
    
  166.     this._cache.invalidate();
    
  167. 
    
  168.     this.emit('profilingData');
    
  169.   }
    
  170. 
    
  171.   clear(): void {
    
  172.     this._dataBackends.splice(0);
    
  173.     this._dataFrontend = null;
    
  174.     this._initialRendererIDs.clear();
    
  175.     this._initialSnapshotsByRootID.clear();
    
  176.     this._inProgressOperationsByRootID.clear();
    
  177.     this._rendererQueue.clear();
    
  178. 
    
  179.     // Invalidate suspense cache if profiling data is being (re-)recorded.
    
  180.     // Note that we clear now because any existing data is "stale".
    
  181.     this._cache.invalidate();
    
  182. 
    
  183.     this.emit('profilingData');
    
  184.   }
    
  185. 
    
  186.   startProfiling(): void {
    
  187.     this._bridge.send('startProfiling', this._store.recordChangeDescriptions);
    
  188. 
    
  189.     // Don't actually update the local profiling boolean yet!
    
  190.     // Wait for onProfilingStatus() to confirm the status has changed.
    
  191.     // This ensures the frontend and backend are in sync wrt which commits were profiled.
    
  192.     // We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
    
  193.   }
    
  194. 
    
  195.   stopProfiling(): void {
    
  196.     this._bridge.send('stopProfiling');
    
  197. 
    
  198.     // Don't actually update the local profiling boolean yet!
    
  199.     // Wait for onProfilingStatus() to confirm the status has changed.
    
  200.     // This ensures the frontend and backend are in sync wrt which commits were profiled.
    
  201.     // We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
    
  202.   }
    
  203. 
    
  204.   _takeProfilingSnapshotRecursive: (
    
  205.     elementID: number,
    
  206.     profilingSnapshots: Map<number, SnapshotNode>,
    
  207.   ) => void = (elementID, profilingSnapshots) => {
    
  208.     const element = this._store.getElementByID(elementID);
    
  209.     if (element !== null) {
    
  210.       const snapshotNode: SnapshotNode = {
    
  211.         id: elementID,
    
  212.         children: element.children.slice(0),
    
  213.         displayName: element.displayName,
    
  214.         hocDisplayNames: element.hocDisplayNames,
    
  215.         key: element.key,
    
  216.         type: element.type,
    
  217.       };
    
  218.       profilingSnapshots.set(elementID, snapshotNode);
    
  219. 
    
  220.       element.children.forEach(childID =>
    
  221.         this._takeProfilingSnapshotRecursive(childID, profilingSnapshots),
    
  222.       );
    
  223.     }
    
  224.   };
    
  225. 
    
  226.   onBridgeOperations: (operations: Array<number>) => void = operations => {
    
  227.     // The first two values are always rendererID and rootID
    
  228.     const rendererID = operations[0];
    
  229.     const rootID = operations[1];
    
  230. 
    
  231.     if (this._isProfiling) {
    
  232.       let profilingOperations = this._inProgressOperationsByRootID.get(rootID);
    
  233.       if (profilingOperations == null) {
    
  234.         profilingOperations = [operations];
    
  235.         this._inProgressOperationsByRootID.set(rootID, profilingOperations);
    
  236.       } else {
    
  237.         profilingOperations.push(operations);
    
  238.       }
    
  239. 
    
  240.       if (!this._initialRendererIDs.has(rendererID)) {
    
  241.         this._initialRendererIDs.add(rendererID);
    
  242.       }
    
  243. 
    
  244.       if (!this._initialSnapshotsByRootID.has(rootID)) {
    
  245.         this._initialSnapshotsByRootID.set(rootID, new Map());
    
  246.       }
    
  247. 
    
  248.       this._rendererIDsThatReportedProfilingData.add(rendererID);
    
  249.     }
    
  250.   };
    
  251. 
    
  252.   onBridgeProfilingData: (dataBackend: ProfilingDataBackend) => void =
    
  253.     dataBackend => {
    
  254.       if (this._isProfiling) {
    
  255.         // This should never happen, but if it does- ignore previous profiling data.
    
  256.         return;
    
  257.       }
    
  258. 
    
  259.       const {rendererID} = dataBackend;
    
  260. 
    
  261.       if (!this._rendererQueue.has(rendererID)) {
    
  262.         throw Error(
    
  263.           `Unexpected profiling data update from renderer "${rendererID}"`,
    
  264.         );
    
  265.       }
    
  266. 
    
  267.       this._dataBackends.push(dataBackend);
    
  268.       this._rendererQueue.delete(rendererID);
    
  269. 
    
  270.       if (this._rendererQueue.size === 0) {
    
  271.         this._dataFrontend = prepareProfilingDataFrontendFromBackendAndStore(
    
  272.           this._dataBackends,
    
  273.           this._inProgressOperationsByRootID,
    
  274.           this._initialSnapshotsByRootID,
    
  275.         );
    
  276. 
    
  277.         this._dataBackends.splice(0);
    
  278. 
    
  279.         this.emit('isProcessingData');
    
  280.       }
    
  281.     };
    
  282. 
    
  283.   onBridgeShutdown: () => void = () => {
    
  284.     this._bridge.removeListener('operations', this.onBridgeOperations);
    
  285.     this._bridge.removeListener('profilingData', this.onBridgeProfilingData);
    
  286.     this._bridge.removeListener('profilingStatus', this.onProfilingStatus);
    
  287.     this._bridge.removeListener('shutdown', this.onBridgeShutdown);
    
  288.   };
    
  289. 
    
  290.   onProfilingStatus: (isProfiling: boolean) => void = isProfiling => {
    
  291.     if (isProfiling) {
    
  292.       this._dataBackends.splice(0);
    
  293.       this._dataFrontend = null;
    
  294.       this._initialRendererIDs.clear();
    
  295.       this._initialSnapshotsByRootID.clear();
    
  296.       this._inProgressOperationsByRootID.clear();
    
  297.       this._rendererIDsThatReportedProfilingData.clear();
    
  298.       this._rendererQueue.clear();
    
  299. 
    
  300.       // Record all renderer IDs initially too (in case of unmount)
    
  301.       // eslint-disable-next-line no-for-of-loops/no-for-of-loops
    
  302.       for (const rendererID of this._store.rootIDToRendererID.values()) {
    
  303.         if (!this._initialRendererIDs.has(rendererID)) {
    
  304.           this._initialRendererIDs.add(rendererID);
    
  305.         }
    
  306.       }
    
  307. 
    
  308.       // Record snapshot of tree at the time profiling is started.
    
  309.       // This info is required to handle cases of e.g. nodes being removed during profiling.
    
  310.       this._store.roots.forEach(rootID => {
    
  311.         const profilingSnapshots = new Map<number, SnapshotNode>();
    
  312.         this._initialSnapshotsByRootID.set(rootID, profilingSnapshots);
    
  313.         this._takeProfilingSnapshotRecursive(rootID, profilingSnapshots);
    
  314.       });
    
  315.     }
    
  316. 
    
  317.     if (this._isProfiling !== isProfiling) {
    
  318.       this._isProfiling = isProfiling;
    
  319. 
    
  320.       // Invalidate suspense cache if profiling data is being (re-)recorded.
    
  321.       // Note that we clear again, in case any views read from the cache while profiling.
    
  322.       // (That would have resolved a now-stale value without any profiling data.)
    
  323.       this._cache.invalidate();
    
  324. 
    
  325.       this.emit('isProfiling');
    
  326. 
    
  327.       // If we've just finished a profiling session, we need to fetch data stored in each renderer interface
    
  328.       // and re-assemble it on the front-end into a format (ProfilingDataFrontend) that can power the Profiler UI.
    
  329.       // During this time, DevTools UI should probably not be interactive.
    
  330.       if (!isProfiling) {
    
  331.         this._dataBackends.splice(0);
    
  332.         this._rendererQueue.clear();
    
  333. 
    
  334.         // Only request data from renderers that actually logged it.
    
  335.         // This avoids unnecessary bridge requests and also avoids edge case mixed renderer bugs.
    
  336.         // (e.g. when v15 and v16 are both present)
    
  337.         this._rendererIDsThatReportedProfilingData.forEach(rendererID => {
    
  338.           if (!this._rendererQueue.has(rendererID)) {
    
  339.             this._rendererQueue.add(rendererID);
    
  340. 
    
  341.             this._bridge.send('getProfilingData', {rendererID});
    
  342.           }
    
  343.         });
    
  344. 
    
  345.         this.emit('isProcessingData');
    
  346.       }
    
  347.     }
    
  348.   };
    
  349. }