Backed out 3 changesets (bug 1203330) for failures in test_delay_update_webextension.js
authorPhil Ringnalda <philringnalda@gmail.com>
Thu, 26 Jan 2017 19:13:11 -0800
changeset 331299 0a15564434ca982325802dbfdbc92e28ebccd1e4
parent 331298 c99f1f911660b4652132e394924401d525a77947
child 331300 4be138bdb7bf895d5d50f2884ff86c1a631c7a34
push id31265
push usercbook@mozilla.com
push dateFri, 27 Jan 2017 09:41:20 +0000
treeherdermozilla-central@dad46f412588 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1203330
milestone54.0a1
backs out2d42350d209a711e5378c1312ba4b173215f95eb
3a12c51c3eca5f1e5d563b0167a8723c5b24c537
31fac390e15db69d51aab3ea76764871a1a2a5ea
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 3 changesets (bug 1203330) for failures in test_delay_update_webextension.js CLOSED TREE Backed out changeset 2d42350d209a (bug 1203330) Backed out changeset 3a12c51c3eca (bug 1203330) Backed out changeset 31fac390e15d (bug 1203330)
browser/components/extensions/ext-bookmarks.js
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-c-omnibox.js
browser/components/extensions/ext-commands.js
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/ext-history.js
browser/components/extensions/ext-omnibox.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/ext-sessions.js
browser/components/extensions/ext-tabs.js
browser/components/extensions/ext-utils.js
browser/components/extensions/ext-windows.js
mobile/android/components/extensions/ext-pageAction.js
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/ext-alarms.js
toolkit/components/extensions/ext-c-test.js
toolkit/components/extensions/ext-cookies.js
toolkit/components/extensions/ext-downloads.js
toolkit/components/extensions/ext-idle.js
toolkit/components/extensions/ext-notifications.js
toolkit/components/extensions/ext-runtime.js
toolkit/components/extensions/ext-storage.js
toolkit/components/extensions/ext-webNavigation.js
toolkit/components/extensions/ext-webRequest.js
toolkit/components/extensions/test/xpcshell/test_ext_alarms.js
toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
toolkit/components/extensions/test/xpcshell/test_ext_idle.js
--- a/browser/components/extensions/ext-bookmarks.js
+++ b/browser/components/extensions/ext-bookmarks.js
@@ -315,60 +315,56 @@ extensions.registerSchemaAPI("bookmarks"
             .catch(error => Promise.reject({message: error.message}));
         } catch (e) {
           return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
         }
       },
 
       onCreated: new SingletonEventManager(context, "bookmarks.onCreated", fire => {
         let listener = (event, bookmark) => {
-          // Bug 1333889: make this asynchronous
-          fire.sync(bookmark.id, bookmark);
+          context.runSafe(fire, bookmark.id, bookmark);
         };
 
         observer.on("created", listener);
         incrementListeners();
         return () => {
           observer.off("created", listener);
           decrementListeners();
         };
       }).api(),
 
       onRemoved: new SingletonEventManager(context, "bookmarks.onRemoved", fire => {
         let listener = (event, data) => {
-          // Bug 1333889: make this asynchronous
-          fire.sync(data.guid, data.info);
+          context.runSafe(fire, data.guid, data.info);
         };
 
         observer.on("removed", listener);
         incrementListeners();
         return () => {
           observer.off("removed", listener);
           decrementListeners();
         };
       }).api(),
 
       onChanged: new SingletonEventManager(context, "bookmarks.onChanged", fire => {
         let listener = (event, data) => {
-          // Bug 1333889: make this asynchronous
-          fire.sync(data.guid, data.info);
+          context.runSafe(fire, data.guid, data.info);
         };
 
         observer.on("changed", listener);
         incrementListeners();
         return () => {
           observer.off("changed", listener);
           decrementListeners();
         };
       }).api(),
 
       onMoved: new SingletonEventManager(context, "bookmarks.onMoved", fire => {
         let listener = (event, data) => {
-          // Bug 1333889: make this asynchronous
-          fire.sync(data.guid, data.info);
+          context.runSafe(fire, data.guid, data.info);
         };
 
         observer.on("moved", listener);
         incrementListeners();
         return () => {
           observer.off("moved", listener);
           decrementListeners();
         };
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -13,17 +13,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "@mozilla.org/inspector/dom-utils;1",
                                    "inIDOMUtils");
 
 Cu.import("resource://devtools/shared/event-emitter.js");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 var {
-  SingletonEventManager,
+  EventManager,
   IconDetails,
 } = ExtensionUtils;
 
 const POPUP_PRELOAD_TIMEOUT_MS = 200;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 function isAncestorOrSelf(target, node) {
@@ -441,20 +441,20 @@ extensions.on("shutdown", (type, extensi
   }
 });
 /* eslint-enable mozilla/balanced-listeners */
 
 extensions.registerSchemaAPI("browserAction", "addon_parent", context => {
   let {extension} = context;
   return {
     browserAction: {
-      onClicked: new SingletonEventManager(context, "browserAction.onClicked", fire => {
+      onClicked: new EventManager(context, "browserAction.onClicked", fire => {
         let listener = () => {
           let tab = TabManager.activeTab;
-          fire.async(TabManager.convert(extension, tab));
+          fire(TabManager.convert(extension, tab));
         };
         BrowserAction.for(extension).on("click", listener);
         return () => {
           BrowserAction.for(extension).off("click", listener);
         };
       }).api(),
 
       enable: function(tabId) {
--- a/browser/components/extensions/ext-c-omnibox.js
+++ b/browser/components/extensions/ext-c-omnibox.js
@@ -1,24 +1,25 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
+  runSafeSyncWithoutClone,
   SingletonEventManager,
 } = ExtensionUtils;
 
 extensions.registerSchemaAPI("omnibox", "addon_child", context => {
   return {
     omnibox: {
       onInputChanged: new SingletonEventManager(context, "omnibox.onInputChanged", fire => {
         let listener = (text, id) => {
-          fire.asyncWithoutClone(text, suggestions => {
+          runSafeSyncWithoutClone(fire, text, suggestions => {
             // TODO: Switch to using callParentFunctionNoReturn once bug 1314903 is fixed.
             context.childManager.callParentAsyncFunction("omnibox_internal.addSuggestions", [
               id,
               suggestions,
             ]);
           });
         };
         context.childManager.getParentEvent("omnibox_internal.onInputChanged").addListener(listener);
--- a/browser/components/extensions/ext-commands.js
+++ b/browser/components/extensions/ext-commands.js
@@ -1,17 +1,17 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 Cu.import("resource://devtools/shared/event-emitter.js");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
-  SingletonEventManager,
+  EventManager,
   PlatformInfo,
 } = ExtensionUtils;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // WeakMap[Extension -> CommandList]
 var commandsMap = new WeakMap();
 
@@ -240,19 +240,19 @@ extensions.registerSchemaAPI("commands",
         return Promise.resolve(Array.from(commands, ([name, command]) => {
           return ({
             name,
             description: command.description,
             shortcut: command.shortcut,
           });
         }));
       },
-      onCommand: new SingletonEventManager(context, "commands.onCommand", fire => {
+      onCommand: new EventManager(context, "commands.onCommand", fire => {
         let listener = (eventName, commandName) => {
-          fire.async(commandName);
+          fire(commandName);
         };
         commandsMap.get(extension).on("command", listener);
         return () => {
           commandsMap.get(extension).off("command", listener);
         };
       }).api(),
     },
   };
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -3,19 +3,19 @@
 "use strict";
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/MatchPattern.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 var {
+  EventManager,
   ExtensionError,
   IconDetails,
-  SingletonEventManager,
 } = ExtensionUtils;
 
 const ACTION_MENU_TOP_LEVEL_LIMIT = 6;
 
 // Map[Extension -> Map[ID -> MenuItem]]
 // Note: we want to enumerate all the menu items so
 // this cannot be a weak map.
 var gContextMenuMap = new Map();
@@ -620,19 +620,19 @@ extensions.registerSchemaAPI("contextMen
 
       removeAll: function() {
         let root = gRootItems.get(extension);
         if (root) {
           root.remove();
         }
       },
 
-      onClicked: new SingletonEventManager(context, "contextMenus.onClicked", fire => {
+      onClicked: new EventManager(context, "contextMenus.onClicked", fire => {
         let listener = (event, info, tab) => {
-          fire.async(info, tab);
+          fire(info, tab);
         };
 
         extension.on("webext-contextmenu-menuitem-click", listener);
         return () => {
           extension.off("webext-contextmenu-menuitem-click", listener);
         };
       }).api(),
     },
--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -217,28 +217,28 @@ extensions.registerSchemaAPI("history", 
         historyQuery.uri = NetUtil.newURI(url);
         let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
         let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToVisitItem);
         return Promise.resolve(results);
       },
 
       onVisited: new SingletonEventManager(context, "history.onVisited", fire => {
         let listener = (event, data) => {
-          fire.sync(data);
+          context.runSafe(fire, data);
         };
 
         getObserver().on("visited", listener);
         return () => {
           getObserver().off("visited", listener);
         };
       }).api(),
 
       onVisitRemoved: new SingletonEventManager(context, "history.onVisitRemoved", fire => {
         let listener = (event, data) => {
-          fire.sync(data);
+          context.runSafe(fire, data);
         };
 
         getObserver().on("visitRemoved", listener);
         return () => {
           getObserver().off("visitRemoved", listener);
         };
       }).api(),
     },
--- a/browser/components/extensions/ext-omnibox.js
+++ b/browser/components/extensions/ext-omnibox.js
@@ -45,37 +45,37 @@ extensions.registerSchemaAPI("omnibox", 
           ExtensionSearchHandler.setDefaultSuggestion(keyword, suggestion);
         } catch (e) {
           return Promise.reject(e.message);
         }
       },
 
       onInputStarted: new SingletonEventManager(context, "omnibox.onInputStarted", fire => {
         let listener = (eventName) => {
-          fire.sync();
+          fire();
         };
         extension.on(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
         return () => {
           extension.off(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
         };
       }).api(),
 
       onInputCancelled: new SingletonEventManager(context, "omnibox.onInputCancelled", fire => {
         let listener = (eventName) => {
-          fire.sync();
+          fire();
         };
         extension.on(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
         return () => {
           extension.off(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
         };
       }).api(),
 
       onInputEntered: new SingletonEventManager(context, "omnibox.onInputEntered", fire => {
         let listener = (eventName, text, disposition) => {
-          fire.sync(text, disposition);
+          fire(text, disposition);
         };
         extension.on(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
         return () => {
           extension.off(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
         };
       }).api(),
     },
 
@@ -87,17 +87,17 @@ extensions.registerSchemaAPI("omnibox", 
         } catch (e) {
           // Silently fail because the extension developer can not know for sure if the user
           // has already invalidated the callback when asynchronously providing suggestions.
         }
       },
 
       onInputChanged: new SingletonEventManager(context, "omnibox_internal.onInputChanged", fire => {
         let listener = (eventName, text, id) => {
-          fire.sync(text, id);
+          fire(text, id);
         };
         extension.on(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
         return () => {
           extension.off(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
         };
       }).api(),
     },
   };
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -1,16 +1,16 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
-  SingletonEventManager,
+  EventManager,
   IconDetails,
 } = ExtensionUtils;
 
 // WeakMap[Extension -> PageAction]
 var pageActionMap = new WeakMap();
 
 // Handles URL bar icons, including the |page_action| manifest entry
 // and associated API.
@@ -239,19 +239,19 @@ PageAction.for = extension => {
 };
 
 global.pageActionFor = PageAction.for;
 
 extensions.registerSchemaAPI("pageAction", "addon_parent", context => {
   let {extension} = context;
   return {
     pageAction: {
-      onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => {
+      onClicked: new EventManager(context, "pageAction.onClicked", fire => {
         let listener = (evt, tab) => {
-          fire.async(TabManager.convert(extension, tab));
+          fire(TabManager.convert(extension, tab));
         };
         let pageAction = PageAction.for(extension);
 
         pageAction.on("click", listener);
         return () => {
           pageAction.off("click", listener);
         };
       }).api(),
--- a/browser/components/extensions/ext-sessions.js
+++ b/browser/components/extensions/ext-sessions.js
@@ -89,17 +89,17 @@ extensions.registerSchemaAPI("sessions",
           closedId = recentlyClosedTabs[0].closedId;
           session = SessionStore.undoCloseById(closedId);
         }
         return createSession(session, extension, closedId);
       },
 
       onChanged: new SingletonEventManager(context, "sessions.onChanged", fire => {
         let observer = () => {
-          fire.async();
+          context.runSafe(fire);
         };
 
         Services.obs.addObserver(observer, SS_ON_CLOSED_OBJECTS_CHANGED, false);
         return () => {
           Services.obs.removeObserver(observer, SS_ON_CLOSED_OBJECTS_CHANGED);
         };
       }).api(),
     },
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -13,17 +13,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
-  SingletonEventManager,
+  EventManager,
   ignoreEvent,
 } = ExtensionUtils;
 
 // This function is pretty tightly tied to Extension.jsm.
 // Its job is to fill in the |tab| property of the sender.
 function getSender(extension, target, sender) {
   let tabId;
   if ("tabId" in sender) {
@@ -278,22 +278,22 @@ extensions.on("startup", () => {
 extensions.registerSchemaAPI("tabs", "addon_parent", context => {
   let {extension} = context;
   let self = {
     tabs: {
       onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
         let tab = event.originalTarget;
         let tabId = TabManager.getId(tab);
         let windowId = WindowManager.getId(tab.ownerGlobal);
-        fire.async({tabId, windowId});
+        fire({tabId, windowId});
       }).api(),
 
-      onCreated: new SingletonEventManager(context, "tabs.onCreated", fire => {
+      onCreated: new EventManager(context, "tabs.onCreated", fire => {
         let listener = (eventName, event) => {
-          fire.async(TabManager.convert(extension, event.tab));
+          fire(TabManager.convert(extension, event.tab));
         };
 
         tabListener.on("tab-created", listener);
         return () => {
           tabListener.off("tab-created", listener);
         };
       }).api(),
 
@@ -302,55 +302,55 @@ extensions.registerSchemaAPI("tabs", "ad
        * essentially acts an alias for self.tabs.onActivated but returns
        * the tabId in an array to match the API.
        * @see  https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
       */
       onHighlighted: new WindowEventManager(context, "tabs.onHighlighted", "TabSelect", (fire, event) => {
         let tab = event.originalTarget;
         let tabIds = [TabManager.getId(tab)];
         let windowId = WindowManager.getId(tab.ownerGlobal);
-        fire.async({tabIds, windowId});
+        fire({tabIds, windowId});
       }).api(),
 
-      onAttached: new SingletonEventManager(context, "tabs.onAttached", fire => {
+      onAttached: new EventManager(context, "tabs.onAttached", fire => {
         let listener = (eventName, event) => {
-          fire.async(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
+          fire(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
         };
 
         tabListener.on("tab-attached", listener);
         return () => {
           tabListener.off("tab-attached", listener);
         };
       }).api(),
 
-      onDetached: new SingletonEventManager(context, "tabs.onDetached", fire => {
+      onDetached: new EventManager(context, "tabs.onDetached", fire => {
         let listener = (eventName, event) => {
-          fire.async(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
+          fire(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
         };
 
         tabListener.on("tab-detached", listener);
         return () => {
           tabListener.off("tab-detached", listener);
         };
       }).api(),
 
-      onRemoved: new SingletonEventManager(context, "tabs.onRemoved", fire => {
+      onRemoved: new EventManager(context, "tabs.onRemoved", fire => {
         let listener = (eventName, event) => {
-          fire.async(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
+          fire(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
         };
 
         tabListener.on("tab-removed", listener);
         return () => {
           tabListener.off("tab-removed", listener);
         };
       }).api(),
 
       onReplaced: ignoreEvent(context, "tabs.onReplaced"),
 
-      onMoved: new SingletonEventManager(context, "tabs.onMoved", fire => {
+      onMoved: new EventManager(context, "tabs.onMoved", fire => {
         // There are certain circumstances where we need to ignore a move event.
         //
         // Namely, the first time the tab is moved after it's created, we need
         // to report the final position as the initial position in the tab's
         // onAttached or onCreated event. This is because most tabs are inserted
         // in a temporary location and then moved after the TabOpen event fires,
         // which generates a TabOpen event followed by a TabMove event, which
         // does not match the contract of our API.
@@ -368,32 +368,32 @@ extensions.registerSchemaAPI("tabs", "ad
         let moveListener = event => {
           let tab = event.originalTarget;
 
           if (ignoreNextMove.has(tab)) {
             ignoreNextMove.delete(tab);
             return;
           }
 
-          fire.async(TabManager.getId(tab), {
+          fire(TabManager.getId(tab), {
             windowId: WindowManager.getId(tab.ownerGlobal),
             fromIndex: event.detail,
             toIndex: tab._tPos,
           });
         };
 
         AllWindowEvents.addListener("TabMove", moveListener);
         AllWindowEvents.addListener("TabOpen", openListener);
         return () => {
           AllWindowEvents.removeListener("TabMove", moveListener);
           AllWindowEvents.removeListener("TabOpen", openListener);
         };
       }).api(),
 
-      onUpdated: new SingletonEventManager(context, "tabs.onUpdated", fire => {
+      onUpdated: new EventManager(context, "tabs.onUpdated", fire => {
         const restricted = ["url", "favIconUrl", "title"];
 
         function sanitize(extension, changeInfo) {
           let result = {};
           let nonempty = false;
           for (let prop in changeInfo) {
             if (extension.hasPermission("tabs") || !restricted.includes(prop)) {
               nonempty = true;
@@ -405,17 +405,17 @@ extensions.registerSchemaAPI("tabs", "ad
 
         let fireForBrowser = (browser, changed) => {
           let [needed, changeInfo] = sanitize(extension, changed);
           if (needed) {
             let gBrowser = browser.ownerGlobal.gBrowser;
             let tabElem = gBrowser.getTabForBrowser(browser);
 
             let tab = TabManager.convert(extension, tabElem);
-            fire.async(tab.id, changeInfo, tab);
+            fire(tab.id, changeInfo, tab);
           }
         };
 
         let listener = event => {
           let needed = [];
           if (event.type == "TabAttrModified") {
             let changed = event.detail.changed;
             if (changed.includes("image")) {
@@ -442,17 +442,17 @@ extensions.registerSchemaAPI("tabs", "ad
 
           if (needed.length) {
             let tab = TabManager.convert(extension, event.originalTarget);
 
             let changeInfo = {};
             for (let prop of needed) {
               changeInfo[prop] = tab[prop];
             }
-            fire.async(tab.id, changeInfo, tab);
+            fire(tab.id, changeInfo, tab);
           }
         };
         let progressListener = {
           onStateChange(browser, webProgress, request, stateFlags, statusCode) {
             if (!webProgress.isTopLevel) {
               return;
             }
 
@@ -1020,17 +1020,17 @@ extensions.registerSchemaAPI("tabs", "ad
         let currentSettings = this._getZoomSettings(tab.id);
 
         if (!Object.keys(settings).every(key => settings[key] === currentSettings[key])) {
           return Promise.reject(`Unsupported zoom settings: ${JSON.stringify(settings)}`);
         }
         return Promise.resolve();
       },
 
-      onZoomChange: new SingletonEventManager(context, "tabs.onZoomChange", fire => {
+      onZoomChange: new EventManager(context, "tabs.onZoomChange", fire => {
         let getZoomLevel = browser => {
           let {ZoomManager} = browser.ownerGlobal;
 
           return ZoomManager.getZoomForBrowser(browser);
         };
 
         // Stores the last known zoom level for each tab's browser.
         // WeakMap[<browser> -> number]
@@ -1070,17 +1070,17 @@ extensions.registerSchemaAPI("tabs", "ad
 
           let oldZoomFactor = zoomLevels.get(browser);
           let newZoomFactor = getZoomLevel(browser);
 
           if (oldZoomFactor != newZoomFactor) {
             zoomLevels.set(browser, newZoomFactor);
 
             let tabId = TabManager.getId(tab);
-            fire.async({
+            fire({
               tabId,
               oldZoomFactor,
               newZoomFactor,
               zoomSettings: self.tabs._getZoomSettings(tabId),
             });
           }
         };
 
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -23,18 +23,18 @@ Cu.import("resource://gre/modules/Extens
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 const POPUP_LOAD_TIMEOUT_MS = 200;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 var {
   DefaultWeakMap,
+  EventManager,
   promiseEvent,
-  SingletonEventManager,
 } = ExtensionUtils;
 
 // This file provides some useful code for the |tabs| and |windows|
 // modules. All of the code is installed on |global|, which is a scope
 // shared among the different ext-*.js scripts.
 
 global.makeWidgetId = id => {
   id = id.toLowerCase();
@@ -1276,21 +1276,21 @@ global.AllWindowEvents = {
         this.addWindowListener(window, eventType, listener);
       }
     }
   },
 };
 
 AllWindowEvents.openListener = AllWindowEvents.openListener.bind(AllWindowEvents);
 
-// Subclass of SingletonEventManager where we just need to call
+// Subclass of EventManager where we just need to call
 // add/removeEventListener on each XUL window.
-global.WindowEventManager = class extends SingletonEventManager {
-  constructor(context, name, event, listener) {
-    super(context, name, fire => {
-      let listener2 = (...args) => listener(fire, ...args);
-      AllWindowEvents.addListener(event, listener2);
-      return () => {
-        AllWindowEvents.removeListener(event, listener2);
-      };
-    });
-  }
+global.WindowEventManager = function(context, name, event, listener) {
+  EventManager.call(this, context, name, fire => {
+    let listener2 = (...args) => listener(fire, ...args);
+    AllWindowEvents.addListener(event, listener2);
+    return () => {
+      AllWindowEvents.removeListener(event, listener2);
+    };
+  });
 };
+
+WindowEventManager.prototype = Object.create(EventManager.prototype);
--- a/browser/components/extensions/ext-windows.js
+++ b/browser/components/extensions/ext-windows.js
@@ -7,50 +7,50 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "nsIAboutNewTabService");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
-  SingletonEventManager,
+  EventManager,
   promiseObserved,
 } = ExtensionUtils;
 
 function onXULFrameLoaderCreated({target}) {
   target.messageManager.sendAsyncMessage("AllowScriptsToClose", {});
 }
 
 extensions.registerSchemaAPI("windows", "addon_parent", context => {
   let {extension} = context;
   return {
     windows: {
       onCreated:
       new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
-        fire.async(WindowManager.convert(extension, window));
+        fire(WindowManager.convert(extension, window));
       }).api(),
 
       onRemoved:
       new WindowEventManager(context, "windows.onRemoved", "domwindowclosed", (fire, window) => {
-        fire.async(WindowManager.getId(window));
+        fire(WindowManager.getId(window));
       }).api(),
 
-      onFocusChanged: new SingletonEventManager(context, "windows.onFocusChanged", fire => {
+      onFocusChanged: new EventManager(context, "windows.onFocusChanged", fire => {
         // Keep track of the last windowId used to fire an onFocusChanged event
         let lastOnFocusChangedWindowId;
 
         let listener = event => {
           // Wait a tick to avoid firing a superfluous WINDOW_ID_NONE
           // event when switching focus between two Firefox windows.
           Promise.resolve().then(() => {
             let window = Services.focus.activeWindow;
             let windowId = window ? WindowManager.getId(window) : WindowManager.WINDOW_ID_NONE;
             if (windowId !== lastOnFocusChangedWindowId) {
-              fire.async(windowId);
+              fire(windowId);
               lastOnFocusChangedWindowId = windowId;
             }
           });
         };
         AllWindowEvents.addListener("focus", listener);
         AllWindowEvents.addListener("blur", listener);
         return () => {
           AllWindowEvents.removeListener("focus", listener);
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -127,17 +127,17 @@ extensions.on("shutdown", (type, extensi
 /* eslint-enable mozilla/balanced-listeners */
 
 extensions.registerSchemaAPI("pageAction", "addon_parent", context => {
   let {extension} = context;
   return {
     pageAction: {
       onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => {
         let listener = (event) => {
-          fire.async();
+          fire();
         };
         pageActionMap.get(extension).on("click", listener);
         return () => {
           pageActionMap.get(extension).off("click", listener);
         };
       }).api(),
 
       show(tabId) {
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -36,16 +36,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
 const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
 
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 const {
   DefaultMap,
+  EventManager,
   SingletonEventManager,
   SpreadArgs,
   defineLazyGetter,
   getInnerWindowID,
   getMessageManager,
   getUniqueId,
   injectAPI,
   promiseEvent,
@@ -113,27 +114,27 @@ class Port {
       disconnect: () => {
         this.disconnect();
       },
 
       postMessage: json => {
         this.postMessage(json);
       },
 
-      onDisconnect: new SingletonEventManager(this.context, "Port.onDisconnect", fire => {
+      onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
         return this.registerOnDisconnect(error => {
           portError = error && this.context.normalizeError(error);
-          fire.asyncWithoutClone(portObj);
+          fire.withoutClone(portObj);
         });
       }).api(),
 
-      onMessage: new SingletonEventManager(this.context, "Port.onMessage", fire => {
+      onMessage: new EventManager(this.context, "Port.onMessage", fire => {
         return this.registerOnMessage(msg => {
           msg = Cu.cloneInto(msg, this.context.cloneScope);
-          fire.asyncWithoutClone(msg, portObj);
+          fire.withoutClone(msg, portObj);
         });
       }).api(),
 
       get error() {
         return portError;
       },
     };
 
@@ -324,17 +325,17 @@ class Messenger {
   }
 
   sendNativeMessage(messageManager, msg, recipient, responseCallback) {
     msg = NativeApp.encodeMessage(this.context, msg);
     return this.sendMessage(messageManager, msg, recipient, responseCallback);
   }
 
   onMessage(name) {
-    return new SingletonEventManager(this.context, name, fire => {
+    return new SingletonEventManager(this.context, name, callback => {
       let listener = {
         messageFilterPermissive: this.optionalFilter,
         messageFilterStrict: this.filter,
 
         filterMessage: (sender, recipient) => {
           // Ignore the message if it was sent by this Messenger.
           return sender.contextId !== this.context.contextId;
         },
@@ -354,17 +355,17 @@ class Messenger {
           });
 
           message = Cu.cloneInto(message, this.context.cloneScope);
           sender = Cu.cloneInto(sender, this.context.cloneScope);
           sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
 
           // Note: We intentionally do not use runSafe here so that any
           // errors are propagated to the message sender.
-          let result = fire.raw(message, sender, sendResponse);
+          let result = callback(message, sender, sendResponse);
           if (result instanceof this.context.cloneScope.Promise) {
             return result;
           } else if (result === true) {
             return promise;
           }
           return response;
         },
       };
@@ -406,17 +407,17 @@ class Messenger {
     let portId = getUniqueId();
 
     let port = new NativePort(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
 
     return this._connect(messageManager, port, recipient);
   }
 
   onConnect(name) {
-    return new SingletonEventManager(this.context, name, fire => {
+    return new SingletonEventManager(this.context, name, callback => {
       let listener = {
         messageFilterPermissive: this.optionalFilter,
         messageFilterStrict: this.filter,
 
         filterMessage: (sender, recipient) => {
           // Ignore the port if it was created by this Messenger.
           return sender.contextId !== this.context.contextId;
         },
@@ -425,17 +426,17 @@ class Messenger {
           let {name, portId} = message;
           let mm = getMessageManager(target);
           let recipient = Object.assign({}, sender);
           if (recipient.tab) {
             recipient.tabId = recipient.tab.id;
             delete recipient.tab;
           }
           let port = new Port(this.context, mm, this.messageManagers, name, portId, sender, recipient);
-          fire.asyncWithoutClone(port.api());
+          this.context.runSafeWithoutClone(callback, port.api());
           return true;
         },
       };
 
       MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
       return () => {
         MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
       };
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -575,104 +575,157 @@ LocaleData.prototype = {
     // Return the browser locale, but convert it to a Chrome-style
     // locale code.
     return Locale.getLocale().replace(/-/g, "_");
   },
 };
 
 // This is a generic class for managing event listeners. Example usage:
 //
-// new SingletonEventManager(context, "api.subAPI", fire => {
+// new EventManager(context, "api.subAPI", fire => {
 //   let listener = (...) => {
 //     // Fire any listeners registered with addListener.
-//     fire.async(arg1, arg2);
+//     fire(arg1, arg2);
 //   };
 //   // Register the listener.
 //   SomehowRegisterListener(listener);
 //   return () => {
 //     // Return a way to unregister the listener.
 //     SomehowUnregisterListener(listener);
 //   };
 // }).api()
 //
 // The result is an object with addListener, removeListener, and
 // hasListener methods. |context| is an add-on scope (either an
 // ExtensionContext in the chrome process or ExtensionContext in a
 // content process). |name| is for debugging. |register| is a function
-// to register the listener. |register| should return an
+// to register the listener. |register| is only called once, even if
+// multiple listeners are registered. |register| should return an
 // unregister function that will unregister the listener.
+function EventManager(context, name, register) {
+  this.context = context;
+  this.name = name;
+  this.register = register;
+  this.unregister = null;
+  this.callbacks = new Set();
+}
+
+EventManager.prototype = {
+  addListener(callback) {
+    if (typeof(callback) != "function") {
+      dump(`Expected function\n${Error().stack}`);
+      return;
+    }
+    if (this.context.unloaded) {
+      dump(`Cannot add listener to ${this.name} after context unloaded`);
+      return;
+    }
+
+    if (!this.callbacks.size) {
+      this.context.callOnClose(this);
+
+      let fireFunc = this.fire.bind(this);
+      let fireWithoutClone = this.fireWithoutClone.bind(this);
+      fireFunc.withoutClone = fireWithoutClone;
+      this.unregister = this.register(fireFunc);
+    }
+    this.callbacks.add(callback);
+  },
+
+  removeListener(callback) {
+    if (!this.callbacks.size) {
+      return;
+    }
+
+    this.callbacks.delete(callback);
+    if (this.callbacks.size == 0) {
+      this.unregister();
+      this.unregister = null;
+
+      this.context.forgetOnClose(this);
+    }
+  },
+
+  hasListener(callback) {
+    return this.callbacks.has(callback);
+  },
+
+  fire(...args) {
+    this._fireCommon("runSafe", args);
+  },
+
+  fireWithoutClone(...args) {
+    this._fireCommon("runSafeWithoutClone", args);
+  },
+
+  _fireCommon(runSafeMethod, args) {
+    for (let callback of this.callbacks) {
+      Promise.resolve(callback).then(callback => {
+        if (this.context.unloaded) {
+          dump(`${this.name} event fired after context unloaded.\n`);
+        } else if (!this.context.active) {
+          dump(`${this.name} event fired while context is inactive.\n`);
+        } else if (this.callbacks.has(callback)) {
+          this.context[runSafeMethod](callback, ...args);
+        }
+      });
+    }
+  },
+
+  close() {
+    if (this.callbacks.size) {
+      this.unregister();
+    }
+    this.callbacks.clear();
+    this.register = null;
+    this.unregister = null;
+  },
+
+  api() {
+    return {
+      addListener: callback => this.addListener(callback),
+      removeListener: callback => this.removeListener(callback),
+      hasListener: callback => this.hasListener(callback),
+    };
+  },
+};
+
+// Similar to EventManager, but it doesn't try to consolidate event
+// notifications. Each addListener call causes us to register once. It
+// allows extra arguments to be passed to addListener.
 function SingletonEventManager(context, name, register) {
   this.context = context;
   this.name = name;
   this.register = register;
   this.unregister = new Map();
 }
 
 SingletonEventManager.prototype = {
   addListener(callback, ...args) {
-    if (this.unregister.has(callback)) {
-      return;
-    }
-
-    let shouldFire = () => {
+    let wrappedCallback = (...args) => {
       if (this.context.unloaded) {
         dump(`${this.name} event fired after context unloaded.\n`);
-      } else if (!this.context.active) {
-        dump(`${this.name} event fired while context is inactive.\n`);
       } else if (this.unregister.has(callback)) {
-        return true;
+        return callback(...args);
       }
-      return false;
     };
 
-    let fire = {
-      sync: (...args) => {
-        if (shouldFire()) {
-          return this.context.runSafe(callback, ...args);
-        }
-      },
-      async: (...args) => {
-        return Promise.resolve().then(() => {
-          if (shouldFire()) {
-            return this.context.runSafe(callback, ...args);
-          }
-        });
-      },
-      raw: (...args) => {
-        if (!shouldFire()) {
-          throw new Error("Called raw() on unloaded/inactive context");
-        }
-        return callback(...args);
-      },
-      asyncWithoutClone: (...args) => {
-        return Promise.resolve().then(() => {
-          if (shouldFire()) {
-            return this.context.runSafeWithoutClone(callback, ...args);
-          }
-        });
-      },
-    };
-
-
-    let unregister = this.register(fire, ...args);
+    let unregister = this.register(wrappedCallback, ...args);
     this.unregister.set(callback, unregister);
     this.context.callOnClose(this);
   },
 
   removeListener(callback) {
     if (!this.unregister.has(callback)) {
       return;
     }
 
     let unregister = this.unregister.get(callback);
     this.unregister.delete(callback);
     unregister();
-    if (this.unregister.size == 0) {
-      this.context.forgetOnClose(this);
-    }
   },
 
   hasListener(callback) {
     return this.unregister.has(callback);
   },
 
   close() {
     for (let unregister of this.unregister.values()) {
@@ -1145,16 +1198,17 @@ this.ExtensionUtils = {
   runSafe,
   runSafeSync,
   runSafeSyncWithoutClone,
   runSafeWithoutClone,
   stylesheetMap,
   DefaultMap,
   DefaultWeakMap,
   EventEmitter,
+  EventManager,
   ExtensionError,
   IconDetails,
   LocaleData,
   MessageManagerProxy,
   PlatformInfo,
   SingletonEventManager,
   SpreadArgs,
 };
--- a/toolkit/components/extensions/ext-alarms.js
+++ b/toolkit/components/extensions/ext-alarms.js
@@ -1,15 +1,15 @@
 "use strict";
 
 var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
-  SingletonEventManager,
+  EventManager,
 } = ExtensionUtils;
 
 // WeakMap[Extension -> Map[name -> Alarm]]
 var alarmsMap = new WeakMap();
 
 // WeakMap[Extension -> Set[callback]]
 var alarmCallbacksMap = new WeakMap();
 
@@ -135,19 +135,19 @@ extensions.registerSchemaAPI("alarms", "
         let cleared = false;
         for (let alarm of alarmsMap.get(extension).values()) {
           alarm.clear();
           cleared = true;
         }
         return Promise.resolve(cleared);
       },
 
-      onAlarm: new SingletonEventManager(context, "alarms.onAlarm", fire => {
+      onAlarm: new EventManager(context, "alarms.onAlarm", fire => {
         let callback = alarm => {
-          fire.sync(alarm.data);
+          fire(alarm.data);
         };
 
         alarmCallbacksMap.get(extension).add(callback);
         return () => {
           alarmCallbacksMap.get(extension).delete(callback);
         };
       }).api(),
     },
--- a/toolkit/components/extensions/ext-c-test.js
+++ b/toolkit/components/extensions/ext-c-test.js
@@ -166,17 +166,17 @@ function makeTestAPI(context) {
           assertTrue(errorMatches(error, expectedError, context),
                      `Function threw, expecting error to match ${toSource(expectedError)}` +
                      `got ${errorMessage}${msg}`);
         }
       },
 
       onMessage: new SingletonEventManager(context, "test.onMessage", fire => {
         let handler = (event, ...args) => {
-          fire.async(...args);
+          context.runSafe(fire, ...args);
         };
 
         extension.on("test-harness-message", handler);
         return () => {
           extension.off("test-harness-message", handler);
         };
       }).api(),
     },
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -4,17 +4,17 @@ const {interfaces: Ci, utils: Cu} = Comp
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
                                   "resource://gre/modules/ContextualIdentityService.jsm");
 
 var {
-  SingletonEventManager,
+  EventManager,
 } = ExtensionUtils;
 
 var DEFAULT_STORE = "firefox-default";
 var PRIVATE_STORE = "firefox-private";
 var CONTAINER_STORE = "firefox-container-";
 
 global.getCookieStoreIdForTab = function(data, tab) {
   if (data.incognito) {
@@ -433,23 +433,23 @@ extensions.registerSchemaAPI("cookies", 
 
         let result = [];
         for (let key in data) {
           result.push({id: key, tabIds: data[key], incognito: key == PRIVATE_STORE});
         }
         return Promise.resolve(result);
       },
 
-      onChanged: new SingletonEventManager(context, "cookies.onChanged", fire => {
+      onChanged: new EventManager(context, "cookies.onChanged", fire => {
         let observer = (subject, topic, data) => {
           let notify = (removed, cookie, cause) => {
             cookie.QueryInterface(Ci.nsICookie2);
 
             if (extension.whiteListedHosts.matchesCookie(cookie)) {
-              fire.async({removed, cookie: convert({cookie, isPrivate: topic == "private-cookie-changed"}), cause});
+              fire({removed, cookie: convert({cookie, isPrivate: topic == "private-cookie-changed"}), cause});
             }
           };
 
           // We do our best effort here to map the incompatible states.
           switch (data) {
             case "deleted":
               notify(true, subject, "explicit");
               break;
--- a/toolkit/components/extensions/ext-downloads.js
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -16,16 +16,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
                                   "resource://devtools/shared/event-emitter.js");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 const {
   ignoreEvent,
   normalizeTime,
+  runSafeSync,
   SingletonEventManager,
   PlatformInfo,
 } = ExtensionUtils;
 
 const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
                               "danger", "mime", "startTime", "endTime",
                               "estimatedEndTime", "state",
                               "paused", "canResume", "error",
@@ -745,47 +746,47 @@ extensions.registerSchemaAPI("downloads"
               changes[fld] = {
                 previous: noundef(item.prechange[fld]),
                 current: noundef(item[fld]),
               };
             }
           });
           if (Object.keys(changes).length > 0) {
             changes.id = item.id;
-            fire.async(changes);
+            runSafeSync(context, fire, changes);
           }
         };
 
         let registerPromise = DownloadMap.getDownloadList().then(() => {
           DownloadMap.on("change", handler);
         });
         return () => {
           registerPromise.then(() => {
             DownloadMap.off("change", handler);
           });
         };
       }).api(),
 
       onCreated: new SingletonEventManager(context, "downloads.onCreated", fire => {
         const handler = (what, item) => {
-          fire.async(item.serialize());
+          runSafeSync(context, fire, item.serialize());
         };
         let registerPromise = DownloadMap.getDownloadList().then(() => {
           DownloadMap.on("create", handler);
         });
         return () => {
           registerPromise.then(() => {
             DownloadMap.off("create", handler);
           });
         };
       }).api(),
 
       onErased: new SingletonEventManager(context, "downloads.onErased", fire => {
         const handler = (what, item) => {
-          fire.async(item.id);
+          runSafeSync(context, fire, item.id);
         };
         let registerPromise = DownloadMap.getDownloadList().then(() => {
           DownloadMap.on("erase", handler);
         });
         return () => {
           registerPromise.then(() => {
             DownloadMap.off("erase", handler);
           });
--- a/toolkit/components/extensions/ext-idle.js
+++ b/toolkit/components/extensions/ext-idle.js
@@ -76,17 +76,17 @@ extensions.registerSchemaAPI("idle", "ad
         }
         return Promise.resolve("idle");
       },
       setDetectionInterval: function(detectionIntervalInSeconds) {
         setDetectionInterval(extension, context, detectionIntervalInSeconds);
       },
       onStateChanged: new SingletonEventManager(context, "idle.onStateChanged", fire => {
         let listener = (event, data) => {
-          fire.sync(data);
+          context.runSafe(fire, data);
         };
 
         getObserver(extension, context).on("stateChanged", listener);
         return () => {
           getObserver(extension, context).off("stateChanged", listener);
         };
       }).api(),
     },
--- a/toolkit/components/extensions/ext-notifications.js
+++ b/toolkit/components/extensions/ext-notifications.js
@@ -3,17 +3,17 @@
 var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
                                   "resource://devtools/shared/event-emitter.js");
 
 var {
-  SingletonEventManager,
+  EventManager,
   ignoreEvent,
 } = ExtensionUtils;
 
 // WeakMap[Extension -> Map[id -> Notification]]
 var notificationsMap = new WeakMap();
 
 // Manages a notification popup (notifications API) created by the extension.
 function Notification(extension, id, options) {
@@ -126,31 +126,31 @@ extensions.registerSchemaAPI("notificati
       getAll: function() {
         let result = {};
         notificationsMap.get(extension).forEach((value, key) => {
           result[key] = value.options;
         });
         return Promise.resolve(result);
       },
 
-      onClosed: new SingletonEventManager(context, "notifications.onClosed", fire => {
+      onClosed: new EventManager(context, "notifications.onClosed", fire => {
         let listener = (event, notificationId) => {
           // FIXME: Support the byUser argument.
-          fire.async(notificationId, true);
+          fire(notificationId, true);
         };
 
         notificationsMap.get(extension).on("closed", listener);
         return () => {
           notificationsMap.get(extension).off("closed", listener);
         };
       }).api(),
 
-      onClicked: new SingletonEventManager(context, "notifications.onClicked", fire => {
+      onClicked: new EventManager(context, "notifications.onClicked", fire => {
         let listener = (event, notificationId) => {
-          fire.async(notificationId, true);
+          fire(notificationId, true);
         };
 
         notificationsMap.get(extension).on("clicked", listener);
         return () => {
           notificationsMap.get(extension).off("clicked", listener);
         };
       }).api(),
 
--- a/toolkit/components/extensions/ext-runtime.js
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -23,55 +23,55 @@ extensions.registerSchemaAPI("runtime", 
     runtime: {
       onStartup: new SingletonEventManager(context, "runtime.onStartup", fire => {
         if (context.incognito) {
           // This event should not fire if we are operating in a private profile.
           return () => {};
         }
         let listener = () => {
           if (extension.startupReason === "APP_STARTUP") {
-            fire.async();
+            fire();
           }
         };
         extension.on("startup", listener);
         return () => {
           extension.off("startup", listener);
         };
       }).api(),
 
       onInstalled: new SingletonEventManager(context, "runtime.onInstalled", fire => {
         let listener = () => {
           switch (extension.startupReason) {
             case "APP_STARTUP":
               if (Extension.browserUpdated) {
-                fire.async({reason: "browser_update"});
+                fire({reason: "browser_update"});
               }
               break;
             case "ADDON_INSTALL":
-              fire.async({reason: "install"});
+              fire({reason: "install"});
               break;
             case "ADDON_UPGRADE":
-              fire.async({reason: "update"});
+              fire({reason: "update"});
               break;
           }
         };
         extension.on("startup", listener);
         return () => {
           extension.off("startup", listener);
         };
       }).api(),
 
       onUpdateAvailable: new SingletonEventManager(context, "runtime.onUpdateAvailable", fire => {
         let instanceID = extension.addonData.instanceID;
         AddonManager.addUpgradeListener(instanceID, upgrade => {
           extension.upgrade = upgrade;
           let details = {
             version: upgrade.version,
           };
-          fire.async(details);
+          context.runSafe(fire, details);
         });
         return () => {
           AddonManager.removeUpgradeListener(instanceID);
         };
       }).api(),
 
       reload: () => {
         if (extension.upgrade) {
--- a/toolkit/components/extensions/ext-storage.js
+++ b/toolkit/components/extensions/ext-storage.js
@@ -6,18 +6,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/ExtensionStorage.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorageSync",
                                   "resource://gre/modules/ExtensionStorageSync.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
                                   "resource://gre/modules/AddonManager.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
+  EventManager,
   ExtensionError,
-  SingletonEventManager,
 } = ExtensionUtils;
 
 function enforceNoTemporaryAddon(extensionId) {
   const EXCEPTION_MESSAGE =
         "The storage API will not work with a temporary addon ID. " +
         "Please add an explicit addon ID to your manifest. " +
         "For more information see https://bugzil.la/1323228.";
   if (AddonManagerPrivate.isTemporaryInstallID(extensionId)) {
@@ -58,22 +58,22 @@ function storageApiFactory(context) {
           return ExtensionStorageSync.remove(extension, keys, context);
         },
         clear: function() {
           enforceNoTemporaryAddon(extension.id);
           return ExtensionStorageSync.clear(extension, context);
         },
       },
 
-      onChanged: new SingletonEventManager(context, "storage.onChanged", fire => {
+      onChanged: new EventManager(context, "storage.onChanged", fire => {
         let listenerLocal = changes => {
-          fire.async(changes, "local");
+          fire(changes, "local");
         };
         let listenerSync = changes => {
-          fire.async(changes, "sync");
+          fire(changes, "sync");
         };
 
         ExtensionStorage.addOnChangedListener(extension.id, listenerLocal);
         ExtensionStorageSync.addOnChangedListener(extension, listenerSync, context);
         return () => {
           ExtensionStorage.removeOnChangedListener(extension.id, listenerLocal);
           ExtensionStorageSync.removeOnChangedListener(extension, listenerSync);
         };
--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -93,17 +93,17 @@ function fillTransitionProperties(eventN
     dst.transitionType = transitionType;
     dst.transitionQualifiers = transitionQualifiers;
   }
 }
 
 // Similar to WebRequestEventManager but for WebNavigation.
 function WebNavigationEventManager(context, eventName) {
   let name = `webNavigation.${eventName}`;
-  let register = (fire, urlFilters) => {
+  let register = (callback, urlFilters) => {
     // Don't create a MatchURLFilters instance if the listener does not include any filter.
     let filters = urlFilters ?
           new MatchURLFilters(urlFilters.url) : null;
 
     let listener = data => {
       if (!data.browser) {
         return;
       }
@@ -122,17 +122,17 @@ function WebNavigationEventManager(conte
       // Fills in tabId typically.
       extensions.emit("fill-browser-data", data.browser, data2);
       if (data2.tabId < 0) {
         return;
       }
 
       fillTransitionProperties(eventName, data, data2);
 
-      fire.async(data2);
+      context.runSafe(callback, data2);
     };
 
     WebNavigation[eventName].addListener(listener, filters);
     return () => {
       WebNavigation[eventName].removeListener(listener);
     };
   };
 
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -15,17 +15,17 @@ var {
   SingletonEventManager,
 } = ExtensionUtils;
 
 // EventManager-like class specifically for WebRequest. Inherits from
 // SingletonEventManager. Takes care of converting |details| parameter
 // when invoking listeners.
 function WebRequestEventManager(context, eventName) {
   let name = `webRequest.${eventName}`;
-  let register = (fire, filter, info) => {
+  let register = (callback, filter, info) => {
     let listener = data => {
       // Prevent listening in on requests originating from system principal to
       // prevent tinkering with OCSP, app and addon updates, etc.
       if (data.isSystemPrincipal) {
         return;
       }
       let browserData = {};
       extensions.emit("fill-browser-data", data.browser, browserData);
@@ -60,17 +60,17 @@ function WebRequestEventManager(context,
       let optional = ["requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl",
                       "requestBody"];
       for (let opt of optional) {
         if (opt in data) {
           data2[opt] = data[opt];
         }
       }
 
-      return fire.sync(data2);
+      return context.runSafe(callback, data2);
     };
 
     let filter2 = {};
     filter2.urls = new MatchPattern(filter.urls);
     if (filter.types) {
       filter2.types = filter.types;
     }
     if (filter.tabId) {
--- a/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js
@@ -47,23 +47,20 @@ add_task(function* test_alarm_fires() {
     background: `(${backgroundScript})()`,
     manifest: {
       permissions: ["alarms"],
     },
   });
 
   yield extension.startup();
   yield extension.awaitFinish("alarm-fires");
-
-  // Defer unloading the extension so the asynchronous event listener
-  // reply finishes.
-  yield new Promise(resolve => setTimeout(resolve, 0));
   yield extension.unload();
 });
 
+
 add_task(function* test_alarm_fires_with_when() {
   function backgroundScript() {
     let ALARM_NAME = "test_ext_alarms";
     let timer;
 
     browser.alarms.onAlarm.addListener(alarm => {
       browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the expected name");
       clearTimeout(timer);
@@ -84,23 +81,20 @@ add_task(function* test_alarm_fires_with
     background: `(${backgroundScript})()`,
     manifest: {
       permissions: ["alarms"],
     },
   });
 
   yield extension.startup();
   yield extension.awaitFinish("alarm-when");
-
-  // Defer unloading the extension so the asynchronous event listener
-  // reply finishes.
-  yield new Promise(resolve => setTimeout(resolve, 0));
   yield extension.unload();
 });
 
+
 add_task(function* test_alarm_clear_non_matching_name() {
   async function backgroundScript() {
     let ALARM_NAME = "test_ext_alarms";
 
     browser.alarms.create(ALARM_NAME, {when: Date.now() + 2000});
 
     let wasCleared = await browser.alarms.clear(ALARM_NAME + "1");
     browser.test.assertFalse(wasCleared, "alarm was not cleared");
@@ -117,16 +111,17 @@ add_task(function* test_alarm_clear_non_
     },
   });
 
   yield extension.startup();
   yield extension.awaitFinish("alarm-clear");
   yield extension.unload();
 });
 
+
 add_task(function* test_alarm_get_and_clear_single_argument() {
   async function backgroundScript() {
     browser.alarms.create({when: Date.now() + 2000});
 
     let alarm = await browser.alarms.get();
     browser.test.assertEq("", alarm.name, "expected alarm returned");
 
     let wasCleared = await browser.alarms.clear();
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
@@ -7,16 +7,17 @@ Cu.import("resource://gre/modules/Timer.
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   BaseContext,
 } = ExtensionCommon;
 
 var {
+  EventManager,
   SingletonEventManager,
 } = ExtensionUtils;
 
 class StubContext extends BaseContext {
   constructor() {
     let fakeExtension = {id: "test@web.extension"};
     super("testEnv", fakeExtension);
     this.sandbox = Cu.Sandbox(global);
@@ -59,54 +60,70 @@ add_task(function* test_post_unload_prom
   // any micro-tasks that get enqueued by the resolution handlers above.
   yield new Promise(resolve => setTimeout(resolve, 0));
 });
 
 
 add_task(function* test_post_unload_listeners() {
   let context = new StubContext();
 
+  let fireEvent;
+  let onEvent = new EventManager(context, "onEvent", fire => {
+    fireEvent = fire;
+    return () => {};
+  });
+
   let fireSingleton;
-  let onSingleton = new SingletonEventManager(context, "onSingleton", fire => {
+  let onSingleton = new SingletonEventManager(context, "onSingleton", callback => {
     fireSingleton = () => {
-      fire.async();
+      Promise.resolve().then(callback);
     };
     return () => {};
   });
 
   let fail = event => {
     ok(false, `Unexpected event: ${event}`);
   };
 
-  // Check that event listeners isn't called after it has been removed.
+  // Check that event listeners aren't called after they've been removed.
+  onEvent.addListener(fail);
   onSingleton.addListener(fail);
 
-  let promise = new Promise(resolve => onSingleton.addListener(resolve));
+  let promises = [
+    new Promise(resolve => onEvent.addListener(resolve)),
+    new Promise(resolve => onSingleton.addListener(resolve)),
+  ];
 
+  fireEvent("onEvent");
   fireSingleton("onSingleton");
 
-  // The `fireSingleton` call ia dispatched asynchronously, so it won't
-  // have fired by this point. The `fail` listener that we remove now
-  // should not be called, even though the event has already been
+  // Both `fireEvent` calls are dispatched asynchronously, so they won't
+  // have fired by this point. The `fail` listeners that we remove now
+  // should not be called, even though the events have already been
   // enqueued.
+  onEvent.removeListener(fail);
   onSingleton.removeListener(fail);
 
-  // Wait for the remaining listener to be called, which should always
-  // happen after the `fail` listener would normally be called.
-  yield promise;
+  // Wait for the remaining listeners to be called, which should always
+  // happen after the `fail` listeners would normally be called.
+  yield Promise.all(promises);
 
-  // Check that the event listener isn't called after the context has
+  // Check that event listeners aren't called after the context has
   // unloaded.
+  onEvent.addListener(fail);
   onSingleton.addListener(fail);
 
-  // The `fire` callback always dispatches events
+  // The EventManager `fire` callback always dispatches events
   // asynchronously, so we need to test that any pending event callbacks
   // aren't fired after the context unloads. We also need to test that
   // any `fire` calls that happen *after* the context is unloaded also
   // do not trigger callbacks.
+  fireEvent("onEvent");
+  Promise.resolve("onEvent").then(fireEvent);
+
   fireSingleton("onSingleton");
   Promise.resolve("onSingleton").then(fireSingleton);
 
   context.unload();
 
   // The `setTimeout` ensures that we return to the event loop after
   // promise resolution, which means we're guaranteed to return after
   // any micro-tasks that get enqueued by the resolution handlers above.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_idle.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_idle.js
@@ -140,19 +140,16 @@ add_task(function* testSetDetectionInter
   });
 
   idleService._reset();
   yield extension.startup();
   yield extension.awaitMessage("listenerAdded");
   idleService._fireObservers("idle");
   yield extension.awaitMessage("listenerFired");
   checkActivity({expectedAdd: [99], expectedRemove: [], expectedFires: ["idle"]});
-  // Defer unloading the extension so the asynchronous event listener
-  // reply finishes.
-  yield new Promise(resolve => setTimeout(resolve, 0));
   yield extension.unload();
 });
 
 add_task(function* testSetDetectionIntervalAfterAddingListener() {
   function background() {
     browser.idle.onStateChanged.addListener(newState => {
       browser.test.assertEq("idle", newState, "listener fired with the expected state");
       browser.test.sendMessage("listenerFired");
@@ -169,20 +166,16 @@ add_task(function* testSetDetectionInter
   });
 
   idleService._reset();
   yield extension.startup();
   yield extension.awaitMessage("detectionIntervalSet");
   idleService._fireObservers("idle");
   yield extension.awaitMessage("listenerFired");
   checkActivity({expectedAdd: [60, 99], expectedRemove: [60], expectedFires: ["idle"]});
-
-  // Defer unloading the extension so the asynchronous event listener
-  // reply finishes.
-  yield new Promise(resolve => setTimeout(resolve, 0));
   yield extension.unload();
 });
 
 add_task(function* testOnlyAddingListener() {
   function background() {
     browser.idle.onStateChanged.addListener(newState => {
       browser.test.assertEq("active", newState, "listener fired with the expected state");
       browser.test.sendMessage("listenerFired");
@@ -200,14 +193,10 @@ add_task(function* testOnlyAddingListene
   idleService._reset();
   yield extension.startup();
   yield extension.awaitMessage("listenerAdded");
   idleService._fireObservers("active");
   yield extension.awaitMessage("listenerFired");
   // check that "idle-daily" topic does not cause a listener to fire
   idleService._fireObservers("idle-daily");
   checkActivity({expectedAdd: [60], expectedRemove: [], expectedFires: ["active", "idle-daily"]});
-
-  // Defer unloading the extension so the asynchronous event listener
-  // reply finishes.
-  yield new Promise(resolve => setTimeout(resolve, 0));
   yield extension.unload();
 });