Skip to content

Commit c28caef

Browse files
committed
Separate priority field for pending updates
Currently there is a single priority field `pendingWorkPriority` that represents the priority of the entire subtree. The scheduler does a breadth-first search to find the next unit of work with a matching priority level. If a subtree's priority does not match, that entire subtree is skipped. That means when an update is scheduled deep in the tree (`setState`), its priority must be bubbled to the top of the tree. The problem is that this causes the all the parent priorities to be overridden — there's no way to schedule a high priority update inside a low priority tree without increasing the priority of the entire tree. To address this, this PR introduces a separate priority field called `pendingUpdatePriority` (not 100% sold on the name). The new field represents the priority of the pending props and state, which may be lower than the priority of the entire tree. Now, when the scheduler is searching for work to perform, it only matches if the ` pendingUpdatePriority` matches. It continues to use the `pendingWorkPriority` as a way to ignore subtrees if they do not match. (To elaborate further: because the work priority is the highest priority of any node in the entire tree, if it does not match, the tree can be skipped.)
1 parent a6dc55f commit c28caef

File tree

8 files changed

+134
-25
lines changed

8 files changed

+134
-25
lines changed

src/renderers/shared/fiber/ReactChildFiber.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ var {
2828

2929
var ReactFiber = require('ReactFiber');
3030
var ReactReifiedYield = require('ReactReifiedYield');
31+
var ReactPriorityLevel = require('ReactPriorityLevel');
3132

3233
const {
3334
cloneFiber,
@@ -40,6 +41,10 @@ const {
4041
createReifiedYield,
4142
} = ReactReifiedYield;
4243

44+
const {
45+
NoWork,
46+
} = ReactPriorityLevel;
47+
4348
const isArray = Array.isArray;
4449

4550
function ChildReconciler(shouldClone) {
@@ -66,7 +71,11 @@ function ChildReconciler(shouldClone) {
6671
const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
6772
if (!shouldClone) {
6873
// TODO: This might be lowering the priority of nested unfinished work.
69-
clone.pendingWorkPriority = priority;
74+
clone.pendingUpdatePriority = priority;
75+
if (clone.pendingWorkPriority === NoWork ||
76+
clone.pendingWorkPriority > priority) {
77+
clone.pendingWorkPriority = priority;
78+
}
7079
}
7180
clone.pendingProps = element.props;
7281
clone.child = existingChild.child;
@@ -134,7 +143,11 @@ function ChildReconciler(shouldClone) {
134143
const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
135144
if (!shouldClone) {
136145
// TODO: This might be lowering the priority of nested unfinished work.
137-
clone.pendingWorkPriority = priority;
146+
clone.pendingUpdatePriority = priority;
147+
if (clone.pendingWorkPriority === NoWork ||
148+
clone.pendingWorkPriority > priority) {
149+
clone.pendingWorkPriority = priority;
150+
}
138151
}
139152
clone.pendingProps = element.props;
140153
clone.child = existingChild.child;

src/renderers/shared/fiber/ReactFiber.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ export type Fiber = Instance & {
9999
lastEffect: ?Fiber,
100100

101101

102-
// This will be used to quickly determine if a subtree has no pending changes.
102+
// The update priority is the priority of a fiber's pending props and state.
103+
// It may be lower than the priority of the entire subtree.
104+
pendingUpdatePriority: PriorityLevel,
105+
// The work priority is the priority of the entire subtree. It will be used to
106+
// quickly determine if a subtree has no pending changes.
103107
pendingWorkPriority: PriorityLevel,
104108

105109
// This is a pooled version of a Fiber. Every fiber that gets updated will
@@ -150,6 +154,7 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
150154
firstEffect: null,
151155
lastEffect: null,
152156

157+
pendingUpdatePriority: NoWork,
153158
pendingWorkPriority: NoWork,
154159

155160
childInProgress: null,
@@ -165,6 +170,28 @@ function shouldConstruct(Component) {
165170

166171
// This is used to create an alternate fiber to do work on.
167172
exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fiber {
173+
// Don't deprioritize when cloning. Unlike other priority comparisons (e.g.
174+
// in the scheduler), this one must check that priorityLevel is not equal to
175+
// NoWork, otherwise work will be dropped. For complete correctness, the other
176+
// priority comparisons should also perform this check, even though it's not
177+
// an issue in practice. I didn't catch this at first and it created a subtle
178+
// bug, which suggests we may need to extract the logic into a
179+
// utility function (shouldOverridePriority).
180+
let updatePriority;
181+
let workPriority;
182+
if (priorityLevel !== NoWork &&
183+
(priorityLevel < fiber.pendingUpdatePriority || fiber.pendingUpdatePriority === NoWork)) {
184+
updatePriority = priorityLevel;
185+
} else {
186+
updatePriority = fiber.pendingWorkPriority;
187+
}
188+
if (updatePriority !== NoWork &&
189+
(updatePriority < fiber.pendingWorkPriority || fiber.pendingWorkPriority === NoWork)) {
190+
workPriority = updatePriority;
191+
} else {
192+
workPriority = fiber.pendingWorkPriority;
193+
}
194+
168195
// We use a double buffering pooling technique because we know that we'll only
169196
// ever need at most two versions of a tree. We pool the "other" unused node
170197
// that we're free to reuse. This is lazily created to avoid allocating extra
@@ -180,7 +207,8 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
180207
alt.pendingProps = fiber.pendingProps;
181208
alt.updateQueue = fiber.updateQueue;
182209
alt.callbackList = fiber.callbackList;
183-
alt.pendingWorkPriority = priorityLevel;
210+
alt.pendingUpdatePriority = updatePriority;
211+
alt.pendingWorkPriority = workPriority;
184212

185213
alt.memoizedProps = fiber.memoizedProps;
186214
alt.output = fiber.output;
@@ -206,7 +234,8 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
206234
alt.pendingProps = fiber.pendingProps;
207235
alt.updateQueue = fiber.updateQueue;
208236
alt.callbackList = fiber.callbackList;
209-
alt.pendingWorkPriority = priorityLevel;
237+
alt.pendingUpdatePriority = updatePriority;
238+
alt.pendingWorkPriority = workPriority;
210239

211240
alt.memoizedProps = fiber.memoizedProps;
212241
alt.output = fiber.output;
@@ -224,6 +253,7 @@ exports.createHostContainerFiber = function() {
224253
exports.createFiberFromElement = function(element : ReactElement, priorityLevel : PriorityLevel) {
225254
const fiber = createFiberFromElementType(element.type, element.key);
226255
fiber.pendingProps = element.props;
256+
fiber.pendingUpdatePriority = priorityLevel;
227257
fiber.pendingWorkPriority = priorityLevel;
228258
return fiber;
229259
};
@@ -253,6 +283,7 @@ exports.createFiberFromCoroutine = function(coroutine : ReactCoroutine, priority
253283
const fiber = createFiber(CoroutineComponent, coroutine.key);
254284
fiber.type = coroutine.handler;
255285
fiber.pendingProps = coroutine;
286+
fiber.pendingUpdatePriority = priorityLevel;
256287
fiber.pendingWorkPriority = priorityLevel;
257288
return fiber;
258289
};

src/renderers/shared/fiber/ReactFiberBeginWork.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
5353
function reconcileChildren(current, workInProgress, nextChildren) {
5454
// TODO: Children needs to be able to reconcile in place if we are
5555
// overriding progressed work.
56-
const priority = workInProgress.pendingWorkPriority;
56+
const priority = workInProgress.pendingUpdatePriority;
5757
reconcileChildrenAtPriority(current, workInProgress, nextChildren, priority);
5858
}
5959

@@ -99,14 +99,29 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
9999
if (fiber.alternate !== null) {
100100
fiber.alternate.updateQueue = updateQueue;
101101
}
102+
103+
// Set the update priority of the fiber and its alternate
104+
if (fiber.pendingUpdatePriority === NoWork ||
105+
fiber.pendingUpdatePriority > priorityLevel) {
106+
fiber.pendingUpdatePriority = priorityLevel;
107+
}
108+
if (fiber.alternate !== null) {
109+
if (fiber.alternate.pendingUpdatePriority === NoWork ||
110+
fiber.alternate.pendingUpdatePriority > priorityLevel) {
111+
fiber.alternate.pendingUpdatePriority = priorityLevel;
112+
}
113+
}
114+
115+
// For this fiber and all its ancestors and their alternates, set the
116+
// work (subtree) priority
102117
while (true) {
103118
if (fiber.pendingWorkPriority === NoWork ||
104-
fiber.pendingWorkPriority >= priorityLevel) {
119+
fiber.pendingWorkPriority > priorityLevel) {
105120
fiber.pendingWorkPriority = priorityLevel;
106121
}
107122
if (fiber.alternate !== null) {
108123
if (fiber.alternate.pendingWorkPriority === NoWork ||
109-
fiber.alternate.pendingWorkPriority >= priorityLevel) {
124+
fiber.alternate.pendingWorkPriority > priorityLevel) {
110125
fiber.alternate.pendingWorkPriority = priorityLevel;
111126
}
112127
}
@@ -216,7 +231,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
216231
function updateHostComponent(current, workInProgress) {
217232
var nextChildren = workInProgress.pendingProps.children;
218233

219-
let priority = workInProgress.pendingWorkPriority;
234+
let priority = workInProgress.pendingUpdatePriority;
220235
if (workInProgress.pendingProps.hidden && priority !== OffscreenPriority) {
221236
// If this host component is hidden, we can reconcile its children at
222237
// the lowest priority and bail out from this particular pass. Unless, we're
@@ -310,7 +325,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
310325
workInProgress.memoizedProps = workInProgress.pendingProps;
311326
workInProgress.memoizedState = state;
312327
workInProgress.output = current.output;
313-
const priorityLevel = workInProgress.pendingWorkPriority;
328+
const priorityLevel = workInProgress.pendingUpdatePriority;
314329
workInProgress.pendingProps = null;
315330
workInProgress.updateQueue = current.updateQueue = null;
316331
workInProgress.stateNode = current.stateNode;
@@ -324,7 +339,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
324339
if (workInProgress.pendingWorkPriority !== NoWork && workInProgress.pendingWorkPriority <= priorityLevel) {
325340
return findNextUnitOfWorkAtPriority(
326341
workInProgress,
327-
workInProgress.pendingWorkPriority
342+
priorityLevel
328343
);
329344
} else {
330345
return null;
@@ -339,7 +354,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
339354
// If we started this work before, and finished it, or if we're in a
340355
// ping-pong update scenario, this version could already be what we're
341356
// looking for. In that case, we should be able to just bail out.
342-
const priorityLevel = workInProgress.pendingWorkPriority;
357+
const priorityLevel = workInProgress.pendingUpdatePriority;
343358
workInProgress.pendingProps = null;
344359
workInProgress.updateQueue = null;
345360
if (workInProgress.alternate) {

src/renderers/shared/fiber/ReactFiberCompleteWork.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
113113

114114
var currentFirstChild = current ? current.stateNode : null;
115115
// Inherit the priority of the returnFiber.
116-
const priority = workInProgress.pendingWorkPriority;
116+
const priority = workInProgress.pendingUpdatePriority;
117117
workInProgress.stateNode = reconcileChildFibers(
118118
workInProgress,
119119
currentFirstChild,

src/renderers/shared/fiber/ReactFiberPendingWork.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function cloneSiblings(current : Fiber, workInProgress : Fiber, returnFiber : Fi
2727
current = current.sibling;
2828
workInProgress = workInProgress.sibling = cloneFiber(
2929
current,
30-
current.pendingWorkPriority
30+
current.pendingUpdatePriority
3131
);
3232
workInProgress.return = returnFiber;
3333
}
@@ -66,7 +66,7 @@ function cloneChildrenIfNeeded(workInProgress : Fiber) {
6666
}
6767
workInProgress.child = cloneFiber(
6868
currentChild,
69-
currentChild.pendingWorkPriority
69+
currentChild.pendingUpdatePriority
7070
);
7171
cloneSiblings(currentChild, workInProgress.child, workInProgress);
7272
}
@@ -77,7 +77,9 @@ exports.findNextUnitOfWorkAtPriority = function(workRoot : Fiber, priorityLevel
7777
if (workInProgress.pendingWorkPriority !== NoWork &&
7878
workInProgress.pendingWorkPriority <= priorityLevel) {
7979
// This node has work to do that fits our priority level criteria.
80-
if (workInProgress.pendingProps !== null || workInProgress.updateQueue !== null) {
80+
if (workInProgress.pendingUpdatePriority !== NoWork &&
81+
workInProgress.pendingUpdatePriority <= priorityLevel &&
82+
(workInProgress.pendingProps !== null || workInProgress.updateQueue !== null)) {
8183
return workInProgress;
8284
}
8385

@@ -94,14 +96,9 @@ exports.findNextUnitOfWorkAtPriority = function(workRoot : Fiber, priorityLevel
9496
}
9597
child = workInProgress.childInProgress;
9698
while (child) {
97-
// Don't bother drilling further down this tree if there is no child
98-
// with more content.
99-
// TODO: Shouldn't this still drill down even though the first
100-
// shallow level doesn't have anything pending on it.
101-
if (child.pendingWorkPriority !== NoWork &&
102-
child.pendingWorkPriority <= priorityLevel &&
103-
child.pendingProps !== null) {
104-
return child;
99+
const next = exports.findNextUnitOfWorkAtPriority(child, priorityLevel);
100+
if (next) {
101+
return next;
105102
}
106103
child = child.sibling;
107104
}
@@ -113,13 +110,15 @@ exports.findNextUnitOfWorkAtPriority = function(workRoot : Fiber, priorityLevel
113110
// If we match the priority but has no child and no work to do,
114111
// then we can safely reset the flag.
115112
workInProgress.pendingWorkPriority = NoWork;
113+
workInProgress.pendingUpdatePriority = NoWork;
116114
}
117115
if (workInProgress === workRoot) {
118116
if (workInProgress.pendingWorkPriority <= priorityLevel) {
119117
// If this subtree had work left to do, we would have returned it by
120118
// now. This could happen if a child with pending work gets cleaned up
121119
// but we don't clear the flag then. It is safe to reset it now.
122120
workInProgress.pendingWorkPriority = NoWork;
121+
workInProgress.pendingUpdatePriority = NoWork;
123122
}
124123
return null;
125124
}
@@ -133,6 +132,7 @@ exports.findNextUnitOfWorkAtPriority = function(workRoot : Fiber, priorityLevel
133132
// now. This could happen if a child with pending work gets cleaned up
134133
// but we don't clear the flag then. It is safe to reset it now.
135134
workInProgress.pendingWorkPriority = NoWork;
135+
workInProgress.pendingUpdatePriority = NoWork;
136136
}
137137
}
138138
workInProgress.sibling.return = workInProgress.return;

src/renderers/shared/fiber/ReactFiberReconciler.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconci
7373
// TODO: This should not override the pendingWorkPriority if there is
7474
// higher priority work in the subtree.
7575
container.pendingProps = element;
76+
container.pendingUpdatePriority = LowPriority;
7677
container.pendingWorkPriority = LowPriority;
7778

7879
scheduleLowPriWork(root, LowPriority);
@@ -88,6 +89,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconci
8889
const root : FiberRoot = (container.stateNode : any);
8990
// TODO: Use pending work/state instead of props.
9091
root.current.pendingProps = element;
92+
root.current.pendingUpdatePriority = LowPriority;
9193
root.current.pendingWorkPriority = LowPriority;
9294

9395
scheduleLowPriWork(root, LowPriority);
@@ -98,6 +100,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconci
98100
const root : FiberRoot = (container.stateNode : any);
99101
// TODO: Use pending work/state instead of props.
100102
root.current.pendingProps = [];
103+
root.current.pendingUpdatePriority = LowPriority;
101104
root.current.pendingWorkPriority = LowPriority;
102105

103106
scheduleLowPriWork(root, LowPriority);

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
7878
while (root) {
7979
let rootInProgress = cloneFiber(
8080
root.current,
81-
root.current.pendingWorkPriority
81+
root.current.pendingUpdatePriority
8282
);
83+
8384
// Find the highest possible priority work to do.
8485
// This loop is unrolled just to satisfy Flow's enum constraint.
8586
// We could make arbitrary many idle priority levels but having
@@ -151,6 +152,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
151152
const current = workInProgress.alternate;
152153
const next = completeWork(current, workInProgress);
153154

155+
workInProgress.pendingUpdatePriority = NoWork;
154156
resetWorkPriority(workInProgress);
155157

156158
// The work is now done. We don't need this anymore. This flags

src/renderers/shared/fiber/__tests__/ReactIncremental-test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,51 @@ describe('ReactIncremental', function() {
692692
expect(instance.state.called).toEqual(true);
693693
});
694694

695+
it('can setState without overriding its parents\' priority', () => {
696+
let ops = [];
697+
let instance;
698+
class Baz extends React.Component {
699+
constructor() {
700+
super();
701+
instance = this;
702+
this.state = { num: 0 };
703+
}
704+
render() {
705+
ops.push('Baz');
706+
return <span />;
707+
}
708+
}
709+
710+
function Bar({ id }) {
711+
ops.push('Bar' + id);
712+
return <div />;
713+
}
714+
715+
function Foo() {
716+
ops.push('Foo');
717+
return [
718+
<section hidden={true}>
719+
<Bar id={1} />
720+
<Baz />
721+
</section>,
722+
<Bar id={2} />,
723+
];
724+
}
725+
726+
ReactNoop.render(<Foo />);
727+
ReactNoop.flush();
728+
expect(ops).toEqual(['Foo', 'Bar2', 'Bar1', 'Baz']);
729+
ops = [];
730+
ReactNoop.render(<Foo />);
731+
// Even though Baz is in a hidden subtree, calling setState gives it a
732+
// higher priority. It should not affect the priority of anything else in
733+
// the subtree.
734+
instance.setState({ num: 1 });
735+
ReactNoop.flush();
736+
// Baz should come before Bar1 because it has higher priority
737+
expect(ops).toEqual(['Foo', 'Bar2', 'Baz', 'Bar1']);
738+
});
739+
695740
it('can replaceState', () => {
696741
let instance;
697742
const Bar = React.createClass({

0 commit comments

Comments
 (0)