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 {
    
  11.   __DEBUG__,
    
  12.   TREE_OPERATION_ADD,
    
  13.   TREE_OPERATION_REMOVE,
    
  14.   TREE_OPERATION_REMOVE_ROOT,
    
  15.   TREE_OPERATION_REORDER_CHILDREN,
    
  16.   TREE_OPERATION_SET_SUBTREE_MODE,
    
  17.   TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
    
  18.   TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
    
  19. } from 'react-devtools-shared/src/constants';
    
  20. import {utfDecodeString} from 'react-devtools-shared/src/utils';
    
  21. import {ElementTypeRoot} from 'react-devtools-shared/src/frontend/types';
    
  22. import ProfilerStore from 'react-devtools-shared/src/devtools/ProfilerStore';
    
  23. 
    
  24. import type {ElementType} from 'react-devtools-shared/src/frontend/types';
    
  25. import type {
    
  26.   CommitTree,
    
  27.   CommitTreeNode,
    
  28.   ProfilingDataForRootFrontend,
    
  29. } from 'react-devtools-shared/src/devtools/views/Profiler/types';
    
  30. 
    
  31. const debug = (methodName: string, ...args: Array<string>) => {
    
  32.   if (__DEBUG__) {
    
  33.     console.log(
    
  34.       `%cCommitTreeBuilder %c${methodName}`,
    
  35.       'color: pink; font-weight: bold;',
    
  36.       'font-weight: bold;',
    
  37.       ...args,
    
  38.     );
    
  39.   }
    
  40. };
    
  41. 
    
  42. const rootToCommitTreeMap: Map<number, Array<CommitTree>> = new Map();
    
  43. 
    
  44. export function getCommitTree({
    
  45.   commitIndex,
    
  46.   profilerStore,
    
  47.   rootID,
    
  48. }: {
    
  49.   commitIndex: number,
    
  50.   profilerStore: ProfilerStore,
    
  51.   rootID: number,
    
  52. }): CommitTree {
    
  53.   if (!rootToCommitTreeMap.has(rootID)) {
    
  54.     rootToCommitTreeMap.set(rootID, []);
    
  55.   }
    
  56. 
    
  57.   const commitTrees = ((rootToCommitTreeMap.get(
    
  58.     rootID,
    
  59.   ): any): Array<CommitTree>);
    
  60.   if (commitIndex < commitTrees.length) {
    
  61.     return commitTrees[commitIndex];
    
  62.   }
    
  63. 
    
  64.   const {profilingData} = profilerStore;
    
  65.   if (profilingData === null) {
    
  66.     throw Error(`No profiling data available`);
    
  67.   }
    
  68. 
    
  69.   const dataForRoot = profilingData.dataForRoots.get(rootID);
    
  70.   if (dataForRoot == null) {
    
  71.     throw Error(`Could not find profiling data for root "${rootID}"`);
    
  72.   }
    
  73. 
    
  74.   const {operations} = dataForRoot;
    
  75.   if (operations.length <= commitIndex) {
    
  76.     throw Error(
    
  77.       `getCommitTree(): Invalid commit "${commitIndex}" for root "${rootID}". There are only "${operations.length}" commits.`,
    
  78.     );
    
  79.   }
    
  80. 
    
  81.   let commitTree: CommitTree = ((null: any): CommitTree);
    
  82.   for (let index = commitTrees.length; index <= commitIndex; index++) {
    
  83.     // Commits are generated sequentially and cached.
    
  84.     // If this is the very first commit, start with the cached snapshot and apply the first mutation.
    
  85.     // Otherwise load (or generate) the previous commit and append a mutation to it.
    
  86.     if (index === 0) {
    
  87.       const nodes = new Map<number, CommitTreeNode>();
    
  88. 
    
  89.       // Construct the initial tree.
    
  90.       recursivelyInitializeTree(rootID, 0, nodes, dataForRoot);
    
  91. 
    
  92.       // Mutate the tree
    
  93.       if (operations != null && index < operations.length) {
    
  94.         commitTree = updateTree({nodes, rootID}, operations[index]);
    
  95. 
    
  96.         if (__DEBUG__) {
    
  97.           __printTree(commitTree);
    
  98.         }
    
  99. 
    
  100.         commitTrees.push(commitTree);
    
  101.       }
    
  102.     } else {
    
  103.       const previousCommitTree = commitTrees[index - 1];
    
  104.       commitTree = updateTree(previousCommitTree, operations[index]);
    
  105. 
    
  106.       if (__DEBUG__) {
    
  107.         __printTree(commitTree);
    
  108.       }
    
  109. 
    
  110.       commitTrees.push(commitTree);
    
  111.     }
    
  112.   }
    
  113. 
    
  114.   return commitTree;
    
  115. }
    
  116. 
    
  117. function recursivelyInitializeTree(
    
  118.   id: number,
    
  119.   parentID: number,
    
  120.   nodes: Map<number, CommitTreeNode>,
    
  121.   dataForRoot: ProfilingDataForRootFrontend,
    
  122. ): void {
    
  123.   const node = dataForRoot.snapshots.get(id);
    
  124.   if (node != null) {
    
  125.     nodes.set(id, {
    
  126.       id,
    
  127.       children: node.children,
    
  128.       displayName: node.displayName,
    
  129.       hocDisplayNames: node.hocDisplayNames,
    
  130.       key: node.key,
    
  131.       parentID,
    
  132.       treeBaseDuration: ((dataForRoot.initialTreeBaseDurations.get(
    
  133.         id,
    
  134.       ): any): number),
    
  135.       type: node.type,
    
  136.     });
    
  137. 
    
  138.     node.children.forEach(childID =>
    
  139.       recursivelyInitializeTree(childID, id, nodes, dataForRoot),
    
  140.     );
    
  141.   }
    
  142. }
    
  143. 
    
  144. function updateTree(
    
  145.   commitTree: CommitTree,
    
  146.   operations: Array<number>,
    
  147. ): CommitTree {
    
  148.   // Clone the original tree so edits don't affect it.
    
  149.   const nodes = new Map(commitTree.nodes);
    
  150. 
    
  151.   // Clone nodes before mutating them so edits don't affect them.
    
  152.   const getClonedNode = (id: number): CommitTreeNode => {
    
  153.     // $FlowFixMe[prop-missing] - recommended fix is to use object spread operator
    
  154.     const clonedNode = ((Object.assign(
    
  155.       {},
    
  156.       nodes.get(id),
    
  157.     ): any): CommitTreeNode);
    
  158.     nodes.set(id, clonedNode);
    
  159.     return clonedNode;
    
  160.   };
    
  161. 
    
  162.   let i = 2;
    
  163.   let id: number = ((null: any): number);
    
  164. 
    
  165.   // Reassemble the string table.
    
  166.   const stringTable: Array<null | string> = [
    
  167.     null, // ID = 0 corresponds to the null string.
    
  168.   ];
    
  169.   const stringTableSize = operations[i++];
    
  170.   const stringTableEnd = i + stringTableSize;
    
  171.   while (i < stringTableEnd) {
    
  172.     const nextLength = operations[i++];
    
  173.     const nextString = utfDecodeString(
    
  174.       (operations.slice(i, i + nextLength): any),
    
  175.     );
    
  176.     stringTable.push(nextString);
    
  177.     i += nextLength;
    
  178.   }
    
  179. 
    
  180.   while (i < operations.length) {
    
  181.     const operation = operations[i];
    
  182. 
    
  183.     switch (operation) {
    
  184.       case TREE_OPERATION_ADD: {
    
  185.         id = ((operations[i + 1]: any): number);
    
  186.         const type = ((operations[i + 2]: any): ElementType);
    
  187. 
    
  188.         i += 3;
    
  189. 
    
  190.         if (nodes.has(id)) {
    
  191.           throw new Error(
    
  192.             `Commit tree already contains fiber "${id}". This is a bug in React DevTools.`,
    
  193.           );
    
  194.         }
    
  195. 
    
  196.         if (type === ElementTypeRoot) {
    
  197.           i++; // isStrictModeCompliant
    
  198.           i++; // Profiling flag
    
  199.           i++; // supportsStrictMode flag
    
  200.           i++; // hasOwnerMetadata flag
    
  201. 
    
  202.           if (__DEBUG__) {
    
  203.             debug('Add', `new root fiber ${id}`);
    
  204.           }
    
  205. 
    
  206.           const node: CommitTreeNode = {
    
  207.             children: [],
    
  208.             displayName: null,
    
  209.             hocDisplayNames: null,
    
  210.             id,
    
  211.             key: null,
    
  212.             parentID: 0,
    
  213.             treeBaseDuration: 0, // This will be updated by a subsequent operation
    
  214.             type,
    
  215.           };
    
  216. 
    
  217.           nodes.set(id, node);
    
  218.         } else {
    
  219.           const parentID = ((operations[i]: any): number);
    
  220.           i++;
    
  221. 
    
  222.           i++; // ownerID
    
  223. 
    
  224.           const displayNameStringID = operations[i];
    
  225.           const displayName = stringTable[displayNameStringID];
    
  226.           i++;
    
  227. 
    
  228.           const keyStringID = operations[i];
    
  229.           const key = stringTable[keyStringID];
    
  230.           i++;
    
  231. 
    
  232.           if (__DEBUG__) {
    
  233.             debug(
    
  234.               'Add',
    
  235.               `fiber ${id} (${displayName || 'null'}) as child of ${parentID}`,
    
  236.             );
    
  237.           }
    
  238. 
    
  239.           const parentNode = getClonedNode(parentID);
    
  240.           parentNode.children = parentNode.children.concat(id);
    
  241. 
    
  242.           const node: CommitTreeNode = {
    
  243.             children: [],
    
  244.             displayName,
    
  245.             hocDisplayNames: null,
    
  246.             id,
    
  247.             key,
    
  248.             parentID,
    
  249.             treeBaseDuration: 0, // This will be updated by a subsequent operation
    
  250.             type,
    
  251.           };
    
  252. 
    
  253.           nodes.set(id, node);
    
  254.         }
    
  255. 
    
  256.         break;
    
  257.       }
    
  258.       case TREE_OPERATION_REMOVE: {
    
  259.         const removeLength = ((operations[i + 1]: any): number);
    
  260.         i += 2;
    
  261. 
    
  262.         for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
    
  263.           id = ((operations[i]: any): number);
    
  264.           i++;
    
  265. 
    
  266.           if (!nodes.has(id)) {
    
  267.             throw new Error(
    
  268.               `Commit tree does not contain fiber "${id}". This is a bug in React DevTools.`,
    
  269.             );
    
  270.           }
    
  271. 
    
  272.           const node = getClonedNode(id);
    
  273.           const parentID = node.parentID;
    
  274. 
    
  275.           nodes.delete(id);
    
  276. 
    
  277.           if (!nodes.has(parentID)) {
    
  278.             // No-op
    
  279.           } else {
    
  280.             const parentNode = getClonedNode(parentID);
    
  281. 
    
  282.             if (__DEBUG__) {
    
  283.               debug('Remove', `fiber ${id} from parent ${parentID}`);
    
  284.             }
    
  285. 
    
  286.             parentNode.children = parentNode.children.filter(
    
  287.               childID => childID !== id,
    
  288.             );
    
  289.           }
    
  290.         }
    
  291.         break;
    
  292.       }
    
  293.       case TREE_OPERATION_REMOVE_ROOT: {
    
  294.         throw Error('Operation REMOVE_ROOT is not supported while profiling.');
    
  295.       }
    
  296.       case TREE_OPERATION_REORDER_CHILDREN: {
    
  297.         id = ((operations[i + 1]: any): number);
    
  298.         const numChildren = ((operations[i + 2]: any): number);
    
  299.         const children = ((operations.slice(
    
  300.           i + 3,
    
  301.           i + 3 + numChildren,
    
  302.         ): any): Array<number>);
    
  303. 
    
  304.         i = i + 3 + numChildren;
    
  305. 
    
  306.         if (__DEBUG__) {
    
  307.           debug('Re-order', `fiber ${id} children ${children.join(',')}`);
    
  308.         }
    
  309. 
    
  310.         const node = getClonedNode(id);
    
  311.         node.children = Array.from(children);
    
  312. 
    
  313.         break;
    
  314.       }
    
  315.       case TREE_OPERATION_SET_SUBTREE_MODE: {
    
  316.         id = operations[i + 1];
    
  317.         const mode = operations[i + 1];
    
  318. 
    
  319.         i += 3;
    
  320. 
    
  321.         if (__DEBUG__) {
    
  322.           debug('Subtree mode', `Subtree with root ${id} set to mode ${mode}`);
    
  323.         }
    
  324.         break;
    
  325.       }
    
  326.       case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: {
    
  327.         id = operations[i + 1];
    
  328. 
    
  329.         const node = getClonedNode(id);
    
  330.         node.treeBaseDuration = operations[i + 2] / 1000; // Convert microseconds back to milliseconds;
    
  331. 
    
  332.         if (__DEBUG__) {
    
  333.           debug(
    
  334.             'Update',
    
  335.             `fiber ${id} treeBaseDuration to ${node.treeBaseDuration}`,
    
  336.           );
    
  337.         }
    
  338. 
    
  339.         i += 3;
    
  340.         break;
    
  341.       }
    
  342.       case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: {
    
  343.         id = operations[i + 1];
    
  344.         const numErrors = operations[i + 2];
    
  345.         const numWarnings = operations[i + 3];
    
  346. 
    
  347.         i += 4;
    
  348. 
    
  349.         if (__DEBUG__) {
    
  350.           debug(
    
  351.             'Warnings and Errors update',
    
  352.             `fiber ${id} has ${numErrors} errors and ${numWarnings} warnings`,
    
  353.           );
    
  354.         }
    
  355.         break;
    
  356.       }
    
  357. 
    
  358.       default:
    
  359.         throw Error(`Unsupported Bridge operation "${operation}"`);
    
  360.     }
    
  361.   }
    
  362. 
    
  363.   return {
    
  364.     nodes,
    
  365.     rootID: commitTree.rootID,
    
  366.   };
    
  367. }
    
  368. 
    
  369. export function invalidateCommitTrees(): void {
    
  370.   rootToCommitTreeMap.clear();
    
  371. }
    
  372. 
    
  373. // DEBUG
    
  374. const __printTree = (commitTree: CommitTree) => {
    
  375.   if (__DEBUG__) {
    
  376.     const {nodes, rootID} = commitTree;
    
  377.     console.group('__printTree()');
    
  378.     const queue = [rootID, 0];
    
  379.     while (queue.length > 0) {
    
  380.       const id = queue.shift();
    
  381.       const depth = queue.shift();
    
  382. 
    
  383.       const node = nodes.get(id);
    
  384.       if (node == null) {
    
  385.         throw Error(`Could not find node with id "${id}" in commit tree`);
    
  386.       }
    
  387. 
    
  388.       console.log(
    
  389.         `${''.repeat(depth)}${node.id}:${node.displayName || ''} ${
    
  390.           node.key ? `key:"${node.key}"` : ''
    
  391.         } (${node.treeBaseDuration})`,
    
  392.       );
    
  393. 
    
  394.       node.children.forEach(childID => {
    
  395.         queue.push(childID, depth + 1);
    
  396.       });
    
  397.     }
    
  398.     console.groupEnd();
    
  399.   }
    
  400. };