Skip to content

[Fiber] Push class context providers even if they crash#8627

Merged
gaearon merged 7 commits intofacebook:masterfrom
gaearon:context-boundaries
Dec 22, 2016
Merged

[Fiber] Push class context providers even if they crash#8627
gaearon merged 7 commits intofacebook:masterfrom
gaearon:context-boundaries

Conversation

@gaearon
Copy link
Collaborator

@gaearon gaearon commented Dec 22, 2016

Previously we used to push them only after the instance was available. This caused issues in cases an error is thrown during componentWillMount().

In that case we never got to pushing the provider in the begin phase, but in complete phase the provider check returned true since the instance existed by that point. As a result we got mismatching context pops.

We solve the issue by making the context check independent of whether the instance actually exists. Instead we're checking the type itself.

This lets us push class context early. However there's another problem: we might not know the context value. If the instance is not yet created, we can't call getChildContext on it.

To fix this, we are introducing a way to replace current value on the stack, and a way to read the previous value. This also helps remove some branching and split the memoized from invalidated code paths.

I also added a test from #8604 to verify this also solves the problem described in that PR.

Previously we used to push them only after the instance was available. This caused issues in cases an error is thrown during componentWillMount().

In that case we never got to pushing the provider in the begin phase, but in complete phase the provider check returned true since the instance existed by that point. As a result we got mismatching context pops.

We solve the issue by making the context check independent of whether the instance actually exists. Instead we're checking the type itself.

This lets us push class context early. However there's another problem: we might not know the context value. If the instance is not yet created, we can't call getChildContext on it.

To fix this, we are introducing a way to replace current value on the stack, and a way to read the previous value. This also helps remove some branching and split the memoized from invalidated code paths.
Also rename another test to have a shorter name.
All uses of push() and pop() are guarded by it anyway.

This makes it more similar to how we use host context.

There is only one other place where isContextProvider() is used and that's legacy code needed for renderSubtree().
@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

I found one additional bug there but I can't figure out how to fix it.

When an error boundary itself is a context provider, beginFailedWork doesn't push it.
If I make it push it though, I get unexpected pops in unwinding.
I don't understand how this part of the code works but maybe @acdlite has ideas.

It's not blocking for this PR though.

const unmaskedContext = getUnmaskedContext();
const context = {};
const hasOwnContext = isContextProvider(workInProgress);
// If it is a context provider then use the previous context instead.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: This comment makes sense to me, with the context I have, but might not be sufficient explanation for someone seeing this part of the code for the first time. Maybe we should elaborate about why we use the previous one. Maybe even just a pointer to see the comments in pushContextProvider?

// Instance might be null, if the fiber errored during construction
fiber.stateNode &&
typeof fiber.stateNode.getChildContext === 'function'
fiber.type.childContextTypes != null
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

exports.invalidateContextProvider = function(workInProgress : Fiber) : void {
const instance = workInProgress.stateNode;
if (instance == null) {
throw new Error('Expected to have an instance by this point.');
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious why you're using throw here instead of invariant? (How do we determine when to do one vs the other?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We should always use invariants but I didn't bother because we're going to do a separate pass to replace them all anyway. (It's in the umbrella.)

if (index === 0) {
return null;
}
return valueStack[index - 1];
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't previous actually the value at valueStack[index]?

Current is in cursor.current and previous is the top item in the stack (at index).

Copy link
Contributor

Choose a reason for hiding this comment

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

PS I was already thinking about doing this, but I believe I'll submit a follow-up PR that adds some unit tests for ReactFiberStack itself.

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, I think you're right. I wonder why tests didn't catch this.

Copy link
Contributor

Choose a reason for hiding this comment

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

No problem. I'll add some tests for this specific type of thing. 👍

}
return null;
}
if (index === 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This check is unnecessary. We only care about < 0 which is already checked for above.

Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than "unnecessary" I should have said "bad" because it would prevent the default value from being returned as previous if one was set. (eg like the default for didPerformWorkStackCursor)

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

Addressed.

@bvaughn
Copy link
Contributor

bvaughn commented Dec 22, 2016

Nice clean up Dan!

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

My only concern is whether c57cf3e is bad from optimization point of view. I.e. maybe it prevents inlining or something. What do you think?

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

Hmm maybe off by one wasn't a bug after all..

@bvaughn
Copy link
Contributor

bvaughn commented Dec 22, 2016

My only concern if c57cf3e is bad from optimization point of view. I.e. maybe it prevents inlining or something. What do you think?

Unfortunately this is not something I'm that knowledgable about yet. I should defer to @sebmarkbage.

Hmm maybe off by one wasn't a bug after all..

You mean getPrevious? It definitely was.

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

Then there must be another off-by-one there that cancels it out since the test started failing after I fixed it (it goes into an infinite loop and shows wrong context in some tests).

@bvaughn
Copy link
Contributor

bvaughn commented Dec 22, 2016

Hm. I'll finish up these ReactFiberStack tests and then I can help diagnose. Should only take a few minutes.

@bvaughn
Copy link
Contributor

bvaughn commented Dec 22, 2016

Tests added for ReactFiberStack ~> gaearon#1

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

Hmm, I just realized valueStack and fiberStack aren't "tracking" each other despite both being indexed by the same index. fiberStack[i] corresponds to the current value whereas valueStack[i] is one value behind.

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

This was a little confusing to me. While we're at it: why doesn't reset reset the current value (but resets its fiber)?

@bvaughn
Copy link
Contributor

bvaughn commented Dec 22, 2016

why doesn't reset reset the current value (but resets its fiber)

The stack doesn't track its allocated cursors so it has no way to reset current values. (ReactFiberContext and ReactFiberHostContext are responsible for doing this.) In the future- if we change the context API- we might be allocating a lot of cursors so it seemed best not to track them/hold onto them. (I was kind of on the fence about this to be honest.)

Once the stack has been reset, any cursor values should be ignored as they are no longer safe.

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

I totally misunderstood how stack works.
I thought values were held separately. 😄

@bvaughn
Copy link
Contributor

bvaughn commented Dec 22, 2016

FYI in ReactIncremental test...

  • This test fails: merges and masks context
  • This test hangs: does not leak own context into context provider

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

My approach worked by accident. By addressing index - 1 I just managed to get the right value (e.g. context rather than didWork) from the stack but it just happened to work.

@bvaughn
Copy link
Contributor

bvaughn commented Dec 22, 2016

Ha! That's funny 😁 Yeah I had a few moments of off-by-one confusion like that when working on the initial stack branch.

@bvaughn
Copy link
Contributor

bvaughn commented Dec 22, 2016

I totally should have spotted that. Sorry.

This type of stack-management stuff is still a bit foreign to my thinking.

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

Do I understand correctly that order of push and pop operations must always be exact reverse or we risk popping the wrong thing corresponding to the right fiber?

@bvaughn
Copy link
Contributor

bvaughn commented Dec 22, 2016

Yes. That is correct. Mirror order.

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

It might make sense to track cursor objects together with fibers in DEV for better checks.

Copy link
Contributor

@bvaughn bvaughn left a comment

Choose a reason for hiding this comment

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

We need an alternate approach to getPrevious. Sorry for not spotting this initially. ☹️

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

I don't think replace can work on anything but innermost level, right? We can't replace two things one after another. We can only pop twice and push twice.

@bvaughn
Copy link
Contributor

bvaughn commented Dec 22, 2016

You can replace multiple cursors, so long as you're only concerned with updating the "current" value of each. (Or you could pop+push again yes.) replace checks for a Fiber match when but that's not technically needed. You could push another cursor/Fiber, and still safely replace a value from a previous cursor/Fiber's push, so long as it's the top-most value for that cursor.

The part about this that doesn't work is getting the previous value. Stack currently doesn't know or care about what's on it or in what order, so the previous value is not guaranteed to be for the same cursor. We'd have to track previous on the cursor itself if we wanted to be able to return it. Alternately we could mirror pop-then-repush but this assumes that nothing else has pushed to the stack in the meanwhile.

@bvaughn
Copy link
Contributor

bvaughn commented Dec 22, 2016

We'd have to track previous on the cursor itself if we wanted to be able to return it.

Actually even this would be problematic, because once we pop- we'd have the same problem, trying to figure what the previous was.

I think the only safe thing to do here is to unwind the stack a little, then push them back on the stack once we've read them.

The previous algorithm was flawed and worked by accident, as shown by the failing tests after an off-by-one was fixed.

The implementation of getPrevious() was incorrect because the context stack currently has no notion of a previous value per cursor.

Instead, we are caching the previous value directly in the ReactFiberContext in a local variable.

Additionally, we are using push() and pop() instead of adding a new replace() method.
@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

OK I think that fixes it.

Copy link
Contributor

@bvaughn bvaughn left a comment

Choose a reason for hiding this comment

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

This looks good!

@gaearon
Copy link
Collaborator Author

gaearon commented Dec 22, 2016

I'll land since it fixes bugs, happy to follow up on perf improvements if @sebmarkbage sees any.

@gaearon gaearon merged commit c978f78 into facebook:master Dec 22, 2016
@gaearon gaearon deleted the context-boundaries branch December 22, 2016 18:36
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.

3 participants