main
js 8,071 lines 283 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 type {
11 Thenable,
12 ReactComponentInfo,
13 ReactDebugInfo,
14 ReactAsyncInfo,
15 ReactIOInfo,
16 ReactStackTrace,
17 ReactCallSite,
18 Wakeable,
19 } from 'shared/ReactTypes';
20
21 import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
22
23 import {
24 ComponentFilterDisplayName,
25 ComponentFilterElementType,
26 ComponentFilterHOC,
27 ComponentFilterLocation,
28 ComponentFilterEnvironmentName,
29 ComponentFilterActivitySlice,
30 ElementTypeClass,
31 ElementTypeContext,
32 ElementTypeFunction,
33 ElementTypeForwardRef,
34 ElementTypeHostComponent,
35 ElementTypeMemo,
36 ElementTypeOtherOrUnknown,
37 ElementTypeProfiler,
38 ElementTypeRoot,
39 ElementTypeSuspense,
40 ElementTypeSuspenseList,
41 ElementTypeTracingMarker,
42 ElementTypeViewTransition,
43 ElementTypeActivity,
44 ElementTypeVirtual,
45 StrictMode,
46 ActivityHiddenMode,
47 ActivityVisibleMode,
48 } from 'react-devtools-shared/src/frontend/types';
49 import {
50 deletePathInObject,
51 getInObject,
52 getUID,
53 renamePathInObject,
54 setInObject,
55 utfEncodeString,
56 } from 'react-devtools-shared/src/utils';
57 import {
58 formatConsoleArgumentsToSingleString,
59 formatDurationToMicrosecondsGranularity,
60 gte,
61 serializeToString,
62 } from 'react-devtools-shared/src/backend/utils';
63 import {
64 extractLocationFromComponentStack,
65 extractLocationFromOwnerStack,
66 parseStackTrace,
67 } from 'react-devtools-shared/src/backend/utils/parseStackTrace';
68 import {
69 cleanForBridge,
70 copyWithDelete,
71 copyWithRename,
72 copyWithSet,
73 getEffectDurations,
74 } from '../utils';
75 import {
76 __DEBUG__,
77 PROFILING_FLAG_BASIC_SUPPORT,
78 PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT,
79 TREE_OPERATION_ADD,
80 TREE_OPERATION_REMOVE,
81 TREE_OPERATION_REORDER_CHILDREN,
82 TREE_OPERATION_SET_SUBTREE_MODE,
83 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
84 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
85 TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
86 SUSPENSE_TREE_OPERATION_ADD,
87 SUSPENSE_TREE_OPERATION_REMOVE,
88 SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
89 SUSPENSE_TREE_OPERATION_RESIZE,
90 SUSPENSE_TREE_OPERATION_SUSPENDERS,
91 UNKNOWN_SUSPENDERS_NONE,
92 UNKNOWN_SUSPENDERS_REASON_PRODUCTION,
93 UNKNOWN_SUSPENDERS_REASON_OLD_VERSION,
94 UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE,
95 } from '../../constants';
96 import {inspectHooksOfFiber} from 'react-debug-tools';
97 import {
98 CONCURRENT_MODE_NUMBER,
99 CONCURRENT_MODE_SYMBOL_STRING,
100 DEPRECATED_ASYNC_MODE_SYMBOL_STRING,
101 PROVIDER_NUMBER,
102 PROVIDER_SYMBOL_STRING,
103 CONTEXT_NUMBER,
104 CONTEXT_SYMBOL_STRING,
105 CONSUMER_SYMBOL_STRING,
106 STRICT_MODE_NUMBER,
107 STRICT_MODE_SYMBOL_STRING,
108 PROFILER_NUMBER,
109 PROFILER_SYMBOL_STRING,
110 LAZY_SYMBOL_STRING,
111 REACT_OPTIMISTIC_KEY,
112 } from '../shared/ReactSymbols';
113 import {enableStyleXFeatures} from 'react-devtools-feature-flags';
114
115 import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponentLogs';
116
117 import {getIODescription} from 'shared/ReactIODescription';
118
119 import {
120 getPublicInstance,
121 getNativeTag,
122 getCurrentTime,
123 } from 'react-devtools-shared/src/backend/DevToolsNativeHost';
124 import {
125 isError,
126 rootSupportsProfiling,
127 isErrorBoundary,
128 getSecondaryEnvironmentName,
129 areEqualRects,
130 } from './shared/DevToolsFiberInspection';
131 import {
132 didFiberRender,
133 getContextChanged,
134 getChangedHooksIndices,
135 getChangedKeys,
136 } from './shared/DevToolsFiberChangeDetection';
137 import {getInternalReactConstants} from './shared/DevToolsFiberInternalReactConstants';
138 import {
139 ioExistsInSuspenseAncestor,
140 getAwaitInSuspendedByFromIO,
141 getVirtualEndTime,
142 } from './shared/DevToolsFiberSuspense';
143
144 import {
145 getStackByFiberInDevAndProd,
146 getOwnerStackByFiberInDev,
147 supportsOwnerStacks,
148 supportsConsoleTasks,
149 } from './DevToolsFiberComponentStack';
150
151 import {getStyleXData} from '../StyleX/utils';
152
153 import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
154 import type {
155 ChangeDescription,
156 CommitDataBackend,
157 DevToolsHook,
158 InspectedElement,
159 InspectedElementPayload,
160 InstanceAndStyle,
161 HostInstance,
162 PathFrame,
163 PathMatch,
164 ProfilingDataBackend,
165 ProfilingDataForRootBackend,
166 ReactRenderer,
167 Rect,
168 RendererInterface,
169 SerializedElement,
170 SerializedAsyncInfo,
171 ProfilingSettings,
172 } from '../types';
173 import type {
174 ComponentFilter,
175 ActivitySliceFilter,
176 ElementType,
177 Plugins,
178 } from 'react-devtools-shared/src/frontend/types';
179 import type {ReactFunctionLocation} from 'shared/ReactTypes';
180 import type {
181 FiberInstance,
182 FilteredFiberInstance,
183 VirtualInstance,
184 DevToolsInstance,
185 SuspenseNode,
186 } from './shared/DevToolsFiberTypes';
187 import {
188 FIBER_INSTANCE,
189 VIRTUAL_INSTANCE,
190 FILTERED_FIBER_INSTANCE,
191 } from './shared/DevToolsFiberTypes';
192 import {getDispatcherRef} from '../shared/DevToolsReactDispatcher';
193 import {getSourceLocationByFiber} from './DevToolsFiberComponentStack';
194 import {formatOwnerStack} from '../shared/DevToolsOwnerStack';
195
196 function createFiberInstance(fiber: Fiber): FiberInstance {
197 return {
198 kind: FIBER_INSTANCE,
199 id: getUID(),
200 parent: null,
201 firstChild: null,
202 nextSibling: null,
203 source: null,
204 logCount: 0,
205 treeBaseDuration: 0,
206 suspendedBy: null,
207 suspenseNode: null,
208 data: fiber,
209 };
210 }
211
212 // This is used to represent a filtered Fiber but still lets us find its host instance.
213 function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance {
214 return {
215 kind: FILTERED_FIBER_INSTANCE,
216 id: 0,
217 parent: null,
218 firstChild: null,
219 nextSibling: null,
220 source: null,
221 logCount: 0,
222 treeBaseDuration: 0,
223 suspendedBy: null,
224 suspenseNode: null,
225 data: fiber,
226 } as any;
227 }
228
229 function createVirtualInstance(
230 debugEntry: ReactComponentInfo,
231 ): VirtualInstance {
232 return {
233 kind: VIRTUAL_INSTANCE,
234 id: getUID(),
235 parent: null,
236 firstChild: null,
237 nextSibling: null,
238 source: null,
239 logCount: 0,
240 treeBaseDuration: 0,
241 suspendedBy: null,
242 suspenseNode: null,
243 data: debugEntry,
244 };
245 }
246
247 // Update flags need to be propagated up until the caller that put the corresponding
248 // node on the stack.
249 // If you push a new node, you need to handle ShouldResetChildren when you pop it.
250 // If you push a new Suspense node, you need to handle ShouldResetSuspenseChildren when you pop it.
251 type UpdateFlags = number;
252 const NoUpdate = /* */ 0b000;
253 const ShouldResetChildren = /* */ 0b001;
254 const ShouldResetSuspenseChildren = /* */ 0b010;
255 const ShouldResetParentSuspenseChildren = /* */ 0b100;
256
257 function createSuspenseNode(
258 instance: FiberInstance | FilteredFiberInstance,
259 ): SuspenseNode {
260 return (instance.suspenseNode = {
261 instance: instance,
262 parent: null,
263 firstChild: null,
264 nextSibling: null,
265 rects: null,
266 suspendedBy: new Map(),
267 environments: new Map(),
268 endTime: 0,
269 hasUniqueSuspenders: false,
270 hasUnknownSuspenders: false,
271 });
272 }
273
274 // All environment names we've seen so far. This lets us create a list of filters to apply.
275 // This should ideally include env of filtered Components too so that you can add those as
276 // filters at the same time as removing some other filter.
277 const knownEnvironmentNames: Set<string> = new Set();
278
279 // Map of FiberRoot to their root FiberInstance.
280 const rootToFiberInstanceMap: Map<FiberRoot, FiberInstance> = new Map();
281
282 // Map of id to FiberInstance or VirtualInstance.
283 // This Map is used to e.g. get the display name for a Fiber or schedule an update,
284 // operations that should be the same whether the current and work-in-progress Fiber is used.
285 const idToDevToolsInstanceMap: Map<
286 FiberInstance['id'] | VirtualInstance['id'],
287 FiberInstance | VirtualInstance,
288 > = new Map();
289
290 let focusedActivityID: null | FiberInstance['id'] = null;
291 let focusedActivity: null | Fiber = null;
292
293 const idToSuspenseNodeMap: Map<FiberInstance['id'], SuspenseNode> = new Map();
294
295 // Map of canonical HostInstances to the nearest parent DevToolsInstance.
296 const publicInstanceToDevToolsInstanceMap: Map<HostInstance, DevToolsInstance> =
297 new Map();
298 // Map of resource DOM nodes to all the nearest DevToolsInstances that depend on it.
299 const hostResourceToDevToolsInstanceMap: Map<
300 HostInstance,
301 Set<DevToolsInstance>,
302 > = new Map();
303
304 function aquireHostInstance(
305 nearestInstance: DevToolsInstance,
306 hostInstance: HostInstance,
307 ): void {
308 const publicInstance = getPublicInstance(hostInstance);
309 publicInstanceToDevToolsInstanceMap.set(publicInstance, nearestInstance);
310 }
311
312 function releaseHostInstance(
313 nearestInstance: DevToolsInstance,
314 hostInstance: HostInstance,
315 ): void {
316 const publicInstance = getPublicInstance(hostInstance);
317 if (
318 publicInstanceToDevToolsInstanceMap.get(publicInstance) === nearestInstance
319 ) {
320 publicInstanceToDevToolsInstanceMap.delete(publicInstance);
321 }
322 }
323
324 function aquireHostResource(
325 nearestInstance: DevToolsInstance,
326 resource: ?{instance?: HostInstance},
327 ): void {
328 const hostInstance = resource && resource.instance;
329 if (hostInstance) {
330 const publicInstance = getPublicInstance(hostInstance);
331 let resourceInstances =
332 hostResourceToDevToolsInstanceMap.get(publicInstance);
333 if (resourceInstances === undefined) {
334 resourceInstances = new Set();
335 hostResourceToDevToolsInstanceMap.set(publicInstance, resourceInstances);
336 // Store the first match in the main map for quick access when selecting DOM node.
337 publicInstanceToDevToolsInstanceMap.set(publicInstance, nearestInstance);
338 }
339 resourceInstances.add(nearestInstance);
340 }
341 }
342
343 function releaseHostResource(
344 nearestInstance: DevToolsInstance,
345 resource: ?{instance?: HostInstance},
346 ): void {
347 const hostInstance = resource && resource.instance;
348 if (hostInstance) {
349 const publicInstance = getPublicInstance(hostInstance);
350 const resourceInstances =
351 hostResourceToDevToolsInstanceMap.get(publicInstance);
352 if (resourceInstances !== undefined) {
353 resourceInstances.delete(nearestInstance);
354 if (resourceInstances.size === 0) {
355 hostResourceToDevToolsInstanceMap.delete(publicInstance);
356 publicInstanceToDevToolsInstanceMap.delete(publicInstance);
357 } else if (
358 publicInstanceToDevToolsInstanceMap.get(publicInstance) ===
359 nearestInstance
360 ) {
361 // This was the first one. Store the next first one in the main map for easy access.
362 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
363 for (const firstInstance of resourceInstances) {
364 publicInstanceToDevToolsInstanceMap.set(
365 publicInstance,
366 firstInstance,
367 );
368 break;
369 }
370 }
371 }
372 }
373 }
374
375 export function attach(
376 hook: DevToolsHook,
377 rendererID: number,
378 renderer: ReactRenderer,
379 global: Object,
380 shouldStartProfilingNow: boolean,
381 profilingSettings: ProfilingSettings,
382 componentFiltersOrComponentFiltersPromise:
383 | Array<ComponentFilter>
384 | Promise<Array<ComponentFilter>>,
385 ): RendererInterface {
386 // Newer versions of the reconciler package also specific reconciler version.
387 // If that version number is present, use it.
388 // Third party renderer versions may not match the reconciler version,
389 // and the latter is what's important in terms of tags and symbols.
390 const version = renderer.reconcilerVersion || renderer.version;
391
392 const {
393 getDisplayNameForFiber,
394 getTypeSymbol,
395 ReactPriorityLevels,
396 ReactTypeOfWork,
397 StrictModeBits,
398 SuspenseyImagesMode,
399 } = getInternalReactConstants(version);
400 const {
401 ActivityComponent,
402 ClassComponent,
403 DehydratedSuspenseComponent,
404 ForwardRef,
405 Fragment,
406 FunctionComponent,
407 HostRoot,
408 HostHoistable,
409 HostSingleton,
410 HostPortal,
411 HostComponent,
412 HostText,
413 IncompleteClassComponent,
414 IncompleteFunctionComponent,
415 IndeterminateComponent,
416 LegacyHiddenComponent,
417 MemoComponent,
418 OffscreenComponent,
419 SimpleMemoComponent,
420 SuspenseComponent,
421 SuspenseListComponent,
422 TracingMarkerComponent,
423 Throw,
424 ViewTransitionComponent,
425 } = ReactTypeOfWork;
426 const {
427 ImmediatePriority,
428 UserBlockingPriority,
429 NormalPriority,
430 LowPriority,
431 IdlePriority,
432 NoPriority,
433 } = ReactPriorityLevels;
434
435 const {
436 overrideHookState,
437 overrideHookStateDeletePath,
438 overrideHookStateRenamePath,
439 overrideProps,
440 overridePropsDeletePath,
441 overridePropsRenamePath,
442 scheduleRefresh,
443 setErrorHandler,
444 setSuspenseHandler,
445 scheduleUpdate,
446 scheduleRetry,
447 getCurrentFiber,
448 } = renderer;
449 const supportsTogglingError =
450 typeof setErrorHandler === 'function' &&
451 typeof scheduleUpdate === 'function';
452 const supportsTogglingSuspense =
453 typeof setSuspenseHandler === 'function' &&
454 typeof scheduleUpdate === 'function';
455 const supportsPerformanceTracks = gte(version, '19.2.0');
456
457 if (typeof scheduleRefresh === 'function') {
458 // When Fast Refresh updates a component, the frontend may need to purge cached information.
459 // For example, ASTs cached for the component (for named hooks) may no longer be valid.
460 // Send a signal to the frontend to purge this cached information.
461 // The "fastRefreshScheduled" dispatched is global (not Fiber or even Renderer specific).
462 // This is less effecient since it means the front-end will need to purge the entire cache,
463 // but this is probably an okay trade off in order to reduce coupling between the DevTools and Fast Refresh.
464 renderer.scheduleRefresh = (...args) => {
465 try {
466 hook.emit('fastRefreshScheduled');
467 } finally {
468 return scheduleRefresh(...args);
469 }
470 };
471 }
472
473 type ComponentLogs = {
474 errors: Map<string, number>,
475 errorsCount: number,
476 warnings: Map<string, number>,
477 warningsCount: number,
478 };
479 // Tracks Errors/Warnings logs added to a Fiber. They are added before the commit and get
480 // picked up a FiberInstance. This keeps it around as long as the Fiber is alive which
481 // lets the Fiber get reparented/remounted and still observe the previous errors/warnings.
482 // Unless we explicitly clear the logs from a Fiber.
483 const fiberToComponentLogsMap: WeakMap<Fiber, ComponentLogs> = new WeakMap();
484 // Tracks whether we've performed a commit since the last log. This is used to know
485 // whether we received any new logs between the commit and post commit phases. I.e.
486 // if any passive effects called console.warn / console.error.
487 let needsToFlushComponentLogs = false;
488
489 function bruteForceFlushErrorsAndWarnings(root: FiberInstance) {
490 // Refresh error/warning count for all mounted unfiltered Fibers.
491 let hasChanges = false;
492 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
493 for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {
494 if (devtoolsInstance.kind === FIBER_INSTANCE) {
495 const fiber = devtoolsInstance.data;
496 const componentLogsEntry = fiberToComponentLogsMap.get(fiber);
497 const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry);
498 if (changed) {
499 hasChanges = true;
500 updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
501 }
502 } else {
503 // Virtual Instances cannot log in passive effects and so never appear here.
504 }
505 }
506 if (hasChanges) {
507 flushPendingEvents(root);
508 }
509 }
510
511 function clearErrorsAndWarnings() {
512 // Note, this only clears logs for Fibers that have instances. If they're filtered
513 // and then mount, the logs are there. Ensuring we only clear what you've seen.
514 // If we wanted to clear the whole set, we'd replace fiberToComponentLogsMap with a
515 // new WeakMap. It's unclear whether we should clear componentInfoToComponentLogsMap
516 // since it's shared by other renderers but presumably it would.
517
518 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
519 for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {
520 if (devtoolsInstance.kind === FIBER_INSTANCE) {
521 const fiber = devtoolsInstance.data;
522 fiberToComponentLogsMap.delete(fiber);
523 if (fiber.alternate) {
524 fiberToComponentLogsMap.delete(fiber.alternate);
525 }
526 } else {
527 componentInfoToComponentLogsMap.delete(devtoolsInstance.data);
528 }
529 const changed = recordConsoleLogs(devtoolsInstance, undefined);
530 if (changed) {
531 updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
532 }
533 }
534 flushPendingEvents(null);
535 }
536
537 function clearConsoleLogsHelper(instanceID: number, type: 'error' | 'warn') {
538 const devtoolsInstance = idToDevToolsInstanceMap.get(instanceID);
539 if (devtoolsInstance !== undefined) {
540 let componentLogsEntry;
541 if (devtoolsInstance.kind === FIBER_INSTANCE) {
542 const fiber = devtoolsInstance.data;
543 componentLogsEntry = fiberToComponentLogsMap.get(fiber);
544
545 if (componentLogsEntry === undefined && fiber.alternate !== null) {
546 componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);
547 }
548 } else {
549 const componentInfo = devtoolsInstance.data;
550 componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo);
551 }
552 if (componentLogsEntry !== undefined) {
553 if (type === 'error') {
554 componentLogsEntry.errors.clear();
555 componentLogsEntry.errorsCount = 0;
556 } else {
557 componentLogsEntry.warnings.clear();
558 componentLogsEntry.warningsCount = 0;
559 }
560 const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry);
561 if (changed) {
562 flushPendingEvents(null);
563 updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
564 }
565 }
566 }
567 }
568
569 function clearErrorsForElementID(instanceID: number) {
570 clearConsoleLogsHelper(instanceID, 'error');
571 }
572
573 function clearWarningsForElementID(instanceID: number) {
574 clearConsoleLogsHelper(instanceID, 'warn');
575 }
576
577 function updateMostRecentlyInspectedElementIfNecessary(
578 fiberID: number,
579 ): void {
580 if (
581 mostRecentlyInspectedElement !== null &&
582 mostRecentlyInspectedElement.id === fiberID
583 ) {
584 hasElementUpdatedSinceLastInspected = true;
585 }
586 }
587
588 function getComponentStack(
589 topFrame: Error,
590 ): null | {enableOwnerStacks: boolean, componentStack: string} {
591 if (getCurrentFiber == null) {
592 // Expected this to be part of the renderer. Ignore.
593 return null;
594 }
595 const current = getCurrentFiber();
596 if (current === null) {
597 // Outside of our render scope.
598 return null;
599 }
600
601 if (supportsConsoleTasks(current)) {
602 // This will be handled natively by console.createTask. No need for
603 // DevTools to add it.
604 return null;
605 }
606
607 const dispatcherRef = getDispatcherRef(renderer);
608 if (dispatcherRef === undefined) {
609 return null;
610 }
611
612 const enableOwnerStacks = supportsOwnerStacks(current);
613 let componentStack = '';
614 if (enableOwnerStacks) {
615 // Prefix the owner stack with the current stack. I.e. what called
616 // console.error. While this will also be part of the native stack,
617 // it is hidden and not presented alongside this argument so we print
618 // them all together.
619 const topStackFrames = formatOwnerStack(topFrame);
620 if (topStackFrames) {
621 componentStack += '\n' + topStackFrames;
622 }
623 componentStack += getOwnerStackByFiberInDev(
624 ReactTypeOfWork,
625 current,
626 dispatcherRef,
627 );
628 } else {
629 componentStack = getStackByFiberInDevAndProd(
630 ReactTypeOfWork,
631 current,
632 dispatcherRef,
633 );
634 }
635 return {enableOwnerStacks, componentStack};
636 }
637
638 // Called when an error or warning is logged during render, commit, or passive (including unmount functions).
639 function onErrorOrWarning(
640 type: 'error' | 'warn',
641 args: $ReadOnlyArray<any>,
642 ): void {
643 if (getCurrentFiber == null) {
644 // Expected this to be part of the renderer. Ignore.
645 return;
646 }
647 const fiber = getCurrentFiber();
648 if (fiber === null) {
649 // Outside of our render scope.
650 return;
651 }
652 if (type === 'error') {
653 // if this is an error simulated by us to trigger error boundary, ignore
654 if (
655 forceErrorForFibers.get(fiber) === true ||
656 (fiber.alternate !== null &&
657 forceErrorForFibers.get(fiber.alternate) === true)
658 ) {
659 return;
660 }
661 }
662
663 // We can't really use this message as a unique key, since we can't distinguish
664 // different objects in this implementation. We have to delegate displaying of the objects
665 // to the environment, the browser console, for example, so this is why this should be kept
666 // as an array of arguments, instead of the plain string.
667 // [Warning: %o, {...}] and [Warning: %o, {...}] will be considered as the same message,
668 // even if objects are different
669 const message = formatConsoleArgumentsToSingleString(...args);
670
671 // Track the warning/error for later.
672 let componentLogsEntry = fiberToComponentLogsMap.get(fiber);
673 if (componentLogsEntry === undefined && fiber.alternate !== null) {
674 componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);
675 if (componentLogsEntry !== undefined) {
676 // Use the same set for both Fibers.
677 fiberToComponentLogsMap.set(fiber, componentLogsEntry);
678 }
679 }
680 if (componentLogsEntry === undefined) {
681 componentLogsEntry = {
682 errors: new Map(),
683 errorsCount: 0 as number,
684 warnings: new Map(),
685 warningsCount: 0 as number,
686 };
687 fiberToComponentLogsMap.set(fiber, componentLogsEntry);
688 }
689
690 const messageMap =
691 type === 'error'
692 ? componentLogsEntry.errors
693 : componentLogsEntry.warnings;
694 const count = messageMap.get(message) || 0;
695 messageMap.set(message, count + 1);
696 if (type === 'error') {
697 componentLogsEntry.errorsCount++;
698 } else {
699 componentLogsEntry.warningsCount++;
700 }
701
702 // The changes will be flushed later when we commit.
703
704 // If the log happened in a passive effect, then this happens after we've
705 // already committed the new tree so the change won't show up until we rerender
706 // that component again. We need to visit a Component with passive effects in
707 // handlePostCommitFiberRoot again to ensure that we flush the changes after passive.
708 needsToFlushComponentLogs = true;
709 }
710
711 function debug(
712 name: string,
713 instance: DevToolsInstance,
714 parentInstance: null | DevToolsInstance,
715 extraString: string = '',
716 ): void {
717 // $FlowFixMe[constant-condition]
718 if (__DEBUG__) {
719 const displayName =
720 instance.kind === VIRTUAL_INSTANCE
721 ? instance.data.name || 'null'
722 : instance.data.tag +
723 ':' +
724 (getDisplayNameForFiber(instance.data) || 'null');
725
726 const maybeID =
727 instance.kind === FILTERED_FIBER_INSTANCE ? '<no id>' : instance.id;
728
729 const parentDisplayName =
730 parentInstance === null
731 ? ''
732 : parentInstance.kind === VIRTUAL_INSTANCE
733 ? parentInstance.data.name || 'null'
734 : parentInstance.data.tag +
735 ':' +
736 (getDisplayNameForFiber(parentInstance.data) || 'null');
737
738 const maybeParentID =
739 parentInstance === null ||
740 parentInstance.kind === FILTERED_FIBER_INSTANCE
741 ? '<no id>'
742 : parentInstance.id;
743
744 console.groupCollapsed(
745 `[renderer] %c${name} %c${displayName} (${maybeID}) %c${
746 parentInstance ? `${parentDisplayName} (${maybeParentID})` : ''
747 } %c${extraString}`,
748 'color: red; font-weight: bold;',
749 'color: blue;',
750 'color: purple;',
751 'color: black;',
752 );
753 console.log(new Error().stack.split('\n').slice(1).join('\n'));
754 console.groupEnd();
755 }
756 }
757
758 // eslint-disable-next-line no-unused-vars
759 function debugTree(instance: DevToolsInstance, indent: number = 0) {
760 // $FlowFixMe[constant-condition]
761 if (__DEBUG__) {
762 const name =
763 (instance.kind !== VIRTUAL_INSTANCE
764 ? getDisplayNameForFiber(instance.data)
765 : instance.data.name) || '';
766 console.log(
767 ' '.repeat(indent) +
768 '- ' +
769 (instance.kind === FILTERED_FIBER_INSTANCE ? 0 : instance.id) +
770 ' (' +
771 name +
772 ')',
773 'parent',
774 instance.parent === null
775 ? ' '
776 : instance.parent.kind === FILTERED_FIBER_INSTANCE
777 ? 0
778 : instance.parent.id,
779 'next',
780 instance.nextSibling === null ? ' ' : instance.nextSibling.id,
781 );
782 let child = instance.firstChild;
783 while (child !== null) {
784 debugTree(child, indent + 1);
785 child = child.nextSibling;
786 }
787 }
788 }
789
790 // Configurable Components tree filters.
791 const hideElementsWithDisplayNames: Set<RegExp> = new Set();
792 const hideElementsWithPaths: Set<RegExp> = new Set();
793 const hideElementsWithTypes: Set<ElementType> = new Set();
794 const hideElementsWithEnvs: Set<string> = new Set();
795 let isInFocusedActivity: boolean = true;
796
797 // Highlight updates
798 let traceUpdatesEnabled: boolean = false;
799 const traceUpdatesForNodes: Set<HostInstance> = new Set();
800
801 function applyComponentFilters(
802 componentFilters: Array<ComponentFilter>,
803 nextActivitySlice: null | Fiber,
804 ) {
805 hideElementsWithTypes.clear();
806 hideElementsWithDisplayNames.clear();
807 hideElementsWithPaths.clear();
808 hideElementsWithEnvs.clear();
809 const previousFocusedActivityID = focusedActivityID;
810 focusedActivityID = null;
811 focusedActivity = null;
812 // Consider everything in the slice by default
813 isInFocusedActivity = true;
814
815 componentFilters.forEach(componentFilter => {
816 if (!componentFilter.isEnabled) {
817 return;
818 }
819
820 switch (componentFilter.type) {
821 case ComponentFilterDisplayName:
822 if (componentFilter.isValid && componentFilter.value !== '') {
823 hideElementsWithDisplayNames.add(
824 new RegExp(componentFilter.value, 'i'),
825 );
826 }
827 break;
828 case ComponentFilterElementType:
829 hideElementsWithTypes.add(componentFilter.value);
830 break;
831 case ComponentFilterLocation:
832 if (componentFilter.isValid && componentFilter.value !== '') {
833 hideElementsWithPaths.add(new RegExp(componentFilter.value, 'i'));
834 }
835 break;
836 case ComponentFilterHOC:
837 hideElementsWithDisplayNames.add(new RegExp('\\('));
838 break;
839 case ComponentFilterEnvironmentName:
840 hideElementsWithEnvs.add(componentFilter.value);
841 break;
842 case ComponentFilterActivitySlice:
843 if (
844 nextActivitySlice !== null &&
845 nextActivitySlice.tag === ActivityComponent
846 ) {
847 focusedActivity = nextActivitySlice;
848 isInFocusedActivity = false;
849 if (componentFilter.rendererID !== rendererID) {
850 // We filtered an Activity from another renderer.
851 // We need to restore the instance ID since we won't be mounting it
852 // in this renderer.
853 focusedActivityID = previousFocusedActivityID;
854 }
855 } else {
856 // We're not filtering by activity slice after all.
857 // Don't mark the filter as disabled here.
858 // Otherwise updateComponentFilters() will think no enabled filter was changed.
859 }
860 break;
861 default:
862 console.warn(
863 `Invalid component filter type "${componentFilter.type}"`,
864 );
865 break;
866 }
867 });
868 }
869
870 if (Array.isArray(componentFiltersOrComponentFiltersPromise)) {
871 applyComponentFilters(componentFiltersOrComponentFiltersPromise, null);
872 } else {
873 componentFiltersOrComponentFiltersPromise.then(componentFilters => {
874 applyComponentFilters(componentFilters, null);
875 });
876 }
877
878 // If necessary, we can revisit optimizing this operation.
879 // For example, we could add a new recursive unmount tree operation.
880 // The unmount operations are already significantly smaller than mount operations though.
881 // This is something to keep in mind for later.
882 function updateComponentFilters(componentFilters: Array<ComponentFilter>) {
883 if (isProfiling) {
884 // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
885 // If necessary, we could support this- but it doesn't seem like a necessary use case.
886 // Supporting change of filters while profiling would require a refactor
887 // to flush after each root instead of at the end.
888 throw Error('Cannot modify filter preferences while profiling');
889 }
890
891 const previousForcedFallbacks =
892 forceFallbackForFibers.size > 0 ? new Set(forceFallbackForFibers) : null;
893 const previousForcedErrors =
894 forceErrorForFibers.size > 0 ? new Map(forceErrorForFibers) : null;
895
896 // The ID will be based on the old tree. We need to find the Fiber based on
897 // that ID before we unmount everything. We set the activity slice ID once
898 // we mount it again.
899 let nextFocusedActivity: null | Fiber = null;
900 let focusedActivityFilter: null | ActivitySliceFilter = null;
901 for (let i = 0; i < componentFilters.length; i++) {
902 const filter = componentFilters[i];
903 if (filter.type === ComponentFilterActivitySlice && filter.isEnabled) {
904 focusedActivityFilter = filter;
905 const instance = idToDevToolsInstanceMap.get(filter.activityID);
906 if (instance !== undefined && instance.kind === FIBER_INSTANCE) {
907 nextFocusedActivity = instance.data;
908 }
909 }
910 }
911
912 // Recursively unmount all roots.
913 hook.getFiberRoots(rendererID).forEach(root => {
914 const rootInstance = rootToFiberInstanceMap.get(root);
915 if (rootInstance === undefined) {
916 throw new Error(
917 'Expected the root instance to already exist when applying filters',
918 );
919 }
920 currentRoot = rootInstance;
921 unmountInstanceRecursively(rootInstance);
922 rootToFiberInstanceMap.delete(root);
923 currentRoot = null as any;
924 });
925
926 if (
927 nextFocusedActivity !== focusedActivity &&
928 (focusedActivityFilter === null ||
929 focusedActivityFilter.rendererID === rendererID)
930 ) {
931 // When we find the applied instance during mount we will send the actual ID.
932 // Otherwise 0 will indicate that we unfocused the activity slice.
933 pushOperation(TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE);
934 pushOperation(0);
935 }
936 applyComponentFilters(componentFilters, nextFocusedActivity);
937
938 // Reset pseudo counters so that new path selections will be persisted.
939 rootDisplayNameCounter.clear();
940
941 // We just cleared all the forced states. Schedule updates on the affected Fibers
942 // so that we get their initial states again according to the new filters.
943 if (typeof scheduleUpdate === 'function') {
944 if (previousForcedFallbacks !== null) {
945 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
946 for (const fiber of previousForcedFallbacks) {
947 if (typeof scheduleRetry === 'function') {
948 scheduleRetry(fiber);
949 } else {
950 scheduleUpdate(fiber);
951 }
952 }
953 }
954 if (
955 previousForcedErrors !== null &&
956 typeof setErrorHandler === 'function'
957 ) {
958 // Unlike for Suspense, disabling the forced error state requires setting
959 // the status to false first. `shouldErrorFiberAccordingToMap` will clear
960 // the Fibers later.
961 setErrorHandler(shouldErrorFiberAccordingToMap);
962 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
963 for (const [fiber, shouldError] of previousForcedErrors) {
964 forceErrorForFibers.set(fiber, false);
965 if (shouldError) {
966 if (typeof scheduleRetry === 'function') {
967 scheduleRetry(fiber);
968 } else {
969 scheduleUpdate(fiber);
970 }
971 }
972 }
973 }
974 }
975
976 // Recursively re-mount all roots with new filter criteria applied.
977 hook.getFiberRoots(rendererID).forEach(root => {
978 const current = root.current;
979 const newRoot = createFiberInstance(current);
980 rootToFiberInstanceMap.set(root, newRoot);
981 idToDevToolsInstanceMap.set(newRoot.id, newRoot);
982
983 // Before the traversals, remember to start tracking
984 // our path in case we have selection to restore.
985 if (trackedPath !== null) {
986 mightBeOnTrackedPath = true;
987 }
988
989 currentRoot = newRoot;
990 setRootPseudoKey(currentRoot.id, root.current);
991 mountFiberRecursively(root.current, false);
992 currentRoot = null as any;
993 });
994
995 // We need to write back the new ID for the focused Fiber.
996 // Otherwise subsequent filter applications will try to focus based on the old ID.
997 // This is also relevant to filter across renderers.
998 if (focusedActivityFilter !== null && focusedActivityID !== null) {
999 focusedActivityFilter.activityID = focusedActivityID;
1000 }
1001
1002 // We're not profiling so it's safe to flush without a specific root.
1003 flushPendingEvents(null);
1004
1005 needsToFlushComponentLogs = false;
1006 }
1007
1008 function getEnvironmentNames(): Array<string> {
1009 return Array.from(knownEnvironmentNames);
1010 }
1011
1012 function isFiberHydrated(fiber: Fiber): boolean {
1013 if (OffscreenComponent === -1) {
1014 throw new Error('not implemented for legacy suspense');
1015 }
1016 switch (fiber.tag) {
1017 case HostRoot:
1018 const rootState = fiber.memoizedState;
1019 return !rootState.isDehydrated;
1020 case SuspenseComponent:
1021 const suspenseState = fiber.memoizedState;
1022 return suspenseState === null || suspenseState.dehydrated === null;
1023 default:
1024 throw new Error('not implemented for work tag ' + fiber.tag);
1025 }
1026 }
1027
1028 function shouldFilterVirtual(
1029 data: ReactComponentInfo,
1030 secondaryEnv: null | string,
1031 ): boolean {
1032 if (!isInFocusedActivity) {
1033 return true;
1034 }
1035
1036 // For purposes of filtering Server Components are always Function Components.
1037 // Environment will be used to filter Server vs Client.
1038 // Technically they can be forwardRef and memo too but those filters will go away
1039 // as those become just plain user space function components like any HoC.
1040 if (hideElementsWithTypes.has(ElementTypeFunction)) {
1041 return true;
1042 }
1043
1044 if (hideElementsWithDisplayNames.size > 0) {
1045 const displayName = data.name;
1046 if (displayName != null) {
1047 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
1048 for (const displayNameRegExp of hideElementsWithDisplayNames) {
1049 if (displayNameRegExp.test(displayName)) {
1050 return true;
1051 }
1052 }
1053 }
1054 }
1055
1056 if (
1057 (data.env == null || hideElementsWithEnvs.has(data.env)) &&
1058 (secondaryEnv === null || hideElementsWithEnvs.has(secondaryEnv))
1059 ) {
1060 // If a Component has two environments, you have to filter both for it not to appear.
1061 return true;
1062 }
1063
1064 return false;
1065 }
1066
1067 // NOTICE Keep in sync with get*ForFiber methods
1068 function shouldFilterFiber(fiber: Fiber): boolean {
1069 const {tag, type, key} = fiber;
1070
1071 // It is never valid to filter the root element.
1072 if (tag !== HostRoot && !isInFocusedActivity) {
1073 return true;
1074 }
1075
1076 switch (tag) {
1077 case DehydratedSuspenseComponent:
1078 // TODO: ideally we would show dehydrated Suspense immediately.
1079 // However, it has some special behavior (like disconnecting
1080 // an alternate and turning into real Suspense) which breaks DevTools.
1081 // For now, ignore it, and only show it once it gets hydrated.
1082 // https://github.com/bvaughn/react-devtools-experimental/issues/197
1083 return true;
1084 case HostPortal:
1085 case HostText:
1086 case LegacyHiddenComponent:
1087 case OffscreenComponent:
1088 case Throw:
1089 return true;
1090 case HostRoot:
1091 // It is never valid to filter the root element.
1092 return false;
1093 case Fragment:
1094 return key === null;
1095 default:
1096 const typeSymbol = getTypeSymbol(type);
1097
1098 switch (typeSymbol) {
1099 case CONCURRENT_MODE_NUMBER:
1100 case CONCURRENT_MODE_SYMBOL_STRING:
1101 case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
1102 case STRICT_MODE_NUMBER:
1103 case STRICT_MODE_SYMBOL_STRING:
1104 return true;
1105 default:
1106 break;
1107 }
1108 }
1109
1110 const elementType = getElementTypeForFiber(fiber);
1111 if (hideElementsWithTypes.has(elementType)) {
1112 return true;
1113 }
1114
1115 if (hideElementsWithDisplayNames.size > 0) {
1116 const displayName = getDisplayNameForFiber(fiber);
1117 if (displayName != null) {
1118 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
1119 for (const displayNameRegExp of hideElementsWithDisplayNames) {
1120 if (displayNameRegExp.test(displayName)) {
1121 return true;
1122 }
1123 }
1124 }
1125 }
1126
1127 if (hideElementsWithEnvs.has('Client')) {
1128 // If we're filtering out the Client environment we should filter out all
1129 // "Client Components". Technically that also includes the built-ins but
1130 // since that doesn't actually include any additional code loading it's
1131 // useful to not filter out the built-ins. Those can be filtered separately.
1132 // There's no other way to filter out just Function components on the Client.
1133 // Therefore, this only filters Class and Function components.
1134 switch (tag) {
1135 case ClassComponent:
1136 case IncompleteClassComponent:
1137 case IncompleteFunctionComponent:
1138 case FunctionComponent:
1139 case IndeterminateComponent:
1140 case ForwardRef:
1141 case MemoComponent:
1142 case SimpleMemoComponent:
1143 return true;
1144 }
1145 }
1146
1147 /* DISABLED: https://github.com/facebook/react/pull/28417
1148 if (hideElementsWithPaths.size > 0) {
1149 const source = getSourceForFiber(fiber);
1150
1151 if (source != null) {
1152 const {fileName} = source;
1153 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
1154 for (const pathRegExp of hideElementsWithPaths) {
1155 if (pathRegExp.test(fileName)) {
1156 return true;
1157 }
1158 }
1159 }
1160 }
1161 */
1162
1163 return false;
1164 }
1165
1166 // NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
1167 function getElementTypeForFiber(fiber: Fiber): ElementType {
1168 const {type, tag} = fiber;
1169
1170 switch (tag) {
1171 case ActivityComponent:
1172 return ElementTypeActivity;
1173 case ClassComponent:
1174 case IncompleteClassComponent:
1175 return ElementTypeClass;
1176 case IncompleteFunctionComponent:
1177 case FunctionComponent:
1178 case IndeterminateComponent:
1179 return ElementTypeFunction;
1180 case ForwardRef:
1181 return ElementTypeForwardRef;
1182 case HostRoot:
1183 return ElementTypeRoot;
1184 case HostComponent:
1185 case HostHoistable:
1186 case HostSingleton:
1187 return ElementTypeHostComponent;
1188 case HostPortal:
1189 case HostText:
1190 case Fragment:
1191 return ElementTypeOtherOrUnknown;
1192 case MemoComponent:
1193 case SimpleMemoComponent:
1194 return ElementTypeMemo;
1195 case SuspenseComponent:
1196 return ElementTypeSuspense;
1197 case SuspenseListComponent:
1198 return ElementTypeSuspenseList;
1199 case TracingMarkerComponent:
1200 return ElementTypeTracingMarker;
1201 case ViewTransitionComponent:
1202 return ElementTypeViewTransition;
1203 default:
1204 const typeSymbol = getTypeSymbol(type);
1205
1206 switch (typeSymbol) {
1207 case CONCURRENT_MODE_NUMBER:
1208 case CONCURRENT_MODE_SYMBOL_STRING:
1209 case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
1210 return ElementTypeOtherOrUnknown;
1211 case PROVIDER_NUMBER:
1212 case PROVIDER_SYMBOL_STRING:
1213 return ElementTypeContext;
1214 case CONTEXT_NUMBER:
1215 case CONTEXT_SYMBOL_STRING:
1216 return ElementTypeContext;
1217 case STRICT_MODE_NUMBER:
1218 case STRICT_MODE_SYMBOL_STRING:
1219 return ElementTypeOtherOrUnknown;
1220 case PROFILER_NUMBER:
1221 case PROFILER_SYMBOL_STRING:
1222 return ElementTypeProfiler;
1223 default:
1224 return ElementTypeOtherOrUnknown;
1225 }
1226 }
1227 }
1228
1229 // When a mount or update is in progress, this value tracks the root that is being operated on.
1230 let currentRoot: FiberInstance = null as any;
1231
1232 // Removes a Fiber (and its alternate) from the Maps used to track their id.
1233 // This method should always be called when a Fiber is unmounting.
1234 function untrackFiber(nearestInstance: DevToolsInstance, fiber: Fiber) {
1235 if (forceErrorForFibers.size > 0) {
1236 forceErrorForFibers.delete(fiber);
1237 if (fiber.alternate) {
1238 forceErrorForFibers.delete(fiber.alternate);
1239 }
1240 if (forceErrorForFibers.size === 0 && setErrorHandler != null) {
1241 setErrorHandler(shouldErrorFiberAlwaysNull);
1242 }
1243 }
1244
1245 if (forceFallbackForFibers.size > 0) {
1246 forceFallbackForFibers.delete(fiber);
1247 if (fiber.alternate) {
1248 forceFallbackForFibers.delete(fiber.alternate);
1249 }
1250 if (forceFallbackForFibers.size === 0 && setSuspenseHandler != null) {
1251 setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
1252 }
1253 }
1254
1255 // TODO: Consider using a WeakMap instead. The only thing where that doesn't work
1256 // is React Native Paper which tracks tags but that support is eventually going away
1257 // and can use the old findFiberByHostInstance strategy.
1258
1259 if (fiber.tag === HostHoistable) {
1260 releaseHostResource(nearestInstance, fiber.memoizedState);
1261 } else if (
1262 fiber.tag === HostComponent ||
1263 fiber.tag === HostText ||
1264 fiber.tag === HostSingleton
1265 ) {
1266 releaseHostInstance(nearestInstance, fiber.stateNode);
1267 }
1268
1269 // Recursively clean up any filtered Fibers below this one as well since
1270 // we won't recordUnmount on those.
1271 for (let child = fiber.child; child !== null; child = child.sibling) {
1272 if (shouldFilterFiber(child)) {
1273 untrackFiber(nearestInstance, child);
1274 }
1275 }
1276 }
1277
1278 function getChangeDescription(
1279 prevFiber: Fiber | null,
1280 nextFiber: Fiber,
1281 ): ChangeDescription | null {
1282 switch (nextFiber.tag) {
1283 case ClassComponent:
1284 if (prevFiber === null) {
1285 return {
1286 context: null,
1287 didHooksChange: false,
1288 isFirstMount: true,
1289 props: null,
1290 state: null,
1291 };
1292 } else {
1293 const data: ChangeDescription = {
1294 context: getContextChanged(prevFiber, nextFiber),
1295 didHooksChange: false,
1296 isFirstMount: false,
1297 props: getChangedKeys(
1298 prevFiber.memoizedProps,
1299 nextFiber.memoizedProps,
1300 ),
1301 state: getChangedKeys(
1302 prevFiber.memoizedState,
1303 nextFiber.memoizedState,
1304 ),
1305 };
1306 return data;
1307 }
1308 case IncompleteFunctionComponent:
1309 case FunctionComponent:
1310 case IndeterminateComponent:
1311 case ForwardRef:
1312 case MemoComponent:
1313 case SimpleMemoComponent:
1314 if (prevFiber === null) {
1315 return {
1316 context: null,
1317 didHooksChange: false,
1318 isFirstMount: true,
1319 props: null,
1320 state: null,
1321 };
1322 } else {
1323 const prevHooks = inspectHooks(prevFiber);
1324 const nextHooks = inspectHooks(nextFiber);
1325 const indices = getChangedHooksIndices(prevHooks, nextHooks);
1326 const data: ChangeDescription = {
1327 context: getContextChanged(prevFiber, nextFiber),
1328 didHooksChange: indices !== null && indices.length > 0,
1329 isFirstMount: false,
1330 props: getChangedKeys(
1331 prevFiber.memoizedProps,
1332 nextFiber.memoizedProps,
1333 ),
1334 state: null,
1335 hooks: indices,
1336 };
1337 // Only traverse the hooks list once, depending on what info we're returning.
1338 return data;
1339 }
1340 default:
1341 return null;
1342 }
1343 }
1344
1345 type OperationsArray = Array<number>;
1346
1347 type StringTableEntry = {
1348 encodedString: Array<number>,
1349 id: number,
1350 };
1351
1352 const pendingOperations: OperationsArray = [];
1353 const pendingRealUnmountedIDs: Array<FiberInstance['id']> = [];
1354 const pendingRealUnmountedSuspenseIDs: Array<FiberInstance['id']> = [];
1355 const pendingSuspenderChanges: Set<FiberInstance['id']> = new Set();
1356 let pendingOperationsQueue: Array<OperationsArray> | null = [];
1357 const pendingStringTable: Map<string, StringTableEntry> = new Map();
1358 let pendingStringTableLength: number = 0;
1359
1360 function pushOperation(op: number): void {
1361 if (__DEV__) {
1362 if (!Number.isInteger(op)) {
1363 console.error(
1364 'pushOperation() was called but the value is not an integer.',
1365 op,
1366 );
1367 }
1368 }
1369 pendingOperations.push(op);
1370 }
1371
1372 function shouldBailoutWithPendingOperations() {
1373 if (isProfiling) {
1374 if (
1375 currentCommitProfilingMetadata != null &&
1376 currentCommitProfilingMetadata.durations.length > 0
1377 ) {
1378 return false;
1379 }
1380 }
1381
1382 return (
1383 pendingOperations.length === 0 &&
1384 pendingRealUnmountedIDs.length === 0 &&
1385 pendingRealUnmountedSuspenseIDs.length === 0 &&
1386 pendingSuspenderChanges.size === 0
1387 );
1388 }
1389
1390 function flushOrQueueOperations(operations: OperationsArray): void {
1391 if (shouldBailoutWithPendingOperations()) {
1392 return;
1393 }
1394
1395 if (pendingOperationsQueue !== null) {
1396 pendingOperationsQueue.push(operations);
1397 } else {
1398 hook.emit('operations', operations);
1399 }
1400 }
1401
1402 function recordConsoleLogs(
1403 instance: FiberInstance | VirtualInstance,
1404 componentLogsEntry: void | ComponentLogs,
1405 ): boolean {
1406 if (componentLogsEntry === undefined) {
1407 if (instance.logCount === 0) {
1408 // Nothing has changed.
1409 return false;
1410 }
1411 // Reset to zero.
1412 instance.logCount = 0;
1413 pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);
1414 pushOperation(instance.id);
1415 pushOperation(0);
1416 pushOperation(0);
1417 return true;
1418 } else {
1419 const totalCount =
1420 componentLogsEntry.errorsCount + componentLogsEntry.warningsCount;
1421 if (instance.logCount === totalCount) {
1422 // Nothing has changed.
1423 return false;
1424 }
1425 // Update counts.
1426 instance.logCount = totalCount;
1427 pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);
1428 pushOperation(instance.id);
1429 pushOperation(componentLogsEntry.errorsCount);
1430 pushOperation(componentLogsEntry.warningsCount);
1431 return true;
1432 }
1433 }
1434
1435 /**
1436 * Allowed to flush pending events without a specific root when:
1437 * - pending operations don't record tree mutations e.g. TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS
1438 * - not profiling (the commit tree builder requires the root of the mutations)
1439 */
1440 function flushPendingEvents(root: FiberInstance | null): void {
1441 if (shouldBailoutWithPendingOperations()) {
1442 // If we aren't profiling, we can just bail out here.
1443 // No use sending an empty update over the bridge.
1444 //
1445 // The Profiler stores metadata for each commit and reconstructs the app tree per commit using:
1446 // (1) an initial tree snapshot and
1447 // (2) the operations array for each commit
1448 // Because of this, it's important that the operations and metadata arrays align,
1449 // So it's important not to omit even empty operations while profiling is active.
1450 return;
1451 }
1452
1453 const numUnmountIDs = pendingRealUnmountedIDs.length;
1454 const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;
1455 const numSuspenderChanges = pendingSuspenderChanges.size;
1456
1457 const operations = new Array<number>(
1458 // Identify which renderer this update is coming from.
1459 2 + // [rendererID, rootFiberID]
1460 // How big is the string table?
1461 1 + // [stringTableLength]
1462 // Then goes the actual string table.
1463 pendingStringTableLength +
1464 // All unmounts of Suspense boundaries are batched in a single message.
1465 // [TREE_OPERATION_REMOVE_SUSPENSE, removedSuspenseIDLength, ...ids]
1466 (numUnmountSuspenseIDs > 0 ? 2 + numUnmountSuspenseIDs : 0) +
1467 // All unmounts are batched in a single message.
1468 // [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
1469 (numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
1470 // Regular operations
1471 pendingOperations.length +
1472 // All suspender changes are batched in a single message.
1473 // [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders, endTime, isSuspended]]
1474 (numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 4 : 0),
1475 );
1476
1477 // Identify which renderer this update is coming from.
1478 // This enables roots to be mapped to renderers,
1479 // Which in turn enables fiber props, states, and hooks to be inspected.
1480 let i = 0;
1481 operations[i++] = rendererID;
1482 if (root === null) {
1483 operations[i++] = -1;
1484 } else {
1485 operations[i++] = root.id;
1486 }
1487
1488 // Now fill in the string table.
1489 // [stringTableLength, str1Length, ...str1, str2Length, ...str2, ...]
1490 operations[i++] = pendingStringTableLength;
1491 pendingStringTable.forEach((entry, stringKey) => {
1492 const encodedString = entry.encodedString;
1493
1494 // Don't use the string length.
1495 // It won't work for multibyte characters (like emoji).
1496 const length = encodedString.length;
1497
1498 operations[i++] = length;
1499 for (let j = 0; j < length; j++) {
1500 operations[i + j] = encodedString[j];
1501 }
1502
1503 i += length;
1504 });
1505
1506 if (numUnmountSuspenseIDs > 0) {
1507 // All unmounts of Suspense boundaries are batched in a single message.
1508 operations[i++] = SUSPENSE_TREE_OPERATION_REMOVE;
1509 // The first number is how many unmounted IDs we're gonna send.
1510 operations[i++] = numUnmountSuspenseIDs;
1511 // Fill in the real unmounts in the reverse order.
1512 // They were inserted parents-first by React, but we want children-first.
1513 // So we traverse our array backwards.
1514 for (let j = 0; j < pendingRealUnmountedSuspenseIDs.length; j++) {
1515 operations[i++] = pendingRealUnmountedSuspenseIDs[j];
1516 }
1517 }
1518
1519 if (numUnmountIDs > 0) {
1520 // All unmounts except roots are batched in a single message.
1521 operations[i++] = TREE_OPERATION_REMOVE;
1522 // The first number is how many unmounted IDs we're gonna send.
1523 operations[i++] = numUnmountIDs;
1524 // Fill in the real unmounts in the reverse order.
1525 // They were inserted parents-first by React, but we want children-first.
1526 // So we traverse our array backwards.
1527 for (let j = 0; j < pendingRealUnmountedIDs.length; j++) {
1528 operations[i++] = pendingRealUnmountedIDs[j];
1529 }
1530 }
1531
1532 // Fill in pending operations.
1533 for (let j = 0; j < pendingOperations.length; j++) {
1534 operations[i + j] = pendingOperations[j];
1535 }
1536 i += pendingOperations.length;
1537
1538 // Suspender changes might affect newly mounted nodes that we already recorded
1539 // in pending operations.
1540 if (numSuspenderChanges > 0) {
1541 operations[i++] = SUSPENSE_TREE_OPERATION_SUSPENDERS;
1542 operations[i++] = numSuspenderChanges;
1543 pendingSuspenderChanges.forEach(fiberIdWithChanges => {
1544 const suspense = idToSuspenseNodeMap.get(fiberIdWithChanges);
1545 if (suspense === undefined) {
1546 // Probably forgot to cleanup pendingSuspenderChanges when this node was removed.
1547 throw new Error(
1548 `Could not send suspender changes for "${fiberIdWithChanges}" since the Fiber no longer exists.`,
1549 );
1550 }
1551 operations[i++] = fiberIdWithChanges;
1552 operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;
1553 operations[i++] = Math.round(suspense.endTime * 1000);
1554 const instance = suspense.instance;
1555 const isSuspended =
1556 // TODO: Track if other SuspenseNode like SuspenseList rows are suspended.
1557 (instance.kind === FIBER_INSTANCE ||
1558 instance.kind === FILTERED_FIBER_INSTANCE) &&
1559 instance.data.tag === SuspenseComponent &&
1560 instance.data.memoizedState !== null;
1561 operations[i++] = isSuspended ? 1 : 0;
1562 operations[i++] = suspense.environments.size;
1563 suspense.environments.forEach((count, env) => {
1564 operations[i++] = getStringID(env);
1565 });
1566 });
1567 }
1568
1569 // Let the frontend know about tree operations.
1570 flushOrQueueOperations(operations);
1571
1572 // Reset all of the pending state now that we've told the frontend about it.
1573 pendingOperations.length = 0;
1574 pendingRealUnmountedIDs.length = 0;
1575 pendingRealUnmountedSuspenseIDs.length = 0;
1576 pendingSuspenderChanges.clear();
1577 pendingStringTable.clear();
1578 pendingStringTableLength = 0;
1579 }
1580
1581 function measureHostInstance(instance: HostInstance): null | Array<Rect> {
1582 // Feature detect measurement capabilities of this environment.
1583 // TODO: Consider making this capability injected by the ReactRenderer.
1584 if (typeof instance !== 'object' || instance === null) {
1585 return null;
1586 }
1587 if (
1588 typeof instance.getClientRects === 'function' ||
1589 instance.nodeType === 3
1590 ) {
1591 // DOM
1592 const doc = instance.ownerDocument;
1593 if (instance === doc.documentElement) {
1594 // This is the document element. The size of this element is not actually
1595 // what determines the whole scrollable area of the screen. Because any
1596 // thing that overflows the document will also contribute to the scrollable.
1597 // This is unlike overflow: scroll which clips those.
1598 // Therefore, we use the scrollable size for this rect instead.
1599 return [
1600 {
1601 x: 0,
1602 y: 0,
1603 width: instance.scrollWidth,
1604 height: instance.scrollHeight,
1605 },
1606 ];
1607 }
1608 const result: Array<Rect> = [];
1609 const win = doc && doc.defaultView;
1610 const scrollX = win ? win.scrollX : 0;
1611 const scrollY = win ? win.scrollY : 0;
1612 let rects;
1613 if (instance.nodeType === 3) {
1614 // Text nodes cannot be measured directly but we can measure a Range.
1615 if (typeof doc.createRange !== 'function') {
1616 return null;
1617 }
1618 const range = doc.createRange();
1619 if (typeof range.getClientRects !== 'function') {
1620 return null;
1621 }
1622 range.selectNodeContents(instance);
1623 rects = range.getClientRects();
1624 } else {
1625 rects = instance.getClientRects();
1626 }
1627 for (let i = 0; i < rects.length; i++) {
1628 const rect = rects[i];
1629 result.push({
1630 x: rect.x + scrollX,
1631 y: rect.y + scrollY,
1632 width: rect.width,
1633 height: rect.height,
1634 });
1635 }
1636 return result;
1637 }
1638 if (instance.canonical) {
1639 // Native
1640 const publicInstance = instance.canonical.publicInstance;
1641 if (!publicInstance) {
1642 // The publicInstance may not have been initialized yet if there was no ref on this node.
1643 // We can't initialize it from any existing Hook but we could fallback to this async form:
1644 // renderer.extraDevToolsConfig.getInspectorDataForInstance(instance).hierarchy[last].getInspectorData().measure(callback)
1645 return null;
1646 }
1647 if (typeof publicInstance.getBoundingClientRect === 'function') {
1648 // enableAccessToHostTreeInFabric / ReadOnlyElement
1649 return [publicInstance.getBoundingClientRect()];
1650 }
1651 if (typeof publicInstance.unstable_getBoundingClientRect === 'function') {
1652 // ReactFabricHostComponent
1653 return [publicInstance.unstable_getBoundingClientRect()];
1654 }
1655 }
1656 return null;
1657 }
1658
1659 function measureInstance(instance: DevToolsInstance): null | Array<Rect> {
1660 // Synchronously return the client rects of the Host instances directly inside this Instance.
1661 const hostInstances = findAllCurrentHostInstances(instance);
1662 let result: null | Array<Rect> = null;
1663 for (let i = 0; i < hostInstances.length; i++) {
1664 const childResult = measureHostInstance(hostInstances[i]);
1665 if (childResult !== null) {
1666 if (result === null) {
1667 result = childResult;
1668 } else {
1669 result = result.concat(childResult);
1670 }
1671 }
1672 }
1673 return result;
1674 }
1675
1676 function getStringID(string: string | null): number {
1677 if (string === null) {
1678 return 0;
1679 }
1680 const existingEntry = pendingStringTable.get(string);
1681 if (existingEntry !== undefined) {
1682 return existingEntry.id;
1683 }
1684
1685 const id = pendingStringTable.size + 1;
1686 const encodedString = utfEncodeString(string);
1687
1688 pendingStringTable.set(string, {
1689 encodedString,
1690 id,
1691 });
1692
1693 // The string table total length needs to account both for the string length,
1694 // and for the array item that contains the length itself.
1695 //
1696 // Don't use string length for this table.
1697 // It won't work for multibyte characters (like emoji).
1698 pendingStringTableLength += encodedString.length + 1;
1699
1700 return id;
1701 }
1702
1703 let isInDisconnectedSubtree = false;
1704
1705 function recordMount(
1706 fiber: Fiber,
1707 parentInstance: DevToolsInstance | null,
1708 ): FiberInstance {
1709 const isRoot = fiber.tag === HostRoot;
1710 let fiberInstance;
1711 if (isRoot) {
1712 const entry = rootToFiberInstanceMap.get(fiber.stateNode);
1713 if (entry === undefined) {
1714 throw new Error('The root should have been registered at this point');
1715 }
1716 fiberInstance = entry;
1717 } else {
1718 fiberInstance = createFiberInstance(fiber);
1719 }
1720 idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance);
1721
1722 // $FlowFixMe[constant-condition]
1723 if (__DEBUG__) {
1724 debug('recordMount()', fiberInstance, parentInstance);
1725 }
1726
1727 recordReconnect(fiberInstance, parentInstance);
1728 return fiberInstance;
1729 }
1730
1731 function recordReconnect(
1732 fiberInstance: FiberInstance,
1733 parentInstance: DevToolsInstance | null,
1734 ): void {
1735 if (isInDisconnectedSubtree) {
1736 // We're disconnected. We'll reconnect a hidden mount after the parent reappears.
1737 return;
1738 }
1739 const id = fiberInstance.id;
1740 const fiber = fiberInstance.data;
1741
1742 const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
1743
1744 const isRoot = fiber.tag === HostRoot;
1745
1746 if (isRoot) {
1747 const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
1748
1749 // Adding a new field here would require a bridge protocol version bump (a backwads breaking change).
1750 // Instead let's re-purpose a pre-existing field to carry more information.
1751 let profilingFlags = 0;
1752 if (isProfilingSupported) {
1753 profilingFlags = PROFILING_FLAG_BASIC_SUPPORT;
1754 if (supportsPerformanceTracks) {
1755 profilingFlags |= PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT;
1756 }
1757 }
1758
1759 // Set supportsStrictMode to false for production renderer builds
1760 const isProductionBuildOfRenderer = renderer.bundleType === 0;
1761
1762 pushOperation(TREE_OPERATION_ADD);
1763 pushOperation(id);
1764 pushOperation(ElementTypeRoot);
1765 pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0);
1766 pushOperation(profilingFlags);
1767 pushOperation(
1768 !isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0,
1769 );
1770 pushOperation(hasOwnerMetadata ? 1 : 0);
1771
1772 if (isProfiling) {
1773 if (displayNamesByRootID !== null) {
1774 displayNamesByRootID.set(id, getDisplayNameForRoot(fiber));
1775 }
1776 }
1777 } else {
1778 const suspenseNode = fiberInstance.suspenseNode;
1779 if (suspenseNode !== null && fiber.memoizedState === null) {
1780 // We're reconnecting an unsuspended Suspense. Measure to see if anything changed.
1781 const prevRects = suspenseNode.rects;
1782 const nextRects = measureInstance(fiberInstance);
1783 if (!areEqualRects(prevRects, nextRects)) {
1784 suspenseNode.rects = nextRects;
1785 recordSuspenseResize(suspenseNode);
1786 }
1787 }
1788
1789 const {key} = fiber;
1790 const displayName = getDisplayNameForFiber(fiber);
1791 const elementType = getElementTypeForFiber(fiber);
1792
1793 // Finding the owner instance might require traversing the whole parent path which
1794 // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we
1795 // need it but we have some synchronous operations in the front end like Alt+Left
1796 // which selects the owner immediately. Typically most owners are only a few parents
1797 // away so maybe it's not so bad.
1798 const debugOwner = getUnfilteredOwner(fiber);
1799 const ownerInstance = findNearestOwnerInstance(
1800 parentInstance,
1801 debugOwner,
1802 );
1803 if (
1804 ownerInstance !== null &&
1805 debugOwner === fiber._debugOwner &&
1806 fiber._debugStack != null &&
1807 ownerInstance.source === null
1808 ) {
1809 // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on
1810 // the debugStack will be a stack frame inside the ownerInstance's source.
1811 ownerInstance.source = fiber._debugStack;
1812 }
1813
1814 let unfilteredParent = parentInstance;
1815 while (
1816 unfilteredParent !== null &&
1817 unfilteredParent.kind === FILTERED_FIBER_INSTANCE
1818 ) {
1819 unfilteredParent = unfilteredParent.parent;
1820 }
1821
1822 const ownerID = ownerInstance === null ? 0 : ownerInstance.id;
1823 const parentID = unfilteredParent === null ? 0 : unfilteredParent.id;
1824
1825 const displayNameStringID = getStringID(displayName);
1826
1827 // This check is a guard to handle a React element that has been modified
1828 // in such a way as to bypass the default stringification of the "key" property.
1829 const keyString =
1830 key === null
1831 ? null
1832 : key === REACT_OPTIMISTIC_KEY
1833 ? 'React.optimisticKey'
1834 : String(key);
1835 const keyStringID = getStringID(keyString);
1836
1837 const nameProp =
1838 fiber.tag === SuspenseComponent
1839 ? fiber.memoizedProps.name
1840 : fiber.tag === ActivityComponent
1841 ? fiber.memoizedProps.name
1842 : null;
1843 const namePropString = nameProp == null ? null : String(nameProp);
1844 const namePropStringID = getStringID(namePropString);
1845
1846 pushOperation(TREE_OPERATION_ADD);
1847 pushOperation(id);
1848 pushOperation(elementType);
1849 pushOperation(parentID);
1850 pushOperation(ownerID);
1851 pushOperation(displayNameStringID);
1852 pushOperation(keyStringID);
1853 pushOperation(namePropStringID);
1854
1855 // If this subtree has a new mode, let the frontend know.
1856 if ((fiber.mode & StrictModeBits) !== 0) {
1857 let parentFiber = null;
1858 let parentFiberInstance = parentInstance;
1859 while (parentFiberInstance !== null) {
1860 if (parentFiberInstance.kind === FIBER_INSTANCE) {
1861 parentFiber = parentFiberInstance.data;
1862 break;
1863 }
1864 parentFiberInstance = parentFiberInstance.parent;
1865 }
1866 if (parentFiber === null || (parentFiber.mode & StrictModeBits) === 0) {
1867 pushOperation(TREE_OPERATION_SET_SUBTREE_MODE);
1868 pushOperation(id);
1869 pushOperation(StrictMode);
1870 }
1871 }
1872
1873 // If this is an Activity component, check if it's hidden.
1874 if (fiber.tag === ActivityComponent) {
1875 const offscreenChild = fiber.child;
1876 if (
1877 offscreenChild !== null &&
1878 offscreenChild.tag === OffscreenComponent &&
1879 offscreenChild.memoizedState !== null
1880 ) {
1881 pushOperation(TREE_OPERATION_SET_SUBTREE_MODE);
1882 pushOperation(id);
1883 pushOperation(ActivityHiddenMode);
1884 }
1885 }
1886 }
1887
1888 let componentLogsEntry = fiberToComponentLogsMap.get(fiber);
1889 if (componentLogsEntry === undefined && fiber.alternate !== null) {
1890 componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);
1891 }
1892 recordConsoleLogs(fiberInstance, componentLogsEntry);
1893
1894 if (isProfilingSupported) {
1895 recordProfilingDurations(fiberInstance, null);
1896 }
1897 }
1898
1899 function recordVirtualMount(
1900 instance: VirtualInstance,
1901 parentInstance: DevToolsInstance | null,
1902 secondaryEnv: null | string,
1903 ): void {
1904 const id = instance.id;
1905
1906 idToDevToolsInstanceMap.set(id, instance);
1907
1908 recordVirtualReconnect(instance, parentInstance, secondaryEnv);
1909 }
1910
1911 function recordVirtualReconnect(
1912 instance: VirtualInstance,
1913 parentInstance: DevToolsInstance | null,
1914 secondaryEnv: null | string,
1915 ): void {
1916 if (isInDisconnectedSubtree) {
1917 // We're disconnected. We'll reconnect a hidden mount after the parent reappears.
1918 return;
1919 }
1920 const componentInfo = instance.data;
1921
1922 const key =
1923 typeof componentInfo.key === 'string' ? componentInfo.key : null;
1924 const env = componentInfo.env;
1925 let displayName = componentInfo.name || '';
1926 if (typeof env === 'string') {
1927 // We model environment as an HoC name for now.
1928 if (secondaryEnv !== null) {
1929 displayName = secondaryEnv + '(' + displayName + ')';
1930 }
1931 displayName = env + '(' + displayName + ')';
1932 }
1933 const elementType = ElementTypeVirtual;
1934
1935 // Finding the owner instance might require traversing the whole parent path which
1936 // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we
1937 // need it but we have some synchronous operations in the front end like Alt+Left
1938 // which selects the owner immediately. Typically most owners are only a few parents
1939 // away so maybe it's not so bad.
1940 const debugOwner = getUnfilteredOwner(componentInfo);
1941 const ownerInstance = findNearestOwnerInstance(parentInstance, debugOwner);
1942 if (
1943 ownerInstance !== null &&
1944 debugOwner === componentInfo.owner &&
1945 componentInfo.debugStack != null &&
1946 ownerInstance.source === null
1947 ) {
1948 // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on
1949 // the debugStack will be a stack frame inside the ownerInstance's source.
1950 ownerInstance.source = componentInfo.debugStack;
1951 }
1952
1953 let unfilteredParent = parentInstance;
1954 while (
1955 unfilteredParent !== null &&
1956 unfilteredParent.kind === FILTERED_FIBER_INSTANCE
1957 ) {
1958 unfilteredParent = unfilteredParent.parent;
1959 }
1960
1961 const ownerID = ownerInstance === null ? 0 : ownerInstance.id;
1962 const parentID = unfilteredParent === null ? 0 : unfilteredParent.id;
1963
1964 const displayNameStringID = getStringID(displayName);
1965
1966 // This check is a guard to handle a React element that has been modified
1967 // in such a way as to bypass the default stringification of the "key" property.
1968 const keyString = key === null ? null : String(key);
1969 const keyStringID = getStringID(keyString);
1970 const namePropStringID = getStringID(null);
1971
1972 const id = instance.id;
1973
1974 pushOperation(TREE_OPERATION_ADD);
1975 pushOperation(id);
1976 pushOperation(elementType);
1977 pushOperation(parentID);
1978 pushOperation(ownerID);
1979 pushOperation(displayNameStringID);
1980 pushOperation(keyStringID);
1981 pushOperation(namePropStringID);
1982
1983 const componentLogsEntry =
1984 componentInfoToComponentLogsMap.get(componentInfo);
1985 recordConsoleLogs(instance, componentLogsEntry);
1986 }
1987
1988 function recordSuspenseMount(
1989 suspenseInstance: SuspenseNode,
1990 parentSuspenseInstance: SuspenseNode | null,
1991 ): void {
1992 const fiberInstance = suspenseInstance.instance;
1993 if (fiberInstance.kind === FILTERED_FIBER_INSTANCE) {
1994 throw new Error('Cannot record a mount for a filtered Fiber instance.');
1995 }
1996 const fiberID = fiberInstance.id;
1997
1998 let unfilteredParent = parentSuspenseInstance;
1999 while (
2000 unfilteredParent !== null &&
2001 unfilteredParent.instance.kind === FILTERED_FIBER_INSTANCE
2002 ) {
2003 unfilteredParent = unfilteredParent.parent;
2004 }
2005 const unfilteredParentInstance =
2006 unfilteredParent !== null ? unfilteredParent.instance : null;
2007 if (
2008 unfilteredParentInstance !== null &&
2009 unfilteredParentInstance.kind === FILTERED_FIBER_INSTANCE
2010 ) {
2011 throw new Error(
2012 'Should not have a filtered instance at this point. This is a bug.',
2013 );
2014 }
2015 const parentID =
2016 unfilteredParentInstance === null ? 0 : unfilteredParentInstance.id;
2017
2018 const fiber = fiberInstance.data;
2019 const props = fiber.memoizedProps;
2020 // The frontend will guess a name based on heuristics (e.g. owner) if no explicit name is given.
2021 const name =
2022 fiber.tag !== SuspenseComponent || props === null
2023 ? null
2024 : props.name || null;
2025 const nameStringID = getStringID(name);
2026
2027 const isSuspended =
2028 fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
2029
2030 // $FlowFixMe[constant-condition]
2031 if (__DEBUG__) {
2032 console.log('recordSuspenseMount()', suspenseInstance);
2033 }
2034
2035 idToSuspenseNodeMap.set(fiberID, suspenseInstance);
2036
2037 pushOperation(SUSPENSE_TREE_OPERATION_ADD);
2038 pushOperation(fiberID);
2039 pushOperation(parentID);
2040 pushOperation(nameStringID);
2041 pushOperation(isSuspended ? 1 : 0);
2042
2043 const rects = suspenseInstance.rects;
2044 if (rects === null) {
2045 pushOperation(-1);
2046 } else {
2047 pushOperation(rects.length);
2048 for (let i = 0; i < rects.length; ++i) {
2049 const rect = rects[i];
2050 pushOperation(Math.round(rect.x * 1000));
2051 pushOperation(Math.round(rect.y * 1000));
2052 pushOperation(Math.round(rect.width * 1000));
2053 pushOperation(Math.round(rect.height * 1000));
2054 }
2055 }
2056 }
2057
2058 function recordUnmount(fiberInstance: FiberInstance): void {
2059 // $FlowFixMe[constant-condition]
2060 if (__DEBUG__) {
2061 debug('recordUnmount()', fiberInstance, reconcilingParent);
2062 }
2063
2064 recordDisconnect(fiberInstance);
2065
2066 const suspenseNode = fiberInstance.suspenseNode;
2067 if (suspenseNode !== null) {
2068 recordSuspenseUnmount(suspenseNode);
2069 }
2070
2071 idToDevToolsInstanceMap.delete(fiberInstance.id);
2072
2073 untrackFiber(fiberInstance, fiberInstance.data);
2074 }
2075
2076 function recordDisconnect(fiberInstance: FiberInstance): void {
2077 if (isInDisconnectedSubtree) {
2078 // Already disconnected.
2079 return;
2080 }
2081
2082 if (trackedPathMatchInstance === fiberInstance) {
2083 // We're in the process of trying to restore previous selection.
2084 // If this fiber matched but is being hidden, there's no use trying.
2085 // Reset the state so we don't keep holding onto it.
2086 setTrackedPath(null);
2087 }
2088
2089 const id = fiberInstance.id;
2090 pendingRealUnmountedIDs.push(id);
2091 }
2092
2093 function recordSuspenseResize(suspenseNode: SuspenseNode): void {
2094 // $FlowFixMe[constant-condition]
2095 if (__DEBUG__) {
2096 console.log('recordSuspenseResize()', suspenseNode);
2097 }
2098 const fiberInstance = suspenseNode.instance;
2099 if (fiberInstance.kind !== FIBER_INSTANCE) {
2100 // TODO: Resizes of filtered Suspense nodes are currently dropped.
2101 return;
2102 }
2103
2104 pushOperation(SUSPENSE_TREE_OPERATION_RESIZE);
2105 pushOperation(fiberInstance.id);
2106 const rects = suspenseNode.rects;
2107 if (rects === null) {
2108 pushOperation(-1);
2109 } else {
2110 pushOperation(rects.length);
2111 for (let i = 0; i < rects.length; ++i) {
2112 const rect = rects[i];
2113 pushOperation(Math.round(rect.x * 1000));
2114 pushOperation(Math.round(rect.y * 1000));
2115 pushOperation(Math.round(rect.width * 1000));
2116 pushOperation(Math.round(rect.height * 1000));
2117 }
2118 }
2119 }
2120
2121 function recordSuspenseSuspenders(suspenseNode: SuspenseNode): void {
2122 // $FlowFixMe[constant-condition]
2123 if (__DEBUG__) {
2124 console.log('recordSuspenseSuspenders()', suspenseNode);
2125 }
2126 const fiberInstance = suspenseNode.instance;
2127 if (fiberInstance.kind !== FIBER_INSTANCE) {
2128 // TODO: Suspender updates of filtered Suspense nodes are currently dropped.
2129 return;
2130 }
2131
2132 // TODO: Just enqueue the operations here instead of stashing by id.
2133
2134 // Ensure each environment gets recorded in the string table since it is emitted
2135 // before we loop it over again later during flush.
2136 suspenseNode.environments.forEach((count, env) => {
2137 getStringID(env);
2138 });
2139 pendingSuspenderChanges.add(fiberInstance.id);
2140 }
2141
2142 function recordSuspenseUnmount(suspenseInstance: SuspenseNode): void {
2143 // $FlowFixMe[constant-condition]
2144 if (__DEBUG__) {
2145 console.log(
2146 'recordSuspenseUnmount()',
2147 suspenseInstance,
2148 reconcilingParentSuspenseNode,
2149 );
2150 }
2151
2152 const devtoolsInstance = suspenseInstance.instance;
2153 if (devtoolsInstance.kind !== FIBER_INSTANCE) {
2154 throw new Error("Can't unmount a filtered SuspenseNode. This is a bug.");
2155 }
2156 const fiberInstance = devtoolsInstance;
2157 const id = fiberInstance.id;
2158
2159 // To maintain child-first ordering,
2160 // we'll push it into one of these queues,
2161 // and later arrange them in the correct order.
2162 pendingRealUnmountedSuspenseIDs.push(id);
2163
2164 pendingSuspenderChanges.delete(id);
2165 idToSuspenseNodeMap.delete(id);
2166 }
2167
2168 // Running state of the remaining children from the previous version of this parent that
2169 // we haven't yet added back. This should be reset anytime we change parent.
2170 // Any remaining ones at the end will be deleted.
2171 let remainingReconcilingChildren: null | DevToolsInstance = null;
2172 // The previously placed child.
2173 let previouslyReconciledSibling: null | DevToolsInstance = null;
2174 // To save on stack allocation and ensure that they are updated as a pair, we also store
2175 // the current parent here as well.
2176 let reconcilingParent: null | DevToolsInstance = null;
2177
2178 let remainingReconcilingChildrenSuspenseNodes: null | SuspenseNode = null;
2179 // The previously placed child.
2180 let previouslyReconciledSiblingSuspenseNode: null | SuspenseNode = null;
2181 // To save on stack allocation and ensure that they are updated as a pair, we also store
2182 // the current parent here as well.
2183 let reconcilingParentSuspenseNode: null | SuspenseNode = null;
2184
2185 function insertSuspendedBy(asyncInfo: ReactAsyncInfo): void {
2186 if (reconcilingParent === null || reconcilingParentSuspenseNode === null) {
2187 throw new Error(
2188 'It should not be possible to have suspended data outside the root. ' +
2189 'Even suspending at the first position is still a child of the root.',
2190 );
2191 }
2192 const parentSuspenseNode = reconcilingParentSuspenseNode;
2193 // Use the nearest unfiltered parent so that there's always some component that has
2194 // the entry on it even if you filter, or the root if all are filtered.
2195 let parentInstance = reconcilingParent;
2196 while (
2197 parentInstance.kind === FILTERED_FIBER_INSTANCE &&
2198 parentInstance.parent !== null &&
2199 // We can't move past the parent Suspense node.
2200 // The Suspense node holding async info must be a parent of the devtools instance (or the instance itself)
2201 parentInstance !== parentSuspenseNode.instance
2202 ) {
2203 parentInstance = parentInstance.parent;
2204 }
2205 if (parentInstance.kind === FIBER_INSTANCE) {
2206 const fiber = parentInstance.data;
2207
2208 if (
2209 fiber.tag === SuspenseComponent &&
2210 parentInstance !== parentSuspenseNode.instance
2211 ) {
2212 // We're about to attach async info to a Suspense boundary we're not
2213 // actually considering the parent Suspense boundary for this async info.
2214 // We must have not found a suitable Fiber inside the fallback (e.g. due to filtering).
2215 // Use the parent of this instance instead since we treat async info
2216 // attached to a Suspense boundary as that async info triggering the
2217 // fallback of that boundary.
2218 const parent = parentInstance.parent;
2219 if (parent === null) {
2220 // This shouldn't happen. Any <Suspense> would have at least have the
2221 // host root as the parent which can't have a fallback.
2222 throw new Error(
2223 'Did not find a suitable instance for this async info. This is a bug in React.',
2224 );
2225 }
2226 parentInstance = parent;
2227 }
2228 }
2229
2230 const suspenseNodeSuspendedBy = parentSuspenseNode.suspendedBy;
2231 const ioInfo = asyncInfo.awaited;
2232 let suspendedBySet = suspenseNodeSuspendedBy.get(ioInfo);
2233 if (suspendedBySet === undefined) {
2234 suspendedBySet = new Set();
2235 suspenseNodeSuspendedBy.set(ioInfo, suspendedBySet);
2236 // We've added a dependency. We must increment the ref count of the environment.
2237 const env = ioInfo.env;
2238 if (env != null) {
2239 const environmentCounts = parentSuspenseNode.environments;
2240 const count = environmentCounts.get(env);
2241 if (count === undefined || count === 0) {
2242 environmentCounts.set(env, 1);
2243 // We've discovered a new environment for this SuspenseNode. We'll to update the node.
2244 recordSuspenseSuspenders(parentSuspenseNode);
2245 } else {
2246 environmentCounts.set(env, count + 1);
2247 }
2248 }
2249 }
2250 // The child of the Suspense boundary that was suspended on this, or null if suspended at the root.
2251 // This is used to keep track of how many dependents are still alive and also to get information
2252 // like owner instances to link down into the tree.
2253 if (!suspendedBySet.has(parentInstance)) {
2254 suspendedBySet.add(parentInstance);
2255 const virtualEndTime = getVirtualEndTime(ioInfo);
2256 if (
2257 !parentSuspenseNode.hasUniqueSuspenders &&
2258 !ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo)
2259 ) {
2260 // This didn't exist in the parent before, so let's mark this boundary as having a unique suspender.
2261 parentSuspenseNode.hasUniqueSuspenders = true;
2262 if (parentSuspenseNode.endTime < virtualEndTime) {
2263 parentSuspenseNode.endTime = virtualEndTime;
2264 }
2265 recordSuspenseSuspenders(parentSuspenseNode);
2266 } else if (parentSuspenseNode.endTime < virtualEndTime) {
2267 parentSuspenseNode.endTime = virtualEndTime;
2268 recordSuspenseSuspenders(parentSuspenseNode);
2269 }
2270 }
2271 // We have observed at least one known reason this might have been suspended.
2272 parentSuspenseNode.hasUnknownSuspenders = false;
2273 // Suspending right below the root is not attributed to any particular component in UI
2274 // other than the SuspenseNode and the HostRoot's FiberInstance.
2275 const suspendedBy = parentInstance.suspendedBy;
2276 if (suspendedBy === null) {
2277 parentInstance.suspendedBy = [asyncInfo];
2278 } else if (suspendedBy.indexOf(asyncInfo) === -1) {
2279 suspendedBy.push(asyncInfo);
2280 }
2281 }
2282
2283 function unblockSuspendedBy(
2284 parentSuspenseNode: SuspenseNode,
2285 ioInfo: ReactIOInfo,
2286 ): void {
2287 const firstChild = parentSuspenseNode.firstChild;
2288 if (firstChild === null) {
2289 return;
2290 }
2291 let node: SuspenseNode = firstChild;
2292 // $FlowFixMe[invalid-compare]
2293 while (node !== null) {
2294 if (node.suspendedBy.has(ioInfo)) {
2295 // We have found a child boundary that depended on the unblocked I/O.
2296 // It can now be marked as having unique suspenders. We can skip its children
2297 // since they'll still be blocked by this one.
2298 if (!node.hasUniqueSuspenders) {
2299 recordSuspenseSuspenders(node);
2300 }
2301 node.hasUniqueSuspenders = true;
2302 node.hasUnknownSuspenders = false;
2303 } else if (node.firstChild !== null) {
2304 node = node.firstChild;
2305 continue;
2306 }
2307 while (node.nextSibling === null) {
2308 if (node.parent === null || node.parent === parentSuspenseNode) {
2309 return;
2310 }
2311 node = node.parent;
2312 }
2313 node = node.nextSibling;
2314 }
2315 }
2316
2317 function computeEndTime(suspenseNode: SuspenseNode) {
2318 let maxEndTime = 0;
2319 suspenseNode.suspendedBy.forEach((set, ioInfo) => {
2320 const virtualEndTime = getVirtualEndTime(ioInfo);
2321 if (virtualEndTime > maxEndTime) {
2322 maxEndTime = virtualEndTime;
2323 }
2324 });
2325 return maxEndTime;
2326 }
2327
2328 function removePreviousSuspendedBy(
2329 instance: DevToolsInstance,
2330 previousSuspendedBy: null | Array<ReactAsyncInfo>,
2331 parentSuspenseNode: null | SuspenseNode,
2332 ): void {
2333 // Remove any async info if they were in the previous set but
2334 // is no longer in the new set.
2335 // If we just reconciled a SuspenseNode, we need to remove from that node instead of the parent.
2336 // This is different from inserting because inserting is done during reconiliation
2337 // whereas removal is done after we're done reconciling.
2338 const suspenseNode =
2339 instance.suspenseNode === null
2340 ? parentSuspenseNode
2341 : instance.suspenseNode;
2342 if (previousSuspendedBy !== null && suspenseNode !== null) {
2343 const nextSuspendedBy = instance.suspendedBy;
2344 let changedEnvironment = false;
2345 let mayHaveChangedEndTime = false;
2346 for (let i = 0; i < previousSuspendedBy.length; i++) {
2347 const asyncInfo = previousSuspendedBy[i];
2348 if (
2349 nextSuspendedBy === null ||
2350 (nextSuspendedBy.indexOf(asyncInfo) === -1 &&
2351 getAwaitInSuspendedByFromIO(nextSuspendedBy, asyncInfo.awaited) ===
2352 null)
2353 ) {
2354 // This IO entry is no longer blocking the current tree.
2355 // Let's remove it from the parent SuspenseNode.
2356 const ioInfo = asyncInfo.awaited;
2357 const suspendedBySet = suspenseNode.suspendedBy.get(ioInfo);
2358
2359 if (suspenseNode.endTime === getVirtualEndTime(ioInfo)) {
2360 // This may be the only remaining entry at this end time. Recompute the end time.
2361 mayHaveChangedEndTime = true;
2362 }
2363
2364 if (
2365 suspendedBySet === undefined ||
2366 !suspendedBySet.delete(instance)
2367 ) {
2368 // A boundary can await the same IO multiple times.
2369 // We still want to error if we're trying to remove IO that isn't present on
2370 // this boundary so we need to check if we've already removed it.
2371 // We're assuming previousSuspendedBy is a small array so this should be faster
2372 // than allocating and maintaining a Set.
2373 let alreadyRemovedIO = false;
2374 for (let j = 0; j < i; j++) {
2375 const removedIOInfo = previousSuspendedBy[j].awaited;
2376 if (removedIOInfo === ioInfo) {
2377 alreadyRemovedIO = true;
2378 break;
2379 }
2380 }
2381 if (!alreadyRemovedIO) {
2382 throw new Error(
2383 'We are cleaning up async info that was not on the parent Suspense boundary. ' +
2384 'This is a bug in React.',
2385 );
2386 }
2387 }
2388 if (suspendedBySet !== undefined && suspendedBySet.size === 0) {
2389 suspenseNode.suspendedBy.delete(ioInfo);
2390 // Successfully removed all dependencies. We can decrement the ref count of the environment.
2391 const env = ioInfo.env;
2392 if (env != null) {
2393 const environmentCounts = suspenseNode.environments;
2394 const count = environmentCounts.get(env);
2395 if (count === undefined || count === 0) {
2396 throw new Error(
2397 'We are removing an environment but it was not in the set. ' +
2398 'This is a bug in React.',
2399 );
2400 }
2401 if (count === 1) {
2402 environmentCounts.delete(env);
2403 // Last one. We've now change the set of environments. We'll need to update the node.
2404 changedEnvironment = true;
2405 } else {
2406 environmentCounts.set(env, count - 1);
2407 }
2408 }
2409
2410 if (
2411 suspenseNode.hasUniqueSuspenders &&
2412 !ioExistsInSuspenseAncestor(suspenseNode, ioInfo)
2413 ) {
2414 // This entry wasn't in any ancestor and is no longer in this suspense boundary.
2415 // This means that a child might now be the unique suspender for this IO.
2416 // Search the child boundaries to see if we can reveal any of them.
2417 unblockSuspendedBy(suspenseNode, ioInfo);
2418 }
2419 }
2420 }
2421 }
2422 const newEndTime = mayHaveChangedEndTime
2423 ? computeEndTime(suspenseNode)
2424 : suspenseNode.endTime;
2425 if (changedEnvironment || newEndTime !== suspenseNode.endTime) {
2426 suspenseNode.endTime = newEndTime;
2427 recordSuspenseSuspenders(suspenseNode);
2428 }
2429 }
2430 }
2431
2432 function insertChild(instance: DevToolsInstance): void {
2433 const parentInstance = reconcilingParent;
2434 if (parentInstance === null) {
2435 // This instance is at the root.
2436 return;
2437 }
2438 // Place it in the parent.
2439 instance.parent = parentInstance;
2440 if (previouslyReconciledSibling === null) {
2441 previouslyReconciledSibling = instance;
2442 parentInstance.firstChild = instance;
2443 } else {
2444 previouslyReconciledSibling.nextSibling = instance;
2445 previouslyReconciledSibling = instance;
2446 }
2447 instance.nextSibling = null;
2448 // Insert any SuspenseNode into its parent Node.
2449 const suspenseNode = instance.suspenseNode;
2450 if (suspenseNode !== null) {
2451 const parentNode = reconcilingParentSuspenseNode;
2452 if (parentNode !== null) {
2453 suspenseNode.parent = parentNode;
2454 if (previouslyReconciledSiblingSuspenseNode === null) {
2455 previouslyReconciledSiblingSuspenseNode = suspenseNode;
2456 parentNode.firstChild = suspenseNode;
2457 } else {
2458 previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode;
2459 previouslyReconciledSiblingSuspenseNode = suspenseNode;
2460 }
2461 suspenseNode.nextSibling = null;
2462 }
2463 }
2464 }
2465
2466 function moveChild(
2467 instance: DevToolsInstance,
2468 previousSibling: null | DevToolsInstance,
2469 ): void {
2470 removeChild(instance, previousSibling);
2471 insertChild(instance);
2472 }
2473
2474 function removeChild(
2475 instance: DevToolsInstance,
2476 previousSibling: null | DevToolsInstance,
2477 ): void {
2478 if (instance.parent === null) {
2479 if (remainingReconcilingChildren === instance) {
2480 throw new Error(
2481 'Remaining children should not have items with no parent',
2482 );
2483 } else if (instance.nextSibling !== null) {
2484 throw new Error('A deleted instance should not have next siblings');
2485 }
2486 // Already deleted.
2487 return;
2488 }
2489 const parentInstance = reconcilingParent;
2490 if (parentInstance === null) {
2491 throw new Error('Should not have a parent if we are at the root');
2492 }
2493 if (instance.parent !== parentInstance) {
2494 throw new Error(
2495 'Cannot remove a node from a different parent than is being reconciled.',
2496 );
2497 }
2498 // Remove an existing child from its current position, which we assume is in the
2499 // remainingReconcilingChildren set.
2500 if (previousSibling === null) {
2501 // We're first in the remaining set. Remove us.
2502 if (remainingReconcilingChildren !== instance) {
2503 throw new Error(
2504 'Expected a placed child to be moved from the remaining set.',
2505 );
2506 }
2507 remainingReconcilingChildren = instance.nextSibling;
2508 } else {
2509 previousSibling.nextSibling = instance.nextSibling;
2510 }
2511 instance.nextSibling = null;
2512 instance.parent = null;
2513
2514 // Remove any SuspenseNode from its parent.
2515 const suspenseNode = instance.suspenseNode;
2516 if (suspenseNode !== null && suspenseNode.parent !== null) {
2517 const parentNode = reconcilingParentSuspenseNode;
2518 if (parentNode === null) {
2519 throw new Error('Should not have a parent if we are at the root');
2520 }
2521 if (suspenseNode.parent !== parentNode) {
2522 throw new Error(
2523 'Cannot remove a Suspense node from a different parent than is being reconciled.',
2524 );
2525 }
2526 let previousSuspenseSibling = remainingReconcilingChildrenSuspenseNodes;
2527 if (previousSuspenseSibling === suspenseNode) {
2528 // We're first in the remaining set. Remove us.
2529 remainingReconcilingChildrenSuspenseNodes = suspenseNode.nextSibling;
2530 } else {
2531 // Search for our previous sibling and remove us.
2532 while (previousSuspenseSibling !== null) {
2533 if (previousSuspenseSibling.nextSibling === suspenseNode) {
2534 previousSuspenseSibling.nextSibling = suspenseNode.nextSibling;
2535 break;
2536 }
2537 previousSuspenseSibling = previousSuspenseSibling.nextSibling;
2538 }
2539 }
2540 suspenseNode.nextSibling = null;
2541 suspenseNode.parent = null;
2542 }
2543 }
2544
2545 function isHiddenOffscreen(fiber: Fiber): boolean {
2546 switch (fiber.tag) {
2547 case LegacyHiddenComponent:
2548 // fallthrough since all published implementations currently implement the same state as Offscreen.
2549 case OffscreenComponent:
2550 return fiber.memoizedState !== null;
2551 default:
2552 return false;
2553 }
2554 }
2555
2556 // Returns true if this is a hidden OffscreenComponent that belongs to
2557 // an Activity boundary (as opposed to Suspense). Activity's children
2558 // should remain visible in the DevTools tree even when hidden.
2559 function isActivityHiddenOffscreen(fiber: Fiber): boolean {
2560 return (
2561 isHiddenOffscreen(fiber) &&
2562 fiber.return !== null &&
2563 fiber.return.tag === ActivityComponent
2564 );
2565 }
2566
2567 /**
2568 * Offscreen of suspended Suspense
2569 */
2570 function isSuspendedOffscreen(fiber: Fiber): boolean {
2571 switch (fiber.tag) {
2572 case LegacyHiddenComponent:
2573 // fallthrough since all published implementations currently implement the same state as Offscreen.
2574 case OffscreenComponent:
2575 return (
2576 fiber.memoizedState !== null &&
2577 fiber.return !== null &&
2578 fiber.return.tag === SuspenseComponent
2579 );
2580 default:
2581 return false;
2582 }
2583 }
2584
2585 function unmountRemainingChildren() {
2586 if (
2587 reconcilingParent !== null &&
2588 (reconcilingParent.kind === FIBER_INSTANCE ||
2589 reconcilingParent.kind === FILTERED_FIBER_INSTANCE) &&
2590 isSuspendedOffscreen(reconcilingParent.data) &&
2591 !isInDisconnectedSubtree
2592 ) {
2593 // This is a hidden offscreen, we need to execute this in the context of a disconnected subtree.
2594 isInDisconnectedSubtree = true;
2595 try {
2596 let child = remainingReconcilingChildren;
2597 while (child !== null) {
2598 unmountInstanceRecursively(child);
2599 child = remainingReconcilingChildren;
2600 }
2601 } finally {
2602 isInDisconnectedSubtree = false;
2603 }
2604 } else {
2605 let child = remainingReconcilingChildren;
2606 while (child !== null) {
2607 unmountInstanceRecursively(child);
2608 child = remainingReconcilingChildren;
2609 }
2610 }
2611 }
2612
2613 function unmountSuspenseChildrenRecursively(
2614 contentInstance: DevToolsInstance,
2615 stashedSuspenseParent: null | SuspenseNode,
2616 stashedSuspensePrevious: null | SuspenseNode,
2617 stashedSuspenseRemaining: null | SuspenseNode,
2618 ): void {
2619 // First unmount only the Offscreen boundary. I.e. the main content.
2620 unmountInstanceRecursively(contentInstance);
2621
2622 // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
2623 // unmount the fallback, unmounting anything in the context of the parent SuspenseNode.
2624 // Since the fallback conceptually blocks the parent.
2625 reconcilingParentSuspenseNode = stashedSuspenseParent;
2626 previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
2627 remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
2628 unmountRemainingChildren();
2629 }
2630
2631 function isChildOf(
2632 parentInstance: DevToolsInstance,
2633 childInstance: DevToolsInstance,
2634 grandParent: DevToolsInstance,
2635 ): boolean {
2636 let instance = childInstance.parent;
2637 while (instance !== null) {
2638 if (parentInstance === instance) {
2639 return true;
2640 }
2641 if (instance === parentInstance.parent || instance === grandParent) {
2642 // This was a sibling but not inside the FiberInstance. We can bail out.
2643 break;
2644 }
2645 instance = instance.parent;
2646 }
2647 return false;
2648 }
2649
2650 function measureUnchangedSuspenseNodesRecursively(
2651 suspenseNode: SuspenseNode,
2652 ): void {
2653 if (isInDisconnectedSubtree) {
2654 // We don't update rects inside disconnected subtrees.
2655 return;
2656 }
2657 const instance = suspenseNode.instance;
2658
2659 const isSuspendedSuspenseComponent =
2660 (instance.kind === FIBER_INSTANCE ||
2661 instance.kind === FILTERED_FIBER_INSTANCE) &&
2662 instance.data.tag === SuspenseComponent &&
2663 instance.data.memoizedState !== null;
2664 if (isSuspendedSuspenseComponent) {
2665 // This boundary itself was suspended and we don't measure those since that would measure
2666 // the fallback. We want to keep a ghost of the rectangle of the content not currently shown.
2667 return;
2668 }
2669
2670 // While this boundary wasn't suspended and the bailed out root and wasn't in a disconnected subtree,
2671 // it's possible that this node was in one. So we need to check if we're offscreen.
2672 let parent = instance.parent;
2673 while (parent !== null) {
2674 if (
2675 (parent.kind === FIBER_INSTANCE ||
2676 parent.kind === FILTERED_FIBER_INSTANCE) &&
2677 isHiddenOffscreen(parent.data)
2678 ) {
2679 // We're inside a hidden offscreen Fiber. We're in a disconnected tree.
2680 return;
2681 }
2682 if (parent.suspenseNode !== null) {
2683 // Found our parent SuspenseNode. We can bail out now.
2684 break;
2685 }
2686 parent = parent.parent;
2687 }
2688
2689 const nextRects = measureInstance(suspenseNode.instance);
2690 const prevRects = suspenseNode.rects;
2691 if (areEqualRects(prevRects, nextRects)) {
2692 return; // Unchanged
2693 }
2694
2695 // We changed inside a visible tree.
2696 // Since this boundary changed, it's possible it also affected its children so lets
2697 // measure them as well.
2698 for (
2699 let child = suspenseNode.firstChild;
2700 child !== null;
2701 child = child.nextSibling
2702 ) {
2703 measureUnchangedSuspenseNodesRecursively(child);
2704 }
2705 suspenseNode.rects = nextRects;
2706 recordSuspenseResize(suspenseNode);
2707 }
2708
2709 function consumeSuspenseNodesOfExistingInstance(
2710 instance: DevToolsInstance,
2711 ): void {
2712 // We need to also consume any unchanged Suspense boundaries.
2713 let suspenseNode = remainingReconcilingChildrenSuspenseNodes;
2714 if (suspenseNode === null) {
2715 return;
2716 }
2717 const parentSuspenseNode = reconcilingParentSuspenseNode;
2718 if (parentSuspenseNode === null) {
2719 throw new Error(
2720 'The should not be any remaining suspense node children if there is no parent.',
2721 );
2722 }
2723 let foundOne = false;
2724 let previousSkippedSibling = null;
2725 while (suspenseNode !== null) {
2726 // Check if this SuspenseNode was a child of the bailed out FiberInstance.
2727 if (
2728 isChildOf(instance, suspenseNode.instance, parentSuspenseNode.instance)
2729 ) {
2730 foundOne = true;
2731 // The suspenseNode was child of the bailed out Fiber.
2732 // First, remove it from the remaining children set.
2733 const nextRemainingSibling = suspenseNode.nextSibling;
2734 if (previousSkippedSibling === null) {
2735 remainingReconcilingChildrenSuspenseNodes = nextRemainingSibling;
2736 } else {
2737 previousSkippedSibling.nextSibling = nextRemainingSibling;
2738 }
2739 suspenseNode.nextSibling = null;
2740 // Then, re-insert it into the newly reconciled set.
2741 if (previouslyReconciledSiblingSuspenseNode === null) {
2742 parentSuspenseNode.firstChild = suspenseNode;
2743 } else {
2744 previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode;
2745 }
2746 previouslyReconciledSiblingSuspenseNode = suspenseNode;
2747 // While React didn't rerender this node, it's possible that it was affected by
2748 // layout due to mutation of a parent or sibling. Check if it changed size.
2749 measureUnchangedSuspenseNodesRecursively(suspenseNode);
2750 // Continue
2751 suspenseNode = nextRemainingSibling;
2752 } else if (foundOne) {
2753 // If we found one and then hit a miss, we assume that we're passed the sequence because
2754 // they should've all been consecutive.
2755 break;
2756 } else {
2757 previousSkippedSibling = suspenseNode;
2758 suspenseNode = suspenseNode.nextSibling;
2759 }
2760 }
2761 }
2762
2763 function mountVirtualInstanceRecursively(
2764 virtualInstance: VirtualInstance,
2765 firstChild: Fiber,
2766 lastChild: null | Fiber, // non-inclusive
2767 traceNearestHostComponentUpdate: boolean,
2768 virtualLevel: number, // the nth level of virtual instances
2769 ): void {
2770 // If we have the tree selection from previous reload, try to match this Instance.
2771 // Also remember whether to do the same for siblings.
2772 const mightSiblingsBeOnTrackedPath =
2773 updateVirtualTrackedPathStateBeforeMount(
2774 virtualInstance,
2775 reconcilingParent,
2776 );
2777
2778 const stashedParent = reconcilingParent;
2779 const stashedPrevious = previouslyReconciledSibling;
2780 const stashedRemaining = remainingReconcilingChildren;
2781 // Push a new DevTools instance parent while reconciling this subtree.
2782 reconcilingParent = virtualInstance;
2783 previouslyReconciledSibling = null;
2784 remainingReconcilingChildren = null;
2785 try {
2786 mountVirtualChildrenRecursively(
2787 firstChild,
2788 lastChild,
2789 traceNearestHostComponentUpdate,
2790 virtualLevel + 1,
2791 );
2792 // Must be called after all children have been appended.
2793 recordVirtualProfilingDurations(virtualInstance);
2794 } finally {
2795 reconcilingParent = stashedParent;
2796 previouslyReconciledSibling = stashedPrevious;
2797 remainingReconcilingChildren = stashedRemaining;
2798 updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
2799 }
2800 }
2801
2802 function recordVirtualUnmount(instance: VirtualInstance) {
2803 recordVirtualDisconnect(instance);
2804 idToDevToolsInstanceMap.delete(instance.id);
2805 }
2806
2807 function recordVirtualDisconnect(instance: VirtualInstance) {
2808 if (isInDisconnectedSubtree) {
2809 return;
2810 }
2811 if (trackedPathMatchInstance === instance) {
2812 // We're in the process of trying to restore previous selection.
2813 // If this fiber matched but is being unmounted, there's no use trying.
2814 // Reset the state so we don't keep holding onto it.
2815 setTrackedPath(null);
2816 }
2817
2818 const id = instance.id;
2819 pendingRealUnmountedIDs.push(id);
2820 }
2821
2822 function trackDebugInfoFromLazyType(fiber: Fiber): void {
2823 // The debugInfo from a Lazy isn't propagated onto _debugInfo of the parent Fiber the way
2824 // it is when used in child position. So we need to pick it up explicitly.
2825 const type = fiber.elementType;
2826 const typeSymbol = getTypeSymbol(type); // The elementType might be have been a LazyComponent.
2827 if (typeSymbol === LAZY_SYMBOL_STRING) {
2828 const debugInfo: ?ReactDebugInfo = type._debugInfo;
2829 if (debugInfo) {
2830 for (let i = 0; i < debugInfo.length; i++) {
2831 const debugEntry = debugInfo[i];
2832 if (debugEntry.awaited) {
2833 const asyncInfo: ReactAsyncInfo = debugEntry as any;
2834 insertSuspendedBy(asyncInfo);
2835 }
2836 }
2837 }
2838 }
2839 }
2840
2841 function trackDebugInfoFromUsedThenables(fiber: Fiber): void {
2842 // If a Fiber called use() in DEV mode then we may have collected _debugThenableState on
2843 // the dependencies. If so, then this will contain the thenables passed to use().
2844 // These won't have their debug info picked up by fiber._debugInfo since that just
2845 // contains things suspending the children. We have to collect use() separately.
2846 const dependencies = fiber.dependencies;
2847 if (dependencies == null) {
2848 return;
2849 }
2850 const thenableState = dependencies._debugThenableState;
2851 if (thenableState == null) {
2852 return;
2853 }
2854 // In DEV the thenableState is an inner object.
2855 const usedThenables: any = thenableState.thenables || thenableState;
2856 if (!Array.isArray(usedThenables)) {
2857 return;
2858 }
2859 for (let i = 0; i < usedThenables.length; i++) {
2860 const thenable: Thenable<mixed> = usedThenables[i];
2861 const debugInfo = thenable._debugInfo;
2862 if (debugInfo) {
2863 for (let j = 0; j < debugInfo.length; j++) {
2864 const debugEntry = debugInfo[j];
2865 if (debugEntry.awaited) {
2866 const asyncInfo: ReactAsyncInfo = debugEntry as any;
2867 insertSuspendedBy(asyncInfo);
2868 }
2869 }
2870 }
2871 }
2872 }
2873
2874 const hostAsyncInfoCache: WeakMap<{...}, ReactAsyncInfo> = new WeakMap();
2875
2876 function trackDebugInfoFromHostResource(
2877 devtoolsInstance: DevToolsInstance,
2878 fiber: Fiber,
2879 ): void {
2880 const resource: ?{
2881 type: 'stylesheet' | 'style' | 'script' | 'void',
2882 instance?: null | HostInstance,
2883 ...
2884 } = fiber.memoizedState;
2885 if (resource == null) {
2886 return;
2887 }
2888
2889 // Use a cached entry based on the resource. This ensures that if we use the same
2890 // resource in multiple places, it gets deduped and inner boundaries don't consider it
2891 // as contributing to those boundaries.
2892 const existingEntry = hostAsyncInfoCache.get(resource);
2893 if (existingEntry !== undefined) {
2894 insertSuspendedBy(existingEntry);
2895 return;
2896 }
2897
2898 const props: {
2899 href?: string,
2900 media?: string,
2901 ...
2902 } = fiber.memoizedProps;
2903
2904 // Stylesheet resources may suspend. We need to track that.
2905 const mayResourceSuspendCommit =
2906 resource.type === 'stylesheet' &&
2907 // If it doesn't match the currently debugged media, then it doesn't count.
2908 (typeof props.media !== 'string' ||
2909 typeof matchMedia !== 'function' ||
2910 matchMedia(props.media));
2911 if (!mayResourceSuspendCommit) {
2912 return;
2913 }
2914
2915 const instance = resource.instance;
2916 if (instance == null) {
2917 return;
2918 }
2919
2920 // Unlike props.href, this href will be fully qualified which we need for comparison below.
2921 const href = instance.href;
2922 if (typeof href !== 'string') {
2923 return;
2924 }
2925 let start = -1;
2926 let end = -1;
2927 let byteSize = 0;
2928 // $FlowFixMe[method-unbinding]
2929 if (typeof performance.getEntriesByType === 'function') {
2930 // We may be able to collect the start and end time of this resource from Performance Observer.
2931 const resourceEntries = performance.getEntriesByType('resource');
2932 for (let i = 0; i < resourceEntries.length; i++) {
2933 const resourceEntry = resourceEntries[i];
2934 if (resourceEntry.name === href) {
2935 start = resourceEntry.startTime;
2936 end = start + resourceEntry.duration;
2937 // $FlowFixMe[prop-missing]
2938 byteSize = (resourceEntry.transferSize as any) || 0;
2939 }
2940 }
2941 }
2942 const value = instance.sheet;
2943 const promise = Promise.resolve(value);
2944 (promise as any).status = 'fulfilled';
2945 (promise as any).value = value;
2946 const ioInfo: ReactIOInfo = {
2947 name: 'stylesheet',
2948 start,
2949 end,
2950 value: promise,
2951 // $FlowFixMe[incompatible-type]: This field doesn't usually take a Fiber but we're only using inside this file.
2952 owner: fiber, // Allow linking to the <link> if it's not filtered.
2953 };
2954 if (byteSize > 0) {
2955 // $FlowFixMe[cannot-write]
2956 ioInfo.byteSize = byteSize;
2957 }
2958 const asyncInfo: ReactAsyncInfo = {
2959 awaited: ioInfo,
2960 // $FlowFixMe[incompatible-type]: This field doesn't usually take a Fiber but we're only using inside this file.
2961 owner: fiber._debugOwner == null ? null : fiber._debugOwner,
2962 debugStack: fiber._debugStack == null ? null : fiber._debugStack,
2963 debugTask: fiber._debugTask == null ? null : fiber._debugTask,
2964 };
2965 hostAsyncInfoCache.set(resource, asyncInfo);
2966 insertSuspendedBy(asyncInfo);
2967 }
2968
2969 function trackDebugInfoFromHostComponent(
2970 devtoolsInstance: DevToolsInstance,
2971 fiber: Fiber,
2972 ): void {
2973 if (fiber.tag !== HostComponent) {
2974 return;
2975 }
2976 if ((fiber.mode & SuspenseyImagesMode) === 0) {
2977 // In any released version, Suspensey Images are only enabled inside a ViewTransition
2978 // subtree, which is enabled by the SuspenseyImagesMode.
2979 // TODO: If we ever enable the enableSuspenseyImages flag then it would be enabled for
2980 // all images and we'd need some other check for if the version of React has that enabled.
2981 return;
2982 }
2983
2984 const type = fiber.type;
2985 const props: {
2986 src?: string,
2987 onLoad?: (event: any) => void,
2988 loading?: 'eager' | 'lazy',
2989 ...
2990 } = fiber.memoizedProps;
2991
2992 const maySuspendCommit =
2993 type === 'img' &&
2994 props.src != null &&
2995 props.src !== '' &&
2996 props.onLoad == null &&
2997 props.loading !== 'lazy';
2998
2999 // Note: We don't track "maySuspendCommitOnUpdate" separately because it doesn't matter if
3000 // it didn't suspend this particular update if it would've suspended if it mounted in this
3001 // state, since we're tracking the dependencies inside the current state.
3002
3003 if (!maySuspendCommit) {
3004 return;
3005 }
3006
3007 const instance = fiber.stateNode;
3008 if (instance == null) {
3009 // Should never happen.
3010 return;
3011 }
3012
3013 // Unlike props.src, currentSrc will be fully qualified which we need for comparison below.
3014 // Unlike instance.src it will be resolved into the media queries currently matching which is
3015 // the state we're inspecting.
3016 const src = instance.currentSrc;
3017 if (typeof src !== 'string' || src === '') {
3018 return;
3019 }
3020 let start = -1;
3021 let end = -1;
3022 let byteSize = 0;
3023 let fileSize = 0;
3024 // $FlowFixMe[method-unbinding]
3025 if (typeof performance.getEntriesByType === 'function') {
3026 // We may be able to collect the start and end time of this resource from Performance Observer.
3027 const resourceEntries = performance.getEntriesByType('resource');
3028 for (let i = 0; i < resourceEntries.length; i++) {
3029 const resourceEntry = resourceEntries[i];
3030 if (resourceEntry.name === src) {
3031 start = resourceEntry.startTime;
3032 end = start + resourceEntry.duration;
3033 // $FlowFixMe[prop-missing]
3034 fileSize = (resourceEntry.decodedBodySize as any) || 0;
3035 // $FlowFixMe[prop-missing]
3036 byteSize = (resourceEntry.transferSize as any) || 0;
3037 }
3038 }
3039 }
3040 // A representation of the image data itself.
3041 // TODO: We could render a little preview in the front end from the resource API.
3042 const value: {
3043 currentSrc: string,
3044 naturalWidth?: number,
3045 naturalHeight?: number,
3046 fileSize?: number,
3047 } = {
3048 currentSrc: src,
3049 };
3050 if (instance.naturalWidth > 0 && instance.naturalHeight > 0) {
3051 // The intrinsic size of the file value itself, if it's loaded
3052 value.naturalWidth = instance.naturalWidth;
3053 value.naturalHeight = instance.naturalHeight;
3054 }
3055 if (fileSize > 0) {
3056 // Cross-origin images won't have a file size that we can access.
3057 value.fileSize = fileSize;
3058 }
3059 const promise = Promise.resolve(value);
3060 (promise as any).status = 'fulfilled';
3061 (promise as any).value = value;
3062 const ioInfo: ReactIOInfo = {
3063 name: 'img',
3064 start,
3065 end,
3066 value: promise,
3067 // $FlowFixMe[incompatible-type]: This field doesn't usually take a Fiber but we're only using inside this file.
3068 owner: fiber, // Allow linking to the <link> if it's not filtered.
3069 };
3070 if (byteSize > 0) {
3071 // $FlowFixMe[cannot-write]
3072 ioInfo.byteSize = byteSize;
3073 }
3074 const asyncInfo: ReactAsyncInfo = {
3075 awaited: ioInfo,
3076 // $FlowFixMe[incompatible-type]: This field doesn't usually take a Fiber but we're only using inside this file.
3077 owner: fiber._debugOwner == null ? null : fiber._debugOwner,
3078 debugStack: fiber._debugStack == null ? null : fiber._debugStack,
3079 debugTask: fiber._debugTask == null ? null : fiber._debugTask,
3080 };
3081 insertSuspendedBy(asyncInfo);
3082 }
3083
3084 function trackThrownPromisesFromRetryCache(
3085 suspenseNode: SuspenseNode,
3086 retryCache: ?WeakSet<Wakeable>,
3087 ): void {
3088 if (retryCache != null) {
3089 // If a Suspense boundary ever committed in fallback state with a retryCache, that
3090 // suggests that something unique to that boundary was suspensey since otherwise
3091 // it wouldn't have thrown and so never created the retryCache.
3092 // Unfortunately if we don't have any DEV time debug info or debug thenables then
3093 // we have no meta data to show. However, we still mark this Suspense boundary as
3094 // participating in the loading sequence since apparently it can suspend.
3095 if (!suspenseNode.hasUniqueSuspenders) {
3096 recordSuspenseSuspenders(suspenseNode);
3097 }
3098 suspenseNode.hasUniqueSuspenders = true;
3099 // We have not seen any reason yet for why this suspense node might have been
3100 // suspended but it clearly has been at some point. If we later discover a reason
3101 // we'll clear this flag again.
3102 suspenseNode.hasUnknownSuspenders = true;
3103 }
3104 }
3105
3106 function mountVirtualChildrenRecursively(
3107 firstChild: Fiber,
3108 lastChild: null | Fiber, // non-inclusive
3109 traceNearestHostComponentUpdate: boolean,
3110 virtualLevel: number, // the nth level of virtual instances
3111 ): void {
3112 // Iterate over siblings rather than recursing.
3113 // This reduces the chance of stack overflow for wide trees (e.g. lists with many items).
3114 let fiber: Fiber | null = firstChild;
3115 let previousVirtualInstance: null | VirtualInstance = null;
3116 let previousVirtualInstanceFirstFiber: Fiber = firstChild;
3117 while (fiber !== null && fiber !== lastChild) {
3118 let level = 0;
3119 if (fiber._debugInfo) {
3120 for (let i = 0; i < fiber._debugInfo.length; i++) {
3121 const debugEntry = fiber._debugInfo[i];
3122 if (debugEntry.awaited) {
3123 // Async Info
3124 const asyncInfo: ReactAsyncInfo = debugEntry as any;
3125 if (level === virtualLevel) {
3126 // Track any async info between the previous virtual instance up until to this
3127 // instance and add it to the parent. This can add the same set multiple times
3128 // so we assume insertSuspendedBy dedupes.
3129 insertSuspendedBy(asyncInfo);
3130 }
3131 continue;
3132 }
3133 if (typeof debugEntry.name !== 'string') {
3134 // Not a Component. Some other Debug Info.
3135 continue;
3136 }
3137 // Scan up until the next Component to see if this component changed environment.
3138 const componentInfo: ReactComponentInfo = debugEntry as any;
3139 const secondaryEnv = getSecondaryEnvironmentName(fiber._debugInfo, i);
3140 if (componentInfo.env != null) {
3141 knownEnvironmentNames.add(componentInfo.env);
3142 }
3143 if (secondaryEnv !== null) {
3144 knownEnvironmentNames.add(secondaryEnv);
3145 }
3146 if (shouldFilterVirtual(componentInfo, secondaryEnv)) {
3147 // Skip.
3148 continue;
3149 }
3150 if (level === virtualLevel) {
3151 if (
3152 previousVirtualInstance === null ||
3153 // Consecutive children with the same debug entry as a parent gets
3154 // treated as if they share the same virtual instance.
3155 previousVirtualInstance.data !== debugEntry
3156 ) {
3157 if (previousVirtualInstance !== null) {
3158 // Mount any previous children that should go into the previous parent.
3159 mountVirtualInstanceRecursively(
3160 previousVirtualInstance,
3161 previousVirtualInstanceFirstFiber,
3162 fiber,
3163 traceNearestHostComponentUpdate,
3164 virtualLevel,
3165 );
3166 }
3167 previousVirtualInstance = createVirtualInstance(componentInfo);
3168 recordVirtualMount(
3169 previousVirtualInstance,
3170 reconcilingParent,
3171 secondaryEnv,
3172 );
3173 insertChild(previousVirtualInstance);
3174 previousVirtualInstanceFirstFiber = fiber;
3175 }
3176 level++;
3177 break;
3178 } else {
3179 level++;
3180 }
3181 }
3182 }
3183 if (level === virtualLevel) {
3184 if (previousVirtualInstance !== null) {
3185 // If we were working on a virtual instance and this is not a virtual
3186 // instance, then we end the sequence and mount any previous children
3187 // that should go into the previous virtual instance.
3188 mountVirtualInstanceRecursively(
3189 previousVirtualInstance,
3190 previousVirtualInstanceFirstFiber,
3191 fiber,
3192 traceNearestHostComponentUpdate,
3193 virtualLevel,
3194 );
3195 previousVirtualInstance = null;
3196 }
3197 // We've reached the end of the virtual levels, but not beyond,
3198 // and now continue with the regular fiber.
3199 mountFiberRecursively(fiber, traceNearestHostComponentUpdate);
3200 }
3201 fiber = fiber.sibling;
3202 }
3203 if (previousVirtualInstance !== null) {
3204 // Mount any previous children that should go into the previous parent.
3205 mountVirtualInstanceRecursively(
3206 previousVirtualInstance,
3207 previousVirtualInstanceFirstFiber,
3208 null,
3209 traceNearestHostComponentUpdate,
3210 virtualLevel,
3211 );
3212 }
3213 }
3214
3215 function mountChildrenRecursively(
3216 firstChild: Fiber,
3217 traceNearestHostComponentUpdate: boolean,
3218 ): void {
3219 mountVirtualChildrenRecursively(
3220 firstChild,
3221 null,
3222 traceNearestHostComponentUpdate,
3223 0, // first level
3224 );
3225 }
3226
3227 function mountSuspenseChildrenRecursively(
3228 contentFiber: Fiber,
3229 traceNearestHostComponentUpdate: boolean,
3230 stashedSuspenseParent: SuspenseNode | null,
3231 stashedSuspensePrevious: SuspenseNode | null,
3232 stashedSuspenseRemaining: SuspenseNode | null,
3233 ) {
3234 const fallbackFiber = contentFiber.sibling;
3235
3236 // First update only the Offscreen boundary. I.e. the main content.
3237 mountVirtualChildrenRecursively(
3238 contentFiber,
3239 fallbackFiber,
3240 traceNearestHostComponentUpdate,
3241 0, // first level
3242 );
3243
3244 // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
3245 // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
3246 // Since the fallback conceptually blocks the parent.
3247 reconcilingParentSuspenseNode = stashedSuspenseParent;
3248 previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3249 remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3250 if (fallbackFiber !== null) {
3251 mountVirtualChildrenRecursively(
3252 fallbackFiber,
3253 null,
3254 traceNearestHostComponentUpdate,
3255 0, // first level
3256 );
3257 }
3258 }
3259
3260 function mountFiberRecursively(
3261 fiber: Fiber,
3262 traceNearestHostComponentUpdate: boolean,
3263 ): void {
3264 const isFocusedActivityEntry =
3265 focusedActivity !== null &&
3266 (fiber === focusedActivity || fiber.alternate === focusedActivity);
3267 if (isFocusedActivityEntry) {
3268 isInFocusedActivity = true;
3269 }
3270
3271 const shouldIncludeInTree = !shouldFilterFiber(fiber);
3272 let newInstance = null;
3273 let newSuspenseNode = null;
3274 if (shouldIncludeInTree) {
3275 newInstance = recordMount(fiber, reconcilingParent);
3276 if (isFocusedActivityEntry) {
3277 focusedActivityID = newInstance.id;
3278 pushOperation(TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE);
3279 pushOperation(newInstance.id);
3280 }
3281 if (fiber.tag === SuspenseComponent || fiber.tag === HostRoot) {
3282 newSuspenseNode = createSuspenseNode(newInstance);
3283 // Measure this Suspense node. In general we shouldn't do this until we have
3284 // inserted the new children but since we know this is a FiberInstance we'll
3285 // just use the Fiber anyway.
3286 // Fallbacks get attributed to the parent so we only measure if we're
3287 // showing primary content.
3288 if (fiber.tag === SuspenseComponent) {
3289 if (OffscreenComponent === -1) {
3290 const isTimedOut = fiber.memoizedState !== null;
3291 if (!isTimedOut) {
3292 newSuspenseNode.rects = measureInstance(newInstance);
3293 }
3294 } else {
3295 const hydrated = isFiberHydrated(fiber);
3296 if (hydrated) {
3297 const contentFiber = fiber.child;
3298 if (contentFiber === null) {
3299 throw new Error(
3300 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3301 );
3302 }
3303 } else {
3304 // This Suspense Fiber is still dehydrated. It won't have any children
3305 // until hydration.
3306 }
3307 const isTimedOut = fiber.memoizedState !== null;
3308 if (!isTimedOut) {
3309 newSuspenseNode.rects = measureInstance(newInstance);
3310 }
3311 }
3312 } else {
3313 newSuspenseNode.rects = measureInstance(newInstance);
3314 }
3315 recordSuspenseMount(newSuspenseNode, reconcilingParentSuspenseNode);
3316 }
3317 insertChild(newInstance);
3318 // $FlowFixMe[constant-condition]
3319 if (__DEBUG__) {
3320 debug('mountFiberRecursively()', newInstance, reconcilingParent);
3321 }
3322 } else if (
3323 (reconcilingParent !== null &&
3324 reconcilingParent.kind === VIRTUAL_INSTANCE) ||
3325 fiber.tag === SuspenseComponent ||
3326 // Use to keep resuspended instances alive inside a SuspenseComponent.
3327 fiber.tag === OffscreenComponent ||
3328 fiber.tag === LegacyHiddenComponent
3329 ) {
3330 // If the parent is a Virtual Instance and we filtered this Fiber we include a
3331 // hidden node. We also include this if it's a Suspense boundary so we can track those
3332 // in the Suspense tree.
3333 if (
3334 reconcilingParent !== null &&
3335 reconcilingParent.kind === VIRTUAL_INSTANCE &&
3336 reconcilingParent.data === fiber._debugOwner &&
3337 fiber._debugStack != null &&
3338 reconcilingParent.source === null
3339 ) {
3340 // The new Fiber is directly owned by the parent. Therefore somewhere on the
3341 // debugStack will be a stack frame inside parent that we can use as its soruce.
3342 reconcilingParent.source = fiber._debugStack;
3343 }
3344
3345 newInstance = createFilteredFiberInstance(fiber);
3346 if (fiber.tag === SuspenseComponent) {
3347 newSuspenseNode = createSuspenseNode(newInstance);
3348 // Measure this Suspense node. In general we shouldn't do this until we have
3349 // inserted the new children but since we know this is a FiberInstance we'll
3350 // just use the Fiber anyway.
3351 // Fallbacks get attributed to the parent so we only measure if we're
3352 // showing primary content.
3353 if (OffscreenComponent === -1) {
3354 const isTimedOut = fiber.memoizedState !== null;
3355 if (!isTimedOut) {
3356 newSuspenseNode.rects = measureInstance(newInstance);
3357 }
3358 } else {
3359 const hydrated = isFiberHydrated(fiber);
3360 if (hydrated) {
3361 const contentFiber = fiber.child;
3362 if (contentFiber === null) {
3363 throw new Error(
3364 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3365 );
3366 }
3367 } else {
3368 // This Suspense Fiber is still dehydrated. It won't have any children
3369 // until hydration.
3370 }
3371 const suspenseState = fiber.memoizedState;
3372 const isTimedOut = suspenseState !== null;
3373 if (!isTimedOut) {
3374 newSuspenseNode.rects = measureInstance(newInstance);
3375 }
3376 }
3377 }
3378 insertChild(newInstance);
3379 // $FlowFixMe[constant-condition]
3380 if (__DEBUG__) {
3381 debug('mountFiberRecursively()', newInstance, reconcilingParent);
3382 }
3383 }
3384
3385 // If we have the tree selection from previous reload, try to match this Fiber.
3386 // Also remember whether to do the same for siblings.
3387 const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount(
3388 fiber,
3389 newInstance,
3390 );
3391
3392 const stashedParent = reconcilingParent;
3393 const stashedPrevious = previouslyReconciledSibling;
3394 const stashedRemaining = remainingReconcilingChildren;
3395 const stashedSuspenseParent = reconcilingParentSuspenseNode;
3396 const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
3397 const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
3398 const stashedIsInActivitySlice = isInFocusedActivity;
3399 if (newInstance !== null) {
3400 // Push a new DevTools instance parent while reconciling this subtree.
3401 reconcilingParent = newInstance;
3402 previouslyReconciledSibling = null;
3403 remainingReconcilingChildren = null;
3404 }
3405 let shouldPopSuspenseNode = false;
3406 if (newSuspenseNode !== null) {
3407 reconcilingParentSuspenseNode = newSuspenseNode;
3408 previouslyReconciledSiblingSuspenseNode = null;
3409 remainingReconcilingChildrenSuspenseNodes = null;
3410 shouldPopSuspenseNode = true;
3411 }
3412 if (
3413 !isFocusedActivityEntry &&
3414 focusedActivity !== null &&
3415 fiber.tag === ActivityComponent
3416 ) {
3417 // We're not filtering how Activity within the focused activity.
3418 // We cut of the bottom in the Frontend if we want to just show the
3419 // Activity slice instead of all Activity descendants.
3420 // The filtering in the backend only happens because filtering out
3421 // everything above the focused Activity is hard to implement in the frontend.
3422 }
3423 try {
3424 if (traceUpdatesEnabled) {
3425 if (traceNearestHostComponentUpdate) {
3426 const elementType = getElementTypeForFiber(fiber);
3427 // If an ancestor updated, we should mark the nearest host nodes for highlighting.
3428 if (elementType === ElementTypeHostComponent) {
3429 traceUpdatesForNodes.add(fiber.stateNode);
3430 traceNearestHostComponentUpdate = false;
3431 }
3432 }
3433
3434 // We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch,
3435 // because we don't want to highlight every host node inside of a newly mounted subtree.
3436 }
3437
3438 trackDebugInfoFromLazyType(fiber);
3439 trackDebugInfoFromUsedThenables(fiber);
3440
3441 if (fiber.tag === HostHoistable) {
3442 const nearestInstance = reconcilingParent;
3443 if (nearestInstance === null) {
3444 throw new Error('Did not expect a host hoistable to be the root');
3445 }
3446 aquireHostResource(nearestInstance, fiber.memoizedState);
3447 trackDebugInfoFromHostResource(nearestInstance, fiber);
3448 } else if (
3449 fiber.tag === HostComponent ||
3450 fiber.tag === HostText ||
3451 fiber.tag === HostSingleton
3452 ) {
3453 const nearestInstance = reconcilingParent;
3454 if (nearestInstance === null) {
3455 throw new Error('Did not expect a host hoistable to be the root');
3456 }
3457 aquireHostInstance(nearestInstance, fiber.stateNode);
3458 trackDebugInfoFromHostComponent(nearestInstance, fiber);
3459 }
3460
3461 if (isSuspendedOffscreen(fiber)) {
3462 // If an Offscreen component is hidden, mount its children as disconnected.
3463 const stashedDisconnected = isInDisconnectedSubtree;
3464 isInDisconnectedSubtree = true;
3465 try {
3466 if (fiber.child !== null) {
3467 mountChildrenRecursively(fiber.child, false);
3468 }
3469 } finally {
3470 isInDisconnectedSubtree = stashedDisconnected;
3471 }
3472 } else if (isHiddenOffscreen(fiber)) {
3473 if (isActivityHiddenOffscreen(fiber)) {
3474 // Activity's hidden children should still be visible in DevTools.
3475 if (fiber.child !== null) {
3476 mountChildrenRecursively(
3477 fiber.child,
3478 traceNearestHostComponentUpdate,
3479 );
3480 }
3481 }
3482 // Otherwise, hidden Offscreen (e.g. non-Activity) is noisy.
3483 // Including it may show overlapping Suspense rects
3484 } else if (fiber.tag === SuspenseComponent && OffscreenComponent === -1) {
3485 // Legacy Suspense without the Offscreen wrapper. For the modern Suspense we just handle the
3486 // Offscreen wrapper itself specially.
3487 if (newSuspenseNode !== null) {
3488 trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
3489 }
3490 const isTimedOut = fiber.memoizedState !== null;
3491 if (isTimedOut) {
3492 // Special case: if Suspense mounts in a timed-out state,
3493 // get the fallback child from the inner fragment and mount
3494 // it as if it was our own child. Updates handle this too.
3495 const primaryChildFragment = fiber.child;
3496 const fallbackChildFragment = primaryChildFragment
3497 ? primaryChildFragment.sibling
3498 : null;
3499 if (fallbackChildFragment) {
3500 const fallbackChild = fallbackChildFragment.child;
3501 if (fallbackChild !== null) {
3502 updateTrackedPathStateBeforeMount(fallbackChildFragment, null);
3503 mountChildrenRecursively(
3504 fallbackChild,
3505 traceNearestHostComponentUpdate,
3506 );
3507 }
3508 }
3509 // TODO: Track SuspenseNode in resuspended trees.
3510 } else {
3511 const primaryChild: Fiber | null = fiber.child;
3512 if (primaryChild !== null) {
3513 mountChildrenRecursively(
3514 primaryChild,
3515 traceNearestHostComponentUpdate,
3516 );
3517 }
3518 }
3519 } else if (
3520 fiber.tag === SuspenseComponent &&
3521 OffscreenComponent !== -1 &&
3522 newInstance !== null &&
3523 newSuspenseNode !== null
3524 ) {
3525 // Modern Suspense path
3526 const contentFiber = fiber.child;
3527 const hydrated = isFiberHydrated(fiber);
3528 if (hydrated) {
3529 if (contentFiber === null) {
3530 throw new Error(
3531 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3532 );
3533 }
3534
3535 trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode);
3536
3537 mountSuspenseChildrenRecursively(
3538 contentFiber,
3539 traceNearestHostComponentUpdate,
3540 stashedSuspenseParent,
3541 stashedSuspensePrevious,
3542 stashedSuspenseRemaining,
3543 );
3544 // mountSuspenseChildrenRecursively popped already
3545 shouldPopSuspenseNode = false;
3546 } else {
3547 // This Suspense Fiber is still dehydrated. It won't have any children
3548 // until hydration.
3549 }
3550 } else {
3551 if (fiber.child !== null) {
3552 mountChildrenRecursively(
3553 fiber.child,
3554 traceNearestHostComponentUpdate,
3555 );
3556 }
3557 }
3558 } finally {
3559 isInFocusedActivity = stashedIsInActivitySlice;
3560 if (newInstance !== null) {
3561 reconcilingParent = stashedParent;
3562 previouslyReconciledSibling = stashedPrevious;
3563 remainingReconcilingChildren = stashedRemaining;
3564 }
3565 if (shouldPopSuspenseNode) {
3566 reconcilingParentSuspenseNode = stashedSuspenseParent;
3567 previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3568 remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3569 }
3570 }
3571
3572 // We're exiting this Fiber now, and entering its siblings.
3573 // If we have selection to restore, we might need to re-activate tracking.
3574 updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
3575 }
3576
3577 // We use this to simulate unmounting for Suspense trees
3578 // when we switch from primary to fallback, or deleting a subtree.
3579 function unmountInstanceRecursively(instance: DevToolsInstance) {
3580 // $FlowFixMe[constant-condition]
3581 if (__DEBUG__) {
3582 debug('unmountInstanceRecursively()', instance, reconcilingParent);
3583 }
3584
3585 let shouldPopSuspenseNode = false;
3586 const stashedParent = reconcilingParent;
3587 const stashedPrevious = previouslyReconciledSibling;
3588 const stashedRemaining = remainingReconcilingChildren;
3589 const stashedSuspenseParent = reconcilingParentSuspenseNode;
3590 const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
3591 const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
3592 const stashedIsInActivitySlice = isInFocusedActivity;
3593 const previousSuspendedBy = instance.suspendedBy;
3594 // Push a new DevTools instance parent while reconciling this subtree.
3595 reconcilingParent = instance;
3596 previouslyReconciledSibling = null;
3597 // Move all the children of this instance to the remaining set.
3598 remainingReconcilingChildren = instance.firstChild;
3599 instance.firstChild = null;
3600 instance.suspendedBy = null;
3601
3602 if (instance.suspenseNode !== null) {
3603 reconcilingParentSuspenseNode = instance.suspenseNode;
3604 previouslyReconciledSiblingSuspenseNode = null;
3605 remainingReconcilingChildrenSuspenseNodes =
3606 instance.suspenseNode.firstChild;
3607
3608 shouldPopSuspenseNode = true;
3609 }
3610
3611 if (focusedActivity !== null) {
3612 if (instance.id === focusedActivityID) {
3613 isInFocusedActivity = true;
3614 } else if (
3615 instance.kind === FIBER_INSTANCE &&
3616 // $FlowFixMe[invalid-compare]
3617 instance.data !== null &&
3618 instance.data.tag === ActivityComponent
3619 ) {
3620 // Filtering nested Activity components inside the focused activity
3621 // is done in the frontend.
3622 }
3623 }
3624
3625 try {
3626 // Unmount the remaining set.
3627 if (
3628 (instance.kind === FIBER_INSTANCE ||
3629 instance.kind === FILTERED_FIBER_INSTANCE) &&
3630 instance.data.tag === SuspenseComponent &&
3631 OffscreenComponent !== -1
3632 ) {
3633 const fiber = instance.data;
3634 const contentFiberInstance = remainingReconcilingChildren;
3635 const hydrated = isFiberHydrated(fiber);
3636 if (hydrated) {
3637 if (contentFiberInstance === null) {
3638 throw new Error(
3639 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
3640 );
3641 }
3642
3643 unmountSuspenseChildrenRecursively(
3644 contentFiberInstance,
3645 stashedSuspenseParent,
3646 stashedSuspensePrevious,
3647 stashedSuspenseRemaining,
3648 );
3649 // unmountSuspenseChildren already popped
3650 shouldPopSuspenseNode = false;
3651 } else {
3652 if (contentFiberInstance !== null) {
3653 throw new Error(
3654 'A dehydrated Suspense node should not have a content Fiber.',
3655 );
3656 }
3657 }
3658 } else {
3659 unmountRemainingChildren();
3660 }
3661 removePreviousSuspendedBy(
3662 instance,
3663 previousSuspendedBy,
3664 reconcilingParentSuspenseNode,
3665 );
3666 } finally {
3667 reconcilingParent = stashedParent;
3668 previouslyReconciledSibling = stashedPrevious;
3669 remainingReconcilingChildren = stashedRemaining;
3670 if (shouldPopSuspenseNode) {
3671 reconcilingParentSuspenseNode = stashedSuspenseParent;
3672 previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3673 remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3674 }
3675 isInFocusedActivity = stashedIsInActivitySlice;
3676 }
3677 if (instance.kind === FIBER_INSTANCE) {
3678 recordUnmount(instance);
3679 } else if (instance.kind === VIRTUAL_INSTANCE) {
3680 recordVirtualUnmount(instance);
3681 } else {
3682 untrackFiber(instance, instance.data);
3683 }
3684 removeChild(instance, null);
3685 }
3686
3687 function recordProfilingDurations(
3688 fiberInstance: FiberInstance,
3689 prevFiber: null | Fiber,
3690 ) {
3691 const id = fiberInstance.id;
3692 const fiber = fiberInstance.data;
3693 const {actualDuration, treeBaseDuration} = fiber;
3694
3695 fiberInstance.treeBaseDuration = treeBaseDuration || 0;
3696
3697 if (isProfiling) {
3698 // It's important to update treeBaseDuration even if the current Fiber did not render,
3699 // because it's possible that one of its descendants did.
3700 if (
3701 prevFiber == null ||
3702 treeBaseDuration !== prevFiber.treeBaseDuration
3703 ) {
3704 // Tree base duration updates are included in the operations typed array.
3705 // So we have to convert them from milliseconds to microseconds so we can send them as ints.
3706 const convertedTreeBaseDuration = Math.floor(
3707 (treeBaseDuration || 0) * 1000,
3708 );
3709 pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION);
3710 pushOperation(id);
3711 pushOperation(convertedTreeBaseDuration);
3712 }
3713
3714 if (
3715 prevFiber == null ||
3716 (prevFiber !== fiber &&
3717 didFiberRender(ReactTypeOfWork, prevFiber, fiber))
3718 ) {
3719 if (actualDuration != null) {
3720 // The actual duration reported by React includes time spent working on children.
3721 // This is useful information, but it's also useful to be able to exclude child durations.
3722 // The frontend can't compute this, since the immediate children may have been filtered out.
3723 // So we need to do this on the backend.
3724 // Note that this calculated self duration is not the same thing as the base duration.
3725 // The two are calculated differently (tree duration does not accumulate).
3726 let selfDuration = actualDuration;
3727 let child = fiber.child;
3728 while (child !== null) {
3729 selfDuration -= child.actualDuration || 0;
3730 child = child.sibling;
3731 }
3732
3733 // If profiling is active, store durations for elements that were rendered during the commit.
3734 // Note that we should do this for any fiber we performed work on, regardless of its actualDuration value.
3735 // In some cases actualDuration might be 0 for fibers we worked on (particularly if we're using Date.now)
3736 // In other cases (e.g. Memo) actualDuration might be greater than 0 even if we "bailed out".
3737 const metadata =
3738 currentCommitProfilingMetadata as any as CommitProfilingData;
3739 metadata.durations.push(id, actualDuration, selfDuration);
3740 metadata.maxActualDuration = Math.max(
3741 metadata.maxActualDuration,
3742 actualDuration,
3743 );
3744
3745 if (recordChangeDescriptions) {
3746 const changeDescription = getChangeDescription(prevFiber, fiber);
3747 if (changeDescription !== null) {
3748 if (metadata.changeDescriptions !== null) {
3749 metadata.changeDescriptions.set(id, changeDescription);
3750 }
3751 }
3752 }
3753 }
3754 }
3755
3756 // If this Fiber was in the set of memoizedUpdaters we need to record
3757 // it to be included in the description of the commit.
3758 const fiberRoot: FiberRoot = currentRoot.data.stateNode;
3759 const updaters = fiberRoot.memoizedUpdaters;
3760 if (
3761 updaters != null &&
3762 (updaters.has(fiber) ||
3763 // We check the alternate here because we're matching identity and
3764 // prevFiber might be same as fiber.
3765 (fiber.alternate !== null && updaters.has(fiber.alternate)))
3766 ) {
3767 const metadata =
3768 currentCommitProfilingMetadata as any as CommitProfilingData;
3769 if (metadata.updaters === null) {
3770 metadata.updaters = [];
3771 }
3772 metadata.updaters.push(instanceToSerializedElement(fiberInstance));
3773 }
3774 }
3775 }
3776
3777 function recordVirtualProfilingDurations(virtualInstance: VirtualInstance) {
3778 const id = virtualInstance.id;
3779
3780 let treeBaseDuration = 0;
3781 // Add up the base duration of the child instances. The virtual base duration
3782 // will be the same as children's duration since we don't take up any render
3783 // time in the virtual instance.
3784 for (
3785 let child = virtualInstance.firstChild;
3786 child !== null;
3787 child = child.nextSibling
3788 ) {
3789 treeBaseDuration += child.treeBaseDuration;
3790 }
3791
3792 if (isProfiling) {
3793 const previousTreeBaseDuration = virtualInstance.treeBaseDuration;
3794 if (treeBaseDuration !== previousTreeBaseDuration) {
3795 // Tree base duration updates are included in the operations typed array.
3796 // So we have to convert them from milliseconds to microseconds so we can send them as ints.
3797 const convertedTreeBaseDuration = Math.floor(
3798 (treeBaseDuration || 0) * 1000,
3799 );
3800 pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION);
3801 pushOperation(id);
3802 pushOperation(convertedTreeBaseDuration);
3803 }
3804 }
3805
3806 virtualInstance.treeBaseDuration = treeBaseDuration;
3807 }
3808
3809 function addUnfilteredChildrenIDs(
3810 parentInstance: DevToolsInstance,
3811 nextChildren: Array<number>,
3812 ): void {
3813 let child: null | DevToolsInstance = parentInstance.firstChild;
3814 while (child !== null) {
3815 if (child.kind === FILTERED_FIBER_INSTANCE) {
3816 const fiber = child.data;
3817 if (isHiddenOffscreen(fiber) && !isActivityHiddenOffscreen(fiber)) {
3818 // The children of this Offscreen are hidden so they don't get added.
3819 // Activity's hidden children are still shown in the tree.
3820 } else {
3821 addUnfilteredChildrenIDs(child, nextChildren);
3822 }
3823 } else {
3824 nextChildren.push(child.id);
3825 }
3826 child = child.nextSibling;
3827 }
3828 }
3829
3830 function recordResetChildren(
3831 parentInstance: FiberInstance | VirtualInstance,
3832 ) {
3833 // $FlowFixMe[constant-condition]
3834 if (__DEBUG__) {
3835 if (parentInstance.firstChild !== null) {
3836 debug(
3837 'recordResetChildren()',
3838 parentInstance.firstChild,
3839 parentInstance,
3840 );
3841 }
3842 }
3843 // The frontend only really cares about the displayName, key, and children.
3844 // The first two don't really change, so we are only concerned with the order of children here.
3845 // This is trickier than a simple comparison though, since certain types of fibers are filtered.
3846 const nextChildren: Array<number> = [];
3847
3848 addUnfilteredChildrenIDs(parentInstance, nextChildren);
3849
3850 const numChildren = nextChildren.length;
3851 if (numChildren < 2) {
3852 // No need to reorder.
3853 return;
3854 }
3855 pushOperation(TREE_OPERATION_REORDER_CHILDREN);
3856 pushOperation(parentInstance.id);
3857 pushOperation(numChildren);
3858 for (let i = 0; i < nextChildren.length; i++) {
3859 pushOperation(nextChildren[i]);
3860 }
3861 }
3862
3863 function addUnfilteredSuspenseChildrenIDs(
3864 parentInstance: SuspenseNode,
3865 nextChildren: Array<number>,
3866 ): void {
3867 let child: null | SuspenseNode = parentInstance.firstChild;
3868 while (child !== null) {
3869 if (child.instance.kind === FILTERED_FIBER_INSTANCE) {
3870 addUnfilteredSuspenseChildrenIDs(child, nextChildren);
3871 } else {
3872 nextChildren.push(child.instance.id);
3873 }
3874 child = child.nextSibling;
3875 }
3876 }
3877
3878 function recordResetSuspenseChildren(parentInstance: SuspenseNode) {
3879 // $FlowFixMe[constant-condition]
3880 if (__DEBUG__) {
3881 if (parentInstance.firstChild !== null) {
3882 console.log(
3883 'recordResetSuspenseChildren()',
3884 parentInstance.firstChild,
3885 parentInstance,
3886 );
3887 }
3888 }
3889 // The frontend only really cares about the name, and children.
3890 // The first two don't really change, so we are only concerned with the order of children here.
3891 // This is trickier than a simple comparison though, since certain types of fibers are filtered.
3892 const nextChildren: Array<number> = [];
3893
3894 addUnfilteredSuspenseChildrenIDs(parentInstance, nextChildren);
3895
3896 const numChildren = nextChildren.length;
3897 if (numChildren < 2) {
3898 // No need to reorder.
3899 return;
3900 }
3901 pushOperation(SUSPENSE_TREE_OPERATION_REORDER_CHILDREN);
3902 // $FlowFixMe[incompatible-call] TODO: Allow filtering SuspenseNode
3903 // $FlowFixMe[incompatible-type]
3904 pushOperation(parentInstance.instance.id);
3905 pushOperation(numChildren);
3906 for (let i = 0; i < nextChildren.length; i++) {
3907 pushOperation(nextChildren[i]);
3908 }
3909 }
3910
3911 function updateVirtualInstanceRecursively(
3912 virtualInstance: VirtualInstance,
3913 nextFirstChild: Fiber,
3914 nextLastChild: null | Fiber, // non-inclusive
3915 prevFirstChild: null | Fiber,
3916 traceNearestHostComponentUpdate: boolean,
3917 virtualLevel: number, // the nth level of virtual instances
3918 ): UpdateFlags {
3919 const stashedParent = reconcilingParent;
3920 const stashedPrevious = previouslyReconciledSibling;
3921 const stashedRemaining = remainingReconcilingChildren;
3922 const previousSuspendedBy = virtualInstance.suspendedBy;
3923 // Push a new DevTools instance parent while reconciling this subtree.
3924 reconcilingParent = virtualInstance;
3925 previouslyReconciledSibling = null;
3926 // Move all the children of this instance to the remaining set.
3927 // We'll move them back one by one, and anything that remains is deleted.
3928 remainingReconcilingChildren = virtualInstance.firstChild;
3929 virtualInstance.firstChild = null;
3930 virtualInstance.suspendedBy = null;
3931 try {
3932 let updateFlags = updateVirtualChildrenRecursively(
3933 nextFirstChild,
3934 nextLastChild,
3935 prevFirstChild,
3936 traceNearestHostComponentUpdate,
3937 virtualLevel + 1,
3938 );
3939 if ((updateFlags & ShouldResetChildren) !== NoUpdate) {
3940 if (!isInDisconnectedSubtree) {
3941 recordResetChildren(virtualInstance);
3942 }
3943 updateFlags &= ~ShouldResetChildren;
3944 }
3945 removePreviousSuspendedBy(
3946 virtualInstance,
3947 previousSuspendedBy,
3948 reconcilingParentSuspenseNode,
3949 );
3950 // Update the errors/warnings count. If this Instance has switched to a different
3951 // ReactComponentInfo instance, such as when refreshing Server Components, then
3952 // we replace all the previous logs with the ones associated with the new ones rather
3953 // than merging. Because deduping is expected to happen at the request level.
3954 const componentLogsEntry = componentInfoToComponentLogsMap.get(
3955 virtualInstance.data,
3956 );
3957 recordConsoleLogs(virtualInstance, componentLogsEntry);
3958 // Must be called after all children have been appended.
3959 recordVirtualProfilingDurations(virtualInstance);
3960
3961 return updateFlags;
3962 } finally {
3963 unmountRemainingChildren();
3964 reconcilingParent = stashedParent;
3965 previouslyReconciledSibling = stashedPrevious;
3966 remainingReconcilingChildren = stashedRemaining;
3967 }
3968 }
3969
3970 function updateVirtualChildrenRecursively(
3971 nextFirstChild: Fiber,
3972 nextLastChild: null | Fiber, // non-inclusive
3973 prevFirstChild: null | Fiber,
3974 traceNearestHostComponentUpdate: boolean,
3975 virtualLevel: number, // the nth level of virtual instances
3976 ): UpdateFlags {
3977 let updateFlags = NoUpdate;
3978 // If the first child is different, we need to traverse them.
3979 // Each next child will be either a new child (mount) or an alternate (update).
3980 let nextChild: null | Fiber = nextFirstChild;
3981 let prevChildAtSameIndex = prevFirstChild;
3982 let previousVirtualInstance: null | VirtualInstance = null;
3983 let previousVirtualInstanceWasMount: boolean = false;
3984 let previousVirtualInstanceNextFirstFiber: Fiber = nextFirstChild;
3985 let previousVirtualInstancePrevFirstFiber: null | Fiber = prevFirstChild;
3986 while (nextChild !== null && nextChild !== nextLastChild) {
3987 let level = 0;
3988 if (nextChild._debugInfo) {
3989 for (let i = 0; i < nextChild._debugInfo.length; i++) {
3990 const debugEntry = nextChild._debugInfo[i];
3991 if (debugEntry.awaited) {
3992 // Async Info
3993 const asyncInfo: ReactAsyncInfo = debugEntry as any;
3994 if (level === virtualLevel) {
3995 // Track any async info between the previous virtual instance up until to this
3996 // instance and add it to the parent. This can add the same set multiple times
3997 // so we assume insertSuspendedBy dedupes.
3998 insertSuspendedBy(asyncInfo);
3999 }
4000 continue;
4001 }
4002 if (typeof debugEntry.name !== 'string') {
4003 // Not a Component. Some other Debug Info.
4004 continue;
4005 }
4006 const componentInfo: ReactComponentInfo = debugEntry as any;
4007 const secondaryEnv = getSecondaryEnvironmentName(
4008 nextChild._debugInfo,
4009 i,
4010 );
4011 if (componentInfo.env != null) {
4012 knownEnvironmentNames.add(componentInfo.env);
4013 }
4014 if (secondaryEnv !== null) {
4015 knownEnvironmentNames.add(secondaryEnv);
4016 }
4017 if (shouldFilterVirtual(componentInfo, secondaryEnv)) {
4018 continue;
4019 }
4020 if (level === virtualLevel) {
4021 if (
4022 previousVirtualInstance === null ||
4023 // Consecutive children with the same debug entry as a parent gets
4024 // treated as if they share the same virtual instance.
4025 previousVirtualInstance.data !== componentInfo
4026 ) {
4027 if (previousVirtualInstance !== null) {
4028 // Mount any previous children that should go into the previous parent.
4029 if (previousVirtualInstanceWasMount) {
4030 mountVirtualInstanceRecursively(
4031 previousVirtualInstance,
4032 previousVirtualInstanceNextFirstFiber,
4033 nextChild,
4034 traceNearestHostComponentUpdate,
4035 virtualLevel,
4036 );
4037 updateFlags |=
4038 ShouldResetChildren | ShouldResetSuspenseChildren;
4039 } else {
4040 updateFlags |= updateVirtualInstanceRecursively(
4041 previousVirtualInstance,
4042 previousVirtualInstanceNextFirstFiber,
4043 nextChild,
4044 previousVirtualInstancePrevFirstFiber,
4045 traceNearestHostComponentUpdate,
4046 virtualLevel,
4047 );
4048 }
4049 }
4050 let previousSiblingOfBestMatch = null;
4051 let bestMatch = remainingReconcilingChildren;
4052 if (
4053 componentInfo.key != null &&
4054 componentInfo.key !== REACT_OPTIMISTIC_KEY
4055 ) {
4056 // If there is a key try to find a matching key in the set.
4057 bestMatch = remainingReconcilingChildren;
4058 while (bestMatch !== null) {
4059 if (
4060 bestMatch.kind === VIRTUAL_INSTANCE &&
4061 bestMatch.data.key === componentInfo.key
4062 ) {
4063 break;
4064 }
4065 previousSiblingOfBestMatch = bestMatch;
4066 bestMatch = bestMatch.nextSibling;
4067 }
4068 }
4069 if (
4070 bestMatch !== null &&
4071 bestMatch.kind === VIRTUAL_INSTANCE &&
4072 bestMatch.data.name === componentInfo.name &&
4073 bestMatch.data.env === componentInfo.env &&
4074 bestMatch.data.key === componentInfo.key
4075 ) {
4076 // If the previous children had a virtual instance in the same slot
4077 // with the same name, then we claim it and reuse it for this update.
4078 // Update it with the latest entry.
4079 bestMatch.data = componentInfo;
4080 moveChild(bestMatch, previousSiblingOfBestMatch);
4081 previousVirtualInstance = bestMatch;
4082 previousVirtualInstanceWasMount = false;
4083 } else {
4084 // Otherwise we create a new instance.
4085 const newVirtualInstance = createVirtualInstance(componentInfo);
4086 recordVirtualMount(
4087 newVirtualInstance,
4088 reconcilingParent,
4089 secondaryEnv,
4090 );
4091 insertChild(newVirtualInstance);
4092 previousVirtualInstance = newVirtualInstance;
4093 previousVirtualInstanceWasMount = true;
4094 updateFlags |= ShouldResetChildren;
4095 }
4096 // Existing children might be reparented into this new virtual instance.
4097 // TODO: This will cause the front end to error which needs to be fixed.
4098 previousVirtualInstanceNextFirstFiber = nextChild;
4099 previousVirtualInstancePrevFirstFiber = prevChildAtSameIndex;
4100 }
4101 level++;
4102 break;
4103 } else {
4104 level++;
4105 }
4106 }
4107 }
4108 if (level === virtualLevel) {
4109 if (previousVirtualInstance !== null) {
4110 // If we were working on a virtual instance and this is not a virtual
4111 // instance, then we end the sequence and update any previous children
4112 // that should go into the previous virtual instance.
4113 if (previousVirtualInstanceWasMount) {
4114 mountVirtualInstanceRecursively(
4115 previousVirtualInstance,
4116 previousVirtualInstanceNextFirstFiber,
4117 nextChild,
4118 traceNearestHostComponentUpdate,
4119 virtualLevel,
4120 );
4121 updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
4122 } else {
4123 updateFlags |= updateVirtualInstanceRecursively(
4124 previousVirtualInstance,
4125 previousVirtualInstanceNextFirstFiber,
4126 nextChild,
4127 previousVirtualInstancePrevFirstFiber,
4128 traceNearestHostComponentUpdate,
4129 virtualLevel,
4130 );
4131 }
4132 previousVirtualInstance = null;
4133 }
4134
4135 // We've reached the end of the virtual levels, but not beyond,
4136 // and now continue with the regular fiber.
4137
4138 // Do a fast pass over the remaining children to find the previous instance.
4139 // TODO: This doesn't have the best O(n) for a large set of children that are
4140 // reordered. Consider using a temporary map if it's not the very next one.
4141 let prevChild;
4142 if (prevChildAtSameIndex === nextChild) {
4143 // This set is unchanged. We're just going through it to place all the
4144 // children again.
4145 prevChild = nextChild;
4146 } else {
4147 // We don't actually need to rely on the alternate here. We could also
4148 // reconcile against stateNode, key or whatever. Doesn't have to be same
4149 // Fiber pair.
4150 prevChild = nextChild.alternate;
4151 }
4152 let previousSiblingOfExistingInstance = null;
4153 let existingInstance = null;
4154 if (prevChild !== null) {
4155 existingInstance = remainingReconcilingChildren;
4156 while (existingInstance !== null) {
4157 if (existingInstance.data === prevChild) {
4158 break;
4159 }
4160 previousSiblingOfExistingInstance = existingInstance;
4161 existingInstance = existingInstance.nextSibling;
4162 }
4163 }
4164 if (existingInstance !== null) {
4165 // Common case. Match in the same parent.
4166 const fiberInstance: FiberInstance | FilteredFiberInstance =
4167 existingInstance as any; // Only matches if it's a Fiber.
4168
4169 // We keep track if the order of the children matches the previous order.
4170 // They are always different referentially, but if the instances line up
4171 // conceptually we'll want to know that.
4172 if (prevChild !== prevChildAtSameIndex) {
4173 updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
4174 }
4175
4176 moveChild(fiberInstance, previousSiblingOfExistingInstance);
4177
4178 // If a nested tree child order changed but it can't handle its own
4179 // child order invalidation (e.g. because it's filtered out like host nodes),
4180 // propagate the need to reset child order upwards to this Fiber.
4181 updateFlags |= updateFiberRecursively(
4182 fiberInstance,
4183 nextChild,
4184 prevChild as any,
4185 traceNearestHostComponentUpdate,
4186 );
4187 } else if (prevChild !== null && shouldFilterFiber(nextChild)) {
4188 // The filtered instance could've reordered.
4189 if (prevChild !== prevChildAtSameIndex) {
4190 updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
4191 }
4192
4193 // If this Fiber should be filtered, we need to still update its children.
4194 // This relies on an alternate since we don't have an Instance with the previous
4195 // child on it. Ideally, the reconciliation wouldn't need previous Fibers that
4196 // are filtered from the tree.
4197 updateFlags |= updateFiberRecursively(
4198 null,
4199 nextChild,
4200 prevChild,
4201 traceNearestHostComponentUpdate,
4202 );
4203 } else {
4204 // It's possible for a FiberInstance to be reparented when virtual parents
4205 // get their sequence split or change structure with the same render result.
4206 // In this case we unmount the and remount the FiberInstances.
4207 // This might cause us to lose the selection but it's an edge case.
4208
4209 // We let the previous instance remain in the "remaining queue" it is
4210 // in to be deleted at the end since it'll have no match.
4211
4212 mountFiberRecursively(nextChild, traceNearestHostComponentUpdate);
4213 // Need to mark the parent set to remount the new instance.
4214 updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
4215 }
4216 }
4217 // Try the next child.
4218 nextChild = nextChild.sibling;
4219 // Advance the pointer in the previous list so that we can
4220 // keep comparing if they line up.
4221 if (
4222 (updateFlags & ShouldResetChildren) === NoUpdate &&
4223 prevChildAtSameIndex !== null
4224 ) {
4225 prevChildAtSameIndex = prevChildAtSameIndex.sibling;
4226 }
4227 }
4228 if (previousVirtualInstance !== null) {
4229 if (previousVirtualInstanceWasMount) {
4230 mountVirtualInstanceRecursively(
4231 previousVirtualInstance,
4232 previousVirtualInstanceNextFirstFiber,
4233 null,
4234 traceNearestHostComponentUpdate,
4235 virtualLevel,
4236 );
4237 updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
4238 } else {
4239 updateFlags |= updateVirtualInstanceRecursively(
4240 previousVirtualInstance,
4241 previousVirtualInstanceNextFirstFiber,
4242 null,
4243 previousVirtualInstancePrevFirstFiber,
4244 traceNearestHostComponentUpdate,
4245 virtualLevel,
4246 );
4247 }
4248 }
4249 // If we have no more children, but used to, they don't line up.
4250 if (prevChildAtSameIndex !== null) {
4251 updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
4252 }
4253 return updateFlags;
4254 }
4255
4256 // Returns whether closest unfiltered fiber parent needs to reset its child list.
4257 function updateChildrenRecursively(
4258 nextFirstChild: null | Fiber,
4259 prevFirstChild: null | Fiber,
4260 traceNearestHostComponentUpdate: boolean,
4261 ): UpdateFlags {
4262 if (nextFirstChild === null) {
4263 return prevFirstChild !== null ? ShouldResetChildren : NoUpdate;
4264 }
4265 return updateVirtualChildrenRecursively(
4266 nextFirstChild,
4267 null,
4268 prevFirstChild,
4269 traceNearestHostComponentUpdate,
4270 0,
4271 );
4272 }
4273
4274 function updateSuspenseChildrenRecursively(
4275 nextContentFiber: Fiber,
4276 prevContentFiber: Fiber,
4277 traceNearestHostComponentUpdate: boolean,
4278 stashedSuspenseParent: null | SuspenseNode,
4279 stashedSuspensePrevious: null | SuspenseNode,
4280 stashedSuspenseRemaining: null | SuspenseNode,
4281 ): UpdateFlags {
4282 let updateFlags = NoUpdate;
4283 const prevFallbackFiber = prevContentFiber.sibling;
4284 const nextFallbackFiber = nextContentFiber.sibling;
4285
4286 // First update only the Offscreen boundary. I.e. the main content.
4287 updateFlags |= updateVirtualChildrenRecursively(
4288 nextContentFiber,
4289 nextFallbackFiber,
4290 prevContentFiber,
4291 traceNearestHostComponentUpdate,
4292 0,
4293 );
4294
4295 // Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4296 // reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
4297 // Since the fallback conceptually blocks the parent.
4298 reconcilingParentSuspenseNode = stashedSuspenseParent;
4299 previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4300 remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4301 if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
4302 if (nextFallbackFiber === null) {
4303 unmountRemainingChildren();
4304 } else {
4305 updateFlags |= updateVirtualChildrenRecursively(
4306 nextFallbackFiber,
4307 null,
4308 prevFallbackFiber,
4309 traceNearestHostComponentUpdate,
4310 0,
4311 );
4312
4313 if ((updateFlags & ShouldResetSuspenseChildren) !== NoUpdate) {
4314 updateFlags |= ShouldResetParentSuspenseChildren;
4315 updateFlags &= ~ShouldResetSuspenseChildren;
4316 }
4317 }
4318 }
4319
4320 return updateFlags;
4321 }
4322
4323 // Returns whether closest unfiltered fiber parent needs to reset its child list.
4324 function updateFiberRecursively(
4325 fiberInstance: null | FiberInstance | FilteredFiberInstance, // null if this should be filtered
4326 nextFiber: Fiber,
4327 prevFiber: Fiber,
4328 traceNearestHostComponentUpdate: boolean,
4329 ): UpdateFlags {
4330 // $FlowFixMe[constant-condition]
4331 if (__DEBUG__) {
4332 if (fiberInstance !== null) {
4333 debug('updateFiberRecursively()', fiberInstance, reconcilingParent);
4334 }
4335 }
4336
4337 if (traceUpdatesEnabled) {
4338 const elementType = getElementTypeForFiber(nextFiber);
4339 if (traceNearestHostComponentUpdate) {
4340 // If an ancestor updated, we should mark the nearest host nodes for highlighting.
4341 if (elementType === ElementTypeHostComponent) {
4342 traceUpdatesForNodes.add(nextFiber.stateNode);
4343 traceNearestHostComponentUpdate = false;
4344 }
4345 } else {
4346 if (
4347 elementType === ElementTypeFunction ||
4348 elementType === ElementTypeClass ||
4349 elementType === ElementTypeContext ||
4350 elementType === ElementTypeMemo ||
4351 elementType === ElementTypeForwardRef
4352 ) {
4353 if (prevFiber !== nextFiber) {
4354 // Otherwise if this is a traced ancestor, flag for the nearest host descendant(s).
4355 traceNearestHostComponentUpdate = didFiberRender(
4356 ReactTypeOfWork,
4357 prevFiber,
4358 nextFiber,
4359 );
4360 }
4361 }
4362 }
4363 }
4364
4365 const stashedParent = reconcilingParent;
4366 const stashedPrevious = previouslyReconciledSibling;
4367 const stashedRemaining = remainingReconcilingChildren;
4368 const stashedSuspenseParent = reconcilingParentSuspenseNode;
4369 const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
4370 const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
4371 const stashedIsInActivitySlice = isInFocusedActivity;
4372 let updateFlags = NoUpdate;
4373 let shouldMeasureSuspenseNode = false;
4374 let shouldPopSuspenseNode = false;
4375 let previousSuspendedBy = null;
4376 if (fiberInstance !== null) {
4377 previousSuspendedBy = fiberInstance.suspendedBy;
4378 // Update the Fiber so we that we always keep the current Fiber on the data.
4379 fiberInstance.data = nextFiber;
4380 if (prevFiber !== nextFiber) {
4381 if (
4382 mostRecentlyInspectedElement !== null &&
4383 (mostRecentlyInspectedElement.id === fiberInstance.id ||
4384 // If we're inspecting a Root, we inspect the Screen.
4385 // Invalidating any Root invalidates the Screen too.
4386 (mostRecentlyInspectedElement.type === ElementTypeRoot &&
4387 nextFiber.tag === HostRoot)) &&
4388 didFiberRender(ReactTypeOfWork, prevFiber, nextFiber)
4389 ) {
4390 // If this Fiber has updated, clear cached inspected data.
4391 // If it is inspected again, it may need to be re-run to obtain updated hooks values.
4392 hasElementUpdatedSinceLastInspected = true;
4393 }
4394 }
4395 // Push a new DevTools instance parent while reconciling this subtree.
4396 reconcilingParent = fiberInstance;
4397 previouslyReconciledSibling = null;
4398 // Move all the children of this instance to the remaining set.
4399 // We'll move them back one by one, and anything that remains is deleted.
4400 remainingReconcilingChildren = fiberInstance.firstChild;
4401 fiberInstance.firstChild = null;
4402 fiberInstance.suspendedBy = null;
4403
4404 const suspenseNode = fiberInstance.suspenseNode;
4405 if (suspenseNode !== null) {
4406 reconcilingParentSuspenseNode = suspenseNode;
4407 previouslyReconciledSiblingSuspenseNode = null;
4408 remainingReconcilingChildrenSuspenseNodes = suspenseNode.firstChild;
4409 suspenseNode.firstChild = null;
4410 shouldMeasureSuspenseNode = true;
4411 shouldPopSuspenseNode = true;
4412 }
4413
4414 if (focusedActivity !== null) {
4415 if (fiberInstance.id === focusedActivityID) {
4416 isInFocusedActivity = true;
4417 } else if (nextFiber.tag === ActivityComponent) {
4418 // Filtering nested Activity components inside the focused activity
4419 // is done in the frontend.
4420 }
4421 }
4422 }
4423 try {
4424 trackDebugInfoFromLazyType(nextFiber);
4425 trackDebugInfoFromUsedThenables(nextFiber);
4426
4427 if (nextFiber.tag === HostHoistable) {
4428 const nearestInstance = reconcilingParent;
4429 if (nearestInstance === null) {
4430 throw new Error('Did not expect a host hoistable to be the root');
4431 }
4432 if (prevFiber.memoizedState !== nextFiber.memoizedState) {
4433 releaseHostResource(nearestInstance, prevFiber.memoizedState);
4434 aquireHostResource(nearestInstance, nextFiber.memoizedState);
4435 }
4436 trackDebugInfoFromHostResource(nearestInstance, nextFiber);
4437 } else if (
4438 nextFiber.tag === HostComponent ||
4439 nextFiber.tag === HostText ||
4440 nextFiber.tag === HostSingleton
4441 ) {
4442 const nearestInstance = reconcilingParent;
4443 if (nearestInstance === null) {
4444 throw new Error('Did not expect a host hoistable to be the root');
4445 }
4446 if (prevFiber.stateNode !== nextFiber.stateNode) {
4447 // In persistent mode, it's possible for the stateNode to update with
4448 // a new clone. In that case we need to release the old one and aquire
4449 // new one instead.
4450 releaseHostInstance(nearestInstance, prevFiber.stateNode);
4451 aquireHostInstance(nearestInstance, nextFiber.stateNode);
4452 }
4453 trackDebugInfoFromHostComponent(nearestInstance, nextFiber);
4454 }
4455
4456 // The behavior of timed-out legacy Suspense trees is unique. Without the Offscreen wrapper.
4457 // Rather than unmount the timed out content (and possibly lose important state),
4458 // React re-parents this content within a hidden Fragment while the fallback is showing.
4459 // This behavior doesn't need to be observable in the DevTools though.
4460 // It might even result in a bad user experience for e.g. node selection in the Elements panel.
4461 // The easiest fix is to strip out the intermediate Fragment fibers,
4462 // so the Elements panel and Profiler don't need to special case them.
4463 const isLegacySuspense =
4464 nextFiber.tag === SuspenseComponent && OffscreenComponent === -1;
4465 // Suspense components only have a non-null memoizedState if they're timed-out.
4466 const prevDidTimeout =
4467 isLegacySuspense && prevFiber.memoizedState !== null;
4468 const nextDidTimeOut =
4469 isLegacySuspense && nextFiber.memoizedState !== null;
4470
4471 const prevWasHidden = isHiddenOffscreen(prevFiber);
4472 const nextIsHidden = isHiddenOffscreen(nextFiber);
4473 const prevWasSuspended = isSuspendedOffscreen(prevFiber);
4474 const nextIsSuspended = isSuspendedOffscreen(nextFiber);
4475
4476 if (isLegacySuspense) {
4477 if (fiberInstance !== null && fiberInstance.suspenseNode !== null) {
4478 const suspenseNode = fiberInstance.suspenseNode;
4479 if (
4480 (prevFiber.stateNode === null) !==
4481 (nextFiber.stateNode === null)
4482 ) {
4483 trackThrownPromisesFromRetryCache(
4484 suspenseNode,
4485 nextFiber.stateNode,
4486 );
4487 }
4488 if (
4489 (prevFiber.memoizedState === null) !==
4490 (nextFiber.memoizedState === null)
4491 ) {
4492 // Toggle suspended state.
4493 recordSuspenseSuspenders(suspenseNode);
4494 }
4495 }
4496 }
4497 // The logic below is inspired by the code paths in updateSuspenseComponent()
4498 // inside ReactFiberBeginWork in the React source code.
4499 if (prevDidTimeout && nextDidTimeOut) {
4500 // Fallback -> Fallback:
4501 // 1. Reconcile fallback set.
4502 const nextFiberChild = nextFiber.child;
4503 const nextFallbackChildSet = nextFiberChild
4504 ? nextFiberChild.sibling
4505 : null;
4506 // Note: We can't use nextFiber.child.sibling.alternate
4507 // because the set is special and alternate may not exist.
4508 const prevFiberChild = prevFiber.child;
4509 const prevFallbackChildSet = prevFiberChild
4510 ? prevFiberChild.sibling
4511 : null;
4512
4513 if (prevFallbackChildSet == null && nextFallbackChildSet != null) {
4514 mountChildrenRecursively(
4515 nextFallbackChildSet,
4516 traceNearestHostComponentUpdate,
4517 );
4518
4519 updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
4520 }
4521
4522 const childrenUpdateFlags =
4523 nextFallbackChildSet != null && prevFallbackChildSet != null
4524 ? updateChildrenRecursively(
4525 nextFallbackChildSet,
4526 prevFallbackChildSet,
4527 traceNearestHostComponentUpdate,
4528 )
4529 : NoUpdate;
4530 updateFlags |= childrenUpdateFlags;
4531 } else if (prevDidTimeout && !nextDidTimeOut) {
4532 // Fallback -> Primary:
4533 // 1. Unmount fallback set
4534 // Note: don't emulate fallback unmount because React actually did it.
4535 // 2. Mount primary set
4536 const nextPrimaryChildSet = nextFiber.child;
4537 if (nextPrimaryChildSet !== null) {
4538 mountChildrenRecursively(
4539 nextPrimaryChildSet,
4540 traceNearestHostComponentUpdate,
4541 );
4542 updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
4543 }
4544 } else if (!prevDidTimeout && nextDidTimeOut) {
4545 // Primary -> Fallback:
4546 // 1. Hide primary set
4547 // We simply don't re-add the fallback children and let
4548 // unmountRemainingChildren() handle it.
4549 // 2. Mount fallback set
4550 const nextFiberChild = nextFiber.child;
4551 const nextFallbackChildSet = nextFiberChild
4552 ? nextFiberChild.sibling
4553 : null;
4554 if (nextFallbackChildSet != null) {
4555 mountChildrenRecursively(
4556 nextFallbackChildSet,
4557 traceNearestHostComponentUpdate,
4558 );
4559 updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
4560 }
4561 } else if (nextIsSuspended) {
4562 if (!prevWasSuspended) {
4563 // We're hiding the children. Disconnect them from the front end but keep state.
4564 if (fiberInstance !== null && !isInDisconnectedSubtree) {
4565 disconnectChildrenRecursively(remainingReconcilingChildren);
4566 }
4567 }
4568 // Update children inside the hidden tree if they committed with a new updates.
4569 const stashedDisconnected = isInDisconnectedSubtree;
4570 isInDisconnectedSubtree = true;
4571 try {
4572 updateFlags |= updateChildrenRecursively(
4573 nextFiber.child,
4574 prevFiber.child,
4575 false,
4576 );
4577 } finally {
4578 isInDisconnectedSubtree = stashedDisconnected;
4579 }
4580 } else if (prevWasSuspended && !nextIsSuspended) {
4581 // We're revealing the hidden children. We now need to update them to the latest state.
4582 // We do this while still in the disconnected state and then we reconnect the new ones.
4583 // This avoids reconnecting things that are about to be removed anyway.
4584 const stashedDisconnected = isInDisconnectedSubtree;
4585 isInDisconnectedSubtree = true;
4586 try {
4587 if (nextFiber.child !== null) {
4588 updateFlags |= updateChildrenRecursively(
4589 nextFiber.child,
4590 prevFiber.child,
4591 false,
4592 );
4593 }
4594 // Ensure we unmount any remaining children inside the isInDisconnectedSubtree flag
4595 // since they should not trigger real deletions.
4596 unmountRemainingChildren();
4597 remainingReconcilingChildren = null;
4598 } finally {
4599 isInDisconnectedSubtree = stashedDisconnected;
4600 }
4601 if (fiberInstance !== null && !isInDisconnectedSubtree) {
4602 reconnectChildrenRecursively(fiberInstance);
4603 // Children may have reordered while they were hidden.
4604 updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
4605 }
4606 } else if (nextIsHidden) {
4607 if (isActivityHiddenOffscreen(nextFiber)) {
4608 // Activity's hidden children stay visible in the DevTools tree.
4609 // Whether staying hidden or transitioning to hidden, update normally.
4610 updateFlags |= updateChildrenRecursively(
4611 nextFiber.child,
4612 prevFiber.child,
4613 traceNearestHostComponentUpdate,
4614 );
4615 } else if (prevWasHidden) {
4616 // still hidden. Nothing to do.
4617 } else {
4618 // We're hiding the children. Remove them from the Frontend
4619 unmountRemainingChildren();
4620 }
4621 } else if (prevWasHidden && !nextIsHidden) {
4622 if (
4623 nextFiber.return !== null &&
4624 nextFiber.return.tag === ActivityComponent
4625 ) {
4626 // Activity children were never unmounted, so just update normally.
4627 updateFlags |= updateChildrenRecursively(
4628 nextFiber.child,
4629 prevFiber.child,
4630 traceNearestHostComponentUpdate,
4631 );
4632 } else {
4633 // Since we don't mount hidden children and unmount children when hiding,
4634 // we need to enter the mount path when revealing.
4635 const nextChildSet = nextFiber.child;
4636 if (nextChildSet !== null) {
4637 mountChildrenRecursively(
4638 nextChildSet,
4639 traceNearestHostComponentUpdate,
4640 );
4641 updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
4642 }
4643 }
4644 } else if (
4645 nextFiber.tag === SuspenseComponent &&
4646 OffscreenComponent !== -1 &&
4647 fiberInstance !== null &&
4648 fiberInstance.suspenseNode !== null
4649 ) {
4650 // Modern Suspense path
4651 const suspenseNode = fiberInstance.suspenseNode;
4652 const prevContentFiber = prevFiber.child;
4653 const nextContentFiber = nextFiber.child;
4654 const previousHydrated = isFiberHydrated(prevFiber);
4655 const nextHydrated = isFiberHydrated(nextFiber);
4656 if (previousHydrated && nextHydrated) {
4657 if (nextContentFiber === null || prevContentFiber === null) {
4658 throw new Error(
4659 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
4660 );
4661 }
4662
4663 if (
4664 (prevFiber.stateNode === null) !==
4665 (nextFiber.stateNode === null)
4666 ) {
4667 trackThrownPromisesFromRetryCache(
4668 suspenseNode,
4669 nextFiber.stateNode,
4670 );
4671 }
4672
4673 if (
4674 (prevFiber.memoizedState === null) !==
4675 (nextFiber.memoizedState === null)
4676 ) {
4677 // Toggle suspended state.
4678 recordSuspenseSuspenders(suspenseNode);
4679 }
4680
4681 shouldMeasureSuspenseNode = false;
4682 updateFlags |= updateSuspenseChildrenRecursively(
4683 nextContentFiber,
4684 prevContentFiber,
4685 traceNearestHostComponentUpdate,
4686 stashedSuspenseParent,
4687 stashedSuspensePrevious,
4688 stashedSuspenseRemaining,
4689 );
4690 // updateSuspenseChildrenRecursively popped already
4691 shouldPopSuspenseNode = false;
4692 if (nextFiber.memoizedState === null) {
4693 // Measure this Suspense node in case it changed. We don't update the rect while
4694 // we're inside a disconnected subtree nor if we are the Suspense boundary that
4695 // is suspended. This lets us keep the rectangle of the displayed content while
4696 // we're suspended to visualize the resulting state.
4697 shouldMeasureSuspenseNode = !isInDisconnectedSubtree;
4698 }
4699 } else if (!previousHydrated && nextHydrated) {
4700 if (nextContentFiber === null) {
4701 throw new Error(
4702 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
4703 );
4704 }
4705
4706 trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode);
4707 // Toggle suspended state.
4708 recordSuspenseSuspenders(suspenseNode);
4709
4710 mountSuspenseChildrenRecursively(
4711 nextContentFiber,
4712 traceNearestHostComponentUpdate,
4713 stashedSuspenseParent,
4714 stashedSuspensePrevious,
4715 stashedSuspenseRemaining,
4716 );
4717 // mountSuspenseChildrenRecursively popped already
4718 shouldPopSuspenseNode = false;
4719 } else if (previousHydrated && !nextHydrated) {
4720 throw new Error(
4721 'Encountered a dehydrated Suspense boundary that was previously hydrated.',
4722 );
4723 } else {
4724 // This Suspense Fiber is still dehydrated. It won't have any children
4725 // until hydration.
4726 }
4727 } else {
4728 // Common case: Primary -> Primary.
4729 // This is the same code path as for non-Suspense fibers.
4730 if (nextFiber.child !== prevFiber.child) {
4731 updateFlags |= updateChildrenRecursively(
4732 nextFiber.child,
4733 prevFiber.child,
4734 traceNearestHostComponentUpdate,
4735 );
4736 } else {
4737 // Children are unchanged.
4738 if (fiberInstance !== null) {
4739 // All the remaining children will be children of this same fiber so we can just reuse them.
4740 // I.e. we just restore them by undoing what we did above.
4741 fiberInstance.firstChild = remainingReconcilingChildren;
4742 remainingReconcilingChildren = null;
4743
4744 consumeSuspenseNodesOfExistingInstance(fiberInstance);
4745
4746 if (traceUpdatesEnabled) {
4747 // If we're tracing updates and we've bailed out before reaching a host node,
4748 // we should fall back to recursively marking the nearest host descendants for highlight.
4749 if (traceNearestHostComponentUpdate) {
4750 const hostInstances =
4751 findAllCurrentHostInstances(fiberInstance);
4752 hostInstances.forEach(hostInstance => {
4753 traceUpdatesForNodes.add(hostInstance);
4754 });
4755 }
4756 }
4757 } else {
4758 const childrenUpdateFlags = updateChildrenRecursively(
4759 nextFiber.child,
4760 prevFiber.child,
4761 false,
4762 );
4763 // If this fiber is filtered there might be changes to this set elsewhere so we have
4764 // to visit each child to place it back in the set. We let the child bail out instead.
4765 if ((childrenUpdateFlags & ShouldResetChildren) !== NoUpdate) {
4766 throw new Error(
4767 'The children should not have changed if we pass in the same set.',
4768 );
4769 }
4770 updateFlags |= childrenUpdateFlags;
4771 }
4772 }
4773 }
4774
4775 if (fiberInstance !== null) {
4776 // Detect Activity hidden/visible mode changes.
4777 if (
4778 prevFiber.tag === ActivityComponent &&
4779 nextFiber.tag === ActivityComponent &&
4780 fiberInstance.kind === FIBER_INSTANCE
4781 ) {
4782 const prevOffscreen = prevFiber.child;
4783 const nextOffscreen = nextFiber.child;
4784 if (prevOffscreen !== null && nextOffscreen !== null) {
4785 const prevHidden = isHiddenOffscreen(prevOffscreen);
4786 const nextHidden = isHiddenOffscreen(nextOffscreen);
4787 if (prevHidden !== nextHidden) {
4788 pushOperation(TREE_OPERATION_SET_SUBTREE_MODE);
4789 pushOperation(fiberInstance.id);
4790 pushOperation(
4791 nextHidden ? ActivityHiddenMode : ActivityVisibleMode,
4792 );
4793 }
4794 }
4795 }
4796
4797 removePreviousSuspendedBy(
4798 fiberInstance,
4799 previousSuspendedBy,
4800 shouldPopSuspenseNode
4801 ? reconcilingParentSuspenseNode
4802 : stashedSuspenseParent,
4803 );
4804
4805 if (fiberInstance.kind === FIBER_INSTANCE) {
4806 let componentLogsEntry = fiberToComponentLogsMap.get(
4807 fiberInstance.data,
4808 );
4809 if (
4810 componentLogsEntry === undefined &&
4811 fiberInstance.data.alternate
4812 ) {
4813 componentLogsEntry = fiberToComponentLogsMap.get(
4814 fiberInstance.data.alternate,
4815 );
4816 }
4817 recordConsoleLogs(fiberInstance, componentLogsEntry);
4818
4819 if (!isInDisconnectedSubtree) {
4820 const isProfilingSupported =
4821 nextFiber.hasOwnProperty('treeBaseDuration');
4822 if (isProfilingSupported) {
4823 recordProfilingDurations(fiberInstance, prevFiber);
4824 }
4825 }
4826 }
4827 }
4828
4829 if ((updateFlags & ShouldResetChildren) !== NoUpdate) {
4830 // We need to crawl the subtree for closest non-filtered Fibers
4831 // so that we can display them in a flat children set.
4832 if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) {
4833 if (!nextIsSuspended && !isInDisconnectedSubtree) {
4834 recordResetChildren(fiberInstance);
4835 }
4836
4837 // We've handled the child order change for this Fiber.
4838 // Since it's included, there's no need to invalidate parent child order.
4839 updateFlags &= ~ShouldResetChildren;
4840 } else {
4841 // Let the closest unfiltered parent Fiber reset its child order instead.
4842 }
4843 } else {
4844 }
4845
4846 if ((updateFlags & ShouldResetSuspenseChildren) !== NoUpdate) {
4847 if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) {
4848 const suspenseNode = fiberInstance.suspenseNode;
4849 if (suspenseNode !== null) {
4850 recordResetSuspenseChildren(suspenseNode);
4851 updateFlags &= ~ShouldResetSuspenseChildren;
4852 }
4853 } else {
4854 // Let the closest unfiltered parent Fiber reset its child order instead.
4855 }
4856 }
4857 if ((updateFlags & ShouldResetParentSuspenseChildren) !== NoUpdate) {
4858 if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) {
4859 const suspenseNode = fiberInstance.suspenseNode;
4860 if (suspenseNode !== null) {
4861 updateFlags &= ~ShouldResetParentSuspenseChildren;
4862 updateFlags |= ShouldResetSuspenseChildren;
4863 }
4864 } else {
4865 // Let the closest unfiltered parent Fiber reset its child order instead.
4866 }
4867 }
4868
4869 return updateFlags;
4870 } finally {
4871 if (fiberInstance !== null) {
4872 unmountRemainingChildren();
4873 reconcilingParent = stashedParent;
4874 previouslyReconciledSibling = stashedPrevious;
4875 remainingReconcilingChildren = stashedRemaining;
4876 if (shouldMeasureSuspenseNode) {
4877 if (!isInDisconnectedSubtree) {
4878 // Measure this Suspense node in case it changed. We don't update the rect
4879 // while we're inside a disconnected subtree so that we keep the outline
4880 // as it was before we hid the parent.
4881 const suspenseNode = fiberInstance.suspenseNode;
4882 if (suspenseNode === null) {
4883 throw new Error(
4884 'Attempted to measure a Suspense node that does not exist.',
4885 );
4886 }
4887 const prevRects = suspenseNode.rects;
4888 const nextRects = measureInstance(fiberInstance);
4889 if (!areEqualRects(prevRects, nextRects)) {
4890 suspenseNode.rects = nextRects;
4891 recordSuspenseResize(suspenseNode);
4892 }
4893 }
4894 }
4895 if (shouldPopSuspenseNode) {
4896 reconcilingParentSuspenseNode = stashedSuspenseParent;
4897 previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4898 remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
4899 }
4900 isInFocusedActivity = stashedIsInActivitySlice;
4901 }
4902 }
4903 }
4904
4905 function disconnectChildrenRecursively(firstChild: null | DevToolsInstance) {
4906 for (let child = firstChild; child !== null; child = child.nextSibling) {
4907 if (
4908 (child.kind === FIBER_INSTANCE ||
4909 child.kind === FILTERED_FIBER_INSTANCE) &&
4910 isSuspendedOffscreen(child.data)
4911 ) {
4912 // This instance's children are already disconnected.
4913 } else {
4914 disconnectChildrenRecursively(child.firstChild);
4915 }
4916 if (child.kind === FIBER_INSTANCE) {
4917 recordDisconnect(child);
4918 } else if (child.kind === VIRTUAL_INSTANCE) {
4919 recordVirtualDisconnect(child);
4920 }
4921 }
4922 }
4923
4924 function reconnectChildrenRecursively(parentInstance: DevToolsInstance) {
4925 for (
4926 let child = parentInstance.firstChild;
4927 child !== null;
4928 child = child.nextSibling
4929 ) {
4930 if (child.kind === FIBER_INSTANCE) {
4931 recordReconnect(child, parentInstance);
4932 } else if (child.kind === VIRTUAL_INSTANCE) {
4933 const secondaryEnv = null; // TODO: We don't have this data anywhere. We could just stash it somewhere.
4934 recordVirtualReconnect(child, parentInstance, secondaryEnv);
4935 }
4936 if (
4937 (child.kind === FIBER_INSTANCE ||
4938 child.kind === FILTERED_FIBER_INSTANCE) &&
4939 isHiddenOffscreen(child.data) &&
4940 !isActivityHiddenOffscreen(child.data)
4941 ) {
4942 // This instance's children should remain disconnected.
4943 // Activity's hidden children are still shown in the tree.
4944 } else {
4945 reconnectChildrenRecursively(child);
4946 }
4947 }
4948 }
4949
4950 function cleanup() {
4951 isProfiling = false;
4952 }
4953
4954 function flushInitialOperations() {
4955 const localPendingOperationsQueue = pendingOperationsQueue;
4956
4957 pendingOperationsQueue = null;
4958
4959 if (
4960 localPendingOperationsQueue !== null &&
4961 localPendingOperationsQueue.length > 0
4962 ) {
4963 // We may have already queued up some operations before the frontend connected
4964 // If so, let the frontend know about them.
4965 localPendingOperationsQueue.forEach(operations => {
4966 hook.emit('operations', operations);
4967 });
4968 } else {
4969 // Before the traversals, remember to start tracking
4970 // our path in case we have selection to restore.
4971 if (trackedPath !== null) {
4972 mightBeOnTrackedPath = true;
4973 }
4974 // If we have not been profiling, then we can just walk the tree and build up its current state as-is.
4975 hook.getFiberRoots(rendererID).forEach(root => {
4976 const current = root.current;
4977 const newRoot = createFiberInstance(current);
4978 rootToFiberInstanceMap.set(root, newRoot);
4979 idToDevToolsInstanceMap.set(newRoot.id, newRoot);
4980 currentRoot = newRoot;
4981 setRootPseudoKey(currentRoot.id, root.current);
4982
4983 // Handle multi-renderer edge-case where only some v16 renderers support profiling.
4984 if (isProfiling && rootSupportsProfiling(root)) {
4985 // If profiling is active, store commit time and duration.
4986 // The frontend may request this information after profiling has stopped.
4987 currentCommitProfilingMetadata = {
4988 changeDescriptions: recordChangeDescriptions ? new Map() : null,
4989 durations: [],
4990 commitTime: getCurrentTime() - profilingStartTime,
4991 maxActualDuration: 0,
4992 priorityLevel: null,
4993 updaters: null,
4994 effectDuration: null,
4995 passiveEffectDuration: null,
4996 };
4997 }
4998
4999 mountFiberRecursively(root.current, false);
5000
Showing first 5,000 of 8,071 lines. View raw