@samitouri / QOS-React / commits / e07235b980

[DevTools] Refactor Error / Warning Count Tracking (#30899)

We can simplify this tracking by not having a separate pending set of logs and the logs tracked per instance and instead we just track the logs per Fiber. This avoids the need to move it back into the pending set after unmounts in case a Fiber is reparented. The main motivation for this is to unify with an upcoming tracking of logs for Server Components. For those it doesn't make sense to move them into a per instance set and because the same Server Component - and its logs - may appear more than once. So no particular instance should steal it. The second part of this change is that instead of looking up the instance from fiber, which requires the fiberToFiberInstanceMap, we instead look up if a component has any new logs when we traverse it in the commit phase. After all for a component to have had a log it must have updated. This is a similar technique to #30897. This technique also works for Server Components without having to maintain a one to many relationship from ComponentInfo to VirtualInstance. So it unifies them. Normally this look up would be fast since the `fiberToComponentsLogs` set would be empty and so doesn't add any significant weight to the commit phase. If there's a ton of logs on many different components then it's not great since it would slow down the commit phase but that's not what we expect to see so in typical usage, this is better. There is an unfortunate consequence though which is that `console.warn/error` in passive effects (i.e. `useEffect`) wouldn't be picked up because currently we traverse the logs in `handleCommitFiberRoot` which is too early. If we moved that to `handlePostCommitFiberRoot` this wouldn't be a problem. In the meantime, I just detect this and do a brute force flush by walking all mounted instances if there's a `console.warn/error` inside a passive effect. If we ever add "owners" to event handlers that are triggered outside the render/commit phases (like `<div onClick={...}>`) and we want to associate error/warnings in those, we'd need a different technique to ensure those get flushed in time.

Sebastian Markbåge committed Sep 9, 2024 at 15:10 UTC e07235b980df30c0ec865500a3e747dd67ce80db
2 files changed +183 -320
packages/react-devtools-shared/src/__tests__/store-test.js
+4 -24
@@ -1817,13 +1817,8 @@ describe('Store', () => {
1817 jest.runOnlyPendingTimers();
1818 }
1819
1820 - // Gross abstraction around pending passive warning/error delay.
1821 - function flushPendingPassiveErrorAndWarningCounts() {
1822 - jest.advanceTimersByTime(1000);
1823 - }
1824 -
1820 // @reactVersion >= 18.0
1826 - it('are counted (after a delay)', () => {
1821 + it('are counted (after no delay)', () => {
1822 function Example() {
1823 React.useEffect(() => {
1824 console.error('test-only: passive error');
@@ -1838,13 +1833,6 @@ describe('Store', () => {
1833 }, false);
1834 });
1835 flushPendingBridgeOperations();
1841 - expect(store).toMatchInlineSnapshot(`
1842 - [root]
1843 - <Example>
1844 - `);
1845 -
1846 - // After a delay, passive effects should be committed as well
1847 - act(flushPendingPassiveErrorAndWarningCounts, false);
1836 expect(store).toMatchInlineSnapshot(`
1837 ✕ 1, ⚠ 1
1838 [root]
@@ -1879,8 +1867,9 @@ describe('Store', () => {
1867 }, false);
1868 flushPendingBridgeOperations();
1869 expect(store).toMatchInlineSnapshot(`
1870 + ✕ 1, ⚠ 1
1871 [root]
1883 - <Example>
1872 + <Example> ✕⚠
1873 `);
1874
1875 // Before warnings and errors have flushed, flush another commit.
@@ -1894,22 +1883,13 @@ describe('Store', () => {
1883 }, false);
1884 flushPendingBridgeOperations();
1885 expect(store).toMatchInlineSnapshot(`
1897 - ✕ 1, ⚠ 1
1886 + ✕ 2, ⚠ 2
1887 [root]
1888 <Example> ✕⚠
1889 <Noop>
1890 `);
1891 });
1892
1904 - // After a delay, passive effects should be committed as well
1905 - act(flushPendingPassiveErrorAndWarningCounts, false);
1906 - expect(store).toMatchInlineSnapshot(`
1907 - ✕ 2, ⚠ 2
1908 - [root]
1909 - <Example> ✕⚠
1910 - <Noop>
1911 - `);
1912 -
1893 act(() => unmount());
1894 expect(store).toMatchInlineSnapshot(``);
1895 });
packages/react-devtools-shared/src/backend/fiber/renderer.js
+179 -296
@@ -157,8 +157,7 @@ type FiberInstance = {
157 firstChild: null | DevToolsInstance,
158 nextSibling: null | DevToolsInstance,
159 source: null | string | Error | Source, // source location of this component function, or owned child stack
160 - errors: null | Map<string, number>, // error messages and count
161 - warnings: null | Map<string, number>, // warning messages and count
160 + logCount: number, // total number of errors/warnings last seen
161 treeBaseDuration: number, // the profiled time of the last render of this subtree
162 data: Fiber, // one of a Fiber pair
163 };
@@ -171,8 +170,7 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
170 firstChild: null,
171 nextSibling: null,
172 source: null,
174 - errors: null,
175 - warnings: null,
173 + logCount: 0,
174 treeBaseDuration: 0,
175 data: fiber,
176 };
@@ -187,8 +185,7 @@ type FilteredFiberInstance = {
185 firstChild: null | DevToolsInstance,
186 nextSibling: null | DevToolsInstance,
187 source: null | string | Error | Source, // always null here.
190 - errors: null, // error messages and count
191 - warnings: null, // warning messages and count
188 + logCount: number, // total number of errors/warnings last seen
189 treeBaseDuration: number, // the profiled time of the last render of this subtree
190 data: Fiber, // one of a Fiber pair
191 };
@@ -201,9 +198,8 @@ function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance {
198 parent: null,
199 firstChild: null,
200 nextSibling: null,
204 - componentStack: null,
205 - errors: null,
206 - warnings: null,
201 + source: null,
202 + logCount: 0,
203 treeBaseDuration: 0,
204 data: fiber,
205 }: any);
@@ -221,11 +217,7 @@ type VirtualInstance = {
217 firstChild: null | DevToolsInstance,
218 nextSibling: null | DevToolsInstance,
219 source: null | string | Error | Source, // source location of this server component, or owned child stack
224 - // Errors and Warnings happen per ReactComponentInfo which can appear in
225 - // multiple places but we track them per stateful VirtualInstance so
226 - // that old errors/warnings don't disappear when the instance is refreshed.
227 - errors: null | Map<string, number>, // error messages and count
228 - warnings: null | Map<string, number>, // warning messages and count
220 + logCount: number, // total number of errors/warnings last seen
221 treeBaseDuration: number, // the profiled time of the last render of this subtree
222 // The latest info for this instance. This can be updated over time and the
223 // same info can appear in more than once ServerComponentInstance.
@@ -242,8 +234,7 @@ function createVirtualInstance(
234 firstChild: null,
235 nextSibling: null,
236 source: null,
245 - errors: null,
246 - warnings: null,
237 + logCount: 0,
238 treeBaseDuration: 0,
239 data: debugEntry,
240 };
@@ -968,74 +959,91 @@ export function attach(
959 toggleProfilingStatus = response.toggleProfilingStatus;
960 }
961
971 - // Tracks Fibers with recently changed number of error/warning messages.
972 - // These collections store the Fiber rather than the DevToolsInstance,
973 - // in order to avoid generating an DevToolsInstance for Fibers that never get mounted
974 - // (due to e.g. Suspense or error boundaries).
975 - // onErrorOrWarning() adds Fibers and recordPendingErrorsAndWarnings() later clears them.
976 - const fibersWithChangedErrorOrWarningCounts: Set<Fiber> = new Set();
977 - const pendingFiberToErrorsMap: WeakMap<
978 - Fiber,
979 - Map<string, number>,
980 - > = new WeakMap();
981 - const pendingFiberToWarningsMap: WeakMap<
982 - Fiber,
983 - Map<string, number>,
984 - > = new WeakMap();
962 + type ComponentLogs = {
963 + errors: Map<string, number>,
964 + errorsCount: number,
965 + warnings: Map<string, number>,
966 + warningsCount: number,
967 + };
968 + // Tracks Errors/Warnings logs added to a Fiber. They are added before the commit and get
969 + // picked up a FiberInstance. This keeps it around as long as the Fiber is alive which
970 + // lets the Fiber get reparented/remounted and still observe the previous errors/warnings.
971 + // Unless we explicitly clear the logs from a Fiber.
972 + const fiberToComponentLogsMap: WeakMap<Fiber, ComponentLogs> = new WeakMap();
973 + // Tracks whether we've performed a commit since the last log. This is used to know
974 + // whether we received any new logs between the commit and post commit phases. I.e.
975 + // if any passive effects called console.warn / console.error.
976 + let needsToFlushComponentLogs = false;
977 +
978 + function bruteForceFlushErrorsAndWarnings() {
979 + // Refresh error/warning count for all mounted unfiltered Fibers.
980 + let hasChanges = false;
981 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
982 + for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {
983 + if (devtoolsInstance.kind === FIBER_INSTANCE) {
984 + const fiber = devtoolsInstance.data;
985 + const componentLogsEntry = fiberToComponentLogsMap.get(fiber);
986 + const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry);
987 + if (changed) {
988 + hasChanges = true;
989 + updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
990 + }
991 + } else {
992 + // Virtual Instances cannot log in passive effects and so never appear here.
993 + }
994 + }
995 + if (hasChanges) {
996 + flushPendingEvents();
997 + }
998 + }
999
1000 function clearErrorsAndWarnings() {
1001 + // Note, this only clears logs for Fibers that have instances. If they're filtered
1002 + // and then mount, the logs are there. Ensuring we only clear what you've seen.
1003 + // If we wanted to clear the whole set, we'd replace fiberToComponentLogsMap with a
1004 + // new WeakMap.
1005 +
1006 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
1007 for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {
989 - devtoolsInstance.errors = null;
990 - devtoolsInstance.warnings = null;
1008 if (devtoolsInstance.kind === FIBER_INSTANCE) {
992 - fibersWithChangedErrorOrWarningCounts.add(devtoolsInstance.data);
1009 + const fiber = devtoolsInstance.data;
1010 + fiberToComponentLogsMap.delete(fiber);
1011 + if (fiber.alternate) {
1012 + fiberToComponentLogsMap.delete(fiber.alternate);
1013 + }
1014 } else {
1015 // TODO: Handle VirtualInstance.
1016 }
996 - updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
1017 + const changed = recordConsoleLogs(devtoolsInstance, undefined);
1018 + if (changed) {
1019 + updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
1020 + }
1021 }
1022 flushPendingEvents();
1023 }
1024
1001 - function clearMessageCountHelper(
1002 - instanceID: number,
1003 - pendingFiberToMessageCountMap: WeakMap<Fiber, Map<string, number>>,
1004 - forError: boolean,
1005 - ) {
1025 + function clearConsoleLogsHelper(instanceID: number, type: 'error' | 'warn') {
1026 const devtoolsInstance = idToDevToolsInstanceMap.get(instanceID);
1027 if (devtoolsInstance !== undefined) {
1008 - let changed = false;
1009 - if (forError) {
1010 - if (
1011 - devtoolsInstance.errors !== null &&
1012 - devtoolsInstance.errors.size > 0
1013 - ) {
1014 - changed = true;
1015 - }
1016 - devtoolsInstance.errors = null;
1017 - } else {
1018 - if (
1019 - devtoolsInstance.warnings !== null &&
1020 - devtoolsInstance.warnings.size > 0
1021 - ) {
1022 - changed = true;
1023 - }
1024 - devtoolsInstance.warnings = null;
1025 - }
1028 if (devtoolsInstance.kind === FIBER_INSTANCE) {
1029 const fiber = devtoolsInstance.data;
1028 - // Throw out any pending changes.
1029 - pendingFiberToMessageCountMap.delete(fiber);
1030 -
1031 - if (changed) {
1032 - // If previous flushed counts have changed, schedule an update too.
1033 - fibersWithChangedErrorOrWarningCounts.add(fiber);
1034 - flushPendingEvents();
1035 -
1036 - updateMostRecentlyInspectedElementIfNecessary(instanceID);
1037 - } else {
1038 - fibersWithChangedErrorOrWarningCounts.delete(fiber);
1030 + const componentLogsEntry = fiberToComponentLogsMap.get(fiber);
1031 + if (componentLogsEntry !== undefined) {
1032 + if (type === 'error') {
1033 + componentLogsEntry.errors.clear();
1034 + componentLogsEntry.errorsCount = 0;
1035 + } else {
1036 + componentLogsEntry.warnings.clear();
1037 + componentLogsEntry.warningsCount = 0;
1038 + }
1039 + const changed = recordConsoleLogs(
1040 + devtoolsInstance,
1041 + componentLogsEntry,
1042 + );
1043 + if (changed) {
1044 + flushPendingEvents();
1045 + updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);
1046 + }
1047 }
1048 } else {
1049 // TODO: Handle VirtualInstance.
@@ -1044,11 +1052,11 @@ export function attach(
1052 }
1053
1054 function clearErrorsForElementID(instanceID: number) {
1047 - clearMessageCountHelper(instanceID, pendingFiberToErrorsMap, true);
1055 + clearConsoleLogsHelper(instanceID, 'error');
1056 }
1057
1058 function clearWarningsForElementID(instanceID: number) {
1051 - clearMessageCountHelper(instanceID, pendingFiberToWarningsMap, false);
1059 + clearConsoleLogsHelper(instanceID, 'warn');
1060 }
1061
1062 function updateMostRecentlyInspectedElementIfNecessary(
@@ -1086,34 +1094,45 @@ export function attach(
1094 // [Warning: %o, {...}] and [Warning: %o, {...}] will be considered as the same message,
1095 // even if objects are different
1096 const message = formatConsoleArgumentsToSingleString(...args);
1089 - if (__DEBUG__) {
1090 - const fiberInstance = fiberToFiberInstanceMap.get(fiber);
1091 - if (fiberInstance !== undefined) {
1092 - debug('onErrorOrWarning', fiberInstance, null, `${type}: "${message}"`);
1093 - }
1094 - }
1095 -
1096 - // Mark this Fiber as needed its warning/error count updated during the next flush.
1097 - fibersWithChangedErrorOrWarningCounts.add(fiber);
1097
1098 // Track the warning/error for later.
1100 - const fiberMap =
1101 - type === 'error' ? pendingFiberToErrorsMap : pendingFiberToWarningsMap;
1102 - const messageMap = fiberMap.get(fiber);
1103 - if (messageMap != null) {
1104 - const count = messageMap.get(message) || 0;
1105 - messageMap.set(message, count + 1);
1099 + let componentLogsEntry = fiberToComponentLogsMap.get(fiber);
1100 + if (componentLogsEntry === undefined && fiber.alternate !== null) {
1101 + componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);
1102 + if (componentLogsEntry !== undefined) {
1103 + // Use the same set for both Fibers.
1104 + fiberToComponentLogsMap.set(fiber, componentLogsEntry);
1105 + }
1106 + }
1107 + if (componentLogsEntry === undefined) {
1108 + componentLogsEntry = {
1109 + errors: new Map(),
1110 + errorsCount: 0,
1111 + warnings: new Map(),
1112 + warningsCount: 0,
1113 + };
1114 + fiberToComponentLogsMap.set(fiber, componentLogsEntry);
1115 + }
1116 +
1117 + const messageMap =
1118 + type === 'error'
1119 + ? componentLogsEntry.errors
1120 + : componentLogsEntry.warnings;
1121 + const count = messageMap.get(message) || 0;
1122 + messageMap.set(message, count + 1);
1123 + if (type === 'error') {
1124 + componentLogsEntry.errorsCount++;
1125 } else {
1107 - fiberMap.set(fiber, new Map([[message, 1]]));
1126 + componentLogsEntry.warningsCount++;
1127 }
1128
1110 - // Passive effects may trigger errors or warnings too;
1111 - // In this case, we should wait until the rest of the passive effects have run,
1112 - // but we shouldn't wait until the next commit because that might be a long time.
1113 - // This would also cause "tearing" between an inspected Component and the tree view.
1114 - // Then again we don't want to flush too soon because this could be an error during async rendering.
1115 - // Use a debounce technique to ensure that we'll eventually flush.
1116 - flushPendingErrorsAndWarningsAfterDelay();
1129 + // The changes will be flushed later when we commit.
1130 +
1131 + // If the log happened in a passive effect, then this happens after we've
1132 + // already committed the new tree so the change won't show up until we rerender
1133 + // that component again. We need to visit a Component with passive effects in
1134 + // handlePostCommitFiberRoot again to ensure that we flush the changes after passive.
1135 + needsToFlushComponentLogs = true;
1136 }
1137
1138 // Patching the console enables DevTools to do a few useful things:
@@ -1330,9 +1349,9 @@ export function attach(
1349 currentRoot = (null: any);
1350 });
1351
1333 - // Also re-evaluate all error and warning counts given the new filters.
1334 - reevaluateErrorsAndWarnings();
1352 flushPendingEvents();
1353 +
1354 + needsToFlushComponentLogs = false;
1355 }
1356
1357 function getEnvironmentNames(): Array<string> {
@@ -1530,29 +1549,6 @@ export function attach(
1549 // When a mount or update is in progress, this value tracks the root that is being operated on.
1550 let currentRoot: FiberInstance = (null: any);
1551
1533 - // Returns a FiberInstance if one has already been generated for the Fiber or null if one has not been generated.
1534 - // Use this method while e.g. logging to avoid over-retaining Fibers.
1535 - function getFiberInstanceUnsafe(fiber: Fiber): FiberInstance | null {
1536 - const fiberInstance = fiberToFiberInstanceMap.get(fiber);
1537 - if (fiberInstance !== undefined) {
1538 - return fiberInstance;
1539 - } else {
1540 - const {alternate} = fiber;
1541 - if (alternate !== null) {
1542 - const alternateInstance = fiberToFiberInstanceMap.get(alternate);
1543 - if (alternateInstance !== undefined) {
1544 - return alternateInstance;
1545 - }
1546 - }
1547 - }
1548 - return null;
1549 - }
1550 -
1551 - function getFiberIDUnsafe(fiber: Fiber): number | null {
1552 - const fiberInstance = getFiberInstanceUnsafe(fiber);
1553 - return fiberInstance === null ? null : fiberInstance.id;
1554 - }
1555 -
1552 // Removes a Fiber (and its alternate) from the Maps used to track their id.
1553 // This method should always be called when a Fiber is unmounting.
1554 function untrackFiber(nearestInstance: DevToolsInstance, fiber: Fiber) {
@@ -1850,164 +1846,40 @@ export function attach(
1846 }
1847 }
1848
1853 - let flushPendingErrorsAndWarningsAfterDelayTimeoutID: null | TimeoutID = null;
1854 -
1855 - function clearPendingErrorsAndWarningsAfterDelay() {
1856 - if (flushPendingErrorsAndWarningsAfterDelayTimeoutID !== null) {
1857 - clearTimeout(flushPendingErrorsAndWarningsAfterDelayTimeoutID);
1858 - flushPendingErrorsAndWarningsAfterDelayTimeoutID = null;
1859 - }
1860 - }
1861 -
1862 - function flushPendingErrorsAndWarningsAfterDelay() {
1863 - clearPendingErrorsAndWarningsAfterDelay();
1864 -
1865 - flushPendingErrorsAndWarningsAfterDelayTimeoutID = setTimeout(() => {
1866 - flushPendingErrorsAndWarningsAfterDelayTimeoutID = null;
1867 -
1868 - if (pendingOperations.length > 0) {
1869 - // On the off chance that something else has pushed pending operations,
1870 - // we should bail on warnings; it's probably not safe to push midway.
1871 - return;
1872 - }
1873 -
1874 - recordPendingErrorsAndWarnings();
1875 -
1876 - if (shouldBailoutWithPendingOperations()) {
1877 - // No warnings or errors to flush; we can bail out early here too.
1878 - return;
1879 - }
1880 -
1881 - // We can create a smaller operations array than flushPendingEvents()
1882 - // because we only need to flush warning and error counts.
1883 - // Only a few pieces of fixed information are required up front.
1884 - const operations: OperationsArray = new Array(
1885 - 3 + pendingOperations.length,
1886 - );
1887 - operations[0] = rendererID;
1888 - if (currentRoot === null) {
1889 - // TODO: This is not always safe so this field is probably not needed.
1890 - operations[1] = -1;
1891 - } else {
1892 - operations[1] = currentRoot.id;
1893 - }
1894 - operations[2] = 0; // String table size
1895 - for (let j = 0; j < pendingOperations.length; j++) {
1896 - operations[3 + j] = pendingOperations[j];
1897 - }
1898 -
1899 - flushOrQueueOperations(operations);
1900 -
1901 - pendingOperations.length = 0;
1902 - }, 1000);
1903 - }
1904 -
1905 - function reevaluateErrorsAndWarnings() {
1906 - fibersWithChangedErrorOrWarningCounts.clear();
1907 - // eslint-disable-next-line no-for-of-loops/no-for-of-loops
1908 - for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {
1909 - if (devtoolsInstance.kind === FIBER_INSTANCE) {
1910 - fibersWithChangedErrorOrWarningCounts.add(devtoolsInstance.data);
1911 - } else {
1912 - // TODO: Handle VirtualInstance.
1913 - }
1914 - }
1915 - recordPendingErrorsAndWarnings();
1916 - }
1917 -
1918 - function mergeMapsAndGetCountHelper(
1919 - fiber: Fiber,
1920 - fiberID: number,
1921 - pendingFiberToMessageCountMap: WeakMap<Fiber, Map<string, number>>,
1922 - forError: boolean,
1923 - ): number {
1924 - let newCount = 0;
1925 -
1926 - const devtoolsInstance = idToDevToolsInstanceMap.get(fiberID);
1927 -
1928 - if (devtoolsInstance === undefined) {
1929 - return 0;
1930 - }
1931 -
1932 - let messageCountMap = forError
1933 - ? devtoolsInstance.errors
1934 - : devtoolsInstance.warnings;
1935 -
1936 - const pendingMessageCountMap = pendingFiberToMessageCountMap.get(fiber);
1937 - if (pendingMessageCountMap != null) {
1938 - if (messageCountMap === null) {
1939 - messageCountMap = pendingMessageCountMap;
1940 - if (forError) {
1941 - devtoolsInstance.errors = pendingMessageCountMap;
1942 - } else {
1943 - devtoolsInstance.warnings = pendingMessageCountMap;
1944 - }
1945 - } else {
1946 - // This Flow refinement should not be necessary and yet...
1947 - const refinedMessageCountMap = ((messageCountMap: any): Map<
1948 - string,
1949 - number,
1950 - >);
1951 -
1952 - pendingMessageCountMap.forEach((pendingCount, message) => {
1953 - const previousCount = refinedMessageCountMap.get(message) || 0;
1954 - refinedMessageCountMap.set(message, previousCount + pendingCount);
1955 - });
1849 + function recordConsoleLogs(
1850 + instance: FiberInstance | VirtualInstance,
1851 + componentLogsEntry: void | ComponentLogs,
1852 + ): boolean {
1853 + if (componentLogsEntry === undefined) {
1854 + if (instance.logCount === 0) {
1855 + // Nothing has changed.
1856 + return false;
1857 }
1957 - }
1958 -
1959 - if (!shouldFilterFiber(fiber)) {
1960 - if (messageCountMap != null) {
1961 - messageCountMap.forEach(count => {
1962 - newCount += count;
1963 - });
1858 + // Reset to zero.
1859 + instance.logCount = 0;
1860 + pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);
1861 + pushOperation(instance.id);
1862 + pushOperation(0);
1863 + pushOperation(0);
1864 + return true;
1865 + } else {
1866 + const totalCount =
1867 + componentLogsEntry.errorsCount + componentLogsEntry.warningsCount;
1868 + if (instance.logCount === totalCount) {
1869 + // Nothing has changed.
1870 + return false;
1871 }
1872 + // Update counts.
1873 + instance.logCount = totalCount;
1874 + pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);
1875 + pushOperation(instance.id);
1876 + pushOperation(componentLogsEntry.errorsCount);
1877 + pushOperation(componentLogsEntry.warningsCount);
1878 + return true;
1879 }
1966 -
1967 - pendingFiberToMessageCountMap.delete(fiber);
1968 -
1969 - return newCount;
1970 - }
1971 -
1972 - function recordPendingErrorsAndWarnings() {
1973 - clearPendingErrorsAndWarningsAfterDelay();
1974 -
1975 - fibersWithChangedErrorOrWarningCounts.forEach(fiber => {
1976 - const fiberID = getFiberIDUnsafe(fiber);
1977 - if (fiberID === null) {
1978 - // Don't send updates for Fibers that didn't mount due to e.g. Suspense or an error boundary.
1979 - } else {
1980 - const errorCount = mergeMapsAndGetCountHelper(
1981 - fiber,
1982 - fiberID,
1983 - pendingFiberToErrorsMap,
1984 - true,
1985 - );
1986 - const warningCount = mergeMapsAndGetCountHelper(
1987 - fiber,
1988 - fiberID,
1989 - pendingFiberToWarningsMap,
1990 - false,
1991 - );
1992 -
1993 - pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);
1994 - pushOperation(fiberID);
1995 - pushOperation(errorCount);
1996 - pushOperation(warningCount);
1997 -
1998 - // Only clear the ones that we've already shown. Leave others in case
1999 - // they mount later.
2000 - pendingFiberToErrorsMap.delete(fiber);
2001 - pendingFiberToWarningsMap.delete(fiber);
2002 - }
2003 - });
2004 - fibersWithChangedErrorOrWarningCounts.clear();
1880 }
1881
1882 function flushPendingEvents(root: Object): void {
2008 - // Add any pending errors and warnings to the operations array.
2009 - recordPendingErrorsAndWarnings();
2010 -
1883 if (shouldBailoutWithPendingOperations()) {
1884 // If we aren't profiling, we can just bail out here.
1885 // No use sending an empty update over the bridge.
@@ -2260,6 +2132,12 @@ export function attach(
2132 }
2133 }
2134
2135 + let componentLogsEntry = fiberToComponentLogsMap.get(fiber);
2136 + if (componentLogsEntry === undefined && fiber.alternate !== null) {
2137 + componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);
2138 + }
2139 + recordConsoleLogs(fiberInstance, componentLogsEntry);
2140 +
2141 if (isProfilingSupported) {
2142 recordProfilingDurations(fiberInstance, null);
2143 }
@@ -2359,19 +2237,6 @@ export function attach(
2237
2238 idToDevToolsInstanceMap.delete(fiberInstance.id);
2239
2362 - // Restore any errors/warnings associated with this fiber to the pending
2363 - // map. I.e. treat it as before we tracked the instances. This lets us
2364 - // restore them if we remount the same Fibers later. Otherwise we rely
2365 - // on the GC of the Fibers to clean them up.
2366 - if (fiberInstance.errors !== null) {
2367 - pendingFiberToErrorsMap.set(fiber, fiberInstance.errors);
2368 - fiberInstance.errors = null;
2369 - }
2370 - if (fiberInstance.warnings !== null) {
2371 - pendingFiberToWarningsMap.set(fiber, fiberInstance.warnings);
2372 - fiberInstance.warnings = null;
2373 - }
2374 -
2240 if (fiberToFiberInstanceMap.get(fiber) === fiberInstance) {
2241 fiberToFiberInstanceMap.delete(fiber);
2242 }
@@ -3515,6 +3380,16 @@ export function attach(
3380 }
3381
3382 if (fiberInstance !== null) {
3383 + let componentLogsEntry = fiberToComponentLogsMap.get(
3384 + fiberInstance.data,
3385 + );
3386 + if (componentLogsEntry === undefined && fiberInstance.data.alternate) {
3387 + componentLogsEntry = fiberToComponentLogsMap.get(
3388 + fiberInstance.data.alternate,
3389 + );
3390 + }
3391 + recordConsoleLogs(fiberInstance, componentLogsEntry);
3392 +
3393 const isProfilingSupported =
3394 nextFiber.hasOwnProperty('treeBaseDuration');
3395 if (isProfilingSupported) {
@@ -3619,6 +3494,8 @@ export function attach(
3494 mountFiberRecursively(root.current, false);
3495
3496 flushPendingEvents(root);
3497 +
3498 + needsToFlushComponentLogs = false;
3499 currentRoot = (null: any);
3500 });
3501 }
@@ -3641,6 +3518,15 @@ export function attach(
3518 passiveEffectDuration;
3519 }
3520 }
3521 +
3522 + if (needsToFlushComponentLogs) {
3523 + // We received new logs after commit. I.e. in a passive effect. We need to
3524 + // traverse the tree to find the affected ones. If we just moved the whole
3525 + // tree traversal from handleCommitFiberRoot to handlePostCommitFiberRoot
3526 + // this wouldn't be needed. For now we just brute force check all instances.
3527 + // This is not that common of a case.
3528 + bruteForceFlushErrorsAndWarnings();
3529 + }
3530 }
3531
3532 function handleCommitFiberRoot(
@@ -3750,6 +3636,8 @@ export function attach(
3636 // We're done here.
3637 flushPendingEvents(root);
3638
3639 + needsToFlushComponentLogs = false;
3640 +
3641 if (traceUpdatesEnabled) {
3642 hook.emit('traceUpdates', traceUpdatesForNodes);
3643 }
@@ -4337,6 +4225,8 @@ export function attach(
4225 source = getSourceForFiberInstance(fiberInstance);
4226 }
4227
4228 + const componentLogsEntry = fiberToComponentLogsMap.get(fiber);
4229 +
4230 return {
4231 id: fiberInstance.id,
4232
@@ -4387,13 +4277,13 @@ export function attach(
4277 props: memoizedProps,
4278 state: showState ? memoizedState : null,
4279 errors:
4390 - fiberInstance.errors === null
4280 + componentLogsEntry === undefined
4281 ? []
4392 - : Array.from(fiberInstance.errors.entries()),
4282 + : Array.from(componentLogsEntry.errors.entries()),
4283 warnings:
4394 - fiberInstance.warnings === null
4284 + componentLogsEntry === undefined
4285 ? []
4396 - : Array.from(fiberInstance.warnings.entries()),
4286 + : Array.from(componentLogsEntry.warnings.entries()),
4287
4288 // List of owners
4289 owners,
@@ -4478,15 +4368,8 @@ export function attach(
4368 hooks: null,
4369 props: props,
4370 state: null,
4481 - errors:
4482 - virtualInstance.errors === null
4483 - ? []
4484 - : Array.from(virtualInstance.errors.entries()),
4485 - warnings:
4486 - virtualInstance.warnings === null
4487 - ? []
4488 - : Array.from(virtualInstance.warnings.entries()),
4489 -
4371 + errors: [], // TODO: Handle errors on Virtual Instances.
4372 + warnings: [], // TODO: Handle warnings on Virtual Instances.
4373 // List of owners
4374 owners,
4375