@samitouri / QOS-React-2 / commits / 09c8d25633

Move update scheduling to microtask (#26512)

When React receives new input (via `setState`, a Suspense promise resolution, and so on), it needs to ensure there's a rendering task associated with the update. Most of this happens `ensureRootIsScheduled`. If a single event contains multiple updates, we end up running the scheduling code once per update. But this is wasteful because we really only need to run it once, at the end of the event (or in the case of flushSync, at the end of the scope function's execution). So this PR moves the scheduling logic to happen in a microtask instead. In some cases, we will force it run earlier than that, like for `flushSync`, but since updates are batched by default, it will almost always happen in the microtask. Even for discrete updates. In production, this should have no observable behavior difference. In a testing environment that uses `act`, this should also not have a behavior difference because React will push these tasks to an internal `act` queue. However, tests that do not use `act` and do not simulate an actual production environment (like an e2e test) may be affected. For example, before this change, if a test were to call `setState` outside of `act` and then immediately call `jest.runAllTimers()`, the update would be synchronously applied. After this change, that will no longer work because the rendering task (a timer, in this case) isn't scheduled until after the microtask queue has run. I don't expect this to be an issue in practice because most people do not write their tests this way. They either use `act`, or they write e2e-style tests. The biggest exception has been... our own internal test suite. Until recently, many of our tests were written in a way that accidentally relied on the updates being scheduled synchronously. Over the past few weeks, @tyao1 and I have gradually converted the test suite to use a new set of testing helpers that are resilient to this implementation detail. (There are also some old Relay tests that were written in the style of React's internal test suite. Those will need to be fixed, too.) The larger motivation behind this change, aside from a minor performance improvement, is we intend to use this new microtask to perform additional logic that doesn't yet exist. Like inferring the priority of a custom event.

Andrew Clark committed Mar 31, 2023 at 13:04 UTC 09c8d2563300621dc91258a4c2839210e2fbdf0e
19 files changed +557 -376
packages/react-devtools-shared/src/__tests__/profilingCache-test.js
+1 -1
@@ -963,7 +963,7 @@ describe('ProfilingCache', () => {
963 2 => 0,
964 },
965 "passiveEffectDuration": null,
966 - "priorityLevel": "Immediate",
966 + "priorityLevel": "Normal",
967 "timestamp": 0,
968 "updaters": [
969 {
packages/react-reconciler/src/ReactFiberRoot.js
+1
@@ -63,6 +63,7 @@ function FiberRootNode(
63 this.cancelPendingCommit = null;
64 this.context = null;
65 this.pendingContext = null;
66 + this.next = null;
67 this.callbackNode = null;
68 this.callbackPriority = NoLane;
69 this.eventTimes = createLaneMap(NoLanes);
packages/react-reconciler/src/ReactFiberRootScheduler.js new
+464
@@ -0,0 +1,464 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {FiberRoot} from './ReactInternalTypes';
11 +import type {Lane} from './ReactFiberLane';
12 +import type {PriorityLevel} from 'scheduler/src/SchedulerPriorities';
13 +
14 +import {enableDeferRootSchedulingToMicrotask} from 'shared/ReactFeatureFlags';
15 +import {
16 + NoLane,
17 + NoLanes,
18 + SyncLane,
19 + getHighestPriorityLane,
20 + getNextLanes,
21 + includesOnlyNonUrgentLanes,
22 + includesSyncLane,
23 + markStarvedLanesAsExpired,
24 +} from './ReactFiberLane';
25 +import {
26 + CommitContext,
27 + NoContext,
28 + RenderContext,
29 + getExecutionContext,
30 + getWorkInProgressRoot,
31 + getWorkInProgressRootRenderLanes,
32 + isWorkLoopSuspendedOnData,
33 + performConcurrentWorkOnRoot,
34 + performSyncWorkOnRoot,
35 +} from './ReactFiberWorkLoop';
36 +import {LegacyRoot} from './ReactRootTags';
37 +import {
38 + ImmediatePriority as ImmediateSchedulerPriority,
39 + UserBlockingPriority as UserBlockingSchedulerPriority,
40 + NormalPriority as NormalSchedulerPriority,
41 + IdlePriority as IdleSchedulerPriority,
42 + cancelCallback as Scheduler_cancelCallback,
43 + scheduleCallback as Scheduler_scheduleCallback,
44 + now,
45 +} from './Scheduler';
46 +import {
47 + DiscreteEventPriority,
48 + ContinuousEventPriority,
49 + DefaultEventPriority,
50 + IdleEventPriority,
51 + lanesToEventPriority,
52 +} from './ReactEventPriorities';
53 +import {supportsMicrotasks, scheduleMicrotask} from './ReactFiberHostConfig';
54 +
55 +import ReactSharedInternals from 'shared/ReactSharedInternals';
56 +const {ReactCurrentActQueue} = ReactSharedInternals;
57 +
58 +// A linked list of all the roots with pending work. In an idiomatic app,
59 +// there's only a single root, but we do support multi root apps, hence this
60 +// extra complexity. But this module is optimized for the single root case.
61 +let firstScheduledRoot: FiberRoot | null = null;
62 +let lastScheduledRoot: FiberRoot | null = null;
63 +
64 +// Used to prevent redundant mircotasks from being scheduled.
65 +let didScheduleMicrotask: boolean = false;
66 +// `act` "microtasks" are scheduled on the `act` queue instead of an actual
67 +// microtask, so we have to dedupe those separately. This wouldn't be an issue
68 +// if we required all `act` calls to be awaited, which we might in the future.
69 +let didScheduleMicrotask_act: boolean = false;
70 +
71 +// Used to quickly bail out of flushSync if there's no sync work to do.
72 +let mightHavePendingSyncWork: boolean = false;
73 +
74 +let isFlushingWork: boolean = false;
75 +
76 +export function ensureRootIsScheduled(root: FiberRoot): void {
77 + // This function is called whenever a root receives an update. It does two
78 + // things 1) it ensures the root is in the root schedule, and 2) it ensures
79 + // there's a pending microtask to process the root schedule.
80 + //
81 + // Most of the actual scheduling logic does not happen until
82 + // `scheduleTaskForRootDuringMicrotask` runs.
83 +
84 + // Add the root to the schedule
85 + if (root === lastScheduledRoot || root.next !== null) {
86 + // Fast path. This root is already scheduled.
87 + } else {
88 + if (lastScheduledRoot === null) {
89 + firstScheduledRoot = lastScheduledRoot = root;
90 + } else {
91 + lastScheduledRoot.next = root;
92 + lastScheduledRoot = root;
93 + }
94 + }
95 +
96 + // Any time a root received an update, we set this to true until the next time
97 + // we process the schedule. If it's false, then we can quickly exit flushSync
98 + // without consulting the schedule.
99 + mightHavePendingSyncWork = true;
100 +
101 + // At the end of the current event, go through each of the roots and ensure
102 + // there's a task scheduled for each one at the correct priority.
103 + if (__DEV__ && ReactCurrentActQueue.current !== null) {
104 + // We're inside an `act` scope.
105 + if (!didScheduleMicrotask_act) {
106 + didScheduleMicrotask_act = true;
107 + scheduleImmediateTask(processRootScheduleInMicrotask);
108 + }
109 + } else {
110 + if (!didScheduleMicrotask) {
111 + didScheduleMicrotask = true;
112 + scheduleImmediateTask(processRootScheduleInMicrotask);
113 + }
114 + }
115 +
116 + if (!enableDeferRootSchedulingToMicrotask) {
117 + // While this flag is disabled, we schedule the render task immediately
118 + // instead of waiting a microtask.
119 + // TODO: We need to land enableDeferRootSchedulingToMicrotask ASAP to
120 + // unblock additional features we have planned.
121 + scheduleTaskForRootDuringMicrotask(root, now());
122 + }
123 +}
124 +
125 +export function flushSyncWorkOnAllRoots() {
126 + // This is allowed to be called synchronously, but the caller should check
127 + // the execution context first.
128 + flushSyncWorkAcrossRoots_impl(false);
129 +}
130 +
131 +export function flushSyncWorkOnLegacyRootsOnly() {
132 + // This is allowed to be called synchronously, but the caller should check
133 + // the execution context first.
134 + flushSyncWorkAcrossRoots_impl(true);
135 +}
136 +
137 +function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
138 + if (isFlushingWork) {
139 + // Prevent reentrancy.
140 + // TODO: Is this overly defensive? The callers must check the execution
141 + // context first regardless.
142 + return;
143 + }
144 +
145 + if (!mightHavePendingSyncWork) {
146 + // Fast path. There's no sync work to do.
147 + return;
148 + }
149 +
150 + const workInProgressRoot = getWorkInProgressRoot();
151 + const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
152 +
153 + // There may or may not be synchronous work scheduled. Let's check.
154 + let didPerformSomeWork;
155 + let errors: Array<mixed> | null = null;
156 + isFlushingWork = true;
157 + do {
158 + didPerformSomeWork = false;
159 + let root = firstScheduledRoot;
160 + while (root !== null) {
161 + if (onlyLegacy && root.tag !== LegacyRoot) {
162 + // Skip non-legacy roots.
163 + } else {
164 + const nextLanes = getNextLanes(
165 + root,
166 + root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
167 + );
168 + if (includesSyncLane(nextLanes)) {
169 + // This root has pending sync work. Flush it now.
170 + try {
171 + // TODO: Pass nextLanes as an argument instead of computing it again
172 + // inside performSyncWorkOnRoot.
173 + didPerformSomeWork = true;
174 + performSyncWorkOnRoot(root);
175 + } catch (error) {
176 + // Collect errors so we can rethrow them at the end
177 + if (errors === null) {
178 + errors = [error];
179 + } else {
180 + errors.push(error);
181 + }
182 + }
183 + }
184 + }
185 + root = root.next;
186 + }
187 + } while (didPerformSomeWork);
188 + isFlushingWork = false;
189 +
190 + // If any errors were thrown, rethrow them right before exiting.
191 + // TODO: Consider returning these to the caller, to allow them to decide
192 + // how/when to rethrow.
193 + if (errors !== null) {
194 + if (errors.length > 1) {
195 + if (typeof AggregateError === 'function') {
196 + // eslint-disable-next-line no-undef
197 + throw new AggregateError(errors);
198 + } else {
199 + for (let i = 1; i < errors.length; i++) {
200 + scheduleImmediateTask(throwError.bind(null, errors[i]));
201 + }
202 + const firstError = errors[0];
203 + throw firstError;
204 + }
205 + } else {
206 + const error = errors[0];
207 + throw error;
208 + }
209 + }
210 +}
211 +
212 +function throwError(error: mixed) {
213 + throw error;
214 +}
215 +
216 +function processRootScheduleInMicrotask() {
217 + // This function is always called inside a microtask. It should never be
218 + // called synchronously.
219 + didScheduleMicrotask = false;
220 + if (__DEV__) {
221 + didScheduleMicrotask_act = false;
222 + }
223 +
224 + // We'll recompute this as we iterate through all the roots and schedule them.
225 + mightHavePendingSyncWork = false;
226 +
227 + const currentTime = now();
228 +
229 + let prev = null;
230 + let root = firstScheduledRoot;
231 + while (root !== null) {
232 + const next = root.next;
233 + const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
234 + if (nextLanes === NoLane) {
235 + // This root has no more pending work. Remove it from the schedule. To
236 + // guard against subtle reentrancy bugs, this microtask is the only place
237 + // we do this — you can add roots to the schedule whenever, but you can
238 + // only remove them here.
239 +
240 + // Null this out so we know it's been removed from the schedule.
241 + root.next = null;
242 + if (prev === null) {
243 + // This is the new head of the list
244 + firstScheduledRoot = next;
245 + } else {
246 + prev.next = next;
247 + }
248 + if (next === null) {
249 + // This is the new tail of the list
250 + lastScheduledRoot = prev;
251 + }
252 + } else {
253 + // This root still has work. Keep it in the list.
254 + prev = root;
255 + if (includesSyncLane(nextLanes)) {
256 + mightHavePendingSyncWork = true;
257 + }
258 + }
259 + root = next;
260 + }
261 +
262 + // At the end of the microtask, flush any pending synchronous work. This has
263 + // to come at the end, because it does actual rendering work that might throw.
264 + flushSyncWorkOnAllRoots();
265 +}
266 +
267 +function scheduleTaskForRootDuringMicrotask(
268 + root: FiberRoot,
269 + currentTime: number,
270 +): Lane {
271 + // This function is always called inside a microtask, or at the very end of a
272 + // rendering task right before we yield to the main thread. It should never be
273 + // called synchronously.
274 + //
275 + // TODO: Unless enableDeferRootSchedulingToMicrotask is off. We need to land
276 + // that ASAP to unblock additional features we have planned.
277 + //
278 + // This function also never performs React work synchronously; it should
279 + // only schedule work to be performed later, in a separate task or microtask.
280 +
281 + // Check if any lanes are being starved by other work. If so, mark them as
282 + // expired so we know to work on those next.
283 + markStarvedLanesAsExpired(root, currentTime);
284 +
285 + // Determine the next lanes to work on, and their priority.
286 + const workInProgressRoot = getWorkInProgressRoot();
287 + const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
288 + const nextLanes = getNextLanes(
289 + root,
290 + root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
291 + );
292 +
293 + const existingCallbackNode = root.callbackNode;
294 + if (
295 + nextLanes === NoLanes ||
296 + // If this root is currently suspended and waiting for data to resolve, don't
297 + // schedule a task to render it. We'll either wait for a ping, or wait to
298 + // receive an update.
299 + (isWorkLoopSuspendedOnData() && root === workInProgressRoot) ||
300 + // We should only interrupt a pending commit if the new update
301 + // is urgent.
302 + (root.cancelPendingCommit !== null && includesOnlyNonUrgentLanes(nextLanes))
303 + ) {
304 + // Fast path: There's nothing to work on.
305 + if (existingCallbackNode !== null) {
306 + cancelCallback(existingCallbackNode);
307 + }
308 + root.callbackNode = null;
309 + root.callbackPriority = NoLane;
310 + return NoLane;
311 + }
312 +
313 + // Schedule a new callback in the host environment.
314 + if (includesSyncLane(nextLanes)) {
315 + // Synchronous work is always flushed at the end of the microtask, so we
316 + // don't need to schedule an additional task.
317 + if (existingCallbackNode !== null) {
318 + cancelCallback(existingCallbackNode);
319 + }
320 + root.callbackPriority = SyncLane;
321 + root.callbackNode = null;
322 + return SyncLane;
323 + } else {
324 + // We use the highest priority lane to represent the priority of the callback.
325 + const existingCallbackPriority = root.callbackPriority;
326 + const newCallbackPriority = getHighestPriorityLane(nextLanes);
327 +
328 + if (
329 + newCallbackPriority === existingCallbackPriority &&
330 + // Special case related to `act`. If the currently scheduled task is a
331 + // Scheduler task, rather than an `act` task, cancel it and re-schedule
332 + // on the `act` queue.
333 + !(
334 + __DEV__ &&
335 + ReactCurrentActQueue.current !== null &&
336 + existingCallbackNode !== fakeActCallbackNode
337 + )
338 + ) {
339 + // The priority hasn't changed. We can reuse the existing task.
340 + return newCallbackPriority;
341 + } else {
342 + // Cancel the existing callback. We'll schedule a new one below.
343 + cancelCallback(existingCallbackNode);
344 + }
345 +
346 + let schedulerPriorityLevel;
347 + switch (lanesToEventPriority(nextLanes)) {
348 + case DiscreteEventPriority:
349 + schedulerPriorityLevel = ImmediateSchedulerPriority;
350 + break;
351 + case ContinuousEventPriority:
352 + schedulerPriorityLevel = UserBlockingSchedulerPriority;
353 + break;
354 + case DefaultEventPriority:
355 + schedulerPriorityLevel = NormalSchedulerPriority;
356 + break;
357 + case IdleEventPriority:
358 + schedulerPriorityLevel = IdleSchedulerPriority;
359 + break;
360 + default:
361 + schedulerPriorityLevel = NormalSchedulerPriority;
362 + break;
363 + }
364 +
365 + const newCallbackNode = scheduleCallback(
366 + schedulerPriorityLevel,
367 + performConcurrentWorkOnRoot.bind(null, root),
368 + );
369 +
370 + root.callbackPriority = newCallbackPriority;
371 + root.callbackNode = newCallbackNode;
372 + return newCallbackPriority;
373 + }
374 +}
375 +
376 +export type RenderTaskFn = (didTimeout: boolean) => RenderTaskFn | null;
377 +
378 +export function getContinuationForRoot(
379 + root: FiberRoot,
380 + originalCallbackNode: mixed,
381 +): RenderTaskFn | null {
382 + // This is called at the end of `performConcurrentWorkOnRoot` to determine
383 + // if we need to schedule a continuation task.
384 + //
385 + // Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask;
386 + // however, since most of the logic for determining if we need a continuation
387 + // versus a new task is the same, we cheat a bit and call it here. This is
388 + // only safe to do because we know we're at the end of the browser task.
389 + // So although it's not an actual microtask, it might as well be.
390 + scheduleTaskForRootDuringMicrotask(root, now());
391 + if (root.callbackNode === originalCallbackNode) {
392 + // The task node scheduled for this root is the same one that's
393 + // currently executed. Need to return a continuation.
394 + return performConcurrentWorkOnRoot.bind(null, root);
395 + }
396 + return null;
397 +}
398 +
399 +const fakeActCallbackNode = {};
400 +
401 +function scheduleCallback(
402 + priorityLevel: PriorityLevel,
403 + callback: RenderTaskFn,
404 +) {
405 + if (__DEV__ && ReactCurrentActQueue.current !== null) {
406 + // Special case: We're inside an `act` scope (a testing utility).
407 + // Instead of scheduling work in the host environment, add it to a
408 + // fake internal queue that's managed by the `act` implementation.
409 + ReactCurrentActQueue.current.push(callback);
410 + return fakeActCallbackNode;
411 + } else {
412 + return Scheduler_scheduleCallback(priorityLevel, callback);
413 + }
414 +}
415 +
416 +function cancelCallback(callbackNode: mixed) {
417 + if (__DEV__ && callbackNode === fakeActCallbackNode) {
418 + // Special `act` case: check if this is the fake callback node used by
419 + // the `act` implementation.
420 + } else if (callbackNode !== null) {
421 + Scheduler_cancelCallback(callbackNode);
422 + }
423 +}
424 +
425 +function scheduleImmediateTask(cb: () => mixed) {
426 + if (__DEV__ && ReactCurrentActQueue.current !== null) {
427 + // Special case: Inside an `act` scope, we push microtasks to the fake `act`
428 + // callback queue. This is because we currently support calling `act`
429 + // without awaiting the result. The plan is to deprecate that, and require
430 + // that you always await the result so that the microtasks have a chance to
431 + // run. But it hasn't happened yet.
432 + ReactCurrentActQueue.current.push(() => {
433 + cb();
434 + return null;
435 + });
436 + }
437 +
438 + // TODO: Can we land supportsMicrotasks? Which environments don't support it?
439 + // Alternatively, can we move this check to the host config?
440 + if (supportsMicrotasks) {
441 + scheduleMicrotask(() => {
442 + // In Safari, appending an iframe forces microtasks to run.
443 + // https://github.com/facebook/react/issues/22459
444 + // We don't support running callbacks in the middle of render
445 + // or commit so we need to check against that.
446 + const executionContext = getExecutionContext();
447 + if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
448 + // Note that this would still prematurely flush the callbacks
449 + // if this happens outside render or commit phase (e.g. in an event).
450 +
451 + // Intentionally using a macrotask instead of a microtask here. This is
452 + // wrong semantically but it prevents an infinite loop. The bug is
453 + // Safari's, not ours, so we just do our best to not crash even though
454 + // the behavior isn't completely correct.
455 + Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
456 + return;
457 + }
458 + cb();
459 + });
460 + } else {
461 + // If microtasks are not supported, use Scheduler.
462 + Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
463 + }
464 +}
packages/react-reconciler/src/ReactFiberSyncTaskQueue.js deleted
-118
@@ -1,118 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import type {SchedulerCallback} from './Scheduler';
11 -
12 -import {
13 - DiscreteEventPriority,
14 - getCurrentUpdatePriority,
15 - setCurrentUpdatePriority,
16 -} from './ReactEventPriorities';
17 -import {ImmediatePriority, scheduleCallback} from './Scheduler';
18 -
19 -let syncQueue: Array<SchedulerCallback> | null = null;
20 -let includesLegacySyncCallbacks: boolean = false;
21 -let isFlushingSyncQueue: boolean = false;
22 -
23 -export function scheduleSyncCallback(callback: SchedulerCallback) {
24 - // Push this callback into an internal queue. We'll flush these either in
25 - // the next tick, or earlier if something calls `flushSyncCallbackQueue`.
26 - if (syncQueue === null) {
27 - syncQueue = [callback];
28 - } else {
29 - // Push onto existing queue. Don't need to schedule a callback because
30 - // we already scheduled one when we created the queue.
31 - syncQueue.push(callback);
32 - }
33 -}
34 -
35 -export function scheduleLegacySyncCallback(callback: SchedulerCallback) {
36 - includesLegacySyncCallbacks = true;
37 - scheduleSyncCallback(callback);
38 -}
39 -
40 -export function flushSyncCallbacksOnlyInLegacyMode() {
41 - // Only flushes the queue if there's a legacy sync callback scheduled.
42 - // TODO: There's only a single type of callback: performSyncOnWorkOnRoot. So
43 - // it might make more sense for the queue to be a list of roots instead of a
44 - // list of generic callbacks. Then we can have two: one for legacy roots, one
45 - // for concurrent roots. And this method would only flush the legacy ones.
46 - if (includesLegacySyncCallbacks) {
47 - flushSyncCallbacks();
48 - }
49 -}
50 -
51 -export function flushSyncCallbacks(): null {
52 - if (!isFlushingSyncQueue && syncQueue !== null) {
53 - // Prevent re-entrance.
54 - isFlushingSyncQueue = true;
55 -
56 - // Set the event priority to discrete
57 - // TODO: Is this necessary anymore? The only user code that runs in this
58 - // queue is in the render or commit phases, which already set the
59 - // event priority. Should be able to remove.
60 - const previousUpdatePriority = getCurrentUpdatePriority();
61 - setCurrentUpdatePriority(DiscreteEventPriority);
62 -
63 - let errors: Array<mixed> | null = null;
64 -
65 - const queue = syncQueue;
66 - // $FlowFixMe[incompatible-use] found when upgrading Flow
67 - for (let i = 0; i < queue.length; i++) {
68 - // $FlowFixMe[incompatible-use] found when upgrading Flow
69 - let callback: SchedulerCallback = queue[i];
70 - try {
71 - do {
72 - const isSync = true;
73 - // $FlowFixMe[incompatible-type] we bail out when we get a null
74 - callback = callback(isSync);
75 - } while (callback !== null);
76 - } catch (error) {
77 - // Collect errors so we can rethrow them at the end
78 - if (errors === null) {
79 - errors = [error];
80 - } else {
81 - errors.push(error);
82 - }
83 - }
84 - }
85 -
86 - syncQueue = null;
87 - includesLegacySyncCallbacks = false;
88 - setCurrentUpdatePriority(previousUpdatePriority);
89 - isFlushingSyncQueue = false;
90 -
91 - if (errors !== null) {
92 - if (errors.length > 1) {
93 - if (typeof AggregateError === 'function') {
94 - // eslint-disable-next-line no-undef
95 - throw new AggregateError(errors);
96 - } else {
97 - for (let i = 1; i < errors.length; i++) {
98 - scheduleCallback(
99 - ImmediatePriority,
100 - throwError.bind(null, errors[i]),
101 - );
102 - }
103 - const firstError = errors[0];
104 - throw firstError;
105 - }
106 - } else {
107 - const error = errors[0];
108 - throw error;
109 - }
110 - }
111 - }
112 -
113 - return null;
114 -}
115 -
116 -function throwError(error: mixed) {
117 - throw error;
118 -}
packages/react-reconciler/src/ReactFiberWorkLoop.js
+54 -224
@@ -22,6 +22,7 @@ import type {
22 TransitionAbort,
23 } from './ReactFiberTracingMarkerComponent';
24 import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
25 +import type {RenderTaskFn} from './ReactFiberRootScheduler';
26
27 import {
28 replayFailedUnitOfWorkWithInvokeGuardedCallback,
@@ -45,21 +46,12 @@ import is from 'shared/objectIs';
46 import {
47 // Aliased because `act` will override and push to an internal queue
48 scheduleCallback as Scheduler_scheduleCallback,
48 - cancelCallback as Scheduler_cancelCallback,
49 shouldYield,
50 requestPaint,
51 now,
52 - ImmediatePriority as ImmediateSchedulerPriority,
53 - UserBlockingPriority as UserBlockingSchedulerPriority,
52 NormalPriority as NormalSchedulerPriority,
53 IdlePriority as IdleSchedulerPriority,
54 } from './Scheduler';
57 -import {
58 - flushSyncCallbacks,
59 - flushSyncCallbacksOnlyInLegacyMode,
60 - scheduleSyncCallback,
61 - scheduleLegacySyncCallback,
62 -} from './ReactFiberSyncTaskQueue';
55 import {
56 logCommitStarted,
57 logCommitStopped,
@@ -78,9 +70,7 @@ import {
70 noTimeout,
71 afterActiveInstanceBlur,
72 getCurrentEventPriority,
81 - supportsMicrotasks,
73 errorHydratingContainer,
83 - scheduleMicrotask,
74 prepareRendererToRender,
75 resetRendererAfterRender,
76 startSuspendingCommit,
@@ -153,7 +143,6 @@ import {
143 includesBlockingLane,
144 includesExpiredLane,
145 getNextLanes,
156 - markStarvedLanesAsExpired,
146 getLanesToRetrySynchronouslyOnError,
147 getMostRecentEventTime,
148 markRootUpdated,
@@ -161,7 +150,6 @@ import {
150 markRootPinged,
151 markRootEntangled,
152 markRootFinished,
164 - getHighestPriorityLane,
153 addFiberToLanesMap,
154 movePendingFibersToMemoized,
155 addTransitionToLanesMap,
@@ -172,9 +160,7 @@ import {
160 } from './ReactFiberLane';
161 import {
162 DiscreteEventPriority,
175 - ContinuousEventPriority,
163 DefaultEventPriority,
177 - IdleEventPriority,
164 getCurrentUpdatePriority,
165 setCurrentUpdatePriority,
166 lowerEventPriority,
@@ -289,6 +275,12 @@ import {
275 } from './ReactFiberSuspenseContext';
276 import {resolveDefaultProps} from './ReactFiberLazyComponent';
277 import {resetChildReconcilerOnUnwind} from './ReactChildFiber';
278 +import {
279 + ensureRootIsScheduled,
280 + flushSyncWorkOnAllRoots,
281 + flushSyncWorkOnLegacyRootsOnly,
282 + getContinuationForRoot,
283 +} from './ReactFiberRootScheduler';
284
285 const ceil = Math.ceil;
286
@@ -609,6 +601,10 @@ export function getWorkInProgressRootRenderLanes(): Lanes {
601 return workInProgressRootRenderLanes;
602 }
603
604 +export function isWorkLoopSuspendedOnData(): boolean {
605 + return workInProgressSuspendedReason === SuspendedOnData;
606 +}
607 +
608 export function requestEventTime(): number {
609 if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
610 // We're inside React, so it's fine to read the actual time.
@@ -819,21 +815,24 @@ export function scheduleUpdateOnFiber(
815 }
816 }
817
822 - ensureRootIsScheduled(root, eventTime);
818 + ensureRootIsScheduled(root);
819 if (
820 lane === SyncLane &&
821 executionContext === NoContext &&
826 - (fiber.mode & ConcurrentMode) === NoMode &&
827 - // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
828 - !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
822 + (fiber.mode & ConcurrentMode) === NoMode
823 ) {
830 - // Flush the synchronous work now, unless we're already working or inside
831 - // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
832 - // scheduleCallbackForFiber to preserve the ability to schedule a callback
833 - // without immediately flushing it. We only do this for user-initiated
834 - // updates, to preserve historical behavior of legacy mode.
835 - resetRenderTimer();
836 - flushSyncCallbacksOnlyInLegacyMode();
824 + if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy) {
825 + // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
826 + ReactCurrentActQueue.didScheduleLegacyUpdate = true;
827 + } else {
828 + // Flush the synchronous work now, unless we're already working or inside
829 + // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
830 + // scheduleCallbackForFiber to preserve the ability to schedule a callback
831 + // without immediately flushing it. We only do this for user-initiated
832 + // updates, to preserve historical behavior of legacy mode.
833 + resetRenderTimer();
834 + flushSyncWorkOnLegacyRootsOnly();
835 + }
836 }
837 }
838 }
@@ -855,7 +854,7 @@ export function scheduleInitialHydrationOnRoot(
854 const current = root.current;
855 current.lanes = lane;
856 markRootUpdated(root, lane, eventTime);
858 - ensureRootIsScheduled(root, eventTime);
857 + ensureRootIsScheduled(root);
858 }
859
860 export function isUnsafeClassRenderPhaseUpdate(fiber: Fiber): boolean {
@@ -864,172 +863,12 @@ export function isUnsafeClassRenderPhaseUpdate(fiber: Fiber): boolean {
863 return (executionContext & RenderContext) !== NoContext;
864 }
865
867 -// Use this function to schedule a task for a root. There's only one task per
868 -// root; if a task was already scheduled, we'll check to make sure the priority
869 -// of the existing task is the same as the priority of the next level that the
870 -// root has work on. This function is called on every update, and right before
871 -// exiting a task.
872 -function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
873 - const existingCallbackNode = root.callbackNode;
874 -
875 - // Check if any lanes are being starved by other work. If so, mark them as
876 - // expired so we know to work on those next.
877 - markStarvedLanesAsExpired(root, currentTime);
878 -
879 - // Determine the next lanes to work on, and their priority.
880 - const nextLanes = getNextLanes(
881 - root,
882 - root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
883 - );
884 -
885 - if (nextLanes === NoLanes) {
886 - // Special case: There's nothing to work on.
887 - if (existingCallbackNode !== null) {
888 - cancelCallback(existingCallbackNode);
889 - }
890 - root.callbackNode = null;
891 - root.callbackPriority = NoLane;
892 - return;
893 - }
894 -
895 - // If this root is currently suspended and waiting for data to resolve, don't
896 - // schedule a task to render it. We'll either wait for a ping, or wait to
897 - // receive an update.
898 - if (
899 - workInProgressSuspendedReason === SuspendedOnData &&
900 - workInProgressRoot === root
901 - ) {
902 - root.callbackPriority = NoLane;
903 - root.callbackNode = null;
904 - return;
905 - }
906 -
907 - const cancelPendingCommit = root.cancelPendingCommit;
908 - if (cancelPendingCommit !== null) {
909 - // We should only interrupt a pending commit if the new update
910 - // is urgent.
911 - if (includesOnlyNonUrgentLanes(nextLanes)) {
912 - // The new update is not urgent. Don't interrupt the pending commit.
913 - root.callbackPriority = NoLane;
914 - root.callbackNode = null;
915 - return;
916 - }
917 - }
918 -
919 - // We use the highest priority lane to represent the priority of the callback.
920 - const newCallbackPriority = getHighestPriorityLane(nextLanes);
921 -
922 - // Check if there's an existing task. We may be able to reuse it.
923 - const existingCallbackPriority = root.callbackPriority;
924 - if (
925 - existingCallbackPriority === newCallbackPriority &&
926 - // Special case related to `act`. If the currently scheduled task is a
927 - // Scheduler task, rather than an `act` task, cancel it and re-scheduled
928 - // on the `act` queue.
929 - !(
930 - __DEV__ &&
931 - ReactCurrentActQueue.current !== null &&
932 - existingCallbackNode !== fakeActCallbackNode
933 - )
934 - ) {
935 - if (__DEV__) {
936 - // If we're going to re-use an existing task, it needs to exist.
937 - // Assume that discrete update microtasks are non-cancellable and null.
938 - // TODO: Temporary until we confirm this warning is not fired.
939 - if (
940 - existingCallbackNode == null &&
941 - !includesSyncLane(existingCallbackPriority)
942 - ) {
943 - console.error(
944 - 'Expected scheduled callback to exist. This error is likely caused by a bug in React. Please file an issue.',
945 - );
946 - }
947 - }
948 - // The priority hasn't changed. We can reuse the existing task. Exit.
949 - return;
950 - }
951 -
952 - if (existingCallbackNode != null) {
953 - // Cancel the existing callback. We'll schedule a new one below.
954 - cancelCallback(existingCallbackNode);
955 - }
956 -
957 - // Schedule a new callback.
958 - let newCallbackNode;
959 - if (includesSyncLane(newCallbackPriority)) {
960 - // Special case: Sync React callbacks are scheduled on a special
961 - // internal queue
962 - if (root.tag === LegacyRoot) {
963 - if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy !== null) {
964 - ReactCurrentActQueue.didScheduleLegacyUpdate = true;
965 - }
966 - scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
967 - } else {
968 - scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
969 - }
970 - if (supportsMicrotasks) {
971 - // Flush the queue in a microtask.
972 - if (__DEV__ && ReactCurrentActQueue.current !== null) {
973 - // Inside `act`, use our internal `act` queue so that these get flushed
974 - // at the end of the current scope even when using the sync version
975 - // of `act`.
976 - ReactCurrentActQueue.current.push(flushSyncCallbacks);
977 - } else {
978 - scheduleMicrotask(() => {
979 - // In Safari, appending an iframe forces microtasks to run.
980 - // https://github.com/facebook/react/issues/22459
981 - // We don't support running callbacks in the middle of render
982 - // or commit so we need to check against that.
983 - if (
984 - (executionContext & (RenderContext | CommitContext)) ===
985 - NoContext
986 - ) {
987 - // Note that this would still prematurely flush the callbacks
988 - // if this happens outside render or commit phase (e.g. in an event).
989 - flushSyncCallbacks();
990 - }
991 - });
992 - }
993 - } else {
994 - // Flush the queue in an Immediate task.
995 - scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
996 - }
997 - newCallbackNode = null;
998 - } else {
999 - let schedulerPriorityLevel;
1000 - switch (lanesToEventPriority(nextLanes)) {
1001 - case DiscreteEventPriority:
1002 - schedulerPriorityLevel = ImmediateSchedulerPriority;
1003 - break;
1004 - case ContinuousEventPriority:
1005 - schedulerPriorityLevel = UserBlockingSchedulerPriority;
1006 - break;
1007 - case DefaultEventPriority:
1008 - schedulerPriorityLevel = NormalSchedulerPriority;
1009 - break;
1010 - case IdleEventPriority:
1011 - schedulerPriorityLevel = IdleSchedulerPriority;
1012 - break;
1013 - default:
1014 - schedulerPriorityLevel = NormalSchedulerPriority;
1015 - break;
1016 - }
1017 - newCallbackNode = scheduleCallback(
1018 - schedulerPriorityLevel,
1019 - performConcurrentWorkOnRoot.bind(null, root),
1020 - );
1021 - }
1022 -
1023 - root.callbackPriority = newCallbackPriority;
1024 - root.callbackNode = newCallbackNode;
1025 -}
1026 -
866 // This is the entry point for every concurrent task, i.e. anything that
867 // goes through Scheduler.
1029 -function performConcurrentWorkOnRoot(
868 +export function performConcurrentWorkOnRoot(
869 root: FiberRoot,
870 didTimeout: boolean,
1032 -): $FlowFixMe {
871 +): RenderTaskFn | null {
872 if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
873 resetNestedUpdateFlag();
874 }
@@ -1062,6 +901,7 @@ function performConcurrentWorkOnRoot(
901
902 // Determine the next lanes to work on, using the fields stored
903 // on the root.
904 + // TODO: This was already computed in the caller. Pass it as an argument.
905 let lanes = getNextLanes(
906 root,
907 root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
@@ -1108,7 +948,7 @@ function performConcurrentWorkOnRoot(
948 const fatalError = workInProgressRootFatalError;
949 prepareFreshStack(root, NoLanes);
950 markRootSuspended(root, lanes);
1111 - ensureRootIsScheduled(root, now());
951 + ensureRootIsScheduled(root);
952 throw fatalError;
953 }
954
@@ -1157,7 +997,7 @@ function performConcurrentWorkOnRoot(
997 const fatalError = workInProgressRootFatalError;
998 prepareFreshStack(root, NoLanes);
999 markRootSuspended(root, lanes);
1160 - ensureRootIsScheduled(root, now());
1000 + ensureRootIsScheduled(root);
1001 throw fatalError;
1002 }
1003
@@ -1173,13 +1013,8 @@ function performConcurrentWorkOnRoot(
1013 }
1014 }
1015
1176 - ensureRootIsScheduled(root, now());
1177 - if (root.callbackNode === originalCallbackNode) {
1178 - // The task node scheduled for this root is the same one that's
1179 - // currently executed. Need to return a continuation.
1180 - return performConcurrentWorkOnRoot.bind(null, root);
1181 - }
1182 - return null;
1016 + ensureRootIsScheduled(root);
1017 + return getContinuationForRoot(root, originalCallbackNode);
1018 }
1019
1020 function recoverFromConcurrentError(
@@ -1531,7 +1366,7 @@ function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
1366
1367 // This is the entry point for synchronous tasks that don't go
1368 // through Scheduler
1534 -function performSyncWorkOnRoot(root: FiberRoot) {
1369 +export function performSyncWorkOnRoot(root: FiberRoot): null {
1370 if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
1371 syncNestedUpdateFlag();
1372 }
@@ -1542,10 +1377,11 @@ function performSyncWorkOnRoot(root: FiberRoot) {
1377
1378 flushPassiveEffects();
1379
1380 + // TODO: This was already computed in the caller. Pass it as an argument.
1381 let lanes = getNextLanes(root, NoLanes);
1382 if (!includesSyncLane(lanes)) {
1383 // There's no remaining sync work left.
1548 - ensureRootIsScheduled(root, now());
1384 + ensureRootIsScheduled(root);
1385 return null;
1386 }
1387
@@ -1574,7 +1410,7 @@ function performSyncWorkOnRoot(root: FiberRoot) {
1410 const fatalError = workInProgressRootFatalError;
1411 prepareFreshStack(root, NoLanes);
1412 markRootSuspended(root, lanes);
1577 - ensureRootIsScheduled(root, now());
1413 + ensureRootIsScheduled(root);
1414 throw fatalError;
1415 }
1416
@@ -1583,7 +1419,7 @@ function performSyncWorkOnRoot(root: FiberRoot) {
1419 // cases where need to exit the current render without producing a
1420 // consistent tree or committing.
1421 markRootSuspended(root, lanes);
1586 - ensureRootIsScheduled(root, now());
1422 + ensureRootIsScheduled(root);
1423 return null;
1424 }
1425
@@ -1600,7 +1436,7 @@ function performSyncWorkOnRoot(root: FiberRoot) {
1436
1437 // Before exiting, make sure there's a callback scheduled for the next
1438 // pending level.
1603 - ensureRootIsScheduled(root, now());
1439 + ensureRootIsScheduled(root);
1440
1441 return null;
1442 }
@@ -1608,10 +1444,13 @@ function performSyncWorkOnRoot(root: FiberRoot) {
1444 export function flushRoot(root: FiberRoot, lanes: Lanes) {
1445 if (lanes !== NoLanes) {
1446 markRootEntangled(root, mergeLanes(lanes, SyncLane));
1611 - ensureRootIsScheduled(root, now());
1447 + ensureRootIsScheduled(root);
1448 if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1449 resetRenderTimer();
1614 - flushSyncCallbacks();
1450 + // TODO: For historical reasons this flushes all sync work across all
1451 + // roots. It shouldn't really matter either way, but we could change this
1452 + // to only flush the given root.
1453 + flushSyncWorkOnAllRoots();
1454 }
1455 }
1456 }
@@ -1649,7 +1488,7 @@ export function batchedUpdates<A, R>(fn: A => R, a: A): R {
1488 !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
1489 ) {
1490 resetRenderTimer();
1652 - flushSyncCallbacksOnlyInLegacyMode();
1491 + flushSyncWorkOnLegacyRootsOnly();
1492 }
1493 }
1494 }
@@ -1717,7 +1556,7 @@ export function flushSync<R>(fn: (() => R) | void): R | void {
1556 // Note that this will happen even if batchedUpdates is higher up
1557 // the stack.
1558 if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1720 - flushSyncCallbacks();
1559 + flushSyncWorkOnAllRoots();
1560 }
1561 }
1562 }
@@ -2313,7 +2152,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2152 // Ensure the root is scheduled. We should do this even if we're
2153 // currently working on a different root, so that we resume
2154 // rendering later.
2316 - ensureRootIsScheduled(root, now());
2155 + ensureRootIsScheduled(root);
2156 };
2157 thenable.then(onResolution, onResolution);
2158 break outer;
@@ -3150,7 +2989,7 @@ function commitRootImpl(
2989
2990 // Always call this before exiting `commitRoot`, to ensure that any
2991 // additional work on this root is scheduled.
3153 - ensureRootIsScheduled(root, now());
2992 + ensureRootIsScheduled(root);
2993
2994 if (recoverableErrors !== null) {
2995 // There were errors during this render, but recovered from them without
@@ -3205,7 +3044,7 @@ function commitRootImpl(
3044 }
3045
3046 // If layout work was scheduled, flush it now.
3208 - flushSyncCallbacks();
3047 + flushSyncWorkOnAllRoots();
3048
3049 if (__DEV__) {
3050 if (enableDebugTracing) {
@@ -3411,7 +3250,7 @@ function flushPassiveEffectsImpl() {
3250
3251 executionContext = prevExecutionContext;
3252
3414 - flushSyncCallbacks();
3253 + flushSyncWorkOnAllRoots();
3254
3255 if (enableTransitionTracing) {
3256 const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks;
@@ -3496,7 +3335,7 @@ function captureCommitPhaseErrorOnRoot(
3335 const eventTime = requestEventTime();
3336 if (root !== null) {
3337 markRootUpdated(root, SyncLane, eventTime);
3499 - ensureRootIsScheduled(root, eventTime);
3338 + ensureRootIsScheduled(root);
3339 }
3340 }
3341
@@ -3539,7 +3378,7 @@ export function captureCommitPhaseError(
3378 const eventTime = requestEventTime();
3379 if (root !== null) {
3380 markRootUpdated(root, SyncLane, eventTime);
3542 - ensureRootIsScheduled(root, eventTime);
3381 + ensureRootIsScheduled(root);
3382 }
3383 return;
3384 }
@@ -3617,7 +3456,6 @@ function pingSuspendedRoot(
3456 pingCache.delete(wakeable);
3457 }
3458
3620 - const eventTime = requestEventTime();
3459 markRootPinged(root, pingedLanes);
3460
3461 warnIfSuspenseResolutionNotWrappedWithActDEV(root);
@@ -3660,7 +3498,7 @@ function pingSuspendedRoot(
3498 }
3499 }
3500
3663 - ensureRootIsScheduled(root, eventTime);
3501 + ensureRootIsScheduled(root);
3502 }
3503
3504 function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
@@ -3678,7 +3516,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
3516 const root = enqueueConcurrentRenderForLane(boundaryFiber, retryLane);
3517 if (root !== null) {
3518 markRootUpdated(root, retryLane, eventTime);
3681 - ensureRootIsScheduled(root, eventTime);
3519 + ensureRootIsScheduled(root);
3520 }
3521 }
3522
@@ -4160,14 +3998,6 @@ function scheduleCallback(priorityLevel: any, callback) {
3998 }
3999 }
4000
4163 -function cancelCallback(callbackNode: any) {
4164 - if (__DEV__ && callbackNode === fakeActCallbackNode) {
4165 - return;
4166 - }
4167 - // In production, always call Scheduler. This function will be stripped out.
4168 - return Scheduler_cancelCallback(callbackNode);
4169 -}
4170 -
4001 function shouldForceFlushFallbacksInDEV() {
4002 // Never force flush in production. This function should get stripped out.
4003 return __DEV__ && ReactCurrentActQueue.current !== null;
packages/react-reconciler/src/ReactInternalTypes.js
+4
@@ -240,6 +240,10 @@ type BaseFiberRootProperties = {
240 MutableSource<any> | MutableSourceVersion,
241 > | null,
242
243 + // Used to create a linked list that represent all the roots that have
244 + // pending work scheduled on them.
245 + next: FiberRoot | null,
246 +
247 // Node returned by Scheduler.scheduleCallback. Represents the next rendering
248 // task that the root will work on.
249 callbackNode: any,
packages/react-reconciler/src/__tests__/ReactFlushSyncNoAggregateError-test.js
+1 -12
@@ -3,7 +3,6 @@ let ReactNoop;
3 let Scheduler;
4 let act;
5 let assertLog;
6 -let waitForThrow;
6
7 let overrideQueueMicrotask;
8 let flushFakeMicrotasks;
@@ -43,7 +42,6 @@ describe('ReactFlushSync (AggregateError not available)', () => {
42
43 const InternalTestUtils = require('internal-test-utils');
44 assertLog = InternalTestUtils.assertLog;
46 - waitForThrow = InternalTestUtils.waitForThrow;
45 });
46
47 function Text({text}) {
@@ -95,15 +93,6 @@ describe('ReactFlushSync (AggregateError not available)', () => {
93 // AggregateError is not available, React throws the first error, then
94 // throws the remaining errors in separate tasks.
95 expect(error).toBe(aahh);
98 -
99 - // TODO: Currently the remaining error is rethrown in an Immediate Scheduler
100 - // task, but this may change to a timer or microtask in the future. The
101 - // exact mechanism is an implementation detail; they just need to be logged
102 - // in the order the occurred.
103 -
104 - // This will start throwing if we change it to rethrow in a microtask.
105 - flushFakeMicrotasks();
106 -
107 - await waitForThrow(nooo);
96 + expect(flushFakeMicrotasks).toThrow(nooo);
97 });
98 });
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+2 -13
@@ -552,19 +552,8 @@ describe('ReactIncrementalUpdates', () => {
552 // The transition should not have expired, so we should be able to
553 // partially render it.
554 await waitFor(['A']);
555 -
556 - // FIXME: We should be able to partially render B, too, but currently it
557 - // expires. This is an existing bug that I discovered, which will be fixed
558 - // in a PR that I'm currently working on.
559 - //
560 - // Correct behavior:
561 - // await waitFor(['B']);
562 - // await waitForAll(['C', 'D']);
563 - //
564 - // Current behavior:
565 - await waitFor(['B'], {
566 - additionalLogsAfterAttemptingToYield: ['C', 'D'],
567 - });
555 + await waitFor(['B']);
556 + await waitForAll(['C', 'D']);
557 });
558
559 it('regression: does not expire soon due to previous expired work', async () => {
packages/react/src/__tests__/ReactProfiler-test.internal.js
+16 -7
@@ -258,13 +258,22 @@ describe(`onRender`, () => {
258
259 // TODO: unstable_now is called by more places than just the profiler.
260 // Rewrite this test so it's less fragile.
261 - assertLog([
262 - 'read current time',
263 - 'read current time',
264 - 'read current time',
265 - 'read current time',
266 - 'read current time',
267 - ]);
261 + if (gate(flags => flags.enableDeferRootSchedulingToMicrotask)) {
262 + assertLog([
263 + 'read current time',
264 + 'read current time',
265 + 'read current time',
266 + ]);
267 + } else {
268 + assertLog([
269 + 'read current time',
270 + 'read current time',
271 + 'read current time',
272 + 'read current time',
273 + 'read current time',
274 + 'read current time',
275 + ]);
276 + }
277
278 // Restore original mock
279 jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock'));
packages/shared/ReactFeatureFlags.js
+4
@@ -48,6 +48,10 @@ export const enableSchedulerDebugging = false;
48 // Need to remove didTimeout argument from Scheduler before landing
49 export const disableSchedulerTimeoutInWorkLoop = false;
50
51 +// This will break some internal tests at Meta so we need to gate this until
52 +// those can be fixed.
53 +export const enableDeferRootSchedulingToMicrotask = true;
54 +
55 // -----------------------------------------------------------------------------
56 // Slated for removal in the future (significant effort)
57 //
packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js
+1
@@ -21,6 +21,7 @@ import typeof * as DynamicFlagsType from 'ReactNativeInternalFeatureFlags';
21 // update the test configuration.
22
23 export const enableUseRefAccessWarning = __VARIANT__;
24 +export const enableDeferRootSchedulingToMicrotask = __VARIANT__;
25
26 // Flow magic to verify the exports of this file match the original version.
27 ((((null: any): ExportsType): DynamicFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-fb.js
+2 -1
@@ -17,7 +17,8 @@ import * as dynamicFlags from 'ReactNativeInternalFeatureFlags';
17
18 // We destructure each value before re-exporting to avoid a dynamic look-up on
19 // the exports object every time a flag is read.
20 -export const {enableUseRefAccessWarning} = dynamicFlags;
20 +export const {enableUseRefAccessWarning, enableDeferRootSchedulingToMicrotask} =
21 + dynamicFlags;
22
23 // The rest of the flags are static for better dead code elimination.
24 export const enableDebugTracing = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -69,6 +69,7 @@ export const enableHostSingletons = true;
69
70 export const useModernStrictMode = false;
71 export const enableFizzExternalRuntime = false;
72 +export const enableDeferRootSchedulingToMicrotask = true;
73
74 // Flow magic to verify the exports of this file match the original version.
75 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -69,6 +69,7 @@ export const enableHostSingletons = true;
69
70 export const useModernStrictMode = false;
71 export const enableFizzExternalRuntime = false;
72 +export const enableDeferRootSchedulingToMicrotask = true;
73
74 // Flow magic to verify the exports of this file match the original version.
75 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -66,6 +66,7 @@ export const enableFloat = true;
66 export const enableHostSingletons = true;
67
68 export const useModernStrictMode = false;
69 +export const enableDeferRootSchedulingToMicrotask = true;
70
71 // Flow magic to verify the exports of this file match the original version.
72 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -71,6 +71,7 @@ export const enableHostSingletons = true;
71
72 export const useModernStrictMode = false;
73 export const enableFizzExternalRuntime = false;
74 +export const enableDeferRootSchedulingToMicrotask = true;
75
76 // Flow magic to verify the exports of this file match the original version.
77 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -23,6 +23,7 @@ export const enableLazyContextPropagation = __VARIANT__;
23 export const enableUnifiedSyncLane = __VARIANT__;
24 export const enableTransitionTracing = __VARIANT__;
25 export const enableCustomElementPropertySupport = __VARIANT__;
26 +export const enableDeferRootSchedulingToMicrotask = __VARIANT__;
27
28 // Enable this flag to help with concurrent mode debugging.
29 // It logs information to the console about React scheduling, rendering, and commit phases.
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -27,6 +27,7 @@ export const {
27 enableUnifiedSyncLane,
28 enableTransitionTracing,
29 enableCustomElementPropertySupport,
30 + enableDeferRootSchedulingToMicrotask,
31 } = dynamicFeatureFlags;
32
33 // On WWW, __EXPERIMENTAL__ is used for a new modern build.
scripts/flow/xplat.js
+1
@@ -9,4 +9,5 @@
9
10 declare module 'ReactNativeInternalFeatureFlags' {
11 declare export var enableUseRefAccessWarning: boolean;
12 + declare export var enableDeferRootSchedulingToMicrotask: boolean;
13 }