1 of 26

Event Delegation to Worker and Worklet

majidvp@, nzolghadr@

2 of 26

Outline

  • Motivation
  • Proposed API
  • Technical Considerations
    • Scrubbing DOM
    • Hit-testing off main
    • Double handling

3 of 26

A Simplified Anatomy of Animation Frame

4 of 26

Event Handling

Dispatch rAF aligned input events and run handlers

Animation Frame

Run user rAF callbacks

Style

Compute new style based on scripted changes and other animations

Layout | Paint

Position all element, and paint them appropriately

Raster | Compositing

Raster painted content and composite (Parallelize and use GPU as much as possible)

Input

Pixels

5 of 26

Mostly Single Thread

  • Limited parallelism in user code
    • All UI code is basically done on main thread.
    • Workers are limited because 1) the type of work that can be done inside them and 2) the input and output has to go through main thread.
  • Parallelism in browser itself to achieve performance
    • Parallel decoding, raster, compositing, scrolling, parsing etc.

6 of 26

Event Handling

Dispatch input events and run event handlers

Animation Frame

Run user rAF callbacks

Style

Compute new style based on scripted changes and other animations

Layout | Paint

Position all element, and paint them appropriately

Raster | Compositing

Raster painted content and composite (Parallelize and use GPU as much as possible)

Input

Pixels

Worker

7 of 26

New Outputs in Worker and Worklets

  • Offscreen Canvas
    • Render into canvas from worker bypassing regular rendering pipeline.
  • Audio Worklet
    • Produce audio output stream for a threaded worklet.
  • Animation Worklet (upcoming)
    • Animate fast-path properties from a threaded worklet.
  • And maybe more...

8 of 26

Event Handling

Dispatch input events and run event handlers

Animation Frame

Run user rAF callbacks

Style

Compute new style based on scripted changes and other animations

Layout | Paint

Position all element, and paint them appropriately

Raster | Compositing

Raster painted content and composite (Parallelize and use GPU as much as possible)

Input

Pixels

Worker + Offscreen Canvas

Animation Worklet

Audio Worklet

Bottleneck

9 of 26

Handling Input in Worker/Worklet?

  • Workers are capable of producing output off-main.
  • However… Input is still a bottleneck!
  • What if we could receive input directly without going through main thread?
  • performance isolation: input => output off main

10 of 26

Event Handling

Dispatch input events and run event handlers

Animation Frame

Run user rAF callbacks

Style

Compute new style based on scripted changes and other animations

Layout | Paint

Position all element, and paint them appropriately

Raster | Compositing

Raster painted content and composite (Parallelize and use GPU as much as possible)

Input

Pixels

Worker + Offscreen Canvas

Animation Worklet

Audio Worklet

Input

11 of 26

Use Cases

  • Low-Latency Drawing and Inking
    • Worker + Offscreen Canvas + Pointer Events
  • Interactive Animations
    • Animation Worklet + Pointer Events
  • Gaming & XR:
    • Worker + Offscreen Canvas + Gamepad + Mouse + Keyboard
    • Worker + Pointer Events + Network streaming
  • Low-Latency Interactive Audio
    • Audio Worklet + Pointer Events

12 of 26

Demo - Main-thread forwarding event using postMessage

13 of 26

Proposal - Passive Event Handling in Worker

  1. Main thread delegates event handling of a target to the worker(s).
  2. Worker registers passive event handler on the delegated target.
    • “Passive” handler: cannot prevent default or propagation.
  3. Events are dispatched to delegated targets in workers.

14 of 26

Proposed API

var t1 =

document.getElementById("target");

var worker = new Worker("worker.js");

worker.addEventTarget(t1);

self.addEventListener("eventtargetadded",(event) => {

// target is t1

event.target.addEventListener(

"pointermove",

(e) => {

// Handle event e

}, {capture: true}

});

Main

Worker

15 of 26

16 of 26

interface mixin EventDelegate {

void addEventTarget(EventTarget target, EventDelegationOptions? option);

void removeEventTarget(EventTarget target);

};

// This dictionary allows us to add options in the future if needed.

dictionary EventDelegationOptions {

any context; // To accommodate for any additional context data from the other thread.

};

interface Worker includes EventDelegate;

interface WorkletAnimation includes EventDelegate;

interface AudioWorkletNode includes EventDelegate;

// The opaque proxy representing an event target in worker.

interface DelegatedEventTarget : EventTarget {};

17 of 26

Scrubbing DOM from Events

  • Worker does not have access to DOM. So we will remove DOM references from Events dispatched in Worker:
  • Event Target
    • dispatchEvent: no-op
  • UIEvents
    • target, currentTarget, sourceElement, composePath, relatedTarget, view: null (or replace with an opaque ID)
    • stopPropagation(), stopImmediatePropagation(), preventDefault(): no-op
    • Coordinates: screenX/Y remain unchanged. Other coordinates may be changed and/or outdated

18 of 26

Hit-Testing Off Main

  • To target events we need to hit-test!
  • Chrome does basic hit-testing on Compositor thread for scrolling (based on layers). Use this to start.
  • May need a more sophisticated hit-testing for pointer events to avoid creating layers for each hit-testable item.

19 of 26

Double Handling Problem

  • Worker cannot prevent default action or propagation.
  • Double handling issue: common pitfall for main and worker to handle the same event twice.
  • Provide declarative methods to prevent default and propagation on main but ultimately up to the application to use appropriately.
    • E.g., worker.addEventTarget(element, {preventDefaultAlways: true, preventPropagationAlways: true}

20 of 26

Polyfillablity

Easy to polyfill using postMessage.

  • Add appropriate event listeners at delegated node.
  • Clone the event and strip any DOM reference.
  • Use post message (or equivalent) to send the event to worker.
  • Dispatch the event in the worker.

21 of 26

What’s Next?

  • Validate the API
    • Find a framework/application to partner with.
    • Implement main thread proof-of-concept (or polyfill) to experiment with.
  • Standardization
    • Find the right standard working group to specify this. Started in WICG (Explainer).

22 of 26

Thanks!

Contact us:

majidvp@chromium.org

@majido

nzolghadr@chromium.org

23 of 26

Why participate in event propagation?

  • The current API forks the event at the “target node” either during bubbling or capturing.
    • Benefit: Gives main thread some control at the cost of performance isolation! Plays nicely with existing conventions for events.
    • Drawback: This can become a footgun, e.g., if one has an event handler up in the DOM tree we have to go to main thread.
  • Alternatively we can fork at root and ignoring all handlers on main
    • No performance footguns but lose the ability for main to prevent propagation.

24 of 26

Alternative Forking Model - fork at root and bypass event propagation altogether

25 of 26

Appendix - Example OffScreen Canvas Drawing - Main

// This canvas could be embedded within an iframe if only the root window events are allowed to be delegated to the worker.<canvas id="canvas"></canvas>��<script>var worker = new Worker("worker.js");var canvas = document.getElementById("canvas")�� var handler = canvas.transferControlToOffscreen();� worker.postMessage({canvas: handler}, [handler]);� worker.addEventTarget(canvas);</script>

26 of 26

Appendix - Example OffScreen Canvas Drawing - Worker

var context;��addEventListener("message", (msg) => {if (msg.data.canvas)� context = msg.data.canvas.getContext("2d");});��addEventListener("eventtargetadded", ({target}) => {� target.addEventListener("pointermove", onPointerMove);});

addEventListener("eventtargetremoved", ({target}) => {� target.removeEventListener("pointermove", onPointerMove);});

function onPointerMove(event){// Use event.clientX/Y or offsetX/Y to draw things on the context.� context.beginPath();� context.arc(event.offsetX, event.offsetY, 5, 0, 2.0* Math.PI, false);� context.closePath();� context.fill();� context.commit();�}