diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js
index 5a01005c25537..5b07409a7729f 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.new.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.new.js
@@ -41,7 +41,6 @@ import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new';
import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
import {
enableDebugTracing,
- enableSchedulingProfiler,
enableLazyContextPropagation,
enableUpdaterTracking,
enablePersistentOffscreenHostContainer,
@@ -71,10 +70,6 @@ import {
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.new';
import {logCapturedError} from './ReactFiberErrorLogger';
import {logComponentSuspended} from './DebugTracing';
-import {
- markComponentRenderStopped,
- markComponentSuspended,
-} from './SchedulingProfiler';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
import {
SyncLane,
@@ -247,11 +242,6 @@ function throwException(
}
}
- if (enableSchedulingProfiler) {
- markComponentRenderStopped();
- markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
- }
-
// Reset the memoizedState to what it was before we attempted to render it.
// A legacy mode Suspense quirk, only relevant to hook components.
const tag = sourceFiber.tag;
diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js
index 6e060108c88ec..dcba4b521aebc 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.old.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.old.js
@@ -41,7 +41,6 @@ import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old';
import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
import {
enableDebugTracing,
- enableSchedulingProfiler,
enableLazyContextPropagation,
enableUpdaterTracking,
enablePersistentOffscreenHostContainer,
@@ -71,10 +70,6 @@ import {
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.old';
import {logCapturedError} from './ReactFiberErrorLogger';
import {logComponentSuspended} from './DebugTracing';
-import {
- markComponentRenderStopped,
- markComponentSuspended,
-} from './SchedulingProfiler';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
import {
SyncLane,
@@ -247,11 +242,6 @@ function throwException(
}
}
- if (enableSchedulingProfiler) {
- markComponentRenderStopped();
- markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
- }
-
// Reset the memoizedState to what it was before we attempted to render it.
// A legacy mode Suspense quirk, only relevant to hook components.
const tag = sourceFiber.tag;
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index ac420850fae82..dbeb72b856326 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -68,6 +68,9 @@ import {
import {
markCommitStarted,
markCommitStopped,
+ markComponentRenderStopped,
+ markComponentSuspended,
+ markComponentErrored,
markLayoutEffectsStarted,
markLayoutEffectsStopped,
markPassiveEffectsStarted,
@@ -1356,6 +1359,29 @@ function handleError(root, thrownValue): void {
stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
}
+ if (enableSchedulingProfiler) {
+ markComponentRenderStopped();
+
+ if (
+ thrownValue !== null &&
+ typeof thrownValue === 'object' &&
+ typeof thrownValue.then === 'function'
+ ) {
+ const wakeable: Wakeable = (thrownValue: any);
+ markComponentSuspended(
+ erroredWork,
+ wakeable,
+ workInProgressRootRenderLanes,
+ );
+ } else {
+ markComponentErrored(
+ erroredWork,
+ thrownValue,
+ workInProgressRootRenderLanes,
+ );
+ }
+ }
+
throwException(
root,
erroredWork.return,
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index c98c7abac71fe..af9e293d712a7 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -68,6 +68,9 @@ import {
import {
markCommitStarted,
markCommitStopped,
+ markComponentRenderStopped,
+ markComponentSuspended,
+ markComponentErrored,
markLayoutEffectsStarted,
markLayoutEffectsStopped,
markPassiveEffectsStarted,
@@ -1356,6 +1359,29 @@ function handleError(root, thrownValue): void {
stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
}
+ if (enableSchedulingProfiler) {
+ markComponentRenderStopped();
+
+ if (
+ thrownValue !== null &&
+ typeof thrownValue === 'object' &&
+ typeof thrownValue.then === 'function'
+ ) {
+ const wakeable: Wakeable = (thrownValue: any);
+ markComponentSuspended(
+ erroredWork,
+ wakeable,
+ workInProgressRootRenderLanes,
+ );
+ } else {
+ markComponentErrored(
+ erroredWork,
+ thrownValue,
+ workInProgressRootRenderLanes,
+ );
+ }
+ }
+
throwException(
root,
erroredWork.return,
diff --git a/packages/react-reconciler/src/SchedulingProfiler.js b/packages/react-reconciler/src/SchedulingProfiler.js
index b4e7f83d3a157..9b0985cf46139 100644
--- a/packages/react-reconciler/src/SchedulingProfiler.js
+++ b/packages/react-reconciler/src/SchedulingProfiler.js
@@ -144,6 +144,33 @@ export function markComponentRenderStopped(): void {
}
}
+export function markComponentErrored(
+ fiber: Fiber,
+ thrownValue: mixed,
+ lanes: Lanes,
+): void {
+ if (enableSchedulingProfiler) {
+ if (supportsUserTimingV3) {
+ const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
+ const phase = fiber.alternate === null ? 'mount' : 'update';
+
+ let message = '';
+ if (
+ thrownValue !== null &&
+ typeof thrownValue === 'object' &&
+ typeof thrownValue.message === 'string'
+ ) {
+ message = thrownValue.message;
+ } else if (typeof thrownValue === 'string') {
+ message = thrownValue;
+ }
+
+ // TODO (scheduling profiler) Add component stack id
+ markAndClear(`--error-${componentName}-${phase}-${message}`);
+ }
+ }
+}
+
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
index 8680d19b710bf..47768c52a88d9 100644
--- a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
@@ -715,4 +715,143 @@ describe('SchedulingProfiler', () => {
`);
}
});
+
+ it('should mark sync render that throws', async () => {
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ componentDidCatch(error) {
+ this.setState({error});
+ }
+ render() {
+ if (this.state.error) {
+ return null;
+ }
+ return this.props.children;
+ }
+ }
+
+ function ExampleThatThrows() {
+ throw Error('Expected error');
+ }
+
+ ReactTestRenderer.create(
+
+
+ ,
+ );
+
+ if (gate(flags => flags.enableSchedulingProfiler)) {
+ expect(getMarks()).toMatchInlineSnapshot(`
+ Array [
+ "--schedule-render-1",
+ "--render-start-1",
+ "--component-render-start-ErrorBoundary",
+ "--component-render-stop",
+ "--component-render-start-ExampleThatThrows",
+ "--component-render-start-ExampleThatThrows",
+ "--component-render-stop",
+ "--error-ExampleThatThrows-mount-Expected error",
+ "--render-stop",
+ "--commit-start-1",
+ "--react-version-17.0.3",
+ "--profiler-version-1",
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
+ "--layout-effects-start-1",
+ "--schedule-state-update-1-ErrorBoundary",
+ "--layout-effects-stop",
+ "--commit-stop",
+ "--render-start-1",
+ "--component-render-start-ErrorBoundary",
+ "--component-render-stop",
+ "--render-stop",
+ "--commit-start-1",
+ "--react-version-17.0.3",
+ "--profiler-version-1",
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
+ "--commit-stop",
+ ]
+ `);
+ }
+ });
+
+ it('should mark concurrent render that throws', async () => {
+ spyOnProd(console, 'error');
+
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ componentDidCatch(error) {
+ this.setState({error});
+ }
+ render() {
+ if (this.state.error) {
+ return null;
+ }
+ return this.props.children;
+ }
+ }
+
+ function ExampleThatThrows() {
+ // eslint-disable-next-line no-throw-literal
+ throw 'Expected error';
+ }
+
+ ReactTestRenderer.create(
+
+
+ ,
+ {unstable_isConcurrent: true},
+ );
+
+ if (gate(flags => flags.enableSchedulingProfiler)) {
+ expect(getMarks()).toMatchInlineSnapshot(`
+ Array [
+ "--schedule-render-16",
+ ]
+ `);
+ }
+
+ clearPendingMarks();
+
+ expect(Scheduler).toFlushUntilNextPaint([]);
+
+ if (gate(flags => flags.enableSchedulingProfiler)) {
+ expect(getMarks()).toMatchInlineSnapshot(`
+ Array [
+ "--render-start-16",
+ "--component-render-start-ErrorBoundary",
+ "--component-render-stop",
+ "--component-render-start-ExampleThatThrows",
+ "--component-render-start-ExampleThatThrows",
+ "--component-render-stop",
+ "--error-ExampleThatThrows-mount-Expected error",
+ "--render-stop",
+ "--render-start-16",
+ "--component-render-start-ErrorBoundary",
+ "--component-render-stop",
+ "--component-render-start-ExampleThatThrows",
+ "--component-render-start-ExampleThatThrows",
+ "--component-render-stop",
+ "--error-ExampleThatThrows-mount-Expected error",
+ "--render-stop",
+ "--commit-start-16",
+ "--react-version-17.0.3",
+ "--profiler-version-1",
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
+ "--layout-effects-start-16",
+ "--schedule-state-update-1-ErrorBoundary",
+ "--layout-effects-stop",
+ "--render-start-1",
+ "--component-render-start-ErrorBoundary",
+ "--component-render-stop",
+ "--render-stop",
+ "--commit-start-1",
+ "--react-version-17.0.3",
+ "--profiler-version-1",
+ "--react-lane-labels-Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen",
+ "--commit-stop",
+ "--commit-stop",
+ ]
+ `);
+ }
+ });
});