Repository: facebook/react
Pull request: [Scheduler] Always yield to native macro tasks when a virtual task completes

As an alternative to #31784.

We should really just always yield each virtual task to a native task. So that it's 1:1 with native tasks. This affects when microtasks within each task happens. This brings us closer to native `postTask` semantics which makes it more seamless to just use that when available.

This still doesn't yield when a task expires to protect against starvation.


Changed files:
filename: packages/scheduler/src/SchedulerFeatureFlags.js
patch: @@ -15,3 +15,5 @@ export const userBlockingPriorityTimeout = 250;
 export const normalPriorityTimeout = 5000;
 export const lowPriorityTimeout = 10000;
 export const enableRequestPaint = true;
+
+export const enableAlwaysYieldScheduler = __EXPERIMENTAL__;

filename: packages/scheduler/src/__tests__/Scheduler-test.js
patch: @@ -201,9 +201,10 @@ describe('SchedulerBrowser', () => {
     runtime.assertLog([
       'Message Event',
       'Task',
-      SchedulerFeatureFlags.enableRequestPaint
-        ? 'Yield at 0ms'
-        : `Yield at ${SchedulerFeatureFlags.frameYieldMs}ms`,
+      gate(flags => flags.enableAlwaysYieldScheduler) ||
+      !SchedulerFeatureFlags.enableRequestPaint
+        ? gate(flags => (flags.www ? 'Yield at 10ms' : 'Yield at 5ms'))
+        : 'Yield at 0ms',
       'Post Message',
     ]);
 
@@ -220,7 +221,13 @@ describe('SchedulerBrowser', () => {
     });
     runtime.assertLog(['Post Message']);
     runtime.fireMessageEvent();
-    runtime.assertLog(['Message Event', 'A', 'B']);
+    if (gate(flags => flags.enableAlwaysYieldScheduler)) {
+      runtime.assertLog(['Message Event', 'A', 'Post Message']);
+      runtime.fireMessageEvent();
+      runtime.assertLog(['Message Event', 'B']);
+    } else {
+      runtime.assertLog(['Message Event', 'A', 'B']);
+    }
   });
 
   it('multiple tasks with a yield in between', () => {
@@ -267,6 +274,10 @@ describe('SchedulerBrowser', () => {
     runtime.assertLog(['Message Event', 'Oops!', 'Post Message']);
 
     runtime.fireMessageEvent();
+    if (gate(flags => flags.enableAlwaysYieldScheduler)) {
+      runtime.assertLog(['Message Event', 'Post Message']);
+      runtime.fireMessageEvent();
+    }
     runtime.assertLog(['Message Event', 'Yay']);
   });
 

filename: packages/scheduler/src/__tests__/SchedulerSetImmediate-test.js
patch: @@ -188,7 +188,13 @@ describe('SchedulerDOMSetImmediate', () => {
     });
     runtime.assertLog(['Set Immediate']);
     runtime.fireSetImmediate();
-    runtime.assertLog(['setImmediate Callback', 'A', 'B']);
+    if (gate(flags => flags.enableAlwaysYieldScheduler)) {
+      runtime.assertLog(['setImmediate Callback', 'A', 'Set Immediate']);
+      runtime.fireSetImmediate();
+      runtime.assertLog(['setImmediate Callback', 'B']);
+    } else {
+      runtime.assertLog(['setImmediate Callback', 'A', 'B']);
+    }
   });
 
   it('multiple tasks at different priority', () => {
@@ -200,7 +206,13 @@ describe('SchedulerDOMSetImmediate', () => {
     });
     runtime.assertLog(['Set Immediate']);
     runtime.fireSetImmediate();
-    runtime.assertLog(['setImmediate Callback', 'B', 'A']);
+    if (gate(flags => flags.enableAlwaysYieldScheduler)) {
+      runtime.assertLog(['setImmediate Callback', 'B', 'Set Immediate']);
+      runtime.fireSetImmediate();
+      runtime.assertLog(['setImmediate Callback', 'A']);
+    } else {
+      runtime.assertLog(['setImmediate Callback', 'B', 'A']);
+    }
   });
 
   it('multiple tasks with a yield in between', () => {
@@ -246,7 +258,13 @@ describe('SchedulerDOMSetImmediate', () => {
     runtime.assertLog(['setImmediate Callback', 'Oops!', 'Set Immediate']);
 
     runtime.fireSetImmediate();
-    runtime.assertLog(['setImmediate Callback', 'Yay']);
+    if (gate(flags => flags.enableAlwaysYieldScheduler)) {
+      runtime.assertLog(['setImmediate Callback', 'Set Immediate']);
+      runtime.fireSetImmediate();
+      runtime.assertLog(['setImmediate Callback', 'Yay']);
+    } else {
+      runtime.assertLog(['setImmediate Callback', 'Yay']);
+    }
   });
 
   it('schedule new task after queue has emptied', () => {

filename: packages/scheduler/src/forks/Scheduler.js
patch: @@ -19,6 +19,7 @@ import {
   lowPriorityTimeout,
   normalPriorityTimeout,
   enableRequestPaint,
+  enableAlwaysYieldScheduler,
 } from '../SchedulerFeatureFlags';
 
 import {push, pop, peek} from '../SchedulerMinHeap';
@@ -196,9 +197,11 @@ function workLoop(initialTime: number) {
     currentTask !== null &&
     !(enableSchedulerDebugging && isSchedulerPaused)
   ) {
-    if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
-      // This currentTask hasn't expired, and we've reached the deadline.
-      break;
+    if (!enableAlwaysYieldScheduler) {
+      if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
+        // This currentTask hasn't expired, and we've reached the deadline.
+        break;
+      }
     }
     // $FlowFixMe[incompatible-use] found when upgrading Flow
     const callback = currentTask.callback;
@@ -242,6 +245,12 @@ function workLoop(initialTime: number) {
       pop(taskQueue);
     }
     currentTask = peek(taskQueue);
+    if (enableAlwaysYieldScheduler) {
+      if (currentTask === null || currentTask.expirationTime > currentTime) {
+        // This currentTask hasn't expired we yield to the browser task.
+        break;
+      }
+    }
   }
   // Return whether there's additional work
   if (currentTask !== null) {
@@ -459,7 +468,7 @@ let frameInterval = frameYieldMs;
 let startTime = -1;
 
 function shouldYieldToHost(): boolean {
-  if (enableRequestPaint && needsPaint) {
+  if (!enableAlwaysYieldScheduler && enableRequestPaint && needsPaint) {
     // Yield now.
     return true;
   }

filename: packages/scheduler/src/forks/SchedulerFeatureFlags.www.js
patch: @@ -19,3 +19,5 @@ export const frameYieldMs = 10;
 export const userBlockingPriorityTimeout = 250;
 export const normalPriorityTimeout = 5000;
 export const lowPriorityTimeout = 10000;
+
+export const enableAlwaysYieldScheduler = false;