Skip to content

Commit

Permalink
[Work-in-progress] Assign expiration times to updates
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Aug 9, 2017
1 parent 5caa65c commit 4af438f
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 13 deletions.
5 changes: 5 additions & 0 deletions src/renderers/art/ReactARTFiberEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,11 @@ const ARTRenderer = ReactFiberReconciler({
);
},

now(): number {
// TODO: Enable expiration by implementing this method.
return 0;
},

useSyncScheduling: true,
});

Expand Down
18 changes: 18 additions & 0 deletions src/renderers/dom/fiber/ReactDOMFiberEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,22 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
return false;
}

// TODO: Better polyfill
let now;
if (
typeof window !== 'undefined' &&
window.performance &&
typeof window.performance.now === 'function'
) {
now = function() {
return performance.now();
};
} else {
now = function() {
return Date.now();
};
}

var DOMRenderer = ReactFiberReconciler({
getRootHostContext(rootContainerInstance: Container): HostContext {
let type;
Expand Down Expand Up @@ -431,6 +447,8 @@ var DOMRenderer = ReactFiberReconciler({
}
},

now: now,

canHydrateInstance(
instance: Instance | TextInstance,
type: string,
Expand Down
5 changes: 5 additions & 0 deletions src/renderers/native/ReactNativeFiberRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,11 @@ const NativeRenderer = ReactFiberReconciler({
},

useSyncScheduling: true,

now(): number {
// TODO: Enable expiration by implementing this method.
return 0;
},
});

module.exports = NativeRenderer;
5 changes: 5 additions & 0 deletions src/renderers/noop/ReactNoopEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ var NoopRenderer = ReactFiberReconciler({
prepareForCommit(): void {},

resetAfterCommit(): void {},

now(): number {
// TODO: Add an API to advance time.
return 0;
},
});

var rootContainers = new Map();
Expand Down
3 changes: 3 additions & 0 deletions src/renderers/shared/fiber/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {HydrationContext} from 'ReactFiberHydrationContext';
import type {FiberRoot} from 'ReactFiberRoot';
import type {HostConfig} from 'ReactFiberReconciler';
import type {PriorityLevel} from 'ReactPriorityLevel';
import type {ExpirationTime} from 'ReactFiberExpirationTime';

var {
mountChildFibersInPlace,
Expand Down Expand Up @@ -75,6 +76,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
hydrationContext: HydrationContext<C>,
scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
recalculateCurrentTime: () => ExpirationTime,
) {
const {
shouldSetTextContent,
Expand All @@ -101,6 +103,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
getPriorityContext,
memoizeProps,
memoizeState,
recalculateCurrentTime,
);

function reconcileChildren(current, workInProgress, nextChildren) {
Expand Down
11 changes: 8 additions & 3 deletions src/renderers/shared/fiber/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import type {Fiber} from 'ReactFiber';
import type {PriorityLevel} from 'ReactPriorityLevel';
import type {ExpirationTime} from 'ReactFiberExpirationTime';

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

Expand Down Expand Up @@ -61,38 +62,42 @@ module.exports = function(
getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
memoizeProps: (workInProgress: Fiber, props: any) => void,
memoizeState: (workInProgress: Fiber, state: any) => void,
recalculateCurrentTime: () => ExpirationTime,
) {
// Class component state updater
const updater = {
isMounted,
enqueueSetState(instance, partialState, callback) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext(fiber, false);
const currentTime = recalculateCurrentTime();
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
addUpdate(fiber, partialState, callback, priorityLevel);
addUpdate(fiber, partialState, callback, priorityLevel, currentTime);
scheduleUpdate(fiber, priorityLevel);
},
enqueueReplaceState(instance, state, callback) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext(fiber, false);
const currentTime = recalculateCurrentTime();
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'replaceState');
}
addReplaceUpdate(fiber, state, callback, priorityLevel);
addReplaceUpdate(fiber, state, callback, priorityLevel, currentTime);
scheduleUpdate(fiber, priorityLevel);
},
enqueueForceUpdate(instance, callback) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext(fiber, false);
const currentTime = recalculateCurrentTime();
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'forceUpdate');
}
addForceUpdate(fiber, callback, priorityLevel);
addForceUpdate(fiber, callback, priorityLevel, currentTime);
scheduleUpdate(fiber, priorityLevel);
},
};
Expand Down
114 changes: 114 additions & 0 deletions src/renderers/shared/fiber/ReactFiberExpirationTime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactFiberExpirationTime
* @flow
*/

'use strict';

import type {PriorityLevel} from 'ReactPriorityLevel';
const {
NoWork,
SynchronousPriority,
TaskPriority,
HighPriority,
LowPriority,
OffscreenPriority,
} = require('ReactPriorityLevel');

const invariant = require('fbjs/lib/invariant');

// TODO: Use an opaque type once ESLint et al support the syntax
export type ExpirationTime = number;

const Done = 0;
exports.Done = Done;

const Never = Infinity;
exports.Never = Infinity;

// 1 unit of expiration time represents 10ms.
function msToExpirationTime(ms: number): ExpirationTime {
// Always add 1 so that we don't clash with the magic number for Done.
return Math.round(ms / 10) + 1;
}
exports.msToExpirationTime = msToExpirationTime;

function expirationTimeToMs(expirationTime: ExpirationTime): number {
return (expirationTime - 1) * 10;
}

function ceiling(time: ExpirationTime, precision: number): ExpirationTime {
return Math.ceil(Math.ceil(time * precision) / precision);
}

// Given the current clock time and a priority level, returns an expiration time
// that represents a point in the future by which some work should complete.
// The lower the priority, the further out the expiration time. We use rounding
// to batch like updates together. The further out the expiration time, the
// more we want to batch, so we use a larger precision when rounding.
function priorityToExpirationTime(
currentTime: ExpirationTime,
priorityLevel: PriorityLevel,
): ExpirationTime {
switch (priorityLevel) {
case NoWork:
return Done;
case SynchronousPriority:
// Return a number lower than the current time, but higher than Done.
return 1;
case TaskPriority:
// Return the current time, so that this work completes in this batch.
return currentTime;
case HighPriority:
// Should complete within ~100ms. 120ms max.
return msToExpirationTime(ceiling(100, 20));
case LowPriority:
// Should complete within ~1000ms. 1200ms max.
return msToExpirationTime(ceiling(1000, 200));
case OffscreenPriority:
return Never;
default:
invariant(
false,
'Switch statement should be exhuastive. ' +
'This error is likely caused by a bug in React. Please file an issue.',
);
}
}
exports.priorityToExpirationTime = priorityToExpirationTime;

// Given the current clock time and an expiration time, returns the
// corresponding priority level. The more time has advanced, the higher the
// priority level.
function expirationTimeToPriorityLevel(
currentTime: ExpirationTime,
expirationTime: ExpirationTime,
): PriorityLevel {
// First check for magic values
if (expirationTime === Done) {
return NoWork;
}
if (expirationTime === Never) {
return OffscreenPriority;
}
const ms = expirationTimeToMs(expirationTime);
if (ms < 0) {
return SynchronousPriority;
}
if (ms === 0) {
return TaskPriority;
}
// Keep this value in sync with priorityToExpirationTime.
if (ms < 120) {
return HighPriority;
}
return LowPriority;
}
exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel;
6 changes: 5 additions & 1 deletion src/renderers/shared/fiber/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
prepareForCommit(): void,
resetAfterCommit(): void,

now(): number,

// Optional hydration
canHydrateInstance?: (instance: I | TI, type: T, props: P) => boolean,
canHydrateTextInstance?: (instance: I | TI, text: string) => boolean,
Expand Down Expand Up @@ -196,6 +198,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
var {
scheduleUpdate,
getPriorityContext,
recalculateCurrentTime,
performWithPriority,
batchedUpdates,
unbatchedUpdates,
Expand Down Expand Up @@ -234,6 +237,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
element.type.prototype != null &&
(element.type.prototype: any).unstable_isAsyncReactComponent === true;
const priorityLevel = getPriorityContext(current, forceAsync);
const currentTime = recalculateCurrentTime();
const nextState = {element};
callback = callback === undefined ? null : callback;
if (__DEV__) {
Expand All @@ -244,7 +248,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
callback,
);
}
addTopLevelUpdate(current, nextState, callback, priorityLevel);
addTopLevelUpdate(current, nextState, callback, priorityLevel, currentTime);
scheduleUpdate(current, priorityLevel);
}

Expand Down
14 changes: 14 additions & 0 deletions src/renderers/shared/fiber/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {FiberRoot} from 'ReactFiberRoot';
import type {HostConfig, Deadline} from 'ReactFiberReconciler';
import type {PriorityLevel} from 'ReactPriorityLevel';
import type {HydrationContext} from 'ReactFiberHydrationContext';
import type {ExpirationTime} from 'ReactFiberExpirationTime';

export type CapturedError = {
componentName: ?string,
Expand Down Expand Up @@ -64,6 +65,8 @@ var {
OffscreenPriority,
} = require('ReactPriorityLevel');

var {msToExpirationTime} = require('ReactFiberExpirationTime');

var {AsyncUpdates} = require('ReactTypeOfInternalContext');

var {
Expand Down Expand Up @@ -160,6 +163,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
hydrationContext,
scheduleUpdate,
getPriorityContext,
recalculateCurrentTime,
);
const {completeWork} = ReactFiberCompleteWork(
config,
Expand All @@ -175,12 +179,16 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
commitDetachRef,
} = ReactFiberCommitWork(config, captureError);
const {
now,
scheduleDeferredCallback,
useSyncScheduling,
prepareForCommit,
resetAfterCommit,
} = config;

// Represents the current time in ms.
let currentTime: ExpirationTime = msToExpirationTime(now());

// The priority level to use when scheduling an update. We use NoWork to
// represent the default priority.
// TODO: Should we change this to an array instead of using the call stack?
Expand Down Expand Up @@ -1491,6 +1499,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
scheduleUpdateImpl(fiber, TaskPriority, true);
}

function recalculateCurrentTime(): ExpirationTime {
currentTime = msToExpirationTime(now());
return currentTime;
}

function performWithPriority(priorityLevel: PriorityLevel, fn: Function) {
const previousPriorityContext = priorityContext;
priorityContext = priorityLevel;
Expand Down Expand Up @@ -1563,6 +1576,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
return {
scheduleUpdate: scheduleUpdate,
getPriorityContext: getPriorityContext,
recalculateCurrentTime: recalculateCurrentTime,
performWithPriority: performWithPriority,
batchedUpdates: batchedUpdates,
unbatchedUpdates: unbatchedUpdates,
Expand Down
Loading

0 comments on commit 4af438f

Please sign in to comment.