[whatwg] Canvas in workers

David Bruant bruant.d at gmail.com
Sun Oct 13 09:01:44 PDT 2013


Le 13/10/2013 06:12, Kyle Huey a écrit :
> I talked at length with Robert O'Callahan about what the DOM API for
> supporting <canvas> in web workers should look like and we came up with the
> following modifications to the spec.
>
>     1. Rename CanvasProxy to WorkerCanvas and only allow it to be
>     transferred to workers.  I don't think we're interested in supporting
>     cross-origin <canvas> via CanvasProxy (I would be curious to hear more
>     about what the use cases are).
>     2. Add a worker-only WorkerCanvas constructor that takes the desired
>     width/height of the drawing surface.
What is the use case for this constructor? Draw something in worker, 
output an image to be sent to main thread?

>     3. Remove the rendering context constructors and the setContext method
>     on WorkerCanvas (née CanvasProxy).
>     4. Copy all of the sensible non-node related things from
>     HTMLCanvasElement to WorkerCanvas.  This would include
>     - width and height as readonly attributes
>        - getContext (to replace what we removed in step 3).  roc prefers to
>        have getContext2D and getContextWebGL, and dispense with the string
>        argument version entirely, but I don't have strong feelings.
For the sake of writing interoperable code in main thread and worker, I 
tend to be against this sort of change. I'm aware of the ugliness of 
some APIs, but consistent ugly APIs beats a mix of beautiful and ugly API.

>        - toBlob.  We do not intend to implement toDataURL here.
>     5. Add a "commit" method to WorkerCanvas.  For a WorkerCanvas obtained
>     from a main thread <canvas> element, this would cause the buffer displayed
>     on screen to swap.  For a WorkerCanvas created *de novo* on a worker
>     thread, it would do nothing.
Let's have this method optional, then? Or create 2 interfaces? I'm not 
sure of what can be expressed in WebIDL to solve this, but useless 
methods aren't a good idea. If I can't do anything, don't give me the 
method. It's like in UI. If I can't click a button, just don't show me 
the button or at least grey it out.


>     This commit method would also commit a minor
>     violation of run-to-completion semantics, described below.
>     6. We would rely on extracting ImageBitmaps from the WorkerCanvas and
>     shipping them to the main thread via postMessage to allow synchronizing
>     canvas updates with DOM updates.  We explored a couple other options but we
>     didn't come up with anything else that allows synchronizing updates to
>     multiple canvases from a worker.  This isn't really sketched out here.
>
> So the IDL would look something like:
>
>> [Constructor(unsigned long width, unsigned long height)]
>>
>> interface WorkerCanvas {
>>
>>    readonly attribute unsigned long width;
>>
>>    readonly attribute unsigned long height;
>>
>>
>>    CanvasRenderingContext2D? getContext2D(any... args);
>>
>>    WebGLRenderingContext? getContextWebGL(any... args);
>>
>>
>>    void toBlob(FileCallback? _callback, optional DOMString type, any...
>> arguments);
>>
>>
>>    bool commit();
Boolean as return value for success? :-s
A promise instead maybe? throw instead of false at least?
In any case, it looks like commit could be a long operation (tell me if 
I'm wrong here. Do you have numbers on how long it takes/would take?), 
having it async sounds reasonable.

>>
>> };
>>
>> WorkerCanvas implements Transferable;
>>
> Everything would be behave pretty much as one would expect, except perhaps
> for the commit method.  The width and height of the canvas can be modified
> on the main thread while the worker is drawing.  This would fire an event
> off to the worker to update the WorkerCanvas's dimensions that would be
> scheduled as if the main thread had postMessage()d something to the
> worker.  But it's possible that the worker would attempt to draw to the
> <canvas> before that update runs.  It's also possible that the worker would
> simply draw in a loop without yielding.  To solve this, if commit is called
> and the current dimensions on the main thread don't match the dimensions of
> the WorkerCanvas it would fail (return false) and update the dimensions of
> the WorkerCanvas before returning.  This is technically a violation of
> run-to-completion semantics, but is needed to support workers that do not
> yield.
I feel fairly strongly against the run-to-completion violation as it's a 
foundation of how JavaScript works, how people reason about programs. 
Also, TC39 is working hard to close the gap between what can be 
expressed in pure ECMAScript and what the web platform does express, 
it'd be inappropriate to add things that widen this gap in my opinion.

I would be much more in favor of a solution like how IndexedDB handles 
transactions. Here is how it could work:
For a canvas which WorkerCanvas has been transfered, when trying to 
change width/height:
*1 Store the values, but don't do it yet
*2 Send a message to the worker to re-dimension,
#1 in the worker: wait for the current message to be taken care of
#2 dispatch a "dimension changed" event (or equivalent)
#3 send a confirmation to main thread
#4 run the listeners
*3 back to main thread: process all committed changes before the 
confirmation
*4 Actually change the dimensions

This guarantees that the worker can always works on a canvas which it 
knows the dimensions of makes life a whole lot easier for authors.

Potentially *3 could be changed to "cancel all unprocessed committed 
changes". If commit returns a promise, the worker could be warned about 
the cancellation.

The downside of this solution is that the worker may take time to yield 
to the event loop, but that results in crappy UX and it's the authors 
responsibility to make sure it doesn't happen.
Also, in my experience, <canvas> most often keep their dimensions, so I 
don't know if it's worth polishing too much this use case.

David



More information about the whatwg mailing list