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

Queue discrete events in microtask (#20669)

* Queue discrete events in microtask * Use callback priority to determine cancellation * Add queueMicrotask to react-reconciler README * Fix invatiant conditon for InputDiscrete * Switch invariant null check * Convert invariant to warning * Remove warning from codes.json

Ricky committed Jan 27, 2021 at 18:24 UTC e51bd6c1fa2731c4fcd39300144e917aedfc989b
18 files changed +111 -27
packages/react-dom/src/client/ReactDOMHostConfig.js
+1 -1
@@ -392,7 +392,7 @@ export const queueMicrotask: any =
392 Promise.resolve(null)
393 .then(callback)
394 .catch(handleErrorInNextTick)
395 - : scheduleTimeout;
395 + : scheduleTimeout; // TODO: Determine the best fallback here.
396
397 function handleErrorInNextTick(error) {
398 setTimeout(() => {
packages/react-dom/src/events/plugins/__tests__/ChangeEventPlugin-test.js
+9 -2
@@ -730,8 +730,15 @@ describe('ChangeEventPlugin', () => {
730
731 // Flush callbacks.
732 // Now the click update has flushed.
733 - expect(Scheduler).toFlushAndYield(['render: ']);
734 - expect(input.value).toBe('');
733 + if (gate(flags => flags.enableDiscreteEventMicroTasks)) {
734 + // Flush microtask queue.
735 + await null;
736 + expect(Scheduler).toHaveYielded(['render: ']);
737 + expect(input.value).toBe('');
738 + } else {
739 + expect(Scheduler).toFlushAndYield(['render: ']);
740 + expect(input.value).toBe('');
741 + }
742 });
743
744 // @gate experimental
packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js
+18 -5
@@ -470,11 +470,24 @@ describe('SimpleEventPlugin', function() {
470 'High-pri count: 7, Low-pri count: 0',
471 ]);
472
473 - // At the end, both counters should equal the total number of clicks
474 - expect(Scheduler).toFlushAndYield([
475 - 'High-pri count: 8, Low-pri count: 0',
476 - 'High-pri count: 8, Low-pri count: 8',
477 - ]);
473 + if (gate(flags => flags.enableDiscreteEventMicroTasks)) {
474 + // Flush the microtask queue
475 + await null;
476 +
477 + // At the end, both counters should equal the total number of clicks
478 + expect(Scheduler).toHaveYielded([
479 + 'High-pri count: 8, Low-pri count: 0',
480 +
481 + // TODO: with cancellation, this required another flush?
482 + 'High-pri count: 8, Low-pri count: 8',
483 + ]);
484 + } else {
485 + // At the end, both counters should equal the total number of clicks
486 + expect(Scheduler).toFlushAndYield([
487 + 'High-pri count: 8, Low-pri count: 0',
488 + 'High-pri count: 8, Low-pri count: 8',
489 + ]);
490 + }
491 expect(button.textContent).toEqual('High-pri count: 8, Low-pri count: 8');
492 });
493 });
packages/react-reconciler/README.md
+4
@@ -203,6 +203,10 @@ You can proxy this to `clearTimeout` or its equivalent in your environment.
203
204 This is a property (not a function) that should be set to something that can never be a valid timeout ID. For example, you can set it to `-1`.
205
206 +#### `queueMicrotask(fn)`
207 +
208 +You can proxy this to `queueMicrotask` or its equivalent in your environment.
209 +
210 #### `isPrimaryRenderer`
211
212 This is a property (not a function) that should be set to `true` if your renderer is the main one on the page. For example, if you're writing a renderer for the Terminal, it makes sense to set it to `true`, but if your renderer is used *on top of* React DOM or some other existing renderer, set it to `false`.
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+33 -9
@@ -92,6 +92,7 @@ import {
92 warnsIfNotActing,
93 afterActiveInstanceBlur,
94 clearContainer,
95 + queueMicrotask,
96 } from './ReactFiberHostConfig';
97
98 import {
@@ -216,6 +217,7 @@ import {
217 syncNestedUpdateFlag,
218 } from './ReactProfilerTimer.new';
219
220 +import {enableDiscreteEventMicroTasks} from 'shared/ReactFeatureFlags';
221 // DEV stuff
222 import getComponentName from 'shared/getComponentName';
223 import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
@@ -714,21 +716,34 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
716 // Special case: There's nothing to work on.
717 if (existingCallbackNode !== null) {
718 cancelCallback(existingCallbackNode);
717 - root.callbackNode = null;
718 - root.callbackPriority = NoLanePriority;
719 }
720 + root.callbackNode = null;
721 + root.callbackPriority = NoLanePriority;
722 return;
723 }
724
725 // Check if there's an existing task. We may be able to reuse it.
724 - if (existingCallbackNode !== null) {
725 - const existingCallbackPriority = root.callbackPriority;
726 - if (existingCallbackPriority === newCallbackPriority) {
727 - // The priority hasn't changed. We can reuse the existing task. Exit.
728 - return;
726 + const existingCallbackPriority = root.callbackPriority;
727 + if (existingCallbackPriority === newCallbackPriority) {
728 + if (__DEV__) {
729 + // If we're going to re-use an existing task, it needs to exist.
730 + // Assume that discrete update microtasks are non-cancellable and null.
731 + // TODO: Temporary until we confirm this warning is not fired.
732 + if (
733 + existingCallbackNode == null &&
734 + existingCallbackPriority !== InputDiscreteLanePriority
735 + ) {
736 + console.error(
737 + 'Expected scheduled callback to exist. This error is likely caused by a bug in React. Please file an issue.',
738 + );
739 + }
740 }
730 - // The priority changed. Cancel the existing callback. We'll schedule a new
731 - // one below.
741 + // The priority hasn't changed. We can reuse the existing task. Exit.
742 + return;
743 + }
744 +
745 + if (existingCallbackNode != null) {
746 + // Cancel the existing callback. We'll schedule a new one below.
747 cancelCallback(existingCallbackNode);
748 }
749
@@ -737,6 +752,8 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
752 if (newCallbackPriority === SyncLanePriority) {
753 // Special case: Sync React callbacks are scheduled on a special
754 // internal queue
755 +
756 + // TODO: After enableDiscreteEventMicroTasks lands, we can remove the fake node.
757 newCallbackNode = scheduleSyncCallback(
758 performSyncWorkOnRoot.bind(null, root),
759 );
@@ -745,6 +762,12 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
762 ImmediateSchedulerPriority,
763 performSyncWorkOnRoot.bind(null, root),
764 );
765 + } else if (
766 + enableDiscreteEventMicroTasks &&
767 + newCallbackPriority === InputDiscreteLanePriority
768 + ) {
769 + queueMicrotask(performSyncWorkOnRoot.bind(null, root));
770 + newCallbackNode = null;
771 } else {
772 const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
773 newCallbackPriority,
@@ -1871,6 +1894,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1894 // commitRoot never returns a continuation; it always finishes synchronously.
1895 // So we can clear these now to allow a new callback to be scheduled.
1896 root.callbackNode = null;
1897 + root.callbackPriority = NoLanePriority;
1898
1899 // Update the first and last pending times on this root. The new first
1900 // pending time is whatever is left on the root fiber.
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+33 -9
@@ -34,6 +34,7 @@ import {
34 disableSchedulerTimeoutInWorkLoop,
35 enableDoubleInvokingEffects,
36 skipUnmountedBoundaries,
37 + enableDiscreteEventMicroTasks,
38 } from 'shared/ReactFeatureFlags';
39 import ReactSharedInternals from 'shared/ReactSharedInternals';
40 import invariant from 'shared/invariant';
@@ -92,6 +93,7 @@ import {
93 warnsIfNotActing,
94 afterActiveInstanceBlur,
95 clearContainer,
96 + queueMicrotask,
97 } from './ReactFiberHostConfig';
98
99 import {
@@ -696,21 +698,34 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
698 // Special case: There's nothing to work on.
699 if (existingCallbackNode !== null) {
700 cancelCallback(existingCallbackNode);
699 - root.callbackNode = null;
700 - root.callbackPriority = NoLanePriority;
701 }
702 + root.callbackNode = null;
703 + root.callbackPriority = NoLanePriority;
704 return;
705 }
706
707 // Check if there's an existing task. We may be able to reuse it.
706 - if (existingCallbackNode !== null) {
707 - const existingCallbackPriority = root.callbackPriority;
708 - if (existingCallbackPriority === newCallbackPriority) {
709 - // The priority hasn't changed. We can reuse the existing task. Exit.
710 - return;
708 + const existingCallbackPriority = root.callbackPriority;
709 + if (existingCallbackPriority === newCallbackPriority) {
710 + if (__DEV__) {
711 + // If we're going to re-use an existing task, it needs to exist.
712 + // Assume that discrete update microtasks are non-cancellable and null.
713 + // TODO: Temporary until we confirm this warning is not fired.
714 + if (
715 + existingCallbackNode == null &&
716 + existingCallbackPriority !== InputDiscreteLanePriority
717 + ) {
718 + console.error(
719 + 'Expected scheduled callback to exist. This error is likely caused by a bug in React. Please file an issue.',
720 + );
721 + }
722 }
712 - // The priority changed. Cancel the existing callback. We'll schedule a new
713 - // one below.
723 + // The priority hasn't changed. We can reuse the existing task. Exit.
724 + return;
725 + }
726 +
727 + if (existingCallbackNode != null) {
728 + // Cancel the existing callback. We'll schedule a new one below.
729 cancelCallback(existingCallbackNode);
730 }
731
@@ -719,6 +734,8 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
734 if (newCallbackPriority === SyncLanePriority) {
735 // Special case: Sync React callbacks are scheduled on a special
736 // internal queue
737 +
738 + // TODO: After enableDiscreteEventMicroTasks lands, we can remove the fake node.
739 newCallbackNode = scheduleSyncCallback(
740 performSyncWorkOnRoot.bind(null, root),
741 );
@@ -727,6 +744,12 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
744 ImmediateSchedulerPriority,
745 performSyncWorkOnRoot.bind(null, root),
746 );
747 + } else if (
748 + enableDiscreteEventMicroTasks &&
749 + newCallbackPriority === InputDiscreteLanePriority
750 + ) {
751 + queueMicrotask(performSyncWorkOnRoot.bind(null, root));
752 + newCallbackNode = null;
753 } else {
754 const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
755 newCallbackPriority,
@@ -1851,6 +1874,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1874 // commitRoot never returns a continuation; it always finishes synchronously.
1875 // So we can clear these now to allow a new callback to be scheduled.
1876 root.callbackNode = null;
1877 + root.callbackPriority = NoLanePriority;
1878
1879 // Update the first and last pending times on this root. The new first
1880 // pending time is whatever is left on the root fiber.
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+1
@@ -3528,6 +3528,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3528 });
3529
3530 // @gate enableCache
3531 + // @gate !enableDiscreteEventMicroTasks
3532 it('regression: empty render at high priority causes update to be dropped', async () => {
3533 // Reproduces a bug where flushDiscreteUpdates starts a new (empty) render
3534 // pass which cancels a scheduled timeout and causes the fallback never to
packages/react-test-renderer/src/ReactTestHostConfig.js
+1 -1
@@ -228,7 +228,7 @@ export const queueMicrotask =
228 Promise.resolve(null)
229 .then(callback)
230 .catch(handleErrorInNextTick)
231 - : scheduleTimeout;
231 + : scheduleTimeout; // TODO: Determine the best fallback here.
232
233 function handleErrorInNextTick(error) {
234 setTimeout(() => {
packages/shared/ReactFeatureFlags.js
+2
@@ -152,3 +152,5 @@ export const disableSchedulerTimeoutInWorkLoop = false;
152
153 // Experiment to simplify/improve how transitions are scheduled
154 export const enableTransitionEntanglement = false;
155 +
156 +export const enableDiscreteEventMicroTasks = 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 enableTransitionEntanglement = false;
62 +export const enableDiscreteEventMicroTasks = false;
63
64 // Flow magic to verify the exports of this file match the original version.
65 // eslint-disable-next-line no-unused-vars
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 enableTransitionEntanglement = false;
61 +export const enableDiscreteEventMicroTasks = false;
62
63 // Flow magic to verify the exports of this file match the original version.
64 // eslint-disable-next-line no-unused-vars
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 enableTransitionEntanglement = false;
61 +export const enableDiscreteEventMicroTasks = false;
62
63 // Flow magic to verify the exports of this file match the original version.
64 // eslint-disable-next-line no-unused-vars
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 enableTransitionEntanglement = false;
61 +export const enableDiscreteEventMicroTasks = false;
62
63 // Flow magic to verify the exports of this file match the original version.
64 // eslint-disable-next-line no-unused-vars
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 enableTransitionEntanglement = false;
61 +export const enableDiscreteEventMicroTasks = false;
62
63 // Flow magic to verify the exports of this file match the original version.
64 // eslint-disable-next-line no-unused-vars
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 enableTransitionEntanglement = false;
61 +export const enableDiscreteEventMicroTasks = false;
62
63 // Flow magic to verify the exports of this file match the original version.
64 // eslint-disable-next-line no-unused-vars
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 enableTransitionEntanglement = false;
61 +export const enableDiscreteEventMicroTasks = false;
62
63 // Flow magic to verify the exports of this file match the original version.
64 // eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -56,3 +56,4 @@ export const enableUseRefAccessWarning = __VARIANT__;
56 export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
57 export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
58 export const enableTransitionEntanglement = __VARIANT__;
59 +export const enableDiscreteEventMicroTasks = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -32,6 +32,7 @@ export const {
32 disableNativeComponentFrames,
33 disableSchedulerTimeoutInWorkLoop,
34 enableTransitionEntanglement,
35 + enableDiscreteEventMicroTasks,
36 } = dynamicFeatureFlags;
37
38 // On WWW, __EXPERIMENTAL__ is used for a new modern build.