The Wayback Machine - https://web.archive.org/web/20230316195651/https://github.com/facebook/react/pull/10426
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expiration times #10426

Merged
merged 10 commits into from Oct 10, 2017
Merged

Expiration times #10426

merged 10 commits into from Oct 10, 2017

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Aug 9, 2017

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.

@acdlite acdlite changed the title [Work-in-progress] Assign expiration times to updates [Work-in-progress] Expiration times Aug 9, 2017
@acdlite acdlite mentioned this pull request Aug 10, 2017
17 tasks
@acdlite acdlite force-pushed the expiration branch 3 times, most recently from ee127ed to 796db96 Compare August 10, 2017 22:46
@acdlite
Copy link
Collaborator Author

acdlite commented Aug 11, 2017

Confirms that this makes the Triangle demo work as expected. Still need to write some unit tests, but this is ready for review.

@acdlite acdlite changed the title [Work-in-progress] Expiration times Expiration times Aug 11, 2017
@acdlite
Copy link
Collaborator Author

acdlite commented Aug 11, 2017

Added unit tests. I confirmed that all the existing tests, including the sync DOM ones, are passing when expiration is enabled.

@@ -249,6 +249,11 @@ var TestRenderer = ReactFiberReconciler({
useSyncScheduling: true,

getPublicInstance,

now(): number {
// Test renderer does not use expiration
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebmarkbage I went back and forth on whether this should be an optional method. Landed on making it required because all renderers will need to account for features like expiration boundaries.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since scheduleDeferredCallback is required it seems reasonable that this would be too since they kind of pair together.

@@ -163,6 +163,22 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
return false;
}

// TODO: Better polyfill
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this polyfill sufficient? Or am I overlooking something?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine. Maybe we should colocate it with ReactDOMFrameScheduling to have all these in one place?

It is also performance.now aware: https://github.com/facebook/react/blob/master/src/renderers/shared/ReactDOMFrameScheduling.js#L73

@acdlite acdlite force-pushed the expiration branch 2 times, most recently from 67388a1 to 14e68e3 Compare August 11, 2017 22:08
window.performance &&
typeof window.performance.now === 'function'
) {
now = function() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not now = performance.now and now = Date.now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance.now needs to be bound, but looks like Date.now doesn't. I'll update.

@@ -42,7 +42,7 @@ var {
Fragment,
} = ReactTypeOfWork;
var {Placement, Ref, Update} = ReactTypeOfSideEffect;
var {OffscreenPriority} = ReactPriorityLevel;
var {Never} = ReactFiberExpirationTime;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that this name change decouples this expiration time from the UI context in which it's used. What we really mean is 'this might never finish updating, and that's ok' and that may or may not be literally "offscreen" in the UI.

TaskPriority,
HighPriority,
LowPriority,
OffscreenPriority,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha, I guess it's not a rename after all. ("OffscreenPriority" to "Never" expirationTime)

@reactjs-bot
Copy link

reactjs-bot commented Sep 28, 2017

Deploy preview ready!

Built with commit 564c7e9

https://deploy-preview-10426--reactjs.netlify.com

@acdlite
Copy link
Collaborator Author

acdlite commented Sep 29, 2017

@gaearon I’ve rebased this locally, just haven’t pushed yet. I’ll work on getting all my PRs updated tomorrow.

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have more to review but left some nits in the meantime...

@@ -37,6 +37,13 @@ if (__DEV__) {
}
}

let now: () => number;
if (typeof performance === 'object' && typeof performance.now === 'function') {
now = performance.now.bind(performance);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this trick is always safe cross-browser. Built-ins have all kinds of quirky behavior.

if (typeof performance === 'object' && typeof performance.now === 'function') {
now = performance.now.bind(performance);
} else {
now = Date.now;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure this one works without a bind? Might be better to error on the safe side and just call it with the context instead of being clever.

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 - now();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call is very time sensitive since we call it in a hot loop. We should do everything we can to make it inlinable. Calling a bound function might just be the thing that breaks that. I'd prefer code duplication and simple code here.

@@ -249,6 +249,11 @@ var TestRenderer = ReactFiberReconciler({
useSyncScheduling: true,

getPublicInstance,

now(): number {
// Test renderer does not use expiration
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since scheduleDeferredCallback is required it seems reasonable that this would be too since they kind of pair together.

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = 10;

exports.Done = Done;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about keeping the NoWork name here? I also wonder if the value should be named something that has something that indicates that it is really a dirty flag and that this essentially means false. Done stood out to me as not indicating that well unless you already know how it works.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'm not happy with any of the names here, was just trying to avoid a naming clash. NoWork seems fine, can import the other one as NoWorkPriority if I need to.

const Done = 0;
const Sync = 1;
const Task = 2;
const Never = Number.MAX_SAFE_INTEGER;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This number is way too high. Since VMs (really mostly just V8) tries to optimize for integer types when they can, sometimes they want them to fit into a single 32bit word on 32-bit systems. This number might cause them to deopt that assumption and have to expand the slot.

It should be 2^30 or whatever is the highest we can fit in 31-bit.

// 1 unit of expiration time represents 10ms.
function msToExpirationTime(ms: number): ExpirationTime {
// Always add an offset so that we don't clash with the magic number for Done.
return Math.round(ms / UNIT_SIZE) + MAGIC_NUMBER_OFFSET;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why round? (Faster to do (ms / UNIT_SIZE) | 0.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebmarkbage I am a little confused, ms / UNIT_SIZE) | 0 equals Math.floor rather than Math.round, right?

exports.msToExpirationTime = msToExpirationTime;

function ceiling(num: number, precision: number): number {
return Math.ceil(Math.ceil(num * precision) / precision);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe ... | 0 + 1 would be faster and good enough?

// 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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this unused? I was kind of expecting that we'd completely remove the concept of PriorityLevel and delete the associated files.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean "remove"?We are based on "ReactPriorityLevel" to scheduling, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function used after checking the deadline so we know whether to resume or yield, and after a setState to schedule a callback. https://github.com/acdlite/react/blob/867aeac5819c997bfee0a0ddf7d973624ef5e3d2/src/renderers/shared/fiber/ReactFiberScheduler.js#L1475

PriorityLevel is also used for going the other direction: priority -> expiration time. When scheduling an update.

@@ -801,14 +820,12 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
'a bug in React. Please file an issue.',
);
// We just completed a root. Commit it now.
priorityContext = TaskPriority;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not needed anymore?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than change the priority context every time we start on a new bucket, and then change it to task in the commit phase, and then reset it to the previous value when an error is thrown, I found it was simpler to move this complexity to getPriorityContext. https://github.com/acdlite/react/blob/867aeac5819c997bfee0a0ddf7d973624ef5e3d2/src/renderers/shared/fiber/ReactFiberScheduler.js#L1520-L1552

@sebmarkbage
Copy link
Collaborator

After the nits I think this is good to go.

@acdlite acdlite force-pushed the expiration branch 2 times, most recently from 29498c1 to 0d6a636 Compare October 8, 2017 03:34
@acdlite
Copy link
Collaborator Author

acdlite commented Oct 8, 2017

Addressed nits

@acdlite acdlite force-pushed the expiration branch 2 times, most recently from 2bbd1c5 to ca14382 Compare October 9, 2017 22:05
const Never = Math.pow(2, 31) - 1; // Max int32

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = 10;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this 10 and not 3?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@acdlite Any explanation here?

const NoWork = 0;
const Sync = 1;
const Task = 2;
const Never = Math.pow(2, 31) - 1; // Max int32
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just hardcode the value so we don't have to hope compilers do it for us?

@@ -104,6 +83,7 @@ function createUpdateQueue(): UpdateQueue {
function cloneUpdate(update: Update): Update {
return {
priorityLevel: update.priorityLevel,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we get rid of coalescing we can get rid of this.

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.
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.
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.)
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.
- Rename Done -> NoWork
- Use max int32 instead of max safe int
- Use bitwise operations instead of Math functions
@acdlite acdlite merged commit 3019210 into facebook:master Oct 10, 2017
@acdlite
Copy link
Collaborator Author

acdlite commented Oct 10, 2017

I'll remove coalescing and PriorityLevel in a follow-up.

},
};
var frameDeadlineObject;
if (hasNativePerformanceNow) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we do the judgement again instead calling now() directly?

MQuy added a commit to MQuy/qreact that referenced this pull request Jan 26, 2019
@gaearon gaearon mentioned this pull request Mar 29, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
6 participants