@samitouri / QOS-React-2 / commits / 3a7844cabb

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.