From 7d028fd8cc047f4ae3739f9d380bf59640ea8129 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 15 Jun 2016 16:06:45 -0700 Subject: [PATCH] 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. --- src/renderers/shared/fiber/ReactFiber.js | 9 +- .../shared/fiber/ReactFiberReconciler.js | 154 +++++++++++------- src/renderers/shared/fiber/ReactFiberRoot.js | 42 +++++ .../fiber/__tests__/ReactIncremental-test.js | 2 + 4 files changed, 142 insertions(+), 65 deletions(-) create mode 100644 src/renderers/shared/fiber/ReactFiberRoot.js diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index b7a4d90255..4bd19709a1 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -140,6 +140,11 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi // Stop using the alternates of parents once we have a parent stack. // $FlowFixMe: This downcast is not safe. It is intentionally an error. alt.parent = fiber.parent.alternate; + if (!alt.parent) { + // TODO: There needs to be a "work in progress" tree all the way up to + // the final root. + throw new Error('The parent must have an alternate already.'); + } } alt.type = fiber.type; @@ -150,10 +155,8 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi return alt; }; -exports.createHostContainerFiber = function(containerInfo : ?Object, priorityLevel : PriorityLevel) { +exports.createHostContainerFiber = function() { const fiber = createFiber(HostContainer, null); - fiber.stateNode = containerInfo; - fiber.pendingWorkPriority = priorityLevel; return fiber; }; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 275570c79d..07e23fad71 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -14,10 +14,12 @@ import type { Fiber } from 'ReactFiber'; import type { PriorityLevel } from 'ReactPriorityLevel'; +import type { FiberRoot } from 'ReactFiberRoot'; var ReactFiber = require('ReactFiber'); var { beginWork } = require('ReactFiberBeginWork'); var { completeWork } = require('ReactFiberCompleteWork'); +var { createFiberRoot } = require('ReactFiberRoot'); var { NoWork, @@ -61,12 +63,15 @@ module.exports = function(config : HostConfig) : Reconciler { // const scheduleHighPriCallback = config.scheduleHighPriCallback; const scheduleLowPriCallback = config.scheduleLowPriCallback; + // The next work in progress fiber that we're currently working on. let nextUnitOfWork : ?Fiber = null; - let currentRootsWithPendingWork : ?Fiber = null; + // Linked list of roots with scheduled work on them. + let nextScheduledRoot : ?FiberRoot = null; + let lastScheduledRoot : ?FiberRoot = null; - function findNextUnitOfWorkAtPriority(priorityLevel : PriorityLevel) : ?Fiber { - let current = currentRootsWithPendingWork; + function findNextUnitOfWorkAtPriority(root : FiberRoot, priorityLevel : PriorityLevel) : ?Fiber { + let current = root.current; while (current) { if (current.pendingWorkPriority !== 0 && current.pendingWorkPriority <= priorityLevel) { @@ -84,6 +89,8 @@ module.exports = function(config : HostConfig) : Reconciler { // TODO: Coroutines can have work in their stateNode which is another // type of child that needs to be searched for work. if (current.child) { + // Ensure we have a work in progress copy to backtrack through. + ReactFiber.cloneFiber(current, NoWork); current = current.child; continue; } @@ -111,18 +118,38 @@ module.exports = function(config : HostConfig) : Reconciler { } function findNextUnitOfWork() { - // Find the highest possible priority work to do. - // This loop is unrolled just to satisfy Flow's enum constraint. - // We could make arbitrary many idle priority levels but having - // too many just means flushing changes too often. - let work = findNextUnitOfWorkAtPriority(HighPriority); - if (!work) { - work = findNextUnitOfWorkAtPriority(LowPriority); - if (!work) { - work = findNextUnitOfWorkAtPriority(OffscreenPriority); + // Clear out roots with no more work on them. + while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) { + nextScheduledRoot.isScheduled = false; + if (nextScheduledRoot === lastScheduledRoot) { + nextScheduledRoot = null; + lastScheduledRoot = null; + return null; } + nextScheduledRoot = nextScheduledRoot.nextScheduledRoot; } - return work; + let root = nextScheduledRoot; + while (root) { + // Find the highest possible priority work to do. + // This loop is unrolled just to satisfy Flow's enum constraint. + // We could make arbitrary many idle priority levels but having + // too many just means flushing changes too often. + let work = findNextUnitOfWorkAtPriority(root, HighPriority); + if (work) { + return work; + } + work = findNextUnitOfWorkAtPriority(root, LowPriority); + if (work) { + return work; + } + work = findNextUnitOfWorkAtPriority(root, OffscreenPriority); + if (work) { + return work; + } + // We didn't find anything to do in this root, so let's try the next one. + root = root.nextScheduledRoot; + } + return null; } function completeUnitOfWork(workInProgress : Fiber) : ?Fiber { @@ -162,24 +189,19 @@ module.exports = function(config : HostConfig) : Reconciler { // If there's no more work in this parent. Complete the parent. workInProgress = parent; } else { - // If we're at the root, there's no more work to do. - console.log('completed flush with remaining work at priority', workInProgress.pendingWorkPriority); - if (workInProgress.pendingWorkPriority !== NoWork) { - // TODO: This removes all but one node. Broken. - currentRootsWithPendingWork = workInProgress; - const nextWork = findNextUnitOfWork(); - if (!nextWork) { - // Something went wrong and there wasn't actually any more work. - // TODO: This currently fails because of the parent/root hacks. - // throw new Error('Should never finish with a priority level unless there is work.'); - currentRootsWithPendingWork = null; - return null; - } - return nextWork; - } else { - currentRootsWithPendingWork = null; - return null; + // If we're at the root, there's no more work to do. We can flush it. + const root : FiberRoot = (workInProgress.stateNode : any); + root.current = workInProgress; + console.log('completed one root flush with remaining work at priority', workInProgress.pendingWorkPriority); + const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork; + // TODO: We can be smarter here and only look for more work in the + // "next" scheduled work since we've already scanned passed. That + // also ensures that work scheduled during reconciliation gets deferred. + const nextWork = findNextUnitOfWork(); + if (!nextWork && hasMoreWork) { + throw new Error('FiberRoots should not have flagged more work if there is none.'); } + return nextWork; } } } @@ -211,39 +233,36 @@ module.exports = function(config : HostConfig) : Reconciler { while (nextUnitOfWork) { if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + if (!nextUnitOfWork) { + // Find more work. We might have time to complete some more. + nextUnitOfWork = findNextUnitOfWork(); + } } else { scheduleLowPriCallback(performLowPriWork); - break; + return; } } } - function scheduleLowPriWork(root : Fiber) { + function scheduleLowPriWork(root : FiberRoot) { // We must reset the current unit of work pointer so that we restart the - // search from the root during the next tick. + // search from the root during the next tick, in case there is now higher + // priority work somewhere earlier than before. nextUnitOfWork = null; - if (currentRootsWithPendingWork) { - if (root === currentRootsWithPendingWork) { - // We're already scheduled. - return; - } - // We already have some work pending in another root. Add this to the - // linked list. - let previousSibling = currentRootsWithPendingWork; - while (previousSibling.sibling) { - previousSibling = previousSibling.sibling; - if (root === previousSibling) { - // We're already scheduled. - return; - } - } - previousSibling.sibling = root; + if (root.isScheduled) { + // If we're already scheduled, we can bail out. + return; + } + root.isScheduled = true; + if (lastScheduledRoot) { + // Schedule ourselves to the end. + lastScheduledRoot.nextScheduledRoot = root; + lastScheduledRoot = root; } else { - // We're the first and only root with new changes. - currentRootsWithPendingWork = root; - root.sibling = null; - // Ensure we will get another callback to process the work. + // We're the only work scheduled. + nextScheduledRoot = root; + lastScheduledRoot = root; scheduleLowPriCallback(performLowPriWork); } } @@ -261,27 +280,38 @@ module.exports = function(config : HostConfig) : Reconciler { return { mountContainer(element : ReactElement, containerInfo : ?Object) : OpaqueNode { - const container = ReactFiber.createHostContainerFiber(containerInfo, LowPriority); - container.alternate = container; + const root = createFiberRoot(containerInfo); + const container = root.current; + // TODO: Use pending work/state instead of props. container.pendingProps = element; + container.pendingWorkPriority = LowPriority; - scheduleLowPriWork(container); + scheduleLowPriWork(root); + // It may seem strange that we don't return the root here, but that will + // allow us to have containers that are in the middle of the tree instead + // of being roots. return container; }, updateContainer(element : ReactElement, container : OpaqueNode) : void { - container.pendingProps = element; - container.pendingWorkPriority = LowPriority; + // TODO: If this is a nested container, this won't be the root. + const root : FiberRoot = (container.stateNode : any); + // TODO: Use pending work/state instead of props. + root.current.pendingProps = element; + root.current.pendingWorkPriority = LowPriority; - scheduleLowPriWork(container); + scheduleLowPriWork(root); }, unmountContainer(container : OpaqueNode) : void { - container.pendingProps = []; - container.pendingWorkPriority = LowPriority; + // TODO: If this is a nested container, this won't be the root. + const root : FiberRoot = (container.stateNode : any); + // TODO: Use pending work/state instead of props. + root.current.pendingProps = []; + root.current.pendingWorkPriority = LowPriority; - scheduleLowPriWork(container); + scheduleLowPriWork(root); }, getPublicRootInstance(container : OpaqueNode) : ?Object { diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js new file mode 100644 index 0000000000..ce186c3fce --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -0,0 +1,42 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactFiberRoot + * @flow + */ + +'use strict'; + +import type { Fiber } from 'ReactFiber'; + +const { createHostContainerFiber } = require('ReactFiber'); + +export type FiberRoot = { + // Any additional information from the host associated with this root. + containerInfo: ?Object, + // The currently active root fiber. This is the mutable root of the tree. + current: Fiber, + // Determines if this root has already been added to the schedule for work. + isScheduled: bool, + // The work schedule is a linked list. + nextScheduledRoot: ?FiberRoot, +}; + +exports.createFiberRoot = function(containerInfo : ?Object) : FiberRoot { + // Cyclic construction. This cheats the type system right now because + // stateNode is any. + const uninitializedFiber = createHostContainerFiber(); + const root = { + current: uninitializedFiber, + containerInfo: containerInfo, + isScheduled: false, + nextScheduledRoot: null, + }; + uninitializedFiber.stateNode = root; + return root; +}; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index 8b6f537822..c39b06b862 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -196,6 +196,8 @@ describe('ReactIncremental', function() { ReactNoop.render(); ReactNoop.flush(); + expect(ops).toEqual(['Foo', 'Bar', 'Bar', 'Middle']); + ops = []; // Render part of the work. This should be enough to flush everything except