-
Notifications
You must be signed in to change notification settings - Fork 45.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Fiber] Separate priority for updates #8538
Conversation
e5c09de
to
3f2d671
Compare
fdfadf1
to
ea76d23
Compare
d2917af
to
f82a1b7
Compare
@sebmarkbage This is ready for review now. I updated it using the algorithm we discussed yesterday. |
render() { | ||
var props = this.props; | ||
var s = props.size * 1.3; | ||
var idx = props.idx; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like dead code.
}; | ||
return ( | ||
<div style={style} onMouseEnter={() => this.enter()} onMouseLeave={() => this.leave()}> | ||
{this.state.hover ? 'X' : props.text} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would actually be nice to use to use something from props here since that shows that we're able to reuse previous props for a new condition. Maybe '"' + props.text + '"'
or something?
|
||
ReactNoop.render(<Foo />); | ||
ReactNoop.flush(); | ||
expect(Object.keys(state)).toEqual(['a', 'b', 'c']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't rely on object iteration order. This has failed us in the past, and in fact, we have talked about enforcing that all keys are always defined to enforce that the state object has a consistent hidden class.
Can you rewrite this to test that there are actually two renders and that the first one only have part of the state and the second one has all three?
This same goes for all the other tests in this file that use the same hack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dang and I thought this was so clever too :D
scheduleUpdateAtPriority(fiber, TaskPriority); | ||
} | ||
|
||
function scheduleSetState(fiber : Fiber, partialState : any) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we fine a way to move all of these out of the scheduler? These are super specific to the class API and as we add new APIs we don't want to bloat the scheduler which already has too many unrelated concepts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had them outside at first but it was awkward because it required exposing access the current priority context, which seems leaky. Either the class module needs to be aware of the priority context or the scheduler needs to be aware of the update queue.
@@ -460,7 +488,11 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C | |||
ReactFiberInstrumentation.debugTool.onWillBeginWork(workInProgress); | |||
} | |||
// See if beginning this work spawns more work. | |||
|
|||
priorityContextBeforeReconciliation = priorityContext; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is in the hot path. Any reason this can't be outside the while loop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm well it needs to change whenever nextPriorityLevel
changes. Maybe in findNextUnitOfWork
? I'll see.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
@@ -479,10 +511,13 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C | |||
|
|||
ReactCurrentOwner.current = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While you're at it, this also doesn't belong in the hot path for the same reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will address in separate PR
@@ -355,6 +376,13 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C | |||
|
|||
function resetWorkPriority(workInProgress : Fiber) { | |||
let newPriority = NoWork; | |||
|
|||
// Check for pending update priority |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a comment that we usually expect this to be null
so this shouldn't be a perf issue.
f977e6e
to
e455853
Compare
@sebmarkbage Updating the example now but I believe I addressed the rest of your feedback |
workInProgress.effectTag |= Update; | ||
} else { | ||
// If an update was already in progress, we should schedule an Update | ||
// effect even though we're bailing out, so that cWU/cDU are called. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't affect CWU. Will need to come back to this to ensure I understand it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a lot to go through here. Will have to continue tomorrow. It's a skill but it might be worth while working on techniques for breaking these PRs apart. Even if you have to build on top of the base refactor, it would be quicker to review the base refactor if you excluded things like the new dev warnings, and the solutions to componentDidUpdate/refs/etc. We're still behind a flag but we're getting to a point where we need to keep the code base pretty stable so that others don't get blocked. So whatever you can do to minimize the risk of a PR is helpful even if it slows you down since it might be the thing that avoids someone being blocked on a bug - which helps parallelism.
Yup that's fair. I'll work on that in the future. Happy to pull things out to help the main bit land. Usually I've tried to avoid any extra stuff unless it's needed to get the tests to pass, or to fix a new bug that surfaced, but some of these commits can indeed be moved to separate PRs. |
b63cb7a
to
0878d04
Compare
return 0; | ||
} | ||
if (a === NoWork && b !== NoWork) { | ||
return -Infinity; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should stick to integer numbers instead of floats (Infinity is a floating point value) if possible so that we can optimize it as such.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Np that value is immediately reset anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh nvm I thought this was a different line (on my phone). Okay I'll just use like 999 or something :D
@@ -26,6 +26,8 @@ const { | |||
TaskPriority, | |||
} = require('ReactPriorityLevel'); | |||
|
|||
const { MAX_SAFE_INTEGER } = Number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just use 255 :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha okay
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MAX_SAFE_INTEGER will also trigger the float type in the VM because it's higher than 32 or 31 bit representations which ever is the one used.
5717e20
to
b17c30c
Compare
We need to be able to access both, and since the list uses forward pointers, it makes more sense to point to the one that comes first. Otherwise to get the last progressed update you have to start at the beginning of the list.
The queue maintains a pointer to the last progressed update in the list. Updates that come after that pointer are pending. The pointer is set to the end of the list during reconciliation. Pending updates are sorted by priority then insertion. Progressed updates are sorted by the order in which they were applied during reconciliation, which may not be by priority: if a component bails out before the updates are committed, in the next render, the progressed updates are applied in the same order that they were previously, even if a higher priority update comes in. Once a progressed update is flushed/committed, it's removed from the queue.
This is needed to get the triangle demo working.
Instead clear updates on the work-in-progress during the begin phase. Aborted updates are recovered by cloning from the current fiber.
This is the most common case, so we should avoid scanning the entire list to get to the end.
The update is scheduled as if the current processing update has already been processed; if it has the same or higher priority, it will be flushed in the same batch. We also print a warning.
setState inside render/cWRP should have the same priority as whatever level is currently being reconciled.
We can just check if the deadline has expired.
...rather than changing it on every unit of work.
Infinity is a floating point value.
b17c30c
to
3a946fe
Compare
Just rebased, didn't change anything |
const queue2 = fiber.alternate ? ensureUpdateQueue(fiber.alternate) : null; | ||
|
||
// Warn if an update is scheduled from inside an updater function. | ||
if (__DEV__ && typeof methodName === 'string' && (queue1.isProcessing || (queue2 && queue2.isProcessing))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make this it's own if block? The console.error
s should also use the warning
module.
It used to be that our dead code elimination didn't work on more conditionals. It might now.
Regardless, I'd like to know for sure that this gets stripped and that's easier to determine with a single block. If the parenthesis are wrong somewhere, and you get a ||
in the condition then the whole thing and all its dependencies won't be stripped.
let insertBefore1; | ||
let insertAfter2; | ||
let insertBefore2; | ||
for (let i = 0; queue && i < 2; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find the intention of this loop really confusing since it is meant to terminate after only two iterations. Seems it would be easier to just explode it but I'll try to read it as is first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was exploded but then whenever I made changes I kept forgetting to update both versions. Maybe that's not a big deal once the code is stable, though.
} | ||
if (update.callback) { | ||
if (callbackList && callbackList.last) { | ||
callbackList.last.next = update; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops I forgot to clone this update before adding it to the callback list
// time we commit. | ||
update2 = cloneUpdate(update1); | ||
} | ||
insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
insertAfter.next = update
is the thing to worry about here. I think that either it is the same and we only set it twice or they're always different and copies but in different queue or one of them is null. It would be nice to colocate this logic somehow so that this assumption can be reasoned about locally. Right now you have to ensure that both of these functions line up and understand that this assumption exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I'll remove insertUpdateIntoQueue
and inline the logic and see if that helps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe I'll keep insertUpdateIntoQueue
but won't use it for the case where the update isn't cloned, and instead handle that case manually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only reason that function exists is for first
and last
, it's really easy to forget to update those.
// However, if incoming update is inserted into the same position of both lists, | ||
// we shouldn't make a copy. | ||
|
||
function insertUpdate(fiber : Fiber, update : Update, methodName : ?string) : void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not great that we have different method signatures for dev and prod and we also don't want to pass this in prod. If we're luck it just gets inlined and dropped. I don't have a better proposal. Just flagging it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose you could move the error out of this function. It's a pretty simple conditional to replicate to the methods (setState / replaceState / forceUpdate). That would be nicer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could perform the check separately inside addUpdate
, addCallback
, et al.
// use the original `prevState`, not the accumulated `state` | ||
state = getStateFromUpdate(update, instance, prevState, props); | ||
dontMutatePrevState = true; | ||
isEmpty = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I don't think this isEmpty
thing is necessary
let partialState; | ||
if (update.isReplace) { | ||
// A replace should drop all previous updates in the queue, so | ||
// use the original `prevState`, not the accumulated `state` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is any previous state needed? Why is prevState
better than state
? Seems arbitrary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming is bad. state
is the accumulated state. prevState
is the initial accumulation.
Roughly what this code is doing is:
// initialState is the state before applying any updates
const nextState = updates.reduce((accumulatedState, update) => {
if (update.isReplace) {
return update.partialState;
} else {
return merge(accumulatedState, update.partialState);
}
}, initialState);
By applying the update to the initial state rather than the accumulation, it effectively drops the previous updates. But... this actually isn't right anyway because we should only drop updates with the same priority. We already do this in addReplaceState
. So this comment is wrong and we should always pass state
(the accumulation).
node = node.next; | ||
|
||
workInProgress.callbackList = callbackList; | ||
workInProgress.memoizedState = state; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have concerned because we don't set memoizedProps here. If we abort and then resume we might end up relying on the wrong memoizedState but I don't see any bugs yet and we probably want to move to a model where memoizedProps is also set in begin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is detritus... something I tried out but isn't actually necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll remove
Placement: 1, // 0b0000001 | ||
Update: 2, // 0b0000010 | ||
PlacementAndUpdate: 3, // 0b0000011 | ||
ForceUpdate: 4, // 0b0000100 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move this back to the update queue.
Review feedback:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some concerns about the code style that we should address in follow ups but I think this works.
Let's just undo the effect tag thing and then we can land.
Okay I'll remove the effect tag thing and then do the rest of the follow-up in a separate PR. |
Go back to using a flag, instead. I removed it before because I thought we might want to get rid of the top-level UpdateQueue type and put the fields directly on the fiber, but since we're keeping UpdateQueue we can put hasForceUpdate on there.
Fixes a few issues with the way updates currently work.
Right now we reset the work-in-progress's update queue during the complete phase, which means updates that occur during
render
or in a child's begin phase (constructor,cWM
,cWRP
,render
) are completely dropped. This isn't an ideal pattern but Stack supports it and it's fairly common. The lack of support in Fiber today is causing a nasty infinite loop bug for some components at Facebook.To fix this, the update queue maintains a pointer to the first pending update. When an update is used during reconciliation, the pointer is set to null to indicate that the entire queue has been processed. If new updates come in before the component is committed, the pointer points to the first new update. Then in the commit phase, the processed updates are dropped, but the pending updates are kept in the queue.
Another problem is that we use the same priority field for both props and updates, and when we reset the priority field during the complete phase, we don't have a way to read the priority of the pending updates. In the first pass, I'll add a priority field to the update queue to solve this. What we really want, though, is for each individual update to have its own priority, so that when we render a component, we only process the updates that match the current priority level.
May do in a follow-up; not needed to fix the infinite loop bug, but necessary to get the Fiber triangle demo working: