1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow8 */910import type {11 Thenable,12 ReactComponentInfo,13 ReactDebugInfo,14 ReactAsyncInfo,15 ReactIOInfo,16 ReactStackTrace,17 ReactCallSite,18 Wakeable,19} from 'shared/ReactTypes';2021import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';2223import {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';49import {50 deletePathInObject,51 getInObject,52 getUID,53 renamePathInObject,54 setInObject,55 utfEncodeString,56} from 'react-devtools-shared/src/utils';57import {58 formatConsoleArgumentsToSingleString,59 formatDurationToMicrosecondsGranularity,60 gte,61 serializeToString,62} from 'react-devtools-shared/src/backend/utils';63import {64 extractLocationFromComponentStack,65 extractLocationFromOwnerStack,66 parseStackTrace,67} from 'react-devtools-shared/src/backend/utils/parseStackTrace';68import {69 cleanForBridge,70 copyWithDelete,71 copyWithRename,72 copyWithSet,73 getEffectDurations,74} from '../utils';75import {76 __DEBUG__,77 PROFILING_FLAG_BASIC_SUPPORT,78 PROFILING_FLAG_TIMELINE_SUPPORT,79 PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT,80 TREE_OPERATION_ADD,81 TREE_OPERATION_REMOVE,82 TREE_OPERATION_REORDER_CHILDREN,83 TREE_OPERATION_SET_SUBTREE_MODE,84 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,85 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,86 TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,87 SUSPENSE_TREE_OPERATION_ADD,88 SUSPENSE_TREE_OPERATION_REMOVE,89 SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,90 SUSPENSE_TREE_OPERATION_RESIZE,91 SUSPENSE_TREE_OPERATION_SUSPENDERS,92 UNKNOWN_SUSPENDERS_NONE,93 UNKNOWN_SUSPENDERS_REASON_PRODUCTION,94 UNKNOWN_SUSPENDERS_REASON_OLD_VERSION,95 UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE,96} from '../../constants';97import {inspectHooksOfFiber} from 'react-debug-tools';98import {99 CONCURRENT_MODE_NUMBER,100 CONCURRENT_MODE_SYMBOL_STRING,101 DEPRECATED_ASYNC_MODE_SYMBOL_STRING,102 PROVIDER_NUMBER,103 PROVIDER_SYMBOL_STRING,104 CONTEXT_NUMBER,105 CONTEXT_SYMBOL_STRING,106 CONSUMER_SYMBOL_STRING,107 STRICT_MODE_NUMBER,108 STRICT_MODE_SYMBOL_STRING,109 PROFILER_NUMBER,110 PROFILER_SYMBOL_STRING,111 LAZY_SYMBOL_STRING,112 REACT_OPTIMISTIC_KEY,113} from '../shared/ReactSymbols';114import {enableStyleXFeatures} from 'react-devtools-feature-flags';115116import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponentLogs';117118import {getIODescription} from 'shared/ReactIODescription';119120import {121 getPublicInstance,122 getNativeTag,123 getCurrentTime,124} from 'react-devtools-shared/src/backend/DevToolsNativeHost';125import {126 isError,127 rootSupportsProfiling,128 isErrorBoundary,129 getSecondaryEnvironmentName,130 areEqualRects,131} from './shared/DevToolsFiberInspection';132import {133 didFiberRender,134 getContextChanged,135 getChangedHooksIndices,136 getChangedKeys,137} from './shared/DevToolsFiberChangeDetection';138import {getInternalReactConstants} from './shared/DevToolsFiberInternalReactConstants';139import {140 ioExistsInSuspenseAncestor,141 getAwaitInSuspendedByFromIO,142 getVirtualEndTime,143} from './shared/DevToolsFiberSuspense';144145import {146 getStackByFiberInDevAndProd,147 getOwnerStackByFiberInDev,148 supportsOwnerStacks,149 supportsConsoleTasks,150} from './DevToolsFiberComponentStack';151152import {getStyleXData} from '../StyleX/utils';153import {createProfilingHooks} from '../profilingHooks';154155import type {GetTimelineData, ToggleProfilingStatus} from '../profilingHooks';156import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';157import type {158 ChangeDescription,159 CommitDataBackend,160 DevToolsHook,161 InspectedElement,162 InspectedElementPayload,163 InstanceAndStyle,164 HostInstance,165 PathFrame,166 PathMatch,167 ProfilingDataBackend,168 ProfilingDataForRootBackend,169 ReactRenderer,170 Rect,171 RendererInterface,172 SerializedElement,173 SerializedAsyncInfo,174 ProfilingSettings,175} from '../types';176import type {177 ComponentFilter,178 ActivitySliceFilter,179 ElementType,180 Plugins,181} from 'react-devtools-shared/src/frontend/types';182import type {ReactFunctionLocation} from 'shared/ReactTypes';183import type {184 FiberInstance,185 FilteredFiberInstance,186 VirtualInstance,187 DevToolsInstance,188 SuspenseNode,189} from './shared/DevToolsFiberTypes';190import {191 FIBER_INSTANCE,192 VIRTUAL_INSTANCE,193 FILTERED_FIBER_INSTANCE,194} from './shared/DevToolsFiberTypes';195import {getDispatcherRef} from '../shared/DevToolsReactDispatcher';196import {getSourceLocationByFiber} from './DevToolsFiberComponentStack';197import {formatOwnerStack} from '../shared/DevToolsOwnerStack';198199function createFiberInstance(fiber: Fiber): FiberInstance {200 return {201 kind: FIBER_INSTANCE,202 id: getUID(),203 parent: null,204 firstChild: null,205 nextSibling: null,206 source: null,207 logCount: 0,208 treeBaseDuration: 0,209 suspendedBy: null,210 suspenseNode: null,211 data: fiber,212 };213}214215// This is used to represent a filtered Fiber but still lets us find its host instance.216function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance {217 return {218 kind: FILTERED_FIBER_INSTANCE,219 id: 0,220 parent: null,221 firstChild: null,222 nextSibling: null,223 source: null,224 logCount: 0,225 treeBaseDuration: 0,226 suspendedBy: null,227 suspenseNode: null,228 data: fiber,229 } as any;230}231232function createVirtualInstance(233 debugEntry: ReactComponentInfo,234): VirtualInstance {235 return {236 kind: VIRTUAL_INSTANCE,237 id: getUID(),238 parent: null,239 firstChild: null,240 nextSibling: null,241 source: null,242 logCount: 0,243 treeBaseDuration: 0,244 suspendedBy: null,245 suspenseNode: null,246 data: debugEntry,247 };248}249250// Update flags need to be propagated up until the caller that put the corresponding251// node on the stack.252// If you push a new node, you need to handle ShouldResetChildren when you pop it.253// If you push a new Suspense node, you need to handle ShouldResetSuspenseChildren when you pop it.254type UpdateFlags = number;255const NoUpdate = /* */ 0b000;256const ShouldResetChildren = /* */ 0b001;257const ShouldResetSuspenseChildren = /* */ 0b010;258const ShouldResetParentSuspenseChildren = /* */ 0b100;259260function createSuspenseNode(261 instance: FiberInstance | FilteredFiberInstance,262): SuspenseNode {263 return (instance.suspenseNode = {264 instance: instance,265 parent: null,266 firstChild: null,267 nextSibling: null,268 rects: null,269 suspendedBy: new Map(),270 environments: new Map(),271 endTime: 0,272 hasUniqueSuspenders: false,273 hasUnknownSuspenders: false,274 });275}276277// All environment names we've seen so far. This lets us create a list of filters to apply.278// This should ideally include env of filtered Components too so that you can add those as279// filters at the same time as removing some other filter.280const knownEnvironmentNames: Set<string> = new Set();281282// Map of FiberRoot to their root FiberInstance.283const rootToFiberInstanceMap: Map<FiberRoot, FiberInstance> = new Map();284285// Map of id to FiberInstance or VirtualInstance.286// This Map is used to e.g. get the display name for a Fiber or schedule an update,287// operations that should be the same whether the current and work-in-progress Fiber is used.288const idToDevToolsInstanceMap: Map<289 FiberInstance['id'] | VirtualInstance['id'],290 FiberInstance | VirtualInstance,291> = new Map();292293let focusedActivityID: null | FiberInstance['id'] = null;294let focusedActivity: null | Fiber = null;295296const idToSuspenseNodeMap: Map<FiberInstance['id'], SuspenseNode> = new Map();297298// Map of canonical HostInstances to the nearest parent DevToolsInstance.299const publicInstanceToDevToolsInstanceMap: Map<HostInstance, DevToolsInstance> =300 new Map();301// Map of resource DOM nodes to all the nearest DevToolsInstances that depend on it.302const hostResourceToDevToolsInstanceMap: Map<303 HostInstance,304 Set<DevToolsInstance>,305> = new Map();306307function aquireHostInstance(308 nearestInstance: DevToolsInstance,309 hostInstance: HostInstance,310): void {311 const publicInstance = getPublicInstance(hostInstance);312 publicInstanceToDevToolsInstanceMap.set(publicInstance, nearestInstance);313}314315function releaseHostInstance(316 nearestInstance: DevToolsInstance,317 hostInstance: HostInstance,318): void {319 const publicInstance = getPublicInstance(hostInstance);320 if (321 publicInstanceToDevToolsInstanceMap.get(publicInstance) === nearestInstance322 ) {323 publicInstanceToDevToolsInstanceMap.delete(publicInstance);324 }325}326327function aquireHostResource(328 nearestInstance: DevToolsInstance,329 resource: ?{instance?: HostInstance},330): void {331 const hostInstance = resource && resource.instance;332 if (hostInstance) {333 const publicInstance = getPublicInstance(hostInstance);334 let resourceInstances =335 hostResourceToDevToolsInstanceMap.get(publicInstance);336 if (resourceInstances === undefined) {337 resourceInstances = new Set();338 hostResourceToDevToolsInstanceMap.set(publicInstance, resourceInstances);339 // Store the first match in the main map for quick access when selecting DOM node.340 publicInstanceToDevToolsInstanceMap.set(publicInstance, nearestInstance);341 }342 resourceInstances.add(nearestInstance);343 }344}345346function releaseHostResource(347 nearestInstance: DevToolsInstance,348 resource: ?{instance?: HostInstance},349): void {350 const hostInstance = resource && resource.instance;351 if (hostInstance) {352 const publicInstance = getPublicInstance(hostInstance);353 const resourceInstances =354 hostResourceToDevToolsInstanceMap.get(publicInstance);355 if (resourceInstances !== undefined) {356 resourceInstances.delete(nearestInstance);357 if (resourceInstances.size === 0) {358 hostResourceToDevToolsInstanceMap.delete(publicInstance);359 publicInstanceToDevToolsInstanceMap.delete(publicInstance);360 } else if (361 publicInstanceToDevToolsInstanceMap.get(publicInstance) ===362 nearestInstance363 ) {364 // This was the first one. Store the next first one in the main map for easy access.365 // eslint-disable-next-line no-for-of-loops/no-for-of-loops366 for (const firstInstance of resourceInstances) {367 publicInstanceToDevToolsInstanceMap.set(368 publicInstance,369 firstInstance,370 );371 break;372 }373 }374 }375 }376}377378export function attach(379 hook: DevToolsHook,380 rendererID: number,381 renderer: ReactRenderer,382 global: Object,383 shouldStartProfilingNow: boolean,384 profilingSettings: ProfilingSettings,385 componentFiltersOrComponentFiltersPromise:386 | Array<ComponentFilter>387 | Promise<Array<ComponentFilter>>,388): RendererInterface {389 // Newer versions of the reconciler package also specific reconciler version.390 // If that version number is present, use it.391 // Third party renderer versions may not match the reconciler version,392 // and the latter is what's important in terms of tags and symbols.393 const version = renderer.reconcilerVersion || renderer.version;394395 const {396 getDisplayNameForFiber,397 getTypeSymbol,398 ReactPriorityLevels,399 ReactTypeOfWork,400 StrictModeBits,401 SuspenseyImagesMode,402 } = getInternalReactConstants(version);403 const {404 ActivityComponent,405 ClassComponent,406 DehydratedSuspenseComponent,407 ForwardRef,408 Fragment,409 FunctionComponent,410 HostRoot,411 HostHoistable,412 HostSingleton,413 HostPortal,414 HostComponent,415 HostText,416 IncompleteClassComponent,417 IncompleteFunctionComponent,418 IndeterminateComponent,419 LegacyHiddenComponent,420 MemoComponent,421 OffscreenComponent,422 SimpleMemoComponent,423 SuspenseComponent,424 SuspenseListComponent,425 TracingMarkerComponent,426 Throw,427 ViewTransitionComponent,428 } = ReactTypeOfWork;429 const {430 ImmediatePriority,431 UserBlockingPriority,432 NormalPriority,433 LowPriority,434 IdlePriority,435 NoPriority,436 } = ReactPriorityLevels;437438 const {439 getLaneLabelMap,440 injectProfilingHooks,441 overrideHookState,442 overrideHookStateDeletePath,443 overrideHookStateRenamePath,444 overrideProps,445 overridePropsDeletePath,446 overridePropsRenamePath,447 scheduleRefresh,448 setErrorHandler,449 setSuspenseHandler,450 scheduleUpdate,451 scheduleRetry,452 getCurrentFiber,453 } = renderer;454 const supportsTogglingError =455 typeof setErrorHandler === 'function' &&456 typeof scheduleUpdate === 'function';457 const supportsTogglingSuspense =458 typeof setSuspenseHandler === 'function' &&459 typeof scheduleUpdate === 'function';460 const supportsPerformanceTracks = gte(version, '19.2.0');461462 if (typeof scheduleRefresh === 'function') {463 // When Fast Refresh updates a component, the frontend may need to purge cached information.464 // For example, ASTs cached for the component (for named hooks) may no longer be valid.465 // Send a signal to the frontend to purge this cached information.466 // The "fastRefreshScheduled" dispatched is global (not Fiber or even Renderer specific).467 // This is less effecient since it means the front-end will need to purge the entire cache,468 // but this is probably an okay trade off in order to reduce coupling between the DevTools and Fast Refresh.469 renderer.scheduleRefresh = (...args) => {470 try {471 hook.emit('fastRefreshScheduled');472 } finally {473 return scheduleRefresh(...args);474 }475 };476 }477478 let getTimelineData: null | GetTimelineData = null;479 let toggleProfilingStatus: null | ToggleProfilingStatus = null;480 if (typeof injectProfilingHooks === 'function') {481 const response = createProfilingHooks({482 getDisplayNameForFiber,483 getIsProfiling: () => isProfiling,484 getLaneLabelMap,485 currentDispatcherRef: getDispatcherRef(renderer),486 workTagMap: ReactTypeOfWork,487 reactVersion: version,488 });489490 // Pass the Profiling hooks to the reconciler for it to call during render.491 injectProfilingHooks(response.profilingHooks);492493 // Hang onto this toggle so we can notify the external methods of profiling status changes.494 getTimelineData = response.getTimelineData;495 toggleProfilingStatus = response.toggleProfilingStatus;496 }497498 type ComponentLogs = {499 errors: Map<string, number>,500 errorsCount: number,501 warnings: Map<string, number>,502 warningsCount: number,503 };504 // Tracks Errors/Warnings logs added to a Fiber. They are added before the commit and get505 // picked up a FiberInstance. This keeps it around as long as the Fiber is alive which506 // lets the Fiber get reparented/remounted and still observe the previous errors/warnings.507 // Unless we explicitly clear the logs from a Fiber.508 const fiberToComponentLogsMap: WeakMap<Fiber, ComponentLogs> = new WeakMap();509 // Tracks whether we've performed a commit since the last log. This is used to know510 // whether we received any new logs between the commit and post commit phases. I.e.511 // if any passive effects called console.warn / console.error.512 let needsToFlushComponentLogs = false;513514 function bruteForceFlushErrorsAndWarnings(root: FiberInstance) {515 // Refresh error/warning count for all mounted unfiltered Fibers.516 let hasChanges = false;517 // eslint-disable-next-line no-for-of-loops/no-for-of-loops518 for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {519 if (devtoolsInstance.kind === FIBER_INSTANCE) {520 const fiber = devtoolsInstance.data;521 const componentLogsEntry = fiberToComponentLogsMap.get(fiber);522 const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry);523 if (changed) {524 hasChanges = true;525 updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);526 }527 } else {528 // Virtual Instances cannot log in passive effects and so never appear here.529 }530 }531 if (hasChanges) {532 flushPendingEvents(root);533 }534 }535536 function clearErrorsAndWarnings() {537 // Note, this only clears logs for Fibers that have instances. If they're filtered538 // and then mount, the logs are there. Ensuring we only clear what you've seen.539 // If we wanted to clear the whole set, we'd replace fiberToComponentLogsMap with a540 // new WeakMap. It's unclear whether we should clear componentInfoToComponentLogsMap541 // since it's shared by other renderers but presumably it would.542543 // eslint-disable-next-line no-for-of-loops/no-for-of-loops544 for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {545 if (devtoolsInstance.kind === FIBER_INSTANCE) {546 const fiber = devtoolsInstance.data;547 fiberToComponentLogsMap.delete(fiber);548 if (fiber.alternate) {549 fiberToComponentLogsMap.delete(fiber.alternate);550 }551 } else {552 componentInfoToComponentLogsMap.delete(devtoolsInstance.data);553 }554 const changed = recordConsoleLogs(devtoolsInstance, undefined);555 if (changed) {556 updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);557 }558 }559 flushPendingEvents(null);560 }561562 function clearConsoleLogsHelper(instanceID: number, type: 'error' | 'warn') {563 const devtoolsInstance = idToDevToolsInstanceMap.get(instanceID);564 if (devtoolsInstance !== undefined) {565 let componentLogsEntry;566 if (devtoolsInstance.kind === FIBER_INSTANCE) {567 const fiber = devtoolsInstance.data;568 componentLogsEntry = fiberToComponentLogsMap.get(fiber);569570 if (componentLogsEntry === undefined && fiber.alternate !== null) {571 componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);572 }573 } else {574 const componentInfo = devtoolsInstance.data;575 componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo);576 }577 if (componentLogsEntry !== undefined) {578 if (type === 'error') {579 componentLogsEntry.errors.clear();580 componentLogsEntry.errorsCount = 0;581 } else {582 componentLogsEntry.warnings.clear();583 componentLogsEntry.warningsCount = 0;584 }585 const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry);586 if (changed) {587 flushPendingEvents(null);588 updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);589 }590 }591 }592 }593594 function clearErrorsForElementID(instanceID: number) {595 clearConsoleLogsHelper(instanceID, 'error');596 }597598 function clearWarningsForElementID(instanceID: number) {599 clearConsoleLogsHelper(instanceID, 'warn');600 }601602 function updateMostRecentlyInspectedElementIfNecessary(603 fiberID: number,604 ): void {605 if (606 mostRecentlyInspectedElement !== null &&607 mostRecentlyInspectedElement.id === fiberID608 ) {609 hasElementUpdatedSinceLastInspected = true;610 }611 }612613 function getComponentStack(614 topFrame: Error,615 ): null | {enableOwnerStacks: boolean, componentStack: string} {616 if (getCurrentFiber == null) {617 // Expected this to be part of the renderer. Ignore.618 return null;619 }620 const current = getCurrentFiber();621 if (current === null) {622 // Outside of our render scope.623 return null;624 }625626 if (supportsConsoleTasks(current)) {627 // This will be handled natively by console.createTask. No need for628 // DevTools to add it.629 return null;630 }631632 const dispatcherRef = getDispatcherRef(renderer);633 if (dispatcherRef === undefined) {634 return null;635 }636637 const enableOwnerStacks = supportsOwnerStacks(current);638 let componentStack = '';639 if (enableOwnerStacks) {640 // Prefix the owner stack with the current stack. I.e. what called641 // console.error. While this will also be part of the native stack,642 // it is hidden and not presented alongside this argument so we print643 // them all together.644 const topStackFrames = formatOwnerStack(topFrame);645 if (topStackFrames) {646 componentStack += '\n' + topStackFrames;647 }648 componentStack += getOwnerStackByFiberInDev(649 ReactTypeOfWork,650 current,651 dispatcherRef,652 );653 } else {654 componentStack = getStackByFiberInDevAndProd(655 ReactTypeOfWork,656 current,657 dispatcherRef,658 );659 }660 return {enableOwnerStacks, componentStack};661 }662663 // Called when an error or warning is logged during render, commit, or passive (including unmount functions).664 function onErrorOrWarning(665 type: 'error' | 'warn',666 args: $ReadOnlyArray<any>,667 ): void {668 if (getCurrentFiber == null) {669 // Expected this to be part of the renderer. Ignore.670 return;671 }672 const fiber = getCurrentFiber();673 if (fiber === null) {674 // Outside of our render scope.675 return;676 }677 if (type === 'error') {678 // if this is an error simulated by us to trigger error boundary, ignore679 if (680 forceErrorForFibers.get(fiber) === true ||681 (fiber.alternate !== null &&682 forceErrorForFibers.get(fiber.alternate) === true)683 ) {684 return;685 }686 }687688 // We can't really use this message as a unique key, since we can't distinguish689 // different objects in this implementation. We have to delegate displaying of the objects690 // to the environment, the browser console, for example, so this is why this should be kept691 // as an array of arguments, instead of the plain string.692 // [Warning: %o, {...}] and [Warning: %o, {...}] will be considered as the same message,693 // even if objects are different694 const message = formatConsoleArgumentsToSingleString(...args);695696 // Track the warning/error for later.697 let componentLogsEntry = fiberToComponentLogsMap.get(fiber);698 if (componentLogsEntry === undefined && fiber.alternate !== null) {699 componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);700 if (componentLogsEntry !== undefined) {701 // Use the same set for both Fibers.702 fiberToComponentLogsMap.set(fiber, componentLogsEntry);703 }704 }705 if (componentLogsEntry === undefined) {706 componentLogsEntry = {707 errors: new Map(),708 errorsCount: 0 as number,709 warnings: new Map(),710 warningsCount: 0 as number,711 };712 fiberToComponentLogsMap.set(fiber, componentLogsEntry);713 }714715 const messageMap =716 type === 'error'717 ? componentLogsEntry.errors718 : componentLogsEntry.warnings;719 const count = messageMap.get(message) || 0;720 messageMap.set(message, count + 1);721 if (type === 'error') {722 componentLogsEntry.errorsCount++;723 } else {724 componentLogsEntry.warningsCount++;725 }726727 // The changes will be flushed later when we commit.728729 // If the log happened in a passive effect, then this happens after we've730 // already committed the new tree so the change won't show up until we rerender731 // that component again. We need to visit a Component with passive effects in732 // handlePostCommitFiberRoot again to ensure that we flush the changes after passive.733 needsToFlushComponentLogs = true;734 }735736 function debug(737 name: string,738 instance: DevToolsInstance,739 parentInstance: null | DevToolsInstance,740 extraString: string = '',741 ): void {742 // $FlowFixMe[constant-condition]743 if (__DEBUG__) {744 const displayName =745 instance.kind === VIRTUAL_INSTANCE746 ? instance.data.name || 'null'747 : instance.data.tag +748 ':' +749 (getDisplayNameForFiber(instance.data) || 'null');750751 const maybeID =752 instance.kind === FILTERED_FIBER_INSTANCE ? '<no id>' : instance.id;753754 const parentDisplayName =755 parentInstance === null756 ? ''757 : parentInstance.kind === VIRTUAL_INSTANCE758 ? parentInstance.data.name || 'null'759 : parentInstance.data.tag +760 ':' +761 (getDisplayNameForFiber(parentInstance.data) || 'null');762763 const maybeParentID =764 parentInstance === null ||765 parentInstance.kind === FILTERED_FIBER_INSTANCE766 ? '<no id>'767 : parentInstance.id;768769 console.groupCollapsed(770 `[renderer] %c${name} %c${displayName} (${maybeID}) %c${771 parentInstance ? `${parentDisplayName} (${maybeParentID})` : ''772 } %c${extraString}`,773 'color: red; font-weight: bold;',774 'color: blue;',775 'color: purple;',776 'color: black;',777 );778 console.log(new Error().stack.split('\n').slice(1).join('\n'));779 console.groupEnd();780 }781 }782783 // eslint-disable-next-line no-unused-vars784 function debugTree(instance: DevToolsInstance, indent: number = 0) {785 // $FlowFixMe[constant-condition]786 if (__DEBUG__) {787 const name =788 (instance.kind !== VIRTUAL_INSTANCE789 ? getDisplayNameForFiber(instance.data)790 : instance.data.name) || '';791 console.log(792 ' '.repeat(indent) +793 '- ' +794 (instance.kind === FILTERED_FIBER_INSTANCE ? 0 : instance.id) +795 ' (' +796 name +797 ')',798 'parent',799 instance.parent === null800 ? ' '801 : instance.parent.kind === FILTERED_FIBER_INSTANCE802 ? 0803 : instance.parent.id,804 'next',805 instance.nextSibling === null ? ' ' : instance.nextSibling.id,806 );807 let child = instance.firstChild;808 while (child !== null) {809 debugTree(child, indent + 1);810 child = child.nextSibling;811 }812 }813 }814815 // Configurable Components tree filters.816 const hideElementsWithDisplayNames: Set<RegExp> = new Set();817 const hideElementsWithPaths: Set<RegExp> = new Set();818 const hideElementsWithTypes: Set<ElementType> = new Set();819 const hideElementsWithEnvs: Set<string> = new Set();820 let isInFocusedActivity: boolean = true;821822 // Highlight updates823 let traceUpdatesEnabled: boolean = false;824 const traceUpdatesForNodes: Set<HostInstance> = new Set();825826 function applyComponentFilters(827 componentFilters: Array<ComponentFilter>,828 nextActivitySlice: null | Fiber,829 ) {830 hideElementsWithTypes.clear();831 hideElementsWithDisplayNames.clear();832 hideElementsWithPaths.clear();833 hideElementsWithEnvs.clear();834 const previousFocusedActivityID = focusedActivityID;835 focusedActivityID = null;836 focusedActivity = null;837 // Consider everything in the slice by default838 isInFocusedActivity = true;839840 componentFilters.forEach(componentFilter => {841 if (!componentFilter.isEnabled) {842 return;843 }844845 switch (componentFilter.type) {846 case ComponentFilterDisplayName:847 if (componentFilter.isValid && componentFilter.value !== '') {848 hideElementsWithDisplayNames.add(849 new RegExp(componentFilter.value, 'i'),850 );851 }852 break;853 case ComponentFilterElementType:854 hideElementsWithTypes.add(componentFilter.value);855 break;856 case ComponentFilterLocation:857 if (componentFilter.isValid && componentFilter.value !== '') {858 hideElementsWithPaths.add(new RegExp(componentFilter.value, 'i'));859 }860 break;861 case ComponentFilterHOC:862 hideElementsWithDisplayNames.add(new RegExp('\\('));863 break;864 case ComponentFilterEnvironmentName:865 hideElementsWithEnvs.add(componentFilter.value);866 break;867 case ComponentFilterActivitySlice:868 if (869 nextActivitySlice !== null &&870 nextActivitySlice.tag === ActivityComponent871 ) {872 focusedActivity = nextActivitySlice;873 isInFocusedActivity = false;874 if (componentFilter.rendererID !== rendererID) {875 // We filtered an Activity from another renderer.876 // We need to restore the instance ID since we won't be mounting it877 // in this renderer.878 focusedActivityID = previousFocusedActivityID;879 }880 } else {881 // We're not filtering by activity slice after all.882 // Don't mark the filter as disabled here.883 // Otherwise updateComponentFilters() will think no enabled filter was changed.884 }885 break;886 default:887 console.warn(888 `Invalid component filter type "${componentFilter.type}"`,889 );890 break;891 }892 });893 }894895 if (Array.isArray(componentFiltersOrComponentFiltersPromise)) {896 applyComponentFilters(componentFiltersOrComponentFiltersPromise, null);897 } else {898 componentFiltersOrComponentFiltersPromise.then(componentFilters => {899 applyComponentFilters(componentFilters, null);900 });901 }902903 // If necessary, we can revisit optimizing this operation.904 // For example, we could add a new recursive unmount tree operation.905 // The unmount operations are already significantly smaller than mount operations though.906 // This is something to keep in mind for later.907 function updateComponentFilters(componentFilters: Array<ComponentFilter>) {908 if (isProfiling) {909 // Re-mounting a tree while profiling is in progress might break a lot of assumptions.910 // If necessary, we could support this- but it doesn't seem like a necessary use case.911 // Supporting change of filters while profiling would require a refactor912 // to flush after each root instead of at the end.913 throw Error('Cannot modify filter preferences while profiling');914 }915916 const previousForcedFallbacks =917 forceFallbackForFibers.size > 0 ? new Set(forceFallbackForFibers) : null;918 const previousForcedErrors =919 forceErrorForFibers.size > 0 ? new Map(forceErrorForFibers) : null;920921 // The ID will be based on the old tree. We need to find the Fiber based on922 // that ID before we unmount everything. We set the activity slice ID once923 // we mount it again.924 let nextFocusedActivity: null | Fiber = null;925 let focusedActivityFilter: null | ActivitySliceFilter = null;926 for (let i = 0; i < componentFilters.length; i++) {927 const filter = componentFilters[i];928 if (filter.type === ComponentFilterActivitySlice && filter.isEnabled) {929 focusedActivityFilter = filter;930 const instance = idToDevToolsInstanceMap.get(filter.activityID);931 if (instance !== undefined && instance.kind === FIBER_INSTANCE) {932 nextFocusedActivity = instance.data;933 }934 }935 }936937 // Recursively unmount all roots.938 hook.getFiberRoots(rendererID).forEach(root => {939 const rootInstance = rootToFiberInstanceMap.get(root);940 if (rootInstance === undefined) {941 throw new Error(942 'Expected the root instance to already exist when applying filters',943 );944 }945 currentRoot = rootInstance;946 unmountInstanceRecursively(rootInstance);947 rootToFiberInstanceMap.delete(root);948 currentRoot = null as any;949 });950951 if (952 nextFocusedActivity !== focusedActivity &&953 (focusedActivityFilter === null ||954 focusedActivityFilter.rendererID === rendererID)955 ) {956 // When we find the applied instance during mount we will send the actual ID.957 // Otherwise 0 will indicate that we unfocused the activity slice.958 pushOperation(TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE);959 pushOperation(0);960 }961 applyComponentFilters(componentFilters, nextFocusedActivity);962963 // Reset pseudo counters so that new path selections will be persisted.964 rootDisplayNameCounter.clear();965966 // We just cleared all the forced states. Schedule updates on the affected Fibers967 // so that we get their initial states again according to the new filters.968 if (typeof scheduleUpdate === 'function') {969 if (previousForcedFallbacks !== null) {970 // eslint-disable-next-line no-for-of-loops/no-for-of-loops971 for (const fiber of previousForcedFallbacks) {972 if (typeof scheduleRetry === 'function') {973 scheduleRetry(fiber);974 } else {975 scheduleUpdate(fiber);976 }977 }978 }979 if (980 previousForcedErrors !== null &&981 typeof setErrorHandler === 'function'982 ) {983 // Unlike for Suspense, disabling the forced error state requires setting984 // the status to false first. `shouldErrorFiberAccordingToMap` will clear985 // the Fibers later.986 setErrorHandler(shouldErrorFiberAccordingToMap);987 // eslint-disable-next-line no-for-of-loops/no-for-of-loops988 for (const [fiber, shouldError] of previousForcedErrors) {989 forceErrorForFibers.set(fiber, false);990 if (shouldError) {991 if (typeof scheduleRetry === 'function') {992 scheduleRetry(fiber);993 } else {994 scheduleUpdate(fiber);995 }996 }997 }998 }999 }10001001 // Recursively re-mount all roots with new filter criteria applied.1002 hook.getFiberRoots(rendererID).forEach(root => {1003 const current = root.current;1004 const newRoot = createFiberInstance(current);1005 rootToFiberInstanceMap.set(root, newRoot);1006 idToDevToolsInstanceMap.set(newRoot.id, newRoot);10071008 // Before the traversals, remember to start tracking1009 // our path in case we have selection to restore.1010 if (trackedPath !== null) {1011 mightBeOnTrackedPath = true;1012 }10131014 currentRoot = newRoot;1015 setRootPseudoKey(currentRoot.id, root.current);1016 mountFiberRecursively(root.current, false);1017 currentRoot = null as any;1018 });10191020 // We need to write back the new ID for the focused Fiber.1021 // Otherwise subsequent filter applications will try to focus based on the old ID.1022 // This is also relevant to filter across renderers.1023 if (focusedActivityFilter !== null && focusedActivityID !== null) {1024 focusedActivityFilter.activityID = focusedActivityID;1025 }10261027 // We're not profiling so it's safe to flush without a specific root.1028 flushPendingEvents(null);10291030 needsToFlushComponentLogs = false;1031 }10321033 function getEnvironmentNames(): Array<string> {1034 return Array.from(knownEnvironmentNames);1035 }10361037 function isFiberHydrated(fiber: Fiber): boolean {1038 if (OffscreenComponent === -1) {1039 throw new Error('not implemented for legacy suspense');1040 }1041 switch (fiber.tag) {1042 case HostRoot:1043 const rootState = fiber.memoizedState;1044 return !rootState.isDehydrated;1045 case SuspenseComponent:1046 const suspenseState = fiber.memoizedState;1047 return suspenseState === null || suspenseState.dehydrated === null;1048 default:1049 throw new Error('not implemented for work tag ' + fiber.tag);1050 }1051 }10521053 function shouldFilterVirtual(1054 data: ReactComponentInfo,1055 secondaryEnv: null | string,1056 ): boolean {1057 if (!isInFocusedActivity) {1058 return true;1059 }10601061 // For purposes of filtering Server Components are always Function Components.1062 // Environment will be used to filter Server vs Client.1063 // Technically they can be forwardRef and memo too but those filters will go away1064 // as those become just plain user space function components like any HoC.1065 if (hideElementsWithTypes.has(ElementTypeFunction)) {1066 return true;1067 }10681069 if (hideElementsWithDisplayNames.size > 0) {1070 const displayName = data.name;1071 if (displayName != null) {1072 // eslint-disable-next-line no-for-of-loops/no-for-of-loops1073 for (const displayNameRegExp of hideElementsWithDisplayNames) {1074 if (displayNameRegExp.test(displayName)) {1075 return true;1076 }1077 }1078 }1079 }10801081 if (1082 (data.env == null || hideElementsWithEnvs.has(data.env)) &&1083 (secondaryEnv === null || hideElementsWithEnvs.has(secondaryEnv))1084 ) {1085 // If a Component has two environments, you have to filter both for it not to appear.1086 return true;1087 }10881089 return false;1090 }10911092 // NOTICE Keep in sync with get*ForFiber methods1093 function shouldFilterFiber(fiber: Fiber): boolean {1094 const {tag, type, key} = fiber;10951096 // It is never valid to filter the root element.1097 if (tag !== HostRoot && !isInFocusedActivity) {1098 return true;1099 }11001101 switch (tag) {1102 case DehydratedSuspenseComponent:1103 // TODO: ideally we would show dehydrated Suspense immediately.1104 // However, it has some special behavior (like disconnecting1105 // an alternate and turning into real Suspense) which breaks DevTools.1106 // For now, ignore it, and only show it once it gets hydrated.1107 // https://github.com/bvaughn/react-devtools-experimental/issues/1971108 return true;1109 case HostPortal:1110 case HostText:1111 case LegacyHiddenComponent:1112 case OffscreenComponent:1113 case Throw:1114 return true;1115 case HostRoot:1116 // It is never valid to filter the root element.1117 return false;1118 case Fragment:1119 return key === null;1120 default:1121 const typeSymbol = getTypeSymbol(type);11221123 switch (typeSymbol) {1124 case CONCURRENT_MODE_NUMBER:1125 case CONCURRENT_MODE_SYMBOL_STRING:1126 case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:1127 case STRICT_MODE_NUMBER:1128 case STRICT_MODE_SYMBOL_STRING:1129 return true;1130 default:1131 break;1132 }1133 }11341135 const elementType = getElementTypeForFiber(fiber);1136 if (hideElementsWithTypes.has(elementType)) {1137 return true;1138 }11391140 if (hideElementsWithDisplayNames.size > 0) {1141 const displayName = getDisplayNameForFiber(fiber);1142 if (displayName != null) {1143 // eslint-disable-next-line no-for-of-loops/no-for-of-loops1144 for (const displayNameRegExp of hideElementsWithDisplayNames) {1145 if (displayNameRegExp.test(displayName)) {1146 return true;1147 }1148 }1149 }1150 }11511152 if (hideElementsWithEnvs.has('Client')) {1153 // If we're filtering out the Client environment we should filter out all1154 // "Client Components". Technically that also includes the built-ins but1155 // since that doesn't actually include any additional code loading it's1156 // useful to not filter out the built-ins. Those can be filtered separately.1157 // There's no other way to filter out just Function components on the Client.1158 // Therefore, this only filters Class and Function components.1159 switch (tag) {1160 case ClassComponent:1161 case IncompleteClassComponent:1162 case IncompleteFunctionComponent:1163 case FunctionComponent:1164 case IndeterminateComponent:1165 case ForwardRef:1166 case MemoComponent:1167 case SimpleMemoComponent:1168 return true;1169 }1170 }11711172 /* DISABLED: https://github.com/facebook/react/pull/284171173 if (hideElementsWithPaths.size > 0) {1174 const source = getSourceForFiber(fiber);11751176 if (source != null) {1177 const {fileName} = source;1178 // eslint-disable-next-line no-for-of-loops/no-for-of-loops1179 for (const pathRegExp of hideElementsWithPaths) {1180 if (pathRegExp.test(fileName)) {1181 return true;1182 }1183 }1184 }1185 }1186 */11871188 return false;1189 }11901191 // NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods1192 function getElementTypeForFiber(fiber: Fiber): ElementType {1193 const {type, tag} = fiber;11941195 switch (tag) {1196 case ActivityComponent:1197 return ElementTypeActivity;1198 case ClassComponent:1199 case IncompleteClassComponent:1200 return ElementTypeClass;1201 case IncompleteFunctionComponent:1202 case FunctionComponent:1203 case IndeterminateComponent:1204 return ElementTypeFunction;1205 case ForwardRef:1206 return ElementTypeForwardRef;1207 case HostRoot:1208 return ElementTypeRoot;1209 case HostComponent:1210 case HostHoistable:1211 case HostSingleton:1212 return ElementTypeHostComponent;1213 case HostPortal:1214 case HostText:1215 case Fragment:1216 return ElementTypeOtherOrUnknown;1217 case MemoComponent:1218 case SimpleMemoComponent:1219 return ElementTypeMemo;1220 case SuspenseComponent:1221 return ElementTypeSuspense;1222 case SuspenseListComponent:1223 return ElementTypeSuspenseList;1224 case TracingMarkerComponent:1225 return ElementTypeTracingMarker;1226 case ViewTransitionComponent:1227 return ElementTypeViewTransition;1228 default:1229 const typeSymbol = getTypeSymbol(type);12301231 switch (typeSymbol) {1232 case CONCURRENT_MODE_NUMBER:1233 case CONCURRENT_MODE_SYMBOL_STRING:1234 case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:1235 return ElementTypeOtherOrUnknown;1236 case PROVIDER_NUMBER:1237 case PROVIDER_SYMBOL_STRING:1238 return ElementTypeContext;1239 case CONTEXT_NUMBER:1240 case CONTEXT_SYMBOL_STRING:1241 return ElementTypeContext;1242 case STRICT_MODE_NUMBER:1243 case STRICT_MODE_SYMBOL_STRING:1244 return ElementTypeOtherOrUnknown;1245 case PROFILER_NUMBER:1246 case PROFILER_SYMBOL_STRING:1247 return ElementTypeProfiler;1248 default:1249 return ElementTypeOtherOrUnknown;1250 }1251 }1252 }12531254 // When a mount or update is in progress, this value tracks the root that is being operated on.1255 let currentRoot: FiberInstance = null as any;12561257 // Removes a Fiber (and its alternate) from the Maps used to track their id.1258 // This method should always be called when a Fiber is unmounting.1259 function untrackFiber(nearestInstance: DevToolsInstance, fiber: Fiber) {1260 if (forceErrorForFibers.size > 0) {1261 forceErrorForFibers.delete(fiber);1262 if (fiber.alternate) {1263 forceErrorForFibers.delete(fiber.alternate);1264 }1265 if (forceErrorForFibers.size === 0 && setErrorHandler != null) {1266 setErrorHandler(shouldErrorFiberAlwaysNull);1267 }1268 }12691270 if (forceFallbackForFibers.size > 0) {1271 forceFallbackForFibers.delete(fiber);1272 if (fiber.alternate) {1273 forceFallbackForFibers.delete(fiber.alternate);1274 }1275 if (forceFallbackForFibers.size === 0 && setSuspenseHandler != null) {1276 setSuspenseHandler(shouldSuspendFiberAlwaysFalse);1277 }1278 }12791280 // TODO: Consider using a WeakMap instead. The only thing where that doesn't work1281 // is React Native Paper which tracks tags but that support is eventually going away1282 // and can use the old findFiberByHostInstance strategy.12831284 if (fiber.tag === HostHoistable) {1285 releaseHostResource(nearestInstance, fiber.memoizedState);1286 } else if (1287 fiber.tag === HostComponent ||1288 fiber.tag === HostText ||1289 fiber.tag === HostSingleton1290 ) {1291 releaseHostInstance(nearestInstance, fiber.stateNode);1292 }12931294 // Recursively clean up any filtered Fibers below this one as well since1295 // we won't recordUnmount on those.1296 for (let child = fiber.child; child !== null; child = child.sibling) {1297 if (shouldFilterFiber(child)) {1298 untrackFiber(nearestInstance, child);1299 }1300 }1301 }13021303 function getChangeDescription(1304 prevFiber: Fiber | null,1305 nextFiber: Fiber,1306 ): ChangeDescription | null {1307 switch (nextFiber.tag) {1308 case ClassComponent:1309 if (prevFiber === null) {1310 return {1311 context: null,1312 didHooksChange: false,1313 isFirstMount: true,1314 props: null,1315 state: null,1316 };1317 } else {1318 const data: ChangeDescription = {1319 context: getContextChanged(prevFiber, nextFiber),1320 didHooksChange: false,1321 isFirstMount: false,1322 props: getChangedKeys(1323 prevFiber.memoizedProps,1324 nextFiber.memoizedProps,1325 ),1326 state: getChangedKeys(1327 prevFiber.memoizedState,1328 nextFiber.memoizedState,1329 ),1330 };1331 return data;1332 }1333 case IncompleteFunctionComponent:1334 case FunctionComponent:1335 case IndeterminateComponent:1336 case ForwardRef:1337 case MemoComponent:1338 case SimpleMemoComponent:1339 if (prevFiber === null) {1340 return {1341 context: null,1342 didHooksChange: false,1343 isFirstMount: true,1344 props: null,1345 state: null,1346 };1347 } else {1348 const prevHooks = inspectHooks(prevFiber);1349 const nextHooks = inspectHooks(nextFiber);1350 const indices = getChangedHooksIndices(prevHooks, nextHooks);1351 const data: ChangeDescription = {1352 context: getContextChanged(prevFiber, nextFiber),1353 didHooksChange: indices !== null && indices.length > 0,1354 isFirstMount: false,1355 props: getChangedKeys(1356 prevFiber.memoizedProps,1357 nextFiber.memoizedProps,1358 ),1359 state: null,1360 hooks: indices,1361 };1362 // Only traverse the hooks list once, depending on what info we're returning.1363 return data;1364 }1365 default:1366 return null;1367 }1368 }13691370 type OperationsArray = Array<number>;13711372 type StringTableEntry = {1373 encodedString: Array<number>,1374 id: number,1375 };13761377 const pendingOperations: OperationsArray = [];1378 const pendingRealUnmountedIDs: Array<FiberInstance['id']> = [];1379 const pendingRealUnmountedSuspenseIDs: Array<FiberInstance['id']> = [];1380 const pendingSuspenderChanges: Set<FiberInstance['id']> = new Set();1381 let pendingOperationsQueue: Array<OperationsArray> | null = [];1382 const pendingStringTable: Map<string, StringTableEntry> = new Map();1383 let pendingStringTableLength: number = 0;13841385 function pushOperation(op: number): void {1386 if (__DEV__) {1387 if (!Number.isInteger(op)) {1388 console.error(1389 'pushOperation() was called but the value is not an integer.',1390 op,1391 );1392 }1393 }1394 pendingOperations.push(op);1395 }13961397 function shouldBailoutWithPendingOperations() {1398 if (isProfiling) {1399 if (1400 currentCommitProfilingMetadata != null &&1401 currentCommitProfilingMetadata.durations.length > 01402 ) {1403 return false;1404 }1405 }14061407 return (1408 pendingOperations.length === 0 &&1409 pendingRealUnmountedIDs.length === 0 &&1410 pendingRealUnmountedSuspenseIDs.length === 0 &&1411 pendingSuspenderChanges.size === 01412 );1413 }14141415 function flushOrQueueOperations(operations: OperationsArray): void {1416 if (shouldBailoutWithPendingOperations()) {1417 return;1418 }14191420 if (pendingOperationsQueue !== null) {1421 pendingOperationsQueue.push(operations);1422 } else {1423 hook.emit('operations', operations);1424 }1425 }14261427 function recordConsoleLogs(1428 instance: FiberInstance | VirtualInstance,1429 componentLogsEntry: void | ComponentLogs,1430 ): boolean {1431 if (componentLogsEntry === undefined) {1432 if (instance.logCount === 0) {1433 // Nothing has changed.1434 return false;1435 }1436 // Reset to zero.1437 instance.logCount = 0;1438 pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);1439 pushOperation(instance.id);1440 pushOperation(0);1441 pushOperation(0);1442 return true;1443 } else {1444 const totalCount =1445 componentLogsEntry.errorsCount + componentLogsEntry.warningsCount;1446 if (instance.logCount === totalCount) {1447 // Nothing has changed.1448 return false;1449 }1450 // Update counts.1451 instance.logCount = totalCount;1452 pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);1453 pushOperation(instance.id);1454 pushOperation(componentLogsEntry.errorsCount);1455 pushOperation(componentLogsEntry.warningsCount);1456 return true;1457 }1458 }14591460 /**1461 * Allowed to flush pending events without a specific root when:1462 * - pending operations don't record tree mutations e.g. TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS1463 * - not profiling (the commit tree builder requires the root of the mutations)1464 */1465 function flushPendingEvents(root: FiberInstance | null): void {1466 if (shouldBailoutWithPendingOperations()) {1467 // If we aren't profiling, we can just bail out here.1468 // No use sending an empty update over the bridge.1469 //1470 // The Profiler stores metadata for each commit and reconstructs the app tree per commit using:1471 // (1) an initial tree snapshot and1472 // (2) the operations array for each commit1473 // Because of this, it's important that the operations and metadata arrays align,1474 // So it's important not to omit even empty operations while profiling is active.1475 return;1476 }14771478 const numUnmountIDs = pendingRealUnmountedIDs.length;1479 const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;1480 const numSuspenderChanges = pendingSuspenderChanges.size;14811482 const operations = new Array<number>(1483 // Identify which renderer this update is coming from.1484 2 + // [rendererID, rootFiberID]1485 // How big is the string table?1486 1 + // [stringTableLength]1487 // Then goes the actual string table.1488 pendingStringTableLength +1489 // All unmounts of Suspense boundaries are batched in a single message.1490 // [TREE_OPERATION_REMOVE_SUSPENSE, removedSuspenseIDLength, ...ids]1491 (numUnmountSuspenseIDs > 0 ? 2 + numUnmountSuspenseIDs : 0) +1492 // All unmounts are batched in a single message.1493 // [TREE_OPERATION_REMOVE, removedIDLength, ...ids]1494 (numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +1495 // Regular operations1496 pendingOperations.length +1497 // All suspender changes are batched in a single message.1498 // [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders, endTime, isSuspended]]1499 (numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 4 : 0),1500 );15011502 // Identify which renderer this update is coming from.1503 // This enables roots to be mapped to renderers,1504 // Which in turn enables fiber props, states, and hooks to be inspected.1505 let i = 0;1506 operations[i++] = rendererID;1507 if (root === null) {1508 operations[i++] = -1;1509 } else {1510 operations[i++] = root.id;1511 }15121513 // Now fill in the string table.1514 // [stringTableLength, str1Length, ...str1, str2Length, ...str2, ...]1515 operations[i++] = pendingStringTableLength;1516 pendingStringTable.forEach((entry, stringKey) => {1517 const encodedString = entry.encodedString;15181519 // Don't use the string length.1520 // It won't work for multibyte characters (like emoji).1521 const length = encodedString.length;15221523 operations[i++] = length;1524 for (let j = 0; j < length; j++) {1525 operations[i + j] = encodedString[j];1526 }15271528 i += length;1529 });15301531 if (numUnmountSuspenseIDs > 0) {1532 // All unmounts of Suspense boundaries are batched in a single message.1533 operations[i++] = SUSPENSE_TREE_OPERATION_REMOVE;1534 // The first number is how many unmounted IDs we're gonna send.1535 operations[i++] = numUnmountSuspenseIDs;1536 // Fill in the real unmounts in the reverse order.1537 // They were inserted parents-first by React, but we want children-first.1538 // So we traverse our array backwards.1539 for (let j = 0; j < pendingRealUnmountedSuspenseIDs.length; j++) {1540 operations[i++] = pendingRealUnmountedSuspenseIDs[j];1541 }1542 }15431544 if (numUnmountIDs > 0) {1545 // All unmounts except roots are batched in a single message.1546 operations[i++] = TREE_OPERATION_REMOVE;1547 // The first number is how many unmounted IDs we're gonna send.1548 operations[i++] = numUnmountIDs;1549 // Fill in the real unmounts in the reverse order.1550 // They were inserted parents-first by React, but we want children-first.1551 // So we traverse our array backwards.1552 for (let j = 0; j < pendingRealUnmountedIDs.length; j++) {1553 operations[i++] = pendingRealUnmountedIDs[j];1554 }1555 }15561557 // Fill in pending operations.1558 for (let j = 0; j < pendingOperations.length; j++) {1559 operations[i + j] = pendingOperations[j];1560 }1561 i += pendingOperations.length;15621563 // Suspender changes might affect newly mounted nodes that we already recorded1564 // in pending operations.1565 if (numSuspenderChanges > 0) {1566 operations[i++] = SUSPENSE_TREE_OPERATION_SUSPENDERS;1567 operations[i++] = numSuspenderChanges;1568 pendingSuspenderChanges.forEach(fiberIdWithChanges => {1569 const suspense = idToSuspenseNodeMap.get(fiberIdWithChanges);1570 if (suspense === undefined) {1571 // Probably forgot to cleanup pendingSuspenderChanges when this node was removed.1572 throw new Error(1573 `Could not send suspender changes for "${fiberIdWithChanges}" since the Fiber no longer exists.`,1574 );1575 }1576 operations[i++] = fiberIdWithChanges;1577 operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;1578 operations[i++] = Math.round(suspense.endTime * 1000);1579 const instance = suspense.instance;1580 const isSuspended =1581 // TODO: Track if other SuspenseNode like SuspenseList rows are suspended.1582 (instance.kind === FIBER_INSTANCE ||1583 instance.kind === FILTERED_FIBER_INSTANCE) &&1584 instance.data.tag === SuspenseComponent &&1585 instance.data.memoizedState !== null;1586 operations[i++] = isSuspended ? 1 : 0;1587 operations[i++] = suspense.environments.size;1588 suspense.environments.forEach((count, env) => {1589 operations[i++] = getStringID(env);1590 });1591 });1592 }15931594 // Let the frontend know about tree operations.1595 flushOrQueueOperations(operations);15961597 // Reset all of the pending state now that we've told the frontend about it.1598 pendingOperations.length = 0;1599 pendingRealUnmountedIDs.length = 0;1600 pendingRealUnmountedSuspenseIDs.length = 0;1601 pendingSuspenderChanges.clear();1602 pendingStringTable.clear();1603 pendingStringTableLength = 0;1604 }16051606 function measureHostInstance(instance: HostInstance): null | Array<Rect> {1607 // Feature detect measurement capabilities of this environment.1608 // TODO: Consider making this capability injected by the ReactRenderer.1609 if (typeof instance !== 'object' || instance === null) {1610 return null;1611 }1612 if (1613 typeof instance.getClientRects === 'function' ||1614 instance.nodeType === 31615 ) {1616 // DOM1617 const doc = instance.ownerDocument;1618 if (instance === doc.documentElement) {1619 // This is the document element. The size of this element is not actually1620 // what determines the whole scrollable area of the screen. Because any1621 // thing that overflows the document will also contribute to the scrollable.1622 // This is unlike overflow: scroll which clips those.1623 // Therefore, we use the scrollable size for this rect instead.1624 return [1625 {1626 x: 0,1627 y: 0,1628 width: instance.scrollWidth,1629 height: instance.scrollHeight,1630 },1631 ];1632 }1633 const result: Array<Rect> = [];1634 const win = doc && doc.defaultView;1635 const scrollX = win ? win.scrollX : 0;1636 const scrollY = win ? win.scrollY : 0;1637 let rects;1638 if (instance.nodeType === 3) {1639 // Text nodes cannot be measured directly but we can measure a Range.1640 if (typeof doc.createRange !== 'function') {1641 return null;1642 }1643 const range = doc.createRange();1644 if (typeof range.getClientRects !== 'function') {1645 return null;1646 }1647 range.selectNodeContents(instance);1648 rects = range.getClientRects();1649 } else {1650 rects = instance.getClientRects();1651 }1652 for (let i = 0; i < rects.length; i++) {1653 const rect = rects[i];1654 result.push({1655 x: rect.x + scrollX,1656 y: rect.y + scrollY,1657 width: rect.width,1658 height: rect.height,1659 });1660 }1661 return result;1662 }1663 if (instance.canonical) {1664 // Native1665 const publicInstance = instance.canonical.publicInstance;1666 if (!publicInstance) {1667 // The publicInstance may not have been initialized yet if there was no ref on this node.1668 // We can't initialize it from any existing Hook but we could fallback to this async form:1669 // renderer.extraDevToolsConfig.getInspectorDataForInstance(instance).hierarchy[last].getInspectorData().measure(callback)1670 return null;1671 }1672 if (typeof publicInstance.getBoundingClientRect === 'function') {1673 // enableAccessToHostTreeInFabric / ReadOnlyElement1674 return [publicInstance.getBoundingClientRect()];1675 }1676 if (typeof publicInstance.unstable_getBoundingClientRect === 'function') {1677 // ReactFabricHostComponent1678 return [publicInstance.unstable_getBoundingClientRect()];1679 }1680 }1681 return null;1682 }16831684 function measureInstance(instance: DevToolsInstance): null | Array<Rect> {1685 // Synchronously return the client rects of the Host instances directly inside this Instance.1686 const hostInstances = findAllCurrentHostInstances(instance);1687 let result: null | Array<Rect> = null;1688 for (let i = 0; i < hostInstances.length; i++) {1689 const childResult = measureHostInstance(hostInstances[i]);1690 if (childResult !== null) {1691 if (result === null) {1692 result = childResult;1693 } else {1694 result = result.concat(childResult);1695 }1696 }1697 }1698 return result;1699 }17001701 function getStringID(string: string | null): number {1702 if (string === null) {1703 return 0;1704 }1705 const existingEntry = pendingStringTable.get(string);1706 if (existingEntry !== undefined) {1707 return existingEntry.id;1708 }17091710 const id = pendingStringTable.size + 1;1711 const encodedString = utfEncodeString(string);17121713 pendingStringTable.set(string, {1714 encodedString,1715 id,1716 });17171718 // The string table total length needs to account both for the string length,1719 // and for the array item that contains the length itself.1720 //1721 // Don't use string length for this table.1722 // It won't work for multibyte characters (like emoji).1723 pendingStringTableLength += encodedString.length + 1;17241725 return id;1726 }17271728 let isInDisconnectedSubtree = false;17291730 function recordMount(1731 fiber: Fiber,1732 parentInstance: DevToolsInstance | null,1733 ): FiberInstance {1734 const isRoot = fiber.tag === HostRoot;1735 let fiberInstance;1736 if (isRoot) {1737 const entry = rootToFiberInstanceMap.get(fiber.stateNode);1738 if (entry === undefined) {1739 throw new Error('The root should have been registered at this point');1740 }1741 fiberInstance = entry;1742 } else {1743 fiberInstance = createFiberInstance(fiber);1744 }1745 idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance);17461747 // $FlowFixMe[constant-condition]1748 if (__DEBUG__) {1749 debug('recordMount()', fiberInstance, parentInstance);1750 }17511752 recordReconnect(fiberInstance, parentInstance);1753 return fiberInstance;1754 }17551756 function recordReconnect(1757 fiberInstance: FiberInstance,1758 parentInstance: DevToolsInstance | null,1759 ): void {1760 if (isInDisconnectedSubtree) {1761 // We're disconnected. We'll reconnect a hidden mount after the parent reappears.1762 return;1763 }1764 const id = fiberInstance.id;1765 const fiber = fiberInstance.data;17661767 const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');17681769 const isRoot = fiber.tag === HostRoot;17701771 if (isRoot) {1772 const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');17731774 // Adding a new field here would require a bridge protocol version bump (a backwads breaking change).1775 // Instead let's re-purpose a pre-existing field to carry more information.1776 let profilingFlags = 0;1777 if (isProfilingSupported) {1778 profilingFlags = PROFILING_FLAG_BASIC_SUPPORT;1779 if (typeof injectProfilingHooks === 'function') {1780 profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT;1781 }1782 if (supportsPerformanceTracks) {1783 profilingFlags |= PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT;1784 }1785 }17861787 // Set supportsStrictMode to false for production renderer builds1788 const isProductionBuildOfRenderer = renderer.bundleType === 0;17891790 pushOperation(TREE_OPERATION_ADD);1791 pushOperation(id);1792 pushOperation(ElementTypeRoot);1793 pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0);1794 pushOperation(profilingFlags);1795 pushOperation(1796 !isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0,1797 );1798 pushOperation(hasOwnerMetadata ? 1 : 0);17991800 if (isProfiling) {1801 if (displayNamesByRootID !== null) {1802 displayNamesByRootID.set(id, getDisplayNameForRoot(fiber));1803 }1804 }1805 } else {1806 const suspenseNode = fiberInstance.suspenseNode;1807 if (suspenseNode !== null && fiber.memoizedState === null) {1808 // We're reconnecting an unsuspended Suspense. Measure to see if anything changed.1809 const prevRects = suspenseNode.rects;1810 const nextRects = measureInstance(fiberInstance);1811 if (!areEqualRects(prevRects, nextRects)) {1812 suspenseNode.rects = nextRects;1813 recordSuspenseResize(suspenseNode);1814 }1815 }18161817 const {key} = fiber;1818 const displayName = getDisplayNameForFiber(fiber);1819 const elementType = getElementTypeForFiber(fiber);18201821 // Finding the owner instance might require traversing the whole parent path which1822 // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we1823 // need it but we have some synchronous operations in the front end like Alt+Left1824 // which selects the owner immediately. Typically most owners are only a few parents1825 // away so maybe it's not so bad.1826 const debugOwner = getUnfilteredOwner(fiber);1827 const ownerInstance = findNearestOwnerInstance(1828 parentInstance,1829 debugOwner,1830 );1831 if (1832 ownerInstance !== null &&1833 debugOwner === fiber._debugOwner &&1834 fiber._debugStack != null &&1835 ownerInstance.source === null1836 ) {1837 // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on1838 // the debugStack will be a stack frame inside the ownerInstance's source.1839 ownerInstance.source = fiber._debugStack;1840 }18411842 let unfilteredParent = parentInstance;1843 while (1844 unfilteredParent !== null &&1845 unfilteredParent.kind === FILTERED_FIBER_INSTANCE1846 ) {1847 unfilteredParent = unfilteredParent.parent;1848 }18491850 const ownerID = ownerInstance === null ? 0 : ownerInstance.id;1851 const parentID = unfilteredParent === null ? 0 : unfilteredParent.id;18521853 const displayNameStringID = getStringID(displayName);18541855 // This check is a guard to handle a React element that has been modified1856 // in such a way as to bypass the default stringification of the "key" property.1857 const keyString =1858 key === null1859 ? null1860 : key === REACT_OPTIMISTIC_KEY1861 ? 'React.optimisticKey'1862 : String(key);1863 const keyStringID = getStringID(keyString);18641865 const nameProp =1866 fiber.tag === SuspenseComponent1867 ? fiber.memoizedProps.name1868 : fiber.tag === ActivityComponent1869 ? fiber.memoizedProps.name1870 : null;1871 const namePropString = nameProp == null ? null : String(nameProp);1872 const namePropStringID = getStringID(namePropString);18731874 pushOperation(TREE_OPERATION_ADD);1875 pushOperation(id);1876 pushOperation(elementType);1877 pushOperation(parentID);1878 pushOperation(ownerID);1879 pushOperation(displayNameStringID);1880 pushOperation(keyStringID);1881 pushOperation(namePropStringID);18821883 // If this subtree has a new mode, let the frontend know.1884 if ((fiber.mode & StrictModeBits) !== 0) {1885 let parentFiber = null;1886 let parentFiberInstance = parentInstance;1887 while (parentFiberInstance !== null) {1888 if (parentFiberInstance.kind === FIBER_INSTANCE) {1889 parentFiber = parentFiberInstance.data;1890 break;1891 }1892 parentFiberInstance = parentFiberInstance.parent;1893 }1894 if (parentFiber === null || (parentFiber.mode & StrictModeBits) === 0) {1895 pushOperation(TREE_OPERATION_SET_SUBTREE_MODE);1896 pushOperation(id);1897 pushOperation(StrictMode);1898 }1899 }19001901 // If this is an Activity component, check if it's hidden.1902 if (fiber.tag === ActivityComponent) {1903 const offscreenChild = fiber.child;1904 if (1905 offscreenChild !== null &&1906 offscreenChild.tag === OffscreenComponent &&1907 offscreenChild.memoizedState !== null1908 ) {1909 pushOperation(TREE_OPERATION_SET_SUBTREE_MODE);1910 pushOperation(id);1911 pushOperation(ActivityHiddenMode);1912 }1913 }1914 }19151916 let componentLogsEntry = fiberToComponentLogsMap.get(fiber);1917 if (componentLogsEntry === undefined && fiber.alternate !== null) {1918 componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);1919 }1920 recordConsoleLogs(fiberInstance, componentLogsEntry);19211922 if (isProfilingSupported) {1923 recordProfilingDurations(fiberInstance, null);1924 }1925 }19261927 function recordVirtualMount(1928 instance: VirtualInstance,1929 parentInstance: DevToolsInstance | null,1930 secondaryEnv: null | string,1931 ): void {1932 const id = instance.id;19331934 idToDevToolsInstanceMap.set(id, instance);19351936 recordVirtualReconnect(instance, parentInstance, secondaryEnv);1937 }19381939 function recordVirtualReconnect(1940 instance: VirtualInstance,1941 parentInstance: DevToolsInstance | null,1942 secondaryEnv: null | string,1943 ): void {1944 if (isInDisconnectedSubtree) {1945 // We're disconnected. We'll reconnect a hidden mount after the parent reappears.1946 return;1947 }1948 const componentInfo = instance.data;19491950 const key =1951 typeof componentInfo.key === 'string' ? componentInfo.key : null;1952 const env = componentInfo.env;1953 let displayName = componentInfo.name || '';1954 if (typeof env === 'string') {1955 // We model environment as an HoC name for now.1956 if (secondaryEnv !== null) {1957 displayName = secondaryEnv + '(' + displayName + ')';1958 }1959 displayName = env + '(' + displayName + ')';1960 }1961 const elementType = ElementTypeVirtual;19621963 // Finding the owner instance might require traversing the whole parent path which1964 // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we1965 // need it but we have some synchronous operations in the front end like Alt+Left1966 // which selects the owner immediately. Typically most owners are only a few parents1967 // away so maybe it's not so bad.1968 const debugOwner = getUnfilteredOwner(componentInfo);1969 const ownerInstance = findNearestOwnerInstance(parentInstance, debugOwner);1970 if (1971 ownerInstance !== null &&1972 debugOwner === componentInfo.owner &&1973 componentInfo.debugStack != null &&1974 ownerInstance.source === null1975 ) {1976 // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on1977 // the debugStack will be a stack frame inside the ownerInstance's source.1978 ownerInstance.source = componentInfo.debugStack;1979 }19801981 let unfilteredParent = parentInstance;1982 while (1983 unfilteredParent !== null &&1984 unfilteredParent.kind === FILTERED_FIBER_INSTANCE1985 ) {1986 unfilteredParent = unfilteredParent.parent;1987 }19881989 const ownerID = ownerInstance === null ? 0 : ownerInstance.id;1990 const parentID = unfilteredParent === null ? 0 : unfilteredParent.id;19911992 const displayNameStringID = getStringID(displayName);19931994 // This check is a guard to handle a React element that has been modified1995 // in such a way as to bypass the default stringification of the "key" property.1996 const keyString = key === null ? null : String(key);1997 const keyStringID = getStringID(keyString);1998 const namePropStringID = getStringID(null);19992000 const id = instance.id;
Findings
✓ No findings reported for this file.