-
Notifications
You must be signed in to change notification settings - Fork 45
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
Clean up yield inheritance #115
base: main
Are you sure you want to change the base?
Conversation
1. Key the scheduling state based on the {{Scheduler}} to prevent leaking it across (potentially cross-origin) windows. This changes the event loop's continuation state to be a small wrapper around a map. The continuation state is propagated in the same way, but the scheduler state is unique to the scheduler and not shared. In practice there will only be one entry in this map (a task or microtask can only have originated from one task), but the mechanism is generic enough to support other use cases, implementations can optimize this, and the key/value mapping hopefully makes the isolation clear. Alternatively, we could propagate only the state for the current scheduler, but we don't always know the current scheduler, e.g. in "queue a microtask", and this model is different enough from AsyncContext and Chrome's "Task Attribution" that we'd need a separate mechanism, which is a performance concern. The main behavioral difference is how propagating is handled in the case of A --> (B microtask) --> A. With this approach, the context is preserved in the second call to A, which matches the synchronous behavior of A --> calls B --> calls A. 2. Propagate the current scheduling state in "queue a microtask", unless coming from JavaScript, in which case the propagation is handled by the abstract job closure. Previously, the state would be inherited only if it wasn't reset by another microtask or after the postTask callback ran. This fixes the inconsistency, making directly scheduled microtasks match microtasks originating from JavaScript.
7680be8
to
c887dc3
Compare
Hi @smaug---- would you mind taking a look? I'm going to start migrating stuff to HTML, which will make the monkey patching easier to reason about, but I wanted to have a starting point. The indirection is maybe a little overkill, but I think having the map makes the isolation clear, and it can probably be expanded later to support AsyncContext since the microtask propagation should be the same, but TBD. |
Oops, the email got buried underneath other bugmail. I'll take a look later today. |
Sorry, this is taking a bit longer. I don't yet quite understand in which all ways this changes the behavior and how this prevents leaking scheduling behavior to other globals. |
It is the change to microtask queuing I don't understand. How does that work with MutationObservers and such? If task has set the state, and then microtask runs for another global, don't we end up using wrong state? Maybe not, since if window.scheduler.yield() is used, it wouldn't find the state for the scheduler. But otherWindow.scheduler.yield() would work? Wouldn't that be surprising? |
Thanks for looking; ran out of time today, will reply tomorrow. |
@mmocny FYI
Some additional context: I realized that I didn't spec the "queue a microtask" propagation, but we had implemented that in Blink. The motivation for that was primarily to align with Task Attribution (soft navigation metrics) so that microtask behavior was consistent (it made sense there, especially for custom elements), but we can deviate from that. Anyway I wanted to include this bit as a better starting point to discuss.
Right, the state would be set, but it wouldn't be used. MutationObservers are interesting because a single microtask is queued for all mutations, which can include mutations from multiple frames ("pending mutation observers" is per agent), so we can't attribute the microtask to a window/scheduler.
I think the behavior is consistent with the other scenarios:
I think MutationObserver is basically case (1), but with a microtask hop.
Maybe. I'm wary about including MutationObservers (and events / other callbacks) because they're asynchronous with respect to the original code flow -- and therefore not necessarily a continuation (vs. awaiting a promise). Whereas scenario (2) above is more obviously a continuation, so dropping the state is a bit surprising to me. |
Key the scheduling state based on the {{Scheduler}} to prevent leaking it across (potentially cross-origin) windows. This changes the event loop's continuation state to be a small wrapper around a map. The continuation state is propagated in the same way, but the scheduler state is unique to the scheduler and not shared. In practice there will only be one entry in this map (a task or microtask can only have originated from one task), but the mechanism is generic enough to support other use cases, implementations can optimize this, and the key/value mapping hopefully makes the isolation clear.
Alternatively, we could propagate only the state for the current scheduler, but we don't always know the current scheduler, e.g. in "queue a microtask", and this model is different enough from AsyncContext and Chrome's "Task Attribution" that we'd need a separate mechanism, which is a performance concern. The main behavioral difference is how propagating is handled in the case of A --> (B microtask) --> A. With this approach, the context is preserved in the second call to A, which matches the synchronous behavior of A --> calls B --> calls A.
Propagate the current scheduling state in "queue a microtask", unless coming from JavaScript, in which case the propagation is handled by the abstract job closure. Previously, the state would be inherited only if it wasn't reset by another microtask or after the postTask callback ran. This fixes the inconsistency, making directly scheduled microtasks match microtasks originating from JavaScript.
Preview | Diff