diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationModes-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationModes-test.js
index 979af05553abf..8086ff4414600 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationModes-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationModes-test.js
@@ -39,6 +39,56 @@ describe('ReactDOMServerIntegration', () => {
resetModules();
});
+ // Test pragmas don't support itRenders abstraction
+ if (
+ __EXPERIMENTAL__ &&
+ require('shared/ReactFeatureFlags').enableDebugTracing
+ ) {
+ describe('React.unstable_DebugTracingMode', () => {
+ beforeEach(() => {
+ spyOnDevAndProd(console, 'log');
+ });
+
+ itRenders('with one child', async render => {
+ const e = await render(
+
+ text1
+ ,
+ );
+ const parent = e.parentNode;
+ expect(parent.childNodes[0].tagName).toBe('DIV');
+ });
+
+ itRenders('mode with several children', async render => {
+ const Header = props => {
+ return
header
;
+ };
+ const Footer = props => {
+ return (
+
+ footer
+ about
+
+ );
+ };
+ const e = await render(
+
+ text1
+ text2
+
+
+ ,
+ );
+ const parent = e.parentNode;
+ expect(parent.childNodes[0].tagName).toBe('DIV');
+ expect(parent.childNodes[1].tagName).toBe('SPAN');
+ expect(parent.childNodes[2].tagName).toBe('P');
+ expect(parent.childNodes[3].tagName).toBe('H2');
+ expect(parent.childNodes[4].tagName).toBe('H3');
+ });
+ });
+ }
+
describe('React.StrictMode', () => {
itRenders('a strict mode with one child', async render => {
const e = await render(
diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js
index c1ff6d2bd35c4..5292796eb3df4 100644
--- a/packages/react-dom/src/server/ReactPartialRenderer.js
+++ b/packages/react-dom/src/server/ReactPartialRenderer.js
@@ -28,6 +28,7 @@ import {
} from 'shared/ReactFeatureFlags';
import {
+ REACT_DEBUG_TRACING_MODE_TYPE,
REACT_FORWARD_REF_TYPE,
REACT_FRAGMENT_TYPE,
REACT_STRICT_MODE_TYPE,
@@ -1002,6 +1003,7 @@ class ReactDOMServerRenderer {
}
switch (elementType) {
+ case REACT_DEBUG_TRACING_MODE_TYPE:
case REACT_STRICT_MODE_TYPE:
case REACT_PROFILER_TYPE:
case REACT_SUSPENSE_LIST_TYPE:
diff --git a/packages/react-reconciler/src/DebugTracing.js b/packages/react-reconciler/src/DebugTracing.js
new file mode 100644
index 0000000000000..5ecb696b575da
--- /dev/null
+++ b/packages/react-reconciler/src/DebugTracing.js
@@ -0,0 +1,226 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import type {Wakeable} from 'shared/ReactTypes';
+
+import {enableDebugTracing} from 'shared/ReactFeatureFlags';
+
+const nativeConsole: Object = console;
+let nativeConsoleLog: null | Function = null;
+
+const pendingGroupArgs: Array = [];
+let printedGroupIndex: number = -1;
+
+function group(...groupArgs): void {
+ pendingGroupArgs.push(groupArgs);
+
+ if (nativeConsoleLog === null) {
+ nativeConsoleLog = nativeConsole.log;
+ nativeConsole.log = log;
+ }
+}
+
+function groupEnd(): void {
+ pendingGroupArgs.pop();
+ while (printedGroupIndex >= pendingGroupArgs.length) {
+ nativeConsole.groupEnd();
+ printedGroupIndex--;
+ }
+
+ if (pendingGroupArgs.length === 0) {
+ nativeConsole.log = nativeConsoleLog;
+ nativeConsoleLog = null;
+ }
+}
+
+function log(...logArgs): void {
+ if (printedGroupIndex < pendingGroupArgs.length - 1) {
+ for (let i = printedGroupIndex + 1; i < pendingGroupArgs.length; i++) {
+ const groupArgs = pendingGroupArgs[i];
+ nativeConsole.group(...groupArgs);
+ }
+ printedGroupIndex = pendingGroupArgs.length - 1;
+ }
+ if (typeof nativeConsoleLog === 'function') {
+ nativeConsoleLog(...logArgs);
+ } else {
+ nativeConsole.log(...logArgs);
+ }
+}
+
+const REACT_LOGO_STYLE =
+ 'background-color: #20232a; color: #61dafb; padding: 0 2px;';
+
+export function logCommitStarted(priorityLabel: string): void {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ group(
+ `%c⚛️%c commit%c (priority: ${priorityLabel})`,
+ REACT_LOGO_STYLE,
+ '',
+ 'font-weight: normal;',
+ );
+ }
+ }
+}
+
+export function logCommitStopped(): void {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ groupEnd();
+ }
+ }
+}
+
+const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
+// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
+const wakeableIDs: WeakMap = new PossiblyWeakMap();
+let wakeableID: number = 0;
+function getWakeableID(wakeable: Wakeable): number {
+ if (!wakeableIDs.has(wakeable)) {
+ wakeableIDs.set(wakeable, wakeableID++);
+ }
+ return ((wakeableIDs.get(wakeable): any): number);
+}
+
+export function logComponentSuspended(
+ componentName: string,
+ wakeable: Wakeable,
+): void {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ const id = getWakeableID(wakeable);
+ const display = (wakeable: any).displayName || wakeable;
+ log(
+ `%c⚛️%c ${componentName} suspended`,
+ REACT_LOGO_STYLE,
+ 'color: #80366d; font-weight: bold;',
+ id,
+ display,
+ );
+ wakeable.then(
+ () => {
+ log(
+ `%c⚛️%c ${componentName} resolved`,
+ REACT_LOGO_STYLE,
+ 'color: #80366d; font-weight: bold;',
+ id,
+ display,
+ );
+ },
+ () => {
+ log(
+ `%c⚛️%c ${componentName} rejected`,
+ REACT_LOGO_STYLE,
+ 'color: #80366d; font-weight: bold;',
+ id,
+ display,
+ );
+ },
+ );
+ }
+ }
+}
+
+export function logLayoutEffectsStarted(priorityLabel: string): void {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ group(
+ `%c⚛️%c layout effects%c (priority: ${priorityLabel})`,
+ REACT_LOGO_STYLE,
+ '',
+ 'font-weight: normal;',
+ );
+ }
+ }
+}
+
+export function logLayoutEffectsStopped(): void {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ groupEnd();
+ }
+ }
+}
+
+export function logPassiveEffectsStarted(priorityLabel: string): void {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ group(
+ `%c⚛️%c passive effects%c (priority: ${priorityLabel})`,
+ REACT_LOGO_STYLE,
+ '',
+ 'font-weight: normal;',
+ );
+ }
+ }
+}
+
+export function logPassiveEffectsStopped(): void {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ groupEnd();
+ }
+ }
+}
+
+export function logRenderStarted(priorityLabel: string): void {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ group(
+ `%c⚛️%c render%c (priority: ${priorityLabel})`,
+ REACT_LOGO_STYLE,
+ '',
+ 'font-weight: normal;',
+ );
+ }
+ }
+}
+
+export function logRenderStopped(): void {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ groupEnd();
+ }
+ }
+}
+
+export function logForceUpdateScheduled(
+ componentName: string,
+ priorityLabel: string,
+): void {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ log(
+ `%c⚛️%c ${componentName} forced update %c(priority: ${priorityLabel})`,
+ REACT_LOGO_STYLE,
+ 'color: #db2e1f; font-weight: bold;',
+ '',
+ );
+ }
+ }
+}
+
+export function logStateUpdateScheduled(
+ componentName: string,
+ priorityLabel: string,
+ payloadOrAction: any,
+): void {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ log(
+ `%c⚛️%c ${componentName} updated state %c(priority: ${priorityLabel})`,
+ REACT_LOGO_STYLE,
+ 'color: #01a252; font-weight: bold;',
+ '',
+ payloadOrAction,
+ );
+ }
+ }
+}
diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js
index 52afd2380cfc1..4c5c832c1edc2 100644
--- a/packages/react-reconciler/src/ReactFiber.new.js
+++ b/packages/react-reconciler/src/ReactFiber.new.js
@@ -67,6 +67,7 @@ import {NoWork} from './ReactFiberExpirationTime.new';
import {
NoMode,
ConcurrentMode,
+ DebugTracingMode,
ProfileMode,
StrictMode,
BlockingMode,
@@ -74,6 +75,7 @@ import {
import {
REACT_FORWARD_REF_TYPE,
REACT_FRAGMENT_TYPE,
+ REACT_DEBUG_TRACING_MODE_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_PROFILER_TYPE,
REACT_PROVIDER_TYPE,
@@ -488,6 +490,10 @@ export function createFiberFromTypeAndProps(
expirationTime,
key,
);
+ case REACT_DEBUG_TRACING_MODE_TYPE:
+ fiberTag = Mode;
+ mode |= DebugTracingMode;
+ break;
case REACT_STRICT_MODE_TYPE:
fiberTag = Mode;
mode |= StrictMode;
diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js
index 05dc3db46f9d3..6972eb810b65a 100644
--- a/packages/react-reconciler/src/ReactFiber.old.js
+++ b/packages/react-reconciler/src/ReactFiber.old.js
@@ -67,6 +67,7 @@ import {NoWork} from './ReactFiberExpirationTime.old';
import {
NoMode,
ConcurrentMode,
+ DebugTracingMode,
ProfileMode,
StrictMode,
BlockingMode,
@@ -74,6 +75,7 @@ import {
import {
REACT_FORWARD_REF_TYPE,
REACT_FRAGMENT_TYPE,
+ REACT_DEBUG_TRACING_MODE_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_PROFILER_TYPE,
REACT_PROVIDER_TYPE,
@@ -488,6 +490,10 @@ export function createFiberFromTypeAndProps(
expirationTime,
key,
);
+ case REACT_DEBUG_TRACING_MODE_TYPE:
+ fiberTag = Mode;
+ mode |= DebugTracingMode;
+ break;
case REACT_STRICT_MODE_TYPE:
fiberTag = Mode;
mode |= StrictMode;
diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js
index 68570bd0e7459..ef5b30fc87244 100644
--- a/packages/react-reconciler/src/ReactFiberClassComponent.new.js
+++ b/packages/react-reconciler/src/ReactFiberClassComponent.new.js
@@ -10,12 +10,14 @@
import type {Fiber} from './ReactInternalTypes';
import type {ExpirationTime} from './ReactFiberExpirationTime.new';
import type {UpdateQueue} from './ReactUpdateQueue.new';
+import type {ReactPriorityLevel} from './ReactInternalTypes';
import * as React from 'react';
import {Update, Snapshot} from './ReactSideEffectTags';
import {
debugRenderPhaseSideEffectsForStrictMode,
disableLegacyContext,
+ enableDebugTracing,
warnAboutDeprecatedLifecycles,
} from 'shared/ReactFeatureFlags';
import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
@@ -27,7 +29,7 @@ import invariant from 'shared/invariant';
import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
-import {StrictMode} from './ReactTypeOfMode';
+import {DebugTracingMode, StrictMode} from './ReactTypeOfMode';
import {
enqueueUpdate,
@@ -53,8 +55,10 @@ import {
requestCurrentTimeForUpdate,
computeExpirationForFiber,
scheduleUpdateOnFiber,
+ priorityLevelToLabel,
} from './ReactFiberWorkLoop.new';
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
+import {logForceUpdateScheduled, logStateUpdateScheduled} from './DebugTracing';
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
@@ -207,6 +211,18 @@ const classComponentUpdater = {
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, expirationTime);
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ if (fiber.mode & DebugTracingMode) {
+ const label = priorityLevelToLabel(
+ ((update.priority: any): ReactPriorityLevel),
+ );
+ const name = getComponentName(fiber.type) || 'Unknown';
+ logStateUpdateScheduled(name, label, payload);
+ }
+ }
+ }
},
enqueueReplaceState(inst, payload, callback) {
const fiber = getInstance(inst);
@@ -231,6 +247,18 @@ const classComponentUpdater = {
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, expirationTime);
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ if (fiber.mode & DebugTracingMode) {
+ const label = priorityLevelToLabel(
+ ((update.priority: any): ReactPriorityLevel),
+ );
+ const name = getComponentName(fiber.type) || 'Unknown';
+ logStateUpdateScheduled(name, label, payload);
+ }
+ }
+ }
},
enqueueForceUpdate(inst, callback) {
const fiber = getInstance(inst);
@@ -254,6 +282,18 @@ const classComponentUpdater = {
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, expirationTime);
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ if (fiber.mode & DebugTracingMode) {
+ const label = priorityLevelToLabel(
+ ((update.priority: any): ReactPriorityLevel),
+ );
+ const name = getComponentName(fiber.type) || 'Unknown';
+ logForceUpdateScheduled(name, label);
+ }
+ }
+ }
},
};
diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.old.js b/packages/react-reconciler/src/ReactFiberClassComponent.old.js
index 52c40ac7c06f7..b52b41b87c672 100644
--- a/packages/react-reconciler/src/ReactFiberClassComponent.old.js
+++ b/packages/react-reconciler/src/ReactFiberClassComponent.old.js
@@ -10,12 +10,14 @@
import type {Fiber} from './ReactInternalTypes';
import type {ExpirationTime} from './ReactFiberExpirationTime.old';
import type {UpdateQueue} from './ReactUpdateQueue.old';
+import type {ReactPriorityLevel} from './ReactInternalTypes';
import * as React from 'react';
import {Update, Snapshot} from './ReactSideEffectTags';
import {
debugRenderPhaseSideEffectsForStrictMode,
disableLegacyContext,
+ enableDebugTracing,
warnAboutDeprecatedLifecycles,
} from 'shared/ReactFeatureFlags';
import ReactStrictModeWarnings from './ReactStrictModeWarnings.old';
@@ -27,7 +29,7 @@ import invariant from 'shared/invariant';
import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
-import {StrictMode} from './ReactTypeOfMode';
+import {DebugTracingMode, StrictMode} from './ReactTypeOfMode';
import {
enqueueUpdate,
@@ -53,8 +55,10 @@ import {
requestCurrentTimeForUpdate,
computeExpirationForFiber,
scheduleUpdateOnFiber,
+ priorityLevelToLabel,
} from './ReactFiberWorkLoop.old';
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
+import {logForceUpdateScheduled, logStateUpdateScheduled} from './DebugTracing';
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
@@ -207,6 +211,18 @@ const classComponentUpdater = {
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, expirationTime);
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ if (fiber.mode & DebugTracingMode) {
+ const label = priorityLevelToLabel(
+ ((update.priority: any): ReactPriorityLevel),
+ );
+ const name = getComponentName(fiber.type) || 'Unknown';
+ logStateUpdateScheduled(name, label, payload);
+ }
+ }
+ }
},
enqueueReplaceState(inst, payload, callback) {
const fiber = getInstance(inst);
@@ -231,6 +247,18 @@ const classComponentUpdater = {
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, expirationTime);
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ if (fiber.mode & DebugTracingMode) {
+ const label = priorityLevelToLabel(
+ ((update.priority: any): ReactPriorityLevel),
+ );
+ const name = getComponentName(fiber.type) || 'Unknown';
+ logStateUpdateScheduled(name, label, payload);
+ }
+ }
+ }
},
enqueueForceUpdate(inst, callback) {
const fiber = getInstance(inst);
@@ -254,6 +282,18 @@ const classComponentUpdater = {
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, expirationTime);
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ if (fiber.mode & DebugTracingMode) {
+ const label = priorityLevelToLabel(
+ ((update.priority: any): ReactPriorityLevel),
+ );
+ const name = getComponentName(fiber.type) || 'Unknown';
+ logForceUpdateScheduled(name, label);
+ }
+ }
+ }
},
};
diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js
index c9e01258f92a5..3d92e92238eb6 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.new.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.new.js
@@ -30,11 +30,15 @@ import type {
} from './ReactFiberHostConfig';
import ReactSharedInternals from 'shared/ReactSharedInternals';
-import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
+import {enableDebugTracing, enableUseEventAPI} from 'shared/ReactFeatureFlags';
import {markRootExpiredAtTime} from './ReactFiberRoot.new';
-import {NoWork, Sync} from './ReactFiberExpirationTime.new';
-import {NoMode, BlockingMode} from './ReactTypeOfMode';
+import {
+ inferPriorityFromExpirationTime,
+ NoWork,
+ Sync,
+} from './ReactFiberExpirationTime.new';
+import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
import {readContext} from './ReactFiberNewContext.new';
import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.new';
import {
@@ -57,6 +61,7 @@ import {
warnIfNotScopedWithMatchingAct,
markRenderEventTimeAndConfig,
markUnprocessedUpdateTime,
+ priorityLevelToLabel,
} from './ReactFiberWorkLoop.new';
import {
registerEvent,
@@ -92,6 +97,7 @@ import {
} from './ReactMutableSource.new';
import {getRootHostContainer} from './ReactFiberHostContext.new';
import {getIsRendering} from './ReactCurrentFiber';
+import {logStateUpdateScheduled} from './DebugTracing';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -1738,6 +1744,20 @@ function dispatchAction(
}
scheduleUpdateOnFiber(fiber, expirationTime);
}
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ if (fiber.mode & DebugTracingMode) {
+ const priorityLevel = inferPriorityFromExpirationTime(
+ currentTime,
+ expirationTime,
+ );
+ const label = priorityLevelToLabel(priorityLevel);
+ const name = getComponentName(fiber.type) || 'Unknown';
+ logStateUpdateScheduled(name, label, action);
+ }
+ }
+ }
}
const noOpMount = () => {};
diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js
index ccd805a7455aa..37fcc9012cd93 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.old.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.old.js
@@ -30,11 +30,15 @@ import type {
} from './ReactFiberHostConfig';
import ReactSharedInternals from 'shared/ReactSharedInternals';
-import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
+import {enableDebugTracing, enableUseEventAPI} from 'shared/ReactFeatureFlags';
import {markRootExpiredAtTime} from './ReactFiberRoot.old';
-import {NoWork, Sync} from './ReactFiberExpirationTime.old';
-import {NoMode, BlockingMode} from './ReactTypeOfMode';
+import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
+import {
+ inferPriorityFromExpirationTime,
+ NoWork,
+ Sync,
+} from './ReactFiberExpirationTime.old';
import {readContext} from './ReactFiberNewContext.old';
import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.old';
import {
@@ -57,6 +61,7 @@ import {
warnIfNotScopedWithMatchingAct,
markRenderEventTimeAndConfig,
markUnprocessedUpdateTime,
+ priorityLevelToLabel,
} from './ReactFiberWorkLoop.old';
import {
registerEvent,
@@ -92,6 +97,7 @@ import {
} from './ReactMutableSource.old';
import {getRootHostContainer} from './ReactFiberHostContext.old';
import {getIsRendering} from './ReactCurrentFiber';
+import {logStateUpdateScheduled} from './DebugTracing';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -1733,6 +1739,20 @@ function dispatchAction(
}
scheduleUpdateOnFiber(fiber, expirationTime);
}
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ if (fiber.mode & DebugTracingMode) {
+ const priorityLevel = inferPriorityFromExpirationTime(
+ currentTime,
+ expirationTime,
+ );
+ const label = priorityLevelToLabel(priorityLevel);
+ const name = getComponentName(fiber.type) || 'Unknown';
+ logStateUpdateScheduled(name, label, action);
+ }
+ }
+ }
}
const noOpMount = () => {};
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js
index a5a54bd597338..decaf11f62fc8 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.new.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js
@@ -36,7 +36,6 @@ import {
import getComponentName from 'shared/getComponentName';
import invariant from 'shared/invariant';
import ReactSharedInternals from 'shared/ReactSharedInternals';
-
import {getPublicInstance} from './ReactFiberHostConfig';
import {
findCurrentUnmaskedContext,
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js
index e64f76f134067..48f817fcf75d7 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.old.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js
@@ -36,7 +36,6 @@ import {
import getComponentName from 'shared/getComponentName';
import invariant from 'shared/invariant';
import ReactSharedInternals from 'shared/ReactSharedInternals';
-
import {getPublicInstance} from './ReactFiberHostConfig';
import {
findCurrentUnmaskedContext,
diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js
index a0da2e5e8652d..e699be7c66235 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.new.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.new.js
@@ -29,9 +29,9 @@ import {
ShouldCapture,
LifecycleEffectMask,
} from './ReactSideEffectTags';
-import {NoMode, BlockingMode} from './ReactTypeOfMode';
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new';
-
+import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
+import {enableDebugTracing} from 'shared/ReactFeatureFlags';
import {createCapturedValue} from './ReactCapturedValue';
import {
enqueueCapturedUpdate,
@@ -55,6 +55,7 @@ import {
pingSuspendedRoot,
} from './ReactFiberWorkLoop.new';
import {logCapturedError} from './ReactFiberErrorLogger';
+import {logComponentSuspended} from './DebugTracing';
import {Sync, NoWork} from './ReactFiberExpirationTime.new';
@@ -194,6 +195,15 @@ function throwException(
// This is a wakeable.
const wakeable: Wakeable = (value: any);
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ if (sourceFiber.mode & DebugTracingMode) {
+ const name = getComponentName(sourceFiber.type) || 'Unknown';
+ logComponentSuspended(name, wakeable);
+ }
+ }
+ }
+
if ((sourceFiber.mode & BlockingMode) === NoMode) {
// Reset the memoizedState to what it was before we attempted
// to render it.
diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js
index d9f09ad36210a..a091c39b41f6f 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.old.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.old.js
@@ -29,9 +29,9 @@ import {
ShouldCapture,
LifecycleEffectMask,
} from './ReactSideEffectTags';
-import {NoMode, BlockingMode} from './ReactTypeOfMode';
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old';
-
+import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
+import {enableDebugTracing} from 'shared/ReactFeatureFlags';
import {createCapturedValue} from './ReactCapturedValue';
import {
enqueueCapturedUpdate,
@@ -55,6 +55,7 @@ import {
pingSuspendedRoot,
} from './ReactFiberWorkLoop.old';
import {logCapturedError} from './ReactFiberErrorLogger';
+import {logComponentSuspended} from './DebugTracing';
import {Sync} from './ReactFiberExpirationTime.old';
@@ -194,6 +195,15 @@ function throwException(
// This is a wakeable.
const wakeable: Wakeable = (value: any);
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ if (sourceFiber.mode & DebugTracingMode) {
+ const name = getComponentName(sourceFiber.type) || 'Unknown';
+ logComponentSuspended(name, wakeable);
+ }
+ }
+ }
+
if ((sourceFiber.mode & BlockingMode) === NoMode) {
// Reset the memoizedState to what it was before we attempted
// to render it.
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index e3faf2b44fd06..508dfca5da459 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -27,6 +27,7 @@ import {
enableSchedulerTracing,
warnAboutUnmockedScheduler,
disableSchedulerTimeoutBasedOnReactExpirationTime,
+ enableDebugTracing,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import invariant from 'shared/invariant';
@@ -48,6 +49,16 @@ import {
flushSyncCallbackQueue,
scheduleSyncCallback,
} from './SchedulerWithReactIntegration.new';
+import {
+ logCommitStarted,
+ logCommitStopped,
+ logLayoutEffectsStarted,
+ logLayoutEffectsStopped,
+ logPassiveEffectsStarted,
+ logPassiveEffectsStopped,
+ logRenderStarted,
+ logRenderStopped,
+} from './DebugTracing';
// The scheduler is imported here *only* to detect whether it's been mocked
import * as Scheduler from 'scheduler';
@@ -378,6 +389,29 @@ export function computeExpirationForFiber(
return expirationTime;
}
+export function priorityLevelToLabel(
+ priorityLevel: ReactPriorityLevel,
+): string {
+ if (__DEV__ && enableDebugTracing) {
+ switch (priorityLevel) {
+ case ImmediatePriority:
+ return 'immediate';
+ case UserBlockingPriority:
+ return 'user-blocking';
+ case NormalPriority:
+ return 'normal';
+ case LowPriority:
+ return 'low';
+ case IdlePriority:
+ return 'idle';
+ default:
+ return 'other';
+ }
+ } else {
+ return '';
+ }
+}
+
export function scheduleUpdateOnFiber(
fiber: Fiber,
expirationTime: ExpirationTime,
@@ -1386,6 +1420,15 @@ function renderRootSync(root, expirationTime) {
}
const prevInteractions = pushInteractions(root);
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ const priorityLevel = getCurrentPriorityLevel();
+ const label = priorityLevelToLabel(priorityLevel);
+ logRenderStarted(label);
+ }
+ }
+
do {
try {
workLoopSync();
@@ -1411,6 +1454,12 @@ function renderRootSync(root, expirationTime) {
);
}
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logRenderStopped();
+ }
+ }
+
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
@@ -1439,6 +1488,15 @@ function renderRootConcurrent(root, expirationTime) {
}
const prevInteractions = pushInteractions(root);
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ const priorityLevel = getCurrentPriorityLevel();
+ const label = priorityLevelToLabel(priorityLevel);
+ logRenderStarted(label);
+ }
+ }
+
do {
try {
workLoopConcurrent();
@@ -1455,6 +1513,12 @@ function renderRootConcurrent(root, expirationTime) {
popDispatcher(prevDispatcher);
executionContext = prevExecutionContext;
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logRenderStopped();
+ }
+ }
+
// Check if the tree has completed.
if (workInProgress !== null) {
// Still work remaining.
@@ -1722,6 +1786,12 @@ function commitRoot(root) {
}
function commitRootImpl(root, renderPriorityLevel) {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ const label = priorityLevelToLabel(renderPriorityLevel);
+ logCommitStarted(label);
+ }
+ }
do {
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
// means `flushPassiveEffects` will sometimes result in additional
@@ -1741,6 +1811,11 @@ function commitRootImpl(root, renderPriorityLevel) {
const finishedWork = root.finishedWork;
const expirationTime = root.finishedExpirationTime;
if (finishedWork === null) {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logCommitStopped();
+ }
+ }
return null;
}
root.finishedWork = null;
@@ -2021,6 +2096,12 @@ function commitRootImpl(root, renderPriorityLevel) {
}
if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logCommitStopped();
+ }
+ }
+
// This is a legacy edge case. We just committed the initial mount of
// a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
// synchronously, but layout updates should be deferred until the end
@@ -2030,6 +2111,13 @@ function commitRootImpl(root, renderPriorityLevel) {
// If layout work was scheduled, flush it now.
flushSyncCallbackQueue();
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logCommitStopped();
+ }
+ }
+
return null;
}
@@ -2137,6 +2225,14 @@ function commitLayoutEffects(
root: FiberRoot,
committedExpirationTime: ExpirationTime,
) {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ const priorityLevel = getCurrentPriorityLevel();
+ const label = priorityLevelToLabel(priorityLevel);
+ logLayoutEffectsStarted(label);
+ }
+ }
+
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
@@ -2160,6 +2256,12 @@ function commitLayoutEffects(
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logLayoutEffectsStopped();
+ }
+ }
}
export function flushPassiveEffects() {
@@ -2238,6 +2340,14 @@ function flushPassiveEffectsImpl() {
'Cannot flush passive effects while already rendering.',
);
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ const priorityLevel = getCurrentPriorityLevel();
+ const label = priorityLevelToLabel(priorityLevel);
+ logPassiveEffectsStarted(label);
+ }
+ }
+
if (__DEV__) {
isFlushingPassiveEffects = true;
}
@@ -2405,6 +2515,12 @@ function flushPassiveEffectsImpl() {
isFlushingPassiveEffects = false;
}
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logPassiveEffectsStopped();
+ }
+ }
+
executionContext = prevExecutionContext;
flushSyncCallbackQueue();
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index 30d5c828fc9e7..ff50b56d3a580 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -27,6 +27,7 @@ import {
enableSchedulerTracing,
warnAboutUnmockedScheduler,
disableSchedulerTimeoutBasedOnReactExpirationTime,
+ enableDebugTracing,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import invariant from 'shared/invariant';
@@ -48,6 +49,16 @@ import {
flushSyncCallbackQueue,
scheduleSyncCallback,
} from './SchedulerWithReactIntegration.old';
+import {
+ logCommitStarted,
+ logCommitStopped,
+ logLayoutEffectsStarted,
+ logLayoutEffectsStopped,
+ logPassiveEffectsStarted,
+ logPassiveEffectsStopped,
+ logRenderStarted,
+ logRenderStopped,
+} from './DebugTracing';
// The scheduler is imported here *only* to detect whether it's been mocked
import * as Scheduler from 'scheduler';
@@ -371,6 +382,29 @@ export function computeExpirationForFiber(
return expirationTime;
}
+export function priorityLevelToLabel(
+ priorityLevel: ReactPriorityLevel,
+): string {
+ if (__DEV__ && enableDebugTracing) {
+ switch (priorityLevel) {
+ case ImmediatePriority:
+ return 'immediate';
+ case UserBlockingPriority:
+ return 'user-blocking';
+ case NormalPriority:
+ return 'normal';
+ case LowPriority:
+ return 'low';
+ case IdlePriority:
+ return 'idle';
+ default:
+ return 'other';
+ }
+ } else {
+ return '';
+ }
+}
+
export function scheduleUpdateOnFiber(
fiber: Fiber,
expirationTime: ExpirationTime,
@@ -1405,6 +1439,15 @@ function renderRootSync(root, expirationTime) {
}
const prevInteractions = pushInteractions(root);
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ const priorityLevel = getCurrentPriorityLevel();
+ const label = priorityLevelToLabel(priorityLevel);
+ logRenderStarted(label);
+ }
+ }
+
do {
try {
workLoopSync();
@@ -1430,6 +1473,12 @@ function renderRootSync(root, expirationTime) {
);
}
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logRenderStopped();
+ }
+ }
+
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
@@ -1458,6 +1507,15 @@ function renderRootConcurrent(root, expirationTime) {
}
const prevInteractions = pushInteractions(root);
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ const priorityLevel = getCurrentPriorityLevel();
+ const label = priorityLevelToLabel(priorityLevel);
+ logRenderStarted(label);
+ }
+ }
+
do {
try {
workLoopConcurrent();
@@ -1474,6 +1532,12 @@ function renderRootConcurrent(root, expirationTime) {
popDispatcher(prevDispatcher);
executionContext = prevExecutionContext;
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logRenderStopped();
+ }
+ }
+
// Check if the tree has completed.
if (workInProgress !== null) {
// Still work remaining.
@@ -1741,6 +1805,12 @@ function commitRoot(root) {
}
function commitRootImpl(root, renderPriorityLevel) {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ const label = priorityLevelToLabel(renderPriorityLevel);
+ logCommitStarted(label);
+ }
+ }
do {
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
// means `flushPassiveEffects` will sometimes result in additional
@@ -1760,6 +1830,11 @@ function commitRootImpl(root, renderPriorityLevel) {
const finishedWork = root.finishedWork;
const expirationTime = root.finishedExpirationTime;
if (finishedWork === null) {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logCommitStopped();
+ }
+ }
return null;
}
root.finishedWork = null;
@@ -2040,6 +2115,12 @@ function commitRootImpl(root, renderPriorityLevel) {
}
if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logCommitStopped();
+ }
+ }
+
// This is a legacy edge case. We just committed the initial mount of
// a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
// synchronously, but layout updates should be deferred until the end
@@ -2049,6 +2130,13 @@ function commitRootImpl(root, renderPriorityLevel) {
// If layout work was scheduled, flush it now.
flushSyncCallbackQueue();
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logCommitStopped();
+ }
+ }
+
return null;
}
@@ -2156,6 +2244,14 @@ function commitLayoutEffects(
root: FiberRoot,
committedExpirationTime: ExpirationTime,
) {
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ const priorityLevel = getCurrentPriorityLevel();
+ const label = priorityLevelToLabel(priorityLevel);
+ logLayoutEffectsStarted(label);
+ }
+ }
+
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
@@ -2179,6 +2275,12 @@ function commitLayoutEffects(
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
+
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logLayoutEffectsStopped();
+ }
+ }
}
export function flushPassiveEffects() {
@@ -2257,6 +2359,14 @@ function flushPassiveEffectsImpl() {
'Cannot flush passive effects while already rendering.',
);
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ const priorityLevel = getCurrentPriorityLevel();
+ const label = priorityLevelToLabel(priorityLevel);
+ logPassiveEffectsStarted(label);
+ }
+ }
+
if (__DEV__) {
isFlushingPassiveEffects = true;
}
@@ -2424,6 +2534,12 @@ function flushPassiveEffectsImpl() {
isFlushingPassiveEffects = false;
}
+ if (__DEV__) {
+ if (enableDebugTracing) {
+ logPassiveEffectsStopped();
+ }
+ }
+
executionContext = prevExecutionContext;
flushSyncCallbackQueue();
diff --git a/packages/react-reconciler/src/ReactTypeOfMode.js b/packages/react-reconciler/src/ReactTypeOfMode.js
index 4f053e5bba474..f6092058d2ba0 100644
--- a/packages/react-reconciler/src/ReactTypeOfMode.js
+++ b/packages/react-reconciler/src/ReactTypeOfMode.js
@@ -9,10 +9,11 @@
export type TypeOfMode = number;
-export const NoMode = 0b0000;
-export const StrictMode = 0b0001;
+export const NoMode = 0b00000;
+export const StrictMode = 0b00001;
// TODO: Remove BlockingMode and ConcurrentMode by reading from the root
// tag instead
-export const BlockingMode = 0b0010;
-export const ConcurrentMode = 0b0100;
-export const ProfileMode = 0b1000;
+export const BlockingMode = 0b00010;
+export const ConcurrentMode = 0b00100;
+export const ProfileMode = 0b01000;
+export const DebugTracingMode = 0b10000;
diff --git a/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js
new file mode 100644
index 0000000000000..445ad6a208479
--- /dev/null
+++ b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js
@@ -0,0 +1,345 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+describe('DebugTracing', () => {
+ let React;
+ let ReactTestRenderer;
+ let Scheduler;
+
+ let logs;
+
+ beforeEach(() => {
+ jest.resetModules();
+
+ React = require('react');
+ ReactTestRenderer = require('react-test-renderer');
+ Scheduler = require('scheduler');
+
+ logs = [];
+
+ const groups = [];
+
+ spyOnDevAndProd(console, 'log').and.callFake(message => {
+ logs.push(`log: ${message.replace(/%c/g, '')}`);
+ });
+ spyOnDevAndProd(console, 'group').and.callFake(message => {
+ logs.push(`group: ${message.replace(/%c/g, '')}`);
+ groups.push(message);
+ });
+ spyOnDevAndProd(console, 'groupEnd').and.callFake(() => {
+ const message = groups.pop();
+ logs.push(`groupEnd: ${message.replace(/%c/g, '')}`);
+ });
+ });
+
+ // @gate experimental
+ it('should not log anything for sync render without suspends or state updates', () => {
+ ReactTestRenderer.create(
+
+
+ ,
+ );
+
+ expect(logs).toEqual([]);
+ });
+
+ // @gate experimental
+ it('should not log anything for concurrent render without suspends or state updates', () => {
+ ReactTestRenderer.create(
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+
+ expect(logs).toEqual([]);
+
+ logs.splice(0);
+
+ expect(Scheduler).toFlushUntilNextPaint([]);
+
+ expect(logs).toEqual([]);
+ });
+
+ // @gate experimental && enableDebugTracing
+ it('should log sync render with suspense', async () => {
+ const fakeSuspensPromise = Promise.resolve(true);
+ function Example() {
+ throw fakeSuspensPromise;
+ }
+
+ ReactTestRenderer.create(
+
+
+
+
+ ,
+ );
+
+ expect(logs).toEqual([
+ 'group: ⚛️ render (priority: immediate)',
+ 'log: ⚛️ Example suspended',
+ 'groupEnd: ⚛️ render (priority: immediate)',
+ ]);
+
+ logs.splice(0);
+
+ await fakeSuspensPromise;
+ expect(logs).toEqual(['log: ⚛️ Example resolved']);
+ });
+
+ // @gate experimental && enableDebugTracing
+ it('should log concurrent render with suspense', async () => {
+ const fakeSuspensPromise = Promise.resolve(true);
+ function Example() {
+ throw fakeSuspensPromise;
+ }
+
+ ReactTestRenderer.create(
+
+
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+
+ expect(logs).toEqual([]);
+
+ logs.splice(0);
+
+ expect(Scheduler).toFlushUntilNextPaint([]);
+
+ expect(logs).toEqual([
+ 'group: ⚛️ render (priority: normal)',
+ 'log: ⚛️ Example suspended',
+ 'groupEnd: ⚛️ render (priority: normal)',
+ ]);
+
+ logs.splice(0);
+
+ await fakeSuspensPromise;
+ expect(logs).toEqual(['log: ⚛️ Example resolved']);
+ });
+
+ // @gate experimental && enableDebugTracing
+ it('should log cascading class component updates', () => {
+ class Example extends React.Component {
+ state = {didMount: false};
+ componentDidMount() {
+ this.setState({didMount: true});
+ }
+ render() {
+ return null;
+ }
+ }
+
+ ReactTestRenderer.create(
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+
+ expect(logs).toEqual([]);
+
+ logs.splice(0);
+
+ expect(Scheduler).toFlushUntilNextPaint([]);
+
+ expect(logs).toEqual([
+ 'group: ⚛️ commit (priority: normal)',
+ 'group: ⚛️ layout effects (priority: immediate)',
+ 'log: ⚛️ Example updated state (priority: immediate)',
+ 'groupEnd: ⚛️ layout effects (priority: immediate)',
+ 'groupEnd: ⚛️ commit (priority: normal)',
+ ]);
+ });
+
+ // @gate experimental && enableDebugTracing
+ it('should log render phase state updates for class component', () => {
+ class Example extends React.Component {
+ state = {didRender: false};
+ render() {
+ if (this.state.didRender === false) {
+ this.setState({didRender: true});
+ }
+ return null;
+ }
+ }
+
+ ReactTestRenderer.create(
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+
+ expect(logs).toEqual([]);
+
+ logs.splice(0);
+
+ expect(() => {
+ expect(Scheduler).toFlushUntilNextPaint([]);
+ }).toErrorDev('Cannot update during an existing state transition');
+
+ expect(logs).toEqual([
+ 'group: ⚛️ render (priority: normal)',
+ 'log: ⚛️ Example updated state (priority: normal)',
+ 'log: ⚛️ Example updated state (priority: normal)',
+ 'groupEnd: ⚛️ render (priority: normal)',
+ ]);
+ });
+
+ // @gate experimental && enableDebugTracing
+ it('should log cascading layout updates', () => {
+ function Example() {
+ const [didMount, setDidMount] = React.useState(false);
+ React.useLayoutEffect(() => {
+ setDidMount(true);
+ }, []);
+ return didMount;
+ }
+
+ ReactTestRenderer.create(
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+
+ expect(logs).toEqual([]);
+
+ logs.splice(0);
+
+ expect(Scheduler).toFlushUntilNextPaint([]);
+
+ expect(logs).toEqual([
+ 'group: ⚛️ commit (priority: normal)',
+ 'group: ⚛️ layout effects (priority: immediate)',
+ 'log: ⚛️ Example updated state (priority: immediate)',
+ 'groupEnd: ⚛️ layout effects (priority: immediate)',
+ 'groupEnd: ⚛️ commit (priority: normal)',
+ ]);
+ });
+
+ // @gate experimental && enableDebugTracing
+ it('should log cascading passive updates', () => {
+ function Example() {
+ const [didMount, setDidMount] = React.useState(false);
+ React.useEffect(() => {
+ setDidMount(true);
+ }, []);
+ return didMount;
+ }
+
+ ReactTestRenderer.act(() => {
+ ReactTestRenderer.create(
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+ });
+ expect(logs).toEqual([
+ 'group: ⚛️ passive effects (priority: normal)',
+ 'log: ⚛️ Example updated state (priority: normal)',
+ 'groupEnd: ⚛️ passive effects (priority: normal)',
+ ]);
+ });
+
+ // @gate experimental && enableDebugTracing
+ it('should log render phase updates', () => {
+ function Example() {
+ const [didRender, setDidRender] = React.useState(false);
+ if (!didRender) {
+ setDidRender(true);
+ }
+ return didRender;
+ }
+
+ ReactTestRenderer.act(() => {
+ ReactTestRenderer.create(
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+ });
+ expect(logs).toEqual([
+ 'group: ⚛️ render (priority: normal)',
+ 'log: ⚛️ Example updated state (priority: normal)',
+ 'log: ⚛️ Example updated state (priority: normal)', // debugRenderPhaseSideEffectsForStrictMode
+ 'groupEnd: ⚛️ render (priority: normal)',
+ ]);
+ });
+
+ // @gate experimental && enableDebugTracing
+ it('should log when user code logs', () => {
+ function Example() {
+ console.log('Hello from user code');
+ return null;
+ }
+
+ ReactTestRenderer.create(
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+
+ expect(logs).toEqual([]);
+
+ logs.splice(0);
+
+ expect(Scheduler).toFlushUntilNextPaint([]);
+
+ expect(logs).toEqual([
+ 'group: ⚛️ render (priority: normal)',
+ 'log: Hello from user code',
+ 'groupEnd: ⚛️ render (priority: normal)',
+ ]);
+ });
+
+ // @gate experimental
+ it('should not log anything outside of a unstable_DebugTracingMode subtree', () => {
+ function ExampleThatCascades() {
+ const [didMount, setDidMount] = React.useState(false);
+ React.useLayoutEffect(() => {
+ setDidMount(true);
+ }, []);
+ return didMount;
+ }
+
+ const fakeSuspensPromise = new Promise(() => {});
+ function ExampleThatSuspends() {
+ throw fakeSuspensPromise;
+ }
+
+ function Example() {
+ return null;
+ }
+
+ ReactTestRenderer.create(
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ expect(logs).toEqual([]);
+ });
+});
diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js
index 998de2bec5caa..9c94c49fb21c9 100644
--- a/packages/react/index.classic.fb.js
+++ b/packages/react/index.classic.fb.js
@@ -51,5 +51,7 @@ export {
// enableScopeAPI
unstable_createScope,
unstable_useOpaqueIdentifier,
+ // enableDebugTracing
+ unstable_DebugTracingMode,
} from './src/React';
export {jsx, jsxs, jsxDEV} from './src/jsx/ReactJSX';
diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js
index 9c2a3d4d93553..c2cb40a815744 100644
--- a/packages/react/index.experimental.js
+++ b/packages/react/index.experimental.js
@@ -46,4 +46,6 @@ export {
// enableBlocksAPI
block,
unstable_useOpaqueIdentifier,
+ // enableDebugTracing
+ unstable_DebugTracingMode,
} from './src/React';
diff --git a/packages/react/index.js b/packages/react/index.js
index 3ca22840f999e..d3fa1b145ff45 100644
--- a/packages/react/index.js
+++ b/packages/react/index.js
@@ -59,6 +59,7 @@ export {
createMutableSource,
Fragment,
Profiler,
+ unstable_DebugTracingMode,
StrictMode,
Suspense,
createElement,
diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js
index 76a904d0c8cf0..a162ef330662c 100644
--- a/packages/react/index.modern.fb.js
+++ b/packages/react/index.modern.fb.js
@@ -50,5 +50,7 @@ export {
// enableScopeAPI
unstable_createScope,
unstable_useOpaqueIdentifier,
+ // enableDebugTracing
+ unstable_DebugTracingMode,
} from './src/React';
export {jsx, jsxs, jsxDEV} from './src/jsx/ReactJSX';
diff --git a/packages/react/src/React.js b/packages/react/src/React.js
index d7896ad299722..8171e89eb2e2f 100644
--- a/packages/react/src/React.js
+++ b/packages/react/src/React.js
@@ -10,6 +10,7 @@
import ReactVersion from 'shared/ReactVersion';
import {
REACT_FRAGMENT_TYPE,
+ REACT_DEBUG_TRACING_MODE_TYPE,
REACT_PROFILER_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_SUSPENSE_TYPE,
@@ -96,6 +97,7 @@ export {
REACT_FRAGMENT_TYPE as Fragment,
REACT_PROFILER_TYPE as Profiler,
REACT_STRICT_MODE_TYPE as StrictMode,
+ REACT_DEBUG_TRACING_MODE_TYPE as unstable_DebugTracingMode,
REACT_SUSPENSE_TYPE as Suspense,
createElement,
cloneElement,
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index 0d944a0d80b99..a683f4cabaf2c 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -11,6 +11,10 @@
// This prevents e.g. from making an unnecessar HTTP request for certain browsers.
export const enableFilterEmptyStringAttributesDOM = false;
+// Adds verbose console logging for e.g. state updates, suspense, and work loop stuff.
+// Intended to enable React core members to more easily debug scheduling issues in DEV builds.
+export const enableDebugTracing = false;
+
// Helps identify side effects in render-phase lifecycle hooks and setState
// reducers by double invoking them in Strict Mode.
export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js
index 53c994cf3bfeb..a4ecac2fa6608 100644
--- a/packages/shared/ReactSymbols.js
+++ b/packages/shared/ReactSymbols.js
@@ -27,6 +27,7 @@ export let REACT_FUNDAMENTAL_TYPE = 0xead5;
export let REACT_RESPONDER_TYPE = 0xead6;
export let REACT_SCOPE_TYPE = 0xead7;
export let REACT_OPAQUE_ID_TYPE = 0xeae0;
+export let REACT_DEBUG_TRACING_MODE_TYPE = 0xeae1;
if (typeof Symbol === 'function' && Symbol.for) {
const symbolFor = Symbol.for;
@@ -48,6 +49,7 @@ if (typeof Symbol === 'function' && Symbol.for) {
REACT_RESPONDER_TYPE = symbolFor('react.responder');
REACT_SCOPE_TYPE = symbolFor('react.scope');
REACT_OPAQUE_ID_TYPE = symbolFor('react.opaque.id');
+ REACT_DEBUG_TRACING_MODE_TYPE = symbolFor('react.debug_trace_mode');
}
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index 1500a0bf36688..0a70e8d2d75e6 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -11,6 +11,7 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as ExportsType from './ReactFeatureFlags.native-fb';
// The rest of the flags are static for better dead code elimination.
+export const enableDebugTracing = false;
export const enableProfilerTimer = __PROFILE__;
export const enableProfilerCommitHooks = false;
export const enableSchedulerTracing = __PROFILE__;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index b01baaf2bd5da..7644b11c36d82 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -11,6 +11,7 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as ExportsType from './ReactFeatureFlags.native-oss';
export const debugRenderPhaseSideEffectsForStrictMode = false;
+export const enableDebugTracing = false;
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
export const warnAboutDeprecatedLifecycles = true;
export const enableProfilerTimer = __PROFILE__;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 0623650b332ac..0e6b4efd9f465 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -11,6 +11,7 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as ExportsType from './ReactFeatureFlags.test-renderer';
export const debugRenderPhaseSideEffectsForStrictMode = false;
+export const enableDebugTracing = false;
export const warnAboutDeprecatedLifecycles = true;
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
export const enableProfilerTimer = __PROFILE__;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index aaa5cf85a6973..705fb4fbb3a3c 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -11,6 +11,7 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as ExportsType from './ReactFeatureFlags.test-renderer.www';
export const debugRenderPhaseSideEffectsForStrictMode = false;
+export const enableDebugTracing = false;
export const warnAboutDeprecatedLifecycles = true;
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
export const enableProfilerTimer = __PROFILE__;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js
index 5936e74cb7e55..53000c93edc02 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.js
@@ -11,6 +11,7 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as ExportsType from './ReactFeatureFlags.testing';
export const debugRenderPhaseSideEffectsForStrictMode = false;
+export const enableDebugTracing = false;
export const warnAboutDeprecatedLifecycles = true;
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
export const enableProfilerTimer = __PROFILE__;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js
index fee06bb56d8a1..c05fbbd2d5adb 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js
@@ -11,6 +11,7 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as ExportsType from './ReactFeatureFlags.testing.www';
export const debugRenderPhaseSideEffectsForStrictMode = false;
+export const enableDebugTracing = false;
export const warnAboutDeprecatedLifecycles = true;
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
export const enableProfilerTimer = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
index fcda77c24b5d9..718c211394ec1 100644
--- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
@@ -20,6 +20,7 @@ export const disableInputAttributeSyncing = __VARIANT__;
export const enableFilterEmptyStringAttributesDOM = __VARIANT__;
export const enableModernEventSystem = __VARIANT__;
export const enableLegacyFBSupport = __VARIANT__;
+export const enableDebugTracing = __VARIANT__;
// These are already tested in both modes using the build type dimension,
// so we don't need to use __VARIANT__ to get extra coverage.
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index 5d1c04fa8c463..43760512d04f4 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -26,6 +26,7 @@ export const {
enableModernEventSystem,
enableFilterEmptyStringAttributesDOM,
enableLegacyFBSupport,
+ enableDebugTracing,
} = dynamicFeatureFlags;
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js
index 93a21b6570835..c4cff2e866d3f 100644
--- a/packages/shared/isValidElementType.js
+++ b/packages/shared/isValidElementType.js
@@ -13,6 +13,7 @@ import {
REACT_FRAGMENT_TYPE,
REACT_PROFILER_TYPE,
REACT_PROVIDER_TYPE,
+ REACT_DEBUG_TRACING_MODE_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_SUSPENSE_TYPE,
REACT_SUSPENSE_LIST_TYPE,
@@ -32,6 +33,7 @@ export default function isValidElementType(type: mixed) {
// Note: its typeof might be other than 'symbol' or 'number' if it's a polyfill.
type === REACT_FRAGMENT_TYPE ||
type === REACT_PROFILER_TYPE ||
+ type === REACT_DEBUG_TRACING_MODE_TYPE ||
type === REACT_STRICT_MODE_TYPE ||
type === REACT_SUSPENSE_TYPE ||
type === REACT_SUSPENSE_LIST_TYPE ||