@samitouri / QOS-React / commits / 7eb7c5a350

Match Fibers to saved selection as they mount

This implements matching Fibers against the tracked selection path. The algorithm is optimized to do as little checks as possible: * When not trying to restore selection, we don't do anything * When restoring selection, we only check .return pointers of new mounts * Only when .return pointers match our current deepest match, we compare the frames

Dan Abramov committed Apr 23, 2019 at 16:56 UTC 7eb7c5a350f10a29c3629a72d08e2183ea2d9920
1 file changed +126 -6
src/backend/renderer.js
+126 -6
@@ -710,7 +710,6 @@ export function attach(
710 const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');
711
712 if (isRoot) {
713 - rootInsertionOrder.set(id, nextRootIndex++);
713 pushOperation(TREE_OPERATION_ADD);
714 pushOperation(id);
715 pushOperation(ElementTypeRoot);
@@ -784,6 +783,18 @@ export function attach(
783 }
784
785 function recordUnmount(fiber: Fiber, isSimulated: boolean) {
786 + if (trackedPathMatchFiber !== null) {
787 + // We're in the process of trying to restore previous selection.
788 + // If this fiber matched but is being unmounted, there's no use trying.
789 + // Reset the state so we don't keep holding onto it.
790 + if (
791 + fiber === trackedPathMatchFiber ||
792 + fiber === trackedPathMatchFiber.alternate
793 + ) {
794 + setTrackedPath(null);
795 + }
796 + }
797 +
798 const isRoot = fiber.tag === HostRoot;
799 const primaryFiber = getPrimaryFiber(fiber);
800 if (!fiberToIDMap.has(primaryFiber)) {
@@ -798,7 +809,6 @@ export function attach(
809 }
810 const id = getFiberID(primaryFiber);
811 if (isRoot) {
801 - rootInsertionOrder.delete(id);
812 // Removing a root needs to happen at the end
813 // so we don't batch it with other unmounts.
814 pushOperation(TREE_OPERATION_REMOVE);
@@ -834,6 +844,12 @@ export function attach(
844 debug('mountFiberRecursively()', fiber, parentFiber);
845 }
846
847 + // If we have the tree selection from previous reload, try to match this Fiber.
848 + // Also remember whether to do the same for siblings.
849 + const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount(
850 + fiber
851 + );
852 +
853 const shouldIncludeInTree = !shouldFilterFiber(fiber);
854 if (shouldIncludeInTree) {
855 recordMount(fiber, parentFiber);
@@ -867,6 +883,10 @@ export function attach(
883 }
884 }
885
886 + // We're exiting this Fiber now, and entering its siblings.
887 + // If we have selection to restore, we might need to re-activate tracking.
888 + updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
889 +
890 if (traverseSiblings && fiber.sibling !== null) {
891 mountFiberRecursively(fiber.sibling, parentFiber, true);
892 }
@@ -1141,9 +1161,15 @@ export function attach(
1161 });
1162 });
1163 } else {
1164 + // Before the traversals, remember to start tracking
1165 + // our path in case we have selection to restore.
1166 + if (trackedPath !== null) {
1167 + mightBeOnTrackedPath = true;
1168 + }
1169 // If we have not been profiling, then we can just walk the tree and build up its current state as-is.
1170 hook.getFiberRoots(rendererID).forEach(root => {
1171 currentRootID = getFiberID(getPrimaryFiber(root.current));
1172 + rootInsertionOrder.set(currentRootID, nextRootIndex++);
1173
1174 if (isProfiling) {
1175 // If profiling is active, store commit time and duration, and the current interactions.
@@ -1181,6 +1207,12 @@ export function attach(
1207
1208 currentRootID = getFiberID(getPrimaryFiber(current));
1209
1210 + // Before the traversals, remember to start tracking
1211 + // our path in case we have selection to restore.
1212 + if (trackedPath !== null) {
1213 + mightBeOnTrackedPath = true;
1214 + }
1215 +
1216 if (isProfiling) {
1217 // If profiling is active, store commit time and duration, and the current interactions.
1218 // The frontend may request this information after profiling has stopped.
@@ -1206,16 +1238,19 @@ export function attach(
1238 current.memoizedState != null && current.memoizedState.element != null;
1239 if (!wasMounted && isMounted) {
1240 // Mount a new root.
1241 + rootInsertionOrder.set(currentRootID, nextRootIndex++);
1242 mountFiberRecursively(current, null);
1243 } else if (wasMounted && isMounted) {
1244 // Update an existing root.
1245 updateFiberRecursively(current, alternate, null);
1246 } else if (wasMounted && !isMounted) {
1247 // Unmount an existing root.
1248 + rootInsertionOrder.delete(currentRootID);
1249 recordUnmount(current, false);
1250 }
1251 } else {
1252 // Mount a new root.
1253 + rootInsertionOrder.set(currentRootID, nextRootIndex++);
1254 mountFiberRecursively(current, null);
1255 }
1256
@@ -2009,12 +2044,82 @@ export function attach(
2044 scheduleUpdate(fiber);
2045 }
2046
2047 + // Remember if we're trying to restore the selection after reload.
2048 + // In that case, we'll do some extra checks for matching mounts.
2049 let trackedPath: Array<PathFrame> | null = null;
2050 + let trackedPathMatchFiber: Fiber | null = null;
2051 + let trackedPathMatchDepth = -1;
2052 + let mightBeOnTrackedPath = false;
2053
2054 function setTrackedPath(path: Array<PathFrame> | null) {
2055 + if (path === null) {
2056 + trackedPathMatchFiber = null;
2057 + trackedPathMatchDepth = -1;
2058 + mightBeOnTrackedPath = false;
2059 + } else if (trackedPath !== null) {
2060 + throw new Error('Tracked path can only be set once.');
2061 + }
2062 trackedPath = path;
2063 }
2064
2065 + // We call this before traversing a new mount.
2066 + // It remembers whether this Fiber is the next best match for tracked path.
2067 + // The return value signals whether we should keep matching siblings or not.
2068 + function updateTrackedPathStateBeforeMount(fiber: Fiber): boolean {
2069 + if (trackedPath === null || !mightBeOnTrackedPath) {
2070 + // Fast path: there's nothing to track so do nothing and ignore siblings.
2071 + return false;
2072 + }
2073 + const returnFiber = fiber.return;
2074 + const returnAlternate = returnFiber !== null ? returnFiber.alternate : null;
2075 + // By now we know there's some selection to restore, and this is a new Fiber.
2076 + // Is this newly mounted Fiber a direct child of the current best match?
2077 + // (This will also be true for new roots if we haven't matched anything yet.)
2078 + if (
2079 + trackedPathMatchFiber === returnFiber ||
2080 + (trackedPathMatchFiber === returnAlternate && returnAlternate !== null)
2081 + ) {
2082 + // Is this the next Fiber we should select? Let's compare the frames.
2083 + const actualFrame = getPathFrame(fiber);
2084 + const expectedFrame = trackedPath[trackedPathMatchDepth + 1];
2085 + if (expectedFrame === undefined) {
2086 + throw new Error('Expected to see a frame at the next depth.');
2087 + }
2088 + if (
2089 + actualFrame.index === expectedFrame.index &&
2090 + actualFrame.key === expectedFrame.key &&
2091 + actualFrame.displayName === expectedFrame.displayName
2092 + ) {
2093 + // We have our next match.
2094 + trackedPathMatchFiber = fiber;
2095 + trackedPathMatchDepth++;
2096 + // Are we out of frames to match?
2097 + if (trackedPathMatchDepth === trackedPath.length - 1) {
2098 + // There's nothing that can possibly match afterwards.
2099 + // Don't check the children.
2100 + mightBeOnTrackedPath = false;
2101 + } else {
2102 + // Check the children, as they might reveal the next match.
2103 + mightBeOnTrackedPath = true;
2104 + }
2105 + // In either case, since we have a match, we don't need
2106 + // to check the siblings. They'll never match.
2107 + return false;
2108 + }
2109 + }
2110 + // This Fiber's parent is on the path, but this Fiber itself isn't.
2111 + // There's no need to check its children--they won't be on the path either.
2112 + mightBeOnTrackedPath = false;
2113 + // However, one of its siblings may be on the path so keep searching.
2114 + return true;
2115 + }
2116 +
2117 + function updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath) {
2118 + // updateTrackedPathStateBeforeMount() told us whether to match siblings.
2119 + // Now that we're entering siblings, let's use that information.
2120 + mightBeOnTrackedPath = mightSiblingsBeOnTrackedPath;
2121 + }
2122 +
2123 function getPathFrame(fiber: Fiber): PathFrame {
2124 let { displayName, key, index } = getDataForFiber(fiber);
2125 if (fiber.tag === HostRoot) {
@@ -2053,11 +2158,26 @@ export function attach(
2158 }
2159
2160 function getBestMatchForTrackedPath(): PathMatch | null {
2056 - if (trackedPath !== null) {
2057 - console.log('Looking for match for path: ', trackedPath);
2161 + if (trackedPath === null) {
2162 + // Nothing to match.
2163 + return null;
2164 }
2059 - // TODO: implement this.
2060 - return null;
2165 + if (trackedPathMatchFiber === null) {
2166 + // We didn't find anything.
2167 + return null;
2168 + }
2169 + // Find the closest Fiber store is aware of.
2170 + let fiber = trackedPathMatchFiber;
2171 + while (fiber !== null && shouldFilterFiber(fiber)) {
2172 + fiber = fiber.return;
2173 + }
2174 + if (fiber === null) {
2175 + return null;
2176 + }
2177 + return {
2178 + id: getFiberID(getPrimaryFiber(fiber)),
2179 + isFullMatch: trackedPathMatchDepth === trackedPath.length - 1,
2180 + };
2181 }
2182
2183 return {