Skip to content

Commit af87f5a

Browse files
author
Brian Vaughn
authored
Scheduling Profiler marks should include thrown Errors (#22417)
The scheduling profiler markComponentRenderStopped method is supposed to be called when rendering finishes or when a value is thrown (Suspense or Error). Previously we were calling this in a Suspense-only path of `throwException`. This PR updates the code to handle errors (or non-Thenables) thrown as well. It also moves the mark logic the work loop `handleError` method, with Suspense/Error agnostic cleanup.
1 parent b1a1cb1 commit af87f5a

6 files changed

+218
-20
lines changed

packages/react-reconciler/src/ReactFiberThrow.new.js

-10
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new';
4141
import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
4242
import {
4343
enableDebugTracing,
44-
enableSchedulingProfiler,
4544
enableLazyContextPropagation,
4645
enableUpdaterTracking,
4746
enablePersistentOffscreenHostContainer,
@@ -71,10 +70,6 @@ import {
7170
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.new';
7271
import {logCapturedError} from './ReactFiberErrorLogger';
7372
import {logComponentSuspended} from './DebugTracing';
74-
import {
75-
markComponentRenderStopped,
76-
markComponentSuspended,
77-
} from './SchedulingProfiler';
7873
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
7974
import {
8075
SyncLane,
@@ -247,11 +242,6 @@ function throwException(
247242
}
248243
}
249244

250-
if (enableSchedulingProfiler) {
251-
markComponentRenderStopped();
252-
markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
253-
}
254-
255245
// Reset the memoizedState to what it was before we attempted to render it.
256246
// A legacy mode Suspense quirk, only relevant to hook components.
257247
const tag = sourceFiber.tag;

packages/react-reconciler/src/ReactFiberThrow.old.js

-10
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old';
4141
import {NoMode, ConcurrentMode, DebugTracingMode} from './ReactTypeOfMode';
4242
import {
4343
enableDebugTracing,
44-
enableSchedulingProfiler,
4544
enableLazyContextPropagation,
4645
enableUpdaterTracking,
4746
enablePersistentOffscreenHostContainer,
@@ -71,10 +70,6 @@ import {
7170
import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext.old';
7271
import {logCapturedError} from './ReactFiberErrorLogger';
7372
import {logComponentSuspended} from './DebugTracing';
74-
import {
75-
markComponentRenderStopped,
76-
markComponentSuspended,
77-
} from './SchedulingProfiler';
7873
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
7974
import {
8075
SyncLane,
@@ -247,11 +242,6 @@ function throwException(
247242
}
248243
}
249244

250-
if (enableSchedulingProfiler) {
251-
markComponentRenderStopped();
252-
markComponentSuspended(sourceFiber, wakeable, rootRenderLanes);
253-
}
254-
255245
// Reset the memoizedState to what it was before we attempted to render it.
256246
// A legacy mode Suspense quirk, only relevant to hook components.
257247
const tag = sourceFiber.tag;

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+26
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ import {
6868
import {
6969
markCommitStarted,
7070
markCommitStopped,
71+
markComponentRenderStopped,
72+
markComponentSuspended,
73+
markComponentErrored,
7174
markLayoutEffectsStarted,
7275
markLayoutEffectsStopped,
7376
markPassiveEffectsStarted,
@@ -1356,6 +1359,29 @@ function handleError(root, thrownValue): void {
13561359
stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
13571360
}
13581361

1362+
if (enableSchedulingProfiler) {
1363+
markComponentRenderStopped();
1364+
1365+
if (
1366+
thrownValue !== null &&
1367+
typeof thrownValue === 'object' &&
1368+
typeof thrownValue.then === 'function'
1369+
) {
1370+
const wakeable: Wakeable = (thrownValue: any);
1371+
markComponentSuspended(
1372+
erroredWork,
1373+
wakeable,
1374+
workInProgressRootRenderLanes,
1375+
);
1376+
} else {
1377+
markComponentErrored(
1378+
erroredWork,
1379+
thrownValue,
1380+
workInProgressRootRenderLanes,
1381+
);
1382+
}
1383+
}
1384+
13591385
throwException(
13601386
root,
13611387
erroredWork.return,

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

+26
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ import {
6868
import {
6969
markCommitStarted,
7070
markCommitStopped,
71+
markComponentRenderStopped,
72+
markComponentSuspended,
73+
markComponentErrored,
7174
markLayoutEffectsStarted,
7275
markLayoutEffectsStopped,
7376
markPassiveEffectsStarted,
@@ -1356,6 +1359,29 @@ function handleError(root, thrownValue): void {
13561359
stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
13571360
}
13581361

1362+
if (enableSchedulingProfiler) {
1363+
markComponentRenderStopped();
1364+
1365+
if (
1366+
thrownValue !== null &&
1367+
typeof thrownValue === 'object' &&
1368+
typeof thrownValue.then === 'function'
1369+
) {
1370+
const wakeable: Wakeable = (thrownValue: any);
1371+
markComponentSuspended(
1372+
erroredWork,
1373+
wakeable,
1374+
workInProgressRootRenderLanes,
1375+
);
1376+
} else {
1377+
markComponentErrored(
1378+
erroredWork,
1379+
thrownValue,
1380+
workInProgressRootRenderLanes,
1381+
);
1382+
}
1383+
}
1384+
13591385
throwException(
13601386
root,
13611387
erroredWork.return,

packages/react-reconciler/src/SchedulingProfiler.js

+27
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,33 @@ export function markComponentRenderStopped(): void {
144144
}
145145
}
146146

147+
export function markComponentErrored(
148+
fiber: Fiber,
149+
thrownValue: mixed,
150+
lanes: Lanes,
151+
): void {
152+
if (enableSchedulingProfiler) {
153+
if (supportsUserTimingV3) {
154+
const componentName = getComponentNameFromFiber(fiber) || 'Unknown';
155+
const phase = fiber.alternate === null ? 'mount' : 'update';
156+
157+
let message = '';
158+
if (
159+
thrownValue !== null &&
160+
typeof thrownValue === 'object' &&
161+
typeof thrownValue.message === 'string'
162+
) {
163+
message = thrownValue.message;
164+
} else if (typeof thrownValue === 'string') {
165+
message = thrownValue;
166+
}
167+
168+
// TODO (scheduling profiler) Add component stack id
169+
markAndClear(`--error-${componentName}-${phase}-${message}`);
170+
}
171+
}
172+
}
173+
147174
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
148175

149176
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps

packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js

+139
Original file line numberDiff line numberDiff line change
@@ -715,4 +715,143 @@ describe('SchedulingProfiler', () => {
715715
`);
716716
}
717717
});
718+
719+
it('should mark sync render that throws', async () => {
720+
class ErrorBoundary extends React.Component {
721+
state = {error: null};
722+
componentDidCatch(error) {
723+
this.setState({error});
724+
}
725+
render() {
726+
if (this.state.error) {
727+
return null;
728+
}
729+
return this.props.children;
730+
}
731+
}
732+
733+
function ExampleThatThrows() {
734+
throw Error('Expected error');
735+
}
736+
737+
ReactTestRenderer.create(
738+
<ErrorBoundary>
739+
<ExampleThatThrows />
740+
</ErrorBoundary>,
741+
);
742+
743+
if (gate(flags => flags.enableSchedulingProfiler)) {
744+
expect(getMarks()).toMatchInlineSnapshot(`
745+
Array [
746+
"--schedule-render-1",
747+
"--render-start-1",
748+
"--component-render-start-ErrorBoundary",
749+
"--component-render-stop",
750+
"--component-render-start-ExampleThatThrows",
751+
"--component-render-start-ExampleThatThrows",
752+
"--component-render-stop",
753+
"--error-ExampleThatThrows-mount-Expected error",
754+
"--render-stop",
755+
"--commit-start-1",
756+
"--react-version-17.0.3",
757+
"--profiler-version-1",
758+
"--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",
759+
"--layout-effects-start-1",
760+
"--schedule-state-update-1-ErrorBoundary",
761+
"--layout-effects-stop",
762+
"--commit-stop",
763+
"--render-start-1",
764+
"--component-render-start-ErrorBoundary",
765+
"--component-render-stop",
766+
"--render-stop",
767+
"--commit-start-1",
768+
"--react-version-17.0.3",
769+
"--profiler-version-1",
770+
"--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",
771+
"--commit-stop",
772+
]
773+
`);
774+
}
775+
});
776+
777+
it('should mark concurrent render that throws', async () => {
778+
spyOnProd(console, 'error');
779+
780+
class ErrorBoundary extends React.Component {
781+
state = {error: null};
782+
componentDidCatch(error) {
783+
this.setState({error});
784+
}
785+
render() {
786+
if (this.state.error) {
787+
return null;
788+
}
789+
return this.props.children;
790+
}
791+
}
792+
793+
function ExampleThatThrows() {
794+
// eslint-disable-next-line no-throw-literal
795+
throw 'Expected error';
796+
}
797+
798+
ReactTestRenderer.create(
799+
<ErrorBoundary>
800+
<ExampleThatThrows />
801+
</ErrorBoundary>,
802+
{unstable_isConcurrent: true},
803+
);
804+
805+
if (gate(flags => flags.enableSchedulingProfiler)) {
806+
expect(getMarks()).toMatchInlineSnapshot(`
807+
Array [
808+
"--schedule-render-16",
809+
]
810+
`);
811+
}
812+
813+
clearPendingMarks();
814+
815+
expect(Scheduler).toFlushUntilNextPaint([]);
816+
817+
if (gate(flags => flags.enableSchedulingProfiler)) {
818+
expect(getMarks()).toMatchInlineSnapshot(`
819+
Array [
820+
"--render-start-16",
821+
"--component-render-start-ErrorBoundary",
822+
"--component-render-stop",
823+
"--component-render-start-ExampleThatThrows",
824+
"--component-render-start-ExampleThatThrows",
825+
"--component-render-stop",
826+
"--error-ExampleThatThrows-mount-Expected error",
827+
"--render-stop",
828+
"--render-start-16",
829+
"--component-render-start-ErrorBoundary",
830+
"--component-render-stop",
831+
"--component-render-start-ExampleThatThrows",
832+
"--component-render-start-ExampleThatThrows",
833+
"--component-render-stop",
834+
"--error-ExampleThatThrows-mount-Expected error",
835+
"--render-stop",
836+
"--commit-start-16",
837+
"--react-version-17.0.3",
838+
"--profiler-version-1",
839+
"--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",
840+
"--layout-effects-start-16",
841+
"--schedule-state-update-1-ErrorBoundary",
842+
"--layout-effects-stop",
843+
"--render-start-1",
844+
"--component-render-start-ErrorBoundary",
845+
"--component-render-stop",
846+
"--render-stop",
847+
"--commit-start-1",
848+
"--react-version-17.0.3",
849+
"--profiler-version-1",
850+
"--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",
851+
"--commit-stop",
852+
"--commit-stop",
853+
]
854+
`);
855+
}
856+
});
718857
});

0 commit comments

Comments
 (0)