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 {PROFILER_EXPORT_VERSION} from 'react-devtools-shared/src/constants';
    
  11. import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
    
  12. 
    
  13. import type {ProfilingDataBackend} from 'react-devtools-shared/src/backend/types';
    
  14. import type {
    
  15.   ProfilingDataExport,
    
  16.   ProfilingDataForRootExport,
    
  17.   ProfilingDataForRootFrontend,
    
  18.   ProfilingDataFrontend,
    
  19.   SnapshotNode,
    
  20. } from './types';
    
  21. import type {
    
  22.   TimelineData,
    
  23.   TimelineDataExport,
    
  24. } from 'react-devtools-timeline/src/types';
    
  25. 
    
  26. const commitGradient = [
    
  27.   'var(--color-commit-gradient-0)',
    
  28.   'var(--color-commit-gradient-1)',
    
  29.   'var(--color-commit-gradient-2)',
    
  30.   'var(--color-commit-gradient-3)',
    
  31.   'var(--color-commit-gradient-4)',
    
  32.   'var(--color-commit-gradient-5)',
    
  33.   'var(--color-commit-gradient-6)',
    
  34.   'var(--color-commit-gradient-7)',
    
  35.   'var(--color-commit-gradient-8)',
    
  36.   'var(--color-commit-gradient-9)',
    
  37. ];
    
  38. 
    
  39. // Combines info from the Store (frontend) and renderer interfaces (backend) into the format required by the Profiler UI.
    
  40. // This format can then be quickly exported (and re-imported).
    
  41. export function prepareProfilingDataFrontendFromBackendAndStore(
    
  42.   dataBackends: Array<ProfilingDataBackend>,
    
  43.   operationsByRootID: Map<number, Array<Array<number>>>,
    
  44.   snapshotsByRootID: Map<number, Map<number, SnapshotNode>>,
    
  45. ): ProfilingDataFrontend {
    
  46.   const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map();
    
  47. 
    
  48.   const timelineDataArray = [];
    
  49. 
    
  50.   dataBackends.forEach(dataBackend => {
    
  51.     const {timelineData} = dataBackend;
    
  52.     if (timelineData != null) {
    
  53.       const {
    
  54.         batchUIDToMeasuresKeyValueArray,
    
  55.         internalModuleSourceToRanges,
    
  56.         laneToLabelKeyValueArray,
    
  57.         laneToReactMeasureKeyValueArray,
    
  58.         ...rest
    
  59.       } = timelineData;
    
  60. 
    
  61.       timelineDataArray.push({
    
  62.         ...rest,
    
  63. 
    
  64.         // Most of the data is safe to parse as-is,
    
  65.         // but we need to convert the nested Arrays back to Maps.
    
  66.         batchUIDToMeasuresMap: new Map(batchUIDToMeasuresKeyValueArray),
    
  67.         internalModuleSourceToRanges: new Map(internalModuleSourceToRanges),
    
  68.         laneToLabelMap: new Map(laneToLabelKeyValueArray),
    
  69.         laneToReactMeasureMap: new Map(laneToReactMeasureKeyValueArray),
    
  70.       });
    
  71.     }
    
  72. 
    
  73.     dataBackend.dataForRoots.forEach(
    
  74.       ({commitData, displayName, initialTreeBaseDurations, rootID}) => {
    
  75.         const operations = operationsByRootID.get(rootID);
    
  76.         if (operations == null) {
    
  77.           throw Error(
    
  78.             `Could not find profiling operations for root "${rootID}"`,
    
  79.           );
    
  80.         }
    
  81. 
    
  82.         const snapshots = snapshotsByRootID.get(rootID);
    
  83.         if (snapshots == null) {
    
  84.           throw Error(
    
  85.             `Could not find profiling snapshots for root "${rootID}"`,
    
  86.           );
    
  87.         }
    
  88. 
    
  89.         // Do not filter empty commits from the profiler data!
    
  90.         // Hiding "empty" commits might cause confusion too.
    
  91.         // A commit *did happen* even if none of the components the Profiler is showing were involved.
    
  92.         const convertedCommitData = commitData.map(
    
  93.           (commitDataBackend, commitIndex) => ({
    
  94.             changeDescriptions:
    
  95.               commitDataBackend.changeDescriptions != null
    
  96.                 ? new Map(commitDataBackend.changeDescriptions)
    
  97.                 : null,
    
  98.             duration: commitDataBackend.duration,
    
  99.             effectDuration: commitDataBackend.effectDuration,
    
  100.             fiberActualDurations: new Map(
    
  101.               commitDataBackend.fiberActualDurations,
    
  102.             ),
    
  103.             fiberSelfDurations: new Map(commitDataBackend.fiberSelfDurations),
    
  104.             passiveEffectDuration: commitDataBackend.passiveEffectDuration,
    
  105.             priorityLevel: commitDataBackend.priorityLevel,
    
  106.             timestamp: commitDataBackend.timestamp,
    
  107.             updaters:
    
  108.               commitDataBackend.updaters !== null
    
  109.                 ? commitDataBackend.updaters.map(serializedElement => {
    
  110.                     const [
    
  111.                       serializedElementDisplayName,
    
  112.                       serializedElementHocDisplayNames,
    
  113.                     ] = separateDisplayNameAndHOCs(
    
  114.                       serializedElement.displayName,
    
  115.                       serializedElement.type,
    
  116.                     );
    
  117.                     return {
    
  118.                       ...serializedElement,
    
  119.                       displayName: serializedElementDisplayName,
    
  120.                       hocDisplayNames: serializedElementHocDisplayNames,
    
  121.                     };
    
  122.                   })
    
  123.                 : null,
    
  124.           }),
    
  125.         );
    
  126. 
    
  127.         dataForRoots.set(rootID, {
    
  128.           commitData: convertedCommitData,
    
  129.           displayName,
    
  130.           initialTreeBaseDurations: new Map(initialTreeBaseDurations),
    
  131.           operations,
    
  132.           rootID,
    
  133.           snapshots,
    
  134.         });
    
  135.       },
    
  136.     );
    
  137.   });
    
  138. 
    
  139.   return {dataForRoots, imported: false, timelineData: timelineDataArray};
    
  140. }
    
  141. 
    
  142. // Converts a Profiling data export into the format required by the Store.
    
  143. export function prepareProfilingDataFrontendFromExport(
    
  144.   profilingDataExport: ProfilingDataExport,
    
  145. ): ProfilingDataFrontend {
    
  146.   const {version} = profilingDataExport;
    
  147. 
    
  148.   if (version !== PROFILER_EXPORT_VERSION) {
    
  149.     throw Error(
    
  150.       `Unsupported profile export version "${version}". Supported version is "${PROFILER_EXPORT_VERSION}".`,
    
  151.     );
    
  152.   }
    
  153. 
    
  154.   const timelineData: Array<TimelineData> = profilingDataExport.timelineData
    
  155.     ? profilingDataExport.timelineData.map(
    
  156.         ({
    
  157.           batchUIDToMeasuresKeyValueArray,
    
  158.           componentMeasures,
    
  159.           duration,
    
  160.           flamechart,
    
  161.           internalModuleSourceToRanges,
    
  162.           laneToLabelKeyValueArray,
    
  163.           laneToReactMeasureKeyValueArray,
    
  164.           nativeEvents,
    
  165.           networkMeasures,
    
  166.           otherUserTimingMarks,
    
  167.           reactVersion,
    
  168.           schedulingEvents,
    
  169.           snapshots,
    
  170.           snapshotHeight,
    
  171.           startTime,
    
  172.           suspenseEvents,
    
  173.           thrownErrors,
    
  174.         }) => ({
    
  175.           // Most of the data is safe to parse as-is,
    
  176.           // but we need to convert the nested Arrays back to Maps.
    
  177.           batchUIDToMeasuresMap: new Map(batchUIDToMeasuresKeyValueArray),
    
  178.           componentMeasures,
    
  179.           duration,
    
  180.           flamechart,
    
  181.           internalModuleSourceToRanges: new Map(internalModuleSourceToRanges),
    
  182.           laneToLabelMap: new Map(laneToLabelKeyValueArray),
    
  183.           laneToReactMeasureMap: new Map(laneToReactMeasureKeyValueArray),
    
  184.           nativeEvents,
    
  185.           networkMeasures,
    
  186.           otherUserTimingMarks,
    
  187.           reactVersion,
    
  188.           schedulingEvents,
    
  189.           snapshots,
    
  190.           snapshotHeight,
    
  191.           startTime,
    
  192.           suspenseEvents,
    
  193.           thrownErrors,
    
  194.         }),
    
  195.       )
    
  196.     : [];
    
  197. 
    
  198.   const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map();
    
  199.   profilingDataExport.dataForRoots.forEach(
    
  200.     ({
    
  201.       commitData,
    
  202.       displayName,
    
  203.       initialTreeBaseDurations,
    
  204.       operations,
    
  205.       rootID,
    
  206.       snapshots,
    
  207.     }) => {
    
  208.       dataForRoots.set(rootID, {
    
  209.         commitData: commitData.map(
    
  210.           ({
    
  211.             changeDescriptions,
    
  212.             duration,
    
  213.             effectDuration,
    
  214.             fiberActualDurations,
    
  215.             fiberSelfDurations,
    
  216.             passiveEffectDuration,
    
  217.             priorityLevel,
    
  218.             timestamp,
    
  219.             updaters,
    
  220.           }) => ({
    
  221.             changeDescriptions:
    
  222.               changeDescriptions != null ? new Map(changeDescriptions) : null,
    
  223.             duration,
    
  224.             effectDuration,
    
  225.             fiberActualDurations: new Map(fiberActualDurations),
    
  226.             fiberSelfDurations: new Map(fiberSelfDurations),
    
  227.             passiveEffectDuration,
    
  228.             priorityLevel,
    
  229.             timestamp,
    
  230.             updaters,
    
  231.           }),
    
  232.         ),
    
  233.         displayName,
    
  234.         initialTreeBaseDurations: new Map(initialTreeBaseDurations),
    
  235.         operations,
    
  236.         rootID,
    
  237.         snapshots: new Map(snapshots),
    
  238.       });
    
  239.     },
    
  240.   );
    
  241. 
    
  242.   return {
    
  243.     dataForRoots,
    
  244.     imported: true,
    
  245.     timelineData,
    
  246.   };
    
  247. }
    
  248. 
    
  249. // Converts a Store Profiling data into a format that can be safely (JSON) serialized for export.
    
  250. export function prepareProfilingDataExport(
    
  251.   profilingDataFrontend: ProfilingDataFrontend,
    
  252. ): ProfilingDataExport {
    
  253.   const timelineData: Array<TimelineDataExport> =
    
  254.     profilingDataFrontend.timelineData.map(
    
  255.       ({
    
  256.         batchUIDToMeasuresMap,
    
  257.         componentMeasures,
    
  258.         duration,
    
  259.         flamechart,
    
  260.         internalModuleSourceToRanges,
    
  261.         laneToLabelMap,
    
  262.         laneToReactMeasureMap,
    
  263.         nativeEvents,
    
  264.         networkMeasures,
    
  265.         otherUserTimingMarks,
    
  266.         reactVersion,
    
  267.         schedulingEvents,
    
  268.         snapshots,
    
  269.         snapshotHeight,
    
  270.         startTime,
    
  271.         suspenseEvents,
    
  272.         thrownErrors,
    
  273.       }) => ({
    
  274.         // Most of the data is safe to serialize as-is,
    
  275.         // but we need to convert the Maps to nested Arrays.
    
  276.         batchUIDToMeasuresKeyValueArray: Array.from(
    
  277.           batchUIDToMeasuresMap.entries(),
    
  278.         ),
    
  279.         componentMeasures: componentMeasures,
    
  280.         duration,
    
  281.         flamechart,
    
  282.         internalModuleSourceToRanges: Array.from(
    
  283.           internalModuleSourceToRanges.entries(),
    
  284.         ),
    
  285.         laneToLabelKeyValueArray: Array.from(laneToLabelMap.entries()),
    
  286.         laneToReactMeasureKeyValueArray: Array.from(
    
  287.           laneToReactMeasureMap.entries(),
    
  288.         ),
    
  289.         nativeEvents,
    
  290.         networkMeasures,
    
  291.         otherUserTimingMarks,
    
  292.         reactVersion,
    
  293.         schedulingEvents,
    
  294.         snapshots,
    
  295.         snapshotHeight,
    
  296.         startTime,
    
  297.         suspenseEvents,
    
  298.         thrownErrors,
    
  299.       }),
    
  300.     );
    
  301. 
    
  302.   const dataForRoots: Array<ProfilingDataForRootExport> = [];
    
  303.   profilingDataFrontend.dataForRoots.forEach(
    
  304.     ({
    
  305.       commitData,
    
  306.       displayName,
    
  307.       initialTreeBaseDurations,
    
  308.       operations,
    
  309.       rootID,
    
  310.       snapshots,
    
  311.     }) => {
    
  312.       dataForRoots.push({
    
  313.         commitData: commitData.map(
    
  314.           ({
    
  315.             changeDescriptions,
    
  316.             duration,
    
  317.             effectDuration,
    
  318.             fiberActualDurations,
    
  319.             fiberSelfDurations,
    
  320.             passiveEffectDuration,
    
  321.             priorityLevel,
    
  322.             timestamp,
    
  323.             updaters,
    
  324.           }) => ({
    
  325.             changeDescriptions:
    
  326.               changeDescriptions != null
    
  327.                 ? Array.from(changeDescriptions.entries())
    
  328.                 : null,
    
  329.             duration,
    
  330.             effectDuration,
    
  331.             fiberActualDurations: Array.from(fiberActualDurations.entries()),
    
  332.             fiberSelfDurations: Array.from(fiberSelfDurations.entries()),
    
  333.             passiveEffectDuration,
    
  334.             priorityLevel,
    
  335.             timestamp,
    
  336.             updaters,
    
  337.           }),
    
  338.         ),
    
  339.         displayName,
    
  340.         initialTreeBaseDurations: Array.from(
    
  341.           initialTreeBaseDurations.entries(),
    
  342.         ),
    
  343.         operations,
    
  344.         rootID,
    
  345.         snapshots: Array.from(snapshots.entries()),
    
  346.       });
    
  347.     },
    
  348.   );
    
  349. 
    
  350.   return {
    
  351.     version: PROFILER_EXPORT_VERSION,
    
  352.     dataForRoots,
    
  353.     timelineData,
    
  354.   };
    
  355. }
    
  356. 
    
  357. export const getGradientColor = (value: number): string => {
    
  358.   const maxIndex = commitGradient.length - 1;
    
  359.   let index;
    
  360.   if (Number.isNaN(value)) {
    
  361.     index = 0;
    
  362.   } else if (!Number.isFinite(value)) {
    
  363.     index = maxIndex;
    
  364.   } else {
    
  365.     index = Math.max(0, Math.min(maxIndex, value)) * maxIndex;
    
  366.   }
    
  367.   return commitGradient[Math.round(index)];
    
  368. };
    
  369. 
    
  370. export const formatDuration = (duration: number): number | string =>
    
  371.   Math.round(duration * 10) / 10 || '<0.1';
    
  372. export const formatPercentage = (percentage: number): number =>
    
  373.   Math.round(percentage * 100);
    
  374. export const formatTime = (timestamp: number): number =>
    
  375.   Math.round(Math.round(timestamp) / 100) / 10;
    
  376. 
    
  377. export const scale =
    
  378.   (
    
  379.     minValue: number,
    
  380.     maxValue: number,
    
  381.     minRange: number,
    
  382.     maxRange: number,
    
  383.   ): ((value: number, fallbackValue: number) => number) =>
    
  384.   (value: number, fallbackValue: number) =>
    
  385.     maxValue - minValue === 0
    
  386.       ? fallbackValue
    
  387.       : ((value - minValue) / (maxValue - minValue)) * (maxRange - minRange);