Skip to content
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

Closed
wants to merge 62 commits into from
Closed

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented May 16, 2017

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:

  • Do we have progressed work since the last commit?
    • If not, reset to current.
    • If we do, did it render at the current priority?
      • If not, stash the progressed work and reset to current. This "forks" the child set.
      • If it did, reuse the progressed work.
  • Do we have pending props, state, or context?
    • If so, reconcile. This will update the progressed work.
    • If not, bailout. This will leave the progressed work alone.

TODO:

  • pendingWorkPriority should represent subtree, excluding fiber it belongs to
  • Rebase on top of Hydration of previously rendered server markup #9580 once that lands.
  • Finish implementing types of work
    • HostRoot
    • HostComponent
      • hidden
    • HostText
    • IndeterminateComponent
    • FunctionalComponent
    • Fragments
    • Classes
    • Portals
    • Coroutines
  • Decide on heuristic for determining whether a work-in-progress fiber has been worked on since the previous commit, or if it's actually the "previous current." This can be tricky after a bailout because fibers that bail out aren't marked as progressed work.
  • Hydration
  • Bikeshed on naming before landing:
    • Distinguish between the last work that was reconciled (currently called "progressed" work) and the most recent fiber that was worked on, including bailouts (currently called "newest" work).
    • "Resume" a work-in-progress versus "resetting" by cloning from current.
    • We use "bailout" to mean "don't reconcile," but can we be more specific? Sometimes you bailout and continue working on the child, and other times we do a "full" bailout, reuse the current child, and return null.
  • Write additional unit tests (or possible rewrite some existing ones) using an explicit 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.)
  • Follow up with DevTools after this is merged Fiber devtools fixes react-devtools#755

Copy link
Contributor

@trueadm trueadm left a 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)
Copy link
Contributor

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.

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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??

Copy link
Collaborator Author

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could newestWork be lastWork?

Copy link
Collaborator Author

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.

Copy link
Contributor

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

@acdlite acdlite force-pushed the beginphase branch 2 times, most recently from ef1df76 to ea7d604 Compare May 30, 2017 20:42
@acdlite acdlite changed the title [Much WIP. Very not working.] Refactor progressed work model Refactor progressed work model May 31, 2017
@acdlite acdlite force-pushed the beginphase branch 4 times, most recently from 061ef03 to 4797a2c Compare June 1, 2017 01:45
@acdlite
Copy link
Collaborator Author

acdlite commented Jun 1, 2017

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.)

@acdlite
Copy link
Collaborator Author

acdlite commented Jun 1, 2017

Just pushed a commit demonstrating a bug.

@acdlite acdlite force-pushed the beginphase branch 5 times, most recently from e78cc31 to ebe18b5 Compare June 2, 2017 21:26
invariant(
ownerFiber.tag === ClassComponent,
'Stateless function components cannot have refs.',
);
Copy link
Collaborator

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"?

Copy link
Collaborator Author

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` " +
Copy link
Collaborator

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.
Copy link
Collaborator

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).

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

Copy link
Collaborator Author

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,
Copy link
Collaborator

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?

Copy link
Collaborator Author

@acdlite acdlite Jun 3, 2017

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',
);
Copy link
Collaborator

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.

Copy link
Collaborator Author

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
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.
acdlite added 16 commits June 5, 2017 18:30
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.
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.
@gaearon
Copy link
Collaborator

gaearon commented Jan 5, 2018

I'll close as stale.

@gaearon gaearon closed this Jan 5, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants