Bubble up pending work priority to the top level

This is a bit poorly structured. I'll restructure when the pieces
are better in place.

Basically we reset the priority of a node before work on the
children. The children then bump their parent if they end up
having work left.

This is the first time we're seeing deep updates happening. The
new unit test demonstrates this.

There is an interesting case that happens when we fall back out of
a deep update. We end up "completing" a node that we didn't begin.
This probably breaks in coroutines. When that completes, it'll try
to render the sibling next but that should bail out so we check
for any pending work on the sibling. That one I'm not sure about.
This commit is contained in:
Sebastian Markbage
2016-06-30 12:55:54 -07:00
parent 8ad8bd1939
commit 5e65f2f622
4 changed files with 131 additions and 24 deletions
+1
View File
@@ -193,5 +193,6 @@ exports.createFiberFromCoroutine = function(coroutine : ReactCoroutine, priority
exports.createFiberFromYield = function(yieldNode : ReactYield, priorityLevel : PriorityLevel) {
const fiber = createFiber(YieldComponent, yieldNode.key);
fiber.pendingProps = {};
return fiber;
};
@@ -28,6 +28,7 @@ var {
YieldComponent,
} = ReactTypeOfWork;
var {
NoWork,
OffscreenPriority,
} = require('ReactPriorityLevel');
@@ -47,6 +48,7 @@ function updateFunctionalComponent(current, workInProgress) {
console.log('update fn:', fn.name);
var nextChildren = fn(props);
reconcileChildren(current, workInProgress, nextChildren);
workInProgress.pendingWorkPriority = NoWork;
}
function updateHostComponent(current, workInProgress) {
@@ -65,6 +67,7 @@ function updateHostComponent(current, workInProgress) {
nextChildren,
OffscreenPriority
);
workInProgress.pendingWorkPriority = OffscreenPriority;
return null;
} else {
workInProgress.child = ReactChildFiber.reconcileChildFibers(
@@ -73,6 +76,7 @@ function updateHostComponent(current, workInProgress) {
nextChildren,
priority
);
workInProgress.pendingWorkPriority = NoWork;
return workInProgress.child;
}
}
@@ -85,12 +89,19 @@ function mountIndeterminateComponent(current, workInProgress) {
console.log('performed work on class:', fn.name);
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
if (workInProgress.alternate) {
workInProgress.alternate.tag = ClassComponent;
}
} else {
console.log('performed work on fn:', fn.name);
// Proceed under the assumption that this is a functional component
workInProgress.tag = FunctionalComponent;
if (workInProgress.alternate) {
workInProgress.alternate.tag = FunctionalComponent;
}
}
reconcileChildren(current, workInProgress, value);
workInProgress.pendingWorkPriority = NoWork;
}
function updateCoroutineComponent(current, workInProgress) {
@@ -100,6 +111,7 @@ function updateCoroutineComponent(current, workInProgress) {
}
console.log('begin coroutine', workInProgress.type.name);
reconcileChildren(current, workInProgress, coroutine.children);
workInProgress.pendingWorkPriority = NoWork;
}
function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
@@ -114,6 +126,7 @@ function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
workInProgress.output = current.output;
workInProgress.child = current.child;
workInProgress.stateNode = current.stateNode;
workInProgress.pendingWorkPriority = NoWork;
return null;
}
if (workInProgress.pendingProps === workInProgress.memoizedProps) {
@@ -135,6 +148,7 @@ function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
reconcileChildren(current, workInProgress, workInProgress.pendingProps);
// A yield component is just a placeholder, we can just run through the
// next one immediately.
workInProgress.pendingWorkPriority = NoWork;
if (workInProgress.child) {
return beginWork(
workInProgress.child.alternate,
@@ -162,6 +176,7 @@ function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
case YieldComponent:
// A yield component is just a placeholder, we can just run through the
// next one immediately.
workInProgress.pendingWorkPriority = NoWork;
if (workInProgress.sibling) {
return beginWork(
workInProgress.sibling.alternate,
@@ -65,7 +65,7 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
let currentRootsWithPendingWork : ?Fiber = null;
function findNextUnitOfWork(priorityLevel : PriorityLevel) : ?Fiber {
function findNextUnitOfWorkAtPriority(priorityLevel : PriorityLevel) : ?Fiber {
let current = currentRootsWithPendingWork;
while (current) {
if (current.pendingWorkPriority !== 0 &&
@@ -74,11 +74,15 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
if (current.pendingProps !== null) {
// We found some work to do. We need to return the "work in progress"
// of this node which will be the alternate.
return current.alternate;
const clone = ReactFiber.cloneFiber(current, current.pendingWorkPriority);
clone.pendingProps = current.pendingProps;
return clone;
}
// If we have a child let's see if any of our children has work to do.
// Only bother doing this at all if the current priority level matches
// because it is the highest priority for the whole subtree.
// TODO: Coroutines can have work in their stateNode which is another
// type of child that needs to be searched for work.
if (current.child) {
current = current.child;
continue;
@@ -106,6 +110,21 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
return null;
}
function findNextUnitOfWork() {
// Find the highest possible priority work to do.
// This loop is unrolled just to satisfy Flow's enum constraint.
// We could make arbitrary many idle priority levels but having
// too many just means flushing changes too often.
let work = findNextUnitOfWorkAtPriority(HighPriority);
if (!work) {
work = findNextUnitOfWorkAtPriority(LowPriority);
if (!work) {
work = findNextUnitOfWorkAtPriority(OffscreenPriority);
}
}
return work;
}
function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
while (true) {
// The current, flushed, state of this fiber is the alternate.
@@ -114,29 +133,62 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
// progress.
const current = workInProgress.alternate;
const next = completeWork(current, 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;
// TODO: Stop using the parent for this purpose. I think this will break
// down in edge cases because when nodes are reused during bailouts, we
// don't know which of two parents was used. Instead we should maintain
// a temporary manual stack.
// $FlowFixMe: This downcast is not safe. It is intentionally an error.
const parent = workInProgress.parent;
// Ensure that remaining work priority bubbles up.
if (parent && workInProgress.pendingWorkPriority !== NoWork &&
(parent.pendingWorkPriority === NoWork ||
parent.pendingWorkPriority > workInProgress.pendingWorkPriority)) {
parent.pendingWorkPriority = workInProgress.pendingWorkPriority;
}
if (next) {
// If completing this work spawned new work, do that next.
return next;
} else if (workInProgress.sibling) {
// If there is more work to do in this parent, do that next.
return workInProgress.sibling;
} else if (workInProgress.parent) {
} else if (parent) {
// If there's no more work in this parent. Complete the parent.
// TODO: Stop using the parent for this purpose. I think this will break
// down in edge cases because when nodes are reused during bailouts, we
// don't know which of two parents was used. Instead we should maintain
// a temporary manual stack.
// $FlowFixMe: This downcast is not safe. It is intentionally an error.
workInProgress = workInProgress.parent;
workInProgress = parent;
} else {
// If we're at the root, there's no more work to do.
currentRootsWithPendingWork = null;
return null;
console.log('completed flush with remaining work at priority', workInProgress.pendingWorkPriority);
if (workInProgress.pendingWorkPriority !== NoWork) {
// TODO: This removes all but one node. Broken.
currentRootsWithPendingWork = workInProgress;
const nextWork = findNextUnitOfWork();
if (!nextWork) {
// Something went wrong and there wasn't actually any more work.
// TODO: This currently fails because of the parent/root hacks.
// throw new Error('Should never finish with a priority level unless there is work.');
currentRootsWithPendingWork = null;
return null;
}
return nextWork;
} else {
currentRootsWithPendingWork = null;
return null;
}
}
}
}
function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
// Ignore work if there is nothing to do.
if (workInProgress.pendingProps === null) {
return null;
}
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
@@ -154,17 +206,7 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
function performLowPriWork(deadline : Deadline) {
if (!nextUnitOfWork) {
// Find the highest possible priority work to do.
// This loop is unrolled just to satisfy Flow's enum constraint.
// We could make arbitrary many idle priority levels but having
// too many just means flushing changes too often.
nextUnitOfWork = findNextUnitOfWork(HighPriority);
if (!nextUnitOfWork) {
nextUnitOfWork = findNextUnitOfWork(LowPriority);
if (!nextUnitOfWork) {
nextUnitOfWork = findNextUnitOfWork(OffscreenPriority);
}
}
nextUnitOfWork = findNextUnitOfWork();
}
while (nextUnitOfWork) {
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
@@ -236,7 +278,7 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
},
unmountContainer(container : OpaqueNode) : void {
container.pendingProps = null;
container.pendingProps = [];
container.pendingWorkPriority = LowPriority;
scheduleLowPriWork(container);
@@ -161,7 +161,56 @@ describe('ReactIncremental', function() {
expect(ops).toEqual(['Foo', 'Bar', 'Bar']);
// TODO: Test the ability for a subtree to resume if it has lower priority.
});
it('can deprioritize unfinished work and resume it later', function() {
var ops = [];
function Bar(props) {
ops.push('Bar');
return <div>{props.children}</div>;
}
function Middle(props) {
ops.push('Middle');
return <span>{props.children}</span>;
}
function Foo(props) {
ops.push('Foo');
return (
<div>
<Bar>{props.text}</Bar>
<div hidden={true}>
<span>
<Middle>{props.text}</Middle>
</span>
</div>
<Bar>{props.text}</Bar>
</div>
);
}
// Init
ReactNoop.render(<Foo text="foo" />);
ReactNoop.flush();
ops = [];
// Render part of the work. This should be enough to flush everything except
// the middle which has lower priority.
ReactNoop.render(<Foo text="bar" />);
ReactNoop.flushLowPri(40);
expect(ops).toEqual(['Foo', 'Bar', 'Bar']);
ops = [];
// Flush only the remaining work
ReactNoop.flush();
expect(ops).toEqual(['Middle']);
});