Currently gecko has a bug where multiple service worker threads can be spun up for the same registration. This can happen when windows in separate child processes open documents controlled by the same registration. Per the spec, the FetchEvents should be dispatched to the same worker instance. In practice, we spin up different work threads for each child process and dispatch there.
While discussing how to fix this, we started to wonder if its really a problem. Service workers already discourage shared global state since the thread can be killed at any time. If the service worker script is solely using Cache, IDB, etc to store state, then running multiple thread instances is not a problem.
It seems the main issue where this could cause problems is for code that wants to use global variables while holding the worker alive using waitUntil() or respondWith(). This is theoretically observable by script, but seems to have limited useful applications.
How would people feel about loosening the current spec to allow a browser to spin up more than one worker thread for a single registration? When to execute multiple threads and how many would be up to the browser implementation.
Service workers already discourage shared global state since the thread can be killed at any time. If the service worker script is solely using Cache, IDB, etc to store state, then running multiple thread instances is not a problem.
I really like this.
Are there any apparent "low-hanging fruit" gains expected from allowing this, except for the obvious gains from reduced complexity, not having to make sure there is only one thread running of the SW script?
I can see this becoming a "want"-ed feature in the (maybe-not-so-distant) future where very high number of (potentially low- or medium-speed) cores will be present in mainstream devices, and sites with complex SW-s become a thing _(a sort-of client-side server, if you will)_. In those cases I can see this becoming an actually useful features, just like how load-balancing between stateless (or sticky-state) high-demand servers operate now.
My initial reaction was that allowing this would never work. There are too many places in the spec that assume there is only one active ServiceWorker, and all kinds of things would break in confusing ways if the ServiceWorker you can postMessage to via controller.postMessage is not necessarily the same worker as the worker who is handling fetch events (or there might even be multiple workers handling fetch events for a single client). But after thinking about it a bit more I don't think any of the problems that would occur by allowing multiple copies of the same service worker to be running at the same time are truly insurmountable.
But I'm also not sure if there really is much value in allowing this. I imagine this could be useful in situations where a serviceworker wants to do some relatively heavy CPU bound task in the process of handling some fetch (or other) event, and we don't want to block other fetch requests on that task?
I think what we should be supporting for such situations is that the SW can spin up a dedicated/shared worker, whose lifetime is linked to the lifetime of the SW (or maybe to the event it is currently handling, but just the SW would work fine; after all it won't get killed until the fetch event the processing is for has finished), just like a regular website would have to spin up a separate worker to do heavy CPU bound tasks in response to an event.
And if we want to allow long-running CPU bound tasks in response to an event, which last past a response has been send to a fetch event, I'm not sure if those should be allowed without some kind of UI presence (for example allowing a new kind of Notification with progress indicator to serve as the UI that keeps a shared worker alive).
To be clear, I'm only suggesting we state the browser may spin up multiple SW instances, but not make it a major feature. We would still spec explicit solutions for heavy CPU tasks, etc.
I just question if maintaining one-and-only-one instance is worth the code complexity and perf hit in a multi-process browser architecture.
Can chrome produce multiple service worker instances for different content processes today? Or do you already have a solution to guarantee a single SW instance across the entire browser?
Chrome already guarantees a single SW instance across the entire browser. Spinning up multiple SW instances does seem like something worth exploring though. I do think we'll have to clarify how various things that return a ServiceWorker instance deal with this, and what happens if you postMessage to them.
In particular this seems like it could be helpful in cases of misbehaving service workers (where we can then fire up a new SW and start sending fetch events there, without having to wait for some timeout blocking all other fetches), or just in general in situations where there is a performance concern (foreign fetch comes to mind as well; having potentially every website you're browsing to try to fetch something from the same SW could result in noticeable slowness).
We've discussed this before a few times. I've always been pretty keen on it, but if memory serves @slightlyoff was less keen.
I'm thinking of http2-optimised pages made up of 100s of requests, is there a performance benefit to multiple workers?
IMHO, this is an implementation detail low enough to not live at the spec level. In the spec we should specify some warranties, for example saying that _logically_ there is only one service worker per scope although it could be implemented as several ones.
It has to be in the spec becauae it's observable with the right combination of waitUntil(), postMessage(), and worker global variables.
By sending global state via postMessage() or respondWith()? If so, well, I suppose it could be a recommendation.
You use set some global state in the SW, then use an evt.waitUntil() to hold the SW alive, and then send a postMessage() or other event triggering mechanism that checks the global state.
Some thoughts on our options here:
I'd prefer the first, but the others are ways out if it breaks the web in ways we can't work around.
I agree that 1 would be the preferred outcome.
Another variant of 2 could be to make concurrency a per-client thing. So all message/fetch events related to the same client would always end up in the same worker, but other events can end up anywhere. That might have the lowest risk of breaking things, would probably address the browser-vendor implementation concerns, but obviously wouldn't allow some of the optimisations that full concurrency could, as it might very well be beneficial to process multiple fetch events from the same client in multiple threads.
From an implementor's point of view (1) is very attractive. It gives us the most flexibility to optimize with the smallest amount of complexity. Options (2) and (3) restrict us more and add more complexity to support unique worker instances across processes.
all message/fetch events related to the same client would always end up in the same worker, but other events can end up anywhere … wouldn't allow some of the optimisations that full concurrency could
Agreed, that's what lead me to 2. That way you would have concurrency with fetch, but not with message/push. We'll see what users think. We don't need to consider 2/3 unless 1 is unattainable.
If we did multiple SW instances I think we could maybe still support one-to-one messaging with the instances. For example, when a SW receives a fetch event it could construct a new MessageChannel. It could then postMessage() the channel back to the client. The client could then use the channel to communicate with that single instance directly.
I think that would work, but maybe I don't fully understand MessageChannel.
I also have some local patches that spin up N thread instances for each service worker. Let me know if there is a particular site that I should test.
https://www.flipkart.com/ might be an interesting one to try.
Yeah your MessageChannel idea would work.
Having the SW send the MessageChannel to the client in its onfetch event works, provided that:
Yeah, that's the plan. clients.get(fetchEvent.reservedClientId) would get you a client and posted messages would be buffered.
Some more thoughts:
Postmessaging to a SW will land in one instance, whereas SharedWorker and BroadcastChannel will expose multiple running instances. Probably not an issue, but wanted to write it down while I remembered.
In theory, ServiceWorker.postMessage() could spawn a new instance each time. Right?
Yeah, whereas BroadcastChannel could land in multiple instances at once.
Just wanted to chime in that I also support option 1 as well.
Just leaving some feedback related to:
https://jakearchibald.com/2016/service-worker-meeting-notes/
Are you maintaining global state within the service worker? Would this change break your stuff? If so, could you use a more persistent storage like shared workers / IndexedDB instead?
So far we have been already using indexedDB (with localForage for simple key/value) the fact that the SW can be killed was strong enough to not to keep states on memory and persist them.
Re: the PR above. We were using persistent state stored in a variable, but have been able to switch to indexedDB. In time it'd be nice to have a higher level API for syncing shared state between sw instances as polling an indexedDB store is a bit hacky, and accessing indexedDB on every fetch event wouldn't be particularly performant. Similarly, I'd love to see a cookies API land in the near future - we're considering implementing something similar to what we've done for feature flags for cookies in the meantime
I use Clients.claim a lot just to choke down the simplicity of the SW life cycle (which for me has always been the most complicated aspect of dealing with Service Workers). How would that work with a litany of SWs claiming the client in rapid succession?
Compared to the folks on the list that are working on this, I think I would have a hard time pouring water out of a bucket with instructions on the bottom. So feel free to take this with a giant grain of salt... but what if there were a new kind of Worker ( a FetchWorker ) that could be provided as an argument to onfetch. Like self.on('fetch', new FetchWorker('fetch.js' )). That way you could parallelize the hell out of that FetchWorker without making everyone manage state in SWs with IndexedDB... which (speaking on behalf of the most obtuse 5% of SW users) sounds about as much fun as being a rattlesnake in a room full of rocking chairs. It would also cleave nicely with the "Progressive Enhancement" philosophy of ServiceWorkers.
I use Clients.claim a lot just to choke down the simplicity of the SW life cycle (which for me has always been the most complicated aspect of dealing with Service Workers). How would that work with a litany of SWs claiming the client in rapid succession?
We would still only allow one instance of the SW to run the install and activate events at a single time. Updates would be single threaded. I mention this because thats when most SW's call clients.claim().
If you call clients.claim() on every FetchEvent, though, I think it would still work. It would be about the same amount of work whether we have a single instance or multiple instances. You have to look across all processes at all windows to potentially mark them as controlled. That workload does not really change with more running service worker instances.
If you are doing heavy js or clients.claim() at the top level script evaluation, though, then you would likely see a performance hit from spinning up more thread instances.
@wanderview Thank you for taking the time to explain that. Makes more sense now.
Follow-up question: I've used patterns in the past of "hanging" a service worker mid-lifecycle with event.waitUntil until "things" happen (some record appears in IndexedDB, a message from the client... see https://github.com/bkimmel/bkimmel.github.io/blob/master/serviceworker_bgs/sw.js ). It seems like if there were multiple SWs running, those wouldn't really be "usable" patterns anymore, right? Not a huge deal: I don't have anything in production doing that, but I guess this concurrent stuff would mean I'd have to think about those sort of creative applications a little bit differently, right?
Edit: Also: How would MessageChannel ports work in a concurrent situation? I like to use MessageChannels to communicate with SWs from the client and I'm having trouble understanding how that would work once I dispatch the message from the window and give port2 to the SW... would the concurrent workers all "share" the port2 I sent to the SW?
Several thoughts on allowing multiple SW instances.
Result: 2 requests to the server, 2 caching operations ? Or i miss something ?
Follow-up question: I've used patterns in the past of "hanging" a service worker mid-lifecycle with event.waitUntil until "things" happen (some record appears in IndexedDB, a message from the client... see https://github.com/bkimmel/bkimmel.github.io/blob/master/serviceworker_bgs/sw.js ). It seems like if there were multiple SWs running, those wouldn't really be "usable" patterns anymore, right? Not a huge deal: I don't have anything in production doing that, but I guess this concurrent stuff would mean I'd have to think about those sort of creative applications a little bit differently, right?
I believe you can use IDB transactions to achieve this.
Edit: Also: How would MessageChannel ports work in a concurrent situation? I like to use MessageChannels to communicate with SWs from the client and I'm having trouble understanding how that would work once I dispatch the message from the window and give port2 to the SW... would the concurrent workers all "share" the port2 I sent to the SW?
When you use navigator.serviceWorker.postMessage() you would only see the message event in a single SW thread instance. You would not be able to control which instance receives the postMessage.
If you want to communicate with all SW thread instances you could use BroadcastChannel.
If you want to communicate with a particular thread instance you would need to have the SW thread create the MessageChannel first and send it back to the window Client. The SW thread could then keep itself alive with waitUntil() until the window messages back, etc.
Result: 2 requests to the server, 2 caching operations ? Or i miss something ?
Yes, you could have 2 requests to the server. But this is no different than with a single thread handling two overlapping FetchEvents.
This open the door to create a process for each fetch request ?
It would leave it up to the browser to decide where to dispatch the FetchEvent. It would be allowed to spawn a new process for each one, but I think its highly unlikely any browser would actually implement it that way. We would be trying to focus on running it where the FetchEvent would have the quickest response time.
As said by wheresrhys, call indexedDB to store globally a state on each fetch or postmessage event can become a pb for performances ?
The question is, are people relying heavily on global state today given that the service worker can be killed whenever you are out of the waitUntil() anyway? If you are doing anything with global state outside of waitUntil then you need to use IDB even with a single SW thread.
We're trying to understand what the use cases for global state across events within a waitUntil() are today. Without seeing the use cases its hard to know if IDB is too heavy-weight.
We would like to use global state for service workers. In my ideal world a service worker would be alive as long as and page accessing it is alive.
In developing mail clients, chat clients, and any app that has a more than trivial data layer, it's ideal to have a single source of truth for the data layer of the application.
Typically we want to store data in IDB, and layer business logic and in memory caching on top of that. Before service workers we would use a SharedWorker for this, a single destination that services data requests from all the UI components.
In trying to add background sync with service workers, this model no longer works because a ServiceWorker cannot open and keep alive a SharedWorker. Meaning the ServiceWorker has to duplicate the business logic of the SharedWorker, in a thread-safe manner. The odds of any developer (let alone the average javascript developer) getting this right and not introducing data consistency bugs is incredibly low.
I think a better model for ServiceWorkers would be to behave more like SharedWorkers. Live in the background as long as any page is open, or background sync is in progress. If anything it is less resource intensive to keep the ServiceWorker alive while my UI is open, rather than have to spin it up repeatedly every time I make a request. It also means that the ServiceWorker can be used to cache indexeddb data in memory, reducing the I/O load and increasing performance.
ServiceWorker cannot open and keep alive a SharedWorker
I think we would like to fix this. You should be able to create/access SharedWorkers from a ServiceWorker. Would this address your particular use case?
I think so, but I'd have to try it to be certain. It sounds promising, assuming I can keep the SharedWorker alive long enough when a background sync event has triggered. It has to do a fair amount of async IDB work, but there's no setTimeouts involved.
Is there a specific bug I can follow about SharedWorkers so I don't derail this issue discussion any further?
That being said, I think the SharedWorker lifecycle would be better suited to the ServiceWorker use case than the current lifecycle.
In order to provide strong offline support, the ServiceWorker has to emulate my existing backend server, but also be lightweight enough to spin up in a few milliseconds to respond to fetch requests.
Those seem like two competing goals. A heavier weight ServiceWorker that can live as long as it is likely to receive events ( as long as UI components are open, and when background sync in imminent ), seems like it would be more suited to its task of providing robust offline support.
I think so, but I'd have to try it to be certain. It sounds promising, assuming I can keep the SharedWorker alive long enough when a background sync event has triggered. It has to do a fair amount of async IDB work, but there's no setTimeouts involved.
You would use a waitUntil() to hold the ServiceWorker alive while the SharedWorker is doing its work. This should work for most workloads. In theory you could trigger the hard kill if you try to keep it alive for many minutes in the background, though.
For other SharedWorker issues, see:
https://github.com/slightlyoff/ServiceWorker/issues/678
https://github.com/whatwg/html/issues/411
I uploaded some custom firefox builds that spawn a new worker thread for every event fired at the service worker:
https://people.mozilla.org/~bkelly/sw-builds/hydra/
I'll write a blog post explaining so people can test for themselves.
Note: These builds are for compat testing only and shouldn't be used to measure perf differences. Its a pretty dumb algorithm and designed just to stress test a new global for every event. The builds also kill workers off much more aggressively to avoid piling up a ton of workers. So it will show where people are not using waitUntil, etc.
https://www.flipkart.com/ might be an interesting one to try.
I did test this one and it seems to have issues. It works in the normal online case, but never seems to fully load when offline.
@wheresrhys re: cookies API, would https://github.com/bsittler/async-cookies-api come anywhere close to meeting those needs? It's still a proposal in its early stages at this point, so even an optimistic timeline for it is likely nowhere near when you'd like it, but I'm certainly interested in any feedback you have on its suitability (or lack thereof) and any features you would like to see
@wheresrhys can you go into detail for why you need to _poll_ IDB? IDB observers will remove the need for polling, but I'm curious about your use-case.
@bkimmel
I use Clients.claim a lot just to choke down the simplicity of the SW life cycle
Can you explain when & why you're calling clients.claim()? I don't find the need to use it all that often vs skipWaiting(). Btw I explained the lifecycle over at https://www.youtube.com/watch?v=TF4AB75PyIc.
@bkimmel
but what if there were a new kind of Worker ( a FetchWorker ) that could be provided as an argument to onfetch. Like self.on('fetch', new FetchWorker('fetch.js' )). That way you could parallelize the hell out of that FetchWorker without making everyone manage state in SWs with IndexedDB
How would this be different to option 2 of https://github.com/slightlyoff/ServiceWorker/issues/756#issuecomment-236948511? (except option 2 doesn't need extra API)
The SW would still be killed when it isn't used though, so global state would still be unreliable.
@bkimmel
I've used patterns in the past of "hanging" a service worker mid-lifecycle with event.waitUntil
event.waitUntil doesn't prevent events of the same type firing. Eg, it doesn't hold up other fetch events.
I get that there's a fear of concurrency here, but do you have an example of something you're doing now that would break with concurrency?
@ju-lien
It's remove the meaning of "this.addEventListener('fetch'", what will be the meaning of "this." ? A random SW scope ? So it could be a good thing to change this syntax to register handler and avoid confusions.
I'm not sure I understand this. this will be the global scope of the worker. Due to how the service worker shuts down when it isn't in use, the value of this in terms of JS equality may not be the same for two fetches. So if you do this.foo = 'bar' in one fetch, it may not be there in another fetch. Parallel workers makes this even less consistent between events, but it's already inconsistent.
@adamvy
We would like to use global state for service workers. In my ideal world a service worker would be alive as long as and page accessing it is alive.
In developing mail clients, chat clients, and any app that has a more than trivial data layer, it's ideal to have a single source of truth for the data layer of the application.
I think what you want already exists - SharedWorker. This is designed to stay alive while clients have a reference to it, whereas service worker terminates to save resources. Like @wanderview says, we need to tweak the spec of SharedWorker to allow them to exist in service workers.
@jakearchibald
Can you explain when & why you're calling clients.claim()?
The when is pretty much every SW I've authored since I learned you could call clients.claim(). The why is to stop myself from quietly sobbing in shame for not being able to control SW lifecycles the way I want to when I'm in development. It's just a thick callous I've developed over one of the friction-points in SW development. I think I picked it up from an article a while back and it really seems to have made things easier for me in dev. Thanks for that video link - I try to keep myself up-to-date on all things Jake Archibald and I missed that somehow.
How would this be different to option 2 of #756 (comment)? (except option 2 doesn't need extra API)
The SW would still be killed when it isn't used though, so global state would still be unreliable.
Maybe it wouldn't be different than #2, but I was under the assumption that all of that was under the umbrella of the "whole SW is concurrent". I have come to _enjoy_ the fact that SW global state is ephemeral, because it enforces some good practices... but I still see a marked difference between "There is 1 of these in active state and it might die at any time" and "There are an unpredictable number of these things active and they die". The latter is harder for me to reason about. I am a renowned idiot so that could have a lot to do with why it would it's difficult for me to grasp.
There are some other really cool things I could think of to do with an API like that (a way to share some stuff more easily with the client)... but I digress; maybe with your #2, we are basically talking about the same approach.
I get that there's a fear of concurrency here, but do you have an example of something you're doing now that would break with concurrency?
https://github.com/bkimmel/bkimmel.github.io/blob/master/serviceworker_bgs/sw.js
The most fun example I can think of: This dark pattern where I abuse sync / event.waitUntil to violate the user's privacy by notifying myself the next time they turn their browser on with no consent on their part. (I sent this to you a few months back on Twitter. I proved it in practice a couple of versions of Chrome ago, haven't tested it since.) This would still work, but would it maybe send 20x messages home for 20x concurrent SWs? Or can you make it so that just one SW gets the sync and the other ones you spun up don't?
But more generally, any pattern where I wait in the SW for a message from the client... since as it was pointed out above, now I have absolutely no idea which evil SW twin will get the message to proceed. Ben helpfully pointed out BroadcastChannel, but last I checked it wasn't in Chrome yet. The short story is I don't have anything in production that does this and I think I could eventually adapt my way of thinking to accomodate.
Overall, I love the work you guys do and if you say "calm down, it'll be OK" then I'll snap to. I'm just baring my ignorance here for the potential benefit of anyone else who is riding the "special bus" with me and has difficulty thinking about how this would work.
@bkimmel
The when is pretty much every SW I've authored since I learned you could call clients.claim(). The why is to stop myself from quietly sobbing in shame for not being able to control SW lifecycles the way I want to
It sounds like (and there's no shame in this) you're not really sure why you call it or what it does, beyond "it seems to help". Is that fair to say? Which way do you want to control the lifecycle. As in, what would the lifecycle be if you had full control?
would it maybe send 20x messages home for 20x concurrent SWs? Or can you make it so that just one SW gets the sync and the other ones you spun up don't?
No, no global event would be duplicated. If you're getting one event for it today, you'll still get one event for it in this concurrent model. Currently, if 50 distinct fetch events need to be fired, they'll happen in the same SW instance unless the SW is terminated in between. In the concurrent model, the 50 fetch events will be load-balanced across multiple instances, but the total number of fetch events will still be 50.
But more generally, any pattern where I wait in the SW for a message from the client... since as it was pointed out above, now I have absolutely no idea which evil SW twin will get the message to proceed
We're looking for cases where that would matter. It should only matter if you're holding state in the global scope, which is already unreliable but will become more unreliable. Are you doing that?
@jakearchibald The use case is changing the sw behaviour without having to release a new version. We have a feature flag system which enables us to roll out any new feature of the site behind a flag, and QA it in production by means of setting a cookie, which then sets headers, template state etc, propagating through the stack for that user's requests only. Having this available for QAing service workers is useful too, so on page load a message is sent to the SW to tell it which flags are on, and caching & retrieval of responses within the sw is conditional on the state of the flag.
If there was guaranteed to be one sw that didn't die then the posted message would be enough. As that isn't the case, storing the flag state in IDB to be retrieved on startup of the worker is necessary. Then, when handling fetches, it would be possible to read from IDB for each fetch request, but that seems like a performance nightmare - I don't know much about IDB perf, but I can't imagine calling it for most fetch requests would be good. Polling IDB to pick up any flag updates that may have been posted to another sw instance, so that the flag state can be read from a local variable, is a good enough compromise.
@wheresrhys polling doesn't sound like great performance in this case. IDB observers will help. But, do your flags often change during the life of a service worker? If not, cache the value in a promise in the global scope. If they change often, request them per fetch - don't assume it'll be slow until you see it for yourself (we say "tools not rules", meaning evidence trumps fear).
But, do your flags often change during the life of a service worker?
I think what that doesn't matter since the intention is to get _current flags_, not to check if they updated. I also guess that IDB Observers won't help because they won't be triggered on each SW startup and it actually will be no different than a pulling on SW startup.
As I understand it right, navigate pages aren't cached by SW or are network-first. SW doesn't have any flags in code, which is why every navigate page have to send a message to the SW to store flags. Even on those flags don't change (but I guess they may, e.g. different flags for different users), SW still have to read those flags from IDB because it could be killed after last fetch and for new fetch it needs flags again.
This is how I understand it.
@NekR
As I understand it right, navigate pages aren't cached by SW
Are we talking about SW in general or a particular site using SW? The only thing cached automatically by the SW is the SW script and its importScripts. No page fetches, navigation or otherwise, are cached by the service worker unless you write code to do so.
It would be good to have some kind of _shared memory_ for SW. That amount of _shared storage_ (possibly with some limit like localStorage) will always be in _memory_ until at least on _client_ of a _registration_ is alive. When all _clients_ of a _registration_ are shut down, then this memory could be written to a disk, and then again restored on a _registration_'s client startup.
SharedWorker seems very similar to it, but if it has to be a SharedWorker, then such worked should be alive a the time while either SW is alive or any of SW's _registration_ _clients_, so pages could access it too (e.g. in a previous case of flags -- set flags).
Also if it would be _kind of a shared memory_, then should be opt-in, as SharedWorker is.
@jakearchibald
The only thing cached automatically by the SW is the SW script and its
I meant _the case with flags_ and guessed what they do. Of course I know SW doesn't cache any pages by default :-) (Sorry for confusion)
@NekR
It would be good to have some kind of shared memory for SW.
Seems like this could be an implementation detail for indexeddb. Browsers may (already) optimise by holding portions in memory.
It's slow polling, every 5 minutes, so shouldn't be a performance worry
don't assume it'll be slow until you see it for yourself
It's not just slowness I worry about, it's the amount of processing needed. Trying to be kind to the device's CPU by holding a fairly small object in memory and reading from the DB every 5 minutes. The downside - that the flags may be up to 5 minutes out of date - is something I can live with. I may do some experiments to test out the perf of reading from IDB each time though. So far it's just a quick fix to deal with the fact that multiple SW threads can exist in firefox, which was news to me.
do your flags often change during the life of a service worker
Typically they'll change very infrequently, but if e.g. a page that's been open in a tab for ages is controlled by one SW instance, and another page is opened in another tab which spins up a new instance of the SW, and this page load comes with an update to the flags, then I'd want the update to propagate to the first SW instance.
@wheresrhys this would avoid polling:
const getFlags = (() => {
let cacheTime;
let cachedFlagsPromise;
return function getFlags() {
if (!cachedFlagsPromise || Date.now() - cacheTime > 1000 * 60 * 5) {
cacheTime = Date.now();
cachedFlagsPromise = getFlagsFromIDB();
}
return cachedFlagsPromise;
};
})();
In general, I think it's good to have this feature if storage-thing will be solved. In the offline-plugin I do some required pre-computations on SW startup. There isn't a lot of work, but I can imagine people doing much more pre-computation on SW startup, because it isn't possible to store global state in SW and requesting data from IDB on each fetch/other-event seems a bit weird.
Maybe it isn't slow and perfectly fine, but from a dev perspective it isn't transparent and sounds much worse with "multiple SW instances".
Other thing is this: does this feature worth complexity? While it makes life easier for Firefox devs, it makes life harder for web-devs, because we will have to handle on consequences. Other point was heavy computations, which as well could be done in SharedWorker without having multiple ServiceWorker instances.
@jakearchibald Thank you for your patience in listening and explaining. To answer the question re: clients.claim()
It sounds like (and there's no shame in this) you're not really sure why you call it or what it does, beyond "it seems to help". Is that fair to say?
The reason I call it is that in conjunction with self.skipWaiting (which I also call in the install phase), it makes it so I don't _have_ to think about the vagaries of the SW lifecycle.. i.e. it just does "what I want it to do" which is "start handling fetch events _and_ messages and everything else and kill any of your older siblings while you're at it so we don't get you mixed up with each other". Before I used it, I seemed to have a lot of problems with the SW I thought was active not being the one that handled message events, fetches, etc. Initially, I only used self.skipWaiting and seemed to have a lot of those problems. According to how I _thought_ SWs worked, skipWaiting would be enough, but I found in _practice_ that I also needed clients.claim to have it start doing what I wanted it to do in a way that was not wildly unpredictable. So it's fair to say my practice of using clients.claim is based on some cargo-cult voodoo, but it's the bridge I've found that helps me jump the gap of how I thought SWs worked and how they actually work when I test them in Chrome. I have no serious attachment to that facet of the API, but I'm "once bitten" on doing it any other way. Maybe I'll go back to using just skipWaiting and see if the problems I used to have pop up again.
As Ben said above, he seems to think it would be a non-problem for implementers, anyway. So as long as that call it keeps doing "what it says on the tin" - or you can abstract it in such a way that as a SW user, it looks like it does - that's good enough for me in the context of this issue.
@jakearchibald
We're looking for cases where that would matter. It should only matter if you're holding state in the global scope, which is already unreliable but will become more unreliable. Are you doing that?
I have used some stunts like this in the past:
https://gist.github.com/bkimmel/4614b8f662a1e8b8d53072500fb74183
But not for production.
@jakearchibald Yep https://github.com/slightlyoff/ServiceWorker/issues/756#issuecomment-238028525 looks like a neat solution. I could include some stale on revalidate type logic too.
Is polling within a SW a pattern to be avoided in general? Does it risk keeping the worker alive longer than it otherwise would be?
@wheresrhys in general, polling is worse than an equivalent solution that doesn't involve polling (I'm sure there are exceptions as always). Nothing unique to service worker.
While it makes life easier for Firefox devs, it makes life harder for web-devs, because we will have to handle on consequences.
Just to clarify, this is not just something we have requested. Knowing if we are going to support this might help us because we're in the middle of designing our multiple content process architecture for service workers, but other reasons were voiced at the meeting:
Also, after thinking about this proposal more I think the change to postMessage() behavior is more breaking than the change to global state. Most people already know that global state is going to be unreliable in service worker.
Its really confusing and unexpected, however, that ServiceWorker.postMessage() does not always go to a consistent place.
Perhaps we could address this by creating a separate ServiceWorker DOM object for each thread instance. A controlled client would have a single ServiceWorker .controller and would therefore see just a single thread instance. Push events, however, might go to a separate ServiceWorker DOM object and thread instance.
If we did this, though, we might need some API to "give me all the ServiceWorker instances for the currently active version", etc.
@wanderview
Perhaps we could address this by creating a separate ServiceWorker DOM object for each thread instance. A controlled client would have a single ServiceWorker .controller and would therefore see just a single thread instance
Is this easier than opting particular events in/out of firing in multiple instances?
Is this easier than opting particular events in/out of firing in multiple instances?
No, its not easier. But I'm worried about the conceptual change we're trying to introduce here.
It seems the web platform typically exposes js runtimes or globals as a DOM object like Window or Worker. Each DOM object instance unambiguously references a single js global. For SharedWorker we may attach many DOM objects to the same global, but they each individually still reference a specific js runtime.
What we are proposing here is to create a DOM object, ServiceWorker, that references a pool of globals. You send an event to this thing and you really don't know where its going to run. This really breaks things like postMessage().
If we could go back in time it might have been better to expose a MessageChannel for each ServiceWorker instead of postMessage() directly. Then it would be easier to reason about how a pool of worker threads function here.
Now, we could just say "all postMessage() events go to a single thread instance". But that doesn't really make it easier to understand whats going on for people using the API. It just makes it look like a single global for some uses, when its really not.
If we did this by exposing different ServiceWorker DOM objects for each global spun up then it might be easier for people to reason about. I have ServiceWorker A and B, which are both running version X. They can both handle the same kind of events, but if I postMessage() them the message events will fire on different globals.
I think I'd like to hear more from the other browser vendors before spending more time on this right now. Internally we have decided to proceed with our "single SW thread across multiple processes" implementation for now.
I threw together a little benchmark thing - https://jakearchibald.github.io/service-worker-benchmark/
Might be useful if we want to assess how much performance we get from multiple instances.
You can also run it locally, including HTTP/2 https://github.com/jakearchibald/service-worker-benchmark
I think I'd like to hear more from the other browser vendors before spending more time on this right now. Internally we have decided to proceed with our "single SW thread across multiple processes" implementation for now.
I don't think we should give up on this just because it introduces complications to certain APIs such as postMessage(). As you mentioned, perhaps we should really be thinking of service workers as worker pools. There shouldn't be a guarantee that a service worker instance that just handled an event would be the same one that would handle the next event, including onmessage. This goes back to the idea that you can't take a dependency on global state because it was never guaranteed to you to begin with due to varying implementations and policies for terminating service workers.
While I understand that this complicates matters for developers, the fact of the matter is that service workers were always intended to be ephemeral and browser vendors are supposed to be able to do the "right thing" depending on device constraints such as memory, CPU, and battery. In the same way that browsers should be able to coalesce service worker instances to limit system pressure, they should also be able to optimize for devices that can afford resources to concurrently run the same service worker script in multiple instances. These are optimizations that the browser will make on a developer's behalf to make their app or site more robust. For developers to buy into this model, they need to ensure that their service worker is truly not relying on state between events being handled.
One other concern here is around developers relying on events being handled in sequence instead of in parallel. For instance, a fetch may do some work in its event handler that's needed by a subsequent fetch handling. Although there may be some optimizations that a developer can make here, perhaps it really shouldn't be supported because, as mentioned earlier, it would be bug prone due to differing implementations and it would limit the ability for a browser to parallelize execution.
I've spent the last 2 weeks or so trying to understand the performance impact, if any, of allowing multiple service worker thread instances.
TL;DR: I believe allowing multiple service worker instances could provide some measurable benefit in multi-process browser architectures.
The details:
For this investigation I mainly used Jake's excellent benchmark here:
https://jakearchibald.github.io/service-worker-benchmark/
I tested the following browsers:
All of my tests were on a developer grade windows 10 desktop. Future testing should include mobile, but some of my tests were not easily achievable in FF for android at the moment.
I tried to answer two questions in this investigation:
In general I only looked at the case where the service worker thread (or threads) was already running. Clearly cold start is important, but for now I was trying to see if we should consider this at all given the most favorable circumstances.
Let's answer question (1) first. Does a thread pool help? Consider these load times for refreshing the benchmark with the cached-fetch script:
FF51 Mean: 311ms
FF51 Median: 302ms
FF51 Pool(2) Mean: 311ms
FF51 Pool(4) Mean: 311ms
FF51 Pool(8) Mean: 310ms
FF51 Pool(16) Mean: 307ms
FF51 Pool(32) Mean: 303ms
FF51 Pool(64) Mean: 306ms
There is perhaps a slight effect here, but its quite small compared to the cost of the number of threads needed. If you look at the median, however, you can see that there is no real effect:
FF51 Pool(2) Median: 300ms
FF51 Pool(4) Median: 309ms
FF51 Pool(8) Median: 301ms
FF51 Pool(16) Median: 304ms
FF51 Pool(32) Median: 301ms
FF51 Pool(64) Median: 303ms
It seems the slight improvement in the mean times was simply due to reduced outliers. Again, this does not seem worth the overhead.
A thread pool does not help a well behaved caching service worker on desktop.
I should note that the thread pool does help with the "blocking worker" case in the benchmark. This is expected. The question is if we should solve that problem with multiple instances or by providing access to SharedWorker or DedicatedWorker to perform heavy CPU work.
Now let's look at question (2). Are there architectural advantages in allowing service worker threads to run co-located with controlled windows in separate content or renderer processes?
To investigate this we can somewhat compare firefox to chrome. Firefox currently executes service workers completely in the content or renderer process. I believe chrome has additional IPC messaging involved with dispatching a FetchEvent because it supports multiple renderer processes.
What does the current data show here:
FF51 Mean: 311ms
FF51 Median: 302ms
Chrome 52 Mean: 376ms
Chrome 52 Median: 383ms
Chrome Canary 54 Mean: 353ms
Chrome Canary 54 Median: 361ms
Remember these numbers are for the reload case where the service worker is already running.
FF51 current runs about 65ms faster than Chrome 52 and 40ms faster than Chrome Canary 54.
But is this due to the content process model and the IPC messaging? Its hard to say with such different engines.
To test further I adapted some work-in-progress we have to move to a multiple content process architecture for service workers in firefox. This adds IPC messaging to dispatching the FetchEvent.
The initial patch to add IPC to FF FetchEvent regressed our numbers from 311ms to 450ms. After additional profiling and optimization I was able to reach this situation:
FF51 multi-e10s Mean: 387ms
FF51 multi-e10s Median: 377ms
This is nearly identical to the current Chrome 52 numbers.
While further optimizations are possible, it seems reasonable to say that requiring IPC to dispatch a FetchEvent adds 50ms to 75ms in current multiple process architectures. Therefore it does seem desirable to allow service worker thread instances to run co-located in the same process as the controlled window. It would be nice if the spec allowed this.
Data for the measurements I took can be found here:
https://docs.google.com/spreadsheets/d/1zqFA6JwMNmwL67oSQlSV70Rj8fUZ2QcVvDrUNFePrFI/edit?usp=sharing
Thank you for your very detailed analysis @wanderview!
There are two main points from your summary that I'd like to pull out as arguments _for_ allowing multiple service worker instances:
thread pool does help with the "blocking worker" case
...and...
it does seem desirable to allow service worker thread instances to run co-located in the same process as the controlled window
I think the real key here is to remove the restriction of only allowing one service worker instance that handles all events. By removing this restriction we should be able to optimize for the two scenarios above as well as allowing push and background sync events that can be handled in a timely and concurrent fashion in the background.
Just to clarify my position, I think there is an architectural advantage for pages with many simultaneous loads like this benchmark. The ~20% improvement from co-locating the SW in the same process as the page may not exist for more typical page loads. We should probably make a similar benchmark for ~20 images or something.
Also, I think we need to weigh this benefit against the compat issues. It did not take me long to find sites that broke with the "create a new thread for every event" build I hacked together. People are definitely expecting to use global state and postMessage() within the context of a waitUntil() holding the worker alive. I think we need a better proposal about how to solve this issue.
thread pool does help with the "blocking worker" case
After looking at this for a while I really don't think we should aim for a thread pool to support the blocking worker case. I'd rather expose explicit APIs so developers can offload heavy CPU load into a separate thread. That way devs don't have to contort their code to try to trigger the "magic parallelism heuristic" different browser might implement. For example, think of how people try to optimize for the v8 jit in ways that may not make sense for all browsers. Lets steer clear of that if we can.
By removing this restriction we should be able to optimize for the two scenarios above as well as allowing push and background sync events that can be handled in a timely and concurrent fashion in the background.
I'm open to the idea of a separate thread instance for these cases in order to support firing the events when the browser is not running. In my mind this is less of a perf issue, though, and more of an OS integration issue.
Again, though, we need to think about how a SW might communicate with a window from a push event using postMessage(). For example, a push/notification SW instance might want to postMessage a Client in order to trigger a SPA route update, etc. If there is any kind of communication back to the SW after this completes, then these sites will break.
If I could go back in time I'd probably remove the ServiceWorker.postMessage() method. I can't think of any valid use case for initiating a postMessage from the window to a service worker. Anything you can do in the service worker (Cache API, etc) you can do from the window.
In contrast, a ServiceWorker should have Client.postMessage() available since that supports many real use cases. If it needs a response message back it could then pass a MessageChannel in its message to the Client.
Anyway, I believe in Firefox we are going to pursue our "FF multi-e10s" approach for now. Its the most web compatible even if it does regress performance. We can then consider optimizations such as co-locating the worker thread in the document process again in the future if we can solve this spec issue.
Some other random observations from the measurements:
The last observation here might actually suggest a new spec addition. If we allowed a service worker to indicate that the returned Response will not change, then the browser could store it in memory caches for images, stylesheets, etc. These could then provide big perf wins for loads that occur in the same process (reload, navigation, etc).
Edit: These caches would have to be invalidated if the service worker updates.
The fact that http cache is not much faster suggests to me that static routing may not be much of a perf win. This does not measure the service worker thread startup costs of course.
@wanderview
I can't think of any valid use case for initiating a postMessage from the window to a service worker. Anything you can do in the service worker (Cache API, etc) you can do from the window.
Well, I have one. When I need to apply update, I send a message to SW to do skipWaiting(). See this: https://github.com/NekR/offline-plugin/blob/master/tpls/runtime-template.js#L184
In an ideal world, probably, ServiceWorker would have 2 types of threads: one _ServiceWorker Thread_ and N _Fetch Thread_s. _ServiceWorker Thread_ would receive all events such as install, activate, message, push, sync, etc. On the other had, _Fetch Thread_ would receive only one event: fetch with a possibility of requests to be multiplexed into N _Fetch Thread_s. Not sure if this would give the benefit, but this is how I see it giving all details here.
Just to clarify: by Fetch Thread I didn't mean that it must have the same environment as _ServiceWorker Thread_, but rather separate type of worker with probably its own API. Of course, this all in ideal world.
Well, I have one. When I need to apply update, I send a message to SW to do skipWaiting(). See this: https://github.com/NekR/offline-plugin/blob/master/tpls/runtime-template.js#L184
Fair enough. But we could expose skipWaiting() on the ServiceWorker object itself in the window as well. Also the Clients API could be exposed in the window (#955).
@wanderview maybe you are right. Anyway, this all seems like a major breaking change.
I think if it needs to be done, then it must be done as soon as possible. While not all the web is using ServiceWorker and while there are just a few of SW libs.
@wanderview maybe you are right. Anyway, this all seems like a major breaking change.
Well, yes. Removing ServiceWorker.postMessage() is probably (?) too breaking. I was just lamenting that it ties our hands in this issue and perhaps may not really be needed.
Anyway, from an implementation point of view I don't think Mozilla will be pursuing multiple SW thread instances right now. If its something other browser vendors strongly want then I'd like to see more of a proposal about how to address web compat issues.
Perhaps a first step could be getting use counter information for ServiceWorker.postMessage() from the window. @jakearchibald does chrome collect that today?
First, love the data @wanderview; amazing stuff! The data about Cache API overhead is fascinating. Something we should all compete on driving down = )
Some other out-of-order thoughts:
ServiceWorker.StartWorker.Purpose) that measures which event caused a SW start. I see ~6% of starts coming from message events vs. ~4% for Push delivery, ~30% for main-frame navigations, and a surprisingly high 53% for sub-resource fetches. Perhaps @mattto, @kinu, or @nhiroki can weigh in or shed more light.Again, amazing data and great work both to @wanderview for prototyping and @jakearchibald for making a benchmark we can use for this sort of thing.
The data about Cache API overhead is fascinating. Something we should all compete on driving down.
I don't know how chrome is implemented, but in gecko we have serialized reads of the Cache metadata during a .match(). Its possible allowing concurrent read transactions in sqlite would help here. That was too big a change to prototype here, though.
Regarding the addition to allow caching in-process, this is something we debated heavily inside the Chrome team regarding our memory-cache behavior (specifically) for images back in '12-'13. I think we'd love to be able to add something to a response that acts as a memory-cache control header.
Yea, it feels like we need something other than just the cache-control header. We don't want a service worker to trigger this by accident with a pass-through fetch. It should be a conscious opt-in by the service worker.
I'd love for us to explore how we can make an opt-in version of the multi-SW system possible for sites that want or need it. Opting into this treatment on an event-type basis seems reasonable to me, but as you mention, I think we should see how far we can get with sub-workers first.
Opt-in seems like a reasonable approach for fetch. Not sure, though, if it addresses @aliams desire to move push handling into a separate process to better support "browser is closed" integration on desktop.
Lastly, I guess I'm a little surprised the gains for multiple SW threads are so small. 10% ain't nothing, but it's in the range some strong optimisation in other subsystems can eek out in a few quarters.
Yea, its not a slam dunk. Although, maybe it has a larger effect on mobile. I wasn't able to really run my prototype stuff on firefox for android to test.
@wanderview amazing data, thank you!
We should add skipWaiting to SW instances, eg reg.waiting.skipWaiting(). We agreed on this at a F2F earlier this year, but I can't find an issue for it (going to search some more for it, because I'm sure we had one).
I still think .postMessage is valuable. If we didn't have it people would just abuse fetch for it and have the same problems.
I'm happy to open the spec up to allow multiple worker instances, and UAs deal with the compatibility issues. We could issue a console warning on writing to the global after initial execution, but I don't know how effective that will be. Apple is probably in the best situation here, as I imagine devs will fix their stuff to get it working on iPhones. If multi-instance fails to show actual benefits in practice, we can remove it from the spec. @slightlyoff what do you think?
Devs I've spoken to since my post have mostly said "We shouldn't rely on the global, so this change is fine", but I don't doubt that reality is less clear cut.
My use case: App has multiple languages. Each can optionally be downloaded for offline use. Therefore, do not want to download all languages during SW install. App uses postMessage to tell SW when a new language should be added to the offline list. SW both downloads the new language and does a full update of all cached responses (in case API has changed and new language is using the new API).
Where global variable comes in: I need a locking mechanism to prevent two updates happening at the same time. I could store a value in IDB, but what if that lock value is set to true and the update process fails without setting it back to false? Then no other update will happen as SW thinks one is still in progress. Instead, I plan to have a global UPDATE_IN_PROGRESS in SW, defaulting to false. Then, even if the update process dies, the value will reset to false when the SW is killed.
Is this a good or bad idea? And is there an alternative if SWs will allow multiple instances?
My use case: App has multiple languages. Each can optionally be downloaded for offline use. Therefore, do not want to download all languages during SW install. App uses postMessage to tell SW when a new language should be added to the offline list. SW both downloads the new language and does a full update of all cached responses (in case API has changed and new language is using the new API).
I would recommend doing this in the window and not in the service worker. You can download the new language pack and save it in cache all from the window. Then you just switch the a "current cache name" value in your IDB atomically to get your fetch event handler to start using it.
Where global variable comes in: I need a locking mechanism to prevent two updates happening at the same time. I could store a value in IDB, but what if that lock value is set to true and the update process fails without setting it back to false? Then no other update will happen as SW thinks one is still in progress. Instead, I plan to have a global UPDATE_IN_PROGRESS in SW, defaulting to false. Then, even if the update process dies, the value will reset to false when the SW is killed.
You need to use the IDB transaction itself as the "lock". If the transaction does not complete it rolls back any changes. Once you complete the transaction and commit the changes then other operations are unblocked.
@wanderview detected not a major speed up when dispatching several SW instances in parallel, why to not wait for other browsers on their way to implement service workers to perform the same experiment before doing any change? Let's say we open a experiment submission period until the next F2F. If this reveals a boost on speed, the browsers can share their implementations in the F2F.
@slightlyoff @wanderview @jakearchibald thoughts?
I'm happy to open the spec up to allow multiple worker instances, and UAs deal with the compatibility issues. We could issue a console warning on writing to the global after initial execution, but I don't know how effective that will be. Apple is probably in the best situation here, as I imagine devs will fix their stuff to get it working on iPhones. If multi-instance fails to show actual benefits in practice, we can remove it from the spec.
I can't help but feel this kind of punts the compat issue to developers. We can't decide what to do, so lets let each browser vendor do whatever they want and leave it to developers to make their stuff work. That seems a bit less than desirable to me.
I'd rather we be more explicit. For example, to satisfy Microsoft's request for separate push/bg-sync threads we could:
ServiceWorker attribute that represents the push thread. Something like reg.pushManager.active.ServiceWorker attribute that represents the bg-sync thread. Something like reg.sync.active.ServiceWorker.version UUID value or other model for the current service worker version.controller.version === reg.active.version === reg.pushManager.active.version === reg.sync.active.version.controller === reg.active.controller !== reg.pushManager.active !== reg.sync.active.This would support MS's request (I think) and expose what is going on to developers without "magic".
- Require
controller !== reg.pushManager.active !== reg.sync.active.
If the spec is open, I think the wording should be "Does not require to controller !== reg.pushManager.active !== reg.sync.active." Because it could be but it is not mandatory, isn't it?
If the spec is open, I think the wording should be "Does not require to controller !== reg.pushManager.active !== reg.sync.active." Because it could be but it is not mandatory, isn't it?
I'm saying if we are going to do this, we should be explicit and not leave it "open". Leaving it open means letting browsers do different things that are observable and end up as compat headaches for devs. Just my opinion, of course.
We do have a metric (ServiceWorker.StartWorker.Purpose) that measures which event caused a SW start. I see ~6% of starts coming from message events vs. ~4% for Push delivery, ~30% for main-frame navigations, and a surprisingly high 53% for sub-resource fetches. Perhaps @mattto, @kinu, or @nhiroki can weigh in or shed more light.
I suspect long-running Facebook (and similar) tabs cause a large number of starts for subresources. The page periodically does a subresource request in the background at an interval greater than our idle timeout (30sec).
Notes:
Sorry If I am out of context and late, but just leaving some feedback related to:
https://jakearchibald.com/2016/service-worker-meeting-notes/
Are you maintaining global state within the service worker?
Would this change break your stuff?
If so, could you use a more persistent storage like shared workers / IndexedDB instead?
We have an app where we show some user generated HTML content (including JS/CSS, etc) in an iframe and the resource references inside that HTML are not present on any server but are available via a web worker running on a different origin.
So, we were using Service Worker to intercept the resource requests and fulfill them by bringing the resource content from the worker. Our implementation was to first have a handshake kind of thing in which the service worker is given one end of a MessageChannel and the worker is given the other and they communicate with each other via the MessagePorts.
Now, for this we have to maintain the service worker end of the port in the ServiceWorkerGlobalScope, so if is unreliable we have a problem with this change as we will loose the communication channel with the worker due to this. It does not seem to be possible to store this port in indexedDB as well
One interesting thing is to be able to use shared workers inside service worker, and then instead have the shared worker communicate with the other web worker and pass the content back to the service worker. So that may help.
For what it's worth, one of the more prominent guides to the Push API depends on global state being maintained in a service worker (i.e. a MessagePort).
But that's probably bad already even without concurrency, right, since user agents can already shut a SW down at any time it doesn't have events to keep it alive?
F2F: Interested to hear if Apple's take on this has changed at all.
@wanderview I ended up making my app rely on global scope, so I'm hoping if multi-threading happens it'll be enabled via an option somehow.
I would recommend doing this in the window and not in the service worker. You can download the new language pack and save it in cache all from the window.
I could, but my app also auto-updates, and I want to avoid the situation where it does a big update twice, simply because two windows are open. Having SW manage all updates seems to work better in this case.
You need to use the IDB transaction itself as the "lock". If the transaction does not complete it rolls back any changes.
I tried doing this but I couldn't get it to work, and it doesn't seem like IDB locks stay locked for very long. "Transactions are expected to be short-lived, so the browser can terminate a transaction that takes too long, in order to free up storage resources that the long-running transaction has locked." (Mozilla)
I ended up using promises to queue tasks, which requires global scope. Each new call to do_blocking_task() adds the passed task to the end of a queue of promises.
let CURRENT_TASK = Promise.resolve(null)
async function do_blocking_task(task){
CURRENT_TASK = CURRENT_TASK.then(task)
}
I would recommend doing this in the window and not in the service worker. You can download the new language pack and save it in cache all from the window.
I could, but my app also auto-updates, and I want to avoid the situation where it does a big update twice, simply because two windows are open. Having SW manage all updates seems to work better in this case.
Wouldn't a SharedWorker be ideal for "only one instance can ever be doing this" use cases?
Copying this from another thread. F2F notes about multi-service worker requirements:
F2F: background SW (push, notification, sync, bg fetch) vs foreground SW (fetch, postMessage) - MS & Apple want this for service workers to exist when the browser is closed. Should we spec this? Fetches for notification icons go through the background SW in Edge, since there isn't a foreground SW.
What happens to the clients API? How do we represent multiple instances.
Facebook global state case:
How does the client message the correct service worker?
Edge can work around this by still ensuring there's only one SW at a time. Either using the bg SW, or fg SW.
We need to think more about use cases that require speaking to a particular SW, or otherwise depend on global state.
Random thought: If we made multiple instances an opt-in (at registration time), browsers that require it for particular features like push could reject push subscriptions if multiple instances wasn't enabled.
That doesn't solve Facebook's case though.
Types of multi-instance use:
No change to the spec. Safari/Edge would have to find a way to work around this by switching from one worker to another, potentially serialising state to cater for use-cases like Facebook's.
If only a few sites break, we could reach out to them. Facebook's case for instance could be fixed by passing message ports or using broadcast channel (https://github.com/w3c/ServiceWorker/issues/1185#issuecomment-322864908).
If everything turns out fine, we only have to make a minor spec change to recognise there may be a pool of instances that can receive top-level events.
If lots of sites break, but postMessage is the main cause, Edge/Safari could use tricks so messageEvent.source.postMessage tries to go back to the instance that sent the original message. That would certainly fix the Facebook case. If this works we could look at speccing it, but I'm not sure how.
navigator.serviceWorker.register(url, { multiInstance: true });
This is kinda messy, as we'd have to think of what happens if the registration is updated to opt-out after push subscriptions have been created. Also, it'll be tough for developers if they have to enable it for Edge/Safari, but disable it for Chrome/Firefox (because they don't support it).
Also, it'll be tough for developers if they have to enable it for Edge/Safari, but disable it for Chrome/Firefox (because they don't support it).
I guess I had assumed something like this would be saying "I'm ok with multi-instances, but I understand there is no guarantee on how many instances I will see."
As long as they're actively testing in a browser that does the multi instance thing in a big way. If a new browser comes along and does multi instance in a different way, we could be back to square one.
I pondered earlier whether opt-in-multi-instance-mode could do something to enforce multi-instance-like behaviour. Eg create a new realm for every top level event. That sounds super expensive, but maybe UAs have tricks here.
I think multi instance SW is more scalable approach for user agents. Single
threaded SW may produce more complications as new SW features/specs added
and changing the behavior from there would be much harder.
On Aug 17, 2017 00:27, "Jake Archibald" notifications@github.com wrote:
As long as they're actively testing in a browser that does the multi
instance thing in a big way. If a new browser comes along and does multi
instance in a different way, we could be back to square one.I pondered earlier whether opt-in-multi-instance-mode could do something
to enforce multi-instance-like behaviour. Eg create a new realm for every
top level event. That sounds super expensive, but maybe UAs have tricks
here.—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/w3c/ServiceWorker/issues/756#issuecomment-322904415,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABIlkVvDHysbqKMIa5n42qyGb6sg59Bgks5sY16sgaJpZM4GGqeS
.
Is there a 4th option in which multiple-instances could be an implementation detail, avoiding exposing it to the developer, making global state to be shared somehow?
Is there a 4th option in which multiple-instances could be an implementation detail, avoiding exposing it to the developer, making global state to be shared somehow?
I don't see a way to implement this given SW instances would likely live in different processes and global state provides synchronous access.
For others reading along, here are the notes from the call https://github.com/w3c/ServiceWorker/issues/1173#issuecomment-323019578.
tl;dr: MS are going to implement multiple instances and see what breaks. If all is well, we can spec the idea of a pool of service workers. If developers want to talk to a particular instance, MessageChannel already provides that, but we may in future expose multiple service worker instances via the clients API.
Just to be very clear, MSFT is trying a very specific version of multi-instance: one instance for receiving push messages and one instance for handling fetches. This isn't parallelism for fetch handling.
So, has Edge shipped this model with separate SW for push notifications?
Yes, the April 2018 Update runs the push event handler in the background. We're investigating forwarding that event handling to the foreground if Microsoft Edge is open.
F2F:
As @mattto mentioned, the latest version (Windows 10 October 2018 Update) forwards the push event from the background to Microsoft Edge in the foreground if it is open. This means that the push event handler in such a case would be run in the same service worker execution context as the fetch event handler.
Most helpful comment
We would like to use global state for service workers. In my ideal world a service worker would be alive as long as and page accessing it is alive.
In developing mail clients, chat clients, and any app that has a more than trivial data layer, it's ideal to have a single source of truth for the data layer of the application.
Typically we want to store data in IDB, and layer business logic and in memory caching on top of that. Before service workers we would use a SharedWorker for this, a single destination that services data requests from all the UI components.
In trying to add background sync with service workers, this model no longer works because a ServiceWorker cannot open and keep alive a SharedWorker. Meaning the ServiceWorker has to duplicate the business logic of the SharedWorker, in a thread-safe manner. The odds of any developer (let alone the average javascript developer) getting this right and not introducing data consistency bugs is incredibly low.
I think a better model for ServiceWorkers would be to behave more like SharedWorkers. Live in the background as long as any page is open, or background sync is in progress. If anything it is less resource intensive to keep the ServiceWorker alive while my UI is open, rather than have to spin it up repeatedly every time I make a request. It also means that the ServiceWorker can be used to cache indexeddb data in memory, reducing the I/O load and increasing performance.