main
js 327 lines 10.2 KB
Raw
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 }