Skip to content

Commit fe9446f

Browse files
author
Brian Vaughn
committed
Refactored interaction tracing to fix ref counting bug
1 parent 2923bf5 commit fe9446f

3 files changed

Lines changed: 108 additions & 107 deletions

File tree

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ import {
4141
Update,
4242
Ref,
4343
} from 'shared/ReactSideEffectTags';
44+
import {captureWillSyncRenderPlaceholder} from './ReactFiberScheduler';
4445
import ReactSharedInternals from 'shared/ReactSharedInternals';
4546
import {
4647
enableGetDerivedStateFromCatch,
4748
enableSuspense,
4849
debugRenderPhaseSideEffects,
4950
debugRenderPhaseSideEffectsForStrictMode,
5051
enableProfilerTimer,
52+
enableSchedulerTracing,
5153
} from 'shared/ReactFeatureFlags';
5254
import invariant from 'shared/invariant';
5355
import getComponentName from 'shared/getComponentName';
@@ -825,6 +827,13 @@ function updatePlaceholderComponent(
825827

826828
let nextDidTimeout;
827829
if (current !== null && workInProgress.updateQueue !== null) {
830+
if (enableSchedulerTracing) {
831+
// Handle special case of rendering a Placeholder for a sync, suspended tree.
832+
// We flag this to properly trace and count interactions.
833+
// Otherwise interaction pending count will be decremented too many times.
834+
captureWillSyncRenderPlaceholder();
835+
}
836+
828837
// We're outside strict mode. Something inside this Placeholder boundary
829838
// suspended during the last commit. Switch to the placholder.
830839
workInProgress.updateQueue = null;

packages/react-reconciler/src/ReactFiberScheduler.js

Lines changed: 90 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@ let interruptedBy: Fiber | null = null;
260260

261261
// Do not decrement interaction counts in the event of suspense timeouts.
262262
// This would lead to prematurely calling the interaction-complete hook.
263-
let suspenseDidTimeout: boolean = false;
263+
// Instead we wait until the suspended promise has resolved.
264+
let interactionsHaveBeenSuspended: boolean = false;
264265

265266
let stashedWorkInProgressProperties;
266267
let replayUnitOfWork;
@@ -566,31 +567,11 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
566567
markCommittedPriorityLevels(root, earliestRemainingTimeBeforeCommit);
567568

568569
let prevInteractions: Set<Interaction> = (null: any);
569-
let committedInteractions: Array<Interaction> = enableSchedulerTracing
570-
? []
571-
: (null: any);
572570
if (enableSchedulerTracing) {
573571
// Restore any pending interactions at this point,
574572
// So that cascading work triggered during the render phase will be accounted for.
575573
prevInteractions = __interactionsRef.current;
576574
__interactionsRef.current = root.memoizedInteractions;
577-
578-
// We are potentially finished with the current batch of interactions.
579-
// So we should clear them out of the pending interaction map.
580-
// We do this at the start of commit in case cascading work is scheduled by commit phase lifecycles.
581-
// In that event, interaction data may be added back into the pending map for a future commit.
582-
// We also store the interactions we are about to commit so that we can notify subscribers after we're done.
583-
// These are stored as an Array rather than a Set,
584-
// Because the same interaction may be pending for multiple expiration times,
585-
// In which case it's important that we decrement the count the right number of times after finishing.
586-
root.pendingInteractionMap.forEach(
587-
(scheduledInteractions, scheduledExpirationTime) => {
588-
if (scheduledExpirationTime <= committedExpirationTime) {
589-
committedInteractions.push(...Array.from(scheduledInteractions));
590-
root.pendingInteractionMap.delete(scheduledExpirationTime);
591-
}
592-
},
593-
);
594575
}
595576

596577
// Reset this to null before calling lifecycles
@@ -789,27 +770,38 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
789770
unhandledError = error;
790771
}
791772
} finally {
792-
// Don't update interaction counts if we're frozen due to suspense.
793-
// In this case, we can skip the completed-work check entirely.
794-
if (!suspenseDidTimeout) {
795-
// Now that we're done, check the completed batch of interactions.
796-
// If no more work is outstanding for a given interaction,
797-
// We need to notify the subscribers that it's finished.
798-
committedInteractions.forEach(interaction => {
799-
interaction.__count--;
800-
if (subscriber !== null && interaction.__count === 0) {
801-
try {
802-
subscriber.onInteractionScheduledWorkCompleted(interaction);
803-
} catch (error) {
804-
// It's not safe for commitRoot() to throw.
805-
// Store the error for now and we'll re-throw in finishRendering().
806-
if (!hasUnhandledError) {
807-
hasUnhandledError = true;
808-
unhandledError = error;
809-
}
773+
if (!interactionsHaveBeenSuspended) {
774+
// Clear completed interactions from the pending Map.
775+
// Unless the render was suspended or cascading work was scheduled,
776+
// In which case– leave pending interactions until the subsequent render.
777+
const pendingInteractionMap = root.pendingInteractionMap;
778+
pendingInteractionMap.forEach(
779+
(scheduledInteractions, scheduledExpirationTime) => {
780+
if (
781+
earliestRemainingTimeAfterCommit === NoWork ||
782+
scheduledExpirationTime < earliestRemainingTimeAfterCommit
783+
) {
784+
pendingInteractionMap.delete(scheduledExpirationTime);
785+
786+
scheduledInteractions.forEach(interaction => {
787+
interaction.__count--;
788+
789+
if (subscriber !== null && interaction.__count === 0) {
790+
try {
791+
subscriber.onInteractionScheduledWorkCompleted(interaction);
792+
} catch (error) {
793+
// It's not safe for commitRoot() to throw.
794+
// Store the error for now and we'll re-throw in finishRendering().
795+
if (!hasUnhandledError) {
796+
hasUnhandledError = true;
797+
unhandledError = error;
798+
}
799+
}
800+
}
801+
});
810802
}
811-
}
812-
});
803+
},
804+
);
813805
}
814806
}
815807
}
@@ -1174,14 +1166,6 @@ function renderRoot(
11741166

11751167
const expirationTime = root.nextExpirationTimeToWorkOn;
11761168

1177-
let prevInteractions: Set<Interaction> = (null: any);
1178-
if (enableSchedulerTracing) {
1179-
// We're about to start new traced work.
1180-
// Restore pending interactions so cascading work triggered during the render phase will be accounted for.
1181-
prevInteractions = __interactionsRef.current;
1182-
__interactionsRef.current = root.memoizedInteractions;
1183-
}
1184-
11851169
// Check if we're starting from a fresh stack, or if we're resuming from
11861170
// previously yielded work.
11871171
if (
@@ -1201,6 +1185,11 @@ function renderRoot(
12011185
root.pendingCommitExpirationTime = NoWork;
12021186

12031187
if (enableSchedulerTracing) {
1188+
// Reset this flag once we start rendering a new root or at a new priority.
1189+
// This might indicate that suspended work has completed.
1190+
// If not, the flag will be reset.
1191+
interactionsHaveBeenSuspended = false;
1192+
12041193
// Determine which interactions this batch of work currently includes,
12051194
// So that we can accurately attribute time spent working on it,
12061195
// And so that cascading work triggered during the render phase will be associated with it.
@@ -1244,6 +1233,14 @@ function renderRoot(
12441233
}
12451234
}
12461235

1236+
let prevInteractions: Set<Interaction> = (null: any);
1237+
if (enableSchedulerTracing) {
1238+
// We're about to start new traced work.
1239+
// Restore pending interactions so cascading work triggered during the render phase will be accounted for.
1240+
prevInteractions = __interactionsRef.current;
1241+
__interactionsRef.current = root.memoizedInteractions;
1242+
}
1243+
12471244
let didFatal = false;
12481245

12491246
startWorkLoopTimer(nextUnitOfWork);
@@ -1403,6 +1400,9 @@ function renderRoot(
14031400

14041401
if (enableSuspense && !isExpired && nextLatestAbsoluteTimeoutMs !== -1) {
14051402
// The tree was suspended.
1403+
if (enableSchedulerTracing) {
1404+
interactionsHaveBeenSuspended = true;
1405+
}
14061406
const suspendedExpirationTime = expirationTime;
14071407
markSuspendedPriorityLevel(root, suspendedExpirationTime);
14081408

@@ -1600,6 +1600,7 @@ function retrySuspendedRoot(
16001600
if (isPriorityLevelSuspended(root, suspendedTime)) {
16011601
// Ping at the original level
16021602
retryTime = suspendedTime;
1603+
16031604
markPingedPriorityLevel(root, retryTime);
16041605
} else {
16051606
// Placeholder already timed out. Compute a new expiration time
@@ -1611,18 +1612,7 @@ function retrySuspendedRoot(
16111612
scheduleWorkToRoot(fiber, retryTime);
16121613
const rootExpirationTime = root.expirationTime;
16131614
if (rootExpirationTime !== NoWork) {
1614-
if (enableSchedulerTracing) {
1615-
// Restore previous interactions so that new work is associated with them.
1616-
let prevInteractions = __interactionsRef.current;
1617-
__interactionsRef.current = root.memoizedInteractions;
1618-
// Because suspense timeouts do not decrement the interaction count,
1619-
// Continued suspense work should also not increment the count.
1620-
storeInteractionsForExpirationTime(root, rootExpirationTime, false);
1621-
requestWork(root, rootExpirationTime);
1622-
__interactionsRef.current = prevInteractions;
1623-
} else {
1624-
requestWork(root, rootExpirationTime);
1625-
}
1615+
requestWork(root, rootExpirationTime);
16261616
}
16271617
}
16281618
}
@@ -1677,49 +1667,6 @@ function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
16771667
return null;
16781668
}
16791669

1680-
function storeInteractionsForExpirationTime(
1681-
root: FiberRoot,
1682-
expirationTime: ExpirationTime,
1683-
updateInteractionCounts: boolean,
1684-
): void {
1685-
if (!enableSchedulerTracing) {
1686-
return;
1687-
}
1688-
1689-
const interactions = __interactionsRef.current;
1690-
if (interactions.size > 0) {
1691-
const pendingInteractions = root.pendingInteractionMap.get(expirationTime);
1692-
if (pendingInteractions != null) {
1693-
interactions.forEach(interaction => {
1694-
if (updateInteractionCounts && !pendingInteractions.has(interaction)) {
1695-
// Update the pending async work count for previously unscheduled interaction.
1696-
interaction.__count++;
1697-
}
1698-
1699-
pendingInteractions.add(interaction);
1700-
});
1701-
} else {
1702-
root.pendingInteractionMap.set(expirationTime, new Set(interactions));
1703-
1704-
// Update the pending async work count for the current interactions.
1705-
if (updateInteractionCounts) {
1706-
interactions.forEach(interaction => {
1707-
interaction.__count++;
1708-
});
1709-
}
1710-
}
1711-
1712-
const subscriber = __subscriberRef.current;
1713-
if (subscriber !== null) {
1714-
const threadID = computeThreadID(
1715-
expirationTime,
1716-
root.interactionThreadID,
1717-
);
1718-
subscriber.onWorkScheduled(interactions, threadID);
1719-
}
1720-
}
1721-
}
1722-
17231670
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
17241671
recordScheduleUpdate();
17251672

@@ -1742,7 +1689,37 @@ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
17421689
}
17431690

17441691
if (enableSchedulerTracing) {
1745-
storeInteractionsForExpirationTime(root, expirationTime, true);
1692+
const interactions = __interactionsRef.current;
1693+
if (interactions.size > 0) {
1694+
const pendingInteractionMap = root.pendingInteractionMap;
1695+
const pendingInteractions = pendingInteractionMap.get(expirationTime);
1696+
if (pendingInteractions != null) {
1697+
interactions.forEach(interaction => {
1698+
if (!pendingInteractions.has(interaction)) {
1699+
// Update the pending async work count for previously unscheduled interaction.
1700+
interaction.__count++;
1701+
}
1702+
1703+
pendingInteractions.add(interaction);
1704+
});
1705+
} else {
1706+
pendingInteractionMap.set(expirationTime, new Set(interactions));
1707+
1708+
// Update the pending async work count for the current interactions.
1709+
interactions.forEach(interaction => {
1710+
interaction.__count++;
1711+
});
1712+
}
1713+
1714+
const subscriber = __subscriberRef.current;
1715+
if (subscriber !== null) {
1716+
const threadID = computeThreadID(
1717+
expirationTime,
1718+
root.interactionThreadID,
1719+
);
1720+
subscriber.onWorkScheduled(interactions, threadID);
1721+
}
1722+
}
17461723
}
17471724

17481725
if (
@@ -1935,9 +1912,8 @@ function onTimeout(root, finishedWork, suspendedExpirationTime) {
19351912
if (enableSchedulerTracing) {
19361913
// Don't update pending interaction counts for suspense timeouts,
19371914
// Because we know we still need to do more work in this case.
1938-
suspenseDidTimeout = true;
1915+
interactionsHaveBeenSuspended = true;
19391916
flushRoot(root, suspendedExpirationTime);
1940-
suspenseDidTimeout = false;
19411917
} else {
19421918
flushRoot(root, suspendedExpirationTime);
19431919
}
@@ -2508,6 +2484,12 @@ function flushControlled(fn: () => mixed): void {
25082484
}
25092485
}
25102486

2487+
function captureWillSyncRenderPlaceholder() {
2488+
if (enableSchedulerTracing) {
2489+
interactionsHaveBeenSuspended = true;
2490+
}
2491+
}
2492+
25112493
export {
25122494
requestCurrentTime,
25132495
computeExpirationForFiber,
@@ -2530,4 +2512,5 @@ export {
25302512
interactiveUpdates,
25312513
flushInteractiveUpdates,
25322514
computeUniqueAsyncExpiration,
2515+
captureWillSyncRenderPlaceholder,
25332516
};

packages/react-reconciler/src/ReactFiberUnwindWork.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
import {
3737
enableGetDerivedStateFromCatch,
3838
enableSuspense,
39+
enableSchedulerTracing,
3940
} from 'shared/ReactFeatureFlags';
4041
import {StrictMode, AsyncMode} from './ReactTypeOfMode';
4142

@@ -60,6 +61,7 @@ import {
6061
markLegacyErrorBoundaryAsFailed,
6162
isAlreadyFailedLegacyErrorBoundary,
6263
retrySuspendedRoot,
64+
captureWillSyncRenderPlaceholder,
6365
} from './ReactFiberScheduler';
6466
import {Sync} from './ReactFiberExpirationTime';
6567

@@ -236,6 +238,13 @@ function throwException(
236238
if ((workInProgress.mode & StrictMode) === NoEffect) {
237239
workInProgress.effectTag |= UpdateEffect;
238240

241+
if (enableSchedulerTracing) {
242+
// Handles the special case of unwinding a suspended sync render.
243+
// We flag this to properly trace and count interactions.
244+
// Otherwise interaction pending count will be decremented too many times.
245+
captureWillSyncRenderPlaceholder();
246+
}
247+
239248
// Unmount the source fiber's children
240249
const nextChildren = null;
241250
reconcileChildren(

0 commit comments

Comments
 (0)