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

Added ReactFiberStack shared by ReactFiberContext and ReactFiberHostContext #8611

Merged
merged 10 commits into from
Dec 22, 2016

Conversation

bvaughn
Copy link
Contributor

@bvaughn bvaughn commented Dec 21, 2016

Overview

ReactFiberStack is the new underlying stack construct used by ReactFiberContext and ReactFiberHostContext. The goal is to simplify the 2 context managers and to add earlier and better dev-warnings when we pop unexpectedly.

This change set causes a lot of tests to fail as we are currently popping too many times and/or in the wrong order. I'm pushing this branch now to share with Sebastian as he is working on a related cleanup in beginWork.

Work needing to be done for this branch

  • Track Fibers with unique host contexts to avoid over-popping; (see this)
  • Tighten up the Fiber | null type in ReactFiberStack by making sure that we always pass a Fiber down; currently this is not the case for pushHostContainer and popHostContainer.
  • Verify that no new test failures are caused by this refactor.

valueStack[index] = emptyObject;

if (__DEV__) {
fiberStack[index] = emptyObject;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why emptyObject instead of null on both of these? We could use ES2015 .fill() here if available.

Another thing we could do if we're worried about leakage is to store the max index of the previous run and then only clear the delta between the previous two runs. That way if we end up refilling the stack every time we rerender we don't have to fill it twice.

Copy link
Contributor Author

@bvaughn bvaughn Dec 21, 2016

Choose a reason for hiding this comment

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

My initial thought was to go with null, but I noticed that ReactFiberContext returned emptyObject in a few cases so I followed that pattern. I'll change it to null. Probably best to have the emptyObject fallback happening in ReactFiberContext rather than ReactFiberStack anyway.


import type { Fiber } from 'ReactFiber';

export type StackCursor = {|
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should make this generic type StackCursor<T> = {| current: T |}.

You will have to make some unsound things when you pop here but then everywhere else is fine.


let index = -1;

exports.createCursor = function() : StackCursor {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you add an initial default value, Flow will let you make this generic.

function<T>(defaultValue : T) : StackCursor<T> {

When you can invoke it with createCursor((null : ?string)) to get a typed version of the stack cursor.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Passing a default value feels a bit awkward since (at least for now) none of the cursors have meaningful defaults. But the generics benefit is nice! So sure! 😄

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Dec 21, 2016

In ReactFiberScheduler we call unwindContext followed by unwindHostContext. The point of these is to reset the context of both to where the error boundary is. However, this will break with your change because the popping needs to be interleaved not sequential.

I.e. you need to pop both (or all three I guess) until we reach the desired error boundary. I think we need to have an unwindWork that does only the normal popping that would happen in completeWork based on the tag. This is effectively what goes into a finally block in a programming language.

EDIT: Basically what unwindHostContext is already doing. Just break that out into a separate module and have it unwind the ReactFiberContext too.

gaearon referenced this pull request in sebmarkbage/react Dec 21, 2016
The functional component branch is a bit special since it has sCU in it.

The class component branch actually already covers this in a bit convoluted
way.

I will next replicate this check in each other branch.
@bvaughn
Copy link
Contributor Author

bvaughn commented Dec 21, 2016

Regarding unwindContext and unwindHostContext, it seems like the simplest solution would be to rename unwindHostContext to something like unwindContexts and add a case for ClassComponent that pop context providers. Not sure what the benefit of it being a separate module is at the moment since the scheduler is the only thing that unwinds. What's the motivation there?

@bvaughn
Copy link
Contributor Author

bvaughn commented Dec 21, 2016

Another TODO item for me: In pushHostContainer, we don't push if the current host context and the next/child host context are equal.

In order to avoid over-popping, I need to add another cursor to track the fibers we push for. And to do that, I think we'll need to address the other TODO to pass the host container Fiber down.

I have a meeting shortly but I'll work on both of these afterward.

@sebmarkbage
Copy link
Collaborator

For some reason I thought that unwindHostContext wasn't in the scheduler. Ignore the comment about separate module.

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Dec 21, 2016

I was thinking that maybe we should tag the host components with a different tag if they don't need to change context. That way we know when we complete that we don't need to pop it. Similarly, the next time we update and beginWork, we can pick the fast path of skipping checking for context changes.

@bvaughn
Copy link
Contributor Author

bvaughn commented Dec 21, 2016

Where would this tagging be done? Seems inappropriate for ReactFiberHostContext to modify a Fiber in any way. I was planning on tracking which host Fibers provided unique context with another StackCursor. In theory this should work but my first attempt causes 3 Jest tests to hang. Still investigating.

If you'd like to chat about ideas here ping me on Messenger. 😄

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Dec 21, 2016

The thing calling pushHostContext can mutate the tag from beginWork. Indeterminate and Coroutine components already do this. I'm not sure the ReactFiberContext and ReactFiberHostContext modules need to exist as abstractions at all. Their can also just be inlined at their callsites. Don't let the abstraction get in your way.


function getRootHostContainer() : C {
if (rootInstance == null) {
if (rootInstanceStackCursor.current == null) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor nit: could you read .current once? I don't know if compiler is smart enough to know it hasn't changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Absolutely.

@gaearon
Copy link
Collaborator

gaearon commented Dec 21, 2016

The issue I'm noticing is that adding

      static childContextTypes = {
        foo: React.PropTypes.number
      };
      getChildContext() {
        return {foo: 42};
      }

to some of the "broken" components in ReactErrorBoundaries-test breaks some tests with unexpected pops. It's not just BrokenRender that fails this way but BrokenComponentWillMount, BrokenComponentWillReceiveProps and a few more.

It's likely the same issue as #8604 but worth noting it doesn't only happen for render method.

@bvaughn
Copy link
Contributor Author

bvaughn commented Dec 21, 2016

There's a kind of obvious design flaw with this single-stack-with-shared-cursors approach that I managed to overlook until a half hour ago.

Action      Cursor A    Cursor b    Stack
------      --------    --------    -----
(init)      null        null        []
push(a,1)   1           null        [1]
push(b,2)   1           2           [1,2]
pop(b)      1           1           [1]     <- b is invalid
pop(a)      null        1           []      <- b is still invalid

Irrespective of mismatched pushes/pops, this breaks a lot of things. I'm reevaluating the best way to proceed at the moment. I think Sebastian was a big fan of a single stack but I'm not currently able to visualize a way for it to work at the moment.

@sebmarkbage
Copy link
Collaborator

The thing that is suppose to go on the stack is the previous value of the cursor. Not the value you push.

Action      Cursor A    Cursor b    Stack
------      --------    --------    -----
(init)      null        null        []
push(a,1)   1           null        [null]
push(b,2)   1           2           [null,null]
pop(b)      1           null        [null]     <- b is valid
pop(a)      null        null        []      <- a and b is valid

@bvaughn
Copy link
Contributor Author

bvaughn commented Dec 21, 2016

Ahhh, that makes sense. Thanks @sebmarkbage.


index++;

valueStack[index] = value;
Copy link
Collaborator

Choose a reason for hiding this comment

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

index++;
valueStack[index] = cursor.current;
cursor.current = value;

@sebmarkbage
Copy link
Collaborator

The mental model is:

var current = null;
function work(foo) {
  // Begin Work
  var prevOnTheStack = current;
  current = getContext(foo);
  try {
    processChildren(foo);
    // Complete Work
    completeSomething();
  } finally {
    // Complete Work and Unwind Context 
    current = prevOnTheStack;
  }
}

@bvaughn
Copy link
Contributor Author

bvaughn commented Dec 21, 2016

As of my most recent push, all tests that were previously passing (in master) are now once again passing on this branch.

Brian Vaughn added 5 commits December 21, 2016 15:11
…ontext

ReactFiberStack is the new underlying stack used by ReactFiberContext and ReactFiberHostContext. The goal is to simplify the 2 context managers and to add more errors/dev-warnings when we pop unexpected.
This changeset currently causes a lot of tests to fail (as we are currently popping too many times and/or in the wrong order). I'm pushing this branch now to share with Sebastian as he is working on a related cleanup pass at beginWork.
@sebmarkbage
Copy link
Collaborator

CI is failing for some reason. Try running grunt build locally?

@bvaughn
Copy link
Contributor Author

bvaughn commented Dec 21, 2016

Yeah, I noticed that. Running locally I see a lot of garbage in the console like

Error message "Missing owner for string ref %s" cannot be found. The current React version and the error map are probably out of sync. Please run gulp react:extract-errors before building React.

...but then it ultimately finishes with

Done, without errors.

@bvaughn
Copy link
Contributor Author

bvaughn commented Dec 21, 2016

I am able to reproduce the error locally by manually running the gulp task ./node_modules/.bin/gulp react:extract-errors. After looking at the node failing, I've determined that it's caused by my use of an exact object type for StackCursor:

// This works
export type StackCursor<T> = { current: T };

// This causes the error even though it's a valid Flow type
export type StackCursor<T> = {| current: T |};

For now I will relax that type so that Gulp works. I assume I should also be committing the updated scripts/error-codes/codes.json file but please let me know if that is not the case.

@bvaughn
Copy link
Contributor Author

bvaughn commented Dec 21, 2016

CI gets further now but ultimately fails at a different stage- ./scripts/circleci/test_coverage.sh. Running that task locally fails for me against both my branch and master.

This was causing the 'react:extract-errors' Gulp taks to fail.
@bvaughn
Copy link
Contributor Author

bvaughn commented Dec 22, 2016

Had an informative chat with Ben about this. The failing Grunt task was caused by the fact that I've added new invariants/warnings in this PR and updated my local error codes JSON. This in turn caused our ReactPropTypesProduction test to fail but in a non-obvious way. Rather than showing the error message from ReactPropTypesProduction, another error- TypeError: process.cwd is not a function- was shown. This is because we're mocking out the process object that Jest is trying to use. Commenting out the call to process.cwd in Jest revealed the underlying error code message.

This further confused me for a while because Grunt failed for me locally on master as well. That turned out to be caused by a caching issue which was fixed with PR #8622.

In general we have a test flakiness problem that will bite us when we next update the error codes (before the next release). That flakiness issue will need to be addressed in a subsequent PR. I don't mind taking that task on once these context PRs have been wrapped up. 😄

For now, CI passes again for this branch. Yay.

const contextStack : Array<Object> = [];
const didPerformWorkStack : Array<boolean> = [];
let contextStackCursor : StackCursor<?Object> = createCursor((null: ?Object));
let didPerformWorkStackCursor : StackCursor<?boolean> = createCursor((null: ?boolean));
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you set the default here to (false : boolean) you can get rid of the nullable type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about doing that, but then I'd lose the ability to differentiate between "empty" and default-state. Which isn't really important in this case but... slightly more compelling is that it would also mean the cursor would have asymmetric behavior- false by default, and then null after the stack gets reset (since we don't track and fall back to the default value). It seemed worth doing the cast to avoid that, but I don't feel strongly about it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It won't have asymmetric behavior if you fix the reset below. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are correct of course! 😁 That turnery was a holdover from when I was pushing/popping incorrectly. D'oh.

@@ -62,7 +66,7 @@ exports.getMaskedContext = function(workInProgress : Fiber) {
};

exports.hasContextChanged = function() : boolean {
return index > -1 && didPerformWorkStack[index];
return Boolean(didPerformWorkStackCursor.current);
Copy link
Collaborator

Choose a reason for hiding this comment

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

By avoiding the nullable type above, you don't need coerce this. You can just return didPerformWorkStackCursor.current.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok. :) I'll go ahead and make that change then. I don't feel strongly about it.

};

exports.reset = function<T>(
cursor : StackCursor<T>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Passing a cursor here seems like an odd API since it's invalid to only reset one cursor. You have to reset all at once. It also causes the unsound assignment below.

Currently this only works because index is already at -1 at the second call to reset so the loop is unnecessary.

I'd recommend just dropping the cursor argument and reset them separately if you think it's necessary to reset them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. It's a little weird either way I guess. I'll drop the cursor from reset and just manually clear the current pointer :)


exports.push = function<T>(
cursor : StackCursor<T>,
value : any,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This can be value : T for a bit stronger guarantees that ensures that we don't push unexpected values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch.


cursor.current = index > 0
? valueStack[index]
: (null : any);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is unsound since it doesn't reset to the defaultValue. However, I don't think this should ever be possible since the first value that goes into the value stack is the defaultValue. So you shouldn't be able to pop further than that.

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 just be:

cursor.current = valueStack[index];

Otherwise you're never reading index 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion.

};

exports.resetContext = function() : void {
index = -1;
reset();
Copy link
Collaborator

@sebmarkbage sebmarkbage Dec 22, 2016

Choose a reason for hiding this comment

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

We now have two calls to reset in a row because the only thing that calls this is:

https://github.com/facebook/react/blob/master/src/renderers/shared/fiber/ReactFiberScheduler.js#L157-L160

So we could just call reset(); at the top of that function instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems slightly weird to bypass the contexts and talk directly to the stack they use, no?

But sure. I can change that.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

Awesome! Let's go!

@bvaughn
Copy link
Contributor Author

bvaughn commented Dec 22, 2016

Thanks for the review Sebastian!!

@bvaughn bvaughn merged commit f1e193b into facebook:master Dec 22, 2016
@bvaughn bvaughn deleted the shared-context-stack branch December 22, 2016 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants