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
+}