-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Refactor progressed work model #9695
Conversation
65b5f78
to
93e5cb3
Compare
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.
Added some comments.
// in-progress if its the most recent fiber. | ||
// TODO: It'd be nice to come up with a heuristic here that didn't require | ||
// an additional fiber field. | ||
!(progressedWork === workInProgress && workInProgress !== newestWork) |
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 this be written differently? I think we can make it easier to interpret by not having the wrapping inverse of the inner condition.
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.
Interesting, I wrote it that way at first but then rewrote it this way because I thought it was easier to understand haha :D
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 like choosing between De Morgan's Law and a double negative: which is more confusing??
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.
Version 1:
!(progressedWork === workInProgress && workInProgress !== newestWork)
"Don't resume on the work-in-progress if it's not the newest work"
Version 2:
progressedWork !== workInProgress || workInProgress === newestWork
"Don't resume on the work-in-progress unless it's the newest work."
Yeah I guess version 2 is better.
@@ -154,6 +154,7 @@ export type Fiber = { | |||
// progressed work or if it is better to continue from the current state. | |||
progressedPriority: PriorityLevel, | |||
progressedWork: ProgressedWork, | |||
newestWork: Fiber, |
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 newestWork
be lastWork
?
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.
Yeah maybe that's a bit better. Don't think either name makes it clear what it does or how it's different from progressedWork
. I'll be sure we circle back to the naming before landing this PR.
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.
Now we have another mostly synonymous term to add to the mix 🤡 current, in-progress, progressed, newest
ef1df76
to
ea7d604
Compare
061ef03
to
4797a2c
Compare
Got all the unit tests passing. This is now ready for review. (Edit: Found a bug, so may want to hold off before doing a thorough review. Feedback is welcome, though.) |
Just pushed a commit demonstrating a bug. |
e78cc31
to
ebe18b5
Compare
invariant( | ||
ownerFiber.tag === ClassComponent, | ||
'Stateless function components cannot have refs.', | ||
); |
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.
Should this say "cannot have string refs. Use callback refs instead"?
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.
Yes
invariant( | ||
owner != null, | ||
'Only a ReactOwner can have refs. You might be adding a ref to a ' + | ||
"component that was not created inside a component's `render` " + |
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 we're at it, can we remove "ReactOwner" from the message? I don't think it has any meaning to people outside.
} | ||
const ref = function(value) { | ||
const refs = | ||
// TODO: Comparison to emptyObject always fails. Don't know why. |
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 make sure we fix this before merging. Should be something simple (eg maybe there's a resetModules call in an unfortunate place).
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.
To the best of my knowledge, comparing object instances, even if structurally "identical" will always evaluate false.
({} === {}) // 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.
It will return true if they reference the same object in memory, which is what this is checking
const emptyObj = {};
const refs = emptyObj;
refs === emptyObj; // true
// A pooled ProgressedWork object, allocated once per fiber pair. Should not | ||
// be accessed outside of this module. | ||
_pooledProgressedWork: null | ProgressedWork, | ||
newestWork: Fiber, |
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 you please add comment for why this field matters?
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 a comment in beginWork but yeah I'll add one here, too. I'm planning to do another comments pass before landing.
invariant( | ||
current !== null && current.child === workInProgress.child, | ||
'Expected child to be the current child', | ||
); |
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 note this is internal error and they should file a bug.
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.
Saving that for the end :D
@@ -911,7 +849,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>( | |||
|
|||
// Complete the boundary as if it rendered null. This will unmount | |||
// the failed tree. | |||
beginFailedWork(boundary.alternate, boundary, priorityLevel); | |||
// TODO: We should unmount with task priority so that nothing else |
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.
Is this TODO part of this PR or a followup? Seems important.
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 it can be a follow up along with the other error issues we know about. Not related to this PR, just something I noticed.
Make sure we resume on progressed hidden children. I split these out into their own functions so that it doesn't distract from the normal, non-hidden code path. Will also make it easier to reuse later if/when we apply the offscreen children feature to other types of components.
There was some unnecessary abstraction in ReactFiberClassComponent that was making it difficult to maintain and, ironically, causing some duplication. I've moved most of that module into the begin phase, and consolidated the separate mount, resume mount, and update functions into a single function that's easier to reason about.
I would expect that whenever we resume on progressed work, the child set is a work-in-progress set.
Without this guard, the function throws
...rather than in reconcile and begin. Easier to keep track of.
Progressed priority could be less than work priority if there's a setState deep in the tree that's less than the current render priority but greater than the priority at which the fiber last reconciled.
ProgressedWork is a subset of Fiber. We use a separate Flow type to prevent access of extra properties, but we use a whole Fiber for monomorphism.
I had this wrong in an earlier commit. Performing work in the work-in-progress tree should never add work to the current tree. Only state updates do. So in the complete phase, we should bubble priority up from the children even if they are current, so that we don't drop pending updates. Also realized there's no reason to check if a work-in-progress is valid during a bailout. By the time we get to that function, we already determined if the work-in-progress is valid during the resume-or-reset step.
New test suite simulates the triangle demo. It renders a recursive tree and flushes it unit by unit, resetting the unit of work pointer to the root after each triangle is rendered. It also simulates the hover effect by updating a target node with high priority. The test is written in such a way that we can add fuzz testing on top of it later. For now, I've "manually" fuzz tested by trying out arbitrary values.
Makes bugs more noticeable.
Avoided doing this originally because I didn't want to add a field to the fork, and figured we could infer this in other ways. But the fork is a full Fiber for monomorphism purposes, anyway, so we already have it.
This allows for multiple nodes to be active at a time, which isn't possible in the demo because to hover over on node you must leave the other.
Most incremental tests rely on yielding at a specific point. Using an explicit API, rather than relying on fake expiration times that rely on implementation details, should make our tests more resilient and reduce false positives.
If `insertAfter` is null, the insertion position is at the start of the list. Similarly, if the `insertBefore` is null, the insertion position is at the end of the list. The starting/end position of one list is not necessarily the same as the starting/end position of another. So, only compare non-null values.
Previous version couldn't increment the counter more than once before flushing, which meant it couldn't reproduce certain errors that occur when running the triangle demo in Firefox, where starvation is worse. The new version uses a Redux-like approach, with actions.
This actually wasn't causing an error because the arguments were flipped twice: the first time to the wrong order, and the second time back to the correct order :D
This bug illustrates that our `progressedPriority` model is wrong. When deciding whether to resume work, we should compare the priority at which the parent reconciled its children, not the priority at which the fiber itself reconciled.
I'll close as stale. |
As the title suggests, this isn't ready for review yet, but I think the big pieces are in place.
This refactors the whole begin phase, particularly our model for progressed work. I tried to avoid doing a big refactor like this in the first pass, but eventually gave up — there are just too many inter-related things that had to change.
This also incorporates a change to make pendingWorkPriority exclude the priority of the fiber it belongs to, as in #8716. Again, I tried to keep this separate but it was too tied to everything else.
I wrote detailed comments inline as I refactored. I'll do an additional pass once it's closer to review.
If you're interested in taking a look, read through ReactFiberBeginWork.js. I think it's easier to follow conceptually. The basic algorithm is now:
TODO:
pendingWorkPriority
should represent subtree, excluding fiber it belongs tohidden
yield
API to avoid brittleness. (Will probably do this in a later PR; tests aren't changing significantly and I don't think it's blocking.)