Reorganize the top level around a FiberRoot
We need a canonical stateful root for each. I don't really want to overload the HostContainer for this purpose since it makes the fiber code more specialized. Instead I create a root which represents an actual stateful root. When these get scheduled they get chained together in a linked list. However, we don't hold onto anything that doesn't have scheduled work. This will help us release everything automatically in the GC, as long as there are no subscriptions nor scheduled work.
Sebastian Markbage committed
Jun 15, 2016 at 16:06 UTC
7d028fd8cc047f4ae3739f9d380bf59640ea8129
4 files changed
+142
-65
src/renderers/shared/fiber/ReactFiber.js
+6
-3
@@ -140,6 +140,11 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
140
// Stop using the alternates of parents once we have a parent stack.
141
// $FlowFixMe: This downcast is not safe. It is intentionally an error.
142
alt.parent = fiber.parent.alternate;
143
+ if (!alt.parent) {
144
+ // TODO: There needs to be a "work in progress" tree all the way up to
145
+ // the final root.
146
+ throw new Error('The parent must have an alternate already.');
147
+ }
148
}
149
150
alt.type = fiber.type;
@@ -150,10 +155,8 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
155
return alt;
156
};
157
153
-exports.createHostContainerFiber = function(containerInfo : ?Object, priorityLevel : PriorityLevel) {
158
+exports.createHostContainerFiber = function() {
159
const fiber = createFiber(HostContainer, null);
155
- fiber.stateNode = containerInfo;
156
- fiber.pendingWorkPriority = priorityLevel;
160
return fiber;
161
};
162
src/renderers/shared/fiber/ReactFiberReconciler.js
+92
-62
@@ -14,10 +14,12 @@
14
15
import type { Fiber } from 'ReactFiber';
16
import type { PriorityLevel } from 'ReactPriorityLevel';
17
+import type { FiberRoot } from 'ReactFiberRoot';
18
19
var ReactFiber = require('ReactFiber');
20
var { beginWork } = require('ReactFiberBeginWork');
21
var { completeWork } = require('ReactFiberCompleteWork');
22
+var { createFiberRoot } = require('ReactFiberRoot');
23
24
var {
25
NoWork,
@@ -61,12 +63,15 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
63
// const scheduleHighPriCallback = config.scheduleHighPriCallback;
64
const scheduleLowPriCallback = config.scheduleLowPriCallback;
65
66
+ // The next work in progress fiber that we're currently working on.
67
let nextUnitOfWork : ?Fiber = null;
68
66
- let currentRootsWithPendingWork : ?Fiber = null;
69
+ // Linked list of roots with scheduled work on them.
70
+ let nextScheduledRoot : ?FiberRoot = null;
71
+ let lastScheduledRoot : ?FiberRoot = null;
72
68
- function findNextUnitOfWorkAtPriority(priorityLevel : PriorityLevel) : ?Fiber {
69
- let current = currentRootsWithPendingWork;
73
+ function findNextUnitOfWorkAtPriority(root : FiberRoot, priorityLevel : PriorityLevel) : ?Fiber {
74
+ let current = root.current;
75
while (current) {
76
if (current.pendingWorkPriority !== 0 &&
77
current.pendingWorkPriority <= priorityLevel) {
@@ -84,6 +89,8 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
89
// TODO: Coroutines can have work in their stateNode which is another
90
// type of child that needs to be searched for work.
91
if (current.child) {
92
+ // Ensure we have a work in progress copy to backtrack through.
93
+ ReactFiber.cloneFiber(current, NoWork);
94
current = current.child;
95
continue;
96
}
@@ -111,18 +118,38 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
118
}
119
120
function findNextUnitOfWork() {
114
- // Find the highest possible priority work to do.
115
- // This loop is unrolled just to satisfy Flow's enum constraint.
116
- // We could make arbitrary many idle priority levels but having
117
- // too many just means flushing changes too often.
118
- let work = findNextUnitOfWorkAtPriority(HighPriority);
119
- if (!work) {
120
- work = findNextUnitOfWorkAtPriority(LowPriority);
121
- if (!work) {
122
- work = findNextUnitOfWorkAtPriority(OffscreenPriority);
121
+ // Clear out roots with no more work on them.
122
+ while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) {
123
+ nextScheduledRoot.isScheduled = false;
124
+ if (nextScheduledRoot === lastScheduledRoot) {
125
+ nextScheduledRoot = null;
126
+ lastScheduledRoot = null;
127
+ return null;
128
}
129
+ nextScheduledRoot = nextScheduledRoot.nextScheduledRoot;
130
}
125
- return work;
131
+ let root = nextScheduledRoot;
132
+ while (root) {
133
+ // Find the highest possible priority work to do.
134
+ // This loop is unrolled just to satisfy Flow's enum constraint.
135
+ // We could make arbitrary many idle priority levels but having
136
+ // too many just means flushing changes too often.
137
+ let work = findNextUnitOfWorkAtPriority(root, HighPriority);
138
+ if (work) {
139
+ return work;
140
+ }
141
+ work = findNextUnitOfWorkAtPriority(root, LowPriority);
142
+ if (work) {
143
+ return work;
144
+ }
145
+ work = findNextUnitOfWorkAtPriority(root, OffscreenPriority);
146
+ if (work) {
147
+ return work;
148
+ }
149
+ // We didn't find anything to do in this root, so let's try the next one.
150
+ root = root.nextScheduledRoot;
151
+ }
152
+ return null;
153
}
154
155
function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
@@ -162,24 +189,19 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
189
// If there's no more work in this parent. Complete the parent.
190
workInProgress = parent;
191
} else {
165
- // If we're at the root, there's no more work to do.
166
- console.log('completed flush with remaining work at priority', workInProgress.pendingWorkPriority);
167
- if (workInProgress.pendingWorkPriority !== NoWork) {
168
- // TODO: This removes all but one node. Broken.
169
- currentRootsWithPendingWork = workInProgress;
170
- const nextWork = findNextUnitOfWork();
171
- if (!nextWork) {
172
- // Something went wrong and there wasn't actually any more work.
173
- // TODO: This currently fails because of the parent/root hacks.
174
- // throw new Error('Should never finish with a priority level unless there is work.');
175
- currentRootsWithPendingWork = null;
176
- return null;
177
- }
178
- return nextWork;
179
- } else {
180
- currentRootsWithPendingWork = null;
181
- return null;
192
+ // If we're at the root, there's no more work to do. We can flush it.
193
+ const root : FiberRoot = (workInProgress.stateNode : any);
194
+ root.current = workInProgress;
195
+ console.log('completed one root flush with remaining work at priority', workInProgress.pendingWorkPriority);
196
+ const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork;
197
+ // TODO: We can be smarter here and only look for more work in the
198
+ // "next" scheduled work since we've already scanned passed. That
199
+ // also ensures that work scheduled during reconciliation gets deferred.
200
+ const nextWork = findNextUnitOfWork();
201
+ if (!nextWork && hasMoreWork) {
202
+ throw new Error('FiberRoots should not have flagged more work if there is none.');
203
}
204
+ return nextWork;
205
}
206
}
207
}
@@ -211,39 +233,36 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
233
while (nextUnitOfWork) {
234
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
235
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
236
+ if (!nextUnitOfWork) {
237
+ // Find more work. We might have time to complete some more.
238
+ nextUnitOfWork = findNextUnitOfWork();
239
+ }
240
} else {
241
scheduleLowPriCallback(performLowPriWork);
216
- break;
242
+ return;
243
}
244
}
245
}
246
221
- function scheduleLowPriWork(root : Fiber) {
247
+ function scheduleLowPriWork(root : FiberRoot) {
248
// We must reset the current unit of work pointer so that we restart the
223
- // search from the root during the next tick.
249
+ // search from the root during the next tick, in case there is now higher
250
+ // priority work somewhere earlier than before.
251
nextUnitOfWork = null;
252
226
- if (currentRootsWithPendingWork) {
227
- if (root === currentRootsWithPendingWork) {
228
- // We're already scheduled.
229
- return;
230
- }
231
- // We already have some work pending in another root. Add this to the
232
- // linked list.
233
- let previousSibling = currentRootsWithPendingWork;
234
- while (previousSibling.sibling) {
235
- previousSibling = previousSibling.sibling;
236
- if (root === previousSibling) {
237
- // We're already scheduled.
238
- return;
239
- }
240
- }
241
- previousSibling.sibling = root;
253
+ if (root.isScheduled) {
254
+ // If we're already scheduled, we can bail out.
255
+ return;
256
+ }
257
+ root.isScheduled = true;
258
+ if (lastScheduledRoot) {
259
+ // Schedule ourselves to the end.
260
+ lastScheduledRoot.nextScheduledRoot = root;
261
+ lastScheduledRoot = root;
262
} else {
243
- // We're the first and only root with new changes.
244
- currentRootsWithPendingWork = root;
245
- root.sibling = null;
246
- // Ensure we will get another callback to process the work.
263
+ // We're the only work scheduled.
264
+ nextScheduledRoot = root;
265
+ lastScheduledRoot = root;
266
scheduleLowPriCallback(performLowPriWork);
267
}
268
}
@@ -261,27 +280,38 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
280
return {
281
282
mountContainer(element : ReactElement<any>, containerInfo : ?Object) : OpaqueNode {
264
- const container = ReactFiber.createHostContainerFiber(containerInfo, LowPriority);
265
- container.alternate = container;
283
+ const root = createFiberRoot(containerInfo);
284
+ const container = root.current;
285
+ // TODO: Use pending work/state instead of props.
286
container.pendingProps = element;
287
+ container.pendingWorkPriority = LowPriority;
288
268
- scheduleLowPriWork(container);
289
+ scheduleLowPriWork(root);
290
291
+ // It may seem strange that we don't return the root here, but that will
292
+ // allow us to have containers that are in the middle of the tree instead
293
+ // of being roots.
294
return container;
295
},
296
297
updateContainer(element : ReactElement<any>, container : OpaqueNode) : void {
274
- container.pendingProps = element;
275
- container.pendingWorkPriority = LowPriority;
298
+ // TODO: If this is a nested container, this won't be the root.
299
+ const root : FiberRoot = (container.stateNode : any);
300
+ // TODO: Use pending work/state instead of props.
301
+ root.current.pendingProps = element;
302
+ root.current.pendingWorkPriority = LowPriority;
303
277
- scheduleLowPriWork(container);
304
+ scheduleLowPriWork(root);
305
},
306
307
unmountContainer(container : OpaqueNode) : void {
281
- container.pendingProps = [];
282
- container.pendingWorkPriority = LowPriority;
308
+ // TODO: If this is a nested container, this won't be the root.
309
+ const root : FiberRoot = (container.stateNode : any);
310
+ // TODO: Use pending work/state instead of props.
311
+ root.current.pendingProps = [];
312
+ root.current.pendingWorkPriority = LowPriority;
313
284
- scheduleLowPriWork(container);
314
+ scheduleLowPriWork(root);
315
},
316
317
getPublicRootInstance(container : OpaqueNode) : ?Object {
src/renderers/shared/fiber/ReactFiberRoot.js
new
+42
@@ -0,0 +1,42 @@
1
+/**
2
+ * Copyright 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @providesModule ReactFiberRoot
10
+ * @flow
11
+ */
12
+
13
+'use strict';
14
+
15
+import type { Fiber } from 'ReactFiber';
16
+
17
+const { createHostContainerFiber } = require('ReactFiber');
18
+
19
+export type FiberRoot = {
20
+ // Any additional information from the host associated with this root.
21
+ containerInfo: ?Object,
22
+ // The currently active root fiber. This is the mutable root of the tree.
23
+ current: Fiber,
24
+ // Determines if this root has already been added to the schedule for work.
25
+ isScheduled: bool,
26
+ // The work schedule is a linked list.
27
+ nextScheduledRoot: ?FiberRoot,
28
+};
29
+
30
+exports.createFiberRoot = function(containerInfo : ?Object) : FiberRoot {
31
+ // Cyclic construction. This cheats the type system right now because
32
+ // stateNode is any.
33
+ const uninitializedFiber = createHostContainerFiber();
34
+ const root = {
35
+ current: uninitializedFiber,
36
+ containerInfo: containerInfo,
37
+ isScheduled: false,
38
+ nextScheduledRoot: null,
39
+ };
40
+ uninitializedFiber.stateNode = root;
41
+ return root;
42
+};
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+2
@@ -196,6 +196,8 @@ describe('ReactIncremental', function() {
196
ReactNoop.render(<Foo text="foo" />);
197
ReactNoop.flush();
198
199
+ expect(ops).toEqual(['Foo', 'Bar', 'Bar', 'Middle']);
200
+
201
ops = [];
202
203
// Render part of the work. This should be enough to flush everything except