Skip to content

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Oct 21, 2019

Updates of like priority that occur within the same browser event are guaranteed to be batched. This is an important guarantee for event emitter or subscription-like patterns where a single browser event can spawn updates on many different components, all of which need to commit consistently.

To do this, we have a heuristic of marking the first "event time" of an update inside an event. Subsequent updates in the same event will reuse the same event time.

At the end of the event, the event time should be cleared. Failing to clear the event time has the effect of "freezing" the timeline, causing weird expiration quirks.

The event time used to be cleared right before entering the render phase. This mostly works, since every update will eventually be followed by a render task. However, it has some flaws: if there's lots of other non-React main thread work in the mean time, subsequent events could get overbatched. The heuristic is also a bit fragile; it happens to be the case that every call to requestCurrentTime is accompanied by a render task, but that may not necessarily always be the case, creating a subtle refactor hazard.

Instead of relying on the next render task to reset the current event time, this change schedules a microtask.

I didn't use an actual microtask because React does not currently require a Promise polyfill. Instead, I'm using a Scheduler task at immediate priority, which has almost the same semantics. (There's even an open question of whether we should get rid of Scheduler's "immediate" priority in favor of microtasks, but since it currently exists in the browser proposal, too, we can figure that out later.)

See #17159 for context on what prompted this PR

@sizebot
Copy link

sizebot commented Oct 21, 2019

Size changes (experimental)

No significant bundle size changes to report.

Generated by 🚫 dangerJS against 34bba1c

@sizebot
Copy link

sizebot commented Oct 21, 2019

Size changes (stable)

No significant bundle size changes to report.

Generated by 🚫 dangerJS against 34bba1c

@acdlite acdlite force-pushed the reset-event-time-microtask branch from bbd7729 to 43129fa Compare October 21, 2019 17:52
@sebmarkbage
Copy link
Collaborator

From what I gathered from @gaearon in his repo it was indefinitely stalled until returning back to the tab and clicking something. Yet nothing was running on the thread.

In your description you mention that everything is accompanied by render tasks. But if that’s the case, how do we explain his bug? I believe Brian had a similar one. Not sure if we fixed that one.

Seems like we need to truly understand that bug until we have a fix.

Not sure about this micro task thing. We need to cut down on overhead as part of doing something as simple as setting state and we end up with a lot of scheduling now and a lot or computing of expirations.

Is there a simpler model here?

@acdlite
Copy link
Collaborator Author

acdlite commented Oct 21, 2019

@sebmarkbage

Is there a simpler model here?

Believe it or not, I asked myself that question before I opened the PR!

With that out of the way...

But if that’s the case, how do we explain his bug? I believe Brian had a similar one. Not sure if we fixed that one.

It's because the DevTools hook calls requestCurrentTime() inside its onCommit hook, which requestCurrentTime doesn't recognize as coming from inside a React event because it happens after the CommitContext flag gets reset.

https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberDevToolsHook.js#L61

So SiMplEr fixes would include:

  • fixing just this one bad caller in the DevTools hook.
  • not resetting the execution context until after the DevTools hook is called.
  • resetting currentEventTime at both the beginning and end of commitRoot, and at the end of every other React entry point.

I'm not satisfied with those solutions because getting this wrong is subtle and the consequences when it goes wrong are bad.

We need to cut down on overhead as part of doing something as simple as setting state and we end up with a lot of scheduling now and a lot or computing of expirations.

I don't think it adds that much relative overhead because it's only for the first update per task. So in the Flux case, it's constant time overhead. I think the worst case is if you have many separate Scheduler or browser tasks that all schedule individual updates really quickly. I could lower the priority from Immediate down to Normal/Default.

@acdlite
Copy link
Collaborator Author

acdlite commented Oct 21, 2019

I think we should also have separate callers for reading the current time without freezing it, which is only needed when computing an update, versus reading the current time for any other reason.

Perhaps:

  • requestCurrentTimeForUpdate
  • getCurrentTime

The DevTools hook calls `requestCurrentTime` after the commit phase has
ended, which has the accidnental consequence of freezing the start
time for subsequent updates. If enough time goes by, the next update
will instantly expire.

I'll push a fix in the next commit.
Updates of like priority that occur within the same browser event are
guaranteed to be batched. This is an important guarantee for event
emitter or subscription-like patterns where a single browser event can
spawn updates on many different components, all of which need to commit
consistently.

To do this, we have a heuristic of marking the first "event time" of
an update inside an event. Subsequent updates in the same event will
reuse the same event time.

At the end of the event, the event time should be cleared. Failing to
clear the event time has the effect of "freezing" the timeline, causing
weird expiration quirks.

The event time used to be cleared right before entering the render
phase. This mostly works, since every update will eventually be followed
by a render task. However, it has some flaws: if there's lots of other
non-React main thread work in the mean time, subsequent events could
get overbatched. The heuristic is also a bit fragile; it happens to be
the case that every call to `requestCurrentTime` is accompanied by
a render task, but that may not necessarily always be the case, creating
a subtle refactor hazard.

Instead of relying on the next render task to reset the current event
time, this change schedules a microtask.

I didn't use an actual microtask because React does not currently
require the use a Promise polyfill. Instead, I'm using a Scheduler task
at immediate priority, which has almost the same semantics. (There's
even an open question of whether we should get rid of Scheduler's
"immediate" priority in favor of microtasks, but since it currently
exists in the browser proposal, too, we can figure that out later.)
@acdlite acdlite force-pushed the reset-event-time-microtask branch from b486715 to 34bba1c Compare October 21, 2019 18:50
@acdlite
Copy link
Collaborator Author

acdlite commented Oct 21, 2019

Bug that motivated this change was fixed in #17158. I'm still concerned about the refactor hazard but I'll close this for now.

@acdlite acdlite closed this Oct 21, 2019
@gaearon
Copy link
Collaborator

gaearon commented Oct 21, 2019

You mean it was fixed in #17160

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants