| 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 {backendToFrontendSerializedElementMapper} 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 | |
| 22 | const commitGradient = [ |
| 23 | 'var(--color-commit-gradient-0)', |
| 24 | 'var(--color-commit-gradient-1)', |
| 25 | 'var(--color-commit-gradient-2)', |
| 26 | 'var(--color-commit-gradient-3)', |
| 27 | 'var(--color-commit-gradient-4)', |
| 28 | 'var(--color-commit-gradient-5)', |
| 29 | 'var(--color-commit-gradient-6)', |
| 30 | 'var(--color-commit-gradient-7)', |
| 31 | 'var(--color-commit-gradient-8)', |
| 32 | 'var(--color-commit-gradient-9)', |
| 33 | ]; |
| 34 | |
| 35 | // Combines info from the Store (frontend) and renderer interfaces (backend) into the format required by the Profiler UI. |
| 36 | // This format can then be quickly exported (and re-imported). |
| 37 | export function prepareProfilingDataFrontendFromBackendAndStore( |
| 38 | dataBackends: Array<ProfilingDataBackend>, |
| 39 | operationsByRootID: Map<number, Array<Array<number>>>, |
| 40 | snapshotsByRootID: Map<number, Map<number, SnapshotNode>>, |
| 41 | ): ProfilingDataFrontend { |
| 42 | const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map(); |
| 43 | |
| 44 | dataBackends.forEach(dataBackend => { |
| 45 | dataBackend.dataForRoots.forEach( |
| 46 | ({commitData, displayName, initialTreeBaseDurations, rootID}) => { |
| 47 | const operations = operationsByRootID.get(rootID); |
| 48 | if (operations == null) { |
| 49 | throw Error( |
| 50 | `Could not find profiling operations for root "${rootID}"`, |
| 51 | ); |
| 52 | } |
| 53 | |
| 54 | const snapshots = snapshotsByRootID.get(rootID); |
| 55 | if (snapshots == null) { |
| 56 | throw Error( |
| 57 | `Could not find profiling snapshots for root "${rootID}"`, |
| 58 | ); |
| 59 | } |
| 60 | |
| 61 | // Do not filter empty commits from the profiler data! |
| 62 | // Hiding "empty" commits might cause confusion too. |
| 63 | // A commit *did happen* even if none of the components the Profiler is showing were involved. |
| 64 | const convertedCommitData = commitData.map( |
| 65 | (commitDataBackend, commitIndex) => ({ |
| 66 | changeDescriptions: |
| 67 | commitDataBackend.changeDescriptions != null |
| 68 | ? new Map(commitDataBackend.changeDescriptions) |
| 69 | : null, |
| 70 | duration: commitDataBackend.duration, |
| 71 | effectDuration: commitDataBackend.effectDuration, |
| 72 | fiberActualDurations: new Map( |
| 73 | commitDataBackend.fiberActualDurations, |
| 74 | ), |
| 75 | fiberSelfDurations: new Map(commitDataBackend.fiberSelfDurations), |
| 76 | passiveEffectDuration: commitDataBackend.passiveEffectDuration, |
| 77 | priorityLevel: commitDataBackend.priorityLevel, |
| 78 | timestamp: commitDataBackend.timestamp, |
| 79 | updaters: |
| 80 | commitDataBackend.updaters !== null |
| 81 | ? commitDataBackend.updaters.map( |
| 82 | backendToFrontendSerializedElementMapper, |
| 83 | ) |
| 84 | : null, |
| 85 | }), |
| 86 | ); |
| 87 | |
| 88 | dataForRoots.set(rootID, { |
| 89 | commitData: convertedCommitData, |
| 90 | displayName, |
| 91 | initialTreeBaseDurations: new Map(initialTreeBaseDurations), |
| 92 | operations, |
| 93 | rootID, |
| 94 | snapshots, |
| 95 | }); |
| 96 | }, |
| 97 | ); |
| 98 | }); |
| 99 | |
| 100 | return {dataForRoots, imported: false}; |
| 101 | } |
| 102 | |
| 103 | // Converts a Profiling data export into the format required by the Store. |
| 104 | export function prepareProfilingDataFrontendFromExport( |
| 105 | profilingDataExport: ProfilingDataExport, |
| 106 | ): ProfilingDataFrontend { |
| 107 | const {version} = profilingDataExport; |
| 108 | |
| 109 | if (version !== PROFILER_EXPORT_VERSION) { |
| 110 | throw Error( |
| 111 | `Unsupported profile export version "${version}". Supported version is "${PROFILER_EXPORT_VERSION}".`, |
| 112 | ); |
| 113 | } |
| 114 | |
| 115 | const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map(); |
| 116 | profilingDataExport.dataForRoots.forEach( |
| 117 | ({ |
| 118 | commitData, |
| 119 | displayName, |
| 120 | initialTreeBaseDurations, |
| 121 | operations, |
| 122 | rootID, |
| 123 | snapshots, |
| 124 | }) => { |
| 125 | dataForRoots.set(rootID, { |
| 126 | commitData: commitData.map( |
| 127 | ({ |
| 128 | changeDescriptions, |
| 129 | duration, |
| 130 | effectDuration, |
| 131 | fiberActualDurations, |
| 132 | fiberSelfDurations, |
| 133 | passiveEffectDuration, |
| 134 | priorityLevel, |
| 135 | timestamp, |
| 136 | updaters, |
| 137 | }) => ({ |
| 138 | changeDescriptions: |
| 139 | changeDescriptions != null ? new Map(changeDescriptions) : null, |
| 140 | duration, |
| 141 | effectDuration, |
| 142 | fiberActualDurations: new Map(fiberActualDurations), |
| 143 | fiberSelfDurations: new Map(fiberSelfDurations), |
| 144 | passiveEffectDuration, |
| 145 | priorityLevel, |
| 146 | timestamp, |
| 147 | updaters, |
| 148 | }), |
| 149 | ), |
| 150 | displayName, |
| 151 | initialTreeBaseDurations: new Map(initialTreeBaseDurations), |
| 152 | operations, |
| 153 | rootID, |
| 154 | snapshots: new Map(snapshots), |
| 155 | }); |
| 156 | }, |
| 157 | ); |
| 158 | |
| 159 | return { |
| 160 | dataForRoots, |
| 161 | imported: true, |
| 162 | }; |
| 163 | } |
| 164 | |
| 165 | // Converts a Store Profiling data into a format that can be safely (JSON) serialized for export. |
| 166 | export function prepareProfilingDataExport( |
| 167 | profilingDataFrontend: ProfilingDataFrontend, |
| 168 | ): ProfilingDataExport { |
| 169 | const dataForRoots: Array<ProfilingDataForRootExport> = []; |
| 170 | profilingDataFrontend.dataForRoots.forEach( |
| 171 | ({ |
| 172 | commitData, |
| 173 | displayName, |
| 174 | initialTreeBaseDurations, |
| 175 | operations, |
| 176 | rootID, |
| 177 | snapshots, |
| 178 | }) => { |
| 179 | dataForRoots.push({ |
| 180 | commitData: commitData.map( |
| 181 | ({ |
| 182 | changeDescriptions, |
| 183 | duration, |
| 184 | effectDuration, |
| 185 | fiberActualDurations, |
| 186 | fiberSelfDurations, |
| 187 | passiveEffectDuration, |
| 188 | priorityLevel, |
| 189 | timestamp, |
| 190 | updaters, |
| 191 | }) => ({ |
| 192 | changeDescriptions: |
| 193 | changeDescriptions != null |
| 194 | ? Array.from(changeDescriptions.entries()) |
| 195 | : null, |
| 196 | duration, |
| 197 | effectDuration, |
| 198 | fiberActualDurations: Array.from(fiberActualDurations.entries()), |
| 199 | fiberSelfDurations: Array.from(fiberSelfDurations.entries()), |
| 200 | passiveEffectDuration, |
| 201 | priorityLevel, |
| 202 | timestamp, |
| 203 | updaters, |
| 204 | }), |
| 205 | ), |
| 206 | displayName, |
| 207 | initialTreeBaseDurations: Array.from( |
| 208 | initialTreeBaseDurations.entries(), |
| 209 | ), |
| 210 | operations, |
| 211 | rootID, |
| 212 | snapshots: Array.from(snapshots.entries()), |
| 213 | }); |
| 214 | }, |
| 215 | ); |
| 216 | |
| 217 | return { |
| 218 | version: PROFILER_EXPORT_VERSION, |
| 219 | dataForRoots, |
| 220 | }; |
| 221 | } |
| 222 | |
| 223 | export const getGradientColor = (value: number): string => { |
| 224 | const maxIndex = commitGradient.length - 1; |
| 225 | let index; |
| 226 | if (Number.isNaN(value)) { |
| 227 | index = 0; |
| 228 | } else if (!Number.isFinite(value)) { |
| 229 | index = maxIndex; |
| 230 | } else { |
| 231 | index = Math.max(0, Math.min(maxIndex, value)) * maxIndex; |
| 232 | } |
| 233 | return commitGradient[Math.round(index)]; |
| 234 | }; |
| 235 | |
| 236 | export const formatDuration = (duration: number): number | string => |
| 237 | Math.round(duration * 10) / 10 || '<0.1'; |
| 238 | export const formatPercentage = (percentage: number): number => |
| 239 | Math.round(percentage * 100); |
| 240 | export const formatTime = (timestamp: number): number => |
| 241 | Math.round(Math.round(timestamp) / 100) / 10; |
| 242 | |
| 243 | export const scale = |
| 244 | ( |
| 245 | minValue: number, |
| 246 | maxValue: number, |
| 247 | minRange: number, |
| 248 | maxRange: number, |
| 249 | ): ((value: number, fallbackValue: number) => number) => |
| 250 | (value: number, fallbackValue: number) => |
| 251 | maxValue - minValue === 0 |
| 252 | ? fallbackValue |
| 253 | : ((value - minValue) / (maxValue - minValue)) * (maxRange - minRange); |