Skip to content

Tie MultiChild queue to ReactReconcileTransaction #1157

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

Closed
wants to merge 1 commit into from

Conversation

sophiebits
Copy link
Collaborator

Fixes #1147.

This now forces all DOM operations for a subtree to be applied when the calling setState or React.renderComponent call returns (when updates aren't being batched). This means that we can't batch innerHTML setting across different component hierarchies, but our strategy for doing so before seems flawed. It could be possible to make the old way work but it would require making setState always async even when batching isn't in play and refactoring DOMChildrenOperations to not be confused by multiple updates to the same node.

Still need to add tests. Not sure this is exactly what we want to do but I think it agrees more with what we've been doing.

@sophiebits
Copy link
Collaborator Author

Opinions here @jordwalke @yungsters @sebmarkbage?

errorThrown ? clearQueue() : processQueue();
}
}
this._updateChildren(nextNestedChildren, transaction);
Copy link
Contributor

Choose a reason for hiding this comment

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

No try-finally here anymore so could bring the function inline again?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

react-art overrides it: https://github.com/facebook/react-art/blob/master/src/ReactART.js#L147-L158. I should update the comment though.

@sebmarkbage
Copy link
Collaborator

I don't yet fully understand the consequences of this change on nested operations and loops such as componentDidUpdate retriggering updates at higher levels.

We do want to move to a fully async setState by making all operations batched.

For systems which only partially uses React, batching of innerHTML across roots is really important but you may be right that it's already flawed.

@jordwalke
Copy link
Contributor

For systems which only partially uses React, batching of innerHTML across roots is really important

I think it's a cool feature, but there is significant complexity in maintaining it. People who use many react roots won't benefit from a nice perf bump, but I'm not sure how likely it is to be the case that an event will cause several roots to simultaneously need child updates (within the same event loop).

@syranide
Copy link
Contributor

Seeing as this is the first we (or I) have ever even heard of this bug, I think it is safe to assume that performance is a non-issue here (as long as it doesn't explode). It's even the double-updating that is the root cause, so it's basically an inefficiency introduced by the user.

@sophiebits
Copy link
Collaborator Author

@syranide @jordwalke Batching innerHTML setting across multiple trees like we do now could be useful for things like petehunt's sortable component (https://gist.github.com/petehunt/7882164) where each child makes a separate component root. We're only running into trouble right now when combining that with multiple updates to the same component at one time. I agree though that there is code complexity and this might not be super common.

@jordwalke
Copy link
Contributor

After an initial look at the diff, either way, this structure seems much cleaner. Whether or not we batch markup generation across multiple parents/roots, queueing mutations is helpful for animations in the future (and sets us up to be more testable). Queueing of mutations in general isn't the main cause of code-complexity - batching markup across multiple parents/roots is. This diff doesn't remove the later, but makes the former more elegant/correct. Note how @spicyj eliminated this updateDepth tracking in the old code. Any time we are executing code only on the first depth of the tree, it's a good sign that we should be using the existing Transaction - that's what it's there for. So this diff seems like win to me (but haven't looked carefully enough to see if there's any bugs). Thanks for doing this, @spicyj!

@jordwalke
Copy link
Contributor

Unrelated to this diff: I would like it if the core were architected more like this - where mutations are queued. That allows us to clearly separate the reconciliation process into two separate, testable stages:

  1. Determine the set of mutations that occur (they would be queued up).
  2. Apply those mutations.

Right now, before and after this diff, child creation mutations are queued up, but other types of mutations are not (things like setting properties etc). If we make sure that every mutation is queued (assuming that it doesn't hurt performance in practice, not that I'm concerned) - then we can compute the diff, and then easily implement various strategies of carrying out the mutations over time. We could throttle across requestAnimationFrame at an even more granular level than was possible before. We could record the stream of mutations, log them and replay them very easily, and analyze where timing jank comes from in the DOM for a mutation stream representing a real world application. Also, it just seems cleaner, consistent, and more functional ;)

This diff nudges us in that direction by making queueing easier to reason about in general by tying it to the Transaction.

@syranide
Copy link
Contributor

Side-note, we should probably add http://jsbin.com/caxipewo/6/edit (#1147 (comment)) or some variation of it as a test.

@sophiebits
Copy link
Collaborator Author

@sebmarkbage Currently, when the reconcile transaction is closed, the mount-ready callbacks are executed, meaning that all DOM operations should have been flushed by that point. Like you suggest, perhaps we should change setState so that the transaction is executed later, but the current behavior is wrong.

sophiebits added a commit to sophiebits/react that referenced this pull request Apr 9, 2014
Should make facebook#1350 better and will also take away any performance hit from facebook#1157.

Test Plan:
grunt test
@facebook-github-bot
Copy link

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@zpao
Copy link
Member

zpao commented May 22, 2014

@sebmarkbage, can you take another look at this before I pull it in

@sebmarkbage
Copy link
Collaborator

Can we get a unit tests for the observable change in behavior? (There is one in the referenced issue.)

This will increase memory usage for large updates. :/

Could another solution be to keep mounts in a hash map based on IDs so that you can clear them or replace them when there is a subsequent remove or update respectively?

Is this even a valid behavior? componentWillUpdate shouldn't cause setState. Can we just forbid it?

@sophiebits
Copy link
Collaborator Author

Sorry, the description here is a bit out of date. The referenced issue was actually fixed by #1363 and I already added a test case for it.

I believe that this PR should actually cause no observable differences; it just cleans up the code.

When does this increase memory usage?

We can probably forbid componentWillUpdate from calling setState. It's already disallowed when calling setState on the component whose componentWillUpdate is running. I can do that as a separate diff.

@sebmarkbage
Copy link
Collaborator

The underlying memory used by the multi-child queue can be reused by other updates in the same transaction. Difficult to predict real world behavior since it can depend on GC implementation and memory load. On a memory constrained device (like low-end Android) this could potentially crash the browser instead of GC:ing.

@sophiebits
Copy link
Collaborator Author

It seems unlikely to me that the list of queued updates would be significant in comparison to the cost of the actual DOM nodes and component instances. Do you disagree that the queueing that ReactMultiChild does is odd and belongs in ReactReconcileTransaction?

@yungsters
Copy link
Contributor

I agree this is cleaner.

From what I can see, this would only increase the number of object allocations by ~3 per simultaneous component tree rendered. I don't think that is a concern.

The reason I originally batched child mutation for the entire component tree (instead of only among siblings) was because I found that generating a bunch of nodes using a single innerHTML roundtrip was cheaper than many small ones. This will still be markedly better than where we were at before, but is definitely worse in the case where there are many separate component hierarchy.

As a follow-up, could we consider batching cross-hierarchy between transactions?

@sophiebits
Copy link
Collaborator Author

@yungsters Not sure if you saw #1358 and #1363; now I believe we share a reconcile transaction in at least every case where MultiChild batched before, so we're still batching innerHTML after this diff.

@sebmarkbage
Copy link
Collaborator

This goes in direct opposite direction of the live reconciler that @jordwalke and I started. That's something we should re-explore.

I was concerned that every update would be batched (which is something I considered for better error handling). This only batches moves (which are rare) and deletions. A large set of deletions are common but the signature is pretty small.

I think this is fine to pull in if we want to get it in. Not sure if there's a huge win since it's potentially slower and comes with a risk.

@yungsters Do you think it would be worth while pulling this in to see if it fixes that weird reconciling bug we've seen?

@yungsters
Copy link
Contributor

Interesting… that bug escapes me, but I can see how debugging across hierarchies might have been making it really hard for me to trace the root cause. Yes! Let's get this in.

@sophiebits
Copy link
Collaborator Author

Assuming by "live reconciler" you mean not pre-flattening children (cf. #942), I don't believe this makes that any harder.

I think this is the first I've heard of "that weird reconciling bug"; if it's still happening let me know how to repro and I'll take a look.

Fixes facebook#1147.

This now forces all DOM operations for a subtree to be applied when the calling `setState` or `React.renderComponent` call returns (when updates aren't being batched). This means that we can't batch innerHTML setting across different component hierarchies, but our strategy for doing so before seems flawed. It could be possible to make the old way work but it would require making setState always async even when batching isn't in play and refactoring DOMChildrenOperations to not be confused by multiple updates to the same node.
@yungsters
Copy link
Contributor

@spicyj That's the trouble, no one can consistently reproduce the issue.

The symptom is that the Facebook comment user interface (e.g. below each post), the row containing the <textarea> will render in place of a comment above it instead of at the bottom where it is supposed to be. We suspect it occurs when new stories are sent down asynchronously.

I traced it to ReactMultiChild, but I ran through the logic several times and could not figure out what would have caused the new child nodes to be inserted or moved incorrectly.

@sophiebits
Copy link
Collaborator Author

@yungsters Today @sebmarkbage concluded it might be #1593.

@zpao
Copy link
Member

zpao commented Aug 13, 2014

Is it valuable to keep this open? Do we have plans to move forward or is this going to end up just being an experiment that influences further designs?

@sophiebits
Copy link
Collaborator Author

I still think this should be merged. Last I remember, @sebmarkbage thought it was probably fine but didn't want to merge it while you were all out on vacation.

@sebmarkbage
Copy link
Collaborator

We should bring this in but it requires a lot of testing to make sure that we don't have functionality or performance regressions. This doesn't actually change any behavior. We can pull this in as part of something larger, such as error boundaries or something related like that, that actually fixes something.

@zpao
Copy link
Member

zpao commented Sep 10, 2014

@spicyj looks like this needs an update before we can try to merge this.

@quantizor
Copy link
Contributor

@spicyj Is this still happening?

but it would require making setState always async even when batching isn't in play

Seems like that is the default behavior now, yes?

@sophiebits
Copy link
Collaborator Author

@yaycmyk Might happen as part of some other upcoming work. Any reason it's important to you?

@quantizor
Copy link
Contributor

@spicyj Nothing in particular. I was looking through some of the older PRs and this one leapt out as interesting since the setState lifecycle was confusing to me when I first started with React.

@sophiebits
Copy link
Collaborator Author

This is just an internal refactoring, should have no user-facing changes.

@sophiebits
Copy link
Collaborator Author

Superseded by #5547.

Do I get a prize for closing an old PR?

@sophiebits sophiebits closed this Nov 25, 2015
@sebmarkbage
Copy link
Collaborator

💝

@zpao
Copy link
Member

zpao commented Nov 28, 2015

🎁

@jordwalke
Copy link
Contributor

🎈 🎉 🎁

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.

Obscure DOMChildrenOperations error when doing multiple updates
9 participants