A dive into event loop specification

Event loop specification has two main parts:

Existing browser conformance to the spec

Conformance was tested with a test page that triggers a number of events at once, and records the order in which they are executed. Fullscreen events were not tested.

Here are the results.

  1. Microtasks are purple.
  2. Timeout task is used as an indicator for the first part of the event loop.
  3. rAF callback is used as an indicator to the end of the event loop.

Predictabilty

The test shows that predictability is poor. This is not surprising. Event loop is one of the oldest parts of browser code, and was specified after implementation. The discrepancies are:

1. Promises

2 browsers diverge from the spec: Safari divergence is minor, bug has been filed. Firefox is aware of the problems, and working on it.

2. Event predictability

There are two ways in which event ordering is broken:

2.1 Events are fired outside of the event loop

Confirming this is tricky. "Start of the loop" cannot be observed directly. Timeout task is used as an approximation for loop start. If browser implements timeouts independently of the event loop, this approximation might not hold. This might be what's throwing off Firefox results.

2 browsers diverge from the spec here:

Edge fires matchMedia and resize outside of the event loop.

Firefox fires all events outside of the event loop.

What should be done about this? Are Firefox results accurate?

2.2 Event ordering

Every browser does it a little differently, and none of them completely follow the spec.

either: Should we all fix our ordering to match the spec?

or: Should the spec be fixed instead?

What are the reasons for the current ordering in the spec?

Other issues

Specification maintenability

Every specification that needs a slot in the paint cycle must add another line to event loop spec.

This is fragile. Currently, "run CSS animations and send events", and "run the fullscreen rendering steps" references are missing links. External spec has changed, but event loop was not updated.

We can rewrite the specification. Instead of it being a list of steps, it can become an ordered task queue.

Should we rewrite existing paint spec as an ordered task queue?

If we do, there are few design questions

Task registration is tricky. External specs will have to specify task registration.

Sort order: tasks still need to be sorted. This sort order must be centralized, global?

Throttling other events

Other events might benefit from throttling. ex: mousemove, pointermove, xhr progress.

Existing spec does not define any mechanism for throttled events. Touch events are currently throttled ad-hoc (Edge, Safari, Chrome for Android?).

Should we do it, and how we do event throttling has been discussed, but it is not settled. The main issue is what to do about dropped events. Leading proposal is to keep dropped events available as getCoalescedEvents

Throttling is already being implemented. Are there things we can agree on we could spec?

Event suppression

We should be aware that spec allows suppression of rendering steps for hidden windows.

This might cause trouble if we move non-suppresable events into rendering slot.

Related work

Rendering Processing Model: Rendering pipeline is intertwined with the event loop. Also discusses ordering of style/layout updates.

Task Scheduling for w3ctag: w3ctag

rAF aligned input events in chrome

Existing spec





Existing spec:
### What does existing spec do?

5) Run the resize steps

If doc’s viewport has had its width or height changed (e.g. as a result of the user resizing the browser window, or changing the page zoom scale factor, or an iframe element’s dimensions are changed) since the last time these steps were run, fire an event named resize at the Window object associated with doc.

6) Run the scroll steps
For each item target in doc’s pending scroll event targets, in the order they were added to the list, run these substeps:

If target is a Document, fire an event named scroll that bubbles at target.
Otherwise, fire an event named scroll at target.
Empty doc’s pending scroll event targets.
Fires an event, first target gets notified first

7) Evaluate media queries and report changes

For each MediaQueryList object target that has doc as its document, in the order they were created, oldest first, run these substeps:

If target’s matches state has changed since the last time these steps were run, dispatch a new event to target using the MediaQueryList interface, with its type attribute initialized to change, its isTrusted attribute initialized to true, its media attribute initialized to target’s media, and its matches attribute initialized to target’s matches state.

8) run CSS animations and send events

Not specified.

9)  run the fullscreen rendering steps

Not specified.

10) run the animation frame callbacks

For each entry in callbacks, in order: invoke the callback, passing now as the only argument, and if an exception is thrown, report the exception.

11) run the update intersection observations steps

Let observer list be a list of all IntersectionObservers whose root is in the DOM tree of document.
For each observer in observer list:

12) Paint

## Predictablility OKRs
Concrete standards proposal and tests for event loop behavior
https://github.com/atotic/event-loop
Specify that some events fire just before rAF
Engage other vendors in discussion about which events should be defined to be coupled to rAF, and to what extent their order should be defined Write interop tests for rAF-coupled events

##Existing event loop spec##
  

An event loop must continually run through the following steps for as long as it exists:

  1. Select the oldest task on one of the event loop's task queues, if any, ignoring, in the case of a browsing context event loop, tasks whose associated Documents are not fully active. The user agent may pick any task queue. If there is no task to select, then jump to the microtasks step below.

  2. Set the event loop's currently running task to the task selected in the previous step.

  3. Run: Run the selected task.

  4. Set the event loop's currently running task back to null.

  5. Remove the task that was run in the run step above from its task queue.

  6. Microtasks: Perform a microtask checkpoint.

  7. Update the rendering: If this event loop is a browsing context event loop (as opposed to a worker event loop), then run the following substeps.

    1. Let now be the value that would be returned by the Performance object's now() method.

    2. Let docs be the list of Document objects associated with the event loop in question, sorted arbitrarily except that the following conditions must be met:

      • Any Document B that is nested through a Document A must be listed after A in the list.

      • If there are two documents A and B whose browsing contexts are both nested browsing contexts and their browsing context containers are both elements in the same Document C, then the order of A and B in the list must match the relative tree order of their respective browsing context containers in C.

      In the steps below that iterate over docs, each Document must be processed in the order it is found in the list.

    3. If there are top-level browsing contexts B that the user agent believes would not benefit from having their rendering updated at this time, then remove from docs all Document objects whose browsing context's top-level browsing context is in B.

      Whether a top-level browsing context would benefit from having its rendering updated depends on various factors, such as the update frequency. For example, if the browser is attempting to achieve a 60Hz refresh rate, then these steps are only necessary every 60th of a second (about 16.7ms). If the browser finds that a top-level browsing context is not able to sustain this rate, it might drop to a more sustainable 30Hz for that set of Documents, rather than occasionally dropping frames. (This specification does not mandate any particular model for when to update the rendering.) Similarly, if a top-level browsing context is in the background, the user agent might decide to drop that page to a much slower 4Hz, or even less.

      Another example of why a browser might skip updating the rendering is to ensure certain tasks are executed immediately after each other, with only microtask checkpoints interleaved (and without, e.g., animation frame callbacks interleaved). For example, a user agent might wish to coalesce timer callbacks together, with no intermediate rendering updates.

    4. If there are a nested browsing contexts B that the user agent believes would not benefit from having their rendering updated at this time, then remove from docs all Document objects whose browsing context is in B.

      As with top-level browsing contexts, a variety of factors can influence whether it is profitable for a browser to update the rendering of nested browsing contexts. For example, a user agent might wish to spend less resources rendering third-party content, especially if it is not currently visible to the user or if resources are constrained. In such cases, the browser could decide to update the rendering for such content infrequently or never.

    5. For each fully active Document in docs, run the resize steps for that Document, passing in now as the timestamp.

    6. For each fully active Document in docs, run the scroll steps for that Document, passing in now as the timestamp.

    7. For each fully active Document in docs, evaluate media queries and report changes for that Document, passing in now as the timestamp.

    8. For each fully active Document in docs, run CSS animations and send events for that Document, passing in now as the timestamp.

    9. For each fully active Document in docs, run the fullscreen rendering steps for that Document, passing in now as the timestamp.

    10. For each fully active Document in docs, run the animation frame callbacks for that Document, passing in now as the timestamp.

    11. For each fully active Document in docs, run the update intersection observations steps for that Document, passing in now as the timestamp.

    12. For each fully active Document in docs, update the rendering or user interface of that Document and its browsing context to reflect the current state.

  8. If this is a worker event loop (i.e. one running for a WorkerGlobalScope), but there are no tasks in the event loop's task queues and the WorkerGlobalScope object's closing flag is true, then destroy the event loop, aborting these steps, resuming the run a worker steps described in the Web workers section below.

  9. Return to the first step of the event loop.