Skip to content

Commit 933880b

Browse files
authored
Make time-slicing opt-in (#21072)
* Add enableSyncDefaultUpdates feature flag * Add enableSyncDefaultUpdates implementation * Fix tests * Switch feature flag to true by default * Finish concurrent render whenever for non-sync lanes * Also return DefaultLane with eventLane * Gate interruption test * Add continuout native event test * Fix tests from rebasing main * Hardcode lanes, remove added export * Sync forks
1 parent b0407b5 commit 933880b

File tree

46 files changed

+1996
-538
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1996
-538
lines changed

packages/create-subscription/src/__tests__/createSubscription-test.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ describe('createSubscription', () => {
268268
expect(Scheduler).toFlushAndYield(['b-1']);
269269
});
270270

271+
// @gate experimental || !enableSyncDefaultUpdates
271272
it('should ignore values emitted by a new subscribable until the commit phase', () => {
272273
const log = [];
273274

@@ -325,7 +326,13 @@ describe('createSubscription', () => {
325326
expect(log).toEqual(['Parent.componentDidMount']);
326327

327328
// Start React update, but don't finish
328-
ReactNoop.render(<Parent observed={observableB} />);
329+
if (gate(flags => flags.enableSyncDefaultUpdates)) {
330+
React.unstable_startTransition(() => {
331+
ReactNoop.render(<Parent observed={observableB} />);
332+
});
333+
} else {
334+
ReactNoop.render(<Parent observed={observableB} />);
335+
}
329336
expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']);
330337
expect(log).toEqual(['Parent.componentDidMount']);
331338

@@ -355,6 +362,7 @@ describe('createSubscription', () => {
355362
]);
356363
});
357364

365+
// @gate experimental || !enableSyncDefaultUpdates
358366
it('should not drop values emitted between updates', () => {
359367
const log = [];
360368

@@ -412,7 +420,13 @@ describe('createSubscription', () => {
412420
expect(log).toEqual(['Parent.componentDidMount']);
413421

414422
// Start React update, but don't finish
415-
ReactNoop.render(<Parent observed={observableB} />);
423+
if (gate(flags => flags.enableSyncDefaultUpdates)) {
424+
React.unstable_startTransition(() => {
425+
ReactNoop.render(<Parent observed={observableB} />);
426+
});
427+
} else {
428+
ReactNoop.render(<Parent observed={observableB} />);
429+
}
416430
expect(Scheduler).toFlushAndYieldThrough(['Subscriber: b-0']);
417431
expect(log).toEqual(['Parent.componentDidMount']);
418432

packages/react-art/src/__tests__/ReactART-test.js

+1
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ describe('ReactART', () => {
360360
expect(onClick2).toBeCalled();
361361
});
362362

363+
// @gate !enableSyncDefaultUpdates
363364
it('can concurrently render with a "primary" renderer while sharing context', () => {
364365
const CurrentRendererContext = React.createContext(null);
365366

packages/react-dom/src/__tests__/ReactDOMNativeEventHeuristic-test.js

+40
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,46 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
284284
expect(container.textContent).toEqual('hovered');
285285
});
286286

287+
// @gate experimental
288+
it('continuous native events flush as expected', async () => {
289+
const root = ReactDOM.unstable_createRoot(container);
290+
291+
const target = React.createRef(null);
292+
function Foo({hovered}) {
293+
const hoverString = hovered ? 'hovered' : 'not hovered';
294+
Scheduler.unstable_yieldValue(hoverString);
295+
return <div ref={target}>{hoverString}</div>;
296+
}
297+
298+
await act(async () => {
299+
root.render(<Foo hovered={false} />);
300+
});
301+
expect(container.textContent).toEqual('not hovered');
302+
303+
await act(async () => {
304+
// Note: React does not use native mouseenter/mouseleave events
305+
// but we should still correctly determine their priority.
306+
const mouseEnterEvent = document.createEvent('MouseEvents');
307+
mouseEnterEvent.initEvent('mouseover', true, true);
308+
target.current.addEventListener('mouseover', () => {
309+
root.render(<Foo hovered={true} />);
310+
});
311+
dispatchAndSetCurrentEvent(target.current, mouseEnterEvent);
312+
313+
// Since mouse end is not discrete, should not have updated yet
314+
expect(Scheduler).toHaveYielded(['not hovered']);
315+
expect(container.textContent).toEqual('not hovered');
316+
317+
expect(Scheduler).toFlushAndYieldThrough(['hovered']);
318+
if (gate(flags => flags.enableSyncDefaultUpdates)) {
319+
expect(container.textContent).toEqual('hovered');
320+
} else {
321+
expect(container.textContent).toEqual('not hovered');
322+
}
323+
});
324+
expect(container.textContent).toEqual('hovered');
325+
});
326+
287327
// @gate experimental
288328
it('should batch inside native events', async () => {
289329
const root = ReactDOM.unstable_createRoot(container);

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -1865,11 +1865,21 @@ describe('ReactDOMServerPartialHydration', () => {
18651865
suspend = true;
18661866

18671867
await act(async () => {
1868-
root.render(<App />);
1869-
expect(Scheduler).toFlushAndYieldThrough(['Before']);
1870-
// This took a long time to render.
1871-
Scheduler.unstable_advanceTime(1000);
1872-
expect(Scheduler).toFlushAndYield(['After']);
1868+
if (gate(flags => flags.enableSyncDefaultUpdates)) {
1869+
React.unstable_startTransition(() => {
1870+
root.render(<App />);
1871+
});
1872+
1873+
expect(Scheduler).toFlushAndYieldThrough(['Before', 'After']);
1874+
} else {
1875+
root.render(<App />);
1876+
1877+
expect(Scheduler).toFlushAndYieldThrough(['Before']);
1878+
// This took a long time to render.
1879+
Scheduler.unstable_advanceTime(1000);
1880+
expect(Scheduler).toFlushAndYield(['After']);
1881+
}
1882+
18731883
// This will cause us to skip the second row completely.
18741884
});
18751885

packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -1934,7 +1934,13 @@ describe('DOMPluginEventSystem', () => {
19341934
log.length = 0;
19351935

19361936
// Increase counter
1937-
root.render(<Test counter={1} />);
1937+
if (gate(flags => flags.enableSyncDefaultUpdates)) {
1938+
React.unstable_startTransition(() => {
1939+
root.render(<Test counter={1} />);
1940+
});
1941+
} else {
1942+
root.render(<Test counter={1} />);
1943+
}
19381944
// Yield before committing
19391945
expect(Scheduler).toFlushAndYieldThrough(['Test']);
19401946

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const NoLane: Lane = /* */ 0b0000000000000000000
5252

5353
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
5454

55-
const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
55+
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
5656
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
5757

5858
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const NoLane: Lane = /* */ 0b0000000000000000000
5252

5353
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
5454

55-
const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
55+
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
5656
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
5757

5858
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;

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

+34-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
disableSchedulerTimeoutInWorkLoop,
3333
enableStrictEffects,
3434
skipUnmountedBoundaries,
35+
enableSyncDefaultUpdates,
3536
enableUpdaterTracking,
3637
} from 'shared/ReactFeatureFlags';
3738
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -138,6 +139,10 @@ import {
138139
NoLanes,
139140
NoLane,
140141
SyncLane,
142+
DefaultLane,
143+
DefaultHydrationLane,
144+
InputContinuousLane,
145+
InputContinuousHydrationLane,
141146
NoTimestamp,
142147
claimNextTransitionLane,
143148
claimNextRetryLane,
@@ -433,6 +438,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
433438
// TODO: Move this type conversion to the event priority module.
434439
const updateLane: Lane = (getCurrentUpdatePriority(): any);
435440
if (updateLane !== NoLane) {
441+
if (
442+
enableSyncDefaultUpdates &&
443+
(updateLane === InputContinuousLane ||
444+
updateLane === InputContinuousHydrationLane)
445+
) {
446+
return DefaultLane;
447+
}
436448
return updateLane;
437449
}
438450

@@ -443,6 +455,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
443455
// use that directly.
444456
// TODO: Move this type conversion to the event priority module.
445457
const eventLane: Lane = (getCurrentEventPriority(): any);
458+
if (
459+
enableSyncDefaultUpdates &&
460+
(eventLane === InputContinuousLane ||
461+
eventLane === InputContinuousHydrationLane)
462+
) {
463+
return DefaultLane;
464+
}
446465
return eventLane;
447466
}
448467

@@ -695,7 +714,16 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
695714

696715
// Schedule a new callback.
697716
let newCallbackNode;
698-
if (newCallbackPriority === SyncLane) {
717+
if (
718+
enableSyncDefaultUpdates &&
719+
(newCallbackPriority === DefaultLane ||
720+
newCallbackPriority === DefaultHydrationLane)
721+
) {
722+
newCallbackNode = scheduleCallback(
723+
ImmediateSchedulerPriority,
724+
performSyncWorkOnRoot.bind(null, root),
725+
);
726+
} else if (newCallbackPriority === SyncLane) {
699727
// Special case: Sync React callbacks are scheduled on a special
700728
// internal queue
701729
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
@@ -1030,7 +1058,11 @@ function performSyncWorkOnRoot(root) {
10301058
const finishedWork: Fiber = (root.current.alternate: any);
10311059
root.finishedWork = finishedWork;
10321060
root.finishedLanes = lanes;
1033-
commitRoot(root);
1061+
if (enableSyncDefaultUpdates && !includesSomeLane(lanes, SyncLane)) {
1062+
finishConcurrentRender(root, exitStatus, lanes);
1063+
} else {
1064+
commitRoot(root);
1065+
}
10341066

10351067
// Before exiting, make sure there's a callback scheduled for the next
10361068
// pending level.

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

+34-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
disableSchedulerTimeoutInWorkLoop,
3333
enableStrictEffects,
3434
skipUnmountedBoundaries,
35+
enableSyncDefaultUpdates,
3536
enableUpdaterTracking,
3637
} from 'shared/ReactFeatureFlags';
3738
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -138,6 +139,10 @@ import {
138139
NoLanes,
139140
NoLane,
140141
SyncLane,
142+
DefaultLane,
143+
DefaultHydrationLane,
144+
InputContinuousLane,
145+
InputContinuousHydrationLane,
141146
NoTimestamp,
142147
claimNextTransitionLane,
143148
claimNextRetryLane,
@@ -433,6 +438,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
433438
// TODO: Move this type conversion to the event priority module.
434439
const updateLane: Lane = (getCurrentUpdatePriority(): any);
435440
if (updateLane !== NoLane) {
441+
if (
442+
enableSyncDefaultUpdates &&
443+
(updateLane === InputContinuousLane ||
444+
updateLane === InputContinuousHydrationLane)
445+
) {
446+
return DefaultLane;
447+
}
436448
return updateLane;
437449
}
438450

@@ -443,6 +455,13 @@ export function requestUpdateLane(fiber: Fiber): Lane {
443455
// use that directly.
444456
// TODO: Move this type conversion to the event priority module.
445457
const eventLane: Lane = (getCurrentEventPriority(): any);
458+
if (
459+
enableSyncDefaultUpdates &&
460+
(eventLane === InputContinuousLane ||
461+
eventLane === InputContinuousHydrationLane)
462+
) {
463+
return DefaultLane;
464+
}
446465
return eventLane;
447466
}
448467

@@ -695,7 +714,16 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
695714

696715
// Schedule a new callback.
697716
let newCallbackNode;
698-
if (newCallbackPriority === SyncLane) {
717+
if (
718+
enableSyncDefaultUpdates &&
719+
(newCallbackPriority === DefaultLane ||
720+
newCallbackPriority === DefaultHydrationLane)
721+
) {
722+
newCallbackNode = scheduleCallback(
723+
ImmediateSchedulerPriority,
724+
performSyncWorkOnRoot.bind(null, root),
725+
);
726+
} else if (newCallbackPriority === SyncLane) {
699727
// Special case: Sync React callbacks are scheduled on a special
700728
// internal queue
701729
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
@@ -1030,7 +1058,11 @@ function performSyncWorkOnRoot(root) {
10301058
const finishedWork: Fiber = (root.current.alternate: any);
10311059
root.finishedWork = finishedWork;
10321060
root.finishedLanes = lanes;
1033-
commitRoot(root);
1061+
if (enableSyncDefaultUpdates && !includesSomeLane(lanes, SyncLane)) {
1062+
finishConcurrentRender(root, exitStatus, lanes);
1063+
} else {
1064+
commitRoot(root);
1065+
}
10341066

10351067
// Before exiting, make sure there's a callback scheduled for the next
10361068
// pending level.

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ describe('ReactSuspenseList', () => {
4545
return Component;
4646
}
4747

48+
// @gate experimental || !enableSyncDefaultUpdates
4849
it('appends rendering tasks to the end of the priority queue', async () => {
4950
const A = createAsyncText('A');
5051
const B = createAsyncText('B');
@@ -63,7 +64,13 @@ describe('ReactSuspenseList', () => {
6364
root.render(<App show={false} />);
6465
expect(Scheduler).toFlushAndYield([]);
6566

66-
root.render(<App show={true} />);
67+
if (gate(flags => flags.enableSyncDefaultUpdates)) {
68+
React.unstable_startTransition(() => {
69+
root.render(<App show={true} />);
70+
});
71+
} else {
72+
root.render(<App show={true} />);
73+
}
6774
expect(Scheduler).toFlushAndYield([
6875
'Suspend! [A]',
6976
'Suspend! [B]',

0 commit comments

Comments
 (0)