Skip to content

Commit d836010

Browse files
authored
Wrap retrySuspendedRoot using SchedulerTracing (#13776)
Previously, we were emptying root.pendingInteractionMap and permanently losing those interactions when applying an unrelated update to a tree that has no scheduled work that is waiting on promise resolution. (That is, one that is showing a fallback and waiting for the suspended content to resolve.) The logic I'm leaving untouched with `nextRenderIncludesTimedOutPlaceholder` is *not* correct -- what we want is instead to know if *any* placeholder anywhere in the tree is showing its fallback -- but we don't currently have a better replacement, and this should unblock tracing with suspense again.
1 parent 40a521a commit d836010

4 files changed

Lines changed: 122 additions & 129 deletions

File tree

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,12 @@ import {
4343
Update,
4444
Ref,
4545
} from 'shared/ReactSideEffectTags';
46-
import {captureWillSyncRenderPlaceholder} from './ReactFiberScheduler';
4746
import ReactSharedInternals from 'shared/ReactSharedInternals';
4847
import {
4948
enableSuspense,
5049
debugRenderPhaseSideEffects,
5150
debugRenderPhaseSideEffectsForStrictMode,
5251
enableProfilerTimer,
53-
enableSchedulerTracing,
5452
} from 'shared/ReactFeatureFlags';
5553
import invariant from 'shared/invariant';
5654
import shallowEqual from 'shared/shallowEqual';
@@ -952,13 +950,6 @@ function updatePlaceholderComponent(
952950

953951
let nextDidTimeout;
954952
if (current !== null && workInProgress.updateQueue !== null) {
955-
if (enableSchedulerTracing) {
956-
// Handle special case of rendering a Placeholder for a sync, suspended tree.
957-
// We flag this to properly trace and count interactions.
958-
// Otherwise interaction pending count will be decremented too many times.
959-
captureWillSyncRenderPlaceholder();
960-
}
961-
962953
// We're outside strict mode. Something inside this Placeholder boundary
963954
// suspended during the last commit. Switch to the placholder.
964955
workInProgress.updateQueue = null;

packages/react-reconciler/src/ReactFiberScheduler.js

Lines changed: 81 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -257,11 +257,6 @@ let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
257257
// Used for performance tracking.
258258
let interruptedBy: Fiber | null = null;
259259

260-
// Do not decrement interaction counts in the event of suspense timeouts.
261-
// This would lead to prematurely calling the interaction-complete hook.
262-
// Instead we wait until the suspended promise has resolved.
263-
let nextRenderIncludesTimedOutPlaceholder: boolean = false;
264-
265260
let stashedWorkInProgressProperties;
266261
let replayUnitOfWork;
267262
let isReplayingFailedUnitOfWork;
@@ -766,42 +761,40 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
766761
unhandledError = error;
767762
}
768763
} finally {
769-
if (!nextRenderIncludesTimedOutPlaceholder) {
770-
// Clear completed interactions from the pending Map.
771-
// Unless the render was suspended or cascading work was scheduled,
772-
// In which case– leave pending interactions until the subsequent render.
773-
const pendingInteractionMap = root.pendingInteractionMap;
774-
pendingInteractionMap.forEach(
775-
(scheduledInteractions, scheduledExpirationTime) => {
776-
// Only decrement the pending interaction count if we're done.
777-
// If there's still work at the current priority,
778-
// That indicates that we are waiting for suspense data.
779-
if (
780-
earliestRemainingTimeAfterCommit === NoWork ||
781-
scheduledExpirationTime < earliestRemainingTimeAfterCommit
782-
) {
783-
pendingInteractionMap.delete(scheduledExpirationTime);
784-
785-
scheduledInteractions.forEach(interaction => {
786-
interaction.__count--;
787-
788-
if (subscriber !== null && interaction.__count === 0) {
789-
try {
790-
subscriber.onInteractionScheduledWorkCompleted(interaction);
791-
} catch (error) {
792-
// It's not safe for commitRoot() to throw.
793-
// Store the error for now and we'll re-throw in finishRendering().
794-
if (!hasUnhandledError) {
795-
hasUnhandledError = true;
796-
unhandledError = error;
797-
}
764+
// Clear completed interactions from the pending Map.
765+
// Unless the render was suspended or cascading work was scheduled,
766+
// In which case– leave pending interactions until the subsequent render.
767+
const pendingInteractionMap = root.pendingInteractionMap;
768+
pendingInteractionMap.forEach(
769+
(scheduledInteractions, scheduledExpirationTime) => {
770+
// Only decrement the pending interaction count if we're done.
771+
// If there's still work at the current priority,
772+
// That indicates that we are waiting for suspense data.
773+
if (
774+
earliestRemainingTimeAfterCommit === NoWork ||
775+
scheduledExpirationTime < earliestRemainingTimeAfterCommit
776+
) {
777+
pendingInteractionMap.delete(scheduledExpirationTime);
778+
779+
scheduledInteractions.forEach(interaction => {
780+
interaction.__count--;
781+
782+
if (subscriber !== null && interaction.__count === 0) {
783+
try {
784+
subscriber.onInteractionScheduledWorkCompleted(interaction);
785+
} catch (error) {
786+
// It's not safe for commitRoot() to throw.
787+
// Store the error for now and we'll re-throw in finishRendering().
788+
if (!hasUnhandledError) {
789+
hasUnhandledError = true;
790+
unhandledError = error;
798791
}
799792
}
800-
});
801-
}
802-
},
803-
);
804-
}
793+
}
794+
});
795+
}
796+
},
797+
);
805798
}
806799
}
807800
}
@@ -1173,11 +1166,6 @@ function renderRoot(
11731166
root.pendingCommitExpirationTime = NoWork;
11741167

11751168
if (enableSchedulerTracing) {
1176-
// Reset this flag once we start rendering a new root or at a new priority.
1177-
// This might indicate that suspended work has completed.
1178-
// If not, the flag will be reset.
1179-
nextRenderIncludesTimedOutPlaceholder = false;
1180-
11811169
// Determine which interactions this batch of work currently includes,
11821170
// So that we can accurately attribute time spent working on it,
11831171
// And so that cascading work triggered during the render phase will be associated with it.
@@ -1388,9 +1376,6 @@ function renderRoot(
13881376

13891377
if (enableSuspense && !isExpired && nextLatestAbsoluteTimeoutMs !== -1) {
13901378
// The tree was suspended.
1391-
if (enableSchedulerTracing) {
1392-
nextRenderIncludesTimedOutPlaceholder = true;
1393-
}
13941379
const suspendedExpirationTime = expirationTime;
13951380
markSuspendedPriorityLevel(root, suspendedExpirationTime);
13961381

@@ -1597,6 +1582,13 @@ function retrySuspendedRoot(
15971582
markPendingPriorityLevel(root, retryTime);
15981583
}
15991584

1585+
// TODO: If the placeholder fiber has already rendered the primary children
1586+
// without suspending (that is, all of the promises have already resolved),
1587+
// we should not trigger another update here. One case this happens is when
1588+
// we are in sync mode and a single promise is thrown both on initial render
1589+
// and on update; we attach two .then(retrySuspendedRoot) callbacks and each
1590+
// one performs Sync work, rerendering the Placeholder.
1591+
16001592
if ((fiber.mode & ConcurrentMode) !== NoContext) {
16011593
if (root === nextRoot && nextRenderExpirationTime === suspendedTime) {
16021594
// Received a ping at the same priority level at which we're currently
@@ -1614,6 +1606,15 @@ function retrySuspendedRoot(
16141606
}
16151607

16161608
function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
1609+
recordScheduleUpdate();
1610+
1611+
if (__DEV__) {
1612+
if (fiber.tag === ClassComponent || fiber.tag === ClassComponentLazy) {
1613+
const instance = fiber.stateNode;
1614+
warnAboutInvalidUpdates(instance);
1615+
}
1616+
}
1617+
16171618
// Update the source fiber's expiration time
16181619
if (
16191620
fiber.expirationTime === NoWork ||
@@ -1631,57 +1632,47 @@ function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
16311632
}
16321633
// Walk the parent path to the root and update the child expiration time.
16331634
let node = fiber.return;
1635+
let root = null;
16341636
if (node === null && fiber.tag === HostRoot) {
1635-
return fiber.stateNode;
1636-
}
1637-
while (node !== null) {
1638-
alternate = node.alternate;
1639-
if (
1640-
node.childExpirationTime === NoWork ||
1641-
node.childExpirationTime > expirationTime
1642-
) {
1643-
node.childExpirationTime = expirationTime;
1637+
root = fiber.stateNode;
1638+
} else {
1639+
while (node !== null) {
1640+
alternate = node.alternate;
16441641
if (
1642+
node.childExpirationTime === NoWork ||
1643+
node.childExpirationTime > expirationTime
1644+
) {
1645+
node.childExpirationTime = expirationTime;
1646+
if (
1647+
alternate !== null &&
1648+
(alternate.childExpirationTime === NoWork ||
1649+
alternate.childExpirationTime > expirationTime)
1650+
) {
1651+
alternate.childExpirationTime = expirationTime;
1652+
}
1653+
} else if (
16451654
alternate !== null &&
16461655
(alternate.childExpirationTime === NoWork ||
16471656
alternate.childExpirationTime > expirationTime)
16481657
) {
16491658
alternate.childExpirationTime = expirationTime;
16501659
}
1651-
} else if (
1652-
alternate !== null &&
1653-
(alternate.childExpirationTime === NoWork ||
1654-
alternate.childExpirationTime > expirationTime)
1655-
) {
1656-
alternate.childExpirationTime = expirationTime;
1657-
}
1658-
if (node.return === null && node.tag === HostRoot) {
1659-
return node.stateNode;
1660-
}
1661-
node = node.return;
1662-
}
1663-
return null;
1664-
}
1665-
1666-
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1667-
recordScheduleUpdate();
1668-
1669-
if (__DEV__) {
1670-
if (fiber.tag === ClassComponent || fiber.tag === ClassComponentLazy) {
1671-
const instance = fiber.stateNode;
1672-
warnAboutInvalidUpdates(instance);
1660+
if (node.return === null && node.tag === HostRoot) {
1661+
root = node.stateNode;
1662+
break;
1663+
}
1664+
node = node.return;
16731665
}
16741666
}
16751667

1676-
const root = scheduleWorkToRoot(fiber, expirationTime);
16771668
if (root === null) {
16781669
if (
16791670
__DEV__ &&
16801671
(fiber.tag === ClassComponent || fiber.tag === ClassComponentLazy)
16811672
) {
16821673
warnAboutUpdateOnUnmounted(fiber);
16831674
}
1684-
return;
1675+
return null;
16851676
}
16861677

16871678
if (enableSchedulerTracing) {
@@ -1718,6 +1709,15 @@ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
17181709
}
17191710
}
17201711

1712+
return root;
1713+
}
1714+
1715+
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
1716+
const root = scheduleWorkToRoot(fiber, expirationTime);
1717+
if (root === null) {
1718+
return;
1719+
}
1720+
17211721
if (
17221722
!isWorking &&
17231723
nextRenderExpirationTime !== NoWork &&
@@ -1904,15 +1904,7 @@ function onTimeout(root, finishedWork, suspendedExpirationTime) {
19041904
// because we're at the top of a timer event.
19051905
recomputeCurrentRendererTime();
19061906
currentSchedulerTime = currentRendererTime;
1907-
1908-
if (enableSchedulerTracing) {
1909-
// Don't update pending interaction counts for suspense timeouts,
1910-
// Because we know we still need to do more work in this case.
1911-
nextRenderIncludesTimedOutPlaceholder = true;
1912-
flushRoot(root, suspendedExpirationTime);
1913-
} else {
1914-
flushRoot(root, suspendedExpirationTime);
1915-
}
1907+
flushRoot(root, suspendedExpirationTime);
19161908
}
19171909
}
19181910

@@ -2480,12 +2472,6 @@ function flushControlled(fn: () => mixed): void {
24802472
}
24812473
}
24822474

2483-
function captureWillSyncRenderPlaceholder() {
2484-
if (enableSchedulerTracing) {
2485-
nextRenderIncludesTimedOutPlaceholder = true;
2486-
}
2487-
}
2488-
24892475
export {
24902476
requestCurrentTime,
24912477
computeExpirationForFiber,
@@ -2508,5 +2494,4 @@ export {
25082494
interactiveUpdates,
25092495
flushInteractiveUpdates,
25102496
computeUniqueAsyncExpiration,
2511-
captureWillSyncRenderPlaceholder,
25122497
};

packages/react-reconciler/src/ReactFiberUnwindWork.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {CapturedValue} from './ReactCapturedValue';
1414
import type {Update} from './ReactUpdateQueue';
1515
import type {Thenable} from './ReactFiberScheduler';
1616

17+
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
1718
import getComponentName from 'shared/getComponentName';
1819
import warningWithoutStack from 'shared/warningWithoutStack';
1920
import {
@@ -59,7 +60,6 @@ import {
5960
markLegacyErrorBoundaryAsFailed,
6061
isAlreadyFailedLegacyErrorBoundary,
6162
retrySuspendedRoot,
62-
captureWillSyncRenderPlaceholder,
6363
} from './ReactFiberScheduler';
6464
import {Sync} from './ReactFiberExpirationTime';
6565

@@ -224,12 +224,15 @@ function throwException(
224224
: renderExpirationTime;
225225

226226
// Attach a listener to the promise to "ping" the root and retry.
227-
const onResolveOrReject = retrySuspendedRoot.bind(
227+
let onResolveOrReject = retrySuspendedRoot.bind(
228228
null,
229229
root,
230230
workInProgress,
231231
pingTime,
232232
);
233+
if (enableSchedulerTracing) {
234+
onResolveOrReject = Schedule_tracing_wrap(onResolveOrReject);
235+
}
233236
thenable.then(onResolveOrReject, onResolveOrReject);
234237

235238
// If the boundary is outside of strict mode, we should *not* suspend
@@ -243,13 +246,6 @@ function throwException(
243246
if ((workInProgress.mode & StrictMode) === NoEffect) {
244247
workInProgress.effectTag |= UpdateEffect;
245248

246-
if (enableSchedulerTracing) {
247-
// Handles the special case of unwinding a suspended sync render.
248-
// We flag this to properly trace and count interactions.
249-
// Otherwise interaction pending count will be decremented too many times.
250-
captureWillSyncRenderPlaceholder();
251-
}
252-
253249
// Unmount the source fiber's children
254250
const nextChildren = null;
255251
reconcileChildren(

0 commit comments

Comments
 (0)