Confusion regarding alarms in mv3 service workers

1,108 views
Skip to first unread message

David Roland

unread,
Apr 24, 2023, 4:14:39 PM4/24/23
to Chromium Extensions
I'm struggling to understand when an alarm created from the chrome.alarms API in an mv3 extension will cause an inactive service worker to load.

I'm using the following code in a service worker to test when the service worker loads in response to an alarm:
_____________
if (Math.random() > 0.5) {
    console.log("adding alarm listener");
    chrome.alarms.onAlarm.addListener(() => console.log("alarm event!"));
}
chrome.alarms.create("testAlarm", { delayInMinutes: 1 });
console.log("Worker loaded");
_____________

On worker load, an alarm is created, but an onAlarm listener is only added with 50% probability. The worker becomes inactive after about 30 seconds which is expected behavior. 

If the onAlarm listener was added, about 30 seconds later, when the one minute alarm fires, the worker becomes active again, and in the console I see the text "worker loaded".  If the listener was added again in this reload, the message "alarm event!" will also display in console.

If the onAlarm listener was not added in the initial worker startup, the worker stays in the inactive state, and never wakes up when the alarm is scheduled to fire. The worker is dead until the browser/extension restarts.

From this behavior, I infer that an alarm scheduled to fire after a worker is inactive will not wake that worker unless an onAlarm listener was added before the worker unloaded. It's not, alternatively, based on whether a listener will be registered when the worker loads again, because with this worker, that's an unknown.

The wrinkle I'm struggling to comprehend is that, if an onAlarm listener is added on extension startup, in later worker lifecycles, after the service worker unloads, it will always wake up to the next alarm firing, regardless of whether an onAlarm listener was added in the prior load/unload cycle. But I assume that the onAlarm listener doesn't survive the worker unload and must be re-added each time the worker loads again. 

Is it the intended behavior that, if a service worker adds an onAlarm listener at startup, then the platform will always load an inactive worker when an alarm fires, regardless of whether that worker adds a listener in every load/unload cycle? Does that imply that the initially added listener is persistent, even though the function object submitted as the alarm listener no longer exists in the context of the next worker load  (in my code, there is no console log "alarm event!" from the initially registered listener if in a later cycle a new listener is not added)? Or does the platform note that a worker listens based on it adding a listener in the startup load, and maintain that flag until restart? If a tree falls in the forest, and nobody is listening, does it make a sound?

david








T S

unread,
Apr 24, 2023, 4:33:41 PM4/24/23
to Chromium Extensions, David Roland
I'm not sure I parsed all that, but in general if you want a listener to reliably activate you need to register it at the top level of your service worker.
https://developer.chrome.com/docs/extensions/mv3/service_workers/#listeners

David Roland

unread,
Apr 24, 2023, 5:01:13 PM4/24/23
to Chromium Extensions, T S, David Roland
I think, in my example, that the listener is added at the "top level", in the sense that it's added synchronously and before yielding the execution event loop back. But it's not always added, because of the conditional check on Math.random(). What confuses me is that the platform doesn't seem to care if my listener isn't added on a particular worker load/unload cycle. It always wakes up my worker as long as the listener was added in the initial cycle at startup. 

My description is unfortunately pretty convoluted. Maybe this would help.

The platform wakes up an inactive service worker when an alarm fires -

1. only if an onAlarm listener was added in the load prior to the worker becoming inactive (not what I'm seeing)
2. only if the platform determines that a listener will be added on the next load and thus somebody will be listening (not what I'm seeing)
3. every time, without regard as to whether a listener was added in prior load or will be added in next load (not what I'm seeing)
4. based on whether the worker added a listener on it's first load at startup, and if it did, the platform will wake up that worker every time an alarm fires, regardless of whether a listener is ever added again in subsequent load/unload cycles (this is what I'm seeing, but feels like a strange, undocumented behavior, or a fundamental misunderstanding on my part)

wOxxOm

unread,
Apr 25, 2023, 2:08:16 AM4/25/23
to Chromium Extensions, David Roland, T S
>  It always wakes up my worker as long as the listener was added in the initial cycle at startup. 

Definitely a bug based on the lack of consistency between the runs. Which behavior is correct though is up for debate. The behavior you described for the first cycle is arguably the one intended to be correct i.e. no listener = no wakey-wakey next time. Although you could argue it shouldn't matter and the script should re-run regardless of the presence of a listener last time to support conditional synchronous registration as shown in your example, but there are no synchronous data providers inside service worker so you can't make a meaningful decision whether to add or not the listener, i.e. this use case is impractical and thus won't be implemented. It may change if top-level `await` is ever re-enabled for service workers in Chrome.

Jackie Han

unread,
Apr 25, 2023, 5:29:21 AM4/25/23
to wOxxOm, Chromium Extensions, David Roland, T S
There was a discussion about how event listeners work in the background.

Your question is a little different. Because you add event listeners randomly at top level (your code is for experimental purposes only, no one actually does this)
What confuses me is that the platform doesn't seem to care if my listener isn't added on a particular worker load/unload cycle. It always wakes up my worker as long as the listener was added in the initial cycle at startup.

To wake up the service worker after it is terminated, the browser needs to remember what event types/listeners it was added. Your question is when the browser saves the background's event types and event listeners, only at the very first (the initial) startup or update these info every time when the service worker startup?

Although Web Service Worker and Extension Service Worker are not exactly the same, since Web Service Worker has a standard spec and Extension Service Worker does not have a spec, I would like to try to explain your question using Web Service Worker's specification.

Here the SW spec says:
Screenshot 2023-04-25 at 17.13.17.png

There is a "script’s has ever been evaluated flag", which is initially unset. After the very first evaluation of the worker script, it is set to true. So I think "the set of event types to handle" is only saved at the initial SW startup. The browser can skip events if serviceWorker’s set of event types to handle does not contain that event name.


--
You received this message because you are subscribed to the Google Groups "Chromium Extensions" group.
To unsubscribe from this group and stop receiving emails from it, send an email to chromium-extens...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/chromium-extensions/2e866eb2-e31e-4e42-88d3-7d7441c7705cn%40chromium.org.

Oliver Dunk

unread,
Apr 25, 2023, 8:23:19 AM4/25/23
to Jackie Han, wOxxOm, Chromium Extensions, David Roland, T S
To add what I know...

Internally to Chrome, the extensions system keeps track of which event listeners were registered by an extension. You can see this for yourself at chrome://extensions-internals/.

When an event is fired, Chrome will only wake up extension service workers that are marked as having a matching event listener.

Notably the code/function itself isn't persisted - only a flag noting that the particular event is interesting to the extension. This is why you're only seeing the log on subsequent loads if you also add the listener - and note that you need to do this synchronously, as Chrome doesn't queue the event forever.

As our docs mention, the general advice is to register listeners in the top-level and unconditionally, which is the same advice given when building service workers on the web. There is actually a bit more flexibility - for example registering inside of an `if (true) {}` would work just as well. But then you quickly get in to the weeds and need to know the implementation details of service workers a lot more to avoid unexpected behaviour.

I'm uncertain if our intent is to update the list of event listeners on each load, and I'll try to see if I can confirm with one of the engineers. But given the language in the spec that Jackie pointed out, and what works today, I expect everything here is behaving as designed.
Oliver Dunk | DevRel, Chrome Extensions | https://developer.chrome.com/ | London, GB


David Roland

unread,
Apr 25, 2023, 12:47:44 PM4/25/23
to Chromium Extensions, Oliver Dunk, wOxxOm, Chromium Extensions, David Roland, T S, Jackie Han
Thank you for the explanations. 

As you note, my example is contrived and makes little sense for a real extension. I wrote it that way only in an attempt to understand the system. My initial thought was that an alarm would wake up my worker, regardless of whether I had added an onAlarm listener. I understood that listeners are added at the top level in order to avoid the situation where the extension ends its first event loop execution without adding a listener and then never receives the event because the listener is added at a later time. But for an alarm, in my extension, I didn't need the event specifically, only that my extension wake up and load.

From the docs, I didn't understand the nuance of how the platform is maintaining a list of events that are interesting to the extension and that this list drives the wakeup decision. I'm still not entirely clear on how that list works.  Does it include only those events with listeners added on first run at the top level? Or maybe those that manage to get added at any point in first run, even asynchronously, as long as done before the first unload? What happens if I add an event listener at the top level of first run, but then my code does some work and decides to remove it, still in the first run. Does that leave the extension in a state that it will wake up on the next event, given that the extension was no longer listening for that event type at the moment of unload?

From Jackie's reference to the service worker spec, maybe the answer is that the only thing that matters is what event type listeners are registered in the first run of the first event loop. That's simple to understand, but implies an extension must always wake up for any event that it will ever need in any situation, even if it knows quickly after startup that it's not interested in an event type for the current situation/settings. That feels like a waste for an architecture that is trying to save memory and power by letting workers get as much sleep as possible.

It also seems like the ambiguity, or need to for the developer to understand the internal implementation of service workers for extensions, will cause problems with workers never waking up.

David Roland

unread,
Apr 25, 2023, 4:24:39 PM4/25/23
to Chromium Extensions, David Roland, Oliver Dunk, wOxxOm, Chromium Extensions, T S, Jackie Han
I wrote more tests, and ran them on Chrome Version 112.0.5615.138 on Windows 64-bit. All tests involve alarm events and onAlarm listeners, so I can't say whether the results generalize to other types of events. From the tests, I conclude:

* The platform will wake the worker if the worker added a listener in a prior load/unload cycle, as long as the worker did not subsequently remove that listener. It's not necessary that the add happened in the cycle directly prior. Nor does it depend at all on what will happen regarding listener adds/removes when the worker loads again
* If the listener is removed, the worker will no longer wake up in the future in response to the event.
* These rules do not depend on whether the listener was added or removed at the top level of a script. They apply whether the listener mutation happened synchronously or asynchronously. So I don't think it makes sense to understand the state of wakeup rules based on what happens at the top level of a script. It's possible (and maybe preferable in terms of avoiding unnecessary wake ups) to manage listeners asynchronously, as long as the architecture takes into account that the execution cycle may not always reach the asynchronous calls. 
* The rules do not rely on whether the listener add happened on the first cycle of the worker after startup, or in a later cycle. I'm not sure if this contradicts the spec that Jackie provided.
* Finally, an event that causes a wakeup will only be received if the associated listener is added at the worker top level, which is the main point made in the mv3 development documentation, I think.

I can't say whether this behavior is what's expected.

As an aside, I find development in the extension service worker environment quite difficult. I've decided that the only reliable approach to code structure is to assume that the worker may unload at any line that awaits the result of an asynchronous call. Events may be received by a worker whose state is fully loaded, or alternatively at the top level of a worker that is just loaded and has had no chance to resurrect its state. Given the many different types of events, all of the various asynchronous methods, the need to serialize and store state to prepare for unload at any moment, and the challenge of combining all of these issues into understanding the application's possible states, it's really tough to write code in a way that is reliable and efficient.  At least for me.

Paul Marks

unread,
Apr 25, 2023, 6:28:38 PM4/25/23
to Chromium Extensions, David Roland, Oliver Dunk, wOxxOm, Chromium Extensions, T S, Jackie Han
On Tuesday, April 25, 2023 at 4:24:39 PM UTC-4 David Roland wrote:
I've decided that the only reliable approach to code structure is to assume that the worker may unload at any line that awaits the result of an asynchronous call. Events may be received by a worker whose state is fully loaded, or alternatively at the top level of a worker that is just loaded and has had no chance to resurrect its state. Given the many different types of events, all of the various asynchronous methods, the need to serialize and store state to prepare for unload at any moment, and the challenge of combining all of these issues into understanding the application's possible states, it's really tough to write code in a way that is reliable and efficient.

IPvFoo used to keep all state in RAM.  This is the general technique I used for migrating to MV3:

- Whenever any state change occurs, write it to storage.session immediately.

- Resurrect the state on startup:
const storageReady = (async () => {
  const items = await chrome.storage.session.get();
  // use 'items' to populate global variables
})();

- "await storageReady;" from every handler, before touching any state.


It seems inefficient to hit the storage API after every state change, but such is the nature of MV3.
Thankfully, storage.session now has more quota than storage.local, so at least keeping state on disk is no longer necessary.

Jackie Han

unread,
Apr 26, 2023, 1:03:53 AM4/26/23
to Paul Marks, Chromium Extensions, David Roland, Oliver Dunk, wOxxOm, T S
Dynamically or asynchronously adding/removing extension events listeners should work in both extension pages(e.g. popup page) and service worker as long as the environment is not terminated. Only waking up service worker and then calling listeners is special.

Since there is no specification of extension's service worker, these behaviors depend entirely on the implementation details. Other browsers may have different implementations. So it's better not to rely on edge cases behavior, since their behaviors are undefined.

If you rely on some data or state asynchronously before processing the event, you can use Paul's example no matter the data is saved in storage.session or storage.local. I also use this way in my extensions like this code https://github.com/w3c/ServiceWorker/issues/1576#issuecomment-814700162

Treat the state/variables in service worker only as cache, and if necessary save them in storage. Also, wait for the asynchronous operation to complete to confirm that it has actually finished executing.


storage.session now has more quota than storage.local
 storage.local quota will also increase to 10MB since Chrome 114.

Oliver Dunk

unread,
Apr 26, 2023, 7:16:01 AM4/26/23
to Jackie Han, Paul Marks, Chromium Extensions, David Roland, wOxxOm, T S
The platform will wake the worker if the worker added a listener in a prior load/unload cycle, as long as the worker did not subsequently remove that listener.

I was able to chat to the engineering team and confirm that this is the intended behaviour. At any point you should be able to add a listener and your service worker will be woken up if needed for subsequent events of that type (i.e, we update the event listeners map at chrome://extensions-internals).

It's possible (and maybe preferable in terms of avoiding unnecessary wake ups) to manage listeners asynchronously, as long as the architecture takes into account that the execution cycle may not always reach the asynchronous calls.

Right, given the above, managing events asynchronously is fine. In most cases though my advice (which probably applies less to everyone in this thread who has more extension knowledge) would be to always register your listener synchronously and at the top level. As you mention, that makes sure your listener will run for any events that wake the service worker, and is less fiddly than trying to make sure you're correctly managing which listeners you have added.

As an aside, I find development in the extension service worker environment quite difficult. I've decided that the only reliable approach to code structure is to assume that the worker may unload at any line that awaits the result of an asynchronous call.

I definitely appreciate that. Building some amount of resilience is generally good, since it's always possible (for example) to experience a browser crash and then we can't make any guarantees whatsoever. Some of the lifetime behaviour feels quite aggressive right now though. We're looking in to having more APIs that extend the lifetime, for example https://bugs.chromium.org/p/chromium/issues/detail?id=1418780, which should hopefully help in the most noticeable cases.
Oliver Dunk | DevRel, Chrome Extensions | https://developer.chrome.com/ | London, GB

Robbi

unread,
Apr 26, 2023, 8:19:41 AM4/26/23
to Chromium Extensions, Paul Marks, David Roland, Oliver Dunk, wOxxOm, Chromium Extensions, T S, Jackie Han
The approach mentioned by Paul is, broadly speaking, the one I'm trying to use as well.
However, I am still not convinced of the absolute reliability of this approach.
I'm working on a special case; I'm migrating an extension that modifies the new browser tab.

I therefore have to manage not only the storage movements made by the SW, but also the movements made by one (or more) extension pages (in my case one or more "new tabs")
and make sure that SW and each page can always read the "most updated" values of these variables\states".
To do this, I exchange a message (in one direction or the other) warning that something may have changed and therefore it is necessary to read all the storage again.
In practice, the first thing the runtime.onMessage handler does is to read (again) the storage.
The SW may fall asleep and wake up when events are triggered.
In this case, upon reactivation, the SW would proceed to read the entire storage again, but could also react to the same events when awake; therefore I cannot rely on the latest storage dump, but will have to read everything again when each event is triggered (exactly as Paul indicated).

Realizing that every event is asynchronous, I wonder how I can be sure that the storage read operation can always be considered reliable.

I mean, let's say the SW responds to an alarm and that a storage variable is set in the chrome.alarms.onAlarm handler.
Now, if at this precise moment another event occurs, for example a message from an extension page,
what ensures that the function that handles the alarms.onAlarm event ends before the message handling function starts (and therefore before reading the aforementioned variable)?

Referring to @Paul's last post we are sure that the statement: > "Whenever any state change occurs, write it to storage.session immediately" <
is it enough to sleep peacefully?



Here is some sample code:

//Service Worker
readAllStorage() //reading all storage...
.then(cachedStorage => {
chrome.storage.session.get({'bar': 0}, itm => {
console.log('bar = ', itm.bar) //at the begining console shows:  bar = 0
})
});

chrome.alarms.onAlarm.addListener(function(alamrm) {
readAllStorage() //reading all storage...
.then(cachedStorage => {
console.log(cacheStorage);

//NOW I'M GOING TO SET "BAR" BUT IN THE MEANTIME AN INCOMING MESSAGE ARRIVES BEFORE THE WRITE OPERATION HAS ACTUALLY BEEN COMPLETED!!!
                       chrome.storage.session.set({'bar': 1}, _ => {
                   
                                 //EXECUTION WENT TO "CHROME.RUNTIME.ONMESSAGE" BUT SOON IT WILL RETURN HERE TO FINISH PENDING WORK.
chrome.action.setBadgeText({'text': 'hi'})
                        });
})
})

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
readAllStorage() //reading all storage...
.then(cachedStorage => {
chrome.storage.session.
get({'bar': 0}, itm => {
console.log(itm.bar)
//WHAT IS THE VALUE OF "BAR"? 0 OR 1 ?
})
})
});

Oliver Dunk

unread,
Apr 26, 2023, 8:35:54 AM4/26/23
to Robbi, Chromium Extensions, Paul Marks, David Roland, wOxxOm, T S, Jackie Han
In general it feels very much like you have a problem that you might want to solve with some sort of lock/queuing mechanism.

For example, to make sure you always read storage before processing any other events, you could do something like this (similar to what Paul suggested):

let bar = 0;
let readyPromise;

readyPromise = new Promise((resolve) => {
  readAllStorage().then(() => {
    chrome.storage.session.get((data) => {
      bar = data.bar;
      resolve();
    })
  });
});

chrome.alarms.onAlarm.addListener(() => {
  await readyPromise;
 
  // TODO: Do something to handle alarm.
});

chrome.runtime.onMessage.addListener(() => {
  await readyPromise;

  // TODO: Do something to handle message.
});

For queuing, you could use a library like https://www.npmjs.com/package/bottleneck with the maximum number of concurrent operations set to 1.
Oliver Dunk | DevRel, Chrome Extensions | https://developer.chrome.com/ | London, GB

Jackie Han

unread,
Apr 26, 2023, 10:53:15 AM4/26/23
to Oliver Dunk, Paul Marks, Chromium Extensions, David Roland, wOxxOm, T S
I did an experiment for adding/removing listeners dynamically in service worker.

Experiment 1
// in service worker code
function onAlarm(e) {
  chrome.tabs.create({url:'https://www.google.com'});
}

chrome.runtime.onMessage.addListener(message => {
  if(message == "remove") {
    chrome.alarms.onAlarm.removeListener(onAlarm);
  }
  if(message == "add") {
    chrome.alarms.onAlarm.addListener(onAlarm);
  }
});


Open another extension page, then do the following tests:
1. add a listener: chrome.runtime.sendMessage("add");
    chrome://extensions-internals/ show the listener is added. ⭕️
2. waiting for SW to become inactive, then fire an alarm: chrome.alarms.create({delayInMinutes: 1});
    Browser wakes up the SW, but nothing happens. ❌
3. waiting for SW became inactive, remove the listener: chrome.runtime.sendMessage("remove");
     chrome://extensions-internals/ show the listener is still there. remove fail. ❌

Experiment 2
// in service worker code
function onAlarm(e) {
  chrome.tabs.create({url:'https://www.google.com'});
}

chrome.alarms.onAlarm.addListener(onAlarm); // note this line
chrome.runtime.onMessage.addListener(message => {
  if(message == "remove") {
    chrome.alarms.onAlarm.removeListener(onAlarm);
  }
  if(message == "add") {
    chrome.alarms.onAlarm.addListener(onAlarm);
  }
});


Open another extension page, then do the following tests:
1. waiting for SW to become inactive, then fire an alarm: chrome.alarms.create({delayInMinutes: 1});
    The listener works correctly (open a tab) ⭕️
2. waiting for SW to become inactive, remove the listener: chrome.runtime.sendMessage("remove");
    chrome://extensions-internals/ show no listener. remove success. ⭕️
3. waiting for SW to become inactive, add a listener: chrome.runtime.sendMessage("add");
    chrome://extensions-internals/ show the listener is added. ⭕️
4. waiting for SW to become inactive, then fire an alarm: chrome.alarms.create({delayInMinutes: 1});
    The listener works correctly (open a tab) ⭕️


Summary: Experiment 1 encountered some problems, but Experiment 2 had no problems.

David Roland

unread,
Apr 26, 2023, 12:33:42 PM4/26/23
to Chromium Extensions, Jackie Han, Paul Marks, Chromium Extensions, David Roland, wOxxOm, T S, Oliver Dunk
Jackie, experiment 1 is an interesting example of the subtle challenge of dealing with listeners in an extension SW. A listener you added in a prior worker load/unload cycle can't be removed in the next cycle. That listener no longer exists in the new cycle. The subtle point is that though you no longer have a registered alarm listener in the new cycle, you do have a flag set saying that your worker is interested in alarm events. In order to reset the flag on your worker, so that it no longer wakes up for an alarm, you need to re-add it and then remove it. Yes, that may not feel right, but I think that's how it works. I summarize this behavior as - you can turn off the event-type interest flag by adding a listener that increments your listener count from zero to one, and then removing that listener, returning your listener count to zero. Perhaps Oliver can clarify whether this is a real rule, or just poor analysis on my part.

It's important to keep in mind that the SW event-type interest flag is related, though distinct from, the existence of a listener in the current load of the SW. 

Also, the reason that your alarm in step 2 doesn't reach your listener and open a tab is that you haven't added the listener at the top level of the SW.  The listener you added before the worker became inactive is not relevant. It's gone. Just the flag that you're interested in alarm events survives.

David Roland

unread,
Apr 26, 2023, 1:49:59 PM4/26/23
to Chromium Extensions, Oliver Dunk, Paul Marks, Chromium Extensions, David Roland, wOxxOm, T S, Jackie Han


As an aside, I find development in the extension service worker environment quite difficult. I've decided that the only reliable approach to code structure is to assume that the worker may unload at any line that awaits the result of an asynchronous call.

I definitely appreciate that. Building some amount of resilience is generally good, since it's always possible (for example) to experience a browser crash and then we can't make any guarantees whatsoever. Some of the lifetime behaviour feels quite aggressive right now though. We're looking in to having more APIs that extend the lifetime, for example https://bugs.chromium.org/p/chromium/issues/detail?id=1418780, which should hopefully help in the most noticeable cases.

That's a good point, and I appreciate that the lifecycle model may change in the future. 

For me, there's a substantial difference between handling browser crashes and worker unloads. For crashes, the browser restarts, and the extension can mostly go through it's normal process of starting up. If the extension was written well, it probably saves state along the way, but crashes are rare and the user is probably OK with the extension looking like it would on a normal browser startup.

In contrast, the SW will unload all the time. And when it reloads, it needs to feel to the user like the unload never happened. In a perfect world, the extension state returns almost immediately to the state it was in at the moment of unload, and the user notices nothing. But it's pretty hard (in my opinion) to recreate a complex state of connected objects, in which there can be many asynchronous functions in the middle of executing the function body, and messages that may have been sent to other contexts, with replies perhaps sent but maybe not yet received, etc, etc. I think a general architecture of storing everything in the session store and reading it back on restart fails to capture the big devil that's in the details. I've found some processes, like a series of connected fetches, are simple to write in a persistent environment, but require alot more thinking, debugging, etc in a SW.

So, to add to Paul's good advice to store everything and not worry about the load on the session store, I try to limit the scope of work that my SW handles, and offload whatever I can to other contexts. That primarily means that if there is a UI related to something that the extension is doing, that UI environment (the extension page, the content script), should do as much for itself as it can. 

For example, I was polling my server from the SW. When the server had something for me, I would follow-up with a sequence of fetch requests for relevant data. Once I had the data, I launched a UI to present it. But this required alot of consideration at each step of fetching regarding what would happen if the SW unloads in the middle of the sequence. So I changed tactics, and now my SW simply polls for a flag (hi, server has something), and the SW launches the relevant UI page where all of the substantial remote data requests take place. My SW poller is still substantially more complex than it was in the old background page environment, but it's compact enough to get my arms around and feel some confidence that it's robust to constant load/unload cycles.

Jackie Han

unread,
Apr 26, 2023, 1:53:26 PM4/26/23
to David Roland, Chromium Extensions, Paul Marks, wOxxOm, T S, Oliver Dunk
Basically, I understand what you mean.

1. removeListener() works only if addListener() is called before removeListener() in the same SW cycle. To remove this listener in different SW cycle,  needs to call addListener(); and then removeListener();
2. if addListener() is called asynchronously, the browser can remember its event type, but when an event fires in the next cycle, the browser can wake up SW but can't find the listener to execute. In summary, addListener() asynchronously only executes the listener in the current SW cycle, and can't execute the listener in the following SW cycle although it can wake up SW. Therefore, addListener() asynchronously is almost always wrong in SW (except you make SW persistent).

This assumption explains the behavior of the current implementation. Managing event listeners dynamically in SW is possible, like the experiment 2. I feel that there is still room for improvement in the current design and implementation.


--
You received this message because you are subscribed to the Google Groups "Chromium Extensions" group.
To unsubscribe from this group and stop receiving emails from it, send an email to chromium-extens...@chromium.org.

David Roland

unread,
Apr 26, 2023, 3:19:11 PM4/26/23
to Chromium Extensions, Jackie Han, Chromium Extensions, Paul Marks, wOxxOm, T S, Oliver Dunk, David Roland

2.Therefore, addListener() asynchronously is almost always wrong in SW (except you make SW persistent).

I think we can still addListeners asynchronously to our advantage. In my code, objects will often add and remove their own listeners, based on their current interest in a particular type of event. I like this encapsulated, OO approach to architecture. But these objects may not even exist when the SW reloads. The challenge we have is accounting for the event whether or not the object still exists, and that challenge, in an extension with a large code base, is significant.

The alternative approach, of only registering general listeners at the top level, and somehow making the events they receive accessible and useful to our objects that come later, feels wrong to me. We could write our own event listening interface, that wraps these top level listeners, and globally access that layer to allow our objects to retrieve and subscribe to events. But that's just duplicating the various extension event-driven API's.

Unfortunately, any listener that is added asynchronously may not be there when the event that it's expecting arrives. So we have to consider that and have a backup plan to deal with it after a SW reload. That plan ranges from ignoring it to making it wait for some objects to be recreated and initialized before it's processed. Also, any event management done asynchronously can impact the flag regarding your SW interest in waking up for an event. So you have to consider the listener adds and removes carefully.

It feels hard to me.

Jackie Han

unread,
Apr 26, 2023, 11:31:22 PM4/26/23
to David Roland, Chromium Extensions, Paul Marks, wOxxOm, T S, Oliver Dunk
In my previous email, I said (but I didn't explain why):
if addListener() is called asynchronously, the browser can remember the event type, but when an event fires in the next SW cycle, the browser can wake up SW but can't find the listener to execute.

Based on my assumptions, I further explain why the browser can't find the listener. Because when an event fired:
1. The browser only executes the listeners that have been added in the current running environment.
2. If the SW is inactive, the browser wakes up the SW, and executes the listeners after SW's first event loop and before the next event loop.

Example:
// service worker code
function onAlarm1() {
  chrome.tabs.create({url:'https://site-A.com'});
}
function onAlarm2() {
  chrome.tabs.create({url:'https://site-B.com'});
}
chrome.alarms.onAlarm.addListener(onAlarm1); // add listener in first event loop
setTimeout(() => chrome.alarms.onAlarm.addListener(onAlarm2), 50); // add 
listener in next event loop

Open another extension page, fire alarm event: chrome.alarms.create({when: Date.now() + 1000}); (unpacked extensions can fire alarms without one minute limitation)
1. when the SW is inactive, only the listener onAlarm1 is executed, the listener onAlarm2 is not executed. Because only onAlarm1 has been added after the first event loop.
2. when the SW is active (before termination), both onAlarm1 and onAlarm2 are executed. Because now onAlarm2 has been added, the current environment has two different listeners.


Now, Back to my previous experiments.
Experiment 1
// in service worker code
function onAlarm(e) {
  chrome.tabs.create({url:'https://www.google.com'});
}

chrome.runtime.onMessage.addListener(message => {
  if(message == "remove") {
    chrome.alarms.onAlarm.removeListener(onAlarm);
  }
  if(message == "add") {
    chrome.alarms.onAlarm.addListener(onAlarm);
  }
});

When the browser wakes up SW for an alarm event, after the first event loop, the browser can't find any listener because `chrome.alarms.onAlarm.addListener` doesn't execute.


Experiment 2
// in service worker code
function onAlarm(e) {
  chrome.tabs.create({url:'https://www.google.com'});
}

chrome.alarms.onAlarm.addListener(onAlarm); // note this line
chrome.runtime.onMessage.addListener(message => {
  if(message == "remove") {
    chrome.alarms.onAlarm.removeListener(onAlarm);
  }
  if(message == "add") {
    chrome.alarms.onAlarm.addListener(onAlarm);
  }
});

When exec `chrome.runtime.sendMessage("remove")` from another page, 
1. the browser wakes up the SW
2. In the first event loop, chrome.alarms.onAlarm.addListener(onAlarm) is executed.
3. the browser executes the onMessage listener, chrome.alarms.onAlarm.removeListener(onAlarm); is executed.
4. now the browser removed the onAlarm event type for SW.

Next time, when an alarm event fires, the browser doesn't wake up SW because there is no alarm event type for SW.
But this state is very unstable! For example, exec `chrome.runtime.sendMessage("other message")` or trigger any other SW events, the top level `chrome.alarms.onAlarm.addListener(onAlarm)` will be executed again! It will re-register the alarm listener.


Further explains addListener and removeListener.
When the SW or an extension page startup, you can think each event type has an empty Set that stores unique listeners in the current environment. Then you can add or remove listeners, if you add the same listener multiple times, only one listener is saved in Set. If you remove a listener but the Set doesn't contain that listener, nothing happens.

In summary, dynamically or async add/remove event listeners is OK for extension pages, but it's not very feasible for SW. The best practice is always adding event listeners at root level(or first event loop), don't remove it or add it dynamically or asynchronously.


--
You received this message because you are subscribed to the Google Groups "Chromium Extensions" group.
To unsubscribe from this group and stop receiving emails from it, send an email to chromium-extens...@chromium.org.

wOxxOm

unread,
Apr 27, 2023, 12:33:16 AM4/27/23
to Chromium Extensions, Jackie Han, Chromium Extensions, Paul Marks, wOxxOm, T S, Oliver Dunk, David Roland
> But it's pretty hard (in my opinion) to recreate a complex state of connected objects, in which there can be many asynchronous functions in the middle of executing the function body, and messages that may have been sent to other contexts, with replies perhaps sent but maybe not yet received, etc, etc. I think a general architecture of storing everything in the session store and reading it back on restart fails to capture the big devil that's in the details. I've found some processes, like a series of connected fetches, are simple to write in a persistent environment, but require alot more thinking, debugging, etc in a SW.

If the state is super complex the most sensible solution is to force the SW to persist as shown in https://stackoverflow.com/a/66618269.

Nontrivial extensions really need an official way to enable persistence explicitly while processing something, like maybe "keep SW running until this Promise settles" similar to the native ExtendableEvent's waitUntil method. The ManifestV3 propaganda says that persistence is inherently bad but this naive claim is not backed by proper statistics. In real life it's all about balance: if the extension works a few times a day or less then persistence is obviously bad, but if the extension works every minute for every navigation/click or it has a super complex state that is more expensive to rebuild than starting the SW then obviously persistence is a blessing. Currently ManifestV3 team treats all extension developer as irresponsible children who can't think for themselves. Maybe most of us are, I don't know, but this approach is alienating developers.
Reply all
Reply to author
Forward
0 new messages