Move commit phase outside of performUnitOfWork

This gives us the ability to complete a tree without having to commit it
within the same frame.
This commit is contained in:
Andrew Clark
2016-12-07 23:09:08 -08:00
committed by Andrew Clark
parent 9510ecfc5e
commit 5fcdebf712
7 changed files with 165 additions and 85 deletions
+1
View File
@@ -1161,6 +1161,7 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
* preserves a previously rendered node when deprioritized
* can reuse side-effects after being preempted
* can reuse side-effects after being preempted, if shouldComponentUpdate is false
* can update a completed tree before it has a chance to commit
* updates a child even though the old props is empty
* can defer side-effects and resume them later on
* can defer side-effects and reuse them later - complex
@@ -143,6 +143,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) :
root.pendingContext = getContextForSubtree(parentComponent);
// TODO: Use pending work/state instead of props.
root.current.pendingProps = element;
if (root.current.alternate) {
root.current.alternate.pendingProps = element;
}
scheduleWork(root);
@@ -156,6 +159,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) :
const root : FiberRoot = (container.stateNode : any);
// TODO: Use pending work/state instead of props.
root.current.pendingProps = [];
if (root.current.alternate) {
root.current.alternate.pendingProps = [];
}
scheduleWork(root);
@@ -89,6 +89,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
let nextUnitOfWork : ?Fiber = null;
let nextPriorityLevel : PriorityLevel = NoWork;
let pendingCommit : Fiber | null = null;
// Linked list of roots with scheduled work on them.
let nextScheduledRoot : ?FiberRoot = null;
let lastScheduledRoot : ?FiberRoot = null;
@@ -115,6 +117,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
function findNextUnitOfWork() {
if (pendingCommit) {
return null;
}
// Clear out roots with no more work on them, or if they have uncaught errors
while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) {
// Unschedule this root.
@@ -160,6 +166,17 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
function commitAllWork(finishedWork : Fiber) {
pendingCommit = null;
const root : FiberRoot = (finishedWork.stateNode : any);
resetWorkPriority(finishedWork);
if (root.current === finishedWork) {
throw new Error(
'Cannot commit the same tree as before. This is probably a bug ' +
'related to the return field.'
);
}
root.current = finishedWork;
prepareForCommit();
// Commit all the side-effects within a tree.
@@ -293,11 +310,16 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
// Flush any task work that was scheduled during this batch
performTaskWork();
return null;
}
function resetWorkPriority(workInProgress : Fiber) {
if (workInProgress.pendingProps) {
// Don't reset the priority if the workInProgress has pending props. This
// could happen if a completed root is updated before it is able to commit.
return;
}
let newPriority = NoWork;
// progressedChild is going to be the child set with the highest priority.
// Either it is the same as child, or it just bailed out because it choose
@@ -324,20 +346,30 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
const current = workInProgress.alternate;
const next = completeWork(current, workInProgress);
resetWorkPriority(workInProgress);
// The work is now done. We don't need this anymore. This flags
// to the system not to redo any work here.
workInProgress.pendingProps = null;
workInProgress.updateQueue = null;
const returnFiber = workInProgress.return;
const siblingFiber = workInProgress.sibling;
const isRoot = !returnFiber && !siblingFiber;
if (!isRoot) {
resetWorkPriority(workInProgress);
}
if (next) {
// If completing this work spawned new work, do that next. We'll come
// back here again.
return next;
}
const returnFiber = workInProgress.return;
if (isRoot) {
// If we've reached the root, commit during the next unit of work.
pendingCommit = workInProgress;
return workInProgress;
}
if (returnFiber) {
// Append all the effects of the subtree and this fiber onto the effect
@@ -369,37 +401,15 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
if (workInProgress.sibling) {
if (siblingFiber) {
// If there is more work to do in this returnFiber, do that next.
return workInProgress.sibling;
return siblingFiber;
} else if (returnFiber) {
// If there's no more work in this returnFiber. Complete the returnFiber.
workInProgress = returnFiber;
continue;
} else {
// If we're at the root, there's no more work to do. We can flush it.
const root : FiberRoot = (workInProgress.stateNode : any);
if (root.current === workInProgress) {
throw new Error(
'Cannot commit the same tree as before. This is probably a bug ' +
'related to the return field.'
);
}
root.current = workInProgress;
// TODO: We can be smarter here and only look for more work in the
// "next" scheduled work since we've already scanned passed. That
// also ensures that work scheduled during reconciliation gets deferred.
// const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork;
commitAllWork(workInProgress);
const nextWork = findNextUnitOfWork();
// if (!nextWork && hasMoreWork) {
// TODO: This can happen when some deep work completes and we don't
// know if this was the last one. We should be able to keep track of
// the highest priority still in the tree for one pass. But if we
// terminate an update we don't know.
// throw new Error('FiberRoots should not have flagged more work if there is none.');
// }
return nextWork;
throw new Error('Should have already handled root.');
}
}
}
@@ -445,13 +455,17 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
function performDeferredWorkUnsafe(deadline) {
if (!nextUnitOfWork) {
if (!nextUnitOfWork && !pendingCommit) {
nextUnitOfWork = findNextUnitOfWork();
}
while (nextUnitOfWork) {
while (nextUnitOfWork || pendingCommit) {
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
if (!nextUnitOfWork) {
if (pendingCommit) {
nextUnitOfWork = commitAllWork(pendingCommit);
} else if (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork && !pendingCommit) {
// Find more work. We might have time to complete some more.
nextUnitOfWork = findNextUnitOfWork();
}
@@ -468,12 +482,19 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
function performAnimationWorkUnsafe() {
// Always start from the root
nextUnitOfWork = findNextUnitOfWork();
while (nextUnitOfWork &&
nextPriorityLevel === AnimationPriority) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
if (!nextUnitOfWork) {
if (!nextUnitOfWork && !pendingCommit) {
nextUnitOfWork = findNextUnitOfWork();
}
while (
pendingCommit ||
(nextUnitOfWork && nextPriorityLevel === AnimationPriority)
) {
if (pendingCommit) {
nextUnitOfWork = commitAllWork(pendingCommit);
} else if (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork && !pendingCommit) {
// Keep searching for animation work until there's no more left
nextUnitOfWork = findNextUnitOfWork();
}
@@ -489,12 +510,20 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
function performSynchronousWorkUnsafe() {
nextUnitOfWork = findNextUnitOfWork();
while (nextUnitOfWork &&
nextPriorityLevel === SynchronousPriority) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
if (!nextUnitOfWork) {
if (!nextUnitOfWork && !pendingCommit) {
nextUnitOfWork = findNextUnitOfWork();
}
while (
pendingCommit ||
(nextUnitOfWork && nextPriorityLevel === SynchronousPriority)
) {
if (pendingCommit) {
nextUnitOfWork = commitAllWork(pendingCommit);
} else if (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork && !pendingCommit) {
// Keep searching for sync work until there's no more left
nextUnitOfWork = findNextUnitOfWork();
}
}
@@ -514,13 +543,20 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
isPerformingTaskWork = true;
try {
nextUnitOfWork = findNextUnitOfWork();
while (nextUnitOfWork &&
nextPriorityLevel === TaskPriority) {
nextUnitOfWork =
performUnitOfWork(nextUnitOfWork);
if (!nextUnitOfWork) {
if (!nextUnitOfWork && !pendingCommit) {
nextUnitOfWork = findNextUnitOfWork();
}
while (
(pendingCommit && pendingCommit.pendingWorkPriority === TaskPriority) ||
(nextUnitOfWork && nextPriorityLevel === TaskPriority)
) {
if (pendingCommit && pendingCommit.pendingWorkPriority === TaskPriority) {
nextUnitOfWork = commitAllWork(pendingCommit);
} else if (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork && !pendingCommit) {
// Keep searching for sync work until there's no more left
nextUnitOfWork = findNextUnitOfWork();
}
}
@@ -597,6 +633,11 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
// Flush any task work that was scheduled during this batch
if (priorityLevel !== TaskPriority) {
performTaskWork();
}
// Throw the first uncaught error
if (!nextUnitOfWork && firstUncaughtError) {
let e = firstUncaughtError;
@@ -410,7 +410,7 @@ describe('ReactIncremental', () => {
// Make a quick update which will create a low pri tree on top of the
// already low pri tree.
ReactNoop.render(<Foo text="bar" />);
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ops).toEqual(['Foo']);
@@ -491,7 +491,7 @@ describe('ReactIncremental', () => {
// Init
ReactNoop.render(<Foo text="foo" text2="foo" step={0} />);
ReactNoop.flushDeferredPri(55 + 25 + 5);
ReactNoop.flushDeferredPri(55 + 25 + 5 + 5);
// We only finish the higher priority work. So the low pri content
// has not yet finished mounting.
@@ -520,7 +520,7 @@ describe('ReactIncremental', () => {
ops = [];
// The middle content is now pending rendering...
ReactNoop.flushDeferredPri(30);
ReactNoop.flushDeferredPri(30 + 5);
expect(ops).toEqual(['Middle', 'Bar']);
ops = [];
@@ -600,7 +600,7 @@ describe('ReactIncremental', () => {
// Make a quick update which will schedule low priority work to
// update the middle content.
ReactNoop.render(<Foo text="bar" step={1} />);
ReactNoop.flushDeferredPri(30);
ReactNoop.flushDeferredPri(30 + 5);
expect(ops).toEqual(['Foo', 'Bar']);
@@ -88,7 +88,7 @@ describe('ReactIncrementalErrorHandling', () => {
expect(ReactNoop.getChildren()).toEqual([]);
ops.length = 0;
ReactNoop.flushDeferredPri(30);
ReactNoop.flushDeferredPri(30 + 5 + 5 + 5);
expect(ops).toEqual([
'BrokenRender',
'ErrorBoundary unstable_handleError',
@@ -134,7 +134,7 @@ describe('ReactIncrementalErrorHandling', () => {
'BrokenRender',
]);
ops = [];
ReactNoop.flushDeferredPri(25);
ReactNoop.flushDeferredPri(25 + 5 + 5);
expect(ops).toEqual([
'ErrorBoundary unstable_handleError',
'ErrorBoundary render error',
@@ -144,7 +144,7 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.flushDeferredPri(10);
expect(ReactNoop.getChildren()).toEqual([]);
ReactNoop.flushDeferredPri(10);
ReactNoop.flushDeferredPri(10 + 5);
expect(ReactNoop.getChildren()).toEqual([span('2')]);
});
@@ -159,17 +159,17 @@ describe('ReactIncrementalScheduling', () => {
expect(ReactNoop.getChildren('b')).toEqual([]);
expect(ReactNoop.getChildren('c')).toEqual([]);
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([]);
expect(ReactNoop.getChildren('c')).toEqual([]);
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([]);
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
@@ -329,19 +329,19 @@ describe('ReactIncrementalScheduling', () => {
it('splits deferred work on multiple roots', () => {
// Schedule one root
ReactNoop.renderToRootWithID(<span prop="a:1" />, 'a');
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
// Schedule two roots
ReactNoop.renderToRootWithID(<span prop="a:2" />, 'a');
ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
// First scheduled one gets processed first
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([]);
expect(ReactNoop.getChildren('c')).toEqual(null);
// Then the second one gets processed
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual(null);
@@ -351,15 +351,15 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.renderToRootWithID(<span prop="b:3" />, 'b');
ReactNoop.renderToRootWithID(<span prop="c:3" />, 'c');
// They get processed in the order they were scheduled
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([]);
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]);
expect(ReactNoop.getChildren('c')).toEqual([]);
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:3')]);
@@ -368,7 +368,7 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.renderToRootWithID(<span prop="a:4" />, 'a');
ReactNoop.renderToRootWithID(<span prop="a:5" />, 'a');
ReactNoop.renderToRootWithID(<span prop="a:6" />, 'a');
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:6')]);
});
@@ -385,18 +385,18 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.renderToRootWithID(<span prop="c:2" />, 'c');
ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
// Ensure it starts in the order it was scheduled
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
// Schedule last bit of work, it will get processed the last
ReactNoop.renderToRootWithID(<span prop="a:2" />, 'a');
// Keep performing work in the order it was scheduled
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
@@ -419,11 +419,11 @@ describe('ReactIncrementalScheduling', () => {
});
// We're flushing deferred work
// Still, roots with animation work are handled first
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]);
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
@@ -434,17 +434,17 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.renderToRootWithID(<span prop="b:3" />, 'b');
});
// Animation is still handled first
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
// Finally we handle deferred root in the order it was scheduled
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:2')]);
ReactNoop.flushDeferredPri(15);
ReactNoop.flushDeferredPri(15 + 5);
expect(ReactNoop.getChildren('a')).toEqual([span('a:2')]);
expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]);
expect(ReactNoop.getChildren('c')).toEqual([span('c:3')]);
@@ -483,7 +483,7 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.render(<Foo />);
ReactNoop.flushDeferredPri(20);
ReactNoop.flushDeferredPri(20 + 5);
expect(ops).toEqual([
'render: 0',
'componentDidMount (before setState): 0',
@@ -496,7 +496,7 @@ describe('ReactIncrementalScheduling', () => {
ops = [];
instance.setState({ tick: 2 });
ReactNoop.flushDeferredPri(20);
ReactNoop.flushDeferredPri(20 + 5);
expect(ops).toEqual([
'render: 2',
@@ -545,7 +545,7 @@ describe('ReactIncrementalScheduling', () => {
ReactNoop.render(<Foo />);
ReactNoop.flushDeferredPri(20);
ReactNoop.flushDeferredPri(20 + 5);
expect(ops).toEqual([
'render: 0',
'componentDidMount (before setState): 0',
@@ -566,7 +566,7 @@ describe('ReactIncrementalScheduling', () => {
ops = [];
instance.setState({ tick: 2 });
ReactNoop.flushDeferredPri(20);
ReactNoop.flushDeferredPri(20 + 5);
expect(ops).toEqual([
'render: 2',
@@ -480,6 +480,38 @@ describe('ReactIncrementalSideEffects', () => {
]);
});
it('can update a completed tree before it has a chance to commit', () => {
function Foo(props) {
return <span prop={props.step}/>;
}
ReactNoop.render(<Foo step={1} />);
// This should be just enough to complete the tree without committing it
ReactNoop.flushDeferredPri(20);
expect(ReactNoop.getChildren()).toEqual([]);
// To confirm, perform one more unit of work. The tree should now be flushed.
// (ReactNoop decrements the time remaining by 5 *before* returning it from
// the deadline, so to perform n units of work, you need to give it 5n + 5.
// TODO: This is confusing. Decerement it after.)
ReactNoop.flushDeferredPri(10);
expect(ReactNoop.getChildren()).toEqual([span(1)]);
ReactNoop.render(<Foo step={2} />);
// This should be just enough to complete the tree without committing it
ReactNoop.flushDeferredPri(20);
expect(ReactNoop.getChildren()).toEqual([span(1)]);
// This time, before we commit the tree, we update the root component with
// new props
ReactNoop.render(<Foo step={3} />);
// Now let's commit. We already had a commit that was pending, which will
// render 2.
ReactNoop.flushDeferredPri(10);
expect(ReactNoop.getChildren()).toEqual([span(2)]);
// If we flush the rest of the work, we should get another commit that
// renders 3. If it renders 2 again, that means an update was dropped.
ReactNoop.flush();
expect(ReactNoop.getChildren()).toEqual([span(3)]);
});
it('updates a child even though the old props is empty', () => {
function Foo(props) {
return (
@@ -611,7 +643,7 @@ describe('ReactIncrementalSideEffects', () => {
);
}
ReactNoop.render(<Foo tick={0} idx={0} />);
ReactNoop.flushDeferredPri(65);
ReactNoop.flushDeferredPri(65 + 5);
expect(ReactNoop.getChildren()).toEqual([
div(
span(0),
@@ -770,7 +802,7 @@ describe('ReactIncrementalSideEffects', () => {
ops = [];
ReactNoop.render(<Foo tick={1} idx={1} />);
ReactNoop.flushDeferredPri(70);
ReactNoop.flushDeferredPri(70 + 5);
expect(ReactNoop.getChildren()).toEqual([
div(
// Updated.
@@ -793,7 +825,7 @@ describe('ReactIncrementalSideEffects', () => {
// items. Including the set state since that is deprioritized.
// TODO: The cycles it takes to do this could be lowered with further
// optimizations.
ReactNoop.flushDeferredPri(60);
ReactNoop.flushDeferredPri(60 + 5);
expect(ReactNoop.getChildren()).toEqual([
div(
// Updated.