Skip to content

Commit 3a7844c

Browse files
committed
Consolidate workLoop and deferredWork loop
Solves a few things: - Moves code out of performWork into workLoop so that it can be optimized. - errorLoop (now clearErrors) is no longer called recursively. - Removes a while (true) loop inside performWork (not that it really matters for optimization, since performWork contains a try block and will be deopted, anyway).
1 parent 24a83a5 commit 3a7844c

File tree

1 file changed

+114
-125
lines changed

1 file changed

+114
-125
lines changed

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 114 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -339,12 +339,6 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
339339
}
340340

341341
priorityContext = previousPriorityContext;
342-
343-
// Clear any errors that may have occurred during this commit
344-
// TODO: Possibly recursive. Can we move this into performWork? Not sure how
345-
// because we need to clear errors after every commit, and one of the call
346-
// sites of commitAllWork is completeUnitOfWork.
347-
errorLoop();
348342
}
349343

350344
function resetWorkPriority(workInProgress : Fiber) {
@@ -521,7 +515,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
521515
performWork(AnimationPriority);
522516
}
523517

524-
function errorLoop() {
518+
function clearErrors() {
525519
if (!nextUnitOfWork) {
526520
nextUnitOfWork = findNextUnitOfWork();
527521
}
@@ -537,156 +531,117 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
537531
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
538532
}
539533
if (!nextUnitOfWork) {
534+
// If performUnitOfWork returns null, that means we just comitted
535+
// a root. Normally we'd need to clear any errors that were scheduled
536+
// during the commit phase. But we're already clearing errors, so
537+
// we can continue.
540538
nextUnitOfWork = findNextUnitOfWork();
541539
}
542540
}
543541
}
544542

545-
function workLoop(priorityLevel) {
546-
if (!nextUnitOfWork) {
547-
nextUnitOfWork = findNextUnitOfWork();
548-
}
549-
while (nextUnitOfWork &&
550-
nextPriorityLevel !== NoWork &&
551-
nextPriorityLevel <= priorityLevel) {
552-
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
553-
if (!nextUnitOfWork) {
554-
nextUnitOfWork = findNextUnitOfWork();
555-
}
556-
}
557-
}
543+
function workLoop(priorityLevel, deadline : Deadline | null, deadlineHasExpired : boolean) : boolean {
544+
// Clear any errors.
545+
clearErrors();
558546

559-
// Returns true if we exit because the deadline has expired
560-
function deferredWorkLoop(deadline) : boolean {
561-
let deadlineHasExpired = false;
562547
if (!nextUnitOfWork) {
563548
nextUnitOfWork = findNextUnitOfWork();
564549
}
565-
while (nextUnitOfWork && !deadlineHasExpired) {
566-
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
567-
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
568-
// The order of the checks here is important: we should only check if
569-
// there's a pending commit if performUnitOfWork returns null.
570-
// Alternatively, to avoid checking for the pending commit, we could
571-
// get it from the nextScheduledRoot. However, that requires additional
572-
// checks to satisfy Flow. Since the logical operator short-circuits,
573-
// this should be fine.
574-
if (!nextUnitOfWork && pendingCommit) {
575-
// performUnitOfWork returned null and we have a pendingCommit. If we
576-
// have time, we should commit the work now.
577-
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
578-
commitAllWork(pendingCommit);
579-
nextUnitOfWork = findNextUnitOfWork();
580-
} else {
581-
deadlineHasExpired = true;
550+
551+
if (deadline) {
552+
// The deferred work loop will run until there's no time left in
553+
// the current frame.
554+
while (nextUnitOfWork && !deadlineHasExpired) {
555+
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
556+
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
557+
// In a deferred work batch, iff nextUnitOfWork returns null, we just
558+
// completed a root and a pendingCommit exists. Logically, we could
559+
// omit either of the checks in the following condition, but we need
560+
// both to satisfy Flow.
561+
if (!nextUnitOfWork && pendingCommit) {
562+
// If we have time, we should commit the work now.
563+
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
564+
commitAllWork(pendingCommit);
565+
nextUnitOfWork = findNextUnitOfWork();
566+
// Clear any errors that were scheduled during the commit phase.
567+
clearErrors();
568+
} else {
569+
deadlineHasExpired = true;
570+
}
571+
// Otherwise the root will committed in the next frame.
582572
}
583-
// Otherwise the root will committed in the next frame.
573+
} else {
574+
deadlineHasExpired = true;
575+
}
576+
}
577+
} else {
578+
// The non-deferred work loop will run until there's no more work
579+
// at the given priority level
580+
if (priorityLevel >= HighPriority) {
581+
throw new Error(
582+
'Deferred work should only be performed using deferredWorkLoop'
583+
);
584+
}
585+
while (nextUnitOfWork &&
586+
nextPriorityLevel !== NoWork &&
587+
nextPriorityLevel <= priorityLevel) {
588+
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
589+
if (!nextUnitOfWork) {
590+
nextUnitOfWork = findNextUnitOfWork();
591+
// performUnitOfWork returned null, which means we just comitted a
592+
// root. Clear any errors that were scheduled during the commit phase.
593+
clearErrors();
584594
}
585-
} else {
586-
deadlineHasExpired = true;
587595
}
588596
}
597+
589598
return deadlineHasExpired;
590599
}
591600

592-
function performWork(priorityLevel : PriorityLevel, deadline : null | Deadline) {
601+
function performWork(priorityLevel : PriorityLevel, deadline : Deadline | null) {
593602
if (isPerformingWork) {
594603
throw new Error('performWork was called recursively.');
595604
}
596605
isPerformingWork = true;
597606
const isPerformingDeferredWork = Boolean(deadline);
598607
let deadlineHasExpired = false;
599608

600-
// Perform work until either there's no more work at this priority level, or
601-
// (in the case of deferred work) we've run out of time.
602-
while (true) {
603-
try {
604-
// Functions that contain a try-catch block are not optimizable by the
605-
// JS engine. The hottest code paths have been extracted to separate
606-
// functions, workLoop and deferredWorkLoop, which run on every unit of
607-
// work. The loop we're in now runs infrequently: to flush task work at
608-
// the end of a frame, or to restart after an error.
609-
while (priorityLevel !== NoWork) {
610-
// Before starting any work, check to see if there are any pending
611-
// commits from the previous frame. An exception if we're flushing
612-
// Task work in a deferred batch and the pending commit does not
613-
// have Task priority.
614-
if (pendingCommit) {
615-
const isFlushingTaskWorkInDeferredBatch =
616-
priorityLevel === TaskPriority &&
617-
isPerformingDeferredWork &&
618-
pendingCommit.pendingWorkPriority !== TaskPriority;
619-
if (!isFlushingTaskWorkInDeferredBatch) {
620-
commitAllWork(pendingCommit);
621-
}
622-
}
623-
624-
// Clear any errors.
625-
errorLoop();
626-
627-
if (deadline) {
628-
// The deferred work loop will run until there's no time left in
629-
// the current frame.
630-
if (!deadlineHasExpired) {
631-
deadlineHasExpired = deferredWorkLoop(deadline);
632-
}
633-
} else {
634-
if (priorityLevel >= HighPriority) {
635-
throw new Error(
636-
'Deferred work should only be performed using deferredWorkLoop'
637-
);
638-
}
639-
// The non-deferred work loop will run until there's no more work
640-
// at the given priority level
641-
workLoop(priorityLevel);
642-
}
643-
644-
// Stop performing work
645-
priorityLevel = NoWork;
646-
647-
// If we additional work, and we're in a deferred batch, check to see
648-
// if the deadline has expired. If not, we may have more time to
649-
// do work.
650-
if (nextPriorityLevel !== NoWork && isPerformingDeferredWork && !deadlineHasExpired) {
651-
priorityLevel = nextPriorityLevel;
652-
continue;
653-
}
654-
655-
// There might still be work left. Depending on the priority, we should
656-
// either perform it now or schedule a callback to perform it later.
657-
switch (nextPriorityLevel) {
658-
case SynchronousPriority:
659-
case TaskPriority:
660-
// Perform work immediately by switching the priority level
661-
// and continuing the loop.
662-
priorityLevel = nextPriorityLevel;
663-
break;
664-
case AnimationPriority:
665-
scheduleAnimationCallback(performAnimationWork);
666-
// Even though the next unit of work has animation priority, there
667-
// may still be deferred work left over as well. I think this is
668-
// only important for unit tests. In a real app, a deferred callback
669-
// would be scheduled during the next animation frame.
670-
scheduleDeferredCallback(performDeferredWork);
671-
break;
672-
case HighPriority:
673-
case LowPriority:
674-
case OffscreenPriority:
675-
scheduleDeferredCallback(performDeferredWork);
676-
break;
677-
}
609+
// This outer loop exists so that we can restart the work loop after
610+
// catching an error. It also lets us flush Task work at the end of a
611+
// deferred batch.
612+
while (priorityLevel !== NoWork) {
613+
// Before starting any work, check to see if there are any pending
614+
// commits from the previous frame. An exception is if we're flushing
615+
// Task work in a deferred batch and the pending commit does not
616+
// have Task priority.
617+
if (pendingCommit) {
618+
const isFlushingTaskWorkInDeferredBatch =
619+
priorityLevel === TaskPriority &&
620+
isPerformingDeferredWork &&
621+
pendingCommit.pendingWorkPriority !== TaskPriority;
622+
if (!isFlushingTaskWorkInDeferredBatch) {
623+
commitAllWork(pendingCommit);
678624
}
625+
}
626+
627+
// Nothing in performWork should be allowed to throw. All unsafe
628+
// operations must happen within workLoop, which is extracted to a
629+
// separate function so that it can be optimized by the JS engine.
630+
try {
631+
deadlineHasExpired = workLoop(priorityLevel, deadline, deadlineHasExpired);
679632
} catch (error) {
680633
// We caught an error during either the begin or complete phases.
681634
const failedWork = nextUnitOfWork;
682635

683636
// "Capture" the error by finding the nearest boundary. If there is no
684-
// error boundary, the nearest host container acts as one.
637+
// error boundary, the nearest host container acts as one. If
638+
// captureError returns null, the error was intentionally ignored.
685639
const maybeBoundary = captureError(failedWork, error);
686640
if (maybeBoundary) {
687641
const boundary = maybeBoundary;
688642

689-
// Complete the boundary as if it rendered null.
643+
// Complete the boundary as if it rendered null. This will unmount
644+
// the failed tree.
690645
beginFailedWork(boundary.alternate, boundary, priorityLevel);
691646

692647
// The next unit of work is now the boundary that captured the error.
@@ -705,7 +660,41 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
705660
// Continue performing work
706661
continue;
707662
}
708-
break;
663+
664+
// Stop performing work
665+
priorityLevel = NoWork;
666+
667+
// If have we more work, and we're in a deferred batch, check to see
668+
// if the deadline has expired.
669+
if (nextPriorityLevel !== NoWork && isPerformingDeferredWork && !deadlineHasExpired) {
670+
// We have more time to do work.
671+
priorityLevel = nextPriorityLevel;
672+
continue;
673+
}
674+
675+
// There might be work left. Depending on the priority, we should
676+
// either perform it now or schedule a callback to perform it later.
677+
switch (nextPriorityLevel) {
678+
case SynchronousPriority:
679+
case TaskPriority:
680+
// Perform work immediately by switching the priority level
681+
// and continuing the loop.
682+
priorityLevel = nextPriorityLevel;
683+
break;
684+
case AnimationPriority:
685+
scheduleAnimationCallback(performAnimationWork);
686+
// Even though the next unit of work has animation priority, there
687+
// may still be deferred work left over as well. I think this is
688+
// only important for unit tests. In a real app, a deferred callback
689+
// would be scheduled during the next animation frame.
690+
scheduleDeferredCallback(performDeferredWork);
691+
break;
692+
case HighPriority:
693+
case LowPriority:
694+
case OffscreenPriority:
695+
scheduleDeferredCallback(performDeferredWork);
696+
break;
697+
}
709698
}
710699

711700
// We're done performing work. Time to clean up.

0 commit comments

Comments
 (0)