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 {formatDuration} from './utils';
    
  11. import ProfilerStore from 'react-devtools-shared/src/devtools/ProfilerStore';
    
  12. 
    
  13. import type {CommitTree} from './types';
    
  14. 
    
  15. export type ChartNode = {
    
  16.   actualDuration: number,
    
  17.   didRender: boolean,
    
  18.   id: number,
    
  19.   label: string,
    
  20.   name: string,
    
  21.   offset: number,
    
  22.   selfDuration: number,
    
  23.   treeBaseDuration: number,
    
  24. };
    
  25. 
    
  26. export type ChartData = {
    
  27.   baseDuration: number,
    
  28.   depth: number,
    
  29.   idToDepthMap: Map<number, number>,
    
  30.   maxSelfDuration: number,
    
  31.   renderPathNodes: Set<number>,
    
  32.   rows: Array<Array<ChartNode>>,
    
  33. };
    
  34. 
    
  35. const cachedChartData: Map<string, ChartData> = new Map();
    
  36. 
    
  37. export function getChartData({
    
  38.   commitIndex,
    
  39.   commitTree,
    
  40.   profilerStore,
    
  41.   rootID,
    
  42. }: {
    
  43.   commitIndex: number,
    
  44.   commitTree: CommitTree,
    
  45.   profilerStore: ProfilerStore,
    
  46.   rootID: number,
    
  47. }): ChartData {
    
  48.   const commitDatum = profilerStore.getCommitData(rootID, commitIndex);
    
  49. 
    
  50.   const {fiberActualDurations, fiberSelfDurations} = commitDatum;
    
  51.   const {nodes} = commitTree;
    
  52. 
    
  53.   const chartDataKey = `${rootID}-${commitIndex}`;
    
  54.   if (cachedChartData.has(chartDataKey)) {
    
  55.     return ((cachedChartData.get(chartDataKey): any): ChartData);
    
  56.   }
    
  57. 
    
  58.   const idToDepthMap: Map<number, number> = new Map();
    
  59.   const renderPathNodes: Set<number> = new Set();
    
  60.   const rows: Array<Array<ChartNode>> = [];
    
  61. 
    
  62.   let maxDepth = 0;
    
  63.   let maxSelfDuration = 0;
    
  64. 
    
  65.   // Generate flame graph structure using tree base durations.
    
  66.   const walkTree = (
    
  67.     id: number,
    
  68.     rightOffset: number,
    
  69.     currentDepth: number,
    
  70.   ): ChartNode => {
    
  71.     idToDepthMap.set(id, currentDepth);
    
  72. 
    
  73.     const node = nodes.get(id);
    
  74.     if (node == null) {
    
  75.       throw Error(`Could not find node with id "${id}" in commit tree`);
    
  76.     }
    
  77. 
    
  78.     const {children, displayName, hocDisplayNames, key, treeBaseDuration} =
    
  79.       node;
    
  80. 
    
  81.     const actualDuration = fiberActualDurations.get(id) || 0;
    
  82.     const selfDuration = fiberSelfDurations.get(id) || 0;
    
  83.     const didRender = fiberActualDurations.has(id);
    
  84. 
    
  85.     const name = displayName || 'Anonymous';
    
  86.     const maybeKey = key !== null ? ` key="${key}"` : '';
    
  87. 
    
  88.     let maybeBadge = '';
    
  89.     if (hocDisplayNames !== null && hocDisplayNames.length > 0) {
    
  90.       maybeBadge = ` (${hocDisplayNames[0]})`;
    
  91.     }
    
  92. 
    
  93.     let label = `${name}${maybeBadge}${maybeKey}`;
    
  94.     if (didRender) {
    
  95.       label += ` (${formatDuration(selfDuration)}ms of ${formatDuration(
    
  96.         actualDuration,
    
  97.       )}ms)`;
    
  98.     }
    
  99. 
    
  100.     maxDepth = Math.max(maxDepth, currentDepth);
    
  101.     maxSelfDuration = Math.max(maxSelfDuration, selfDuration);
    
  102. 
    
  103.     const chartNode: ChartNode = {
    
  104.       actualDuration,
    
  105.       didRender,
    
  106.       id,
    
  107.       label,
    
  108.       name,
    
  109.       offset: rightOffset - treeBaseDuration,
    
  110.       selfDuration,
    
  111.       treeBaseDuration,
    
  112.     };
    
  113. 
    
  114.     if (currentDepth > rows.length) {
    
  115.       rows.push([chartNode]);
    
  116.     } else {
    
  117.       rows[currentDepth - 1].push(chartNode);
    
  118.     }
    
  119. 
    
  120.     for (let i = children.length - 1; i >= 0; i--) {
    
  121.       const childID = children[i];
    
  122.       const childChartNode: $FlowFixMe = walkTree(
    
  123.         childID,
    
  124.         rightOffset,
    
  125.         currentDepth + 1,
    
  126.       );
    
  127.       rightOffset -= childChartNode.treeBaseDuration;
    
  128.     }
    
  129. 
    
  130.     return chartNode;
    
  131.   };
    
  132. 
    
  133.   let baseDuration = 0;
    
  134. 
    
  135.   // Special case to handle unmounted roots.
    
  136.   if (nodes.size > 0) {
    
  137.     // Skip over the root; we don't want to show it in the flamegraph.
    
  138.     const root = nodes.get(rootID);
    
  139.     if (root == null) {
    
  140.       throw Error(
    
  141.         `Could not find root node with id "${rootID}" in commit tree`,
    
  142.       );
    
  143.     }
    
  144. 
    
  145.     // Don't assume a single root.
    
  146.     // Component filters or Fragments might lead to multiple "roots" in a flame graph.
    
  147.     for (let i = root.children.length - 1; i >= 0; i--) {
    
  148.       const id = root.children[i];
    
  149.       const node = nodes.get(id);
    
  150.       if (node == null) {
    
  151.         throw Error(`Could not find node with id "${id}" in commit tree`);
    
  152.       }
    
  153.       baseDuration += node.treeBaseDuration;
    
  154.       walkTree(id, baseDuration, 1);
    
  155.     }
    
  156. 
    
  157.     fiberActualDurations.forEach((duration, id) => {
    
  158.       let node = nodes.get(id);
    
  159.       if (node != null) {
    
  160.         let currentID = node.parentID;
    
  161.         while (currentID !== 0) {
    
  162.           if (renderPathNodes.has(currentID)) {
    
  163.             // We've already walked this path; we can skip it.
    
  164.             break;
    
  165.           } else {
    
  166.             renderPathNodes.add(currentID);
    
  167.           }
    
  168. 
    
  169.           node = nodes.get(currentID);
    
  170.           currentID = node != null ? node.parentID : 0;
    
  171.         }
    
  172.       }
    
  173.     });
    
  174.   }
    
  175. 
    
  176.   const chartData = {
    
  177.     baseDuration,
    
  178.     depth: maxDepth,
    
  179.     idToDepthMap,
    
  180.     maxSelfDuration,
    
  181.     renderPathNodes,
    
  182.     rows,
    
  183.   };
    
  184. 
    
  185.   cachedChartData.set(chartDataKey, chartData);
    
  186. 
    
  187.   return chartData;
    
  188. }
    
  189. 
    
  190. export function invalidateChartData(): void {
    
  191.   cachedChartData.clear();
    
  192. }