Skip to content

Commit 7ef34ab

Browse files
committed
[Work-in-progress] Assign expiration times to updates
An expiration time represents a time in the future by which an update should flush. The priority of the update is related to the difference between the current clock time and the expiration time. This has the effect of increasing the priority of updates as time progresses, to prevent starvation. This lays the initial groundwork for expiration times without changing any behavior. Future commits will replace work priority with expiration times.
1 parent 5caa65c commit 7ef34ab

12 files changed

+250
-13
lines changed

src/renderers/art/ReactARTFiberEntry.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,11 @@ const ARTRenderer = ReactFiberReconciler({
534534
);
535535
},
536536

537+
now(): number {
538+
// TODO: Enable expiration by implementing this method.
539+
return 0;
540+
},
541+
537542
useSyncScheduling: true,
538543
});
539544

src/renderers/dom/fiber/ReactDOMFiberEntry.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,22 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
163163
return false;
164164
}
165165

166+
// TODO: Better polyfill
167+
let now;
168+
if (
169+
typeof window !== 'undefined' &&
170+
window.performance &&
171+
typeof window.performance.now === 'function'
172+
) {
173+
now = function() {
174+
return performance.now();
175+
};
176+
} else {
177+
now = function() {
178+
return Date.now();
179+
};
180+
}
181+
166182
var DOMRenderer = ReactFiberReconciler({
167183
getRootHostContext(rootContainerInstance: Container): HostContext {
168184
let type;
@@ -431,6 +447,8 @@ var DOMRenderer = ReactFiberReconciler({
431447
}
432448
},
433449

450+
now: now,
451+
434452
canHydrateInstance(
435453
instance: Instance | TextInstance,
436454
type: string,

src/renderers/native/ReactNativeFiberRenderer.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,11 @@ const NativeRenderer = ReactFiberReconciler({
376376
},
377377

378378
useSyncScheduling: true,
379+
380+
now(): number {
381+
// TODO: Enable expiration by implementing this method.
382+
return 0;
383+
},
379384
});
380385

381386
module.exports = NativeRenderer;

src/renderers/noop/ReactNoopEntry.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,11 @@ var NoopRenderer = ReactFiberReconciler({
204204
prepareForCommit(): void {},
205205

206206
resetAfterCommit(): void {},
207+
208+
now(): number {
209+
// TODO: Add an API to advance time.
210+
return 0;
211+
},
207212
});
208213

209214
var rootContainers = new Map();

src/renderers/shared/fiber/ReactFiberBeginWork.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {HydrationContext} from 'ReactFiberHydrationContext';
1919
import type {FiberRoot} from 'ReactFiberRoot';
2020
import type {HostConfig} from 'ReactFiberReconciler';
2121
import type {PriorityLevel} from 'ReactPriorityLevel';
22+
import type {ExpirationTime} from 'ReactFiberExpirationTime';
2223

2324
var {
2425
mountChildFibersInPlace,
@@ -75,6 +76,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
7576
hydrationContext: HydrationContext<C>,
7677
scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
7778
getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
79+
recalculateCurrentTime: () => ExpirationTime,
7880
) {
7981
const {
8082
shouldSetTextContent,
@@ -101,6 +103,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
101103
getPriorityContext,
102104
memoizeProps,
103105
memoizeState,
106+
recalculateCurrentTime,
104107
);
105108

106109
function reconcileChildren(current, workInProgress, nextChildren) {

src/renderers/shared/fiber/ReactFiberClassComponent.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import type {Fiber} from 'ReactFiber';
1616
import type {PriorityLevel} from 'ReactPriorityLevel';
17+
import type {ExpirationTime} from 'ReactFiberExpirationTime';
1718

1819
var {Update} = require('ReactTypeOfSideEffect');
1920

@@ -61,38 +62,42 @@ module.exports = function(
6162
getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
6263
memoizeProps: (workInProgress: Fiber, props: any) => void,
6364
memoizeState: (workInProgress: Fiber, state: any) => void,
65+
recalculateCurrentTime: () => ExpirationTime,
6466
) {
6567
// Class component state updater
6668
const updater = {
6769
isMounted,
6870
enqueueSetState(instance, partialState, callback) {
6971
const fiber = ReactInstanceMap.get(instance);
7072
const priorityLevel = getPriorityContext(fiber, false);
73+
const currentTime = recalculateCurrentTime();
7174
callback = callback === undefined ? null : callback;
7275
if (__DEV__) {
7376
warnOnInvalidCallback(callback, 'setState');
7477
}
75-
addUpdate(fiber, partialState, callback, priorityLevel);
78+
addUpdate(fiber, partialState, callback, priorityLevel, currentTime);
7679
scheduleUpdate(fiber, priorityLevel);
7780
},
7881
enqueueReplaceState(instance, state, callback) {
7982
const fiber = ReactInstanceMap.get(instance);
8083
const priorityLevel = getPriorityContext(fiber, false);
84+
const currentTime = recalculateCurrentTime();
8185
callback = callback === undefined ? null : callback;
8286
if (__DEV__) {
8387
warnOnInvalidCallback(callback, 'replaceState');
8488
}
85-
addReplaceUpdate(fiber, state, callback, priorityLevel);
89+
addReplaceUpdate(fiber, state, callback, priorityLevel, currentTime);
8690
scheduleUpdate(fiber, priorityLevel);
8791
},
8892
enqueueForceUpdate(instance, callback) {
8993
const fiber = ReactInstanceMap.get(instance);
9094
const priorityLevel = getPriorityContext(fiber, false);
95+
const currentTime = recalculateCurrentTime();
9196
callback = callback === undefined ? null : callback;
9297
if (__DEV__) {
9398
warnOnInvalidCallback(callback, 'forceUpdate');
9499
}
95-
addForceUpdate(fiber, callback, priorityLevel);
100+
addForceUpdate(fiber, callback, priorityLevel, currentTime);
96101
scheduleUpdate(fiber, priorityLevel);
97102
},
98103
};
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Copyright 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule ReactFiberExpirationTime
10+
* @flow
11+
*/
12+
13+
'use strict';
14+
15+
import type {PriorityLevel} from 'ReactPriorityLevel';
16+
const {
17+
NoWork,
18+
SynchronousPriority,
19+
TaskPriority,
20+
HighPriority,
21+
LowPriority,
22+
OffscreenPriority,
23+
} = require('ReactPriorityLevel');
24+
25+
const invariant = require('fbjs/lib/invariant');
26+
27+
// TODO: Use an opaque type once ESLint et al support the syntax
28+
export type ExpirationTime = number;
29+
30+
const Done = 0;
31+
exports.Done = Done;
32+
33+
const Never = Infinity;
34+
exports.Never = Infinity;
35+
36+
// 1 unit of expiration time represents 10ms.
37+
function msToExpirationTime(ms: number): ExpirationTime {
38+
// Always add 1 so that we don't clash with the magic number for Done.
39+
return Math.round(ms / 10) + 1;
40+
}
41+
exports.msToExpirationTime = msToExpirationTime;
42+
43+
function expirationTimeToMs(expirationTime: ExpirationTime): number {
44+
return (expirationTime - 1) * 10;
45+
}
46+
47+
function ceiling(time: ExpirationTime, precision: number): ExpirationTime {
48+
return Math.ceil(Math.ceil(time * precision) / precision);
49+
}
50+
51+
// Given the current clock time and a priority level, returns an expiration time
52+
// that represents a point in the future by which some work should complete.
53+
// The lower the priority, the further out the expiration time. We use rounding
54+
// to batch like updates together. The further out the expiration time, the
55+
// more we want to batch, so we use a larger precision when rounding.
56+
function priorityToExpirationTime(
57+
currentTime: ExpirationTime,
58+
priorityLevel: PriorityLevel,
59+
): ExpirationTime {
60+
switch (priorityLevel) {
61+
case NoWork:
62+
return Done;
63+
case SynchronousPriority:
64+
// Return a number lower than the current time, but higher than Done.
65+
return 1;
66+
case TaskPriority:
67+
// Return the current time, so that this work completes in this batch.
68+
return currentTime;
69+
case HighPriority:
70+
// Should complete within ~100ms. 120ms max.
71+
return msToExpirationTime(ceiling(100, 20));
72+
case LowPriority:
73+
// Should complete within ~1000ms. 1200ms max.
74+
return msToExpirationTime(ceiling(1000, 200));
75+
case OffscreenPriority:
76+
return Never;
77+
default:
78+
invariant(
79+
false,
80+
'Switch statement should be exhuastive. ' +
81+
'This error is likely caused by a bug in React. Please file an issue.',
82+
);
83+
}
84+
}
85+
exports.priorityToExpirationTime = priorityToExpirationTime;
86+
87+
// Given the current clock time and an expiration time, returns the
88+
// corresponding priority level. The more time has advanced, the higher the
89+
// priority level.
90+
function expirationTimeToPriorityLevel(
91+
currentTime: ExpirationTime,
92+
expirationTime: ExpirationTime,
93+
): PriorityLevel {
94+
// First check for magic values
95+
if (expirationTime === Done) {
96+
return NoWork;
97+
}
98+
if (expirationTime === Never) {
99+
return OffscreenPriority;
100+
}
101+
if (expirationTime < currentTime) {
102+
return SynchronousPriority;
103+
}
104+
if (expirationTime === currentTime) {
105+
return TaskPriority;
106+
}
107+
// Keep this value in sync with priorityToExpirationTime.
108+
if (expirationTimeToMs(expirationTime) < 120) {
109+
return HighPriority;
110+
}
111+
return LowPriority;
112+
}
113+
exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel;

src/renderers/shared/fiber/ReactFiberReconciler.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
124124
prepareForCommit(): void,
125125
resetAfterCommit(): void,
126126

127+
now(): number,
128+
127129
// Optional hydration
128130
canHydrateInstance?: (instance: I | TI, type: T, props: P) => boolean,
129131
canHydrateTextInstance?: (instance: I | TI, text: string) => boolean,
@@ -196,6 +198,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
196198
var {
197199
scheduleUpdate,
198200
getPriorityContext,
201+
recalculateCurrentTime,
199202
performWithPriority,
200203
batchedUpdates,
201204
unbatchedUpdates,
@@ -234,6 +237,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
234237
element.type.prototype != null &&
235238
(element.type.prototype: any).unstable_isAsyncReactComponent === true;
236239
const priorityLevel = getPriorityContext(current, forceAsync);
240+
const currentTime = recalculateCurrentTime();
237241
const nextState = {element};
238242
callback = callback === undefined ? null : callback;
239243
if (__DEV__) {
@@ -244,7 +248,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
244248
callback,
245249
);
246250
}
247-
addTopLevelUpdate(current, nextState, callback, priorityLevel);
251+
addTopLevelUpdate(current, nextState, callback, priorityLevel, currentTime);
248252
scheduleUpdate(current, priorityLevel);
249253
}
250254

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {FiberRoot} from 'ReactFiberRoot';
1717
import type {HostConfig, Deadline} from 'ReactFiberReconciler';
1818
import type {PriorityLevel} from 'ReactPriorityLevel';
1919
import type {HydrationContext} from 'ReactFiberHydrationContext';
20+
import type {ExpirationTime} from 'ReactFiberExpirationTime';
2021

2122
export type CapturedError = {
2223
componentName: ?string,
@@ -64,6 +65,8 @@ var {
6465
OffscreenPriority,
6566
} = require('ReactPriorityLevel');
6667

68+
var {msToExpirationTime} = require('ReactFiberExpirationTime');
69+
6770
var {AsyncUpdates} = require('ReactTypeOfInternalContext');
6871

6972
var {
@@ -160,6 +163,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
160163
hydrationContext,
161164
scheduleUpdate,
162165
getPriorityContext,
166+
recalculateCurrentTime,
163167
);
164168
const {completeWork} = ReactFiberCompleteWork(
165169
config,
@@ -175,12 +179,16 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
175179
commitDetachRef,
176180
} = ReactFiberCommitWork(config, captureError);
177181
const {
182+
now,
178183
scheduleDeferredCallback,
179184
useSyncScheduling,
180185
prepareForCommit,
181186
resetAfterCommit,
182187
} = config;
183188

189+
// Represents the current time in ms.
190+
let currentTime: ExpirationTime = msToExpirationTime(now());
191+
184192
// The priority level to use when scheduling an update. We use NoWork to
185193
// represent the default priority.
186194
// TODO: Should we change this to an array instead of using the call stack?
@@ -1491,6 +1499,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
14911499
scheduleUpdateImpl(fiber, TaskPriority, true);
14921500
}
14931501

1502+
function recalculateCurrentTime(): ExpirationTime {
1503+
currentTime = msToExpirationTime(now());
1504+
return currentTime;
1505+
}
1506+
14941507
function performWithPriority(priorityLevel: PriorityLevel, fn: Function) {
14951508
const previousPriorityContext = priorityContext;
14961509
priorityContext = priorityLevel;
@@ -1563,6 +1576,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
15631576
return {
15641577
scheduleUpdate: scheduleUpdate,
15651578
getPriorityContext: getPriorityContext,
1579+
recalculateCurrentTime: recalculateCurrentTime,
15661580
performWithPriority: performWithPriority,
15671581
batchedUpdates: batchedUpdates,
15681582
unbatchedUpdates: unbatchedUpdates,

0 commit comments

Comments
 (0)