9
10
import type {Thenable, Wakeable} from 'shared/ReactTypes';
11
import type {Fiber, FiberRoot} from './ReactInternalTypes';
12
-import type {ExpirationTime} from './ReactFiberExpirationTime.new';
12
+import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
13
import type {ReactPriorityLevel} from './ReactInternalTypes';
14
import type {Interaction} from 'scheduler/src/Tracing';
15
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
26
enableProfilerCommitHooks,
27
enableSchedulerTracing,
28
warnAboutUnmockedScheduler,
29
- disableSchedulerTimeoutBasedOnReactExpirationTime,
29
} from 'shared/ReactFeatureFlags';
30
import ReactSharedInternals from 'shared/ReactSharedInternals';
31
import invariant from 'shared/invariant';
38
shouldYield,
39
requestPaint,
40
now,
42
- NoPriority,
43
- ImmediatePriority,
44
- UserBlockingPriority,
45
- NormalPriority,
46
- LowPriority,
47
- IdlePriority,
41
+ NoPriority as NoSchedulerPriority,
42
+ ImmediatePriority as ImmediateSchedulerPriority,
43
+ UserBlockingPriority as UserBlockingSchedulerPriority,
44
+ NormalPriority as NormalSchedulerPriority,
45
+ LowPriority as LowSchedulerPriority,
46
+ IdlePriority as IdleSchedulerPriority,
47
flushSyncCallbackQueue,
48
scheduleSyncCallback,
49
} from './SchedulerWithReactIntegration.new';
115
import {
116
NoWork,
117
Sync,
118
+ UserBlockingUpdateTime,
119
+ DefaultUpdateTime,
120
Never,
120
- msToExpirationTime,
121
- expirationTimeToMs,
122
- computeInteractiveExpiration,
123
- computeAsyncExpiration,
124
- computeSuspenseTimeout,
125
- LOW_PRIORITY_EXPIRATION,
121
inferPriorityFromExpirationTime,
122
Batched,
123
Idle,
124
+ ContinuousHydration,
125
ShortTransition,
126
LongTransition,
127
+ isSameOrHigherPriority,
128
+ isSameExpirationTime,
129
+ bumpPriorityLower,
130
} from './ReactFiberExpirationTime.new';
131
+import {
132
+ SyncLanePriority,
133
+ SyncBatchedLanePriority,
134
+ InputDiscreteLanePriority,
135
+ InputContinuousLanePriority,
136
+ DefaultLanePriority,
137
+ TransitionShortLanePriority,
138
+ TransitionLongLanePriority,
139
+ HydrationContinuousLanePriority,
140
+ IdleLanePriority,
141
+ OffscreenLanePriority,
142
+} from './ReactFiberLane';
143
import {beginWork as originalBeginWork} from './ReactFiberBeginWork.new';
144
import {completeWork} from './ReactFiberCompleteWork.new';
145
import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork.new';
230
// The fiber we're working on
231
let workInProgress: Fiber | null = null;
232
// The expiration time we're rendering
222
-let renderExpirationTime: ExpirationTime = NoWork;
233
+let renderExpirationTime: ExpirationTimeOpaque = NoWork;
234
// Whether to root completed, errored, suspended, etc.
235
let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
236
// A fatal error, if one is thrown
237
let workInProgressRootFatalError: mixed = null;
238
// Most recent event time among processed updates during this render.
228
-// This is conceptually a time stamp but expressed in terms of an ExpirationTime
239
+// This is conceptually a time stamp but expressed in terms of an ExpirationTimeOpaque
240
// because we deal mostly with expiration times in the hot path, so this avoids
241
// the conversion happening in the hot path.
231
-let workInProgressRootLatestProcessedEventTime: ExpirationTime = Sync;
232
-let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync;
242
+let workInProgressRootLatestProcessedEventTime: number = -1;
243
+let workInProgressRootLatestSuspenseTimeout: number = -1;
244
let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
245
// The work left over by components that were visited during this render. Only
246
// includes unprocessed updates, not work in bailed out children.
236
-let workInProgressRootNextUnprocessedUpdateTime: ExpirationTime = NoWork;
247
+let workInProgressRootNextUnprocessedUpdateTime: ExpirationTimeOpaque = NoWork;
248
249
// If we're pinged while rendering we don't always restart immediately.
250
// This flag determines if it might be worthwhile to restart if an opportunity
254
// model where we don't commit new loading states in too quick succession.
255
let globalMostRecentFallbackTime: number = 0;
256
const FALLBACK_THROTTLE_MS: number = 500;
257
+const DEFAULT_TIMEOUT_MS: number = 5000;
258
259
let nextEffect: Fiber | null = null;
260
let hasUncaughtError = false;
263
264
let rootDoesHavePassiveEffects: boolean = false;
265
let rootWithPendingPassiveEffects: FiberRoot | null = null;
254
-let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoPriority;
255
-let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork;
266
+let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoSchedulerPriority;
267
+let pendingPassiveEffectsExpirationTime: ExpirationTimeOpaque = NoWork;
268
let pendingPassiveHookEffectsMount: Array<HookEffect | Fiber> = [];
269
let pendingPassiveHookEffectsUnmount: Array<HookEffect | Fiber> = [];
270
let pendingPassiveProfilerEffects: Array<Fiber> = [];
271
272
let rootsWithPendingDiscreteUpdates: Map<
273
FiberRoot,
262
- ExpirationTime,
274
+ ExpirationTimeOpaque,
275
> | null = null;
276
277
// Use these to prevent an infinite loop of nested updates
286
// during the commit phase. This enables them to be traced across components
287
// that spawn new work during render. E.g. hidden boundaries, suspended SSR
288
// hydration or SuspenseList.
277
-let spawnedWorkDuringRender: null | Array<ExpirationTime> = null;
278
-
279
-// Expiration times are computed by adding to the current time (the start
280
-// time). However, if two updates are scheduled within the same event, we
281
-// should treat their start times as simultaneous, even if the actual clock
282
-// time has advanced between the first and second call.
289
+let spawnedWorkDuringRender: null | Array<ExpirationTimeOpaque> = null;
290
284
-// In other words, because expiration times determine how updates are batched,
285
-// we want all updates of like priority that occur within the same event to
286
-// receive the same expiration time. Otherwise we get tearing.
287
-let currentEventTime: ExpirationTime = NoWork;
291
+// If two updates are scheduled within the same event, we should treat their
292
+// event times as simultaneous, even if the actual clock time has advanced
293
+// between the first and second call.
294
+let currentEventTime: number = -1;
295
296
// Dev only flag that tracks if passive effects are currently being flushed.
297
// We warn about state updates for unmounted components differently in this case.
304
return workInProgressRoot;
305
}
306
300
-export function requestCurrentTimeForUpdate() {
307
+export function requestEventTime() {
308
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
309
// We're inside React, so it's fine to read the actual time.
303
- return msToExpirationTime(now());
310
+ return now();
311
}
312
// We're not inside React, so we may be in the middle of a browser event.
306
- if (currentEventTime !== NoWork) {
313
+ if (currentEventTime !== -1) {
314
// Use the same start time for all updates until we enter React again.
315
return currentEventTime;
316
}
317
// This is the first update since React yielded. Compute a new start time.
311
- currentEventTime = msToExpirationTime(now());
318
+ currentEventTime = now();
319
return currentEventTime;
320
}
321
322
export function getCurrentTime() {
316
- return msToExpirationTime(now());
323
+ return now();
324
}
325
319
-export function computeExpirationForFiber(
320
- currentTime: ExpirationTime,
326
+export function requestUpdateExpirationTime(
327
fiber: Fiber,
322
- suspenseConfig: null | SuspenseConfig,
323
-): ExpirationTime {
328
+ suspenseConfig: SuspenseConfig | null,
329
+): ExpirationTimeOpaque {
330
+ // Special cases
331
const mode = fiber.mode;
332
if ((mode & BlockingMode) === NoMode) {
333
return Sync;
327
- }
328
-
329
- const priorityLevel = getCurrentPriorityLevel();
330
- if ((mode & ConcurrentMode) === NoMode) {
331
- return priorityLevel === ImmediatePriority ? Sync : Batched;
332
- }
333
-
334
- if ((executionContext & RenderContext) !== NoContext) {
334
+ } else if ((mode & ConcurrentMode) === NoMode) {
335
+ return getCurrentPriorityLevel() === ImmediateSchedulerPriority
336
+ ? (Sync: ExpirationTimeOpaque)
337
+ : Batched;
338
+ } else if ((executionContext & RenderContext) !== NoContext) {
339
// Use whatever time we're already rendering
336
- // TODO: Should there be a way to opt out, like with `runWithPriority`?
340
+ // TODO: Treat render phase updates as if they came from an
341
+ // interleaved event.
342
return renderExpirationTime;
343
}
344
340
- let expirationTime;
345
+ let updateLanePriority;
346
if (suspenseConfig !== null) {
347
// If there's a SuspenseConfig, choose an expiration time that's lower
348
// priority than a normal concurrent update (regardless of the current
344
- // Scheduler priority.) Timeouts larger than 10 seconds move one level lower
345
- // than that.
349
+ // Scheduler priority.) Timeouts larger than 10 seconds move one level
350
+ // lower than that.
351
+ // TODO: This will coerce numbers larger than 31 bits to 0.
352
const timeoutMs = suspenseConfig.timeoutMs;
347
- expirationTime =
348
- // TODO: This will coerce numbers larger than 31 bits to 0.
353
+ updateLanePriority =
354
timeoutMs === undefined || (timeoutMs | 0) < 10000
350
- ? ShortTransition
351
- : LongTransition;
355
+ ? TransitionShortLanePriority
356
+ : TransitionLongLanePriority;
357
} else {
353
- // Compute an expiration time based on the Scheduler priority.
358
+ // TODO: If we're not inside `runWithPriority`, this returns the priority
359
+ // of the currently running task. That's probably not what we want.
360
+ const priorityLevel = getCurrentPriorityLevel();
361
switch (priorityLevel) {
355
- case ImmediatePriority:
356
- expirationTime = Sync;
362
+ case ImmediateSchedulerPriority:
363
+ updateLanePriority = SyncLanePriority;
364
break;
358
- case UserBlockingPriority:
359
- // TODO: Rename this to computeUserBlockingExpiration
360
- expirationTime = computeInteractiveExpiration(currentTime);
365
+ case UserBlockingSchedulerPriority:
366
+ updateLanePriority = InputContinuousLanePriority;
367
break;
362
- case NormalPriority:
363
- case LowPriority: // TODO: Handle LowPriority
364
- // TODO: Rename this to... something better.
365
- expirationTime = computeAsyncExpiration(currentTime);
368
+ case NormalSchedulerPriority:
369
+ case LowSchedulerPriority:
370
+ // TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
371
+ updateLanePriority = DefaultLanePriority;
372
break;
367
- case IdlePriority:
368
- expirationTime = Idle;
373
+ case IdleSchedulerPriority:
374
+ updateLanePriority = IdleLanePriority;
375
break;
376
default:
377
invariant(false, 'Expected a valid priority level');
378
}
379
}
380
381
+ // TODO: In the new system, what we'll do here is claim one of the bits of
382
+ // the root's `pendingLanes` field, based on its priority. We'll combine this
383
+ // function with `scheduleUpdateOnFiber` and `markRootUpdatedAtTime`.
384
+ let expirationTime;
385
+ switch (updateLanePriority) {
386
+ case SyncLanePriority:
387
+ return Sync;
388
+ case SyncBatchedLanePriority:
389
+ return Batched;
390
+ case InputDiscreteLanePriority:
391
+ case InputContinuousLanePriority:
392
+ expirationTime = UserBlockingUpdateTime;
393
+ break;
394
+ case DefaultLanePriority:
395
+ expirationTime = DefaultUpdateTime;
396
+ break;
397
+ case TransitionShortLanePriority:
398
+ expirationTime = ShortTransition;
399
+ break;
400
+ case TransitionLongLanePriority:
401
+ expirationTime = LongTransition;
402
+ break;
403
+ case HydrationContinuousLanePriority:
404
+ expirationTime = ContinuousHydration;
405
+ break;
406
+ case IdleLanePriority:
407
+ expirationTime = Idle;
408
+ break;
409
+ case OffscreenLanePriority:
410
+ expirationTime = Never;
411
+ break;
412
+ default:
413
+ invariant(false, 'Expected a valid priority level');
414
+ }
415
+
416
// If we're in the middle of rendering a tree, do not update at the same
417
// expiration time that is already rendering.
418
// TODO: We shouldn't have to do this if the update is on a different root.
378
- // Refactor computeExpirationForFiber + scheduleUpdate so we have access to
379
- // the root when we check for this condition.
380
- if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
419
+ // TODO: In the new system, we'll find a different bit that's not the one
420
+ // we're currently rendering.
421
+ if (
422
+ workInProgressRoot !== null &&
423
+ isSameExpirationTime(expirationTime, renderExpirationTime)
424
+ ) {
425
// This is a trick to move this update into a separate batch
382
- expirationTime -= 1;
426
+ // TODO: This probably causes problems with ContinuousHydration and Idle
427
+ expirationTime = bumpPriorityLower(expirationTime);
428
}
429
430
return expirationTime;
432
433
export function scheduleUpdateOnFiber(
434
fiber: Fiber,
390
- expirationTime: ExpirationTime,
435
+ expirationTime: ExpirationTimeOpaque,
436
) {
437
checkForNestedUpdates();
438
warnAboutRenderPhaseUpdatesInDEV(fiber);
443
return null;
444
}
445
401
- // TODO: computeExpirationForFiber also reads the priority. Pass the
446
+ // TODO: requestUpdateLanePriority also reads the priority. Pass the
447
// priority as an argument to that function and this one.
448
const priorityLevel = getCurrentPriorityLevel();
449
405
- if (expirationTime === Sync) {
450
+ if (isSameExpirationTime(expirationTime, (Sync: ExpirationTimeOpaque))) {
451
if (
452
// Check if we're inside unbatchedUpdates
453
(executionContext & LegacyUnbatchedContext) !== NoContext &&
482
(executionContext & DiscreteEventContext) !== NoContext &&
483
// Only updates at user-blocking priority or greater are considered
484
// discrete, even inside a discrete event.
440
- (priorityLevel === UserBlockingPriority ||
441
- priorityLevel === ImmediatePriority)
485
+ (priorityLevel === UserBlockingSchedulerPriority ||
486
+ priorityLevel === ImmediateSchedulerPriority)
487
) {
488
// This is the result of a discrete event. Track the lowest priority
489
// discrete update per root so we can flush them early, if needed.
491
rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
492
} else {
493
const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
449
- if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
494
+ if (
495
+ lastDiscreteTime === undefined ||
496
+ !isSameOrHigherPriority(expirationTime, lastDiscreteTime)
497
+ ) {
498
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
499
}
500
}
507
// on a fiber.
508
function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
509
// Update the source fiber's expiration time
462
- if (fiber.expirationTime < expirationTime) {
463
- fiber.expirationTime = expirationTime;
510
+ if (!isSameOrHigherPriority(fiber.expirationTime_opaque, expirationTime)) {
511
+ fiber.expirationTime_opaque = expirationTime;
512
}
513
let alternate = fiber.alternate;
466
- if (alternate !== null && alternate.expirationTime < expirationTime) {
467
- alternate.expirationTime = expirationTime;
514
+ if (
515
+ alternate !== null &&
516
+ !isSameOrHigherPriority(alternate.expirationTime_opaque, expirationTime)
517
+ ) {
518
+ alternate.expirationTime_opaque = expirationTime;
519
}
520
// Walk the parent path to the root and update the child expiration time.
521
let node = fiber.return;
525
} else {
526
while (node !== null) {
527
alternate = node.alternate;
477
- if (node.childExpirationTime < expirationTime) {
478
- node.childExpirationTime = expirationTime;
528
+ if (
529
+ !isSameOrHigherPriority(node.childExpirationTime_opaque, expirationTime)
530
+ ) {
531
+ node.childExpirationTime_opaque = expirationTime;
532
if (
533
alternate !== null &&
481
- alternate.childExpirationTime < expirationTime
534
+ !isSameOrHigherPriority(
535
+ alternate.childExpirationTime_opaque,
536
+ expirationTime,
537
+ )
538
) {
483
- alternate.childExpirationTime = expirationTime;
539
+ alternate.childExpirationTime_opaque = expirationTime;
540
}
541
} else if (
542
alternate !== null &&
487
- alternate.childExpirationTime < expirationTime
543
+ !isSameOrHigherPriority(
544
+ alternate.childExpirationTime_opaque,
545
+ expirationTime,
546
+ )
547
) {
489
- alternate.childExpirationTime = expirationTime;
548
+ alternate.childExpirationTime_opaque = expirationTime;
549
}
550
if (node.return === null && node.tag === HostRoot) {
551
root = node.stateNode;
567
// suspended now, right before marking the incoming update. This has the
568
// effect of interrupting the current render and switching to the update.
569
// TODO: This happens to work when receiving an update during the render
511
- // phase, because of the trick inside computeExpirationForFiber to
570
+ // phase, because of the trick inside requestUpdateExpirationTime to
571
// subtract 1 from `renderExpirationTime` to move it into a
572
// separate bucket. But we should probably model it with an exception,
573
// using the same mechanism we use to force hydration of a subtree.
585
return root;
586
}
587
529
-function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
588
+function getNextRootExpirationTimeToWorkOn(
589
+ root: FiberRoot,
590
+): ExpirationTimeOpaque {
591
// Determines the next expiration time that the root should render, taking
592
// into account levels that may be suspended, or levels that may have
593
// received a ping.
594
534
- const lastExpiredTime = root.lastExpiredTime;
535
- if (lastExpiredTime !== NoWork) {
595
+ const lastExpiredTime = root.lastExpiredTime_opaque;
596
+ if (!isSameExpirationTime(lastExpiredTime, (NoWork: ExpirationTimeOpaque))) {
597
return lastExpiredTime;
598
}
599
600
// "Pending" refers to any update that hasn't committed yet, including if it
601
// suspended. The "suspended" range is therefore a subset.
541
- const firstPendingTime = root.firstPendingTime;
602
+ const firstPendingTime = root.firstPendingTime_opaque;
603
if (!isRootSuspendedAtTime(root, firstPendingTime)) {
604
// The highest priority pending time is not suspended. Let's work on that.
605
return firstPendingTime;
608
// If the first pending time is suspended, check if there's a lower priority
609
// pending level that we know about. Or check if we received a ping. Work
610
// on whichever is higher priority.
550
- const lastPingedTime = root.lastPingedTime;
551
- const nextKnownPendingLevel = root.nextKnownPendingLevel;
552
- const nextLevel =
553
- lastPingedTime > nextKnownPendingLevel
554
- ? lastPingedTime
555
- : nextKnownPendingLevel;
556
- if (nextLevel <= Idle && firstPendingTime !== nextLevel) {
611
+ const lastPingedTime = root.lastPingedTime_opaque;
612
+ const nextKnownPendingLevel = root.nextKnownPendingLevel_opaque;
613
+ const nextLevel = !isSameOrHigherPriority(
614
+ nextKnownPendingLevel,
615
+ lastPingedTime,
616
+ )
617
+ ? lastPingedTime
618
+ : nextKnownPendingLevel;
619
+ if (
620
+ isSameOrHigherPriority((Idle: ExpirationTimeOpaque), nextLevel) &&
621
+ !isSameExpirationTime(firstPendingTime, nextLevel)
622
+ ) {
623
// Don't work on Idle/Never priority unless everything else is committed.
624
return NoWork;
625
}
632
// the next level that the root has work on. This function is called on every
633
// update, and right before exiting a task.
634
function ensureRootIsScheduled(root: FiberRoot) {
569
- const lastExpiredTime = root.lastExpiredTime;
570
- if (lastExpiredTime !== NoWork) {
571
- // Special case: Expired work should flush synchronously.
572
- root.callbackExpirationTime = Sync;
573
- root.callbackPriority = ImmediatePriority;
574
- root.callbackNode = scheduleSyncCallback(
575
- performSyncWorkOnRoot.bind(null, root),
576
- );
577
- return;
578
- }
579
-
580
- const expirationTime = getNextRootExpirationTimeToWorkOn(root);
635
const existingCallbackNode = root.callbackNode;
582
- if (expirationTime === NoWork) {
583
- // There's nothing to work on.
636
+
637
+ const newCallbackId = getNextRootExpirationTimeToWorkOn(root);
638
+ if (newCallbackId === (NoWork: ExpirationTimeOpaque)) {
639
+ // Special case: There's nothing to work on.
640
if (existingCallbackNode !== null) {
641
+ cancelCallback(existingCallbackNode);
642
+ root.expiresAt = -1;
643
root.callbackNode = null;
586
- root.callbackExpirationTime = NoWork;
587
- root.callbackPriority = NoPriority;
644
+ root.callbackIsSync = false;
645
+ root.callbackId = NoWork;
646
}
647
return;
648
}
649
592
- // TODO: If this is an update, we already read the current time. Pass the
593
- // time as an argument.
594
- const currentTime = requestCurrentTimeForUpdate();
595
- const priorityLevel = inferPriorityFromExpirationTime(
596
- currentTime,
597
- expirationTime,
598
- );
650
+ const newTaskIsSync =
651
+ newCallbackId === (Sync: ExpirationTimeOpaque) ||
652
+ !isSameExpirationTime(
653
+ root.lastExpiredTime_opaque,
654
+ (NoWork: ExpirationTimeOpaque),
655
+ );
656
600
- // If there's an existing render task, confirm it has the correct priority and
601
- // expiration time. Otherwise, we'll cancel it and schedule a new one.
602
- if (existingCallbackNode !== null) {
603
- const existingCallbackPriority = root.callbackPriority;
604
- const existingCallbackExpirationTime = root.callbackExpirationTime;
605
- if (
606
- // Callback must have the exact same expiration time.
607
- existingCallbackExpirationTime === expirationTime &&
608
- // Callback must have greater or equal priority.
609
- existingCallbackPriority >= priorityLevel
610
- ) {
611
- // Existing callback is sufficient.
612
- return;
657
+ // Check if there's an existing task. We may be able to reuse it.
658
+ const existingTaskId = root.callbackId;
659
+ const existingCallbackIsSync = root.callbackIsSync;
660
+ if (existingTaskId !== (NoWork: ExpirationTimeOpaque)) {
661
+ if (newCallbackId === existingTaskId) {
662
+ // This task is already scheduled. Let's check its priority.
663
+ if (
664
+ (newTaskIsSync && existingCallbackIsSync) ||
665
+ (!newTaskIsSync && !existingCallbackIsSync)
666
+ ) {
667
+ // The priority hasn't changed. Exit.
668
+ return;
669
+ }
670
+ // The task ID is the same but the priority changed. Cancel the existing
671
+ // callback. We'll schedule a new one below.
672
}
614
- // Need to schedule a new task.
615
- // TODO: Instead of scheduling a new task, we should be able to change the
616
- // priority of the existing one.
673
cancelCallback(existingCallbackNode);
674
}
675
620
- root.callbackExpirationTime = expirationTime;
621
- root.callbackPriority = priorityLevel;
622
-
623
- let callbackNode;
624
- if (expirationTime === Sync) {
625
- // Sync React callbacks are scheduled on a special internal queue
626
- callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
627
- } else if (disableSchedulerTimeoutBasedOnReactExpirationTime) {
628
- callbackNode = scheduleCallback(
629
- priorityLevel,
630
- performConcurrentWorkOnRoot.bind(null, root),
676
+ // Schedule a new callback.
677
+ let newCallbackNode;
678
+ if (newTaskIsSync) {
679
+ // Special case: Sync React callbacks are scheduled on a special internal queue
680
+ newCallbackNode = scheduleSyncCallback(
681
+ performSyncWorkOnRoot.bind(null, root),
682
);
683
} else {
633
- callbackNode = scheduleCallback(
634
- priorityLevel,
635
- performConcurrentWorkOnRoot.bind(null, root),
636
- // Compute a task timeout based on the expiration time. This also affects
637
- // ordering because tasks are processed in timeout order.
638
- {timeout: expirationTimeToMs(expirationTime) - now()},
639
- );
684
+ // TODO: Use LanePriority instead of SchedulerPriority
685
+ const priorityLevel = inferPriorityFromExpirationTime(newCallbackId);
686
+ if (
687
+ priorityLevel === NormalSchedulerPriority ||
688
+ priorityLevel === UserBlockingSchedulerPriority
689
+ ) {
690
+ const existingExpirationTime = root.expiresAt;
691
+ const currentTimeMs = now();
692
+
693
+ // Compute an expiration time based on the priority level.
694
+ const expiration =
695
+ priorityLevel === UserBlockingSchedulerPriority ? 250 : 5000;
696
+
697
+ let msUntilExpiration;
698
+ if (existingExpirationTime === -1) {
699
+ // This is the first concurrent update on the root. Use the expiration
700
+ // time we just computed.
701
+ msUntilExpiration = expiration;
702
+ root.expiresAt = msUntilExpiration + currentTimeMs;
703
+ } else {
704
+ // There's already an expiration time. Use the smaller of the current
705
+ // expiration and the one we just computed.
706
+ msUntilExpiration = existingExpirationTime - currentTimeMs;
707
+ if (expiration < msUntilExpiration) {
708
+ root.expiresAt = expiration;
709
+ }
710
+ }
711
+ newCallbackNode = scheduleCallback(
712
+ priorityLevel,
713
+ performConcurrentWorkOnRoot.bind(null, root),
714
+ );
715
+ } else {
716
+ newCallbackNode = scheduleCallback(
717
+ priorityLevel,
718
+ performConcurrentWorkOnRoot.bind(null, root),
719
+ );
720
+ }
721
}
722
642
- root.callbackNode = callbackNode;
723
+ root.callbackId = newCallbackId;
724
+ root.callbackNode = newCallbackNode;
725
+ root.callbackIsSync = newTaskIsSync;
726
}
727
728
// This is the entry point for every concurrent task, i.e. anything that
730
function performConcurrentWorkOnRoot(root, didTimeout) {
731
// Since we know we're in a React event, we can clear the current
732
// event time. The next update will compute a new event time.
650
- currentEventTime = NoWork;
733
+ currentEventTime = -1;
734
+
735
+ // Determine the next expiration time to work on, using the fields stored
736
+ // on the root.
737
+ let expirationTime = getNextRootExpirationTimeToWorkOn(root);
738
+ if (isSameExpirationTime(expirationTime, (NoWork: ExpirationTimeOpaque))) {
739
+ return null;
740
+ }
741
652
- // Check if the render expired.
742
if (didTimeout) {
654
- // The render task took too long to complete. Mark the current time as
655
- // expired to synchronously render all expired work in a single batch.
656
- const currentTime = requestCurrentTimeForUpdate();
657
- markRootExpiredAtTime(root, currentTime);
743
+ // The render task took too long to complete. Mark the root as expired to
744
+ // prevent yielding to other tasks until this one finishes.
745
+ markRootExpiredAtTime(root, expirationTime);
746
// This will schedule a synchronous callback.
747
ensureRootIsScheduled(root);
748
return null;
749
}
750
663
- // Determine the next expiration time to work on, using the fields stored
664
- // on the root.
665
- let expirationTime = getNextRootExpirationTimeToWorkOn(root);
666
- if (expirationTime === NoWork) {
667
- return null;
668
- }
751
const originalCallbackNode = root.callbackNode;
752
invariant(
753
(executionContext & (RenderContext | CommitContext)) === NoContext,
765
// render at Idle (or lower) so that all pending updates are included.
766
// If it still fails after the second attempt, we'll give up and commit
767
// the resulting tree.
686
- expirationTime = expirationTime > Idle ? Idle : expirationTime;
768
+ expirationTime = !isSameOrHigherPriority(
769
+ (Idle: ExpirationTimeOpaque),
770
+ expirationTime,
771
+ )
772
+ ? (Idle: ExpirationTimeOpaque)
773
+ : expirationTime;
774
exitStatus = renderRootSync(root, expirationTime);
775
}
776
786
// or, if something suspended, wait to commit it after a timeout.
787
const finishedWork: Fiber = (root.current.alternate: any);
788
root.finishedWork = finishedWork;
702
- root.finishedExpirationTime = expirationTime;
703
- root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
789
+ root.finishedExpirationTime_opaque = expirationTime;
790
+ root.nextKnownPendingLevel_opaque = getRemainingExpirationTime(
791
+ finishedWork,
792
+ );
793
finishConcurrentRender(root, finishedWork, exitStatus, expirationTime);
794
}
795
824
}
825
case RootSuspended: {
826
markRootSuspendedAtTime(root, expirationTime);
738
- const lastSuspendedTime = root.lastSuspendedTime;
827
+ const lastSuspendedTime = root.lastSuspendedTime_opaque;
828
829
// We have an acceptable loading state. We need to figure out if we
830
// should immediately commit it or wait a bit.
833
// have a new loading state ready. We want to ensure that we commit
834
// that as soon as possible.
835
const hasNotProcessedNewUpdates =
747
- workInProgressRootLatestProcessedEventTime === Sync;
836
+ workInProgressRootLatestProcessedEventTime === -1;
837
if (
838
hasNotProcessedNewUpdates &&
839
// do not delay if we're inside an act() scope
850
// Don't bother with a very short suspense time.
851
if (msUntilTimeout > 10) {
852
if (workInProgressRootHasPendingPing) {
764
- const lastPingedTime = root.lastPingedTime;
765
- if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
853
+ const lastPingedTime = root.lastPingedTime_opaque;
854
+ if (
855
+ isSameExpirationTime(
856
+ lastPingedTime,
857
+ (NoWork: ExpirationTimeOpaque),
858
+ ) ||
859
+ isSameOrHigherPriority(lastPingedTime, expirationTime)
860
+ ) {
861
// This render was pinged but we didn't get to restart
862
// earlier so try restarting now instead.
768
- root.lastPingedTime = expirationTime;
863
+ root.lastPingedTime_opaque = expirationTime;
864
prepareFreshStack(root, expirationTime);
865
break;
866
}
867
}
868
869
const nextTime = getNextRootExpirationTimeToWorkOn(root);
775
- if (nextTime !== NoWork && nextTime !== expirationTime) {
870
+ if (
871
+ !isSameExpirationTime(nextTime, (NoWork: ExpirationTimeOpaque)) &&
872
+ !isSameExpirationTime(nextTime, expirationTime)
873
+ ) {
874
// There's additional work on this root.
875
break;
876
}
877
if (
780
- lastSuspendedTime !== NoWork &&
781
- lastSuspendedTime !== expirationTime
878
+ !isSameExpirationTime(
879
+ lastSuspendedTime,
880
+ (NoWork: ExpirationTimeOpaque),
881
+ ) &&
882
+ !isSameExpirationTime(lastSuspendedTime, expirationTime)
883
) {
884
// We should prefer to render the fallback of at the last
885
// suspended level. Ping the last suspended level to try
886
// rendering it again.
786
- root.lastPingedTime = lastSuspendedTime;
887
+ root.lastPingedTime_opaque = lastSuspendedTime;
888
break;
889
}
890
904
}
905
case RootSuspendedWithDelay: {
906
markRootSuspendedAtTime(root, expirationTime);
806
- const lastSuspendedTime = root.lastSuspendedTime;
907
+ const lastSuspendedTime = root.lastSuspendedTime_opaque;
908
909
if (
910
// do not delay if we're inside an act() scope
913
// We're suspended in a state that should be avoided. We'll try to
914
// avoid committing it for as long as the timeouts let us.
915
if (workInProgressRootHasPendingPing) {
815
- const lastPingedTime = root.lastPingedTime;
816
- if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
916
+ const lastPingedTime = root.lastPingedTime_opaque;
917
+ if (
918
+ isSameExpirationTime(
919
+ lastPingedTime,
920
+ (NoWork: ExpirationTimeOpaque),
921
+ ) ||
922
+ isSameOrHigherPriority(lastPingedTime, expirationTime)
923
+ ) {
924
// This render was pinged but we didn't get to restart earlier
925
// so try restarting now instead.
819
- root.lastPingedTime = expirationTime;
926
+ root.lastPingedTime_opaque = expirationTime;
927
prepareFreshStack(root, expirationTime);
928
break;
929
}
930
}
931
932
const nextTime = getNextRootExpirationTimeToWorkOn(root);
826
- if (nextTime !== NoWork && nextTime !== expirationTime) {
933
+ if (
934
+ !isSameExpirationTime(nextTime, (NoWork: ExpirationTimeOpaque)) &&
935
+ !isSameExpirationTime(nextTime, expirationTime)
936
+ ) {
937
// There's additional work on this root.
938
break;
939
}
940
if (
831
- lastSuspendedTime !== NoWork &&
832
- lastSuspendedTime !== expirationTime
941
+ !isSameExpirationTime(
942
+ lastSuspendedTime,
943
+ (NoWork: ExpirationTimeOpaque),
944
+ ) &&
945
+ !isSameExpirationTime(lastSuspendedTime, expirationTime)
946
) {
947
// We should prefer to render the fallback of at the last
948
// suspended level. Ping the last suspended level to try
949
// rendering it again.
837
- root.lastPingedTime = lastSuspendedTime;
950
+ root.lastPingedTime_opaque = lastSuspendedTime;
951
break;
952
}
953
954
let msUntilTimeout;
842
- if (workInProgressRootLatestSuspenseTimeout !== Sync) {
955
+ if (workInProgressRootLatestSuspenseTimeout !== -1) {
956
// We have processed a suspense config whose expiration time we
957
// can use as the timeout.
845
- msUntilTimeout =
846
- expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) - now();
847
- } else if (workInProgressRootLatestProcessedEventTime === Sync) {
958
+ msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
959
+ } else if (workInProgressRootLatestProcessedEventTime === -1) {
960
// This should never normally happen because only new updates
961
// cause delayed states, so we should have processed something.
962
// However, this could also happen in an offscreen tree.
964
} else {
965
// If we didn't process a suspense config, compute a JND based on
966
// the amount of time elapsed since the most recent event time.
855
- const eventTimeMs = expirationTimeToMs(
856
- workInProgressRootLatestProcessedEventTime,
857
- );
967
+ const eventTimeMs = workInProgressRootLatestProcessedEventTime;
968
const timeElapsedMs = now() - eventTimeMs;
969
msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
970
}
990
if (
991
// do not delay if we're inside an act() scope
992
!shouldForceFlushFallbacksInDEV() &&
883
- workInProgressRootLatestProcessedEventTime !== Sync &&
993
+ workInProgressRootLatestProcessedEventTime !== -1 &&
994
workInProgressRootCanSuspendUsingConfig !== null
995
) {
996
// If we have exceeded the minimum loading delay, which probably
999
// enough time.
1000
const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
1001
workInProgressRootLatestProcessedEventTime,
892
- expirationTime,
1002
workInProgressRootCanSuspendUsingConfig,
1003
);
1004
if (msUntilTimeout > 10) {
1029
1030
flushPassiveEffects();
1031
923
- const lastExpiredTime = root.lastExpiredTime;
1032
+ const lastExpiredTime = root.lastExpiredTime_opaque;
1033
1034
let expirationTime;
926
- if (lastExpiredTime !== NoWork) {
1035
+ if (!isSameExpirationTime(lastExpiredTime, (NoWork: ExpirationTimeOpaque))) {
1036
// There's expired work on this root. Check if we have a partial tree
1037
// that we can reuse.
1038
if (
1039
root === workInProgressRoot &&
931
- renderExpirationTime >= lastExpiredTime
1040
+ isSameOrHigherPriority(renderExpirationTime, lastExpiredTime)
1041
) {
1042
// There's a partial tree with equal or greater than priority than the
1043
// expired level. Finish rendering it before rendering the rest of the
1060
// render at Idle (or lower) so that all pending updates are included.
1061
// If it still fails after the second attempt, we'll give up and commit
1062
// the resulting tree.
954
- expirationTime = expirationTime > Idle ? Idle : expirationTime;
1063
+ expirationTime = !isSameOrHigherPriority(
1064
+ (Idle: ExpirationTimeOpaque),
1065
+ expirationTime,
1066
+ )
1067
+ ? (Idle: ExpirationTimeOpaque)
1068
+ : expirationTime;
1069
exitStatus = renderRootSync(root, expirationTime);
1070
}
1071
1081
// will commit it even if something suspended.
1082
const finishedWork: Fiber = (root.current.alternate: any);
1083
root.finishedWork = finishedWork;
970
- root.finishedExpirationTime = expirationTime;
971
- root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
1084
+ root.finishedExpirationTime_opaque = expirationTime;
1085
+ root.nextKnownPendingLevel_opaque = getRemainingExpirationTime(finishedWork);
1086
commitRoot(root);
1087
1088
// Before exiting, make sure there's a callback scheduled for the next
1092
return null;
1093
}
1094
981
-export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1095
+export function flushRoot(
1096
+ root: FiberRoot,
1097
+ expirationTime: ExpirationTimeOpaque,
1098
+) {
1099
markRootExpiredAtTime(root, expirationTime);
1100
ensureRootIsScheduled(root);
1101
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1133
1134
export function deferredUpdates<A>(fn: () => A): A {
1135
// TODO: Remove in favor of Scheduler.next
1019
- return runWithPriority(NormalPriority, fn);
1136
+ return runWithPriority(NormalSchedulerPriority, fn);
1137
}
1138
1139
export function syncUpdates<A, B, C, R>(
1142
b: B,
1143
c: C,
1144
): R {
1028
- return runWithPriority(ImmediatePriority, fn.bind(null, a, b, c));
1145
+ return runWithPriority(ImmediateSchedulerPriority, fn.bind(null, a, b, c));
1146
}
1147
1148
function flushPendingDiscreteUpdates() {
1199
executionContext |= DiscreteEventContext;
1200
try {
1201
// Should this
1085
- return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c, d));
1202
+ return runWithPriority(
1203
+ UserBlockingSchedulerPriority,
1204
+ fn.bind(null, a, b, c, d),
1205
+ );
1206
} finally {
1207
executionContext = prevExecutionContext;
1208
if (executionContext === NoContext) {
1238
const prevExecutionContext = executionContext;
1239
executionContext |= BatchedContext;
1240
try {
1121
- return runWithPriority(ImmediatePriority, fn.bind(null, a));
1241
+ return runWithPriority(ImmediateSchedulerPriority, fn.bind(null, a));
1242
} finally {
1243
executionContext = prevExecutionContext;
1244
// Flush the immediate callbacks that were scheduled during this batch.
1252
const prevExecutionContext = executionContext;
1253
executionContext |= BatchedContext;
1254
try {
1135
- runWithPriority(ImmediatePriority, fn);
1255
+ runWithPriority(ImmediateSchedulerPriority, fn);
1256
} finally {
1257
executionContext = prevExecutionContext;
1258
if (executionContext === NoContext) {
1264
1265
function prepareFreshStack(root, expirationTime) {
1266
root.finishedWork = null;
1147
- root.finishedExpirationTime = NoWork;
1267
+ root.finishedExpirationTime_opaque = NoWork;
1268
1269
const timeoutHandle = root.timeoutHandle;
1270
if (timeoutHandle !== noTimeout) {
1276
}
1277
1278
// Check if there's a suspended level at lower priority.
1159
- const lastSuspendedTime = root.lastSuspendedTime;
1160
- if (lastSuspendedTime !== NoWork && lastSuspendedTime < expirationTime) {
1161
- const lastPingedTime = root.lastPingedTime;
1279
+ const lastSuspendedTime = root.lastSuspendedTime_opaque;
1280
+ if (
1281
+ !isSameExpirationTime(lastSuspendedTime, (NoWork: ExpirationTimeOpaque)) &&
1282
+ !isSameOrHigherPriority(lastSuspendedTime, expirationTime)
1283
+ ) {
1284
+ const lastPingedTime = root.lastPingedTime_opaque;
1285
// Make sure the suspended level is marked as pinged so that we return back
1286
// to it later, in case the render we're about to start gets aborted.
1287
// Generally we only reach this path via a ping, but we shouldn't assume
1288
// that will always be the case.
1289
// Note: This is defensive coding to prevent a pending commit from
1290
// being dropped without being rescheduled. It shouldn't be necessary.
1168
- if (lastPingedTime === NoWork || lastPingedTime > lastSuspendedTime) {
1169
- root.lastPingedTime = lastSuspendedTime;
1291
+ if (
1292
+ isSameExpirationTime(lastPingedTime, (NoWork: ExpirationTimeOpaque)) ||
1293
+ !isSameOrHigherPriority(lastSuspendedTime, lastPingedTime)
1294
+ ) {
1295
+ root.lastPingedTime_opaque = lastSuspendedTime;
1296
}
1297
}
1298
1308
renderExpirationTime = expirationTime;
1309
workInProgressRootExitStatus = RootIncomplete;
1310
workInProgressRootFatalError = null;
1185
- workInProgressRootLatestProcessedEventTime = Sync;
1186
- workInProgressRootLatestSuspenseTimeout = Sync;
1311
+ workInProgressRootLatestProcessedEventTime = -1;
1312
+ workInProgressRootLatestSuspenseTimeout = -1;
1313
workInProgressRootCanSuspendUsingConfig = null;
1314
workInProgressRootNextUnprocessedUpdateTime = NoWork;
1315
workInProgressRootHasPendingPing = false;
1422
}
1423
1424
export function markRenderEventTimeAndConfig(
1299
- eventTime: ExpirationTime,
1425
+ eventTime: number,
1426
suspenseConfig: null | SuspenseConfig,
1427
): void {
1302
- // Anything lower pri than Idle is not an update, so we should skip it.
1303
- if (eventTime > Idle) {
1304
- // Track the most recent event time of all updates processed in this batch.
1305
- if (workInProgressRootLatestProcessedEventTime > eventTime) {
1306
- workInProgressRootLatestProcessedEventTime = eventTime;
1307
- }
1308
-
1309
- // Track the largest/latest timeout deadline in this batch.
1310
- // TODO: If there are two transitions in the same batch, shouldn't we
1311
- // choose the smaller one? Maybe this is because when an intermediate
1312
- // transition is superseded, we should ignore its suspense config, but
1313
- // we don't currently.
1314
- if (suspenseConfig !== null) {
1315
- // If `timeoutMs` is not specified, we default to 5 seconds. We have to
1316
- // resolve this default here because `suspenseConfig` is owned
1317
- // by userspace.
1318
- // TODO: Store this on the root instead (transition -> timeoutMs)
1319
- // TODO: Should this default to a JND instead?
1320
- const timeoutMs = suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION;
1321
- const timeoutTime = computeSuspenseTimeout(eventTime, timeoutMs);
1322
- if (timeoutTime < workInProgressRootLatestSuspenseTimeout) {
1323
- workInProgressRootLatestSuspenseTimeout = timeoutTime;
1324
- workInProgressRootCanSuspendUsingConfig = suspenseConfig;
1325
- }
1428
+ // Track the most recent event time of all updates processed in this batch.
1429
+ if (workInProgressRootLatestProcessedEventTime < eventTime) {
1430
+ workInProgressRootLatestProcessedEventTime = eventTime;
1431
+ }
1432
+
1433
+ // Track the largest/latest timeout deadline in this batch.
1434
+ // TODO: If there are two transitions in the same batch, shouldn't we
1435
+ // choose the smaller one? Maybe this is because when an intermediate
1436
+ // transition is superseded, we should ignore its suspense config, but
1437
+ // we don't currently.
1438
+ if (suspenseConfig !== null) {
1439
+ // If `timeoutMs` is not specified, we default to 5 seconds. We have to
1440
+ // resolve this default here because `suspenseConfig` is owned
1441
+ // by userspace.
1442
+ // TODO: Store this on the root instead (transition -> timeoutMs)
1443
+ // TODO: Should this default to a JND instead?
1444
+ const timeoutMs = suspenseConfig.timeoutMs | 0 || DEFAULT_TIMEOUT_MS;
1445
+ const timeoutTime = eventTime + timeoutMs;
1446
+ if (timeoutTime > workInProgressRootLatestSuspenseTimeout) {
1447
+ workInProgressRootLatestSuspenseTimeout = timeoutTime;
1448
+ workInProgressRootCanSuspendUsingConfig = suspenseConfig;
1449
}
1450
}
1451
}
1452
1453
export function markUnprocessedUpdateTime(
1331
- expirationTime: ExpirationTime,
1454
+ expirationTime: ExpirationTimeOpaque,
1455
): void {
1333
- if (expirationTime > workInProgressRootNextUnprocessedUpdateTime) {
1456
+ if (
1457
+ !isSameOrHigherPriority(
1458
+ workInProgressRootNextUnprocessedUpdateTime,
1459
+ expirationTime,
1460
+ )
1461
+ ) {
1462
workInProgressRootNextUnprocessedUpdateTime = expirationTime;
1463
}
1464
}
1479
1480
// Check if there's a lower priority update somewhere else in the tree.
1481
if (
1354
- workInProgressRootNextUnprocessedUpdateTime !== NoWork &&
1482
+ !isSameExpirationTime(
1483
+ workInProgressRootNextUnprocessedUpdateTime,
1484
+ (NoWork: ExpirationTimeOpaque),
1485
+ ) &&
1486
workInProgressRoot !== null
1487
) {
1488
// Mark the current render as suspended, and then mark that there's a
1518
1519
// If the root or expiration time have changed, throw out the existing stack
1520
// and prepare a fresh one. Otherwise we'll continue where we left off.
1390
- if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
1521
+ if (
1522
+ root !== workInProgressRoot ||
1523
+ !isSameExpirationTime(expirationTime, renderExpirationTime)
1524
+ ) {
1525
prepareFreshStack(root, expirationTime);
1526
startWorkOnPendingInteractions(root, expirationTime);
1527
}
1575
1576
// If the root or expiration time have changed, throw out the existing stack
1577
// and prepare a fresh one. Otherwise we'll continue where we left off.
1444
- if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
1578
+ if (
1579
+ root !== workInProgressRoot ||
1580
+ !isSameExpirationTime(expirationTime, renderExpirationTime)
1581
+ ) {
1582
prepareFreshStack(root, expirationTime);
1583
startWorkOnPendingInteractions(root, expirationTime);
1584
}
1785
}
1786
1787
function getRemainingExpirationTime(fiber: Fiber) {
1651
- const updateExpirationTime = fiber.expirationTime;
1652
- const childExpirationTime = fiber.childExpirationTime;
1653
- return updateExpirationTime > childExpirationTime
1788
+ const updateExpirationTime = fiber.expirationTime_opaque;
1789
+ const childExpirationTime = fiber.childExpirationTime_opaque;
1790
+ return !isSameOrHigherPriority(childExpirationTime, updateExpirationTime)
1791
? updateExpirationTime
1792
: childExpirationTime;
1793
}
1794
1795
function resetChildExpirationTime(completedWork: Fiber) {
1796
if (
1660
- renderExpirationTime !== Never &&
1661
- completedWork.childExpirationTime === Never
1797
+ !isSameExpirationTime(
1798
+ renderExpirationTime,
1799
+ (Never: ExpirationTimeOpaque),
1800
+ ) &&
1801
+ isSameExpirationTime(
1802
+ completedWork.childExpirationTime_opaque,
1803
+ (Never: ExpirationTimeOpaque),
1804
+ )
1805
) {
1806
// The children of this component are hidden. Don't bubble their
1807
// expiration times.
1830
1831
let child = completedWork.child;
1832
while (child !== null) {
1690
- const childUpdateExpirationTime = child.expirationTime;
1691
- const childChildExpirationTime = child.childExpirationTime;
1692
- if (childUpdateExpirationTime > newChildExpirationTime) {
1833
+ const childUpdateExpirationTime = child.expirationTime_opaque;
1834
+ const childChildExpirationTime = child.childExpirationTime_opaque;
1835
+ if (
1836
+ !isSameOrHigherPriority(
1837
+ newChildExpirationTime,
1838
+ childUpdateExpirationTime,
1839
+ )
1840
+ ) {
1841
newChildExpirationTime = childUpdateExpirationTime;
1842
}
1695
- if (childChildExpirationTime > newChildExpirationTime) {
1843
+ if (
1844
+ !isSameOrHigherPriority(
1845
+ newChildExpirationTime,
1846
+ childChildExpirationTime,
1847
+ )
1848
+ ) {
1849
newChildExpirationTime = childChildExpirationTime;
1850
}
1851
if (shouldBubbleActualDurations) {
1859
} else {
1860
let child = completedWork.child;
1861
while (child !== null) {
1709
- const childUpdateExpirationTime = child.expirationTime;
1710
- const childChildExpirationTime = child.childExpirationTime;
1711
- if (childUpdateExpirationTime > newChildExpirationTime) {
1862
+ const childUpdateExpirationTime = child.expirationTime_opaque;
1863
+ const childChildExpirationTime = child.childExpirationTime_opaque;
1864
+ if (
1865
+ !isSameOrHigherPriority(
1866
+ newChildExpirationTime,
1867
+ childUpdateExpirationTime,
1868
+ )
1869
+ ) {
1870
newChildExpirationTime = childUpdateExpirationTime;
1871
}
1714
- if (childChildExpirationTime > newChildExpirationTime) {
1872
+ if (
1873
+ !isSameOrHigherPriority(
1874
+ newChildExpirationTime,
1875
+ childChildExpirationTime,
1876
+ )
1877
+ ) {
1878
newChildExpirationTime = childChildExpirationTime;
1879
}
1880
child = child.sibling;
1881
}
1882
}
1883
1721
- completedWork.childExpirationTime = newChildExpirationTime;
1884
+ completedWork.childExpirationTime_opaque = newChildExpirationTime;
1885
}
1886
1887
function commitRoot(root) {
1888
const renderPriorityLevel = getCurrentPriorityLevel();
1889
runWithPriority(
1727
- ImmediatePriority,
1890
+ ImmediateSchedulerPriority,
1891
commitRootImpl.bind(null, root, renderPriorityLevel),
1892
);
1893
return null;
1911
);
1912
1913
const finishedWork = root.finishedWork;
1751
- const expirationTime = root.finishedExpirationTime;
1914
+ const expirationTime = root.finishedExpirationTime_opaque;
1915
if (finishedWork === null) {
1916
return null;
1917
}
1918
root.finishedWork = null;
1756
- root.finishedExpirationTime = NoWork;
1919
+ root.finishedExpirationTime_opaque = NoWork;
1920
1921
invariant(
1922
finishedWork !== root.current,
1927
// commitRoot never returns a continuation; it always finishes synchronously.
1928
// So we can clear these now to allow a new callback to be scheduled.
1929
root.callbackNode = null;
1767
- root.callbackExpirationTime = NoWork;
1768
- root.callbackPriority = NoPriority;
1930
+ root.callbackId = NoWork;
1931
+ // TODO: Use LanePriority instead of SchedulerPriority
1932
+ if (renderPriorityLevel < ImmediateSchedulerPriority) {
1933
+ // If this was a concurrent render, we can reset the expiration time.
1934
+ root.expiresAt = -1;
1935
+ }
1936
1937
// Update the first and last pending times on this root. The new first
1938
// pending time is whatever is left on the root fiber.
1952
const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
1953
if (
1954
lastDiscreteTime !== undefined &&
1788
- remainingExpirationTimeBeforeCommit < lastDiscreteTime
1955
+ !isSameOrHigherPriority(
1956
+ remainingExpirationTimeBeforeCommit,
1957
+ lastDiscreteTime,
1958
+ )
1959
) {
1960
rootsWithPendingDiscreteUpdates.delete(root);
1961
}
2150
}
2151
2152
// Check if there's remaining work on this root
1983
- const remainingExpirationTime = root.firstPendingTime;
1984
- if (remainingExpirationTime !== NoWork) {
2153
+ const remainingExpirationTime = root.firstPendingTime_opaque;
2154
+ if (
2155
+ !isSameExpirationTime(
2156
+ remainingExpirationTime,
2157
+ (NoWork: ExpirationTimeOpaque),
2158
+ )
2159
+ ) {
2160
if (enableSchedulerTracing) {
2161
if (spawnedWorkDuringRender !== null) {
2162
const expirationTimes = spawnedWorkDuringRender;
2187
}
2188
}
2189
2015
- if (remainingExpirationTime === Sync) {
2190
+ if (
2191
+ isSameExpirationTime(remainingExpirationTime, (Sync: ExpirationTimeOpaque))
2192
+ ) {
2193
// Count the number of times the root synchronously re-renders without
2194
// finishing. If there are too many, it indicates an infinite update loop.
2195
if (root === rootWithNestedUpdates) {
2202
nestedUpdateCount = 0;
2203
}
2204
2028
- onCommitRoot(finishedWork.stateNode, expirationTime);
2205
+ onCommitRoot(finishedWork.stateNode, renderPriorityLevel);
2206
2207
// Always call this before exiting `commitRoot`, to ensure that any
2208
// additional work on this root is scheduled.
2253
// the earliest opportunity.
2254
if (!rootDoesHavePassiveEffects) {
2255
rootDoesHavePassiveEffects = true;
2079
- scheduleCallback(NormalPriority, () => {
2256
+ scheduleCallback(NormalSchedulerPriority, () => {
2257
flushPassiveEffects();
2258
return null;
2259
});
2339
2340
function commitLayoutEffects(
2341
root: FiberRoot,
2165
- committedExpirationTime: ExpirationTime,
2342
+ committedExpirationTime: ExpirationTimeOpaque,
2343
) {
2344
// TODO: Should probably move the bulk of this function to commitWork.
2345
while (nextEffect !== null) {
2367
}
2368
2369
export function flushPassiveEffects() {
2193
- if (pendingPassiveEffectsRenderPriority !== NoPriority) {
2370
+ if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
2371
const priorityLevel =
2195
- pendingPassiveEffectsRenderPriority > NormalPriority
2196
- ? NormalPriority
2372
+ pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
2373
+ ? NormalSchedulerPriority
2374
: pendingPassiveEffectsRenderPriority;
2198
- pendingPassiveEffectsRenderPriority = NoPriority;
2375
+ pendingPassiveEffectsRenderPriority = NoSchedulerPriority;
2376
return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
2377
}
2378
}
2382
pendingPassiveProfilerEffects.push(fiber);
2383
if (!rootDoesHavePassiveEffects) {
2384
rootDoesHavePassiveEffects = true;
2208
- scheduleCallback(NormalPriority, () => {
2385
+ scheduleCallback(NormalSchedulerPriority, () => {
2386
flushPassiveEffects();
2387
return null;
2388
});
2398
pendingPassiveHookEffectsMount.push(effect, fiber);
2399
if (!rootDoesHavePassiveEffects) {
2400
rootDoesHavePassiveEffects = true;
2224
- scheduleCallback(NormalPriority, () => {
2401
+ scheduleCallback(NormalSchedulerPriority, () => {
2402
flushPassiveEffects();
2403
return null;
2404
});
2423
}
2424
if (!rootDoesHavePassiveEffects) {
2425
rootDoesHavePassiveEffects = true;
2249
- scheduleCallback(NormalPriority, () => {
2426
+ scheduleCallback(NormalSchedulerPriority, () => {
2427
flushPassiveEffects();
2428
return null;
2429
});
2670
error: mixed,
2671
) {
2672
const errorInfo = createCapturedValue(error, sourceFiber);
2496
- const update = createRootErrorUpdate(rootFiber, errorInfo, Sync);
2673
+ const update = createRootErrorUpdate(
2674
+ rootFiber,
2675
+ errorInfo,
2676
+ (Sync: ExpirationTimeOpaque),
2677
+ );
2678
enqueueUpdate(rootFiber, update);
2498
- const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync);
2679
+ const root = markUpdateTimeFromFiberToRoot(
2680
+ rootFiber,
2681
+ (Sync: ExpirationTimeOpaque),
2682
+ );
2683
if (root !== null) {
2684
ensureRootIsScheduled(root);
2501
- schedulePendingInteractions(root, Sync);
2685
+ schedulePendingInteractions(root, (Sync: ExpirationTimeOpaque));
2686
}
2687
}
2688
2711
const update = createClassErrorUpdate(
2712
fiber,
2713
errorInfo,
2530
- // TODO: This is always sync
2531
- Sync,
2714
+ (Sync: ExpirationTimeOpaque),
2715
);
2716
enqueueUpdate(fiber, update);
2534
- const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
2717
+ const root = markUpdateTimeFromFiberToRoot(
2718
+ fiber,
2719
+ (Sync: ExpirationTimeOpaque),
2720
+ );
2721
if (root !== null) {
2722
ensureRootIsScheduled(root);
2537
- schedulePendingInteractions(root, Sync);
2723
+ schedulePendingInteractions(root, (Sync: ExpirationTimeOpaque));
2724
}
2725
return;
2726
}
2732
export function pingSuspendedRoot(
2733
root: FiberRoot,
2734
wakeable: Wakeable,
2549
- suspendedTime: ExpirationTime,
2735
+ suspendedTime: ExpirationTimeOpaque,
2736
) {
2737
const pingCache = root.pingCache;
2738
if (pingCache !== null) {
2741
pingCache.delete(wakeable);
2742
}
2743
2558
- if (workInProgressRoot === root && renderExpirationTime === suspendedTime) {
2744
+ if (
2745
+ workInProgressRoot === root &&
2746
+ isSameExpirationTime(renderExpirationTime, suspendedTime)
2747
+ ) {
2748
// Received a ping at the same priority level at which we're currently
2749
// rendering. We might want to restart this render. This should mirror
2750
// the logic of whether or not a root suspends once it completes.
2761
if (
2762
workInProgressRootExitStatus === RootSuspendedWithDelay ||
2763
(workInProgressRootExitStatus === RootSuspended &&
2575
- workInProgressRootLatestProcessedEventTime === Sync &&
2764
+ workInProgressRootLatestProcessedEventTime === -1 &&
2765
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2766
) {
2767
// Restart from the root. Don't need to schedule a ping because
2780
return;
2781
}
2782
2594
- const lastPingedTime = root.lastPingedTime;
2595
- if (lastPingedTime !== NoWork && lastPingedTime < suspendedTime) {
2783
+ const lastPingedTime = root.lastPingedTime_opaque;
2784
+ if (
2785
+ !isSameExpirationTime(lastPingedTime, (NoWork: ExpirationTimeOpaque)) &&
2786
+ !isSameOrHigherPriority(lastPingedTime, suspendedTime)
2787
+ ) {
2788
// There's already a lower priority ping scheduled.
2789
return;
2790
}
2791
2792
// Mark the time at which this ping was scheduled.
2601
- root.lastPingedTime = suspendedTime;
2793
+ root.lastPingedTime_opaque = suspendedTime;
2794
2795
ensureRootIsScheduled(root);
2796
schedulePendingInteractions(root, suspendedTime);
2798
2799
function retryTimedOutBoundary(
2800
boundaryFiber: Fiber,
2609
- retryTime: ExpirationTime,
2801
+ retryTime: ExpirationTimeOpaque,
2802
) {
2803
// The boundary fiber (a Suspense component or SuspenseList component)
2804
// previously was rendered in its fallback state. One of the promises that
2805
// suspended it has resolved, which means at least part of the tree was
2806
// likely unblocked. Try rendering again, at a new expiration time.
2615
- if (retryTime === NoWork) {
2807
+ if (isSameExpirationTime(retryTime, (NoWork: ExpirationTimeOpaque))) {
2808
const suspenseConfig = null; // Retries don't carry over the already committed update.
2617
- const currentTime = requestCurrentTimeForUpdate();
2618
- retryTime = computeExpirationForFiber(
2619
- currentTime,
2620
- boundaryFiber,
2621
- suspenseConfig,
2622
- );
2809
+ retryTime = requestUpdateExpirationTime(boundaryFiber, suspenseConfig);
2810
}
2811
// TODO: Special case idle priority?
2812
const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime);
2886
}
2887
2888
function computeMsUntilSuspenseLoadingDelay(
2702
- mostRecentEventTime: ExpirationTime,
2703
- committedExpirationTime: ExpirationTime,
2889
+ mostRecentEventTime: number,
2890
suspenseConfig: SuspenseConfig,
2891
) {
2892
const busyMinDurationMs = (suspenseConfig.busyMinDurationMs: any) | 0;
2897
2898
// Compute the time until this render pass would expire.
2899
const currentTimeMs: number = now();
2714
- const eventTimeMs: number = expirationTimeToMs(mostRecentEventTime);
2900
+ const eventTimeMs: number = mostRecentEventTime;
2901
const timeElapsed = currentTimeMs - eventTimeMs;
2902
if (timeElapsed <= busyDelayMs) {
2903
// If we haven't yet waited longer than the initial delay, we don't
3287
}
3288
}
3289
3104
-function computeThreadID(root, expirationTime) {
3290
+function computeThreadID(
3291
+ root: FiberRoot,
3292
+ expirationTime: ExpirationTimeOpaque,
3293
+) {
3294
// Interaction threads are unique per root and expiration time.
3106
- return expirationTime * 1000 + root.interactionThreadID;
3295
+ // NOTE: Intentionally unsound cast. All that matters is that it's a number
3296
+ // and it represents a batch of work. Could make a helper function instead,
3297
+ // but meh this is fine for now.
3298
+ return (expirationTime: any) * 1000 + root.interactionThreadID;
3299
}
3300
3109
-export function markSpawnedWork(expirationTime: ExpirationTime) {
3301
+export function markSpawnedWork(expirationTime: ExpirationTimeOpaque) {
3302
if (!enableSchedulerTracing) {
3303
return;
3304
}
3309
}
3310
}
3311
3120
-function scheduleInteractions(root, expirationTime, interactions) {
3312
+function scheduleInteractions(
3313
+ root: FiberRoot,
3314
+ expirationTime: ExpirationTimeOpaque,
3315
+ interactions: Set<Interaction>,
3316
+) {
3317
if (!enableSchedulerTracing) {
3318
return;
3319
}
3320
3321
if (interactions.size > 0) {
3126
- const pendingInteractionMap = root.pendingInteractionMap;
3322
+ const pendingInteractionMap = root.pendingInteractionMap_new;
3323
const pendingInteractions = pendingInteractionMap.get(expirationTime);
3324
if (pendingInteractions != null) {
3325
interactions.forEach(interaction => {
3347
}
3348
}
3349
3154
-function schedulePendingInteractions(root, expirationTime) {
3350
+function schedulePendingInteractions(
3351
+ root: FiberRoot,
3352
+ expirationTime: ExpirationTimeOpaque,
3353
+) {
3354
// This is called when work is scheduled on a root.
3355
// It associates the current interactions with the newly-scheduled expiration.
3356
// They will be restored when that expiration is later committed.
3361
scheduleInteractions(root, expirationTime, __interactionsRef.current);
3362
}
3363
3165
-function startWorkOnPendingInteractions(root, expirationTime) {
3364
+function startWorkOnPendingInteractions(
3365
+ root: FiberRoot,
3366
+ expirationTime: ExpirationTimeOpaque,
3367
+) {
3368
// This is called when new work is started on a root.
3369
if (!enableSchedulerTracing) {
3370
return;
3374
// we can accurately attribute time spent working on it, And so that cascading
3375
// work triggered during the render phase will be associated with it.
3376
const interactions: Set<Interaction> = new Set();
3175
- root.pendingInteractionMap.forEach(
3377
+ root.pendingInteractionMap_new.forEach(
3378
(scheduledInteractions, scheduledExpirationTime) => {
3177
- if (scheduledExpirationTime >= expirationTime) {
3379
+ if (isSameOrHigherPriority(scheduledExpirationTime, expirationTime)) {
3380
scheduledInteractions.forEach(interaction =>
3381
interactions.add(interaction),
3382
);
3399
subscriber.onWorkStarted(interactions, threadID);
3400
} catch (error) {
3401
// If the subscriber throws, rethrow it in a separate task
3200
- scheduleCallback(ImmediatePriority, () => {
3402
+ scheduleCallback(ImmediateSchedulerPriority, () => {
3403
throw error;
3404
});
3405
}
3412
return;
3413
}
3414
3213
- const earliestRemainingTimeAfterCommit = root.firstPendingTime;
3415
+ const earliestRemainingTimeAfterCommit = root.firstPendingTime_opaque;
3416
3417
let subscriber;
3418
3424
}
3425
} catch (error) {
3426
// If the subscriber throws, rethrow it in a separate task
3225
- scheduleCallback(ImmediatePriority, () => {
3427
+ scheduleCallback(ImmediateSchedulerPriority, () => {
3428
throw error;
3429
});
3430
} finally {
3431
// Clear completed interactions from the pending Map.
3432
// Unless the render was suspended or cascading work was scheduled,
3433
// In which case– leave pending interactions until the subsequent render.
3232
- const pendingInteractionMap = root.pendingInteractionMap;
3434
+ const pendingInteractionMap = root.pendingInteractionMap_new;
3435
pendingInteractionMap.forEach(
3436
(scheduledInteractions, scheduledExpirationTime) => {
3437
// Only decrement the pending interaction count if we're done.
3438
// If there's still work at the current priority,
3439
// That indicates that we are waiting for suspense data.
3238
- if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) {
3440
+ if (
3441
+ !isSameOrHigherPriority(
3442
+ earliestRemainingTimeAfterCommit,
3443
+ scheduledExpirationTime,
3444
+ )
3445
+ ) {
3446
pendingInteractionMap.delete(scheduledExpirationTime);
3447
3448
scheduledInteractions.forEach(interaction => {
3453
subscriber.onInteractionScheduledWorkCompleted(interaction);
3454
} catch (error) {
3455
// If the subscriber throws, rethrow it in a separate task
3249
- scheduleCallback(ImmediatePriority, () => {
3456
+ scheduleCallback(ImmediateSchedulerPriority, () => {
3457
throw error;
3458
});
3459
}