[webkit-dev] SharedWorkers alternate design

Drew Wilson atwilson at google.com
Thu May 21 16:20:08 PDT 2009


Hi all,

I'm still waiting to hear back from Ian Hickson regarding an alternate
proposal for SharedWorkers that does not depend on cross-thread MessagePort
support. It does seem that we have general agreement on some basic
principles, including simplifying the worker lifecycle to rely solely on the
existence of an originating window rather than the arcane "reachable via
some past chain of MessagePort entanglements" and eliminating the need for
distributed garbage collection. This has the nice side-effect of allowing us
to continue to proxy network requests to a same-domain active window, as we
currently do for dedicated Workers.

I've updated the SharedWorker design doc to reflect this new approach
pending Ian's feedback - some minor details may change (it's possible that
we may find a way to safely introduce MessagePorts and so won't need to
create a new WorkerPort interface) but the general functioning of the
SharedWorkerRepository should be close to its final format.

I'm starting implementation on this now - please let me know if you have any
feedback.

-atw


WebKit SharedWorker Alternate design
Alternate DesignThis document is derived from the original SharedWorker
design doc<https://docs.google.com/a/google.com/Doc?docid=0AaSJ7ekxGiStZGNuazV2MnZfMTRmNWNkN2dnbg&hl=en>,
but this one is based around the assumption that MessagePort support in
Workers is not an initial requirement and if supported will not impact the
worker lifecycle. This change leads to simplifications in the resulting
design, as described below. Not supporting MessagePorts in Workers means:

   - Messages are sent to SharedWorkers via SharedWorker.postMessage()
   rather than via an exposed port attribute.
   - SharedWorkers receive a port-like object with their connect and message
   events. This port-like object is similar to MessagePorts, except it cannot
   be passed to a new owner via postMessage(), and it does not have the same
   remote entanglement properties that MessagePorts have (the port-like object
   is not directly entangled with anything in page context and so does not
   impact the worker lifecycle).
   - Worker lifecycle is significantly simplified - rather than being based
   on past entanglement with a MessagePort, Worker permissibility is based
   instead on page authorization and reachability (more on this "*
   DocumentSet*" below). Additionally, SharedWorker objects are treated as
   "reachable" as long as they have an event handler registered on them
   (onmessage/onclose) - they do not rely on whether the SharedWorker is idle
   or whether any other page/context holds a reference to the underlying
   worker, so there is no distributed GC required.

Overview
Shared workers (http://dev.w3.org/html5/workers/#shared-workers) are similar
to dedicated workers (which are currently supported by WebKit), with a few
API differences.


   - SharedWorkers are shared - if an application creates a SharedWorker()
   while there's already a non-closing instance of that worker anywhere in the
   browser, then it gets a reference to the existing worker thread.
   - While dedicated Workers have a 1:1 relationship between Worker objects
   and Worker threads, with SharedWorkers there is an N:1 relationship. This
   means that SharedWorker threads receive a connect event containing a
   port-like object which can be used to send messages back to the parent.
   - (not clear how messages are delivered to workers - do they register for
   message events on the port-like objects, or do they get messages via
   onmessage() with the port-like object set as the source of the event?)
   - SharedWorkers have a similar lifecycle to dedicated Workers - workers
   contain a set of authorizing pages (essentially active pages which have
   initiated a worker). As workers fire off other workers, the new worker uses
   the same authorizing DocumentSet as its parent (in the case of pre-existing
   SharedWorkers, the two sets are merged).
   - SharedWorkers do not need to communicate their idle status back to
   their parent contexts - garbage collection of SharedWorker objects is
   independent of the worker thread idle status or reachability of the
   entangled WorkerPort.
   - SharedWorkers have explicit access to the ApplicationCache APIs, while
   dedicated Workers merely inherit the ApplicationCache from their parent
   window.


>From the browser point of view, SharedWorkers are largely indistinguishable
from dedicated Workers. They run in their own SharedWorkerThread with a
SharedWorkerContext both of which derive from common base classes shared
with dedicated WorkerThreads/WorkerContexts. In Chrome, SharedWorkers will
run in a separate process (not in the renderer process) just like dedicated
Workers.
Creating SharedWorkersThe core of our support for SharedWorkers is the
SharedWorkerRepository, which provides a thread-safe interface to a map
whose keys are a combination of SecurityOrigin and workerName, and whose
values are references to SharedWorkerThread objects. The
SharedWorkerRepository is also responsible for tracking which SharedWorker
objects are associated with a given SharedWorkerContext, for the purposes of
sending close events when the worker shuts down.

This section describes the default WebKit implementation of the repository -
Chrome will provide its own implementation whose behavior is similar, but
whose internals are different because it runs in the browser process
(required because it's the only way to provide the necessary
cross-render-process synchronization). The SharedWorkerRepository creates
objects that implement the SharedWorkerContextProxy and
SharedWorkerObjectProxy interfaces - these are used by the SharedWorker and
SharedWorkerContext objects respectively to communicate with their remote
counterparts in a platform-independent nature (Chrome can replace the
default WebKit implementations with versions that support cross-process
messaging). Similar to the current dedicated Worker implementation, the
default WebKit implementation will consist of a single
SharedWorkerMessagingProxy (one per SharedWorker object) that manages the
shared state between the two sides and is guaranteed to stay alive as long
as either side still exists.

class SharedWorkerRepository {
  // Static factory method for getting the correct repository implementation
for a given browser
  static public SharedWorkerRepository* getRepository();

  // Does a synchronous get-or-create of a worker with the specified name.
  public SharedWorkerContextProxy* getWorker(SharedWorker *worker,
                                             SecurityOrigin* origin,
                                             const String& url,
                                             const String& name) = 0;

  // Given a worker thread, fetches a document from its worker set to
perform resource loads (NOTE: This API is incorrect -
  // we need something that will work for Chrome as well ; Suggestions
needed).
  public Document* getActiveDocument(SharedWorkerThread *thread);

  // Invoked from Document::detach() - removes the document from any worker
DocumentSets
  public void documentDetached(Document *document);

  // Invoked when a SharedWorker thread has no pending activity (is going
idle). If there are no associated SharedWorker
  // objects for the thread, then the thread will be shut down.
  public workerThreadIdle(SharedWorkerThread *thread);

  // Invoked when a SharedWorker thread is closing, to remove the worker
from the repository and send close()
  // events to all associated SharedWorker objects.
  public workerThreadClosing(SharedWorkerThread *thread);
}

class SharedWorkerContextProxy {
  // Queues up a connect event for the worker
  void connect();

  // Sends a message to the worker thread. In the future this may be
extended to allow passing MessagePorts as well.
  void postMessageToWorkerContext(const String& message);

  // Invoked when a SharedWorker object is destroyed. This causes the
  // SharedWorker object to be disassociated with the SharedWorker thread in
the repository. Any
  // pending events for the object will be dropped.
  void workerObjectDestroyed();
}

class SharedWorkerObjectProxy {

  // Sends a message to the worker object. In the future this may be
extended to allow passing MessagePorts as well.
  void postMessageToWorkerObject(const String& message);

  // TODO: Do we need a way to send console messages up to the parent?

  // There's no need to expose a workerContextDestroyed() API here - calling
workerThreadClosing() on the repository
  // performs the associated tasks (sending close events, clearing out
references to the thread) for all associated
  // SharedWorkerContextProxy objects.
}


As noted above, the SharedWorkerRepository refers (via the
SharedWorkerContextProxy interface) to the set of all SharedWorkerContext
objects whose *closing* flag is false, in addition to all SharedWorker
objects associated with each SharedWorkerContext.

On the SharedWorker side, we define a WorkerPort, which is the
MessagePort-like object that allows the worker to post/receive messages -
the idl definition would look like:

module threads {

    interface [
        CustomMarkFunction,
        Conditional=WORKERS,
    ] WorkerPort {

        // DedicatedWorkerGlobalScope
        [Custom] void postMessage(in DOMString message);
                 attribute EventListener onmessage;

        // EventTarget interface
        [Custom] void addEventListener(in DOMString type,
                                       in EventListener listener,
                                       in boolean useCapture);
        [Custom] void removeEventListener(in DOMString type,
                                          in EventListener listener,
                                          in boolean useCapture);
        boolean dispatchEvent(in Event evt)
            raises(EventException);
    };

}

Internally, a WorkerPort is associated with a SharedWorkerObjectProxy which
it uses to send messages to the client.


SharedWorkerRepository::getWorker()The SharedWorker constructor passes a
copy of the newly-created object into SharedWorkerRepository::getWorker().
This grabs the repository mutex, and then performs the following steps:

*If SharedWorkerThread for passed origin/name does not exist in map:
    create new SharedWorkerThread object and add to map
If SharedWorkerThread not yet started:
    initiate code load (call SharedWorkerMessagingProxy::loadSourceCode()) Do
we need to do anything special here re: the ApplicationCache, to make sure
we load from the most recent cache rather than from the current context's
cache?
Create SharedWorkerMessagingProxy representing the association between the
SharedWorker object and the SharedWorkerThread and add to list of objects
associated with the SharedWorkerThread.
If the current execution context is a Document
    add the Document to the WorkerThread's DocumentSet
else it is a worker (only needed to support nested workers)
    get the DocumentSet for the current execution context
    merge with the DocumentSet for the SharedWorker and use this DocumentSet
for both workers
return SharedWorkerContextProxy*

The SharedWorker constructor stores away a reference to the
SharedWorkerContextProxy, invokes SharedWorkerContextProxy::connect(), then
returns to the caller.
SharedWorkerMessagingProxy::loadSourceCode()This kicks off the loading of
the source code, the same way that the current Worker constructor does it:

    Document* document = static_cast<Document*>(scriptExecutionContext());

    m_cachedScript = document->docLoader()->requestScript(m_scriptURL,
"UTF-8");

SharedWorkerMessagingProxy::notifyFinished() (code is loaded)

*When the code load is complete:*
*  if code load error:*
*    invoke app error handler directly on SharedWorker object*
*    call SharedWorkerContextProxy::workerObjectDestroyed() to remove
association (see lifecycle discussion below for how WorkerThread is freed)
**    clear reference to SharedWorkerContextProxy
**  else: // code load success*
*    call SharedWorkerContextProxy::scriptLoaded() to send source code to
remote thread*

SharedWorkerMessagingProxy::scriptLoaded():

*Grab repository mutex*
*if workerThread not yet started*
*    start workerThread passing in script
**send any queued up tasks*

*SharedWorkerMessagingProxy::connect()  *

This is responsible for sending the connect event to a given worker thread.
Like SharedWorkerMessagingProxy::postMessageToWorkerContext() below, it
needs to handle the case where the worker thread has not yet been created
(waiting on script to load):

*Grab repository mutex*
*if workerThread already started*
*  send SharedWorkerConnectTask to worker thread*
*else:*
*  add SharedWorkerConnectTask to pending queue (sent in scriptLoaded()
above).
*

SharedWorkerConnectTask::performTask()

*Allocate new WorkerPort and associate with SharedWorkerObjectProxy
Register WorkerPort with the SharedWorkerContext
Invoke the worker's onconnect() handler passing the WorkerPort as the
MessageEvent's port (or source?) attribute*

SharedWorkerMesssagingProxy::postMessageToWorkerContext()

*If pending queue is non-empty (implies thread is not yet started)
    add MessageSharedWorkerContextTask to pending queue
else
    if workerThread != null
        send MessageSharedWorkerContextTask to worker thread
*

MessageSharedWorkerContextTask::performTask()

*Get associated WorkerPort (via SharedWorkerContext::getWorkerPort())
Dispatch MessageEvent to WorkerPort via WorkerPort::dispatchEvent()

*

WorkerPort::postMessage()  - delegates to
SharedWorkerMessagingProxy::postMessageToWorkerObject()

*if parentScriptExecutionContext != null
    Send MessageSharedWorkerTask to parent context*

Open issue: What about console/inspector messages generated by
SharedWorkers. Can we send them off to the console/inspector directly, or do
we need to route them through a document similar to how network requests are
handled?MessageSharedWorkerTask::performTask()

*if (workerObject != null)
    Invoke SharedWorker::dispatchMessage() to invoke the worker's
onmessage() handler. *


SharedWorkerMessagingProxy::workerObjectDestroyed()

*set SharedWorkerMessagingProxy.workerObject = null (stops any pending tasks
from being invoked)
grab repository mutex
remove SharedWorkerMessagingProxy from the repository
The following code can be omitted if we decide to leave SharedWorker threads
running as long as the parent page(s) are active:
if there are no more SharedWorkerMessagingProxy objects associated with the
WorkerThread
    fire off an event at the worker to wake it up (results in
workerThreadIdle() call if no pending activity)
*

SharedWorkerRepository::workerThreadIdle()This is invoked when the worker
thread is idle (no pending activity) - this code may be omitted if we decide
not to shutdown idle SharedWorker threads.

*Grab repository mutex
if there are no SharedWorkerMessagingProxy objects associated with this
SharedWorker
    fire off a close event at the worker
**    invoke workerThreadClosing() to remove the worker from the repository*


SharedWorkerRepository::workerThreadClosing()

*Grab repository mutex
For each SharedWorkerMessagingProxy associated with this SharedWorkerThread
    Send SharedWorkerCloseTask to parent object's context
(SharedWorkerMessagingProxy->m_parentExecutionContext->postTask())
Remove SharedWorkerThread and all associated SharedWorkerMessagingProxy
objects from the repository
** Kick off a timer to terminate the thread if it does not exit promptly*

SharedWorkerRepository::documentDetached()Invoked by Document::detach():

*Grab repository mutex
For each DocumentSet
    remove the document from the set
    if the set is now empty
        for each worker associated with the set:
            fire off a close event at the worker
            invoke workerThreadClosing() to remove the worker from the
repository.
*


SharedWorkerCloseTask::performTask()This just invokes the close() event on
the worker object:

*if (workerObject != null)
**    workerObject->onclose()

*


Network requestsCurrently, all worker XHR requests are proxied to the parent
page and executed on the main thread. This approach currently works for
dedicated workers because there is a 1:1 mapping between dedicated workers
and active pages, and the worker is shutdown when the page closes. For
SharedWorkers (and for dedicated workers once we introduce nested workers)
this is no longer the case - the worker can outlive the parent page.

To address this, we will use the DocumentSet for the worker. Worker XHR
requests will still be proxied to the main thread. This task will request
the DocumentSet from the repository, and select a document from that set to
use to satisfy the request. If the DocumentSet is empty, then it means that
the worker is shutting down, so the XHR should return a failure response.
Closing SharedWorkersShared workers can be closed through various means: by
becoming unreachable, through user action (closing documents), or by
invoking SharedWorkerContext::close().

When a worker is closing by the worker itself calling close(), it is first
disassociated from the repository by invoking
SharedWorkerRepository::workerThreadClosing() which grabs the repository
mutex and performs the actions described previously. This ensures that all
existing SharedWorker objects receive the proper close() notifications, and
that no new SharedWorker objects are associated with the
SharedWorkerContext.

At this point, the SharedWorkerContext is left to manage its own demise, by
queueing a task that fires a close event at the worker global scope. Once
the close event has been fired, WorkerRunLoop.terminate() is invoked to drop
all remaining tasks for the worker and cause the thread to exit, freeing the
SharedWorkerContext. The "kill a worker" algorithm described in section 4.6
of the WebWorkers spec suggests that timeouts may be imposed by the
UserAgent for the close() handler as well as for any tasks that are
executing before the close task is executed. How can we enforce these
timeouts by aborting currently executing script? There is currently a
Chromium bug logged for this issue (
http://code.google.com/p/chromium/issues/detail?id=11672) but it impacts
WebKit as well - we should detect once a worker is closing and invoke the
"kill a worker" algorithm if it does not exit on its own after some time.

The repository is notified when a document is no longer active - when a
worker's DocumentSet is empty, it is no longer permissible and its closing
flag should be set.

SharedWorker objects are treated as "reachable" as long as there is either
an explicit external reference to that object, or if the object itself has
an event handler registered (onclose/onmessage) and the associated thread is
still running. The SharedWorkerRepository keeps a reference count of the
number of SharedWorker objects at any given moment - whenever the worker
thread goes idle (no pending activity) it notifies the repository via
workerThreadIdle() so the worker can be shutdown when there are no more
reachable SharedWorker objects associated with it. Likewise, as SharedWorker
objects become unreachable (are GC'd) they are removed from the repository -
when the count of reachable SharedWorker objects reaches 0, an event is
fired off to the worker thread to initiate an idle check, which will result
in the thread shutting down if appropriate.
Reuse/Refactoring of existing dedicated Worker codeBoth WorkerThread and
WorkerRunLoop can be re-used nearly entirely - we'll need to refactor out
the code in WorkerThread that deals with notifying the parent context with
pending activity/outstanding messages since we don't care about that for
SharedWorkers. We'll need to create a factory method for creating the
WorkerContext, but the rest of the code should work largely verbatim.

Most of WorkerContext should be common between shared and dedicated workers
- there are a few APIs (like postMessage() and dispatchMessage()) that
aren't needed for SharedWorkers, so we'll create a common baseclass that
contains the base functionality and support for items in WorkerGlobalScope
without any of the dedicated/specific shared functionality.

class SharedWorkerContext : public BaseWorkerContext {
   // Support for specific items in SharedWorkerGlobalScope
   public:
     String name() const;
     void setOnconnect(PassRefPtr<EventListener> eventListener) {
         m_onconnectListener = eventListener;
     }
     EventListener* onconnect() const { return m_onconnectListener.get(); }
     // TODO: Add applicationCache functionality
}

Nested WorkersThere are some extra issues that need to be resolved for
nested workers - the initial implementation will not support them but we may
need to address them eventually:

   - With nested workers, the workers should share a single merged
   DocumentSet. In the case of a nested dedicated worker or newly-created
   SharedWorker, the child's previous DocumentSet will be empty so the effect
   is that the child worker will inherit a shared DocumentSet with the parent.
   In the case where the child worker is a pre-existing SharedWorker, the two
   worker DocumentSets are merged into a single shared set.
   - In the case of fire-and-forget workers, it's possible that the parent
   worker thread may no longer exist, in which case there is no thread to run
   any final cleanup code (deletion of the WorkerMessagingProxy). This will
   require some work, although the simplest approach may just be to grab the
   repository mutex when disassociating a worker context/object from the
   WorkerMessagingProxy to avoid race conditions, and route the final cleanup
   via the main thread if the parent context has already shut down.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.webkit.org/pipermail/webkit-dev/attachments/20090521/e1049e61/attachment.html>


More information about the webkit-dev mailing list