@samitouri / QOS-React-2 / commits / c581cdd480

Schedule sync updates in microtask (#20872)

* Schedule sync updates in microtask * Updates from review * Fix comment

Ricky committed Feb 25, 2021 at 17:25 UTC c581cdd480f71af6ee87168c72ecabb87b6984c3
23 files changed +121 -51
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+1
@@ -380,6 +380,7 @@ describe('ReactDOMServerPartialHydration', () => {
380 resolve();
381 await promise;
382 Scheduler.unstable_flushAll();
383 + await null;
384 jest.runAllTimers();
385
386 // We should now have hydrated with a ref on the existing span.
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+1
@@ -488,6 +488,7 @@ describe('ReactDOMServerHydration', () => {
488 jest.runAllTimers();
489 await Promise.resolve();
490 Scheduler.unstable_flushAll();
491 + await null;
492 expect(element.textContent).toBe('Hello world');
493 });
494
packages/react-reconciler/src/SchedulerWithReactIntegration.new.js
+13 -5
@@ -16,6 +16,7 @@ import {__interactionsRef} from 'scheduler/tracing';
16 import {
17 enableSchedulerTracing,
18 decoupleUpdatePriorityFromScheduler,
19 + enableSyncMicroTasks,
20 } from 'shared/ReactFeatureFlags';
21 import invariant from 'shared/invariant';
22 import {
@@ -23,6 +24,7 @@ import {
24 getCurrentUpdateLanePriority,
25 setCurrentUpdateLanePriority,
26 } from './ReactFiberLane.new';
27 +import {scheduleMicrotask, supportsMicrotasks} from './ReactFiberHostConfig';
28
29 const {
30 unstable_runWithPriority: Scheduler_runWithPriority,
@@ -144,13 +146,19 @@ export function scheduleSyncCallback(callback: SchedulerCallback) {
146 // the next tick, or earlier if something calls `flushSyncCallbackQueue`.
147 if (syncQueue === null) {
148 syncQueue = [callback];
147 - // Flush the queue in the next tick, at the earliest.
149 +
150 // TODO: Figure out how to remove this It's only here as a last resort if we
151 // forget to explicitly flush.
150 - immediateQueueCallbackNode = Scheduler_scheduleCallback(
151 - Scheduler_ImmediatePriority,
152 - flushSyncCallbackQueueImpl,
153 - );
152 + if (enableSyncMicroTasks && supportsMicrotasks) {
153 + // Flush the queue in a microtask.
154 + scheduleMicrotask(flushSyncCallbackQueueImpl);
155 + } else {
156 + // Flush the queue in the next tick.
157 + immediateQueueCallbackNode = Scheduler_scheduleCallback(
158 + Scheduler_ImmediatePriority,
159 + flushSyncCallbackQueueImpl,
160 + );
161 + }
162 } else {
163 // Push onto existing queue. Don't need to schedule a callback because
164 // we already scheduled one when we created the queue.
packages/react-reconciler/src/SchedulerWithReactIntegration.old.js
+13 -5
@@ -16,6 +16,7 @@ import {__interactionsRef} from 'scheduler/tracing';
16 import {
17 enableSchedulerTracing,
18 decoupleUpdatePriorityFromScheduler,
19 + enableSyncMicroTasks,
20 } from 'shared/ReactFeatureFlags';
21 import invariant from 'shared/invariant';
22 import {
@@ -23,6 +24,7 @@ import {
24 getCurrentUpdateLanePriority,
25 setCurrentUpdateLanePriority,
26 } from './ReactFiberLane.old';
27 +import {scheduleMicrotask, supportsMicrotasks} from './ReactFiberHostConfig';
28
29 const {
30 unstable_runWithPriority: Scheduler_runWithPriority,
@@ -144,13 +146,19 @@ export function scheduleSyncCallback(callback: SchedulerCallback) {
146 // the next tick, or earlier if something calls `flushSyncCallbackQueue`.
147 if (syncQueue === null) {
148 syncQueue = [callback];
147 - // Flush the queue in the next tick, at the earliest.
149 +
150 // TODO: Figure out how to remove this It's only here as a last resort if we
151 // forget to explicitly flush.
150 - immediateQueueCallbackNode = Scheduler_scheduleCallback(
151 - Scheduler_ImmediatePriority,
152 - flushSyncCallbackQueueImpl,
153 - );
152 + if (enableSyncMicroTasks && supportsMicrotasks) {
153 + // Flush the queue in a microtask.
154 + scheduleMicrotask(flushSyncCallbackQueueImpl);
155 + } else {
156 + // Flush the queue in the next tick.
157 + immediateQueueCallbackNode = Scheduler_scheduleCallback(
158 + Scheduler_ImmediatePriority,
159 + flushSyncCallbackQueueImpl,
160 + );
161 + }
162 } else {
163 // Push onto existing queue. Don't need to schedule a callback because
164 // we already scheduled one when we created the queue.
packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+6 -5
@@ -598,12 +598,13 @@ describe('ReactExpiration', () => {
598 // second one.
599 Scheduler.unstable_advanceTime(1000);
600 // Attempt to interrupt with a high pri update.
601 - updateHighPri();
601 + await ReactNoop.act(async () => {
602 + updateHighPri();
603 + });
604
603 - // The first update expired, so first will finish it without
604 - // interrupting. But not the second update, which hasn't expired yet.
605 - expect(Scheduler).toFlushExpired(['Sibling']);
606 - expect(Scheduler).toFlushAndYield([
605 + expect(Scheduler).toHaveYielded([
606 + // The first update expired
607 + 'Sibling',
608 // Then render the high pri update
609 'High pri: 1',
610 'Normal pri: 1',
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+5 -3
@@ -1792,7 +1792,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1792 it(
1793 'in legacy mode, useEffect is deferred and updates finish synchronously ' +
1794 '(in a single batch)',
1795 - () => {
1795 + async () => {
1796 function Counter(props) {
1797 const [count, updateCount] = useState('(empty)');
1798 useEffect(() => {
@@ -1807,10 +1807,12 @@ describe('ReactHooksWithNoopRenderer', () => {
1807 }, [props.count]);
1808 return <Text text={'Count: ' + count} />;
1809 }
1810 - act(() => {
1810 + await act(async () => {
1811 ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
1812 +
1813 // Even in legacy mode, effects are deferred until after paint
1813 - expect(Scheduler).toFlushAndYieldThrough(['Count: (empty)']);
1814 + ReactNoop.flushSync();
1815 + expect(Scheduler).toHaveYielded(['Count: (empty)']);
1816 expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1817 });
1818
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+4
@@ -1400,6 +1400,10 @@ describe('ReactIncrementalErrorHandling', () => {
1400 'BrokenRenderAndUnmount componentWillUnmount',
1401 ]);
1402 expect(ReactNoop.getChildren()).toEqual([]);
1403 +
1404 + expect(() => {
1405 + ReactNoop.flushSync();
1406 + }).toThrow('One does not simply unmount me.');
1407 });
1408
1409 it('does not interrupt unmounting if detaching a ref throws', () => {
packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+4 -1
@@ -98,8 +98,11 @@ describe('ReactOffscreen', () => {
98 <Text text="Outside" />
99 </>,
100 );
101 +
102 + ReactNoop.flushSync();
103 +
104 // Should not defer the hidden tree
102 - expect(Scheduler).toFlushUntilNextPaint(['A', 'Outside']);
105 + expect(Scheduler).toHaveYielded(['A', 'Outside']);
106 });
107 expect(root).toMatchRenderedOutput(
108 <>
packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js
+3 -1
@@ -569,9 +569,11 @@ describe(
569 ReactNoop.render(<App />);
570 });
571
572 + ReactNoop.flushSync();
573 +
574 // Because the render expired, React should finish the tree without
575 // consulting `shouldYield` again
574 - expect(Scheduler).toFlushExpired(['B', 'C']);
576 + expect(Scheduler).toHaveYielded(['B', 'C']);
577 });
578 });
579 },
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+8 -4
@@ -292,9 +292,11 @@ describe('ReactSuspenseList', () => {
292 </>,
293 );
294
295 - await C.resolve();
295 + await ReactNoop.act(async () => {
296 + C.resolve();
297 + });
298
297 - expect(Scheduler).toFlushAndYield(['C']);
299 + expect(Scheduler).toHaveYielded(['C']);
300
301 expect(ReactNoop).toMatchRenderedOutput(
302 <>
@@ -304,9 +306,11 @@ describe('ReactSuspenseList', () => {
306 </>,
307 );
308
307 - await B.resolve();
309 + await ReactNoop.act(async () => {
310 + B.resolve();
311 + });
312
309 - expect(Scheduler).toFlushAndYield(['B']);
313 + expect(Scheduler).toHaveYielded(['B']);
314
315 expect(ReactNoop).toMatchRenderedOutput(
316 <>
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+10 -4
@@ -310,7 +310,7 @@ describe('ReactSuspensePlaceholder', () => {
310 });
311
312 describe('when suspending during mount', () => {
313 - it('properly accounts for base durations when a suspended times out in a legacy tree', () => {
313 + it('properly accounts for base durations when a suspended times out in a legacy tree', async () => {
314 ReactNoop.renderLegacySyncRoot(<App shouldSuspend={true} />);
315 expect(Scheduler).toHaveYielded([
316 'App',
@@ -331,7 +331,10 @@ describe('ReactSuspensePlaceholder', () => {
331 jest.advanceTimersByTime(1000);
332
333 expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
334 - expect(Scheduler).toFlushExpired(['Loaded']);
334 +
335 + ReactNoop.flushSync();
336 +
337 + expect(Scheduler).toHaveYielded(['Loaded']);
338 expect(ReactNoop).toMatchRenderedOutput('LoadedText');
339 expect(onRender).toHaveBeenCalledTimes(2);
340
@@ -378,7 +381,7 @@ describe('ReactSuspensePlaceholder', () => {
381 });
382
383 describe('when suspending during update', () => {
381 - it('properly accounts for base durations when a suspended times out in a legacy tree', () => {
384 + it('properly accounts for base durations when a suspended times out in a legacy tree', async () => {
385 ReactNoop.renderLegacySyncRoot(
386 <App shouldSuspend={false} textRenderDuration={5} />,
387 );
@@ -427,7 +430,10 @@ describe('ReactSuspensePlaceholder', () => {
430 jest.advanceTimersByTime(1000);
431
432 expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
430 - expect(Scheduler).toFlushExpired(['Loaded']);
433 +
434 + ReactNoop.flushSync();
435 +
436 + expect(Scheduler).toHaveYielded(['Loaded']);
437 expect(ReactNoop).toMatchRenderedOutput('LoadedNew');
438 expect(onRender).toHaveBeenCalledTimes(4);
439
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+36 -18
@@ -1088,9 +1088,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1088 expect(Scheduler).toHaveYielded(['Suspend! [Result]', 'Loading...']);
1089 expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1090
1091 - await resolveText('Result');
1091 + await ReactNoop.act(async () => {
1092 + resolveText('Result');
1093 + });
1094
1093 - expect(Scheduler).toFlushExpired(['Result']);
1095 + expect(Scheduler).toHaveYielded(['Result']);
1096 expect(ReactNoop.getChildren()).toEqual([span('Result')]);
1097 });
1098
@@ -1156,8 +1158,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1158 </>,
1159 );
1160
1159 - await resolveText('Step: 2');
1160 - expect(Scheduler).toFlushExpired(['Step: 2']);
1161 + await ReactNoop.act(async () => {
1162 + resolveText('Step: 2');
1163 + });
1164 + expect(Scheduler).toHaveYielded(['Step: 2']);
1165 expect(ReactNoop).toMatchRenderedOutput(
1166 <>
1167 <span prop="Step: 2" />
@@ -1227,9 +1231,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1231 </>,
1232 );
1233
1230 - await resolveText('B');
1234 + await ReactNoop.act(async () => {
1235 + resolveText('B');
1236 + });
1237
1232 - expect(Scheduler).toFlushExpired(['B']);
1238 + expect(Scheduler).toHaveYielded(['B']);
1239 expect(ReactNoop).toMatchRenderedOutput(
1240 <>
1241 <span prop="A" />
@@ -1271,9 +1277,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1277 ]);
1278 expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1279
1274 - await resolveText('Hi');
1280 + await ReactNoop.act(async () => {
1281 + resolveText('Hi');
1282 + });
1283
1276 - expect(Scheduler).toFlushExpired([
1284 + expect(Scheduler).toHaveYielded([
1285 'constructor',
1286 'Hi',
1287 'componentDidMount',
@@ -1316,8 +1324,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1324 'Loading...',
1325 ]);
1326 expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1319 - await resolveText('Hi');
1320 - expect(Scheduler).toFlushExpired(['Hi']);
1327 + await ReactNoop.act(async () => {
1328 + resolveText('Hi');
1329 + });
1330 + expect(Scheduler).toHaveYielded(['Hi']);
1331 expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1332 });
1333
@@ -1360,8 +1370,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1370 </>,
1371 ]);
1372
1363 - await resolveText('Hi');
1364 - expect(Scheduler).toFlushExpired(['Hi']);
1373 + await ReactNoop.act(async () => {
1374 + resolveText('Hi');
1375 + });
1376 + expect(Scheduler).toHaveYielded(['Hi']);
1377 });
1378 } else {
1379 // @gate enableCache
@@ -1401,9 +1413,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1413 'Child is hidden: true',
1414 ]);
1415
1404 - await resolveText('Hi');
1416 + await ReactNoop.act(async () => {
1417 + resolveText('Hi');
1418 + });
1419
1406 - expect(Scheduler).toFlushExpired(['Hi']);
1420 + expect(Scheduler).toHaveYielded(['Hi']);
1421 });
1422 }
1423
@@ -1647,9 +1661,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1661 </>,
1662 );
1663
1650 - await resolveText('B');
1664 + await ReactNoop.act(async () => {
1665 + resolveText('B');
1666 + });
1667
1652 - expect(Scheduler).toFlushAndYield([
1668 + expect(Scheduler).toHaveYielded([
1669 'B',
1670 'Destroy Layout Effect [Loading...]',
1671 'Layout Effect [B]',
@@ -1681,9 +1697,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1697 'Effect [Loading...]',
1698 ]);
1699
1684 - await resolveText('B2');
1700 + await ReactNoop.act(async () => {
1701 + resolveText('B2');
1702 + });
1703
1686 - expect(Scheduler).toFlushAndYield([
1704 + expect(Scheduler).toHaveYielded([
1705 'B2',
1706 'Destroy Layout Effect [Loading...]',
1707 'Destroy Layout Effect [B]',
packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
+6 -5
@@ -1794,7 +1794,7 @@ describe('useMutableSource', () => {
1794 });
1795
1796 // @gate experimental
1797 - it('should not misidentify mutations after render as side effects', () => {
1797 + it('should not misidentify mutations after render as side effects', async () => {
1798 const source = createSource('initial');
1799 const mutableSource = createMutableSource(
1800 source,
@@ -1811,15 +1811,16 @@ describe('useMutableSource', () => {
1811 return null;
1812 }
1813
1814 - act(() => {
1814 + await act(async () => {
1815 ReactNoop.renderLegacySyncRoot(
1816 <React.StrictMode>
1817 <MutateDuringRead />
1818 </React.StrictMode>,
1819 );
1820 - expect(Scheduler).toFlushAndYieldThrough([
1821 - 'MutateDuringRead:initial',
1822 - ]);
1820 + });
1821 + expect(Scheduler).toHaveYielded(['MutateDuringRead:initial']);
1822 +
1823 + await act(async () => {
1824 source.value = 'updated';
1825 });
1826 expect(Scheduler).toHaveYielded(['MutateDuringRead:updated']);
packages/shared/ReactFeatureFlags.js
+2
@@ -155,4 +155,6 @@ export const disableSchedulerTimeoutInWorkLoop = false;
155
156 export const enableDiscreteEventMicroTasks = false;
157
158 +export const enableSyncMicroTasks = false;
159 +
160 export const enableNativeEventPriorityInference = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
59 export const enableRecursiveCommitTraversal = false;
60 export const disableSchedulerTimeoutInWorkLoop = false;
61 export const enableDiscreteEventMicroTasks = false;
62 +export const enableSyncMicroTasks = false;
63 export const enableNativeEventPriorityInference = false;
64
65 // Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58 export const enableRecursiveCommitTraversal = false;
59 export const disableSchedulerTimeoutInWorkLoop = false;
60 export const enableDiscreteEventMicroTasks = false;
61 +export const enableSyncMicroTasks = false;
62 export const enableNativeEventPriorityInference = false;
63
64 // Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58 export const enableRecursiveCommitTraversal = false;
59 export const disableSchedulerTimeoutInWorkLoop = false;
60 export const enableDiscreteEventMicroTasks = false;
61 +export const enableSyncMicroTasks = false;
62 export const enableNativeEventPriorityInference = false;
63
64 // Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58 export const enableRecursiveCommitTraversal = false;
59 export const disableSchedulerTimeoutInWorkLoop = false;
60 export const enableDiscreteEventMicroTasks = false;
61 +export const enableSyncMicroTasks = false;
62 export const enableNativeEventPriorityInference = false;
63
64 // Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58 export const enableRecursiveCommitTraversal = false;
59 export const disableSchedulerTimeoutInWorkLoop = false;
60 export const enableDiscreteEventMicroTasks = false;
61 +export const enableSyncMicroTasks = false;
62 export const enableNativeEventPriorityInference = false;
63
64 // Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58 export const enableRecursiveCommitTraversal = false;
59 export const disableSchedulerTimeoutInWorkLoop = false;
60 export const enableDiscreteEventMicroTasks = false;
61 +export const enableSyncMicroTasks = false;
62 export const enableNativeEventPriorityInference = false;
63
64 // Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58 export const enableRecursiveCommitTraversal = false;
59 export const disableSchedulerTimeoutInWorkLoop = false;
60 export const enableDiscreteEventMicroTasks = false;
61 +export const enableSyncMicroTasks = false;
62 export const enableNativeEventPriorityInference = false;
63
64 // Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -57,4 +57,5 @@ export const enableUseRefAccessWarning = __VARIANT__;
57 export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
58 export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
59 export const enableDiscreteEventMicroTasks = __VARIANT__;
60 +export const enableSyncMicroTasks = __VARIANT__;
61 export const enableNativeEventPriorityInference = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -33,6 +33,7 @@ export const {
33 disableNativeComponentFrames,
34 disableSchedulerTimeoutInWorkLoop,
35 enableDiscreteEventMicroTasks,
36 + enableSyncMicroTasks,
37 enableNativeEventPriorityInference,
38 } = dynamicFeatureFlags;
39