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
Expiration times #10426
Conversation
ee127ed
to
796db96
Compare
|
Confirms that this makes the Triangle demo work as expected. Still need to write some unit tests, but this is ready for review. |
|
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 | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
67388a1
to
14e68e3
Compare
| window.performance && | ||
| typeof window.performance.now === 'function' | ||
| ) { | ||
| now = function() { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
da21094
to
43360d9
Compare
0d4f363
to
36244ac
Compare
| @@ -42,7 +42,7 @@ var { | |||
| Fragment, | |||
| } = ReactTypeOfWork; | |||
| var {Placement, Ref, Update} = ReactTypeOfSideEffect; | |||
| var {OffscreenPriority} = ReactPriorityLevel; | |||
| var {Never} = ReactFiberExpirationTime; | |||
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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)
|
Deploy preview ready! Built with commit 564c7e9 |
|
@gaearon I’ve rebased this locally, just haven’t pushed yet. I’ll work on getting all my PRs updated tomorrow. |
There was a problem hiding this 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); | |||
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; | |||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
|
After the nits I think this is good to go. |
29498c1
to
0d6a636
Compare
|
Addressed nits |
2bbd1c5
to
ca14382
Compare
| const Never = Math.pow(2, 31) - 1; // Max int32 | ||
|
|
||
| const UNIT_SIZE = 10; | ||
| const MAGIC_NUMBER_OFFSET = 10; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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, | |||
There was a problem hiding this comment.
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
|
I'll remove coalescing and |
| }, | ||
| }; | ||
| var frameDeadlineObject; | ||
| if (hasNativePerformanceNow) { |
There was a problem hiding this comment.
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?
Related prs: + facebook/react#10715 + facebook/react#10426


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.