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',