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).
Andrew Clark committed
Dec 7, 2016 at 12:24 UTC
3a7844cabb9d815e6dac8ae5b9eddd255a82b7a6
1 file changed
+114
-125
src/renderers/shared/fiber/ReactFiberScheduler.js
+114
-125
@@ -339,12 +339,6 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
339
}
340
341
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();
342
}
343
344
function resetWorkPriority(workInProgress : Fiber) {
@@ -521,7 +515,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
515
performWork(AnimationPriority);
516
}
517
524
- function errorLoop() {
518
+ function clearErrors() {
519
if (!nextUnitOfWork) {
520
nextUnitOfWork = findNextUnitOfWork();
521
}
@@ -537,59 +531,74 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
531
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
532
}
533
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.
538
nextUnitOfWork = findNextUnitOfWork();
539
}
540
}
541
}
542
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();
546
559
- // Returns true if we exit because the deadline has expired
560
- function deferredWorkLoop(deadline) : boolean {
561
- let deadlineHasExpired = false;
547
if (!nextUnitOfWork) {
548
nextUnitOfWork = findNextUnitOfWork();
549
}
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.
572
}
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();
594
}
585
- } else {
586
- deadlineHasExpired = true;
595
}
596
}
597
+
598
return deadlineHasExpired;
599
}
600
592
- function performWork(priorityLevel : PriorityLevel, deadline : null | Deadline) {
601
+ function performWork(priorityLevel : PriorityLevel, deadline : Deadline | null) {
602
if (isPerformingWork) {
603
throw new Error('performWork was called recursively.');
604
}
@@ -597,96 +606,42 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
606
const isPerformingDeferredWork = Boolean(deadline);
607
let deadlineHasExpired = false;
608
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);
624
}
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);
632
} catch (error) {
633
// We caught an error during either the begin or complete phases.
634
const failedWork = nextUnitOfWork;
635
636
// "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.
639
const maybeBoundary = captureError(failedWork, error);
640
if (maybeBoundary) {
641
const boundary = maybeBoundary;
642
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.
645
beginFailedWork(boundary.alternate, boundary, priorityLevel);
646
647
// 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>) {
660
// Continue performing work
661
continue;
662
}
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
+ }
698
}
699
700
// We're done performing work. Time to clean up.