Skip to content

Commit 41ad083

Browse files
committed
Don't shift interleaved updates to separate lane
Now that interleaved updates are added to a special queue, we no longer need to shift them into their own lane. We can add to a lane that's already in the middle of rendering without risk of tearing. See facebook#20615 for more background. I've only changed this in the new fork, and only behind the enableTransitionEntanglements flag. Most of this commit involves updating tests. The "shift-to-a-new" lane trick was intentionally used in a handful of tests where two or more updates need to be scheduled in different lanes. Most of these tests were written before `startTransition` existed, and all of them were written before transitions were assigned arbitrary lanes. So I ported these tests to use `startTransition` instead, which is the idiomatic way to mark an update as parallel. I didn't change the old fork at all. Writing these tests in such a way that they also pass in the old fork actually revealed a few flaws in the current implementation regarding interrupting a suspended refresh transition early, which is a good reminder that we should be writing our tests using idiomatic patterns as much as we possibly can.
1 parent 3cc1f20 commit 41ad083

10 files changed

+406
-292
lines changed

packages/react-reconciler/src/ReactFiberLane.new.js

Lines changed: 75 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -512,56 +512,88 @@ export function findUpdateLane(
512512
lanePriority: LanePriority,
513513
wipLanes: Lanes,
514514
): Lane {
515-
switch (lanePriority) {
516-
case NoLanePriority:
517-
break;
518-
case SyncLanePriority:
519-
return SyncLane;
520-
case SyncBatchedLanePriority:
521-
return SyncBatchedLane;
522-
case InputDiscreteLanePriority: {
523-
const lane = pickArbitraryLane(InputDiscreteLanes & ~wipLanes);
524-
if (lane === NoLane) {
525-
// Shift to the next priority level
526-
return findUpdateLane(InputContinuousLanePriority, wipLanes);
515+
if (enableTransitionEntanglement) {
516+
// Ignore wipLanes. Always assign to the same bit per priority.
517+
switch (lanePriority) {
518+
case NoLanePriority:
519+
break;
520+
case SyncLanePriority:
521+
return SyncLane;
522+
case SyncBatchedLanePriority:
523+
return SyncBatchedLane;
524+
case InputDiscreteLanePriority: {
525+
return pickArbitraryLane(InputDiscreteLanes);
527526
}
528-
return lane;
529-
}
530-
case InputContinuousLanePriority: {
531-
const lane = pickArbitraryLane(InputContinuousLanes & ~wipLanes);
532-
if (lane === NoLane) {
533-
// Shift to the next priority level
534-
return findUpdateLane(DefaultLanePriority, wipLanes);
527+
case InputContinuousLanePriority: {
528+
return pickArbitraryLane(InputContinuousLanes);
529+
}
530+
case DefaultLanePriority: {
531+
return pickArbitraryLane(DefaultLanes);
535532
}
536-
return lane;
533+
case TransitionPriority: // Should be handled by findTransitionLane instead
534+
case RetryLanePriority: // Should be handled by findRetryLane instead
535+
break;
536+
case IdleLanePriority:
537+
return pickArbitraryLane(IdleLanes);
538+
default:
539+
// The remaining priorities are not valid for updates
540+
break;
537541
}
538-
case DefaultLanePriority: {
539-
let lane = pickArbitraryLane(DefaultLanes & ~wipLanes);
540-
if (lane === NoLane) {
541-
// If all the default lanes are already being worked on, look for a
542-
// lane in the transition range.
543-
lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
542+
} else {
543+
// Old behavior that uses wipLanes to shift interleaved updates into a
544+
// separate lane. This is no longer needed because we put interleaved
545+
// updates on a special queue.
546+
switch (lanePriority) {
547+
case NoLanePriority:
548+
break;
549+
case SyncLanePriority:
550+
return SyncLane;
551+
case SyncBatchedLanePriority:
552+
return SyncBatchedLane;
553+
case InputDiscreteLanePriority: {
554+
const lane = pickArbitraryLane(InputDiscreteLanes & ~wipLanes);
544555
if (lane === NoLane) {
545-
// All the transition lanes are taken, too. This should be very
546-
// rare, but as a last resort, pick a default lane. This will have
547-
// the effect of interrupting the current work-in-progress render.
548-
lane = pickArbitraryLane(DefaultLanes);
556+
// Shift to the next priority level
557+
return findUpdateLane(InputContinuousLanePriority, wipLanes);
549558
}
559+
return lane;
550560
}
551-
return lane;
552-
}
553-
case TransitionPriority: // Should be handled by findTransitionLane instead
554-
case RetryLanePriority: // Should be handled by findRetryLane instead
555-
break;
556-
case IdleLanePriority:
557-
let lane = pickArbitraryLane(IdleLanes & ~wipLanes);
558-
if (lane === NoLane) {
559-
lane = pickArbitraryLane(IdleLanes);
561+
case InputContinuousLanePriority: {
562+
const lane = pickArbitraryLane(InputContinuousLanes & ~wipLanes);
563+
if (lane === NoLane) {
564+
// Shift to the next priority level
565+
return findUpdateLane(DefaultLanePriority, wipLanes);
566+
}
567+
return lane;
560568
}
561-
return lane;
562-
default:
563-
// The remaining priorities are not valid for updates
564-
break;
569+
case DefaultLanePriority: {
570+
let lane = pickArbitraryLane(DefaultLanes & ~wipLanes);
571+
if (lane === NoLane) {
572+
// If all the default lanes are already being worked on, look for a
573+
// lane in the transition range.
574+
lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
575+
if (lane === NoLane) {
576+
// All the transition lanes are taken, too. This should be very
577+
// rare, but as a last resort, pick a default lane. This will have
578+
// the effect of interrupting the current work-in-progress render.
579+
lane = pickArbitraryLane(DefaultLanes);
580+
}
581+
}
582+
return lane;
583+
}
584+
case TransitionPriority: // Should be handled by findTransitionLane instead
585+
case RetryLanePriority: // Should be handled by findRetryLane instead
586+
break;
587+
case IdleLanePriority:
588+
let lane = pickArbitraryLane(IdleLanes & ~wipLanes);
589+
if (lane === NoLane) {
590+
lane = pickArbitraryLane(IdleLanes);
591+
}
592+
return lane;
593+
default:
594+
// The remaining priorities are not valid for updates
595+
break;
596+
}
565597
}
566598
invariant(
567599
false,

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
disableSchedulerTimeoutInWorkLoop,
3535
enableDoubleInvokingEffects,
3636
skipUnmountedBoundaries,
37+
enableTransitionEntanglement,
3738
} from 'shared/ReactFeatureFlags';
3839
import ReactSharedInternals from 'shared/ReactSharedInternals';
3940
import invariant from 'shared/invariant';
@@ -809,6 +810,7 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
809810
let exitStatus = renderRootConcurrent(root, lanes);
810811

811812
if (
813+
!enableTransitionEntanglement &&
812814
includesSomeLane(
813815
workInProgressRootIncludedLanes,
814816
workInProgressRootUpdatedLanes,
@@ -1014,6 +1016,7 @@ function performSyncWorkOnRoot(root) {
10141016
lanes = workInProgressRootRenderLanes;
10151017
exitStatus = renderRootSync(root, lanes);
10161018
if (
1019+
!enableTransitionEntanglement &&
10171020
includesSomeLane(
10181021
workInProgressRootIncludedLanes,
10191022
workInProgressRootUpdatedLanes,

packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,9 @@ describe('DebugTracing', () => {
327327
]);
328328
});
329329

330+
// This test is coupled to lane implementation details, so I'm disabling it in
331+
// the new fork until it stabilizes so we don't have to repeatedly update it.
332+
// @gate !enableParallelTransitions
330333
// @gate experimental && build === 'development' && enableDebugTracing
331334
it('should log cascading passive updates', () => {
332335
function Example() {

packages/react-reconciler/src/__tests__/ReactExpiration-test.js

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let ReactNoop;
1414
let Scheduler;
1515
let readText;
1616
let resolveText;
17+
let startTransition;
1718

1819
describe('ReactExpiration', () => {
1920
beforeEach(() => {
@@ -22,6 +23,7 @@ describe('ReactExpiration', () => {
2223
React = require('react');
2324
ReactNoop = require('react-noop-renderer');
2425
Scheduler = require('scheduler');
26+
startTransition = React.unstable_startTransition;
2527

2628
const textCache = new Map();
2729

@@ -610,6 +612,7 @@ describe('ReactExpiration', () => {
610612
expect(root).toMatchRenderedOutput('Sync pri: 2, Idle pri: 2');
611613
});
612614

615+
// @gate experimental
613616
it('a single update can expire without forcing all other updates to expire', async () => {
614617
const {useState} = React;
615618

@@ -648,12 +651,18 @@ describe('ReactExpiration', () => {
648651

649652
await ReactNoop.act(async () => {
650653
// Partially render an update
651-
updateNormalPri();
654+
startTransition(() => {
655+
updateNormalPri();
656+
});
652657
expect(Scheduler).toFlushAndYieldThrough(['High pri: 0']);
653-
// Some time goes by. In an interleaved event, schedule another update.
658+
659+
// Some time goes by. Schedule another update.
654660
// This will be placed into a separate batch.
655661
Scheduler.unstable_advanceTime(4000);
656-
updateNormalPri();
662+
663+
startTransition(() => {
664+
updateNormalPri();
665+
});
657666
// Keep rendering the first update
658667
expect(Scheduler).toFlushAndYieldThrough(['Normal pri: 1']);
659668
// More time goes by. Enough to expire the first batch, but not the
@@ -662,20 +671,30 @@ describe('ReactExpiration', () => {
662671
// Attempt to interrupt with a high pri update.
663672
updateHighPri();
664673

665-
// The first update expired, so first will finish it without interrupting.
666-
// But not the second update, which hasn't expired yet.
667-
expect(Scheduler).toFlushExpired(['Sibling']);
674+
if (gate(flags => flags.enableParallelTransitions)) {
675+
// The first update expired, so first will finish it without
676+
// interrupting. But not the second update, which hasn't expired yet.
677+
expect(Scheduler).toFlushExpired(['Sibling']);
678+
expect(Scheduler).toFlushAndYield([
679+
// Then render the high pri update
680+
'High pri: 1',
681+
'Normal pri: 1',
682+
'Sibling',
683+
// Then the second normal pri update
684+
'High pri: 1',
685+
'Normal pri: 2',
686+
'Sibling',
687+
]);
688+
} else {
689+
// In the old implementation, all updates get entangled together.
690+
expect(Scheduler).toFlushExpired([
691+
'High pri: 1',
692+
'Normal pri: 2',
693+
'Sibling',
694+
]);
695+
expect(Scheduler).toFlushAndYield([]);
696+
}
668697
});
669-
expect(Scheduler).toHaveYielded([
670-
// Then render the high pri update
671-
'High pri: 1',
672-
'Normal pri: 1',
673-
'Sibling',
674-
// Then the second normal pri update
675-
'High pri: 1',
676-
'Normal pri: 2',
677-
'Sibling',
678-
]);
679698
});
680699

681700
it('detects starvation in multiple batches', async () => {

packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ let NormalPriority;
1919
let LowPriority;
2020
let IdlePriority;
2121
let runWithPriority;
22+
let startTransition;
2223

2324
describe('ReactSchedulerIntegration', () => {
2425
beforeEach(() => {
@@ -33,6 +34,7 @@ describe('ReactSchedulerIntegration', () => {
3334
LowPriority = Scheduler.unstable_LowPriority;
3435
IdlePriority = Scheduler.unstable_IdlePriority;
3536
runWithPriority = Scheduler.unstable_runWithPriority;
37+
startTransition = React.unstable_startTransition;
3638
});
3739

3840
function getCurrentPriorityAsString() {
@@ -446,6 +448,7 @@ describe(
446448
React = require('react');
447449
ReactNoop = require('react-noop-renderer');
448450
Scheduler = require('scheduler');
451+
startTransition = React.unstable_startTransition;
449452
});
450453

451454
afterEach(() => {
@@ -494,6 +497,7 @@ describe(
494497
});
495498
});
496499

500+
// @gate experimental
497501
it('mock Scheduler module to check if `shouldYield` is called', async () => {
498502
// This test reproduces a bug where React's Scheduler task timed out but
499503
// the `shouldYield` method returned true. Usually we try not to mock
@@ -518,7 +522,9 @@ describe(
518522

519523
await ReactNoop.act(async () => {
520524
// Partially render the tree, then yield
521-
ReactNoop.render(<App />);
525+
startTransition(() => {
526+
ReactNoop.render(<App />);
527+
});
522528
expect(Scheduler).toFlushAndYieldThrough(['A']);
523529

524530
// Start logging whenever shouldYield is called
@@ -535,11 +541,17 @@ describe(
535541
// We only check before yielding to the main thread (to avoid starvation
536542
// by other main thread work) or when receiving an update (to avoid
537543
// starvation by incoming updates).
538-
ReactNoop.render(<App />);
544+
startTransition(() => {
545+
ReactNoop.render(<App />);
546+
});
539547

540548
// Because the render expired, React should finish the tree without
541549
// consulting `shouldYield` again
542-
expect(Scheduler).toFlushExpired(['B', 'C']);
550+
if (gate(flags => flags.enableParallelTransitions)) {
551+
expect(Scheduler).toFlushExpired(['B', 'C']);
552+
} else {
553+
expect(Scheduler).toFlushExpired(['B', 'C', 'A', 'B', 'C']);
554+
}
543555
});
544556
});
545557
},

0 commit comments

Comments
 (0)