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] Simplify coroutines by making yields stateless #8840

Merged
merged 5 commits into from
Jan 26, 2017

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Jan 21, 2017

Builds on top of #8414 and #8413

Coroutines was kind of broken because it tried to do reparenting and enabling state preservation to be passed along the coroutine. However, since we couldn't determine which Fiber was "current" on a reified yield this was kind of broken.

This removes the "continuation" part of yields so they're basically just return values. It is still possible to do continuations by just passing simple functions or classes as part of the return value but they're not stateful.

This means that we won't have reparenting, but I actually don't think we need it. There's another way to structure this by doing all the state in the first phase and then yielding a stateless representation of the result. This stateless representation of the tree can then be rendered in different (or even multiple) locations.

Because we no longer have a stateful continuation, you may have noticed that this really no longer represent the "coroutine" concept. I will rename it in a follow up PR. I'm thinking createWait and createReturn maybe?

@sebmarkbage sebmarkbage changed the title Simplify coroutines by making yields stateless [Fiber] Simplify coroutines by making yields stateless Jan 21, 2017
@gaearon
Copy link
Collaborator

gaearon commented Jan 21, 2017

I don't understand what "stateful" meant in this context even before the change.

Can you write a small example that would've been possible with the old implementation but is not possible now?

@sebmarkbage
Copy link
Collaborator Author

class Continuation extends React.Component {
  state = { renders: 0 };
  componentWillRecieveProps() {
    this.setState({ renders: this.state.renders + 1 });
  }
  render() {
    return <div>{this.context.parentName}{renders}</div>;
  }
}

class Child extends React.Component {
  getChildContext() {
    return { parentName: 'Child '};
  }
  render() {
    return ReactCoroutine.createYield(Continuation);
  }
}

function HandleYields(props, yields) {
  return yields.map(ContinuationComponent => props.wrap ? 
    <div><ContinuationComponent /></div> : <ContinuationComponent />);
}

class Parent extends React.Component {
  getChildContext() {
    return { parentName: 'Parent' };
  }
  render() {
    return ReactCoroutine.createCoroutine(
      this.props.children,
      HandleYields,
      this.props
    );
  }
}

render(<Parent><Child /><Parent>);

render(<Parent wrap><Child /><Parent>);

In this scenario, it used to be that the "ContinuationComponent" would get its state from "Child".

Therefore, it would keep its state when wrap is toggled. I.e. when it is reparented. That is no longer the case, it will get unmounted and remounted with this PR.

It used to be that the context passed to the "ContinuationComponent" should be "Child" because the Fiber hangs off of that tree (although that was already broken). With this PR the context is "Parent" because the Fiber now materializes as a child of HandleYields whose parent is "Parent".

@gaearon
Copy link
Collaborator

gaearon commented Jan 25, 2017

I played with it a little, and this example seems to break it:

      function increment(prevState) {
        return {
          value: prevState.value + 1,
        };
      }

      class Counter extends React.Component {
        constructor(props) {
          super(props);
          this.state = {value: 0};
          this.tick = this.tick.bind(this);
        }
        componentDidMount() {
          this.interval = setInterval(this.tick, 1000);
        }
        tick() {
          this.setState(increment);
        };
        componentWillUnmount() {
          clearInterval(this.interval);
        }
        render() {
          return createYield(<h1>{this.state.value}</h1>)
        }
      }

      function App() {
        return (
          <div>
            {createCoroutine(
              [<Counter />],
              (props, yields) => yields.map(y => y)
            )}
            <h1 />
          </div>
        )
      }

      ReactDOMFiber.render(
        <App />,
        document.getElementById('container')
      );

In particular the first mount renders, but the updates fail with "A coroutine cannot have host component children.".

Is this a bug?

Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

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

Updates seem still broken.

@gaearon
Copy link
Collaborator

gaearon commented Jan 25, 2017

I tried to write a failing test for this but it just hangs:

  fit('should handle deep updates in coroutine', () => {
    let instances = {};

    class Counter extends React.Component {
      state = {value: 5};
      render() {
        instances[this.props.id] = this;
        return ReactCoroutine.createYield(this.state.value);
      }
    }

    function App(props) {
      return ReactCoroutine.createCoroutine(
        [
          <Counter id="a" />,
          <Counter id="b" />,
          <Counter id="c" />,
        ],
        (props, yields) => yields.map(y => <span prop={y * 100} />),
        {}
      );
    }

    ReactNoop.render(<App />);
    ReactNoop.flush();
    expect(ReactNoop.getChildren()).toEqual([
      span(500),
      span(500),
      span(500),
    ]);

    instances.a.setState({value: 1});
    instances.b.setState({value: 2});
    ReactNoop.flush();
    expect(ReactNoop.getChildren()).toEqual([
      span(100),
      span(200),
      span(500),
    ]);
  });

const created = createFiberFromYield(yieldNode, priority);
created.type = reifiedYield;
created.type = yieldNode.value;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this break hidden classes if it's a number?

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 but only in V8. Others use NaN tagging. It's likely already unlike that you'd use it this way. We can warn if it becomes an issue.

It is slightly more useful this way because when we want to find host nodes
we typically want to do so in the second phase. That's the real tree where
as the first phase is more of a virtual part of the tree.
Coroutines was kind of broken because it tried to do reparenting and
enabling state preservation to be passed along the coroutine. However,
since we couldn't determine which Fiber was "current" on a reified yield
this was kind of broken.

This removes the "continuation" part of yields so they're basically just
return values. It is still possible to do continuations by just passing
simple functions or classes as part of the return value but they're not
stateful.

This means that we won't have reparenting, but I actually don't think we
need it. There's another way to structure this by doing all the state in
the first phase and then yielding a stateless representation of the result.
This stateless representation of the tree can then be rendered in different
(or even multiple) locations.

Because we no longer have a stateful continuation, you may have noticed
that this really no longer represent the "coroutine" concept. I will
rename it in a follow up commit.
There are a few different issues:

* Updates result in unnecessary duplicate placements because it can't find the current fiber for continuations.
* When run together, coroutine update and unmounting tests appear to lock down in an infinite loop. They don't freeze in isolation.

I don't have a solution for this but just leaving it for future fixes.
@sebmarkbage
Copy link
Collaborator Author

So I fixed the "A coroutine cannot have host component children." issue. That was a bug introduced by this PR. a44f19b

The other issues has to do with the scheduler forgetting to look in the stateNode. There's a few TODO around but unrelated to this.

@sebmarkbage
Copy link
Collaborator Author

Actually, that's not right. The fix is not right and the bug is not the remaining TODO but that memoization bail out is wrong.

Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

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

I don't fully understand this yet so I may have missed something. I'll be happy to review more when we fix the scheduler stuff and I can throw more corner cases at it.


transferDeletions(workInProgress);
}

memoizeProps(workInProgress, nextCoroutine);
// This doesn't take arbitrary time so we could synchronously just begin
// eagerly do the work of workInProgress.child as an optimization.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: probably need to change workInProgress.child to workInProgress.stateNode in the comment (or drop it)

@@ -66,31 +65,43 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
popHostContainer,
} = hostContext;

function markChildAsProgressed(current, workInProgress, priorityLevel) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe move to child fiber since it's duplicated between phases?

@@ -474,7 +462,10 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
}

case REACT_YIELD_TYPE: {
if (newChild.key === key) {
// Yields doesn't have keys. If the previous node is implicitly keyed
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: don't have keys

@gaearon
Copy link
Collaborator

gaearon commented Jan 26, 2017

This test now breaks with the last commit:

  fit('whatever', () => {
    let instances = {};

    class Counter extends React.Component {
      state = {value: 5};
      render() {
        instances[this.props.id] = this;
        return ReactCoroutine.createYield(this.state.value);
      }
    }

    function App(props) {
      return ReactCoroutine.createCoroutine(
        [
          <Counter id="a" />,
          <Counter id="b" />,
          <Counter id="c" />,
        ],
        (props, yields) => yields.map(y => <span prop={y * 100} />),
        {}
      );
    }

    ReactNoop.render(<App />);
    ReactNoop.flush();
    expect(ReactNoop.getChildren()).toEqual([
      span(500),
      span(500),
      span(500),
    ]);
  });

Note I'm not actually testing updates, this is a subset of the previous (hanging) test. This part so far used to pass, but now I only see a single span.

When visiting the yields, the root is the stateNode of the coroutine. If
its return value is wrong we'll end up at the alternate of the
workInProgress which will start scanning its children which is wrong.
Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

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

@sebmarkbage
Copy link
Collaborator Author

Yea, but not with this: eb0bae3

@sebmarkbage sebmarkbage dismissed gaearon’s stale review January 26, 2017 01:40

Passes @gaearon's stress testing. New unit test added. Tested in stand alone demo using above ticks.

Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

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

I think this works.
(You'll want to update Fiber tests before merging)

This should bail to stateNode instead.
@sebmarkbage sebmarkbage merged commit b262657 into facebook:master Jan 26, 2017
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