Skip to content

Conversation

@acdlite
Copy link
Collaborator

@acdlite acdlite commented May 18, 2020

Combines all the sub-phases of the render phase — begin, complete, and unwind — into a single generator function.

It works by pushing/popping generator objects onto a stack. This replaces the stack in ReactFiberStack.

The cursor objects used by ReactFiberStack are replaced by module level variables that are referenced via closure.

For our release builds, what we would do is compile the generator functions to normal, static functions that push/pop to the stack directly, skipping the overhead of the generator objects.

The first one I've ported is Context providers.

Combines all the sub-phases of the render phase -- begin, complete,
and unwind -- into a single generator function.

It works by pushing/popping iterator functions onto a stack. This
replaces the stack in ReactFiberStack.

The cursor objects used by ReactFiberStack are replaced by module
level variables that are referenced via closure.

For our release builds, what we would do is compile the generator
functions to normal, static functions that push/pop to the stack
directly, skipping the overhead of the iterator objects.

The first one I've ported is Context providers.
@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels May 18, 2020
@codesandbox-ci
Copy link

codesandbox-ci bot commented May 18, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit be85aea:

Sandbox Source
compassionate-einstein-svggw Configuration

@acdlite acdlite marked this pull request as draft May 18, 2020 00:25
The value is only read by the provider component that overrides it,
so this can be expressed as a local variable.
@acdlite
Copy link
Collaborator Author

acdlite commented May 18, 2020

This is a rough idea of what I had in mind for the generator output. This is a first draft. I'll add more/better examples later.

If anyone is interested in working on the compiler part of this, I wouldn't worry too much about getting it running in the actual codebase. There's probably stuff in the runtime part that I haven't fully fleshed out. But if you can get the basics of storing the continuations and variables in the stack, and then reading them back out again, that would be enormously helpful. That's 90% of the work.

Input:

let threadLocalVariable = 'initial value';

export function* renderSomeComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): RenderStateMachine {
  const prevValue = threadLocalVariable;
  threadLocalVariable = 'override subtree with new value';
  try {
    const newChildren = newProps.children;
    reconcileChildren(current, workInProgress, newChildren, renderLanes);
    yield workInProgress.child;
  } finally {
    threadLocalVariable = prevValue;
  }
}

Output:

let stack = [];
let threadLocalVariable = 'initial value';

let offset = 0;

// The arguments are passed in from the work loop, so we don't need to store
// them on the stack
function renderSomeComponent_0(current, workInProgress, renderLanes) {
  const prevValue = threadLocalVariable;
  threadLocalVariable = 'override subtree with new value';

  const newChildren = newProps.children;
  reconcileChildren(current, workInProgress, newChildren, renderLanes);
  
  const yieldedValue = workInProgress.child;

  // First slot is the continuation to use if the children render successfully
  // Second slot is if something throws
  stack.push(renderSomeComponent_1, renderSomeComponent_2);
  offset += 2;

  // Remaining slots are used for local variables
  stack.push(prevValue);
  offset += 1;

  return yieldedValue;
}

// "success" continuation
function renderSomeComponent_1(current, workInProgress, renderLanes) {
  // Load local variables
  const prevValue = stack[offset + 2];

  threadLocalVariable = prevValue;

  // Pop local variables
  array.pop();
  return null;
}

// "error" continuation
// Should probably pick a different example since in this case the two branches are the same :D
function renderSomeComponent_2(current, workInProgress, renderLanes) {
  // Load local variables
  const prevValue = stack[offset + 2];

  threadLocalVariable = prevValue;

  // Pop local variables
  array.pop();
  return null;
}

And here's a sketch of the runtime we'd use. Replaces the equivalent functions in ReactFiberGeneratorComponent. These are called by the work loop.

function beginWork(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) {
  let next;
  switch (tag) {
    case SomeComponent:
      next = renderSomeComponent_0(current, workInProgress, renderLanes)
      break;
    default:
      throw Error('Hasn\'t been ported to generators yet: ' + tag) ;
  }

  return next;
}

function completeWork(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) {
  const continuation = stack[offset];
  const next = continuation(current, workInProgress, renderLanes);
  if (next === null) {
    // Pop continuations off stack
    stack.pop();
    stack.pop();
    offset -= 2;
  }
  return next;
}

function unwindWork(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) {
  const continuation = stack[offset + 1];
  const next = continuation(current, workInProgress, renderLanes);
  if (next === null) {
    // Pop continuations off stack
    stack.pop();
    stack.pop();
    offset -= 2;
  }
  return next;
}

@chirgjn
Copy link

chirgjn commented May 18, 2020

👋 @acdlite
I'd like to pick this up.
What would be the best way to build upon this? Should I fork acdlite/react ?

@acdlite
Copy link
Collaborator Author

acdlite commented May 18, 2020

@chirgjn Yeah you can fork the PR branch (generator-component)

@awto
Copy link

awto commented May 19, 2020

This is very simple and fast to implement with EffectfulJS, much simpler than rewriting Regenerator. Let me know if interested. But I also have a few comments if you choose anything else.

There is no way to handle variables captured from generators and finally continuations (for-of also needs finally continuation). If you don't use them - fine.

Using functions as state callbacks is much slower than switch-case. Effectful JS supports both, so you'll be able to play with, but I've checked a lot of implementations switch-case is always much faster. Esp if the function aren't in module's top level, so always recreated.

Effectful JS can optionally move them to top level if needed, with handling closure captured variables properly, but switch-case is anyway faster.

@markerikson
Copy link
Contributor

Out of curiosity, what's the intended goal / benefit of this approach?

@tylerhou
Copy link

I wrote https://github.com/tylerhou/fiber (HN: https://news.ycombinator.com/item?id=29628772) which may be of interest.

@sebmarkbage sebmarkbage deleted the branch facebook:master October 20, 2022 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants