Skip to content

Add support for re-entrant SSR stacks#13181

Merged
gaearon merged 2 commits intofacebook:masterfrom
gaearon:ssr-more
Jul 11, 2018
Merged

Add support for re-entrant SSR stacks#13181
gaearon merged 2 commits intofacebook:masterfrom
gaearon:ssr-more

Conversation

@gaearon
Copy link
Copy Markdown
Collaborator

@gaearon gaearon commented Jul 9, 2018

Fixes #10188.

This is an alternative for #13158.

This embraces that we keep a stack of stacks of stacks, and explains why.

@gaearon gaearon requested a review from sebmarkbage July 9, 2018 13:55
@gaearon gaearon force-pushed the ssr-more branch 2 times, most recently from 64aa1a2 to aa039da Compare July 9, 2018 14:06
@pull-bot
Copy link
Copy Markdown

pull-bot commented Jul 9, 2018

Details of bundled changes.

Comparing: e79366d...503ddd8

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom-server.browser.development.js +1.6% +2.0% 99.26 KB 100.82 KB 26.19 KB 26.72 KB UMD_DEV
react-dom-server.browser.development.js +1.6% +2.1% 95.39 KB 96.95 KB 25.24 KB 25.77 KB NODE_DEV
react-dom-server.node.development.js +1.6% +2.0% 97.31 KB 98.87 KB 25.78 KB 26.31 KB NODE_DEV
ReactDOMServer-dev.js +1.7% +2.1% 96.44 KB 98.04 KB 24.79 KB 25.31 KB FB_WWW_DEV

Generated by 🚫 dangerJS

Copy link
Copy Markdown
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 okay to me. Left a minor question or two.

' in div (at **)\n' +
' in App (at **)',
]);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice tests 👍

getStackAddendum = function(): null | string {
if (currentDebugStack === null) {
const currentDebugFrame = currentDebugFrames[currentDebugFrames.length - 1];
if (currentDebugFrame === null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just curious, but when would this happen? I don't see anywhere we push null to this stack.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Oops. Shouldn't happen, but I'll change it to test for array length instead. It's for exceptional case where we accidentally call getStackAddendum too late.

function pushFrame(stack: Array<Frame>, frame: Frame): void {
if (__DEV__) {
const returnFrame =
stack.length > 0 ? ((stack[stack.length - 1]: any): FrameDev) : null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need this explicit DEV-only pointer? Isn't it always the frame at index - 1?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, but then we need to get a reference to the frame. I could do it — it's just very confusing to think about what currentDebugStacks would mean.

I guess my solution is also confusing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure I follow. We only use the debugReturn attribute in getStackAddendum() where we just start at the top frame in currentDebugFrames and walk back. Seems like we could just do that with a for loop?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No, currentDebugFrames is not the same thing as frames :-(

That's why this is so confusing. I rewrote and pushed a more explicit solution which doesn't try to hide that we have stacks of stacks of stacks. I hope that will clarify.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Huh. The change you pushed to getStackAddendum in 5e3f51e looks like what I was suggesting 😅 but it looks like some commits are missing now so I can't step backwards....

popCurrentDebugFrame();
}
} else {
// Be careful! Make sure this matches the DEV path above.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good comment 👍

@gaearon gaearon force-pushed the ssr-more branch 2 times, most recently from 5e3f51e to 0f8577d Compare July 11, 2018 17:48
@gaearon
Copy link
Copy Markdown
Collaborator Author

gaearon commented Jul 11, 2018

I rewrote the PR to be close to the original code, so this is just fixing the bug now. I added a bunch of comments to explain why it works this way and what it's doing exactly.

@gaearon gaearon force-pushed the ssr-more branch 2 times, most recently from 2eb4060 to 39568bb Compare July 11, 2018 17:57
if (currentDebugStacks.length === 1) {
// We are entering a server renderer.
// Remember the previous (e.g. client) global stack implementation.
prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Huh... does this work? Do we never push more than once before popping?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm... or is this just assuming that all pushes before the pop will be for the same renderer and will have the same previous stack impl? I find this more confusing initially 😆

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's assuming that once you're inside ReactDOMServer call, you're only going to make other ReactDOMServer calls (at most), but not, e.g., ReactDOMClient or ReactART calls. Because it's synchronous. I think it's a safe assumption in 99% cases.

All ReactDOMServer calls share the same implementation. Happy to talk through it via VC — I'm worried this is getting difficult to explain like this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Gotcha. Yeah, that's what I was thinking from reading it. Was just unsure if that would ever cause problems.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

In the worst case you'll end up with no stacks. Which is exactly what's happening today with any nesting. :-)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fair enough!

@NE-SmallTown
Copy link
Copy Markdown
Contributor

NE-SmallTown commented Aug 13, 2018

@gaearon @bvaughn
Could anyone please give me some details about the 're-entrant' in React? I searched it about 2 years ago , 1 year ago, half a year ago through this repo, wikipedia, stackoverflow, FP programing, your youtube talks, twitter......, but I find I still can't understand it very well. Thanks!

@bvaughn
Copy link
Copy Markdown
Contributor

bvaughn commented Aug 13, 2018

Reentrancy is a general computing term:

In computing, a computer program or subroutine is called reentrant if it can be interrupted in the middle of its execution and then safely be called again before its previous invocations complete execution.

In the context of this PR, a "reentrant" call can be seen in the tests Dan added- where e.g. ReactDOMServer.renderToString() is called to render some components– and while it's rendering, one of the components being rendered also calls ReactDOMServer.renderToString() to render another set of components. The outer render stops while the inner render works, then picks back up and continues afterward.

@NE-SmallTown
Copy link
Copy Markdown
Contributor

@bvaughn Thank you Brian!

But I had read that many times and seen questions on stackoverflow are talking about that. Almost all of them think it's about multithreading because the keyword 'interrupted' and 'safely', but in javascript it's nonexistent(except worker). The only thing I can imagine is that generator could be 'interrupted' and 'be called again', but React doesn't use generator, it use its own stack implementation(Fiber), but I don't know how to associate it with these keywords.

BTW, in javascript, we should call it 'recursion' rather than 'Reentrancy'? (Searched on google about the different in javascript but don't get answer)

@bvaughn
Copy link
Copy Markdown
Contributor

bvaughn commented Aug 14, 2018

BTW, in javascript, we should call it 'recursion' rather than 'Reentrancy'?

Recursive functions must also be reentrant, but not all reentrancy is recursive.

A function is recursive if it calls itself. For example:

function foo(shouldRecurse) {
  console.log('foo start');
  if (shouldRecurse) {
    foo(false);
  }
  console.log('foo stop');
}
foo(true);

This is not recursion, but it is an example of reentrancy:

function foo(fn) {
  console.log('foo start');
  fn();
  console.log('foo stop');
}
function bar() {
  console.log('bar');
  foo(baz);
}
function baz() {
  console.log('baz');
}
foo(bar);

@NE-SmallTown
Copy link
Copy Markdown
Contributor

NE-SmallTown commented Aug 15, 2018

@bvaughn Thank you Brian, love it ! Before I think the second example is also a recursive function because the foo function call it self, even though it's indirect. But seems I'm wrong, we must call itself in the foo function body, can't be indirect

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.

5 participants