| 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 {didFiberRender} from 'react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection'; |
| 11 | |
| 12 | import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; |
| 13 | import type {RendererInternals, ProfilingState} from './DevToolsFacade'; |
| 14 | import type {ToolError} from './DevToolsFacadeTreeTools'; |
| 15 | |
| 16 | import {getTypeTag} from './DevToolsFacadeTreeTools'; |
| 17 | |
| 18 | // Per-component render timing within a single commit. Durations are null when |
| 19 | // the build does not collect profiler timing. |
| 20 | export type CommitComponent = { |
| 21 | uid: string, |
| 22 | name: string, |
| 23 | type: string, |
| 24 | actualDuration: number | null, |
| 25 | selfDuration: number | null, |
| 26 | }; |
| 27 | |
| 28 | // One row of a trace overview — a per-commit timing summary. |
| 29 | export type TraceOverviewRow = { |
| 30 | commit: number, |
| 31 | committedAt: number, |
| 32 | renderDuration: number | null, |
| 33 | layoutDuration: number | null, |
| 34 | passiveDuration: number | null, |
| 35 | componentsChanged: number, |
| 36 | }; |
| 37 | |
| 38 | // A detailed report for a single commit. |
| 39 | export type CommitReport = { |
| 40 | committedAt: number, |
| 41 | priority: string, |
| 42 | renderDuration: number | null, |
| 43 | layoutDuration: number | null, |
| 44 | passiveDuration: number | null, |
| 45 | components: Array<CommitComponent>, |
| 46 | }; |
| 47 | |
| 48 | export type StartProfilingResult = {status: 'started', traceName: string}; |
| 49 | export type StopProfilingResult = { |
| 50 | status: 'stopped', |
| 51 | traceName: string, |
| 52 | commits: number, |
| 53 | }; |
| 54 | |
| 55 | export type ProfilerTools = { |
| 56 | startProfiling: (traceName?: string) => StartProfilingResult | ToolError, |
| 57 | stopProfiling: () => StopProfilingResult | ToolError, |
| 58 | getTraceOverview: (traceName: string) => Array<TraceOverviewRow> | ToolError, |
| 59 | getCommitReport: ( |
| 60 | traceName: string, |
| 61 | commitIndex: number, |
| 62 | ) => CommitReport | ToolError, |
| 63 | }; |
| 64 | |
| 65 | // Internal per-commit record (durations captured at commit time). |
| 66 | type CommitRecord = { |
| 67 | timestamp: number, |
| 68 | priority: string, |
| 69 | renderDuration: number | null, |
| 70 | layoutDuration: number | null, |
| 71 | passiveDuration: number | null, |
| 72 | durations: Array<CommitComponent>, |
| 73 | }; |
| 74 | |
| 75 | type TraceData = { |
| 76 | startTime: number, |
| 77 | commits: Array<CommitRecord>, |
| 78 | }; |
| 79 | |
| 80 | function priorityToString( |
| 81 | internals: RendererInternals, |
| 82 | schedulerPriority: number | void, |
| 83 | ): string { |
| 84 | const { |
| 85 | ImmediatePriority, |
| 86 | UserBlockingPriority, |
| 87 | NormalPriority, |
| 88 | IdlePriority, |
| 89 | } = internals.ReactPriorityLevels; |
| 90 | switch (schedulerPriority) { |
| 91 | case ImmediatePriority: |
| 92 | return 'Sync'; |
| 93 | case UserBlockingPriority: |
| 94 | return 'UserBlocking'; |
| 95 | case NormalPriority: |
| 96 | return 'Normal'; |
| 97 | case IdlePriority: |
| 98 | return 'Idle'; |
| 99 | default: |
| 100 | return 'Normal'; |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * Build the profiler tools from a renderer-internals map, the shared profiling |
| 106 | * state, and the tree tools' getUid (so component uids are consistent with |
| 107 | * getComponentTree/getComponentByUid). The hook installed by installFacade |
| 108 | * invokes profilingState.onCommit/onPostCommit while a session is active. |
| 109 | */ |
| 110 | export function createProfilerTools( |
| 111 | rendererInternals: Map<number, RendererInternals>, |
| 112 | profilingState: ProfilingState, |
| 113 | getUid: (fiber: Fiber) => string, |
| 114 | ): ProfilerTools { |
| 115 | // Walk the fiber tree collecting timing for fibers that actually rendered. |
| 116 | // Matches the same didFiberRender check and display-name filtering as the |
| 117 | // DevTools Profiler — only fibers with a non-null display name are recorded, |
| 118 | // which filters out internal types (HostRoot, Fragment, Mode, HostText, etc.). |
| 119 | function collectDurations( |
| 120 | internals: RendererInternals, |
| 121 | fiber: Fiber, |
| 122 | durations: Array<CommitComponent>, |
| 123 | ): void { |
| 124 | const {ReactTypeOfWork, getDisplayNameForFiber} = internals; |
| 125 | const displayName = getDisplayNameForFiber(fiber); |
| 126 | if (displayName != null) { |
| 127 | const prevFiber = fiber.alternate; |
| 128 | if ( |
| 129 | prevFiber == null || |
| 130 | didFiberRender(ReactTypeOfWork, prevFiber, fiber) |
| 131 | ) { |
| 132 | const actual = |
| 133 | fiber.actualDuration != null ? fiber.actualDuration : null; |
| 134 | let self: number | null = actual; |
| 135 | if (actual != null) { |
| 136 | let selfDuration: number = actual; |
| 137 | let child = fiber.child; |
| 138 | while (child !== null) { |
| 139 | selfDuration -= child.actualDuration || 0; |
| 140 | child = child.sibling; |
| 141 | } |
| 142 | self = selfDuration; |
| 143 | } |
| 144 | durations.push({ |
| 145 | uid: getUid(fiber), |
| 146 | name: displayName, |
| 147 | type: getTypeTag(ReactTypeOfWork, fiber.tag), |
| 148 | actualDuration: actual, |
| 149 | selfDuration: self, |
| 150 | }); |
| 151 | } |
| 152 | } |
| 153 | // Recurse into children regardless of whether this node rendered. |
| 154 | let child = fiber.child; |
| 155 | while (child !== null) { |
| 156 | collectDurations(internals, child, durations); |
| 157 | child = child.sibling; |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | // Commits awaiting their passive-effect pass, keyed by root so that a late |
| 162 | // onPostCommit attributes passiveDuration to the right commit even when |
| 163 | // multiple roots commit before their passive passes run. |
| 164 | const pendingPassive: Map<FiberRoot, CommitRecord> = new Map(); |
| 165 | |
| 166 | /** |
| 167 | * Start a named profiling session that captures per-commit render timing. |
| 168 | * While active, every React commit records timing for components that |
| 169 | * rendered. Errors if a session is already active. |
| 170 | * |
| 171 | * @param traceName - Optional trace name (auto-generated if omitted). |
| 172 | */ |
| 173 | function startProfiling( |
| 174 | traceName?: string, |
| 175 | ): StartProfilingResult | ToolError { |
| 176 | if (profilingState.isActive) { |
| 177 | return { |
| 178 | error: |
| 179 | 'Already profiling trace "' + |
| 180 | (profilingState.currentTraceName || '') + |
| 181 | '"', |
| 182 | }; |
| 183 | } |
| 184 | const resolvedTraceName = traceName || 'trace-' + Date.now(); |
| 185 | const trace: TraceData = {startTime: Date.now(), commits: []}; |
| 186 | profilingState.traces.set(resolvedTraceName, trace); |
| 187 | profilingState.isActive = true; |
| 188 | profilingState.currentTraceName = resolvedTraceName; |
| 189 | |
| 190 | profilingState.onCommit = function onCommit( |
| 191 | rendererID: number, |
| 192 | root: FiberRoot, |
| 193 | schedulerPriority: number | void, |
| 194 | ) { |
| 195 | const internals = rendererInternals.get(rendererID); |
| 196 | if (internals == null) { |
| 197 | console.error( |
| 198 | 'react-devtools-facade: Missing internals for renderer %s, commit not recorded.', |
| 199 | rendererID, |
| 200 | ); |
| 201 | return; |
| 202 | } |
| 203 | const durations: Array<CommitComponent> = []; |
| 204 | collectDurations(internals, root.current, durations); |
| 205 | const rootFiber = root.current; |
| 206 | const record: CommitRecord = { |
| 207 | timestamp: Date.now(), |
| 208 | priority: priorityToString(internals, schedulerPriority), |
| 209 | renderDuration: |
| 210 | rootFiber.actualDuration != null ? rootFiber.actualDuration : null, |
| 211 | layoutDuration: |
| 212 | root.effectDuration != null ? root.effectDuration : null, |
| 213 | passiveDuration: null, |
| 214 | durations, |
| 215 | }; |
| 216 | trace.commits.push(record); |
| 217 | pendingPassive.set(root, record); |
| 218 | }; |
| 219 | |
| 220 | profilingState.onPostCommit = function onPostCommit(root: FiberRoot) { |
| 221 | const record = pendingPassive.get(root); |
| 222 | if (record != null) { |
| 223 | record.passiveDuration = |
| 224 | root.passiveEffectDuration != null |
| 225 | ? root.passiveEffectDuration |
| 226 | : null; |
| 227 | pendingPassive.delete(root); |
| 228 | } |
| 229 | }; |
| 230 | |
| 231 | return {status: 'started', traceName: resolvedTraceName}; |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * Stop the active profiling session. Errors if no session is active. |
| 236 | */ |
| 237 | function stopProfiling(): StopProfilingResult | ToolError { |
| 238 | if (!profilingState.isActive) { |
| 239 | return {error: 'Not currently profiling'}; |
| 240 | } |
| 241 | const traceName = profilingState.currentTraceName; |
| 242 | if (traceName == null) { |
| 243 | return {error: 'No active trace'}; |
| 244 | } |
| 245 | const trace = profilingState.traces.get(traceName); |
| 246 | const commitCount = trace ? trace.commits.length : 0; |
| 247 | profilingState.isActive = false; |
| 248 | profilingState.currentTraceName = null; |
| 249 | profilingState.onCommit = null; |
| 250 | profilingState.onPostCommit = null; |
| 251 | pendingPassive.clear(); |
| 252 | return {status: 'stopped', traceName, commits: commitCount}; |
| 253 | } |
| 254 | |
| 255 | function getTrace(traceName: string): TraceData | null { |
| 256 | return profilingState.traces.get(traceName) || null; |
| 257 | } |
| 258 | |
| 259 | /** |
| 260 | * Return an overview of a trace — one row per commit with a timing breakdown |
| 261 | * (render, layout effects, passive effects) and the number of components that |
| 262 | * changed. |
| 263 | * |
| 264 | * @param traceName - The name of the trace to query. |
| 265 | */ |
| 266 | function getTraceOverview( |
| 267 | traceName: string, |
| 268 | ): Array<TraceOverviewRow> | ToolError { |
| 269 | const trace = getTrace(traceName); |
| 270 | if (trace == null) { |
| 271 | return {error: 'Unknown trace "' + traceName + '"'}; |
| 272 | } |
| 273 | const rows: Array<TraceOverviewRow> = []; |
| 274 | for (let i = 0; i < trace.commits.length; i++) { |
| 275 | const commit = trace.commits[i]; |
| 276 | rows.push({ |
| 277 | commit: i, |
| 278 | committedAt: commit.timestamp - trace.startTime, |
| 279 | renderDuration: commit.renderDuration, |
| 280 | layoutDuration: commit.layoutDuration, |
| 281 | passiveDuration: commit.passiveDuration, |
| 282 | componentsChanged: commit.durations.length, |
| 283 | }); |
| 284 | } |
| 285 | return rows; |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * Return a detailed report for a single commit — timing metadata |
| 290 | * (committedAt, priority, duration breakdown) and per-component render |
| 291 | * durations sorted by actualDuration descending. |
| 292 | * |
| 293 | * @param traceName - The name of the trace. |
| 294 | * @param commitIndex - Zero-based index of the commit within the trace. |
| 295 | */ |
| 296 | function getCommitReport( |
| 297 | traceName: string, |
| 298 | commitIndex: number, |
| 299 | ): CommitReport | ToolError { |
| 300 | const trace = getTrace(traceName); |
| 301 | if (trace == null) { |
| 302 | return {error: 'Unknown trace "' + traceName + '"'}; |
| 303 | } |
| 304 | if (commitIndex < 0 || commitIndex >= trace.commits.length) { |
| 305 | return {error: 'Commit index out of range'}; |
| 306 | } |
| 307 | const commit = trace.commits[commitIndex]; |
| 308 | const components = commit.durations |
| 309 | .slice() |
| 310 | .sort((a, b) => (b.actualDuration || 0) - (a.actualDuration || 0)); |
| 311 | return { |
| 312 | committedAt: commit.timestamp - trace.startTime, |
| 313 | priority: commit.priority, |
| 314 | renderDuration: commit.renderDuration, |
| 315 | layoutDuration: commit.layoutDuration, |
| 316 | passiveDuration: commit.passiveDuration, |
| 317 | components, |
| 318 | }; |
| 319 | } |
| 320 | |
| 321 | return { |
| 322 | startProfiling, |
| 323 | stopProfiling, |
| 324 | getTraceOverview, |
| 325 | getCommitReport, |
| 326 | }; |
| 327 | } |