@samitouri / QOS-React / commits / f827c792ea

[DevTools] Add DevToolsInstance to Store Stateful Information (#30517)

Stacked on #30494 and #30491. This is setting us up to be able to track Server Components. This is now split into a FiberInstance (Client Component) and a VirtualInstance (Server Component). We're not actually creating any VirtualInstances yet though this is just the data structures. Server Components and potentially other compiled away or runtime optimized away (e.g. calling through a function without creating an intermediate Fiber) don't have a stateful instance. They just represent the latest data. It's kind of like a React Element. However, in the DevTools UI we need them to be stateful partly just so that you can select and refer to them separately. E.g. the same Server Component output rendered into different slots on the client should still have two different representations in the DevTools. Also if the same child Fibers update in place because the Server Component refreshed we shouldn't lose the selection if you've selected a Server Component. I'll implement this by creating Virtual Instances that only exist for the purpose of the DevTools UI and so it'll be implemented in the DevTools. We could just make a Map from `id` to `Fiber | ReactComponentInfo` but that requires a branching without a consistent hidden class. We also need some more states on there. We also have some other Maps that tracks extra states like that of component stacks, errors and warnings. Constantly resizing and adding/removing from a Map isn't exactly fast. It's faster to have a single Map with an object in it than one Map per object. However, having extra fields that are usually just `null` can instead mean more memory gets used. Since only a very small fraction of instances will have errors/warnings or having initialized its component stack, it made sense to store that in a separate Map that is usually just empty. However, with the addition of particularly the `parent` field and the ability to do a fast hidden-class safe branching on the `kind` field I think it's probably worth actually allocating an extra first class object per Instance to store DevTools state into. That's why I converted from just storing `Fiber` -> `id` to storing `Fiber` -> `DevToolsInstance` which then keeps the warnings/errors/componentStack as extra fields that are usually `null`. That is a lot of objects though since it's one per Fiber pair basically.

Sebastian Markbåge committed Jul 30, 2024 at 15:15 UTC f827c792eac95badcbe501ab7d5798985f41c0e6
1 file changed +419 -219
packages/react-devtools-shared/src/backend/fiber/renderer.js
+419 -219
@@ -7,6 +7,8 @@
7 * @flow
8 */
9
10 +import type {ReactComponentInfo} from 'shared/ReactTypes';
11 +
12 import {
13 ComponentFilterDisplayName,
14 ComponentFilterElementType,
@@ -131,6 +133,64 @@ import type {
133 import type {Source} from 'react-devtools-shared/src/shared/types';
134 import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack';
135
136 +// Kinds
137 +const FIBER_INSTANCE = 0;
138 +// const VIRTUAL_INSTANCE = 1;
139 +
140 +// Flags
141 +const FORCE_SUSPENSE_FALLBACK = /* */ 0b001;
142 +const FORCE_ERROR = /* */ 0b010;
143 +const FORCE_ERROR_RESET = /* */ 0b100;
144 +
145 +// This type represents a stateful instance of a Client Component i.e. a Fiber pair.
146 +// These instances also let us track stateful DevTools meta data like id and warnings.
147 +type FiberInstance = {
148 + kind: 0,
149 + id: number,
150 + parent: null | DevToolsInstance, // virtual parent
151 + flags: number, // Force Error/Suspense
152 + componentStack: null | string,
153 + errors: null | Map<string, number>, // error messages and count
154 + warnings: null | Map<string, number>, // warning messages and count
155 + data: Fiber, // one of a Fiber pair
156 +};
157 +
158 +function createFiberInstance(fiber: Fiber): FiberInstance {
159 + return {
160 + kind: 0,
161 + id: getUID(),
162 + parent: null,
163 + flags: 0,
164 + componentStack: null,
165 + errors: null,
166 + warnings: null,
167 + data: fiber,
168 + };
169 +}
170 +
171 +// This type represents a stateful instance of a Server Component or a Component
172 +// that gets optimized away - e.g. call-through without creating a Fiber.
173 +// It's basically a virtual Fiber. This is not a semantic concept in React.
174 +// It only exists as a virtual concept to let the same Element in the DevTools
175 +// persist. To be selectable separately from all ReactComponentInfo and overtime.
176 +type VirtualInstance = {
177 + kind: 1,
178 + id: number,
179 + parent: null | DevToolsInstance, // virtual parent
180 + flags: number,
181 + componentStack: null | string,
182 + // Errors and Warnings happen per ReactComponentInfo which can appear in
183 + // multiple places but we track them per stateful VirtualInstance so
184 + // that old errors/warnings don't disappear when the instance is refreshed.
185 + errors: null | Map<string, number>, // error messages and count
186 + warnings: null | Map<string, number>, // warning messages and count
187 + // The latest info for this instance. This can be updated over time and the
188 + // same info can appear in more than once ServerComponentInstance.
189 + data: ReactComponentInfo,
190 +};
191 +
192 +type DevToolsInstance = FiberInstance | VirtualInstance;
193 +
194 type getDisplayNameForFiberType = (fiber: Fiber) => string | null;
195 type getTypeSymbolType = (type: any) => symbol | number;
196
@@ -629,14 +689,12 @@ export function getInternalReactConstants(version: string): {
689 // We track both Fibers to support Fast Refresh,
690 // which may forcefully replace one of the pair as part of hot reloading.
691 // In that case it's still important to be able to locate the previous ID during subsequent renders.
632 -const fiberToIDMap: Map<Fiber, number> = new Map();
692 +const fiberToFiberInstanceMap: Map<Fiber, FiberInstance> = new Map();
693
694 // Map of id to one (arbitrary) Fiber in a pair.
695 // This Map is used to e.g. get the display name for a Fiber or schedule an update,
696 // operations that should be the same whether the current and work-in-progress Fiber is used.
637 -const idToArbitraryFiberMap: Map<number, Fiber> = new Map();
638 -
639 -const fiberToComponentStackMap: WeakMap<Fiber, string> = new WeakMap();
697 +const idToDevToolsInstanceMap: Map<number, DevToolsInstance> = new Map();
698
699 export function attach(
700 hook: DevToolsHook,
@@ -750,81 +808,80 @@ export function attach(
808 }
809
810 // Tracks Fibers with recently changed number of error/warning messages.
753 - // These collections store the Fiber rather than the ID,
754 - // in order to avoid generating an ID for Fibers that never get mounted
811 + // These collections store the Fiber rather than the DevToolsInstance,
812 + // in order to avoid generating an DevToolsInstance for Fibers that never get mounted
813 // (due to e.g. Suspense or error boundaries).
814 // onErrorOrWarning() adds Fibers and recordPendingErrorsAndWarnings() later clears them.
815 const fibersWithChangedErrorOrWarningCounts: Set<Fiber> = new Set();
816 const pendingFiberToErrorsMap: Map<Fiber, Map<string, number>> = new Map();
817 const pendingFiberToWarningsMap: Map<Fiber, Map<string, number>> = new Map();
818
761 - // Mapping of fiber IDs to error/warning messages and counts.
762 - const fiberIDToErrorsMap: Map<number, Map<string, number>> = new Map();
763 - const fiberIDToWarningsMap: Map<number, Map<string, number>> = new Map();
764 -
819 function clearErrorsAndWarnings() {
820 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
767 - for (const id of fiberIDToErrorsMap.keys()) {
768 - const fiber = idToArbitraryFiberMap.get(id);
769 - if (fiber != null) {
770 - fibersWithChangedErrorOrWarningCounts.add(fiber);
771 - updateMostRecentlyInspectedElementIfNecessary(id);
772 - }
773 - }
774 -
775 - // eslint-disable-next-line no-for-of-loops/no-for-of-loops
776 - for (const id of fiberIDToWarningsMap.keys()) {
777 - const fiber = idToArbitraryFiberMap.get(id);
778 - if (fiber != null) {
779 - fibersWithChangedErrorOrWarningCounts.add(fiber);
780 - updateMostRecentlyInspectedElementIfNecessary(id);
821 + for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {
822 + devtoolsInstance.errors = null;
823 + devtoolsInstance.warnings = null;
824 + if (devtoolsInstance.kind === FIBER_INSTANCE) {
825 + fibersWithChangedErrorOrWarningCounts.add(devtoolsInstance.data);
826 + } else {
827 + // TODO: Handle VirtualInstance.
828 }
829 + updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
830 }
783 -
784 - fiberIDToErrorsMap.clear();
785 - fiberIDToWarningsMap.clear();
786 -
831 flushPendingEvents();
832 }
833
834 function clearMessageCountHelper(
791 - fiberID: number,
835 + instanceID: number,
836 pendingFiberToMessageCountMap: Map<Fiber, Map<string, number>>,
793 - fiberIDToMessageCountMap: Map<number, Map<string, number>>,
837 + forError: boolean,
838 ) {
795 - const fiber = idToArbitraryFiberMap.get(fiberID);
796 - if (fiber != null) {
797 - // Throw out any pending changes.
798 - pendingFiberToErrorsMap.delete(fiber);
799 -
800 - if (fiberIDToMessageCountMap.has(fiberID)) {
801 - fiberIDToMessageCountMap.delete(fiberID);
839 + const devtoolsInstance = idToDevToolsInstanceMap.get(instanceID);
840 + if (devtoolsInstance !== undefined) {
841 + let changed = false;
842 + if (forError) {
843 + if (
844 + devtoolsInstance.errors !== null &&
845 + devtoolsInstance.errors.size > 0
846 + ) {
847 + changed = true;
848 + }
849 + devtoolsInstance.errors = null;
850 + } else {
851 + if (
852 + devtoolsInstance.warnings !== null &&
853 + devtoolsInstance.warnings.size > 0
854 + ) {
855 + changed = true;
856 + }
857 + devtoolsInstance.warnings = null;
858 + }
859 + if (devtoolsInstance.kind === FIBER_INSTANCE) {
860 + const fiber = devtoolsInstance.data;
861 + // Throw out any pending changes.
862 + pendingFiberToErrorsMap.delete(fiber);
863
803 - // If previous flushed counts have changed, schedule an update too.
804 - fibersWithChangedErrorOrWarningCounts.add(fiber);
805 - flushPendingEvents();
864 + if (changed) {
865 + // If previous flushed counts have changed, schedule an update too.
866 + fibersWithChangedErrorOrWarningCounts.add(fiber);
867 + flushPendingEvents();
868
807 - updateMostRecentlyInspectedElementIfNecessary(fiberID);
869 + updateMostRecentlyInspectedElementIfNecessary(instanceID);
870 + } else {
871 + fibersWithChangedErrorOrWarningCounts.delete(fiber);
872 + }
873 } else {
809 - fibersWithChangedErrorOrWarningCounts.delete(fiber);
874 + // TODO: Handle VirtualInstance.
875 }
876 }
877 }
878
814 - function clearErrorsForElementID(fiberID: number) {
815 - clearMessageCountHelper(
816 - fiberID,
817 - pendingFiberToErrorsMap,
818 - fiberIDToErrorsMap,
819 - );
879 + function clearErrorsForElementID(instanceID: number) {
880 + clearMessageCountHelper(instanceID, pendingFiberToErrorsMap, true);
881 }
882
822 - function clearWarningsForElementID(fiberID: number) {
823 - clearMessageCountHelper(
824 - fiberID,
825 - pendingFiberToWarningsMap,
826 - fiberIDToWarningsMap,
827 - );
883 + function clearWarningsForElementID(instanceID: number) {
884 + clearMessageCountHelper(instanceID, pendingFiberToWarningsMap, false);
885 }
886
887 function updateMostRecentlyInspectedElementIfNecessary(
@@ -845,9 +902,12 @@ export function attach(
902 args: $ReadOnlyArray<any>,
903 ): void {
904 if (type === 'error') {
848 - const maybeID = getFiberIDUnsafe(fiber);
905 + let fiberInstance = fiberToFiberInstanceMap.get(fiber);
906 + if (fiberInstance === undefined && fiber.alternate !== null) {
907 + fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
908 + }
909 // if this is an error simulated by us to trigger error boundary, ignore
850 - if (maybeID != null && forceErrorForFiberIDs.get(maybeID) === true) {
910 + if (fiberInstance !== undefined && fiberInstance.flags & FORCE_ERROR) {
911 return;
912 }
913 }
@@ -1186,39 +1246,24 @@ export function attach(
1246 // Returns the unique ID for a Fiber or generates and caches a new one if the Fiber hasn't been seen before.
1247 // Once this method has been called for a Fiber, untrackFiberID() should always be called later to avoid leaking.
1248 function getOrGenerateFiberID(fiber: Fiber): number {
1189 - let id = null;
1190 - if (fiberToIDMap.has(fiber)) {
1191 - id = fiberToIDMap.get(fiber);
1192 - } else {
1249 + let fiberInstance = fiberToFiberInstanceMap.get(fiber);
1250 + if (fiberInstance === undefined) {
1251 const {alternate} = fiber;
1194 - if (alternate !== null && fiberToIDMap.has(alternate)) {
1195 - id = fiberToIDMap.get(alternate);
1252 + if (alternate !== null) {
1253 + fiberInstance = fiberToFiberInstanceMap.get(alternate);
1254 + if (fiberInstance !== undefined) {
1255 + // We found the other pair, so we need to make sure we track the other side.
1256 + fiberToFiberInstanceMap.set(fiber, fiberInstance);
1257 + }
1258 }
1259 }
1260
1261 let didGenerateID = false;
1200 - if (id === null) {
1262 + if (fiberInstance === undefined) {
1263 didGenerateID = true;
1202 - id = getUID();
1203 - }
1204 -
1205 - // This refinement is for Flow purposes only.
1206 - const refinedID = ((id: any): number);
1207 -
1208 - // Make sure we're tracking this Fiber
1209 - // e.g. if it just mounted or an error was logged during initial render.
1210 - if (!fiberToIDMap.has(fiber)) {
1211 - fiberToIDMap.set(fiber, refinedID);
1212 - idToArbitraryFiberMap.set(refinedID, fiber);
1213 - }
1214 -
1215 - // Also make sure we're tracking its alternate,
1216 - // e.g. in case this is the first update after mount.
1217 - const {alternate} = fiber;
1218 - if (alternate !== null) {
1219 - if (!fiberToIDMap.has(alternate)) {
1220 - fiberToIDMap.set(alternate, refinedID);
1221 - }
1264 + fiberInstance = createFiberInstance(fiber);
1265 + fiberToFiberInstanceMap.set(fiber, fiberInstance);
1266 + idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance);
1267 }
1268
1269 if (__DEBUG__) {
@@ -1232,7 +1277,7 @@ export function attach(
1277 }
1278 }
1279
1235 - return refinedID;
1280 + return fiberInstance.id;
1281 }
1282
1283 // Returns an ID if one has already been generated for the Fiber or throws.
@@ -1249,12 +1294,16 @@ export function attach(
1294 // Returns an ID if one has already been generated for the Fiber or null if one has not been generated.
1295 // Use this method while e.g. logging to avoid over-retaining Fibers.
1296 function getFiberIDUnsafe(fiber: Fiber): number | null {
1252 - if (fiberToIDMap.has(fiber)) {
1253 - return ((fiberToIDMap.get(fiber): any): number);
1297 + const fiberInstance = fiberToFiberInstanceMap.get(fiber);
1298 + if (fiberInstance !== undefined) {
1299 + return fiberInstance.id;
1300 } else {
1301 const {alternate} = fiber;
1256 - if (alternate !== null && fiberToIDMap.has(alternate)) {
1257 - return ((fiberToIDMap.get(alternate): any): number);
1302 + if (alternate !== null) {
1303 + const alternateInstance = fiberToFiberInstanceMap.get(alternate);
1304 + if (alternateInstance !== undefined) {
1305 + return alternateInstance.id;
1306 + }
1307 }
1308 }
1309 return null;
@@ -1306,29 +1355,34 @@ export function attach(
1355 }
1356
1357 untrackFibersSet.forEach(fiber => {
1309 - const fiberID = getFiberIDUnsafe(fiber);
1310 - if (fiberID !== null) {
1311 - idToArbitraryFiberMap.delete(fiberID);
1358 + const fiberInstance = fiberToFiberInstanceMap.get(fiber);
1359 + if (fiberInstance !== undefined) {
1360 + idToDevToolsInstanceMap.delete(fiberInstance.id);
1361
1362 // Also clear any errors/warnings associated with this fiber.
1314 - clearErrorsForElementID(fiberID);
1315 - clearWarningsForElementID(fiberID);
1363 + clearErrorsForElementID(fiberInstance.id);
1364 + clearWarningsForElementID(fiberInstance.id);
1365 + if (fiberInstance.flags & FORCE_ERROR) {
1366 + fiberInstance.flags &= ~FORCE_ERROR;
1367 + forceErrorCount--;
1368 + if (forceErrorCount === 0 && setErrorHandler != null) {
1369 + setErrorHandler(shouldErrorFiberAlwaysNull);
1370 + }
1371 + }
1372 + if (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) {
1373 + fiberInstance.flags &= ~FORCE_SUSPENSE_FALLBACK;
1374 + forceFallbackCount--;
1375 + if (forceFallbackCount === 0 && setSuspenseHandler != null) {
1376 + setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
1377 + }
1378 + }
1379 }
1380
1318 - fiberToIDMap.delete(fiber);
1319 - fiberToComponentStackMap.delete(fiber);
1381 + fiberToFiberInstanceMap.delete(fiber);
1382
1383 const {alternate} = fiber;
1384 if (alternate !== null) {
1323 - fiberToIDMap.delete(alternate);
1324 - fiberToComponentStackMap.delete(alternate);
1325 - }
1326 -
1327 - if (forceErrorForFiberIDs.has(fiberID)) {
1328 - forceErrorForFiberIDs.delete(fiberID);
1329 - if (forceErrorForFiberIDs.size === 0 && setErrorHandler != null) {
1330 - setErrorHandler(shouldErrorFiberAlwaysNull);
1331 - }
1385 + fiberToFiberInstanceMap.delete(alternate);
1386 }
1387 });
1388 untrackFibersSet.clear();
@@ -1738,18 +1792,14 @@ export function attach(
1792
1793 function reevaluateErrorsAndWarnings() {
1794 fibersWithChangedErrorOrWarningCounts.clear();
1741 - fiberIDToErrorsMap.forEach((countMap, fiberID) => {
1742 - const fiber = idToArbitraryFiberMap.get(fiberID);
1743 - if (fiber != null) {
1744 - fibersWithChangedErrorOrWarningCounts.add(fiber);
1745 - }
1746 - });
1747 - fiberIDToWarningsMap.forEach((countMap, fiberID) => {
1748 - const fiber = idToArbitraryFiberMap.get(fiberID);
1749 - if (fiber != null) {
1750 - fibersWithChangedErrorOrWarningCounts.add(fiber);
1795 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
1796 + for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {
1797 + if (devtoolsInstance.kind === FIBER_INSTANCE) {
1798 + fibersWithChangedErrorOrWarningCounts.add(devtoolsInstance.data);
1799 + } else {
1800 + // TODO: Handle VirtualInstance.
1801 }
1752 - });
1802 + }
1803 recordPendingErrorsAndWarnings();
1804 }
1805
@@ -1757,18 +1807,29 @@ export function attach(
1807 fiber: Fiber,
1808 fiberID: number,
1809 pendingFiberToMessageCountMap: Map<Fiber, Map<string, number>>,
1760 - fiberIDToMessageCountMap: Map<number, Map<string, number>>,
1810 + forError: boolean,
1811 ): number {
1812 let newCount = 0;
1813
1764 - let messageCountMap = fiberIDToMessageCountMap.get(fiberID);
1814 + const devtoolsInstance = idToDevToolsInstanceMap.get(fiberID);
1815 +
1816 + if (devtoolsInstance === undefined) {
1817 + return 0;
1818 + }
1819 +
1820 + let messageCountMap = forError
1821 + ? devtoolsInstance.errors
1822 + : devtoolsInstance.warnings;
1823
1824 const pendingMessageCountMap = pendingFiberToMessageCountMap.get(fiber);
1825 if (pendingMessageCountMap != null) {
1768 - if (messageCountMap == null) {
1826 + if (messageCountMap === null) {
1827 messageCountMap = pendingMessageCountMap;
1770 -
1771 - fiberIDToMessageCountMap.set(fiberID, pendingMessageCountMap);
1828 + if (forError) {
1829 + devtoolsInstance.errors = pendingMessageCountMap;
1830 + } else {
1831 + devtoolsInstance.warnings = pendingMessageCountMap;
1832 + }
1833 } else {
1834 // This Flow refinement should not be necessary and yet...
1835 const refinedMessageCountMap = ((messageCountMap: any): Map<
@@ -1808,13 +1869,13 @@ export function attach(
1869 fiber,
1870 fiberID,
1871 pendingFiberToErrorsMap,
1811 - fiberIDToErrorsMap,
1872 + true,
1873 );
1874 const warningCount = mergeMapsAndGetCountHelper(
1875 fiber,
1876 fiberID,
1877 pendingFiberToWarningsMap,
1817 - fiberIDToWarningsMap,
1878 + false,
1879 );
1880
1881 pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);
@@ -2830,7 +2891,17 @@ export function attach(
2891
2892 function findAllCurrentHostFibers(id: number): $ReadOnlyArray<Fiber> {
2893 const fibers = [];
2833 - const fiber = findCurrentFiberUsingSlowPathById(id);
2894 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
2895 + if (devtoolsInstance === undefined) {
2896 + console.warn(`Could not find DevToolsInstance with id "${id}"`);
2897 + return fibers;
2898 + }
2899 + if (devtoolsInstance.kind !== FIBER_INSTANCE) {
2900 + // TODO: Handle VirtualInstance.
2901 + return fibers;
2902 + }
2903 + const fiber =
2904 + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
2905 if (!fiber) {
2906 return fibers;
2907 }
@@ -2864,7 +2935,17 @@ export function attach(
2935
2936 function findHostInstancesForElementID(id: number) {
2937 try {
2867 - const fiber = findCurrentFiberUsingSlowPathById(id);
2938 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
2939 + if (devtoolsInstance === undefined) {
2940 + console.warn(`Could not find DevToolsInstance with id "${id}"`);
2941 + return null;
2942 + }
2943 + if (devtoolsInstance.kind !== FIBER_INSTANCE) {
2944 + // TODO: Handle VirtualInstance.
2945 + return null;
2946 + }
2947 + const fiber =
2948 + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
2949 if (fiber === null) {
2950 return null;
2951 }
@@ -2878,8 +2959,15 @@ export function attach(
2959 }
2960
2961 function getDisplayNameForElementID(id: number): null | string {
2881 - const fiber = idToArbitraryFiberMap.get(id);
2882 - return fiber != null ? getDisplayNameForFiber(fiber) : null;
2962 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
2963 + if (devtoolsInstance === undefined) {
2964 + return null;
2965 + }
2966 + if (devtoolsInstance.kind === FIBER_INSTANCE) {
2967 + return getDisplayNameForFiber(devtoolsInstance.data);
2968 + } else {
2969 + return devtoolsInstance.data.name || '';
2970 + }
2971 }
2972
2973 function getNearestMountedHostInstance(
@@ -2960,13 +3048,10 @@ export function attach(
3048 // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberTreeReflection.js
3049 // It would be nice if we updated React to inject this function directly (vs just indirectly via findDOMNode).
3050 // BEGIN copied code
2963 - function findCurrentFiberUsingSlowPathById(id: number): Fiber | null {
2964 - const fiber = idToArbitraryFiberMap.get(id);
2965 - if (fiber == null) {
2966 - console.warn(`Could not find Fiber with id "${id}"`);
2967 - return null;
2968 - }
2969 -
3051 + function findCurrentFiberUsingSlowPathByFiberInstance(
3052 + fiberInstance: FiberInstance,
3053 + ): Fiber | null {
3054 + const fiber = fiberInstance.data;
3055 const alternate = fiber.alternate;
3056 if (!alternate) {
3057 // If there is no alternate, then we only need to check if it is mounted.
@@ -3126,11 +3211,16 @@ export function attach(
3211 }
3212
3213 function prepareViewElementSource(id: number): void {
3129 - const fiber = idToArbitraryFiberMap.get(id);
3130 - if (fiber == null) {
3131 - console.warn(`Could not find Fiber with id "${id}"`);
3214 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
3215 + if (devtoolsInstance === undefined) {
3216 + console.warn(`Could not find DevToolsInstance with id "${id}"`);
3217 + return;
3218 + }
3219 + if (devtoolsInstance.kind !== FIBER_INSTANCE) {
3220 + // TODO: Handle VirtualInstance.
3221 return;
3222 }
3223 + const fiber = devtoolsInstance.data;
3224
3225 const {elementType, tag, type} = fiber;
3226
@@ -3168,7 +3258,17 @@ export function attach(
3258 }
3259
3260 function getOwnersList(id: number): Array<SerializedElement> | null {
3171 - const fiber = findCurrentFiberUsingSlowPathById(id);
3261 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
3262 + if (devtoolsInstance === undefined) {
3263 + console.warn(`Could not find DevToolsInstance with id "${id}"`);
3264 + return null;
3265 + }
3266 + if (devtoolsInstance.kind !== FIBER_INSTANCE) {
3267 + // TODO: Handle VirtualInstance.
3268 + return null;
3269 + }
3270 + const fiber =
3271 + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
3272 if (fiber == null) {
3273 return null;
3274 }
@@ -3197,7 +3297,18 @@ export function attach(
3297 let instance = null;
3298 let style = null;
3299
3200 - const fiber = findCurrentFiberUsingSlowPathById(id);
3300 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
3301 + if (devtoolsInstance === undefined) {
3302 + console.warn(`Could not find DevToolsInstance with id "${id}"`);
3303 + return {instance, style};
3304 + }
3305 + if (devtoolsInstance.kind !== FIBER_INSTANCE) {
3306 + // TODO: Handle VirtualInstance.
3307 + return {instance, style};
3308 + }
3309 +
3310 + const fiber =
3311 + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
3312 if (fiber !== null) {
3313 instance = fiber.stateNode;
3314
@@ -3238,7 +3349,17 @@ export function attach(
3349 }
3350
3351 function inspectElementRaw(id: number): InspectedElement | null {
3241 - const fiber = findCurrentFiberUsingSlowPathById(id);
3352 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
3353 + if (devtoolsInstance === undefined) {
3354 + console.warn(`Could not find DevToolsInstance with id "${id}"`);
3355 + return null;
3356 + }
3357 + if (devtoolsInstance.kind !== FIBER_INSTANCE) {
3358 + // TODO: Handle VirtualInstance.
3359 + return null;
3360 + }
3361 + const fiber =
3362 + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
3363 if (fiber == null) {
3364 return null;
3365 }
@@ -3427,9 +3548,6 @@ export function attach(
3548 rootType = fiberRoot._debugRootType;
3549 }
3550
3430 - const errors = fiberIDToErrorsMap.get(id) || new Map();
3431 - const warnings = fiberIDToWarningsMap.get(id) || new Map();
3432 -
3551 let isErrored = false;
3552 let targetErrorBoundaryID;
3553 if (isErrorBoundary(fiber)) {
@@ -3444,7 +3562,7 @@ export function attach(
3562 const DidCapture = 0b000000000000000000010000000;
3563 isErrored =
3564 (fiber.flags & DidCapture) !== 0 ||
3447 - forceErrorForFiberIDs.get(id) === true;
3565 + (devtoolsInstance.flags & FORCE_ERROR) !== 0;
3566 targetErrorBoundaryID = isErrored ? id : getNearestErrorBoundaryID(fiber);
3567 } else {
3568 targetErrorBoundaryID = getNearestErrorBoundaryID(fiber);
@@ -3493,7 +3611,7 @@ export function attach(
3611 (!isTimedOutSuspense ||
3612 // If it's showing fallback because we previously forced it to,
3613 // allow toggling it back to remove the fallback override.
3496 - forceFallbackForSuspenseIDs.has(id)),
3614 + (devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0),
3615
3616 // Can view component source location.
3617 canViewSource,
@@ -3513,8 +3631,14 @@ export function attach(
3631 hooks,
3632 props: memoizedProps,
3633 state: showState ? memoizedState : null,
3516 - errors: Array.from(errors.entries()),
3517 - warnings: Array.from(warnings.entries()),
3634 + errors:
3635 + devtoolsInstance.errors === null
3636 + ? []
3637 + : Array.from(devtoolsInstance.errors.entries()),
3638 + warnings:
3639 + devtoolsInstance.warnings === null
3640 + ? []
3641 + : Array.from(devtoolsInstance.warnings.entries()),
3642
3643 // List of owners
3644 owners,
@@ -3612,12 +3736,17 @@ export function attach(
3736 function updateSelectedElement(inspectedElement: InspectedElement): void {
3737 const {hooks, id, props} = inspectedElement;
3738
3615 - const fiber = idToArbitraryFiberMap.get(id);
3616 - if (fiber == null) {
3617 - console.warn(`Could not find Fiber with id "${id}"`);
3739 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
3740 + if (devtoolsInstance === undefined) {
3741 + console.warn(`Could not find DevToolsInstance with id "${id}"`);
3742 + return;
3743 + }
3744 + if (devtoolsInstance.kind !== FIBER_INSTANCE) {
3745 + // TODO: Handle VirtualInstance.
3746 return;
3747 }
3748
3749 + const fiber = devtoolsInstance.data;
3750 const {elementType, stateNode, tag, type} = fiber;
3751
3752 switch (tag) {
@@ -3751,9 +3880,7 @@ export function attach(
3880 // Log error & cause for user to debug
3881 console.error(message + '\n\n', error);
3882 if (error.cause != null) {
3754 - const fiber = findCurrentFiberUsingSlowPathById(id);
3755 - const componentName =
3756 - fiber != null ? getDisplayNameForFiber(fiber) : null;
3883 + const componentName = getDisplayNameForElementID(id);
3884 console.error(
3885 'React DevTools encountered an error while trying to inspect hooks. ' +
3886 'This is most likely caused by an error in current inspected component' +
@@ -3855,7 +3982,7 @@ export function attach(
3982 ? mostRecentlyInspectedElement
3983 : inspectElementRaw(id);
3984 if (result === null) {
3858 - console.warn(`Could not find Fiber with id "${id}"`);
3985 + console.warn(`Could not find DevToolsInstance with id "${id}"`);
3986 return;
3987 }
3988
@@ -3896,7 +4023,17 @@ export function attach(
4023 hookID: ?number,
4024 path: Array<string | number>,
4025 ): void {
3899 - const fiber = findCurrentFiberUsingSlowPathById(id);
4026 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
4027 + if (devtoolsInstance === undefined) {
4028 + console.warn(`Could not find DevToolsInstance with id "${id}"`);
4029 + return;
4030 + }
4031 + if (devtoolsInstance.kind !== FIBER_INSTANCE) {
4032 + // TODO: Handle VirtualInstance.
4033 + return;
4034 + }
4035 + const fiber =
4036 + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
4037 if (fiber !== null) {
4038 const instance = fiber.stateNode;
4039
@@ -3952,7 +4089,17 @@ export function attach(
4089 oldPath: Array<string | number>,
4090 newPath: Array<string | number>,
4091 ): void {
3955 - const fiber = findCurrentFiberUsingSlowPathById(id);
4092 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
4093 + if (devtoolsInstance === undefined) {
4094 + console.warn(`Could not find DevToolsInstance with id "${id}"`);
4095 + return;
4096 + }
4097 + if (devtoolsInstance.kind !== FIBER_INSTANCE) {
4098 + // TODO: Handle VirtualInstance.
4099 + return;
4100 + }
4101 + const fiber =
4102 + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
4103 if (fiber !== null) {
4104 const instance = fiber.stateNode;
4105
@@ -4018,7 +4165,17 @@ export function attach(
4165 path: Array<string | number>,
4166 value: any,
4167 ): void {
4021 - const fiber = findCurrentFiberUsingSlowPathById(id);
4168 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
4169 + if (devtoolsInstance === undefined) {
4170 + console.warn(`Could not find DevToolsInstance with id "${id}"`);
4171 + return;
4172 + }
4173 + if (devtoolsInstance.kind !== FIBER_INSTANCE) {
4174 + // TODO: Handle VirtualInstance.
4175 + return;
4176 + }
4177 + const fiber =
4178 + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance);
4179 if (fiber !== null) {
4180 const instance = fiber.stateNode;
4181
@@ -4281,44 +4438,45 @@ export function attach(
4438 return null;
4439 }
4440
4284 - // Map of id and its force error status: true (error), false (toggled off),
4285 - // null (do nothing)
4286 - const forceErrorForFiberIDs = new Map<number | null, $FlowFixMe>();
4441 + let forceErrorCount = 0;
4442
4288 - function shouldErrorFiberAccordingToMap(fiber: any) {
4443 + function shouldErrorFiberAccordingToMap(fiber: any): null | boolean {
4444 if (typeof setErrorHandler !== 'function') {
4445 throw new Error(
4446 'Expected overrideError() to not get called for earlier React versions.',
4447 );
4448 }
4449
4295 - const id = getFiberIDUnsafe(fiber);
4296 - if (id === null) {
4450 + let fiberInstance = fiberToFiberInstanceMap.get(fiber);
4451 + if (fiberInstance === undefined && fiber.alternate !== null) {
4452 + fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
4453 + }
4454 + if (fiberInstance === undefined) {
4455 return null;
4456 }
4457
4300 - let status = null;
4301 - if (forceErrorForFiberIDs.has(id)) {
4302 - status = forceErrorForFiberIDs.get(id);
4303 - if (status === false) {
4304 - // TRICKY overrideError adds entries to this Map,
4305 - // so ideally it would be the method that clears them too,
4306 - // but that would break the functionality of the feature,
4307 - // since DevTools needs to tell React to act differently than it normally would
4308 - // (don't just re-render the failed boundary, but reset its errored state too).
4309 - // So we can only clear it after telling React to reset the state.
4310 - // Technically this is premature and we should schedule it for later,
4311 - // since the render could always fail without committing the updated error boundary,
4312 - // but since this is a DEV-only feature, the simplicity is worth the trade off.
4313 - forceErrorForFiberIDs.delete(id);
4314 -
4315 - if (forceErrorForFiberIDs.size === 0) {
4316 - // Last override is gone. Switch React back to fast path.
4317 - setErrorHandler(shouldErrorFiberAlwaysNull);
4318 - }
4458 + if (fiberInstance.flags & FORCE_ERROR_RESET) {
4459 + // TRICKY overrideError adds entries to this Map,
4460 + // so ideally it would be the method that clears them too,
4461 + // but that would break the functionality of the feature,
4462 + // since DevTools needs to tell React to act differently than it normally would
4463 + // (don't just re-render the failed boundary, but reset its errored state too).
4464 + // So we can only clear it after telling React to reset the state.
4465 + // Technically this is premature and we should schedule it for later,
4466 + // since the render could always fail without committing the updated error boundary,
4467 + // but since this is a DEV-only feature, the simplicity is worth the trade off.
4468 + forceErrorCount--;
4469 + fiberInstance.flags &= ~FORCE_ERROR_RESET;
4470 + if (forceErrorCount === 0) {
4471 + // Last override is gone. Switch React back to fast path.
4472 + setErrorHandler(shouldErrorFiberAlwaysNull);
4473 }
4474 + return false;
4475 + } else if (fiberInstance.flags & FORCE_ERROR) {
4476 + return true;
4477 + } else {
4478 + return null;
4479 }
4321 - return status;
4480 }
4481
4482 function overrideError(id: number, forceError: boolean) {
@@ -4331,16 +4489,25 @@ export function attach(
4489 );
4490 }
4491
4334 - forceErrorForFiberIDs.set(id, forceError);
4335 -
4336 - if (forceErrorForFiberIDs.size === 1) {
4337 - // First override is added. Switch React to slower path.
4338 - setErrorHandler(shouldErrorFiberAccordingToMap);
4492 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
4493 + if (devtoolsInstance === undefined) {
4494 + return;
4495 }
4496 + if ((devtoolsInstance.flags & (FORCE_ERROR | FORCE_ERROR_RESET)) === 0) {
4497 + forceErrorCount++;
4498 + if (forceErrorCount === 1) {
4499 + // First override is added. Switch React to slower path.
4500 + setErrorHandler(shouldErrorFiberAccordingToMap);
4501 + }
4502 + }
4503 + devtoolsInstance.flags &= forceError ? ~FORCE_ERROR_RESET : ~FORCE_ERROR;
4504 + devtoolsInstance.flags |= forceError ? FORCE_ERROR : FORCE_ERROR_RESET;
4505
4341 - const fiber = idToArbitraryFiberMap.get(id);
4342 - if (fiber != null) {
4506 + if (devtoolsInstance.kind === FIBER_INSTANCE) {
4507 + const fiber = devtoolsInstance.data;
4508 scheduleUpdate(fiber);
4509 + } else {
4510 + // TODO: Handle VirtualInstance.
4511 }
4512 }
4513
@@ -4348,11 +4515,17 @@ export function attach(
4515 return false;
4516 }
4517
4351 - const forceFallbackForSuspenseIDs = new Set<number>();
4518 + let forceFallbackCount = 0;
4519
4520 function shouldSuspendFiberAccordingToSet(fiber: any) {
4354 - const maybeID = getFiberIDUnsafe(((fiber: any): Fiber));
4355 - return maybeID !== null && forceFallbackForSuspenseIDs.has(maybeID);
4521 + let fiberInstance = fiberToFiberInstanceMap.get(fiber);
4522 + if (fiberInstance === undefined && fiber.alternate !== null) {
4523 + fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
4524 + }
4525 + return (
4526 + fiberInstance !== undefined &&
4527 + (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0
4528 + );
4529 }
4530
4531 function overrideSuspense(id: number, forceFallback: boolean) {
@@ -4364,22 +4537,36 @@ export function attach(
4537 'Expected overrideSuspense() to not get called for earlier React versions.',
4538 );
4539 }
4540 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
4541 + if (devtoolsInstance === undefined) {
4542 + return;
4543 + }
4544 +
4545 if (forceFallback) {
4368 - forceFallbackForSuspenseIDs.add(id);
4369 - if (forceFallbackForSuspenseIDs.size === 1) {
4370 - // First override is added. Switch React to slower path.
4371 - setSuspenseHandler(shouldSuspendFiberAccordingToSet);
4546 + if ((devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) === 0) {
4547 + devtoolsInstance.flags |= FORCE_SUSPENSE_FALLBACK;
4548 + forceFallbackCount++;
4549 + if (forceFallbackCount === 1) {
4550 + // First override is added. Switch React to slower path.
4551 + setSuspenseHandler(shouldSuspendFiberAccordingToSet);
4552 + }
4553 }
4554 } else {
4374 - forceFallbackForSuspenseIDs.delete(id);
4375 - if (forceFallbackForSuspenseIDs.size === 0) {
4376 - // Last override is gone. Switch React back to fast path.
4377 - setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
4555 + if ((devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0) {
4556 + devtoolsInstance.flags &= ~FORCE_SUSPENSE_FALLBACK;
4557 + forceFallbackCount--;
4558 + if (forceFallbackCount === 0) {
4559 + // Last override is gone. Switch React back to fast path.
4560 + setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
4561 + }
4562 }
4563 }
4380 - const fiber = idToArbitraryFiberMap.get(id);
4381 - if (fiber != null) {
4564 +
4565 + if (devtoolsInstance.kind === FIBER_INSTANCE) {
4566 + const fiber = devtoolsInstance.data;
4567 scheduleUpdate(fiber);
4568 + } else {
4569 + // TODO: Handle VirtualInstance.
4570 }
4571 }
4572
@@ -4557,10 +4744,16 @@ export function attach(
4744 // The return path will contain Fibers that are "invisible" to the store
4745 // because their keys and indexes are important to restoring the selection.
4746 function getPathForElement(id: number): Array<PathFrame> | null {
4560 - let fiber: ?Fiber = idToArbitraryFiberMap.get(id);
4561 - if (fiber == null) {
4747 + const devtoolsInstance = idToDevToolsInstanceMap.get(id);
4748 + if (devtoolsInstance === undefined) {
4749 + return null;
4750 + }
4751 + if (devtoolsInstance.kind !== FIBER_INSTANCE) {
4752 + // TODO: Handle VirtualInstance.
4753 return null;
4754 }
4755 +
4756 + let fiber: null | Fiber = devtoolsInstance.data;
4757 const keyPath = [];
4758 while (fiber !== null) {
4759 // $FlowFixMe[incompatible-call] found when upgrading Flow
@@ -4623,26 +4816,33 @@ export function attach(
4816 }
4817
4818 function hasElementWithId(id: number): boolean {
4626 - return idToArbitraryFiberMap.has(id);
4819 + return idToDevToolsInstanceMap.has(id);
4820 }
4821
4822 function getComponentStackForFiber(fiber: Fiber): string | null {
4630 - let componentStack = fiberToComponentStackMap.get(fiber);
4631 - if (componentStack == null) {
4632 - const dispatcherRef = getDispatcherRef(renderer);
4633 - if (dispatcherRef == null) {
4634 - return null;
4635 - }
4636 -
4637 - componentStack = getStackByFiberInDevAndProd(
4638 - ReactTypeOfWork,
4639 - fiber,
4640 - dispatcherRef,
4641 - );
4642 - fiberToComponentStackMap.set(fiber, componentStack);
4823 + // TODO: This should really just take an DevToolsInstance directly.
4824 + let fiberInstance = fiberToFiberInstanceMap.get(fiber);
4825 + if (fiberInstance === undefined && fiber.alternate !== null) {
4826 + fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
4827 + }
4828 + if (fiberInstance === undefined) {
4829 + // We're no longer tracking this instance.
4830 + return null;
4831 + }
4832 + if (fiberInstance.componentStack !== null) {
4833 + // Cached entry.
4834 + return fiberInstance.componentStack;
4835 + }
4836 + const dispatcherRef = getDispatcherRef(renderer);
4837 + if (dispatcherRef == null) {
4838 + return null;
4839 }
4840
4645 - return componentStack;
4841 + return (fiberInstance.componentStack = getStackByFiberInDevAndProd(
4842 + ReactTypeOfWork,
4843 + fiber,
4844 + dispatcherRef,
4845 + ));
4846 }
4847
4848 function getSourceForFiber(fiber: Fiber): Source | null {