main
js 556 lines 15.5 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 {
11 __DEBUG__,
12 TREE_OPERATION_ADD,
13 TREE_OPERATION_REMOVE,
14 TREE_OPERATION_REORDER_CHILDREN,
15 TREE_OPERATION_SET_SUBTREE_MODE,
16 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
17 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
18 TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
19 SUSPENSE_TREE_OPERATION_ADD,
20 SUSPENSE_TREE_OPERATION_REMOVE,
21 SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
22 SUSPENSE_TREE_OPERATION_RESIZE,
23 SUSPENSE_TREE_OPERATION_SUSPENDERS,
24 } from 'react-devtools-shared/src/constants';
25 import {
26 parseElementDisplayNameFromBackend,
27 utfDecodeStringWithRanges,
28 } from 'react-devtools-shared/src/utils';
29 import {ElementTypeRoot} from 'react-devtools-shared/src/frontend/types';
30 import ProfilerStore from 'react-devtools-shared/src/devtools/ProfilerStore';
31
32 import type {ElementType} from 'react-devtools-shared/src/frontend/types';
33 import type {
34 CommitTree,
35 CommitTreeNode,
36 ProfilingDataForRootFrontend,
37 } from 'react-devtools-shared/src/devtools/views/Profiler/types';
38
39 const debug = (methodName: string, ...args: Array<string>) => {
40 // $FlowFixMe[constant-condition]
41 if (__DEBUG__) {
42 console.log(
43 `%cCommitTreeBuilder %c${methodName}`,
44 'color: pink; font-weight: bold;',
45 'font-weight: bold;',
46 ...args,
47 );
48 }
49 };
50
51 const rootToCommitTreeMap: Map<number, Array<CommitTree>> = new Map();
52
53 export function getCommitTree({
54 commitIndex,
55 profilerStore,
56 rootID,
57 }: {
58 commitIndex: number,
59 profilerStore: ProfilerStore,
60 rootID: number,
61 }): CommitTree {
62 if (!rootToCommitTreeMap.has(rootID)) {
63 rootToCommitTreeMap.set(rootID, []);
64 }
65
66 const commitTrees = rootToCommitTreeMap.get(
67 rootID,
68 ) as any as Array<CommitTree>;
69 if (commitIndex < commitTrees.length) {
70 return commitTrees[commitIndex];
71 }
72
73 const {profilingData} = profilerStore;
74 if (profilingData === null) {
75 throw Error(`No profiling data available`);
76 }
77
78 const dataForRoot = profilingData.dataForRoots.get(rootID);
79 if (dataForRoot == null) {
80 throw Error(`Could not find profiling data for root "${rootID}"`);
81 }
82
83 const {operations} = dataForRoot;
84 if (operations.length <= commitIndex) {
85 throw Error(
86 `getCommitTree(): Invalid commit "${commitIndex}" for root "${rootID}". There are only "${operations.length}" commits.`,
87 );
88 }
89
90 let commitTree: CommitTree = null as any as CommitTree;
91 for (let index = commitTrees.length; index <= commitIndex; index++) {
92 // Commits are generated sequentially and cached.
93 // If this is the very first commit, start with the cached snapshot and apply the first mutation.
94 // Otherwise load (or generate) the previous commit and append a mutation to it.
95 if (index === 0) {
96 const nodes = new Map<number, CommitTreeNode>();
97
98 // Construct the initial tree.
99 recursivelyInitializeTree(rootID, 0, nodes, dataForRoot);
100
101 // Mutate the tree
102 if (operations != null && index < operations.length) {
103 commitTree = updateTree({nodes, rootID}, operations[index]);
104
105 // $FlowFixMe[constant-condition]
106 if (__DEBUG__) {
107 __printTree(commitTree);
108 }
109
110 commitTrees.push(commitTree);
111 }
112 } else {
113 const previousCommitTree = commitTrees[index - 1];
114 commitTree = updateTree(previousCommitTree, operations[index]);
115
116 // $FlowFixMe[constant-condition]
117 if (__DEBUG__) {
118 __printTree(commitTree);
119 }
120
121 commitTrees.push(commitTree);
122 }
123 }
124
125 return commitTree;
126 }
127
128 function recursivelyInitializeTree(
129 id: number,
130 parentID: number,
131 nodes: Map<number, CommitTreeNode>,
132 dataForRoot: ProfilingDataForRootFrontend,
133 ): void {
134 const node = dataForRoot.snapshots.get(id);
135 if (node != null) {
136 nodes.set(id, {
137 id,
138 children: node.children,
139 displayName: node.displayName,
140 hocDisplayNames: node.hocDisplayNames,
141 key: node.key,
142 parentID,
143 treeBaseDuration: dataForRoot.initialTreeBaseDurations.get(
144 id,
145 ) as any as number,
146 type: node.type,
147 compiledWithForget: node.compiledWithForget,
148 });
149
150 node.children.forEach(childID =>
151 recursivelyInitializeTree(childID, id, nodes, dataForRoot),
152 );
153 }
154 }
155
156 function updateTree(
157 commitTree: CommitTree,
158 operations: Array<number>,
159 ): CommitTree {
160 // Clone the original tree so edits don't affect it.
161 const nodes = new Map(commitTree.nodes);
162
163 // Clone nodes before mutating them so edits don't affect them.
164 const getClonedNode = (id: number): CommitTreeNode => {
165 const existingNode = nodes.get(id);
166 if (existingNode == null) {
167 throw new Error(
168 `Could not clone the node: commit tree does not contain fiber "${id}". This is a bug in React DevTools.`,
169 );
170 }
171
172 const clonedNode = {...existingNode};
173 nodes.set(id, clonedNode);
174 return clonedNode;
175 };
176
177 let i = 2;
178 let id: number = null as any as number;
179
180 // Reassemble the string table.
181 const stringTable: Array<null | string> = [
182 null, // ID = 0 corresponds to the null string.
183 ];
184 const stringTableSize = operations[i++];
185 const stringTableEnd = i + stringTableSize;
186 while (i < stringTableEnd) {
187 const nextLength = operations[i++];
188 const nextString = utfDecodeStringWithRanges(
189 operations,
190 i,
191 i + nextLength - 1,
192 );
193 stringTable.push(nextString);
194 i += nextLength;
195 }
196
197 while (i < operations.length) {
198 const operation = operations[i];
199
200 switch (operation) {
201 case TREE_OPERATION_ADD: {
202 id = operations[i + 1] as any as number;
203 const type = operations[i + 2] as any as ElementType;
204
205 i += 3;
206
207 if (nodes.has(id)) {
208 throw new Error(
209 `Commit tree already contains fiber "${id}". This is a bug in React DevTools.`,
210 );
211 }
212
213 if (type === ElementTypeRoot) {
214 i++; // isStrictModeCompliant
215 i++; // Profiling flag
216 i++; // supportsStrictMode flag
217 i++; // hasOwnerMetadata flag
218
219 // $FlowFixMe[constant-condition]
220 if (__DEBUG__) {
221 debug('Add', `new root fiber ${id}`);
222 }
223
224 const node: CommitTreeNode = {
225 children: [],
226 displayName: null,
227 hocDisplayNames: null,
228 id,
229 key: null,
230 parentID: 0,
231 treeBaseDuration: 0, // This will be updated by a subsequent operation
232 type,
233 compiledWithForget: false,
234 };
235
236 nodes.set(id, node);
237 } else {
238 const parentID = operations[i] as any as number;
239 i++;
240
241 i++; // ownerID
242
243 const displayNameStringID = operations[i];
244 const displayName = stringTable[displayNameStringID];
245 i++;
246
247 const keyStringID = operations[i];
248 const key = stringTable[keyStringID];
249 i++;
250
251 // skip name prop
252 i++;
253
254 // $FlowFixMe[constant-condition]
255 if (__DEBUG__) {
256 debug(
257 'Add',
258 `fiber ${id} (${displayName || 'null'}) as child of ${parentID}`,
259 );
260 }
261
262 const parentNode = getClonedNode(parentID);
263 parentNode.children = parentNode.children.concat(id);
264
265 const {formattedDisplayName, hocDisplayNames, compiledWithForget} =
266 parseElementDisplayNameFromBackend(displayName, type);
267
268 const node: CommitTreeNode = {
269 children: [],
270 displayName: formattedDisplayName,
271 hocDisplayNames: hocDisplayNames,
272 id,
273 key,
274 parentID,
275 treeBaseDuration: 0, // This will be updated by a subsequent operation
276 type,
277 compiledWithForget,
278 };
279
280 nodes.set(id, node);
281 }
282
283 break;
284 }
285 case TREE_OPERATION_REMOVE: {
286 const removeLength = operations[i + 1] as any as number;
287 i += 2;
288
289 for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
290 id = operations[i] as any as number;
291 i++;
292
293 if (!nodes.has(id)) {
294 throw new Error(
295 `Commit tree does not contain fiber "${id}". This is a bug in React DevTools.`,
296 );
297 }
298
299 const node = getClonedNode(id);
300 const parentID = node.parentID;
301
302 nodes.delete(id);
303
304 if (!nodes.has(parentID)) {
305 // No-op
306 } else {
307 const parentNode = getClonedNode(parentID);
308
309 // $FlowFixMe[constant-condition]
310 if (__DEBUG__) {
311 debug('Remove', `fiber ${id} from parent ${parentID}`);
312 }
313
314 parentNode.children = parentNode.children.filter(
315 childID => childID !== id,
316 );
317 }
318 }
319 break;
320 }
321 case TREE_OPERATION_REORDER_CHILDREN: {
322 id = operations[i + 1] as any as number;
323 const numChildren = operations[i + 2] as any as number;
324 const children = operations.slice(
325 i + 3,
326 i + 3 + numChildren,
327 ) as any as Array<number>;
328
329 i = i + 3 + numChildren;
330
331 // $FlowFixMe[constant-condition]
332 if (__DEBUG__) {
333 debug('Re-order', `fiber ${id} children ${children.join(',')}`);
334 }
335
336 const node = getClonedNode(id);
337 node.children = Array.from(children);
338
339 break;
340 }
341 case TREE_OPERATION_SET_SUBTREE_MODE: {
342 id = operations[i + 1];
343 const mode = operations[i + 1];
344
345 i += 3;
346
347 // $FlowFixMe[constant-condition]
348 if (__DEBUG__) {
349 debug('Subtree mode', `Subtree with root ${id} set to mode ${mode}`);
350 }
351 break;
352 }
353 case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: {
354 id = operations[i + 1];
355
356 const node = getClonedNode(id);
357 node.treeBaseDuration = operations[i + 2] / 1000; // Convert microseconds back to milliseconds;
358
359 // $FlowFixMe[constant-condition]
360 if (__DEBUG__) {
361 debug(
362 'Update',
363 `fiber ${id} treeBaseDuration to ${node.treeBaseDuration}`,
364 );
365 }
366
367 i += 3;
368 break;
369 }
370 case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: {
371 id = operations[i + 1];
372 const numErrors = operations[i + 2];
373 const numWarnings = operations[i + 3];
374
375 i += 4;
376
377 // $FlowFixMe[constant-condition]
378 if (__DEBUG__) {
379 debug(
380 'Warnings and Errors update',
381 `fiber ${id} has ${numErrors} errors and ${numWarnings} warnings`,
382 );
383 }
384 break;
385 }
386
387 case SUSPENSE_TREE_OPERATION_ADD: {
388 const fiberID = operations[i + 1];
389 const parentID = operations[i + 2];
390 const nameStringID = operations[i + 3];
391 const isSuspended = operations[i + 4];
392 const numRects = operations[i + 5];
393 const name = stringTable[nameStringID];
394
395 // $FlowFixMe[constant-condition]
396 if (__DEBUG__) {
397 let rects: string;
398 if (numRects === -1) {
399 rects = 'null';
400 } else {
401 rects =
402 '[' +
403 operations.slice(i + 6, i + 6 + numRects * 4).join(',') +
404 ']';
405 }
406 debug(
407 'Add suspense',
408 `node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID} suspended ${isSuspended}`,
409 );
410 }
411
412 i += 6 + (numRects === -1 ? 0 : numRects * 4);
413 break;
414 }
415
416 case SUSPENSE_TREE_OPERATION_REMOVE: {
417 const removeLength = operations[i + 1] as any as number;
418 i += 2 + removeLength;
419
420 break;
421 }
422
423 case SUSPENSE_TREE_OPERATION_REORDER_CHILDREN: {
424 const suspenseID = operations[i + 1] as any as number;
425 const numChildren = operations[i + 2] as any as number;
426 const children = operations.slice(
427 i + 3,
428 i + 3 + numChildren,
429 ) as any as Array<number>;
430
431 i = i + 3 + numChildren;
432
433 // $FlowFixMe[constant-condition]
434 if (__DEBUG__) {
435 debug(
436 'Suspense re-order',
437 `suspense ${suspenseID} children ${children.join(',')}`,
438 );
439 }
440
441 break;
442 }
443
444 case SUSPENSE_TREE_OPERATION_RESIZE: {
445 const suspenseID = operations[i + 1] as any as number;
446 const numRects = operations[i + 2] as any as number;
447
448 // $FlowFixMe[constant-condition]
449 if (__DEBUG__) {
450 if (numRects === -1) {
451 debug('Suspense resize', `suspense ${suspenseID} rects null`);
452 } else {
453 const rects = operations.slice(
454 i + 3,
455 i + 3 + numRects * 4,
456 ) as any as Array<number>;
457 debug(
458 'Suspense resize',
459 `suspense ${suspenseID} rects [${rects.join(',')}]`,
460 );
461 }
462 }
463
464 i += 3 + (numRects === -1 ? 0 : numRects * 4);
465
466 break;
467 }
468
469 case SUSPENSE_TREE_OPERATION_SUSPENDERS: {
470 i++;
471 const changeLength = operations[i++] as any as number;
472
473 for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
474 const suspenseNodeId = operations[i++];
475 const hasUniqueSuspenders = operations[i++] === 1;
476 const endTime = operations[i++] / 1000;
477 const isSuspended = operations[i++] === 1;
478 const environmentNamesLength = operations[i++];
479 i += environmentNamesLength;
480 // $FlowFixMe[constant-condition]
481 if (__DEBUG__) {
482 debug(
483 'Suspender changes',
484 `Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} ending at ${String(endTime)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
485 );
486 }
487 }
488
489 break;
490 }
491
492 case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
493 i++;
494 const activitySliceIDChange = operations[i++];
495 // $FlowFixMe[constant-condition]
496 if (__DEBUG__) {
497 debug(
498 'Applied activity slice change',
499 activitySliceIDChange === 0
500 ? 'Reset applied activity slice'
501 : `Changed to activity slice ID ${activitySliceIDChange}`,
502 );
503 }
504 break;
505 }
506
507 default:
508 throw Error(`Unsupported Bridge operation "${operation}"`);
509 }
510 }
511
512 return {
513 nodes,
514 rootID: commitTree.rootID,
515 };
516 }
517
518 export function invalidateCommitTrees(): void {
519 rootToCommitTreeMap.clear();
520 }
521
522 // DEBUG
523 const __printTree = (commitTree: CommitTree) => {
524 // $FlowFixMe[constant-condition]
525 if (__DEBUG__) {
526 const {nodes, rootID} = commitTree;
527 console.group('__printTree()');
528 const queue = [rootID, 0];
529 while (queue.length > 0) {
530 const id = queue.shift();
531 const depth = queue.shift();
532
533 // $FlowFixMe[incompatible-call]
534 // $FlowFixMe[incompatible-type]
535 const node = nodes.get(id);
536 if (node == null) {
537 // $FlowFixMe[incompatible-type]
538 throw Error(`Could not find node with id "${id}" in commit tree`);
539 }
540
541 console.log(
542 // $FlowFixMe[incompatible-call]
543 // $FlowFixMe[incompatible-type]
544 `${''.repeat(depth)}${node.id}:${node.displayName || ''} ${
545 node.key ? `key:"${node.key}"` : ''
546 } (${node.treeBaseDuration})`,
547 );
548
549 node.children.forEach(childID => {
550 // $FlowFixMe[unsafe-addition]
551 queue.push(childID, depth + 1);
552 });
553 }
554 console.groupEnd();
555 }
556 };