@@ -18,10 +18,11 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
1818
1919import { noTimeout } from './ReactFiberHostConfig' ;
2020import { createHostRootFiber } from './ReactFiber' ;
21- import { NoWork } from './ReactFiberExpirationTime' ;
21+ import { NoWork , Idle } from './ReactFiberExpirationTime' ;
2222import {
2323 enableSchedulerTracing ,
2424 enableSuspenseCallback ,
25+ enableTrainModelFix ,
2526} from 'shared/ReactFeatureFlags' ;
2627import { unstable_getThreadID } from 'scheduler/tracing' ;
2728import { NoPriority } from './SchedulerWithReactIntegration' ;
@@ -70,6 +71,12 @@ type BaseFiberRootProperties = {|
7071 lastSuspendedTime : ExpirationTime ,
7172 // The next known expiration time after the suspended range
7273 nextKnownPendingLevel : ExpirationTime ,
74+ // Ranges of expiration times each of which must be committed as a single
75+ // batch. Any update that is part of a range must commit at the same time as
76+ // all of the other updates in that range. Ranges do not overlap.
77+ //
78+ // For ranges 1..n, structure is [start1, end1, start2, end2...startN, endN]
79+ pendingRanges : Array < ExpirationTime > | null ,
7380 // The latest time at which a suspended component pinged the root to
7481 // render again
7582 lastPingedTime : ExpirationTime ,
@@ -121,6 +128,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
121128 this . firstSuspendedTime = NoWork ;
122129 this . lastSuspendedTime = NoWork ;
123130 this . nextKnownPendingLevel = NoWork ;
131+ this . pendingRanges = null ;
124132 this . lastPingedTime = NoWork ;
125133 this . lastExpiredTime = NoWork ;
126134
@@ -156,17 +164,79 @@ export function createFiberRoot(
156164 return root ;
157165}
158166
159- export function isRootSuspendedAtTime (
167+ export function getNextRootExpirationTimeToWorkOn (
160168 root : FiberRoot ,
161- expirationTime : ExpirationTime ,
162- ) : boolean {
169+ ) : ExpirationTime {
170+ // Determines the next expiration time that the root should render, taking
171+ // into account levels that may be suspended, or levels that may have
172+ // received a ping.
173+ const lastExpiredTime = resolveTransitionTime ( root , root . lastExpiredTime ) ;
174+ if ( lastExpiredTime !== NoWork ) {
175+ return lastExpiredTime ;
176+ }
177+
178+ // Check if the root is suspended. "Pending" refers to any update that hasn't
179+ // committed yet, including if it suspended. The "suspended" range is
180+ // therefore a subset.
181+ const firstPendingTime = resolveTransitionTime ( root , root . firstPendingTime ) ;
163182 const firstSuspendedTime = root . firstSuspendedTime ;
164183 const lastSuspendedTime = root . lastSuspendedTime ;
165- return (
166- firstSuspendedTime !== NoWork &&
167- firstSuspendedTime >= expirationTime &&
168- lastSuspendedTime <= expirationTime
184+ if (
185+ ! (
186+ firstSuspendedTime !== NoWork &&
187+ firstSuspendedTime >= firstPendingTime &&
188+ lastSuspendedTime <= firstPendingTime
189+ )
190+ ) {
191+ // The highest priority pending time is not suspended. Let's work on that.
192+ return firstPendingTime ;
193+ }
194+
195+ // If the first pending time is suspended, check if there's a lower priority
196+ // pending level that we know about. Or check if we received a ping. Work
197+ // on whichever is higher priority.
198+ const lastPingedTime = root . lastPingedTime ;
199+ const nextKnownPendingLevel = root . nextKnownPendingLevel ;
200+ const nextLevel = resolveTransitionTime (
201+ root ,
202+ lastPingedTime > nextKnownPendingLevel
203+ ? lastPingedTime
204+ : nextKnownPendingLevel ,
169205 ) ;
206+ if (
207+ enableTrainModelFix &&
208+ nextLevel <= Idle &&
209+ firstPendingTime !== nextLevel
210+ ) {
211+ // Don't work on Idle/Never priority unless everything else is committed.
212+ return NoWork ;
213+ }
214+ return nextLevel ;
215+ }
216+
217+ function resolveTransitionTime ( root , expirationTime ) {
218+ if ( expirationTime === NoWork ) {
219+ return NoWork ;
220+ }
221+ const pendingRanges = root . pendingRanges ;
222+ if ( pendingRanges !== null ) {
223+ // Check if the expiration time is part of a transition range. If so, resolve
224+ // it to the end of that range, since that will encompass all the times in the
225+ // entire range. A single pass is sufficient because the ranges do
226+ // not overlap.
227+ let resolvedTime = expirationTime ;
228+ for ( let i = 0 ; i < pendingRanges . length ; i += 2 ) {
229+ const start = pendingRanges [ i ] ;
230+ const end = pendingRanges [ i + 1 ] ;
231+ if ( start >= resolvedTime ) {
232+ if ( resolvedTime > end ) {
233+ resolvedTime = end ;
234+ }
235+ }
236+ }
237+ return resolvedTime ;
238+ }
239+ return expirationTime ;
170240}
171241
172242export function markRootSuspendedAtTime (
@@ -191,6 +261,42 @@ export function markRootSuspendedAtTime(
191261 }
192262}
193263
264+ export function markRootPingedAtTime (
265+ root : FiberRoot ,
266+ expirationTime : ExpirationTime ,
267+ ) : void {
268+ const lastPingedTime = root . lastPingedTime ;
269+ if ( enableTrainModelFix ) {
270+ // Track the lowest priority ping that isn't at Idle priority. Unless
271+ // there are *only* Idle pings. Any non-Idle ping beats an Idle ping.
272+ if ( expirationTime <= Idle ) {
273+ // This is an Idle ping.
274+ if (
275+ lastPingedTime === NoWork ||
276+ ( lastPingedTime <= Idle && lastPingedTime > expirationTime )
277+ ) {
278+ // There are only Idle pings.
279+ root . lastPingedTime = expirationTime ;
280+ }
281+ } else {
282+ // This is a non-Idle ping.
283+ if (
284+ lastPingedTime === NoWork ||
285+ lastPingedTime > expirationTime ||
286+ lastPingedTime <= Idle
287+ ) {
288+ // This is the lowest priority non-Idle ping.
289+ root. lastPingedTime = expirationTime ;
290+ }
291+ }
292+ } else {
293+ // Don't special case Idle pings.
294+ if ( lastPingedTime === NoWork || lastPingedTime > expirationTime ) {
295+ root . lastPingedTime = expirationTime ;
296+ }
297+ }
298+ }
299+
194300export function markRootUpdatedAtTime (
195301 root : FiberRoot ,
196302 expirationTime : ExpirationTime ,
@@ -249,6 +355,21 @@ export function markRootFinishedAtTime(
249355 // Clear the expired time
250356 root . lastExpiredTime = NoWork ;
251357 }
358+
359+ // Clear pending transition ranges
360+ const pendingRanges = root . pendingRanges ;
361+ if ( pendingRanges !== null ) {
362+ for ( let i = 0 ; i < pendingRanges . length ; ) {
363+ const end = pendingRanges [ i + 1 ] ;
364+ if ( finishedExpirationTime <= end ) {
365+ // Remove this range from the set
366+ pendingRanges . splice ( i , 2 ) ;
367+ } else {
368+ // Only increment if we didn't remove the range in the block above.
369+ i += 2 ;
370+ }
371+ }
372+ }
252373}
253374
254375export function markRootExpiredAtTime (
0 commit comments