Skip to content

Commit

Permalink
[CS] Persistent Updates (facebook#11260)
Browse files Browse the repository at this point in the history
* Update build size

* [CS] Clone container instead of new root concept

The extra "root" concept is kind of unnecessary. Instead of having a
mutable container even in the persistent mode, I'll instead make the
container be immutable too and be cloned. Then the "commit" just becomes
swapping the previous container for the new one.

* Change the signature or persistence again

We may need to clone without any updates, e.g. when the children are changed.

Passing in the previous node is not enough to recycle since it won't have the
up-to-date props and children. It's really only useful to for allocation pooling.

* Implement persistent updates

This forks the update path for host fibers. For mutation mode we mark
them as having an effect. For persistence mode, we clone the stateNode with
new props/children.

Next I'll do HostRoot and HostPortal.

* Refine protocol into a complete and commit phase

finalizeContainerChildren will get called at the complete phase.
replaceContainer will get called at commit.

Also, drop the keepChildren flag. We'll never keep children as we'll never
update a container if none of the children has changed.

* Implement persistent updates of roots and portals

These are both "containers". Normally we rely on placement/deletion effects
to deal with insertions into the containers. In the persistent mode we need
to clone the container and append all the changed children to it.

I needed somewhere to store these new containers before they're committed
so I added another field.

* Commit persistent work at the end by swapping out the container

* Unify cloneOrRecycle

Originally I tried to make the recyclable instance nullable but Flow didn't
like that and it's kind of sketchy since the instance type might not be
nullable.

However, the real difference which one we call is depending on whether they
are equal. We can just offload that to the renderer. Most of them won't
need to know about this at all since they'll always clone or just create
new.

The ones that do know now have to be careful to compare them so they don't
reuse an existing instance but that's probably fine to simplify the
implementation and API.

* Add persistent noop renderer for testing

* Add basic persistent tree test

* Test bail out

This adds a test for bailouts. This revealed a subtle bug. We don't set the
return pointer when stepping into newly created fibers because there
can only be one. However, since I'm reusing this mechanism for persistent
updates, I'll need to set the return pointer because a bailed out tree
won't have the right return pointer.

* Test persistent text nodes

Found another bug.

* Add persistent portal test

This creates a bit of an unfortunate feature testing in the unmount
branch.

That's because we want to trigger nested host deletions in portals in the
mutation mode.

* Don't consider container when determining portal identity

Basically, we can't use the container to determine if we should keep
identity and update an existing portal instead of recreate it. Because
for persistent containers, there is no permanent identity.

This makes it kind of strange to even use portals in this mode. It's
probably more ideal to have another concept that has permanent identity
rather than trying to swap out containers.

* Clear portals when the portal is deleted

When a portal gets deleted we need to create a new empty container and
replace the current one with the empty one.

* Add renderer mode flags for dead code elimination

* Simplify ReactNoop fix

* Add new type to the host config for persistent configs

We need container to stay as the persistent identity of the root atom.
So that we can refer to portals over time.

Instead, I'll introduce a new type just to temporarily hold the children
of a container until they're ready to be committed into the permanent
container. Essentially, this is just a fancy array that is not an array
so that the host can choose data structure/allocation for it.

* Implement new hooks

Now containers are singletons and instead their children swap. That way
portals can use the container as part of their identity again.

* Update build size and error codes

* Address comment

* Move new files to new location
  • Loading branch information
sebmarkbage committed Oct 19, 2017
1 parent 4af2a24 commit b52a562
Show file tree
Hide file tree
Showing 18 changed files with 757 additions and 135 deletions.
24 changes: 24 additions & 0 deletions packages/react-cs-renderer/src/ReactNativeCSFeatureFlags.js
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule ReactNativeCSFeatureFlags
* @flow
*/

'use strict';

import type {FeatureFlags} from 'ReactFeatureFlags';

var ReactNativeCSFeatureFlags: FeatureFlags = {
enableAsyncSubtreeAPI: true,
enableAsyncSchedulingByDefaultInReactDOM: false,
// React Native CS uses persistent reconciler.
enableMutatingReconciler: false,
enableNoopReconciler: false,
enablePersistentReconciler: true,
};

module.exports = ReactNativeCSFeatureFlags;
39 changes: 21 additions & 18 deletions packages/react-cs-renderer/src/ReactNativeCSFiberEntry.js
Expand Up @@ -154,34 +154,37 @@ const ReactNativeCSFiberRenderer = ReactFiberReconciler({
persistence: {
cloneInstance(
instance: Instance,
updatePayload: Object,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
keepChildren: boolean,
): Instance {
return 0;
},
tryToReuseInstance(
instance: Instance,
updatePayload: Object,
updatePayload: null | Object,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
keepChildren: boolean,
recyclableInstance: null | Instance,
): Instance {
return 0;
},

createRootInstance(
rootContainerInstance: Container,
hostContext: {},
): Instance {
return 123;
createContainerChildSet(
container: Container,
): Array<Instance | TextInstance> {
return [];
},
commitRootInstance(rootInstance: Instance): void {},

appendChildToContainerChildSet(
childSet: Array<Instance | TextInstance>,
child: Instance | TextInstance,
): void {},

finalizeContainerChildren(
container: Container,
newChildren: Array<Instance | TextInstance>,
): void {},

replaceContainerChildren(
container: Container,
newChildren: Array<Instance | TextInstance>,
): void {},
},
});

Expand Down
Expand Up @@ -12,6 +12,8 @@
var React;
var ReactNativeCS;

jest.mock('ReactFeatureFlags', () => require('ReactNativeCSFeatureFlags'));

describe('ReactNativeCS', () => {
beforeEach(() => {
jest.resetModules();
Expand Down
82 changes: 81 additions & 1 deletion packages/react-noop-renderer/src/ReactNoopEntry.js
Expand Up @@ -20,6 +20,7 @@
import type {Fiber} from 'ReactFiber';
import type {UpdateQueue} from 'ReactFiberUpdateQueue';

var ReactFeatureFlags = require('ReactFeatureFlags');
var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
var ReactFiberReconciler = require('ReactFiberReconciler');
var ReactInstanceMap = require('ReactInstanceMap');
Expand Down Expand Up @@ -85,7 +86,7 @@ function removeChild(

let elapsedTimeInMs = 0;

var NoopRenderer = ReactFiberReconciler({
var SharedHostConfig = {
getRootHostContext() {
if (failInBeginPhase) {
throw new Error('Error in host config.');
Expand Down Expand Up @@ -176,7 +177,10 @@ var NoopRenderer = ReactFiberReconciler({
now(): number {
return elapsedTimeInMs;
},
};

var NoopRenderer = ReactFiberReconciler({
...SharedHostConfig,
mutation: {
commitMount(instance: Instance, type: string, newProps: Props): void {
// Noop
Expand Down Expand Up @@ -211,8 +215,64 @@ var NoopRenderer = ReactFiberReconciler({
},
});

var PersistentNoopRenderer = ReactFeatureFlags.enablePersistentReconciler
? ReactFiberReconciler({
...SharedHostConfig,
persistence: {
cloneInstance(
instance: Instance,
updatePayload: null | Object,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
keepChildren: boolean,
recyclableInstance: null | Instance,
): Instance {
const clone = {
id: instance.id,
type: type,
children: keepChildren ? instance.children : [],
prop: newProps.prop,
};
Object.defineProperty(clone, 'id', {
value: clone.id,
enumerable: false,
});
return clone;
},

createContainerChildSet(
container: Container,
): Array<Instance | TextInstance> {
return [];
},

appendChildToContainerChildSet(
childSet: Array<Instance | TextInstance>,
child: Instance | TextInstance,
): void {
childSet.push(child);
},

finalizeContainerChildren(
container: Container,
newChildren: Array<Instance | TextInstance>,
): void {},

replaceContainerChildren(
container: Container,
newChildren: Array<Instance | TextInstance>,
): void {
container.children = newChildren;
},
},
})
: null;

var rootContainers = new Map();
var roots = new Map();
var persistentRoots = new Map();
var DEFAULT_ROOT_ID = '<default>';

let yieldedValues = null;
Expand Down Expand Up @@ -275,6 +335,26 @@ var ReactNoop = {
NoopRenderer.updateContainer(element, root, null, callback);
},

renderToPersistentRootWithID(
element: React$Element<any>,
rootID: string,
callback: ?Function,
) {
if (PersistentNoopRenderer === null) {
throw new Error(
'Enable ReactFeatureFlags.enablePersistentReconciler to use it in tests.',
);
}
let root = persistentRoots.get(rootID);
if (!root) {
const container = {rootID: rootID, children: []};
rootContainers.set(rootID, container);
root = PersistentNoopRenderer.createContainer(container, false);
persistentRoots.set(rootID, root);
}
PersistentNoopRenderer.updateContainer(element, root, null, callback);
},

unmountRootWithID(rootID: string) {
const root = roots.get(rootID);
if (root) {
Expand Down
1 change: 1 addition & 0 deletions packages/react-reconciler/src/ReactFiber.js
Expand Up @@ -445,6 +445,7 @@ exports.createFiberFromPortal = function(
fiber.expirationTime = expirationTime;
fiber.stateNode = {
containerInfo: portal.containerInfo,
pendingChildren: null, // Used by persistent updates

This comment has been minimized.

Copy link
@VariableVasasMT

VariableVasasMT Apr 22, 2019

Owner

@sebmarkbage I was looking at this code for this issue, why didnt we pass pendingChildren: portal.children

implementation: portal.implementation,
};
return fiber;
Expand Down
4 changes: 2 additions & 2 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Expand Up @@ -67,8 +67,8 @@ if (__DEV__) {
var warnedAboutStatelessRefs = {};
}

module.exports = function<T, P, I, TI, PI, C, CX, PL>(
config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
hostContext: HostContext<C, CX>,
hydrationContext: HydrationContext<C, CX>,
scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void,
Expand Down

0 comments on commit b52a562

Please sign in to comment.