@samitouri / QOS-React-2 / commits / 33589f7423

useMutableSource: "Entangle" instead of expiring (#18889)

* useMutableSource: "Entangle" instead of expiring A lane is said to be entangled with another when it's not allowed to render in a batch that does not also include the other lane. This commit implements entanglement for `useMutableSource`. If a source is mutated in between when it's read in the render phase, but before it's subscribed to in the commit phase, we must account for whether the same source has pending mutations elsewhere. The old subscriptions must not be allowed to re-render without also including the new subscription (and vice versa), to prevent tearing. In the old reconciler, we did this by synchronously flushing all the pending subscription updates. This works, but isn't ideal. The new reconciler can entangle the updates without de-opting to sync. In the future, we plan to use this same mechanism for other features, like skipping over intermediate useTransition states. * Use clz instead of ctrz to pick an arbitrary lane Should be slightly faster since most engines have built-in support.

Andrew Clark committed May 13, 2020 at 11:33 UTC 33589f7423fa498c97001898af730c461a1fdc7d
6 files changed +107 -40
packages/react-reconciler/src/ReactFiberHooks.new.js
+6 -7
@@ -32,7 +32,7 @@ import {
32 isSubsetOfLanes,
33 mergeLanes,
34 removeLanes,
35 - markRootExpired,
35 + markRootEntangled,
36 markRootMutableRead,
37 } from './ReactFiberLane';
38 import {readContext} from './ReactFiberNewContext.new';
@@ -999,12 +999,11 @@ function useMutableSource<Source, Snapshot>(
999 markRootMutableRead(root, lane);
1000
1001 // If the source mutated between render and now,
1002 - // there may be state updates already scheduled from the old getSnapshot.
1003 - // Those updates should not commit without this value.
1004 - // There is no mechanism currently to associate these updates though,
1005 - // so for now we fall back to synchronously flushing all pending updates.
1006 - // TODO: This should entangle the lanes instead of expiring everything.
1007 - markRootExpired(root, root.mutableReadLanes);
1002 + // there may be state updates already scheduled from the old source.
1003 + // Entangle the updates so that they render in the same batch.
1004 + // TODO: I think we need to entangle even if the snapshot matches,
1005 + // because there could have been an update to a different hook.
1006 + markRootEntangled(root, root.mutableReadLanes);
1007 }
1008 }
1009 }, [getSnapshot, source, subscribe]);
packages/react-reconciler/src/ReactFiberLane.js
+55 -14
@@ -365,6 +365,37 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
365 }
366 }
367
368 + // Check for entangled lanes and add them to the batch.
369 + //
370 + // A lane is said to be entangled with another when it's not allowed to render
371 + // in a batch that does not also include the other lane. Typically we do this
372 + // when multiple updates have the same source, and we only want to respond to
373 + // the most recent event from that source.
374 + //
375 + // Note that we apply entanglements *after* checking for partial work above.
376 + // This means that if a lane is entangled during an interleaved event while
377 + // it's already rendering, we won't interrupt it. This is intentional, since
378 + // entanglement is usually "best effort": we'll try our best to render the
379 + // lanes in the same batch, but it's not worth throwing out partially
380 + // completed work in order to do it.
381 + //
382 + // For those exceptions where entanglement is semantically important, like
383 + // useMutableSource, we should ensure that there is no partial work at the
384 + // time we apply the entanglement.
385 + const entangledLanes = root.entangledLanes;
386 + if (entangledLanes !== NoLanes) {
387 + const entanglements = root.entanglements;
388 + let lanes = nextLanes & entangledLanes;
389 + while (lanes > 0) {
390 + const index = pickArbitraryLaneIndex(lanes);
391 + const lane = 1 << index;
392 +
393 + nextLanes |= entanglements[index];
394 +
395 + lanes &= ~lane;
396 + }
397 + }
398 +
399 return nextLanes;
400 }
401
@@ -401,7 +432,7 @@ export function markStarvedLanesAsExpired(
432 // it as expired to force it to finish.
433 let lanes = pendingLanes;
434 while (lanes > 0) {
404 - const index = ctrz(lanes);
435 + const index = pickArbitraryLaneIndex(lanes);
436 const lane = 1 << index;
437
438 const expirationTime = expirationTimes[index];
@@ -588,6 +619,10 @@ export function pickArbitraryLane(lanes: Lanes): Lane {
619 return getLowestPriorityLane(lanes);
620 }
621
622 +function pickArbitraryLaneIndex(lanes: Lane | Lanes) {
623 + return 31 - clz32(lanes);
624 +}
625 +
626 export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
627 return (a & b) !== NoLanes;
628 }
@@ -647,7 +682,7 @@ export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
682 const expirationTimes = root.expirationTimes;
683 let lanes = suspendedLanes;
684 while (lanes > 0) {
650 - const index = ctrz(lanes);
685 + const index = pickArbitraryLaneIndex(lanes);
686 const lane = 1 << index;
687
688 expirationTimes[index] = NoTimestamp;
@@ -692,10 +727,12 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
727 root.expiredLanes &= remainingLanes;
728 root.mutableReadLanes &= remainingLanes;
729
730 + root.entangledLanes &= remainingLanes;
731 +
732 const expirationTimes = root.expirationTimes;
733 let lanes = noLongerPendingLanes;
734 while (lanes > 0) {
698 - const index = ctrz(lanes);
735 + const index = pickArbitraryLaneIndex(lanes);
736 const lane = 1 << index;
737
738 // Clear the expiration time
@@ -705,6 +742,21 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
742 }
743 }
744
745 +export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
746 + root.entangledLanes |= entangledLanes;
747 +
748 + const entanglements = root.entanglements;
749 + let lanes = entangledLanes;
750 + while (lanes > 0) {
751 + const index = pickArbitraryLaneIndex(lanes);
752 + const lane = 1 << index;
753 +
754 + entanglements[index] |= entangledLanes;
755 +
756 + lanes &= ~lane;
757 + }
758 +}
759 +
760 export function getBumpedLaneForHydration(
761 root: FiberRoot,
762 renderLanes: Lanes,
@@ -777,14 +829,3 @@ function clz32Fallback(lanes: Lanes | Lane) {
829 }
830 return (31 - ((log(lanes) / LN2) | 0)) | 0;
831 }
780 -
781 -// Count trailing zeros. Only used on lanes, so assume input is an integer.
782 -function ctrz(lanes: Lanes | Lane) {
783 - let bits = lanes;
784 - bits |= bits << 16;
785 - bits |= bits << 8;
786 - bits |= bits << 4;
787 - bits |= bits << 2;
788 - bits |= bits << 1;
789 - return 32 - clz32(~bits);
790 -}
packages/react-reconciler/src/ReactFiberRoot.new.js
+3
@@ -49,6 +49,9 @@ function FiberRootNode(containerInfo, tag, hydrate) {
49
50 this.finishedLanes = NoLanes;
51
52 + this.entangledLanes = NoLanes;
53 + this.entanglements = createLaneMap(NoLanes);
54 +
55 if (enableSchedulerTracing) {
56 this.interactionThreadID = unstable_getThreadID();
57 this.memoizedInteractions = new Set();
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+9 -6
@@ -673,6 +673,15 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
673 currentEventWipLanes = NoLanes;
674 currentEventPendingLanes = NoLanes;
675
676 + invariant(
677 + (executionContext & (RenderContext | CommitContext)) === NoContext,
678 + 'Should not already be working.',
679 + );
680 +
681 + // Flush any pending passive effects before deciding which lanes to work on,
682 + // in case they schedule additional work.
683 + flushPassiveEffects();
684 +
685 // Determine the next expiration time to work on, using the fields stored
686 // on the root.
687 let lanes = getNextLanes(
@@ -697,12 +706,6 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
706 }
707
708 const originalCallbackNode = root.callbackNode;
700 - invariant(
701 - (executionContext & (RenderContext | CommitContext)) === NoContext,
702 - 'Should not already be working.',
703 - );
704 -
705 - flushPassiveEffects();
709
710 let exitStatus = renderRootConcurrent(root, lanes);
711
packages/react-reconciler/src/ReactInternalTypes.js
+3
@@ -262,6 +262,9 @@ type BaseFiberRootProperties = {|
262 mutableReadLanes: Lanes,
263
264 finishedLanes: Lanes,
265 +
266 + entangledLanes: Lanes,
267 + entanglements: LaneMap<Lanes>,
268 |};
269
270 // The following attributes are only used by interaction tracing builds.
packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
+31 -13
@@ -1481,21 +1481,39 @@ describe('useMutableSource', () => {
1481 source.valueB = '3';
1482 },
1483 );
1484 - });
1485 -
1486 - expect(Scheduler).toHaveYielded([
1487 - // The partial render completes
1488 - 'Child: 2',
1489 - 'Commit: 2, 2',
1484
1491 - // Then we start rendering the low priority mutation
1492 - 'Parent: 3',
1485 + expect(Scheduler).toFlushAndYieldThrough([
1486 + // The partial render completes
1487 + 'Child: 2',
1488 + 'Commit: 2, 2',
1489 + ]);
1490
1494 - // Eventually the child corrects itself, because of the check that
1495 - // occurs when re-subscribing.
1496 - 'Child: 3',
1497 - 'Commit: 3, 3',
1498 - ]);
1491 + // Now there are two pending mutations at different priorities. But they
1492 + // both read the same verion of the mutable source, so we must render
1493 + // them simultaneously.
1494 + //
1495 + if (gate(flags => flags.new)) {
1496 + // In the new reconciler, we can do this with entanglement: when the
1497 + // high priority render starts, we'll also include the low pri work.
1498 + expect(Scheduler).toFlushAndYieldThrough([
1499 + 'Parent: 3',
1500 + // Demonstrates that we can yield here
1501 + ]);
1502 + expect(Scheduler).toFlushAndYield([
1503 + // Now finish the rest of the update
1504 + 'Child: 3',
1505 + 'Commit: 3, 3',
1506 + ]);
1507 + } else {
1508 + // In the old reconciler, we don't have an entanglement mechanism. The
1509 + // best we can do is synchronously flush both updates.
1510 + expect(Scheduler).toFlushAndYield([
1511 + 'Parent: 3',
1512 + 'Child: 3',
1513 + 'Commit: 3, 3',
1514 + ]);
1515 + }
1516 + });
1517 });
1518
1519 // @gate experimental