Skip to content

Commit

Permalink
Expiration times (#10426)
Browse files Browse the repository at this point in the history
* [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.

* Replace pendingWorkPriority with expiration times

Instead of a priority, a fiber has an expiration time that represents
a point in the future by which it should render.

Pending updates still have priorities so that they can be coalesced.

We use a host config method to read the current time. This commit
implements everything except that method, which currently returns a
constant value. So this just proves that expiration times work the same
as priorities when time is frozen. Subsequent commits will show the
effect of advancing time.

* Triangle Demo should use a class

shouldComponentUpdate was removed from functional components.

Running the demo shows, now that expiration is enabled, the demo does
not starve. (Still won't run smoothly until we add back the ability to
resume interrupted work.)

* Use a magic value for task expiration time

There are a few cases related to sync mode where we need to distinguish
between work that is scheduled as task and work that is treated like
task because it expires. For example, batchedUpdates. We don't want to
perform any work until the end of the batch, regardless of how much
time has elapsed.

* Use current time to calculate expiration time

* Add unit tests for expiration and coalescing

* Delete unnecessary abstraction

* Move performance.now polyfill to ReactDOMFrameScheduling

* Add expiration to fuzz tester

* Expiration nits

- Rename Done -> NoWork
- Use max int32 instead of max safe int
- Use bitwise operations instead of Math functions
  • Loading branch information
acdlite authored Oct 10, 2017
1 parent 65b16b1 commit 3019210
Show file tree
Hide file tree
Showing 20 changed files with 965 additions and 393 deletions.
81 changes: 42 additions & 39 deletions fixtures/fiber-triangle/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,51 +76,54 @@ <h1>Fiber Example</h1>
}
}

function SierpinskiTriangle({ x, y, s, children }) {
if (s <= targetSize) {
return (
<Dot
x={x - (targetSize / 2)}
y={y - (targetSize / 2)}
size={targetSize}
text={children}
/>
class SierpinskiTriangle extends React.Component {
shouldComponentUpdate(nextProps) {
var o = this.props;
var n = nextProps;
return !(
o.x === n.x &&
o.y === n.y &&
o.s === n.s &&
o.children === n.children
);
return r;
}
var newSize = s / 2;
var slowDown = true;
if (slowDown) {
var e = performance.now() + 0.8;
while (performance.now() < e) {
// Artificially long execution time.
render() {
let {x, y, s, children} = this.props;
if (s <= targetSize) {
return (
<Dot
x={x - (targetSize / 2)}
y={y - (targetSize / 2)}
size={targetSize}
text={children}
/>
);
return r;
}
var newSize = s / 2;
var slowDown = true;
if (slowDown) {
var e = performance.now() + 0.8;
while (performance.now() < e) {
// Artificially long execution time.
}
}
}

s /= 2;
s /= 2;

return [
<SierpinskiTriangle x={x} y={y - (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
<SierpinskiTriangle x={x - s} y={y + (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
<SierpinskiTriangle x={x + s} y={y + (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
];
return [
<SierpinskiTriangle x={x} y={y - (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
<SierpinskiTriangle x={x - s} y={y + (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
<SierpinskiTriangle x={x + s} y={y + (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
];
}
}
SierpinskiTriangle.shouldComponentUpdate = function(oldProps, newProps) {
var o = oldProps;
var n = newProps;
return !(
o.x === n.x &&
o.y === n.y &&
o.s === n.s &&
o.children === n.children
);
};

class ExampleApplication extends React.Component {
constructor() {
Expand Down
2 changes: 2 additions & 0 deletions src/renderers/art/ReactARTFiberEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@ const ARTRenderer = ReactFiberReconciler({
);
},

now: ReactDOMFrameScheduling.now,

useSyncScheduling: true,
});

Expand Down
2 changes: 2 additions & 0 deletions src/renderers/dom/fiber/ReactDOMFiberEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,8 @@ var DOMRenderer = ReactFiberReconciler({
}
},

now: ReactDOMFrameScheduling.now,

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

useSyncScheduling: true,

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

module.exports = NativeRTRenderer;
5 changes: 5 additions & 0 deletions src/renderers/native/ReactNativeFiberRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,11 @@ const NativeRenderer = ReactFiberReconciler({
},

useSyncScheduling: true,

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

module.exports = NativeRenderer;
20 changes: 17 additions & 3 deletions src/renderers/noop/ReactNoopEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ function removeChild(
parentInstance.children.splice(index, 1);
}

let elapsedTimeInMs = 0;

var NoopRenderer = ReactFiberReconciler({
getRootHostContext() {
if (failInBeginPhase) {
Expand Down Expand Up @@ -201,6 +203,10 @@ var NoopRenderer = ReactFiberReconciler({
prepareForCommit(): void {},

resetAfterCommit(): void {},

now(): number {
return elapsedTimeInMs;
},
});

var rootContainers = new Map();
Expand Down Expand Up @@ -336,6 +342,14 @@ var ReactNoop = {
expect(actual).toEqual(expected);
},

expire(ms: number): void {
elapsedTimeInMs += ms;
},

flushExpired(): Array<mixed> {
return ReactNoop.flushUnitsOfWork(0);
},

yield(value: mixed) {
if (yieldedValues === null) {
yieldedValues = [value];
Expand Down Expand Up @@ -400,15 +414,15 @@ var ReactNoop = {
' '.repeat(depth + 1) + '~',
firstUpdate && firstUpdate.partialState,
firstUpdate.callback ? 'with callback' : '',
'[' + firstUpdate.priorityLevel + ']',
'[' + firstUpdate.expirationTime + ']',
);
var next;
while ((next = firstUpdate.next)) {
log(
' '.repeat(depth + 1) + '~',
next.partialState,
next.callback ? 'with callback' : '',
'[' + firstUpdate.priorityLevel + ']',
'[' + firstUpdate.expirationTime + ']',
);
}
}
Expand All @@ -418,7 +432,7 @@ var ReactNoop = {
' '.repeat(depth) +
'- ' +
(fiber.type ? fiber.type.name || fiber.type : '[root]'),
'[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']',
'[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']',
);
if (fiber.updateQueue) {
logUpdateQueue(fiber.updateQueue, depth);
Expand Down
45 changes: 32 additions & 13 deletions src/renderers/shared/ReactDOMFrameScheduling.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ if (__DEV__) {
}
}

const hasNativePerformanceNow =
typeof performance === 'object' && typeof performance.now === 'function';

let now;
if (hasNativePerformanceNow) {
now = function() {
return performance.now();
};
} else {
now = function() {
return Date.now();
};
}

// TODO: There's no way to cancel, because Fiber doesn't atm.
let rIC: (callback: (deadline: Deadline) => void) => number;

Expand Down Expand Up @@ -67,19 +81,23 @@ if (!ExecutionEnvironment.canUseDOM) {
var previousFrameTime = 33;
var activeFrameTime = 33;

var frameDeadlineObject = {
timeRemaining: typeof performance === 'object' &&
typeof performance.now === 'function'
? function() {
// We assume that if we have a performance timer that the rAF callback
// gets a performance timer value. Not sure if this is always true.
return frameDeadline - performance.now();
}
: function() {
// As a fallback we use Date.now.
return frameDeadline - Date.now();
},
};
var frameDeadlineObject;
if (hasNativePerformanceNow) {
frameDeadlineObject = {
timeRemaining() {
// We assume that if we have a performance timer that the rAF callback
// gets a performance timer value. Not sure if this is always true.
return frameDeadline - performance.now();
},
};
} else {
frameDeadlineObject = {
timeRemaining() {
// Fallback to Date.now()
return frameDeadline - Date.now();
},
};
}

// We use the postMessage trick to defer idle work until after the repaint.
var messageKey = '__reactIdleCallback$' + Math.random().toString(36).slice(2);
Expand Down Expand Up @@ -153,4 +171,5 @@ if (!ExecutionEnvironment.canUseDOM) {
rIC = requestIdleCallback;
}

exports.now = now;
exports.rIC = rIC;
Loading

0 comments on commit 3019210

Please sign in to comment.