@samitouri / QOS-React / commits / db8539a47d

Consolidate work loops

This makes it easier to track when we enter and exit a batch of work. Further steps needed in this refactor: - Get rid of tryComponentDidMount, tryComponentDidUpdate, etc. in favor of the try-catch blocks that wrap each pass of the commit phase. - Need to be able to switch between performing work that is possibly failed (slower because it requires an extra check on each iteration) and work that we know for sure has no errors.

Andrew Clark committed Dec 2, 2016 at 15:27 UTC db8539a47d32772e022689959296798bf59ec0c6
4 files changed +197 -160
src/renderers/shared/fiber/ReactFiberScheduler.js
+191 -155
@@ -79,11 +79,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
79 SynchronousPriority :
80 LowPriority;
81
82 - // Whether updates should be batched. Only applies when using sync scheduling.
83 - let shouldBatchUpdates : boolean = false;
84 -
85 - // Need this to prevent recursion while in a Task loop.
86 - let isPerformingTaskWork : boolean = false;
82 + // Keeps track of whether we're currently in a work loop. Used to batch
83 + // nested updates.
84 + let isPerformingWork : boolean = false;
85
86 // The next work in progress fiber that we're currently working on.
87 let nextUnitOfWork : ?Fiber = null;
@@ -425,14 +423,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
423 ReactFiberInstrumentation.debugTool.onWillBeginWork(workInProgress);
424 }
425 // See if beginning this work spawns more work.
428 - let next;
429 - const isFailedWork = capturedErrors && capturedErrors.has(workInProgress);
430 - if (isFailedWork) {
431 - next = beginFailedWork(current, workInProgress, nextPriorityLevel);
432 - workInProgress.effectTag |= Err;
433 - } else {
434 - next = beginWork(current, workInProgress, nextPriorityLevel);
435 - }
426 + let next = beginWork(current, workInProgress, nextPriorityLevel);
427
428 if (__DEV__ && ReactFiberInstrumentation.debugTool) {
429 ReactFiberInstrumentation.debugTool.onDidBeginWork(workInProgress);
@@ -454,162 +445,209 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
445 return next;
446 }
447
457 - function performDeferredWorkUnsafe(deadline) {
458 - if (!nextUnitOfWork && !pendingCommit) {
459 - nextUnitOfWork = findNextUnitOfWork();
448 + function performFailedUnitOfWork(workInProgress : Fiber) : ?Fiber {
449 + // The current, flushed, state of this fiber is the alternate.
450 + // Ideally nothing should rely on this, but relying on it here
451 + // means that we don't need an additional field on the work in
452 + // progress.
453 + const current = workInProgress.alternate;
454 +
455 + if (__DEV__ && ReactFiberInstrumentation.debugTool) {
456 + ReactFiberInstrumentation.debugTool.onWillBeginWork(workInProgress);
457 }
461 - while (nextUnitOfWork || pendingCommit) {
462 - if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
463 - if (pendingCommit) {
464 - nextUnitOfWork = commitAllWork(pendingCommit);
465 - } else if (nextUnitOfWork) {
466 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
467 - }
468 - if (!nextUnitOfWork && !pendingCommit) {
469 - // Find more work. We might have time to complete some more.
470 - nextUnitOfWork = findNextUnitOfWork();
471 - }
472 - } else {
473 - scheduleDeferredCallback(performDeferredWork);
474 - return;
458 + // See if beginning this work spawns more work.
459 + let next = beginFailedWork(current, workInProgress, nextPriorityLevel);
460 + workInProgress.effectTag |= Err;
461 +
462 + if (__DEV__ && ReactFiberInstrumentation.debugTool) {
463 + ReactFiberInstrumentation.debugTool.onDidBeginWork(workInProgress);
464 + }
465 +
466 + if (!next) {
467 + if (__DEV__ && ReactFiberInstrumentation.debugTool) {
468 + ReactFiberInstrumentation.debugTool.onWillCompleteWork(workInProgress);
469 + }
470 + // If this doesn't spawn new work, complete the current work.
471 + next = completeUnitOfWork(workInProgress);
472 + if (__DEV__ && ReactFiberInstrumentation.debugTool) {
473 + ReactFiberInstrumentation.debugTool.onDidCompleteWork(workInProgress);
474 }
475 }
476 +
477 + ReactCurrentOwner.current = null;
478 +
479 + return next;
480 }
481
482 function performDeferredWork(deadline) {
483 + // We pass the lowest deferred priority here because it acts as a minimum.
484 + // Higher priorities will also be performed.
485 isDeferredCallbackScheduled = false;
481 - performAndHandleErrors(LowPriority, deadline);
482 - }
483 -
484 - function performAnimationWorkUnsafe() {
485 - if (!nextUnitOfWork && !pendingCommit) {
486 - nextUnitOfWork = findNextUnitOfWork();
487 - }
488 - while (
489 - pendingCommit ||
490 - (nextUnitOfWork && nextPriorityLevel === AnimationPriority)
491 - ) {
492 - if (pendingCommit) {
493 - nextUnitOfWork = commitAllWork(pendingCommit);
494 - } else if (nextUnitOfWork) {
495 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
496 - }
497 - if (!nextUnitOfWork && !pendingCommit) {
498 - // Keep searching for animation work until there's no more left
499 - nextUnitOfWork = findNextUnitOfWork();
500 - }
501 - }
502 - if (nextUnitOfWork) {
503 - scheduleCallbackAtPriority(nextPriorityLevel);
504 - }
486 + performWork(OffscreenPriority, deadline);
487 }
488
489 function performAnimationWork() {
490 isAnimationCallbackScheduled = false;
509 - performAndHandleErrors(AnimationPriority);
491 + performWork(AnimationPriority);
492 }
493
512 - function performSynchronousWorkUnsafe() {
513 - if (!nextUnitOfWork && !pendingCommit) {
514 - nextUnitOfWork = findNextUnitOfWork();
515 - }
516 - while (
517 - pendingCommit ||
518 - (nextUnitOfWork && nextPriorityLevel === SynchronousPriority)
519 - ) {
520 - if (pendingCommit) {
521 - nextUnitOfWork = commitAllWork(pendingCommit);
522 - } else if (nextUnitOfWork) {
523 - nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
494 + function workLoop(priorityLevel) {
495 + // If there are errors, use a forked version of performUnitOfWork
496 + // TODO: Need to make commitAllWork throw on error before doing this
497 + if (true || (capturedErrors && capturedErrors.size)) {
498 + while (true) {
499 + if (!nextUnitOfWork && !pendingCommit) {
500 + nextUnitOfWork = findNextUnitOfWork();
501 + }
502 + if (pendingCommit && !(priorityLevel === TaskPriority && pendingCommit.pendingWorkPriority !== TaskPriority)) {
503 + nextUnitOfWork = commitAllWork(pendingCommit);
504 + } else if (nextUnitOfWork && nextPriorityLevel !== NoWork && nextPriorityLevel <= priorityLevel) {
505 + if (capturedErrors && capturedErrors.has(nextUnitOfWork)) {
506 + nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork);
507 + } else {
508 + nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
509 + }
510 + } else {
511 + return;
512 + }
513 }
514 + }
515 +
516 + // Otherwise, handle the normal, faster case where there are no errors:
517 + while (true) {
518 if (!nextUnitOfWork && !pendingCommit) {
526 - // Keep searching for sync work until there's no more left
519 nextUnitOfWork = findNextUnitOfWork();
520 }
529 - }
530 - if (nextUnitOfWork) {
531 - scheduleCallbackAtPriority(nextPriorityLevel);
521 + if (pendingCommit && !(priorityLevel === TaskPriority && pendingCommit.pendingWorkPriority !== TaskPriority)) {
522 + nextUnitOfWork = commitAllWork(pendingCommit);
523 + } else if (nextUnitOfWork && nextPriorityLevel !== NoWork && nextPriorityLevel <= priorityLevel) {
524 + nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
525 + } else {
526 + return;
527 + }
528 }
529 }
530
535 - function performSynchronousWork() {
536 - performAndHandleErrors(SynchronousPriority);
537 - }
538 -
539 - function performTaskWorkUnsafe() {
540 - if (isPerformingTaskWork) {
541 - throw new Error('Already performing task work');
531 + function deferredWorkLoop(deadline) {
532 + if (!nextUnitOfWork) {
533 + nextUnitOfWork = findNextUnitOfWork();
534 }
535
544 - isPerformingTaskWork = true;
545 - try {
546 - if (!nextUnitOfWork && !pendingCommit) {
547 - nextUnitOfWork = findNextUnitOfWork();
536 + // If there are errors, use a forked version of performUnitOfWork
537 + // TODO: Need to make commitAllWork throw on error before doing this
538 + if (true || (capturedErrors && capturedErrors.size)) {
539 + while (true) {
540 + if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
541 + if (!nextUnitOfWork && !pendingCommit) {
542 + nextUnitOfWork = findNextUnitOfWork();
543 + }
544 + if (pendingCommit) {
545 + nextUnitOfWork = commitAllWork(pendingCommit);
546 + } else if (nextUnitOfWork) {
547 + if (capturedErrors && capturedErrors.has(nextUnitOfWork)) {
548 + nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork);
549 + } else {
550 + nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
551 + }
552 + } else {
553 + return;
554 + }
555 + } else {
556 + return;
557 + }
558 }
549 - while (
550 - (pendingCommit && pendingCommit.pendingWorkPriority === TaskPriority) ||
551 - (nextUnitOfWork && nextPriorityLevel === TaskPriority)
552 - ) {
553 - if (pendingCommit && pendingCommit.pendingWorkPriority === TaskPriority) {
559 + }
560 +
561 + // Otherwise, handle the normal, faster case where there are no errors:
562 + while (true) {
563 + if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
564 + if (!nextUnitOfWork && !pendingCommit) {
565 + nextUnitOfWork = findNextUnitOfWork();
566 + }
567 + if (pendingCommit) {
568 nextUnitOfWork = commitAllWork(pendingCommit);
569 } else if (nextUnitOfWork) {
570 nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
571 + } else {
572 + return;
573 }
558 - if (!nextUnitOfWork && !pendingCommit) {
559 - // Keep searching for sync work until there's no more left
560 - nextUnitOfWork = findNextUnitOfWork();
561 - }
562 - }
563 - if (nextUnitOfWork) {
564 - scheduleCallbackAtPriority(nextPriorityLevel);
574 + } else {
575 + return;
576 }
566 - } finally {
567 - isPerformingTaskWork = false;
577 }
578 }
579
571 - function performTaskWork() {
572 - performAndHandleErrors(TaskPriority);
573 - }
580 + function performWork(priorityLevel : PriorityLevel, deadline : null | Deadline) {
581 + if (isPerformingWork) {
582 + throw new Error('performWork was called recursively.');
583 + }
584 + isPerformingWork = true;
585
575 - function performAndHandleErrors(priorityLevel : PriorityLevel, deadline : null | Deadline) {
576 - // The exact priority level doesn't matter, so long as it's in range of the
577 - // work (sync, animation, deferred) being performed.
578 - let shouldContinue = true;
579 - while (shouldContinue) {
580 - shouldContinue = false;
581 - const prevShouldBatchUpdates = shouldBatchUpdates;
582 - shouldBatchUpdates = true;
586 + // Perform work until either there's no more work at this priority level, or
587 + // (in the case of deferred work) we've run out of time.
588 + while (priorityLevel !== NoWork) {
589 + // Functions that contain a try-catch block are not optimizable by the
590 + // JS engine. The hottest code paths have been extracted to separate
591 + // functions, workLoop and deferredWorkLoop, which run on every unit of
592 + // work. The loop we're in now runs infrequently: to flush task work at
593 + // the end of a frame, or to restart after an error.
594 try {
584 - switch (priorityLevel) {
595 + if (priorityLevel >= HighPriority) {
596 + if (!deadline) {
597 + throw new Error('Cannot perform deferred work without a deadline.');
598 + }
599 + // The deferred work loop will run until there's no time left in
600 + // the current frame
601 + deferredWorkLoop(deadline);
602 + } else {
603 + // The non-deferred work loop will run until there's no more work
604 + // at the given priority level
605 + workLoop(priorityLevel);
606 + }
607 +
608 + // Stop performing work
609 + priorityLevel = NoWork;
610 +
611 + // There might still be work left. Depending on the priority, we should
612 + // either perform it now or schedule a callback to perform it later.
613 + switch (nextPriorityLevel) {
614 case SynchronousPriority:
586 - performSynchronousWorkUnsafe();
587 - break;
615 case TaskPriority:
589 - if (!isPerformingTaskWork) {
590 - performTaskWorkUnsafe();
591 - }
616 + // Perform work immediately by switching the priority level
617 + // and continuing the loop.
618 + priorityLevel = nextPriorityLevel;
619 break;
620 case AnimationPriority:
594 - performAnimationWorkUnsafe();
621 + scheduleAnimationCallback(performAnimationWork);
622 + // Even though the next unit of work has animation priority, there
623 + // may still be deferred work left over as well. I think this is
624 + // only important for unit tests. In a real app, a deferred callback
625 + // would be scheduled during the next animation frame.
626 + scheduleDeferredCallback(performDeferredWork);
627 break;
628 case HighPriority:
629 case LowPriority:
630 case OffscreenPriority:
599 - if (!deadline) {
600 - throw new Error('No deadline');
601 - } else {
602 - performDeferredWorkUnsafe(deadline);
603 - }
604 - break;
605 - default:
631 + scheduleDeferredCallback(performDeferredWork);
632 break;
633 }
634 } catch (error) {
635 + // We caught an error during either the begin or complete phases.
636 const failedWork = nextUnitOfWork;
610 - const boundary = captureError(failedWork, error, false);
611 - if (boundary) {
612 - // The boundary failed to complete. Complete it as if rendered null.
637 +
638 + // "Capture" the error by finding the nearest boundary. If there is no
639 + // error boundary, the nearest host container acts as one.
640 + const maybeBoundary = captureError(failedWork, error, false);
641 + if (maybeBoundary) {
642 + const boundary = maybeBoundary;
643 +
644 + // Schedule an Task update on the boundary
645 + const previousPriorityContext = priorityContext;
646 + priorityContext = TaskPriority;
647 + scheduleUpdate(boundary);
648 + priorityContext = previousPriorityContext;
649 +
650 + // Complete the boundary as if rendered null.
651 beginFailedWork(boundary.alternate, boundary, priorityLevel);
652
653 // The next unit of work is now the boundary that captured the error.
@@ -624,22 +662,19 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
662 unwindContext(failedWork, boundary);
663 }
664 nextUnitOfWork = completeUnitOfWork(boundary);
627 -
628 - // We were interupted by an error. Continue performing work.
629 - shouldContinue = true;
665 }
631 - } finally {
632 - shouldBatchUpdates = prevShouldBatchUpdates;
666 + // The loop will continue performing work
667 }
668 }
669
636 - // Flush any task work that was scheduled during this batch
637 - if (priorityLevel !== TaskPriority) {
638 - performTaskWork();
670 + // We're done performing work. Time to clean up.
671 + isPerformingWork = false;
672 + if (capturedErrors && !capturedErrors.size) {
673 + capturedErrors = null;
674 }
675
641 - // Throw the first uncaught error
642 - if (!nextUnitOfWork && firstUncaughtError) {
676 + // It's now safe to throw the first uncaught error.
677 + if (firstUncaughtError) {
678 let e = firstUncaughtError;
679 firstUncaughtError = null;
680 throw e;
@@ -747,7 +782,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
782 let priorityLevel = priorityContext;
783
784 // If we're in a batch, switch to task priority
750 - if (priorityLevel === SynchronousPriority && shouldBatchUpdates) {
785 + if (priorityLevel === SynchronousPriority && isPerformingWork) {
786 priorityLevel = TaskPriority;
787 }
788
@@ -774,24 +809,26 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
809 }
810 }
811
777 - // We must reset the current unit of work pointer so that we restart the
778 - // search from the root during the next tick, in case there is now higher
779 - // priority work somewhere earlier than before.
812 if (priorityLevel <= nextPriorityLevel) {
813 + // We must reset the current unit of work pointer so that we restart the
814 + // search from the root during the next tick, in case there is now higher
815 + // priority work somewhere earlier than before.
816 nextUnitOfWork = null;
817 }
818
784 - scheduleCallbackAtPriority(priorityLevel);
785 - }
786 -
787 - function scheduleCallbackAtPriority(priorityLevel : PriorityLevel) {
819 + // Depending on the priority level, either perform work now or schedule
820 + // a callback to perform work later.
821 switch (priorityLevel) {
822 case SynchronousPriority:
823 // Perform work immediately
791 - performSynchronousWork();
824 + performWork(SynchronousPriority);
825 return;
826 case TaskPriority:
794 - // Do nothing. Task work should be flushed after committing.
827 + // If we're already performing work, Task work will be flushed before
828 + // exiting the current batch. So we can skip it here.
829 + if (!isPerformingWork) {
830 + performWork(TaskPriority);
831 + }
832 return;
833 case AnimationPriority:
834 scheduleAnimationCallback(performAnimationWork);
@@ -806,8 +843,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
843
844 function scheduleUpdate(fiber : Fiber) {
845 let priorityLevel = priorityContext;
809 - // If we're in a batch, switch to task priority
810 - if (priorityLevel === SynchronousPriority && shouldBatchUpdates) {
846 + // If we're in a batch, downgrade sync priority to task priority
847 + if (priorityLevel === SynchronousPriority && isPerformingWork) {
848 priorityLevel = TaskPriority;
849 }
850
@@ -856,18 +893,17 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
893 }
894
895 function batchedUpdates<A, R>(fn : (a: A) => R, a : A) : R {
859 - const prev = shouldBatchUpdates;
860 - shouldBatchUpdates = true;
896 + const previousIsPerformingWork = isPerformingWork;
897 + // Simulate that we're performing work so that sync work is batched
898 + isPerformingWork = true;
899 try {
900 return fn(a);
901 } finally {
864 - // If we're exiting the batch, perform any scheduled task work
865 - try {
866 - if (!prev) {
867 - performTaskWork();
868 - }
869 - } finally {
870 - shouldBatchUpdates = prev;
902 + isPerformingWork = previousIsPerformingWork;
903 + // If we're not already performing work, we need to flush any task work
904 + // that was created by the user-provided function.
905 + if (!isPerformingWork) {
906 + performWork(TaskPriority);
907 }
908 }
909 }
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+1
@@ -1465,6 +1465,7 @@ describe('ReactIncremental', () => {
1465 ]);
1466 expect(instance.state.n).toEqual(4);
1467 });
1468 +
1469 it('can handle if setState callback throws', () => {
1470 var ops = [];
1471 var instance;
src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
+2 -2
@@ -88,7 +88,7 @@ describe('ReactIncrementalErrorHandling', () => {
88 expect(ReactNoop.getChildren()).toEqual([]);
89
90 ops.length = 0;
91 - ReactNoop.flushDeferredPri(30 + 5 + 5 + 5);
91 + ReactNoop.flushDeferredPri(30 + 5 + 5 + 5 + 5);
92 expect(ops).toEqual([
93 'BrokenRender',
94 'ErrorBoundary unstable_handleError',
@@ -134,7 +134,7 @@ describe('ReactIncrementalErrorHandling', () => {
134 'BrokenRender',
135 ]);
136 ops = [];
137 - ReactNoop.flushDeferredPri(25 + 5 + 5);
137 + ReactNoop.flushDeferredPri(25 + 5 + 5 + 5);
138 expect(ops).toEqual([
139 'ErrorBoundary unstable_handleError',
140 'ErrorBoundary render error',
src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js
+3 -3
@@ -341,7 +341,7 @@ describe('ReactIncrementalScheduling', () => {
341 expect(ReactNoop.getChildren('b')).toEqual([]);
342 expect(ReactNoop.getChildren('c')).toEqual(null);
343 // Then the second one gets processed
344 - ReactNoop.flushDeferredPri(15 + 5);
344 + ReactNoop.flushDeferredPri(15 + 5 + 5);
345 expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
346 expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
347 expect(ReactNoop.getChildren('c')).toEqual(null);
@@ -483,7 +483,7 @@ describe('ReactIncrementalScheduling', () => {
483
484 ReactNoop.render(<Foo />);
485
486 - ReactNoop.flushDeferredPri(20 + 5);
486 + ReactNoop.flushDeferredPri(20 + 5 + 5);
487 expect(ops).toEqual([
488 'render: 0',
489 'componentDidMount (before setState): 0',
@@ -496,7 +496,7 @@ describe('ReactIncrementalScheduling', () => {
496
497 ops = [];
498 instance.setState({ tick: 2 });
499 - ReactNoop.flushDeferredPri(20 + 5);
499 + ReactNoop.flushDeferredPri(20 + 5 + 5);
500
501 expect(ops).toEqual([
502 'render: 2',