Skip to content
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

Workers & SharedWorkers within ServiceWorkers #678

Closed
jakearchibald opened this issue Apr 14, 2015 · 26 comments
Closed

Workers & SharedWorkers within ServiceWorkers #678

jakearchibald opened this issue Apr 14, 2015 · 26 comments
Milestone

Comments

@jakearchibald
Copy link
Contributor

ServiceWorkers should be able to create dedicated workers to do processing work that would otherwise hold up the thread. With canvas-in-workers proposals starting to mature, this could include image pixel manipulation, but may also include complex diffing etc.

Multi-tab apps sometimes use a SharedWorker to ensure only one connection is held to the server, and that worker distributes the data to the appropriate clients. A ServiceWorker should be able to connect to an existing ServiceWorker instance or create a new one, to make use of this single connection. Mozilla have a use-case for their email app.

Workers and SharedWorkers are currently tied to documents, changing this to also allow ServiceWorkers shouldn't be too much of a problem spec-wise.

Since creating a Worker/SharedWorker forms a new client & will select its own registration, workers created within a SW may trigger onfetch within the same ServiceWorker.

@wanderview
Copy link
Member

It would be nice if we could investigate a new ServiceWorker() constructor to handle this case. It seems like it would fit the situation well and would start moving the SW API closer to the other worker APIs.

var sw = new ServiceWorker('./heavy-lifting-sw.js');
sw.postMessage(aLotOfWorkToDo);

@domenic
Copy link
Contributor

domenic commented Apr 14, 2015

@wanderview why new ServiceWorker instead of new Worker? From what I can tell comparing the interfaces, you would get:

  • scriptURL
  • state
  • id
  • onstatechange

and lose

  • onmessage
  • terminate (!)

plus of course you'd have to have a new conceptual service worker and ServiceWorkerGlobalScope with install/activate/fetch events... Seems weird.

@wanderview
Copy link
Member

I guess I don't understand the life cycle of a worker using the ServiceWorker as its parent. A ServiceWorker can be shut down at any time. Does that drop the ref to the worker? Doesn't that defeat the point of all this?

I guess I'm not so confident we can make SharedWorker and Worker function properly with a ServiceWorker parent instead of a document.

@domenic
Copy link
Contributor

domenic commented Apr 14, 2015

Yeah, I don't have any answers to that concern, just was confused what new ServiceWorker inside a service worker would mean...

@jrburke
Copy link

jrburke commented Apr 14, 2015

For the Mozilla email case, I am hoping this sort of relationship would work:

// in service worker that handles background sync notifications
self.onsync = function(event) {
  if (event.registration.id === "periodicSync") {
    event.waitUntil(new Promise(resolve, reject) {
      // This worker connects to the email servers and syncs data to IDB.
      var emailWorker = new SharedWorker("email-worker.js");

      // Listen for a message from the worker to know when it is done
      // with its sync work.
      emailWorker.port.onmessage = function(e) {
        var data = e.data;
        if (data.type === 'syncComplete') {
          // Shared worker complete. Resolve the waitUntil
          // promise to allow this service worker to be
          // shut down. If no other documents or workers
          // have a reference to email-worker.js, then the
          // shared worker would be shut down. If there
          // are other documents/workers with a reference
          // to email-worker.js would keep the shared worker
          // alive. 
          resolve();
        }
      };

      // Start communicating with the worker, ask it to sync.
      // Wait to be messaged back to resolve the waitUntil
      // promise.
      emailWorker.port.start();
      emailWorker.port.postMessage({
        type: 'sync'
      });
    });
  }
};

Just a sketch, things like errors not handled here, and in the email case, we could have multiple accounts on different sync intervals, so probably be a bit more complicated than this sketch, but I was hoping this sort of thing would be possible.

@asutherland
Copy link

In terms of life-cycle for the Mozilla email case (context: which I also work on), if the ServiceWorker needs to be shut down for memory-pressure reasons (or other reasons), it does seem appropriate to reap the email SharedWorker if there's no active documents keeping the SharedWorker alive.

I don't think that would defeat the point, which as I see it (and @jrburke explained on https://github.com/slightlyoff/ServiceWorker/issues/slightlyoff/BackgroundSync#70 quite well), the main benefit for the email app is the single execution context for the back-end that talks to the mail servers and potentially multiple UI pages. The ServiceWorker, like a UI page, is a consumer of the back-end but with the very simple goal in the requestsync case of saying "hey, go sync".

@wanderview
Copy link
Member

if ServiceWorker needs to be shut down for memory-pressure reasons

I think current plans are to shutdown more aggressively than that. Like, after some small amount of time (few seconds) have passed without a SW triggering event.

@asutherland
Copy link

You mean like despite an active waitUntil? The SharedWorker can obviously spam the ServiceWorker with a heartbeat if needed, but I would hope that given the explicit triggering by the https://github.com/slightlyoff/BackgroundSync mechanism that the ServiceWorker could be blessed with a level of runtime goodwill. (Like let's assume that there's a countdown-to-termination timer that gets bumped with events. For BackgroundSync, it seems like it's reasonable to start with a pretty big value, or at least allow the API to make it an option.)

@jungkees
Copy link
Collaborator

I guess I'm not so confident we can make SharedWorker and Worker function properly with a ServiceWorker parent instead of a document.

Me neither. Shared worker/worker's inherent tying to their list of associated documents doesn't seem to easily fit to the service worker's model. Maybe we can think of the possibility where the SW's controlled clients are added to the shared worker's document list through the SW's invocation of the SharedWorker ctor. But I can hardly think of the case we set the parent of the shared worker to the service worker itself. The lifetime of that parent would be that of the registration of the service worker which won't allow the shared worker to be terminated even if the associated documents are unloaded.

@jungkees
Copy link
Collaborator

@jrburke I'm not saying the existing case should be replaced as such, but just curious to know for reasoning. Assuming we have ClientMessageEvent that has waitUntil() (#669 (comment)), can the logic in the shared worker also be written with a service worker? In that, the client code will do navigator.serviceWorker.register("email-worker.js") and use registration.active.postMessage() instead of creating a shared worker. And the email worker can use e.waitUntil(p) in the event listener of the message event.

@jrburke
Copy link

jrburke commented Apr 15, 2015

@jungkees for the email app, it wants to use a shared worker to hold the data layer and convert network fetches to JS objects used by the data layer, and all UI windows talk to this shared worker to get data for its views. That shared worker should live as long as the UI windows live, and be possible to spin it up from the UI windows.

That model would be used even without service workers in play. The user can manually trigger syncs in the UI via a button. Now with service workers we have an automated entry point for backgroundsync and we just want to hand off to that shared worker, which already has all the smarts to do the syncing.

If you are suggesting that the shared worker, email-worker.js, could implement the service worker entry point for backgroundsync and that shared worker could be registered as a service worker, that seems fine as long as:

  • email-worker.js is guaranteed to stay running if the email app's browser windows somehow have a reference to it, and that reference could be acquired while the backgroundsync is waiting to complete (how would that work in the UI window, is that new SharedWorker('email-worker.js') or something else?)
  • the email app could still use a separate service worker to handle fetches for its UI resources.

My impression is that the service worker is not long-lived enough to be able to also function in the same role as our shared worker, and it was actually desirable to make sure the service worker did not do too much to limit its scope and purpose. However if that is not true, then it would be great to walk through a more thorough code example that demonstrates how that would work.

@jungkees
Copy link
Collaborator

Thanks for elaborating on the context and answering to the question @jrburke!

Basically, I agree to the point that the service worker's not designed for long-lived tasks and don't want to deviate from its event based reasoning. But no clear idea about what's a better approach either. So just pondering upon many cases.

About your suggestion - supporting shared worker/worker in service workers, it seems it makes sense to use waitUntil(p) in the way in your example so the SW won't be killed until it receives the result back from the shared worker. But if devs don't use it in this pattern and there has been no document bound to the shared worker, we cannot guarantee the execution of the background process and the lifetime of both the SW and the shared worker.

For the following case:

.. that shared worker could be registered as a service worker, that seems fine as long as:

email-worker.js is guaranteed to stay running if the email app's browser windows somehow have a reference to it,

The SW may run until ClientMessageEvent.waitUntil(p)'s promise settles rather than the document's lifetime. But you can spin up the SW anytime you'd need to by registration.active.postMessage(). In this case, the global state should be kept in persistent storage like IndexedDB. Also even when the document is unloaded, the SW can still hold a reference to it with the ClientMessageEvent object's e.source which is either a WindowClient or a Client. You can communicate to it via Client.postMessage() again.

To clarify, we're counting the case where no tabs are alive when a sync event signals to do the background jobs, right?

and that reference could be acquired while the backgroundsync is waiting to complete (how would that work in the UI window, is that new SharedWorker('email-worker.js') or something else?)

From the document that registered the SW, you always has a reference to it: registration.installing, registration.active, navigator.serviceWorker.controller, etc. They're ServiceWorker objects and have postMessage() to the service worker's global.

FYI. #651 discusses another use case that needs longer lifetime of SWs.

@jrburke
Copy link

jrburke commented Apr 17, 2015

@jungkees:

To clarify, we're counting the case where no tabs are alive when a sync event signals to do the background jobs, right?

Correct, the backgroundsync service worker case could trigger the email sync logic when there are no browser windows showing an document from the email app.

It is good to know that the document has options via navigator.serviceWorker.register and its registration return value. Although, I think the longer term hope is that the service worker(s) used by the app can be registered via the webapp manifest, since we really want the fetch service worker to be able to handle all requests, streamline the service worker bootstrapping. In that scenario, I can see where we would code the HTML/JS for the web pages to not explicitly reference register a service worker, and not explicitly manipulate a registration return value.

We likely should not get into that bootstrapping in this bug, just mentioning it to give more signal to this bug, where I believe just allowing service workers to participate in the shared worker reference refcounting should be enough for the email use case (with the knowledge that waitUntil(p) needs to be used by the service worker to keep it alive long enough to keep the shared worker alive, as you mentioned).

@lewispham
Copy link

IMHO, we should consider ServiceWorker as a replacement for SharedWorker instead of creating some more complex approaches. Whatever the solution is , it should make developer's life easier whenever it can. Do you guys know how hard is it to build an offline web app? APIs like IndexedDB, Appcache (who knows ServiceWorker can replace it), ServiceWorker (Cache API, Push API, BackgroundSync...), SharedWorker, WebSocket, WebRTC... cause a huge amount of time to study. If things keep getting more and more complex like this, how can we web developers fight with the native one?

@jakearchibald
Copy link
Contributor Author

@jrburke's example (#678 (comment)) is exactly how I'd see this working.

Sure, without waitUntil, the ServiceWorker may get killed and take the (Shared)Worker down with it if it's now out of refs, but that seems fine.

Of course, even with waitUntil the browser reserves the right to kill the SW if it suspects foul play, such as using push/sync to be performing bitcoin mining or the like.

@jakearchibald
Copy link
Contributor Author

@Tresdin

IMHO, we should consider ServiceWorker as a replacement for SharedWorker instead of creating some more complex approaches.

If you're using SharedWorker to maintain a single websocket connection for multiple tabs, ServiceWorker is not a good replacement. ServiceWorker should be allowed to close when it isn't handling events, and the websocket use-case requires persistence.

@wanderview
Copy link
Member

Of course, even with waitUntil the browser reserves the right to kill the SW if it suspects foul play, such as using push/sync to be performing bitcoin mining or the like.

But I can create 100 background sync service workers that all attach to the same SharedWorker to try to keep it alive so the SharedWorker can do bitcoin mining? This seems like an uphill battle to me.

It just seems these "heuristics" are going to make a lot of compat issues between browsers and browser version s that will bite developers while still letting people game the system.

Maybe the time has come to be more explicit in the spec about lifecycle of the ServiceWorker?

Or perhaps we should put a CPU throttle on workers without a window. They are useful for I/O things, but if you try to use them for heavy CPU usage you will run slow.

@jrburke
Copy link

jrburke commented May 28, 2015

I am curious about the possible next steps for this issue, to allow service workers to talk to shared workers, and for a service worker to count as a reference count for keeping a shared worker alive.

Is it getting an update to the Shared worker part in the whatwg document, then a corresponding service worker spec change? Although looks like event.waitUntil already exists in the service worker spec, maybe that is enough? Or is it more important to have an implementation try it out first?

If it is about the whatwg document, in section 10.2.6.4:

Step 7 mentions:

Let docs be the list of relevant Document objects to add given the incumbent settings object.

Seems like docs might want to be expanded to some term that encapsulates Document or Service Worker objects, something like owners, but maybe not that exact word. Maybe there is already a spec term for this: "thing that holds a reference to target and if thing goes away and target has no other things with references, target goes away"?

Then these steps adjusted to use those terms:

Step 8.7.7:

Add to worker global scope's list of the worker's Documents the Document objects in docs.

And Step 11:

Add to worker global scope's list of the worker's Documents the Document objects in docs.

If a whatwg doc update is the next step, and if it helps, I can start a thread on the whatwg list about it. If an implementation is best, then I can file a bug for Gecko to track experimenting with it.

@jakearchibald
Copy link
Contributor Author

Is it getting an update to the Shared worker part in the whatwg document,

Yep, I've asked for this at https://www.w3.org/Bugs/Public/show_bug.cgi?id=28504#c4

looks like event.waitUntil already exists in the service worker spec, maybe that is enough?

Yep, this is the right way to signal ongoing activity in a service worker. There are a couple of things we need to think about, will make a separate comment for those.

I can start a thread on the whatwg list about it.

That'd be great! Would show that multiple vendors are interested in this.

@jakearchibald
Copy link
Contributor Author

Requests from a service worker always go to the network, which could be a gotcha here. However, you could get a script from the cache & create a blob url.

caches.match('/my-worker.js')
  .then(workerJS => workerJS.blob())
  .then(blob => URL.createObjectURL(blob))
  .then(objectURL => {
    new Worker(objectURL);
  });

In the case of SharedWorkers, you'd have to use the name param new SharedWorker(blobURL, "my-shared-worker") in both the page and the service worker to ensure the same execution context is used.

Is this acceptable?

Otherwise we have to:

  • White/blacklist the types of requests that do/don't go via a service worker if the request originates from a service worker
  • Figure out which service worker would get the fetch event for requests from a service worker (a SW script url may fall under the scope of a different registration)
  • Make service workers show up in clients.matchAll({ includeUncontrolled: true }), should they become valid request clients

@jrburke
Copy link

jrburke commented Jun 19, 2015

Requests from a service worker always go to the network, which could be a gotcha here. However, you could get a script from the cache & create a blob url.

From my brief inspection in Chrome and Firefox Nightly, it looks like caches is also available in a browser window context, so as long as that will hold true in the future, that works for me. A bit verbose, but happy to start with that and prove it out, thanks for calling out this wrinkle and the example code!

I posted to the whatwg on the shared worker spec issue:
https://lists.w3.org/Archives/Public/public-whatwg-archive/2015Jun/0088.html

@jakearchibald
Copy link
Contributor Author

Related, maybe even a better way of doing this stuff #756

@jrburke
Copy link

jrburke commented Nov 6, 2015

It is unclear to me how #756 helps this situation: that ticket seems to be about allowing multiple service workers to be running. The issue here: we actually only want one thing doing some worker work in a shared worker, that is also shared with browser windows for the app to do the data layer work.

@wanderview
Copy link
Member

It is unclear to me how #756 helps this situation: that ticket seems to be about allowing multiple service workers to be running. The issue here: we actually only want one thing doing some worker work in a shared worker, that is also shared with browser windows for the app to do the data layer work.

It helps in the sense that you can do the heavy processing work inline during background-sync without worrying about blocking other service worker events. In theory those later service worker events could get scheduled on a different SW thread.

@jrburke
Copy link

jrburke commented Nov 6, 2015

It helps in the sense that you can do the heavy processing work inline during background-sync without worrying about blocking other service worker events. In theory those later service worker events could get scheduled on a different SW thread.

That seems to imply loading the code that talks to the server and deals with data models in the service worker as well as the shared worker (or page-specific worker), and probably more coordination to make sure no one duplicates their efforts. If so, I would prefer to just talk to one central authority, like a Shared Worker, to handle that work.

@jakearchibald
Copy link
Contributor Author

Closing in favour of whatwg/html#411

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants