Skip to content

Commit 3c7667a

Browse files
authored
Unify perform{Sync,Concurrent}WorkOnRoot implementation (#31029)
Over time the behavior of these two paths has converged to be essentially the same. So this merges them back into one function. This should save some code size and also make it harder for the behavior to accidentally diverge. (For the same reason, rolling out this change might expose some areas where we had already accidentally diverged.)
1 parent f9ebd85 commit 3c7667a

File tree

4 files changed

+115
-176
lines changed

4 files changed

+115
-176
lines changed

packages/react-reconciler/src/ReactFiberRootScheduler.js

+85-10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent';
1515
import {
1616
disableLegacyMode,
1717
enableDeferRootSchedulingToMicrotask,
18+
disableSchedulerTimeoutInWorkLoop,
19+
enableProfilerTimer,
20+
enableProfilerNestedUpdatePhase,
1821
} from 'shared/ReactFeatureFlags';
1922
import {
2023
NoLane,
@@ -31,12 +34,12 @@ import {
3134
CommitContext,
3235
NoContext,
3336
RenderContext,
37+
flushPassiveEffects,
3438
getExecutionContext,
3539
getWorkInProgressRoot,
3640
getWorkInProgressRootRenderLanes,
3741
isWorkLoopSuspendedOnData,
38-
performConcurrentWorkOnRoot,
39-
performSyncWorkOnRoot,
42+
performWorkOnRoot,
4043
} from './ReactFiberWorkLoop';
4144
import {LegacyRoot} from './ReactRootTags';
4245
import {
@@ -62,6 +65,10 @@ import {
6265
} from './ReactFiberConfig';
6366

6467
import ReactSharedInternals from 'shared/ReactSharedInternals';
68+
import {
69+
resetNestedUpdateFlag,
70+
syncNestedUpdateFlag,
71+
} from './ReactProfilerTimer';
6572

6673
// A linked list of all the roots with pending work. In an idiomatic app,
6774
// there's only a single root, but we do support multi root apps, hence this
@@ -387,7 +394,7 @@ function scheduleTaskForRootDuringMicrotask(
387394

388395
const newCallbackNode = scheduleCallback(
389396
schedulerPriorityLevel,
390-
performConcurrentWorkOnRoot.bind(null, root),
397+
performWorkOnRootViaSchedulerTask.bind(null, root),
391398
);
392399

393400
root.callbackPriority = newCallbackPriority;
@@ -396,15 +403,67 @@ function scheduleTaskForRootDuringMicrotask(
396403
}
397404
}
398405

399-
export type RenderTaskFn = (didTimeout: boolean) => RenderTaskFn | null;
406+
type RenderTaskFn = (didTimeout: boolean) => RenderTaskFn | null;
400407

401-
export function getContinuationForRoot(
408+
function performWorkOnRootViaSchedulerTask(
402409
root: FiberRoot,
403-
originalCallbackNode: mixed,
410+
didTimeout: boolean,
404411
): RenderTaskFn | null {
405-
// This is called at the end of `performConcurrentWorkOnRoot` to determine
406-
// if we need to schedule a continuation task.
407-
//
412+
// This is the entry point for concurrent tasks scheduled via Scheduler (and
413+
// postTask, in the future).
414+
415+
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
416+
resetNestedUpdateFlag();
417+
}
418+
419+
// Flush any pending passive effects before deciding which lanes to work on,
420+
// in case they schedule additional work.
421+
const originalCallbackNode = root.callbackNode;
422+
const didFlushPassiveEffects = flushPassiveEffects();
423+
if (didFlushPassiveEffects) {
424+
// Something in the passive effect phase may have canceled the current task.
425+
// Check if the task node for this root was changed.
426+
if (root.callbackNode !== originalCallbackNode) {
427+
// The current task was canceled. Exit. We don't need to call
428+
// `ensureRootIsScheduled` because the check above implies either that
429+
// there's a new task, or that there's no remaining work on this root.
430+
return null;
431+
} else {
432+
// Current task was not canceled. Continue.
433+
}
434+
}
435+
436+
// Determine the next lanes to work on, using the fields stored on the root.
437+
// TODO: We already called getNextLanes when we scheduled the callback; we
438+
// should be able to avoid calling it again by stashing the result on the
439+
// root object. However, because we always schedule the callback during
440+
// a microtask (scheduleTaskForRootDuringMicrotask), it's possible that
441+
// an update was scheduled earlier during this same browser task (and
442+
// therefore before the microtasks have run). That's because Scheduler batches
443+
// together multiple callbacks into a single browser macrotask, without
444+
// yielding to microtasks in between. We should probably change this to align
445+
// with the postTask behavior (and literally use postTask when
446+
// it's available).
447+
const workInProgressRoot = getWorkInProgressRoot();
448+
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
449+
const lanes = getNextLanes(
450+
root,
451+
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
452+
);
453+
if (lanes === NoLanes) {
454+
// No more work on this root.
455+
return null;
456+
}
457+
458+
// Enter the work loop.
459+
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
460+
// bug we're still investigating. Once the bug in Scheduler is fixed,
461+
// we can remove this, since we track expiration ourselves.
462+
const forceSync = !disableSchedulerTimeoutInWorkLoop && didTimeout;
463+
performWorkOnRoot(root, lanes, forceSync);
464+
465+
// The work loop yielded, but there may or may not be work left at the current
466+
// priority. Need to determine whether we need to schedule a continuation.
408467
// Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask;
409468
// however, since most of the logic for determining if we need a continuation
410469
// versus a new task is the same, we cheat a bit and call it here. This is
@@ -414,11 +473,27 @@ export function getContinuationForRoot(
414473
if (root.callbackNode === originalCallbackNode) {
415474
// The task node scheduled for this root is the same one that's
416475
// currently executed. Need to return a continuation.
417-
return performConcurrentWorkOnRoot.bind(null, root);
476+
return performWorkOnRootViaSchedulerTask.bind(null, root);
418477
}
419478
return null;
420479
}
421480

481+
function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes) {
482+
// This is the entry point for synchronous tasks that don't go
483+
// through Scheduler.
484+
const didFlushPassiveEffects = flushPassiveEffects();
485+
if (didFlushPassiveEffects) {
486+
// If passive effects were flushed, exit to the outer work loop in the root
487+
// scheduler, so we can recompute the priority.
488+
return null;
489+
}
490+
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
491+
syncNestedUpdateFlag();
492+
}
493+
const forceSync = true;
494+
performWorkOnRoot(root, lanes, forceSync);
495+
}
496+
422497
const fakeActCallbackNode = {};
423498

424499
function scheduleCallback(

packages/react-reconciler/src/ReactFiberWorkLoop.js

+10-148
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import type {
2222
TransitionAbort,
2323
} from './ReactFiberTracingMarkerComponent';
2424
import type {OffscreenInstance} from './ReactFiberActivityComponent';
25-
import type {RenderTaskFn} from './ReactFiberRootScheduler';
2625
import type {Resource} from './ReactFiberConfig';
2726

2827
import {
@@ -32,7 +31,6 @@ import {
3231
enableProfilerNestedUpdatePhase,
3332
enableDebugTracing,
3433
enableSchedulingProfiler,
35-
disableSchedulerTimeoutInWorkLoop,
3634
enableUpdaterTracking,
3735
enableCache,
3836
enableTransitionTracing,
@@ -250,11 +248,9 @@ import {
250248
recordRenderTime,
251249
recordCommitTime,
252250
recordCommitEndTime,
253-
resetNestedUpdateFlag,
254251
startProfilerTimer,
255252
stopProfilerTimerIfRunningAndRecordDuration,
256253
stopProfilerTimerIfRunningAndRecordIncompleteDuration,
257-
syncNestedUpdateFlag,
258254
} from './ReactProfilerTimer';
259255
import {setCurrentTrackFromLanes} from './ReactFiberPerformanceTrack';
260256

@@ -308,7 +304,6 @@ import {
308304
ensureRootIsScheduled,
309305
flushSyncWorkOnAllRoots,
310306
flushSyncWorkOnLegacyRootsOnly,
311-
getContinuationForRoot,
312307
requestTransitionLane,
313308
} from './ReactFiberRootScheduler';
314309
import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
@@ -890,59 +885,22 @@ export function isUnsafeClassRenderPhaseUpdate(fiber: Fiber): boolean {
890885
return (executionContext & RenderContext) !== NoContext;
891886
}
892887

893-
// This is the entry point for every concurrent task, i.e. anything that
894-
// goes through Scheduler.
895-
export function performConcurrentWorkOnRoot(
888+
export function performWorkOnRoot(
896889
root: FiberRoot,
897-
didTimeout: boolean,
898-
): RenderTaskFn | null {
899-
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
900-
resetNestedUpdateFlag();
901-
}
902-
890+
lanes: Lanes,
891+
forceSync: boolean,
892+
): void {
903893
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
904894
throw new Error('Should not already be working.');
905895
}
906896

907-
// Flush any pending passive effects before deciding which lanes to work on,
908-
// in case they schedule additional work.
909-
const originalCallbackNode = root.callbackNode;
910-
const didFlushPassiveEffects = flushPassiveEffects();
911-
if (didFlushPassiveEffects) {
912-
// Something in the passive effect phase may have canceled the current task.
913-
// Check if the task node for this root was changed.
914-
if (root.callbackNode !== originalCallbackNode) {
915-
// The current task was canceled. Exit. We don't need to call
916-
// `ensureRootIsScheduled` because the check above implies either that
917-
// there's a new task, or that there's no remaining work on this root.
918-
return null;
919-
} else {
920-
// Current task was not canceled. Continue.
921-
}
922-
}
923-
924-
// Determine the next lanes to work on, using the fields stored
925-
// on the root.
926-
// TODO: This was already computed in the caller. Pass it as an argument.
927-
let lanes = getNextLanes(
928-
root,
929-
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
930-
);
931-
if (lanes === NoLanes) {
932-
// Defensive coding. This is never expected to happen.
933-
return null;
934-
}
935-
936897
// We disable time-slicing in some cases: if the work has been CPU-bound
937898
// for too long ("expired" work, to prevent starvation), or we're in
938899
// sync-updates-by-default mode.
939-
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
940-
// bug we're still investigating. Once the bug in Scheduler is fixed,
941-
// we can remove this, since we track expiration ourselves.
942900
const shouldTimeSlice =
901+
!forceSync &&
943902
!includesBlockingLane(lanes) &&
944-
!includesExpiredLane(root, lanes) &&
945-
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
903+
!includesExpiredLane(root, lanes);
946904
let exitStatus = shouldTimeSlice
947905
? renderRootConcurrent(root, lanes)
948906
: renderRootSync(root, lanes);
@@ -984,7 +942,10 @@ export function performConcurrentWorkOnRoot(
984942
}
985943

986944
// Check if something threw
987-
if (exitStatus === RootErrored) {
945+
if (
946+
(disableLegacyMode || root.tag !== LegacyRoot) &&
947+
exitStatus === RootErrored
948+
) {
988949
const lanesThatJustErrored = lanes;
989950
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
990951
root,
@@ -1033,7 +994,6 @@ export function performConcurrentWorkOnRoot(
1033994
}
1034995

1035996
ensureRootIsScheduled(root);
1036-
return getContinuationForRoot(root, originalCallbackNode);
1037997
}
1038998

1039999
function recoverFromConcurrentError(
@@ -1464,104 +1424,6 @@ function markRootSuspended(
14641424
);
14651425
}
14661426

1467-
// This is the entry point for synchronous tasks that don't go
1468-
// through Scheduler
1469-
export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
1470-
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
1471-
throw new Error('Should not already be working.');
1472-
}
1473-
1474-
const didFlushPassiveEffects = flushPassiveEffects();
1475-
if (didFlushPassiveEffects) {
1476-
// If passive effects were flushed, exit to the outer work loop in the root
1477-
// scheduler, so we can recompute the priority.
1478-
// TODO: We don't actually need this `ensureRootIsScheduled` call because
1479-
// this path is only reachable if the root is already part of the schedule.
1480-
// I'm including it only for consistency with the other exit points from
1481-
// this function. Can address in a subsequent refactor.
1482-
ensureRootIsScheduled(root);
1483-
return null;
1484-
}
1485-
1486-
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
1487-
syncNestedUpdateFlag();
1488-
}
1489-
1490-
let exitStatus = renderRootSync(root, lanes);
1491-
if (
1492-
(disableLegacyMode || root.tag !== LegacyRoot) &&
1493-
exitStatus === RootErrored
1494-
) {
1495-
// If something threw an error, try rendering one more time. We'll render
1496-
// synchronously to block concurrent data mutations, and we'll includes
1497-
// all pending updates are included. If it still fails after the second
1498-
// attempt, we'll give up and commit the resulting tree.
1499-
const originallyAttemptedLanes = lanes;
1500-
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
1501-
root,
1502-
originallyAttemptedLanes,
1503-
);
1504-
if (errorRetryLanes !== NoLanes) {
1505-
lanes = errorRetryLanes;
1506-
exitStatus = recoverFromConcurrentError(
1507-
root,
1508-
originallyAttemptedLanes,
1509-
errorRetryLanes,
1510-
);
1511-
}
1512-
}
1513-
1514-
if (exitStatus === RootFatalErrored) {
1515-
prepareFreshStack(root, NoLanes);
1516-
markRootSuspended(root, lanes, NoLane, false);
1517-
ensureRootIsScheduled(root);
1518-
return null;
1519-
}
1520-
1521-
if (exitStatus === RootDidNotComplete) {
1522-
// The render unwound without completing the tree. This happens in special
1523-
// cases where need to exit the current render without producing a
1524-
// consistent tree or committing.
1525-
markRootSuspended(
1526-
root,
1527-
lanes,
1528-
workInProgressDeferredLane,
1529-
workInProgressRootDidSkipSuspendedSiblings,
1530-
);
1531-
ensureRootIsScheduled(root);
1532-
return null;
1533-
}
1534-
1535-
let renderEndTime = 0;
1536-
if (enableProfilerTimer && enableComponentPerformanceTrack) {
1537-
renderEndTime = now();
1538-
}
1539-
1540-
// We now have a consistent tree. Because this is a sync render, we
1541-
// will commit it even if something suspended.
1542-
const finishedWork: Fiber = (root.current.alternate: any);
1543-
root.finishedWork = finishedWork;
1544-
root.finishedLanes = lanes;
1545-
commitRoot(
1546-
root,
1547-
workInProgressRootRecoverableErrors,
1548-
workInProgressTransitions,
1549-
workInProgressRootDidIncludeRecursiveRenderUpdate,
1550-
workInProgressDeferredLane,
1551-
workInProgressRootInterleavedUpdatedLanes,
1552-
workInProgressSuspendedRetryLanes,
1553-
IMMEDIATE_COMMIT,
1554-
renderStartTime,
1555-
renderEndTime,
1556-
);
1557-
1558-
// Before exiting, make sure there's a callback scheduled for the next
1559-
// pending level.
1560-
ensureRootIsScheduled(root);
1561-
1562-
return null;
1563-
}
1564-
15651427
export function flushRoot(root: FiberRoot, lanes: Lanes) {
15661428
if (lanes !== NoLanes) {
15671429
upgradePendingLanesToSync(root, lanes);

0 commit comments

Comments
 (0)