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

[Fiber] Compute the Host Diff During Reconciliation #8607

Merged
merged 11 commits into from
Jan 20, 2017

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Dec 20, 2016

This uses prepareUpdate to compute the diff to apply. If there is no work to do, we don't schedule any updates at all for the commit phase.

Unfortunately wrapper components have custom logic that gets applied in the commit phase so for now we need to always schedule an update for those.

The payload has an array of only the prop key and values that need to update.

This makes the commit phase in the Sierpinski Triangle demo go from 2.5-3ms to <1.5ms. The overall time probably increases but at least now we won't risk dropping frames as we're updating low-pri content.

A neat side-effect of this is that we no longer depend on any "contextual information" like host context or root instance in the commit phase.

TODO 1: This doesn't correctly apply the diff when the is property switches from custom component mode to built-in component mode. I'll just need to deal with that when deleting values. Errr... This is not true. We can't switch between modes. I just added some unused code at some point because I thought we could.

TODO 2: A more fundamental problem is that we always need to update the pointer to the "current" fiber atm because of how the lazy event listener thing works. I could probably change it to store the props object instead of going through the fiber.

TODO 3: Flow infinite loops. (Updating to 0.37.1 doesn't help. #8608) Fixed using workaround.

@@ -127,6 +127,10 @@ var EventPluginHub = {
// live here; needs to be moved to a better place soon
if (typeof inst.tag === 'number') {
const props = inst.memoizedProps;
if (!props) {
// Work in progress.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is part of TODO 2 because if we have no event listeners, then we don't update the fiber pointer. If we don't update the Fiber pointer, then the new one becomes work in progress. If we try to read from work in progress, this might be null. If it completes, it might have event listeners on it that are not yet committed.

Therefore this isn't a great solution.

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like maybe this PR comment should be an inline comment so it doesn't get lost or forgotten?

} else {
(updatePayload = updatePayload || []).push(propKey, '' + nextHtml);
}
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should probably be (from #8652):

        if (nextHtml != null) {
          if (lastHtml !== nextHtml) {
              (updatePayload = updatePayload || []).push(propKey, '' + nextHtml);
          }
        } else {
          // TODO: ...

@gaearon
Copy link
Collaborator

gaearon commented Jan 11, 2017

(I used GH interface for resolving the conflict.)

@sebmarkbage
Copy link
Collaborator Author

I already rebased locally.

@gaearon
Copy link
Collaborator

gaearon commented Jan 11, 2017

:-( I should've asked.

@gaearon
Copy link
Collaborator

gaearon commented Jan 12, 2017

@sebmarkbage

Did you use a time machine writing these commits? 😄

To the next person utterly confused by GitHub's display of this PR, ba11c3b depends on 6ae87cf but for some reason was committed a day earlier, and thus appears before it in the list.

screen shot 2017-01-11 at 23 59 26

@sebmarkbage
Copy link
Collaborator Author

Like politics, rebasing is all about rewriting history. In chronological order or not.

for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
if (!styleUpdates) {
styleUpdates = {};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we pack style updates in the same array with special keys or would that be too complicated?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Interestingly RN did this by packing all fields into one payload object. Caused a bunch of weird artifacts that made diffing harder to optimize.

We should reevaluate the actual algorithms here but for now I just wanted compatibility with CSSPropertyOperations since it is shared with Stack.

} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
// We eagerly listen to this even though we haven't committed yet.
ensureListeningTo(rootContainerElement, propKey);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we listen here instead of the commit?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One of the things this fixes is that getRootHostContainer is no longer needed in the commit phase if we do it here. As we know, getRootHostContainer is not safe to use during commits since we may not have the right context.

There's also no need to do this in the commit phase so might as well do it during the time sliced parts.

isCustomComponentTag : boolean,
) : void {
// TODO: Handle wasCustomComponentTag
for (var i = 0; i < updatePayload.length; i+=2) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Style nit: i += 2

}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML ||
propKey === CHILDREN) {
// TODO: Clear innerHTML. This is currently broken in Fiber because we are
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think @acdlite said this was solved.

} else if (registrationNameModules.hasOwnProperty(propKey)) {
// This is a special case. If any listener updates we need to ensure
// that the "current" fiber pointer gets updated so we need a commit
// to update this element.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This depends on precacheFiber call in ReactDOMFiber.js, right? It's a bit non-obvious that an empty array here is needed to trigger the path there even though updateProperties in this file won't do anything. I guess doesn’t matter if we’ll unify them soon.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Also note that there is an open TODO items related to this.

for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp =
lastProps != null ? lastProps[propKey] : undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think lastProps can be null here. You can probably read directly now that mount and update is separate.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nice catch. I wish Flow would warn about this.

(updatePayload = updatePayload || []).push(propKey, null);
}
}
for (propKey in nextProps) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

While unrelated to this PR, I think the most common path is lastProps and nextProps with the same prop keys in the same order. I wonder if we could optimize this as a faster case with just one loop, and only do the second loop if we find a mismatch during the first one.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That assumes that you can iterate both interleaved which for...in doesn't let you do. It would require something like Object.keys or a generator with for...in to be really fast. Not too worried about this though. Something we can micro-optimize easily if needed.

}
}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Stray comment.

}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
if (!styleUpdates) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

How can styleUpdates be truthy here? It could only get created if lastStyle existed and we wanted to delete some styles. But in this case we would get into if (lastProp) branch instead of this one. Unless I’m missing something.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The code in this branch is also wrong since it'll push a null. I'll delete.

}
}
if (styleUpdates) {
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it still necessary to delay it until the end? Why? This structure makes the above style code a little hard to follow, especially the case where you have to reassign styleUpdates right after pushing it to the payload so that it doesn't get skipped when it's falsy.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Donno. I just did a line-by-line translation.

Copy link
Collaborator Author

@sebmarkbage sebmarkbage Jan 12, 2017

Choose a reason for hiding this comment

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

If I don't put it here, I need it in all the places where I could create styleUpdates. Because there may not be a STYLE field in the nextProps but might be one in lastProps or vice versa. I also don't want to end up pushing it twice. I'm not sure that's better.

@sebmarkbage sebmarkbage changed the title [WIP][Fiber] Compute the Host Diff During Reconciliation [Fiber] Compute the Host Diff During Reconciliation Jan 17, 2017
@sebmarkbage
Copy link
Collaborator Author

Ok, this I fixed the outstanding TODO by storing the "current" props on the DOM node and in the React Native instance cache. Not too happy about how this all came together but not really sure what we should do about the event system.

This is nice because it minimizes work during commit and initial mount but still relies on "diffing" event listeners during updates to figure out if we should update at all.

A plausible alternative model would be to traverse the "current" tree while processing the event, or even process events as part of reconciliation which does that traversal anyway.

workInProgress.updateQueue = (updatePayload : any);
// If the update payload indicates that there is a change or if there
// is a new ref we mark this as an update.
if (updatePayload || current.ref !== workInProgress.ref) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Refs shouldn't schedule an Update effect anymore, since we added the Ref effect

Copy link
Collaborator

@acdlite acdlite left a comment

Choose a reason for hiding this comment

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

👍

@@ -37,6 +37,9 @@ src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js
* should throw on full document render w/ no markup
* supports findDOMNode on full-page components

src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js
* should control a value in reentrant events
Copy link
Collaborator

Choose a reason for hiding this comment

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

I missed why this is failing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah yes. I missed this too. This is probably either a bug or behavior change regarding event timing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed: #8839

This then gets stored on updateQueue so that the renderer doesn't need to
think about how to store this.

It then gets passed into commitUpdate during the commit phase.

This allows renderers to do the diffing during the time sliced path,
allocate a queue for changes and do only the absolute minimal work to
apply those changes in the commit phase.

If refs update we still schedule and update.
We now take advantage of the new capability to diff properties early.
We do this by generating an update payload in the form of an array with
each property name and value that we're about to update.
Wrapper components have custom logic that gets applied at the commit phase
so we always need to ensure that we schedule an update for them.
No use case yet and I removed it from commitUpdate earlier.
This illustrates the problem that happens if we store a pointer to the
Fiber and then choose not to update that pointer when no properties change.
That causes an old Fiber to be retained on the DOM node. Then that Fiber
can be reused by the pooling mechanism which then will mutate that Fiber
with new event handlers, which makes them active before commit.
This represents the current set of event listeners. By not relying on the
Fiber, it allows us to avoid doing any effects in the commit phase when
nothing changes.

This is a bit ugly. Not super happy how this all came together.
@sebmarkbage sebmarkbage merged commit b354db2 into facebook:master Jan 20, 2017
GordyD added a commit to GordyD/react that referenced this pull request Nov 19, 2017
…rrentPropsFromNode

 - After researching usage of getNodeFromInstance we can test getNodeFromInstance dispatching some events and asserting the id of the currentTarget
 - After checking git blame for getFiberCurrentPropsFromNode and reading through facebook#8607 I found a test that we can simplify to assert behavior of the function by ensuring event handler props are updated from the fiber props. Swapping out the implementation of this function with `return node[internalInstanceKey].memoizedProps` results in a failure.
gaearon pushed a commit that referenced this pull request Nov 19, 2017
…11383)

* Rewrite ReactDOMComponentTree-test to test behavior using Public API

 - Part of #11299
 - I've tried to identify cases where code within ReactDOMComponentTree is exercised and have updated accordingly but I'm not entirely sure whether I'm on the right track. I thought I'd PR to get feedback from the community. Looking forward to comments.

* Prettier and lint changes

* Remove testing of internals and add test cases for testing behavior exhibited after use of getInstanceFromNode

* [RFC] Update testing approach to verify exhibited behavior dependent upon methods in ReactDOMComponentTree

* Remove tests from event handlers and use sync tests

* Prettier changes

* Rename variables to be more semantic

* Prettier updates

* Update test following review

 - Use beforeEach and afterEach to set up and tear down container element for use in each test
 - Move any functions specific to one test to within test body (improves readability imo)

* Add coverage for getNodeFromInstance and implementation of getFiberCurrentPropsFromNode
 - After researching usage of getNodeFromInstance we can test getNodeFromInstance dispatching some events and asserting the id of the currentTarget
 - After checking git blame for getFiberCurrentPropsFromNode and reading through #8607 I found a test that we can simplify to assert behavior of the function by ensuring event handler props are updated from the fiber props. Swapping out the implementation of this function with `return node[internalInstanceKey].memoizedProps` results in a failure.
Ethan-Arrowood pushed a commit to Ethan-Arrowood/react that referenced this pull request Dec 8, 2017
…acebook#11383)

* Rewrite ReactDOMComponentTree-test to test behavior using Public API

 - Part of facebook#11299
 - I've tried to identify cases where code within ReactDOMComponentTree is exercised and have updated accordingly but I'm not entirely sure whether I'm on the right track. I thought I'd PR to get feedback from the community. Looking forward to comments.

* Prettier and lint changes

* Remove testing of internals and add test cases for testing behavior exhibited after use of getInstanceFromNode

* [RFC] Update testing approach to verify exhibited behavior dependent upon methods in ReactDOMComponentTree

* Remove tests from event handlers and use sync tests

* Prettier changes

* Rename variables to be more semantic

* Prettier updates

* Update test following review

 - Use beforeEach and afterEach to set up and tear down container element for use in each test
 - Move any functions specific to one test to within test body (improves readability imo)

* Add coverage for getNodeFromInstance and implementation of getFiberCurrentPropsFromNode
 - After researching usage of getNodeFromInstance we can test getNodeFromInstance dispatching some events and asserting the id of the currentTarget
 - After checking git blame for getFiberCurrentPropsFromNode and reading through facebook#8607 I found a test that we can simplify to assert behavior of the function by ensuring event handler props are updated from the fiber props. Swapping out the implementation of this function with `return node[internalInstanceKey].memoizedProps` results in a failure.
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