Bug 1479524 - Always use message manager with NetworkMonitorActor. r=jdescottes
authorAlexandre Poirot <poirot.alex@gmail.com>
Fri, 27 Jul 2018 02:05:14 -0700
changeset 487073 0f25b95e97408a4dfcb5ff223c130cec304c6d5e
parent 487072 a84948944042e82007e61cb32f6eb768d037edda
child 487074 7f6bb6c2bc018d3a25a877f62e79ed103661ea7b
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes
bugs1479524
milestone63.0a1
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
Bug 1479524 - Always use message manager with NetworkMonitorActor. r=jdescottes MozReview-Commit-ID: AXOd0i4NOjH
devtools/server/actors/network-monitor.js
devtools/server/actors/webconsole.js
devtools/server/actors/webconsole/message-manager-mock.js
devtools/server/actors/webconsole/moz.build
devtools/shared/webconsole/network-monitor.js
--- a/devtools/server/actors/network-monitor.js
+++ b/devtools/server/actors/network-monitor.js
@@ -23,94 +23,90 @@ const NetworkMonitorActor = ActorClassWi
    *
    * @param object filters
    *        Contains an `outerWindowID` attribute when this is used across processes.
    *        Or a `window` attribute when instanciated in the same process.
    * @param number parentID (optional)
    *        To be removed, specify the ID of the Web console actor.
    *        This is used to fake emitting an event from it to prevent changing RDP
    *        behavior.
-   * @param nsIMessageManager messageManager (optional)
-   *        Passed only when it is instanciated across processes. This is the manager to
-   *        use to communicate with the other process.
-   * @param object stackTraceCollector (optional)
-   *        When the actor runs in the same process than the requests we are inspecting,
-   *        the web console actor hands over a shared instance to the stack trace
-   *        collector.
+   * @param nsIMessageManager messageManager
+   *        This is the manager to use to communicate with the console actor. When both
+   *        netmonitor and console actor runs in the same process, this is an instance
+   *        of MockMessageManager instead of a real message manager.
    */
-  initialize(conn, filters, parentID, messageManager, stackTraceCollector) {
+  initialize(conn, filters, parentID, messageManager) {
     Actor.prototype.initialize.call(this, conn);
 
     this.parentID = parentID;
     this.messageManager = messageManager;
-    this.stackTraceCollector = stackTraceCollector;
 
     // Immediately start watching for new request according to `filters`.
     // NetworkMonitor will call `onNetworkEvent` method.
     this.netMonitor = new NetworkMonitor(filters, this);
     this.netMonitor.init();
 
-    if (this.messageManager) {
-      this.stackTraces = new Set();
-      this.onStackTraceAvailable = this.onStackTraceAvailable.bind(this);
-      this.messageManager.addMessageListener("debug:request-stack-available",
-        this.onStackTraceAvailable);
-      this.onRequestContent = this.onRequestContent.bind(this);
-      this.messageManager.addMessageListener("debug:request-content",
-        this.onRequestContent);
-      this.onSetPreference = this.onSetPreference.bind(this);
-      this.messageManager.addMessageListener("debug:netmonitor-preference",
-        this.onSetPreference);
-      this.onGetNetworkEventActor = this.onGetNetworkEventActor.bind(this);
-      this.messageManager.addMessageListener("debug:get-network-event-actor",
-        this.onGetNetworkEventActor);
-      this.destroy = this.destroy.bind(this);
-      this.messageManager.addMessageListener("debug:destroy-network-monitor",
-        this.destroy);
+    this.stackTraces = new Set();
+    this.onStackTraceAvailable = this.onStackTraceAvailable.bind(this);
+    this.messageManager.addMessageListener("debug:request-stack-available",
+      this.onStackTraceAvailable);
+    this.onRequestContent = this.onRequestContent.bind(this);
+    this.messageManager.addMessageListener("debug:request-content",
+      this.onRequestContent);
+    this.onSetPreference = this.onSetPreference.bind(this);
+    this.messageManager.addMessageListener("debug:netmonitor-preference",
+      this.onSetPreference);
+    this.onGetNetworkEventActor = this.onGetNetworkEventActor.bind(this);
+    this.messageManager.addMessageListener("debug:get-network-event-actor",
+      this.onGetNetworkEventActor);
+    this.onDestroyMessage = this.onDestroyMessage.bind(this);
+    this.messageManager.addMessageListener("debug:destroy-network-monitor",
+      this.onDestroyMessage);
+  },
+
+  onDestroyMessage({ data }) {
+    if (data.actorID == this.parentID) {
+      this.destroy();
     }
   },
 
   destroy() {
     Actor.prototype.destroy.call(this);
 
     if (this.netMonitor) {
       this.netMonitor.destroy();
       this.netMonitor = null;
     }
 
+    this.stackTraces.clear();
     if (this.messageManager) {
-      this.stackTraces.clear();
       this.messageManager.removeMessageListener("debug:request-stack-available",
         this.onStackTraceAvailable);
       this.messageManager.removeMessageListener("debug:request-content",
         this.onRequestContent);
       this.messageManager.removeMessageListener("debug:netmonitor-preference",
         this.onSetPreference);
       this.messageManager.removeMessageListener("debug:get-network-event-actor",
         this.onGetNetworkEventActor);
       this.messageManager.removeMessageListener("debug:destroy-network-monitor",
-        this.destroy);
+        this.onDestroyMessage);
       this.messageManager = null;
     }
   },
 
   onStackTraceAvailable(msg) {
     const { channelId } = msg.data;
     if (!msg.data.stacktrace) {
       this.stackTraces.delete(channelId);
     } else {
       this.stackTraces.add(channelId);
     }
   },
 
-  getRequestContentForURL(url) {
-    const actor = this._networkEventActorsByURL.get(url);
-    if (!actor) {
-      return null;
-    }
+  getRequestContentForActor(actor) {
     const content = actor._response.content;
     if (actor._discardResponseBody || actor._truncated || !content || !content.size) {
       // Do not return the stylesheet text if there is no meaningful content or if it's
       // still loading. Let the caller handle it by doing its own separate request.
       return null;
     }
 
     if (content.text.type != "longString") {
@@ -128,17 +124,20 @@ const NetworkMonitorActor = ActorClassWi
     return {
       content: longStringActor.str,
       contentType: content.mimeType,
     };
   },
 
   onRequestContent(msg) {
     const { url } = msg.data;
-    const content = this.getRequestContentForURL(url);
+    const actor = this._networkEventActorsByURL.get(url);
+    // Always reply with a message, but with a null `content` if this instance
+    // did not processed this request
+    const content = actor ? this.getRequestContentForActor(actor) : null;
     this.messageManager.sendAsyncMessage("debug:request-content", {
       url,
       content,
     });
   },
 
   onSetPreference({ data }) {
     if ("saveRequestAndResponseBodies" in data) {
@@ -146,17 +145,20 @@ const NetworkMonitorActor = ActorClassWi
     }
     if ("throttleData" in data) {
       this.netMonitor.throttleData = data.throttleData;
     }
   },
 
   onGetNetworkEventActor({ data }) {
     const actor = this.getNetworkEventActor(data.channelId);
-    this.messageManager.sendAsyncMessage("debug:get-network-event-actor", actor.form());
+    this.messageManager.sendAsyncMessage("debug:get-network-event-actor", {
+      channelId: data.channelId,
+      actor: actor.form()
+    });
   },
 
   getNetworkEventActor(channelId) {
     let actor = this._netEvents.get(channelId);
     if (actor) {
       return actor;
     }
 
@@ -170,23 +172,19 @@ const NetworkMonitorActor = ActorClassWi
 
   // This method is called by NetworkMonitor instance when a new request is fired
   onNetworkEvent(event) {
     const { channelId } = event;
 
     const actor = this.getNetworkEventActor(channelId);
     this._netEvents.set(channelId, actor);
 
-    if (this.messageManager) {
-      event.cause.stacktrace = this.stackTraces.has(channelId);
-      if (event.cause.stacktrace) {
-        this.stackTraces.delete(channelId);
-      }
-    } else {
-      event.cause.stacktrace = this.stackTraceCollector.getStackTrace(channelId);
+    event.cause.stacktrace = this.stackTraces.has(channelId);
+    if (event.cause.stacktrace) {
+      this.stackTraces.delete(channelId);
     }
     actor.init(event);
 
     this._networkEventActorsByURL.set(actor._request.url, actor);
 
     const packet = {
       from: this.parentID,
       type: "networkEvent",
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -25,16 +25,17 @@ loader.lazyRequireGetter(this, "StackTra
 loader.lazyRequireGetter(this, "JSPropertyProvider", "devtools/shared/webconsole/js-property-provider", true);
 loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true);
 loader.lazyRequireGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm", true);
 loader.lazyRequireGetter(this, "WebConsoleCommands", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "addWebConsoleCommands", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "formatCommand", "devtools/server/actors/webconsole/commands", true);
 loader.lazyRequireGetter(this, "isCommand", "devtools/server/actors/webconsole/commands", true);
 loader.lazyRequireGetter(this, "validCommands", "devtools/server/actors/webconsole/commands", true);
+loader.lazyRequireGetter(this, "createMessageManagerMocks", "devtools/server/actors/webconsole/message-manager-mock", true);
 loader.lazyRequireGetter(this, "CONSOLE_WORKER_IDS", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "WebConsoleUtils", "devtools/server/actors/webconsole/utils", true);
 loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true);
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 
 // Overwrite implemented listeners for workers so that we don't attempt
 // to load an unsupported module.
 if (isWorker) {
@@ -320,32 +321,23 @@ WebConsoleActor.prototype =
   /**
    * Destroy the current WebConsoleActor instance.
    */
   destroy() {
     if (this.consoleServiceListener) {
       this.consoleServiceListener.destroy();
       this.consoleServiceListener = null;
     }
-    if (this.networkMonitorActor) {
-      this.networkMonitorActor.destroy();
-      this.networkMonitorActor = null;
-    }
-    if (this.networkMonitorActorId) {
-      const messageManager = this.parentActor.messageManager;
-      if (messageManager) {
+    if (this.netmonitors) {
+      for (const { messageManager } of this.netmonitors) {
         messageManager.sendAsyncMessage("debug:destroy-network-monitor", {
-          actorId: this.networkMonitorActorId
+          actorID: this.actorID
         });
       }
-      this.networkMonitorActorId = null;
-    }
-    if (this.networkMonitorChildActor) {
-      this.networkMonitorChildActor.destroy();
-      this.networkMonitorChildActor = null;
+      this.netmonitors = null;
     }
     if (this.consoleAPIListener) {
       this.consoleAPIListener.destroy();
       this.consoleAPIListener = null;
     }
     if (this.stackTraceCollector) {
       this.stackTraceCollector.destroy();
       this.stackTraceCollector = null;
@@ -584,30 +576,16 @@ WebConsoleActor.prototype =
    * @param object request
    *        The JSON request object received from the Web Console client.
    * @return object
    *         The response object which holds the startedListeners array.
    */
   startListeners: async function(request) {
     const startedListeners = [];
     const window = !this.parentActor.isRootActor ? this.window : null;
-    let messageManager = null;
-
-    // Check if the actor is running in a child process (but only if
-    // Services.appinfo exists, to prevent startListeners to fail
-    // when the target is a Worker).
-    const processBoundary = Services.appinfo && (
-      Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
-    );
-
-    // Retrieve a message manager from the parent actor if this actor is
-    // not currently running in the main process.
-    if (processBoundary) {
-      messageManager = this.parentActor.messageManager;
-    }
 
     while (request.listeners.length > 0) {
       const listener = request.listeners.shift();
       switch (listener) {
         case "PageError":
           // Workers don't support this message type yet
           if (isWorker) {
             break;
@@ -629,48 +607,79 @@ WebConsoleActor.prototype =
           }
           startedListeners.push(listener);
           break;
         case "NetworkActivity":
           // Workers don't support this message type
           if (isWorker) {
             break;
           }
-          if (!this.networkMonitorActorId && !this.networkMonitorActor) {
-            // Create a StackTraceCollector that's going to be shared both by
-            // the NetworkMonitorActor running in the same process for service worker
-            // requests, as well with the NetworkMonitorActor running in the parent
-            // process. It will communicate via message manager for this one.
-            this.stackTraceCollector = new StackTraceCollector({ window },
-              messageManager);
-            this.stackTraceCollector.init();
+          if (!this.netmonitors) {
+            // Instanciate fake message managers used for service worker's netmonitor
+            // when running in the content process, and for netmonitor running in the
+            // same process when running in the parent process.
+            // `createMessageManagerMocks` returns a couple of connected messages
+            // managers that pass messages to each other to simulate the process
+            // boundary. We will use the first one for the webconsole-actor and the
+            // second one will be used by the netmonitor-actor.
+            const [ mmMockParent, mmMockChild ] = createMessageManagerMocks();
 
-            if (messageManager && processBoundary) {
+            // Maintain the list of message manager we should message to/listen from
+            // to support the netmonitor instances, also records actorID of each
+            // NetworkMonitorActor.
+            // Array of `{ messageManager, parentProcess }`.
+            // Where `parentProcess` is true for the netmonitor actor instanciated in the
+            // parent process.
+            this.netmonitors = [];
+
+            // Check if the actor is running in a content process
+            const isInContentProcess =
+              Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT &&
+              this.parentActor.messageManager;
+            if (isInContentProcess) {
               // Start a network monitor in the parent process to listen to
-              // most requests than happen in parent
-              this.networkMonitorActorId = await this.conn.spawnActorInParentProcess(
+              // most requests that happen in parent. This one will communicate through
+              // `messageManager`.
+              await this.conn.spawnActorInParentProcess(
                 this.actorID, {
                   module: "devtools/server/actors/network-monitor",
                   constructor: "NetworkMonitorActor",
                   args: [
                     { outerWindowID: this.parentActor.outerWindowID },
                     this.actorID
                   ],
                 });
+              this.netmonitors.push({
+                messageManager: this.parentActor.messageManager,
+                parentProcess: true
+              });
+            }
 
-              // Spawn also one in the child to listen to service workers
-              this.networkMonitorChildActor = new NetworkMonitorActor(this.conn,
-                { window },
-                this.actorID,
-                null,
-                this.stackTraceCollector);
-            } else {
-              this.networkMonitorActor = new NetworkMonitorActor(this.conn, { window },
-                this.actorID, null, this.stackTraceCollector);
-            }
+            // When the console actor runs in the parent process, Netmonitor can be ran
+            // in the process and communicate through `messageManagerMock`.
+            // And while it runs in the content process, we also spawn one in the content
+            // to listen to requests that happen in the content process (for instance
+            // service workers requests)
+            new NetworkMonitorActor(this.conn,
+              { window },
+              this.actorID,
+              mmMockParent);
+
+            this.netmonitors.push({
+              messageManager: mmMockChild,
+              parentProcess: !isInContentProcess
+            });
+
+            // Create a StackTraceCollector that's going to be shared both by
+            // the NetworkMonitorActor running in the same process for service worker
+            // requests, as well with the NetworkMonitorActor running in the parent
+            // process. It will communicate via message manager for this one.
+            this.stackTraceCollector = new StackTraceCollector({ window },
+              this.netmonitors);
+            this.stackTraceCollector.init();
           }
           startedListeners.push(listener);
           break;
         case "FileActivity":
           // Workers don't support this message type
           if (isWorker) {
             break;
           }
@@ -759,32 +768,23 @@ WebConsoleActor.prototype =
         case "ConsoleAPI":
           if (this.consoleAPIListener) {
             this.consoleAPIListener.destroy();
             this.consoleAPIListener = null;
           }
           stoppedListeners.push(listener);
           break;
         case "NetworkActivity":
-          if (this.networkMonitorActor) {
-            this.networkMonitorActor.destroy();
-            this.networkMonitorActor = null;
-          }
-          if (this.networkMonitorActorId) {
-            const messageManager = this.parentActor.messageManager;
-            if (messageManager) {
+          if (this.netmonitors) {
+            for (const { messageManager } of this.netmonitors) {
               messageManager.sendAsyncMessage("debug:destroy-network-monitor", {
-                actorId: this.networkMonitorActorId
+                actorID: this.actorID
               });
             }
-            this.networkMonitorActorId = null;
-          }
-          if (this.networkMonitorChildActor) {
-            this.networkMonitorChildActor.destroy();
-            this.networkMonitorChildActor = null;
+            this.netmonitors = null;
           }
           if (this.stackTraceCollector) {
             this.stackTraceCollector.destroy();
             this.stackTraceCollector = null;
           }
           stoppedListeners.push(listener);
           break;
         case "FileActivity":
@@ -1247,41 +1247,29 @@ WebConsoleActor.prototype =
    *
    * @param object request
    *        The request message - which preferences need to be updated.
    */
   setPreferences: function(request) {
     for (const key in request.preferences) {
       this._prefs[key] = request.preferences[key];
 
-      if (key == "NetworkMonitor.saveRequestAndResponseBodies") {
-        if (this.networkMonitorActor) {
-          this.networkMonitorActor.netMonitor.saveRequestAndResponseBodies =
-            this._prefs[key];
-        }
-        if (this.networkMonitorChildActor) {
-          this.networkMonitorChildActor.netMonitor.saveRequestAndResponseBodies =
-            this._prefs[key];
-        }
-        if (this.networkMonitorActorId) {
-          const messageManager = this.parentActor.messageManager;
-          messageManager.sendAsyncMessage("debug:netmonitor-preference",
-            { saveRequestAndResponseBodies: this._prefs[key] });
-        }
-      } else if (key == "NetworkMonitor.throttleData") {
-        if (this.networkMonitorActor) {
-          this.networkMonitorActor.netMonitor.throttleData = this._prefs[key];
-        }
-        if (this.networkMonitorChildActor) {
-          this.networkMonitorChildActor.netMonitor.throttleData = this._prefs[key];
-        }
-        if (this.networkMonitorActorId) {
-          const messageManager = this.parentActor.messageManager;
-          messageManager.sendAsyncMessage("debug:netmonitor-preference",
-            { throttleData: this._prefs[key] });
+      if (this.netmonitors) {
+        if (key == "NetworkMonitor.saveRequestAndResponseBodies") {
+          for (const { messageManager } of this.netmonitors) {
+            messageManager.sendAsyncMessage("debug:netmonitor-preference", {
+              saveRequestAndResponseBodies: this._prefs[key]
+            });
+          }
+        } else if (key == "NetworkMonitor.throttleData") {
+          for (const { messageManager } of this.netmonitors) {
+            messageManager.sendAsyncMessage("debug:netmonitor-preference", {
+              throttleData: this._prefs[key]
+            });
+          }
         }
       }
     }
     return { updated: Object.keys(request.preferences) };
   },
 
   // End of request handlers.
 
@@ -1813,36 +1801,40 @@ WebConsoleActor.prototype =
    * Get the NetworkEventActor for a given URL that may have been noticed by the network
    * listener.  Requests are added when they start, so the actor might not yet have all
    * data for the request until it has completed.
    *
    * @param string url
    *        The URL of the request to search for.
    */
   getRequestContentForURL(url) {
-    // When running in Parent Process, call the NetworkMonitorActor directly.
-    if (this.networkMonitorActor) {
-      return this.networkMonitorActor.getRequestContentForURL(url);
-    } else if (this.networkMonitorActorId) {
-      // Otherwise, if the netmonitor is started, but on the parent process,
-      // pipe the data through the message manager
-      const messageManager = this.parentActor.messageManager;
-      return new Promise(resolve => {
-        const onMessage = ({ data }) => {
-          if (data.url == url) {
+    if (!this.netmonitors) {
+      return null;
+    }
+    return new Promise(resolve => {
+      let messagesReceived = 0;
+      const onMessage = ({ data }) => {
+        if (data.url != url) {
+          return;
+        }
+        messagesReceived++;
+        // Either use the first response with a content, or return a null content
+        // if we received the responses from all the message managers.
+        if (data.content || messagesReceived == this.netmonitors.length) {
+          for (const { messageManager } of this.netmonitors) {
             messageManager.removeMessageListener("debug:request-content", onMessage);
-            resolve(data.content);
           }
-        };
+          resolve(data.content);
+        }
+      };
+      for (const { messageManager } of this.netmonitors) {
         messageManager.addMessageListener("debug:request-content", onMessage);
         messageManager.sendAsyncMessage("debug:request-content", { url });
-      });
-    }
-    // Finally, if the netmonitor is not started at all, return null
-    return null;
+      }
+    });
   },
 
   /**
    * Send a new HTTP request from the target's window.
    *
    * @param object message
    *        Object with 'request' - the HTTP request details.
    */
@@ -1878,40 +1870,38 @@ WebConsoleActor.prototype =
       const bodyStream = Cc["@mozilla.org/io/string-input-stream;1"]
         .createInstance(Ci.nsIStringInputStream);
       bodyStream.setData(body, body.length);
       channel.explicitSetUploadStream(bodyStream, null, -1, method, false);
     }
 
     NetUtil.asyncFetch(channel, () => {});
 
-    // When running in Parent Process, call the NetworkMonitorActor directly.
+    if (!this.netmonitors) {
+      return null;
+    }
     const { channelId } = channel;
-    if (this.networkMonitorActor) {
-      const actor = this.networkMonitorActor.getNetworkEventActor(channelId);
-      return {
-        eventActor: actor.form()
-      };
-    } else if (this.networkMonitorActorId) {
-      // Otherwise, if the netmonitor is started, but on the parent process,
-      // pipe the data through the message manager
-      const messageManager = this.parentActor.messageManager;
-      return new Promise(resolve => {
-        const onMessage = ({ data }) => {
+    // Only query the NetworkMonitorActor running in the parent process, where the
+    // request will be done. There always is one listener running in the parent process,
+    // see startListeners.
+    const netmonitor = this.netmonitors.filter(({ parentProcess }) => parentProcess)[0];
+    const { messageManager } = netmonitor;
+    return new Promise(resolve => {
+      const onMessage = ({ data }) => {
+        if (data.channelId == channelId) {
           messageManager.removeMessageListener("debug:get-network-event-actor",
             onMessage);
           resolve({
-            eventActor: data
+            eventActor: data.actor
           });
-        };
-        messageManager.addMessageListener("debug:get-network-event-actor", onMessage);
-        messageManager.sendAsyncMessage("debug:get-network-event-actor", { channelId });
-      });
-    }
-    return null;
+        }
+      };
+      messageManager.addMessageListener("debug:get-network-event-actor", onMessage);
+      messageManager.sendAsyncMessage("debug:get-network-event-actor", { channelId });
+    });
   },
 
   /**
    * Handler for file activity. This method sends the file request information
    * to the remote Web Console client.
    *
    * @see ConsoleProgressListener
    * @param string fileURI
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/webconsole/message-manager-mock.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Implements a fake MessageManager class that allows to use the message
+ * manager API within the same process. This implementation will forward
+ * messages within the same process.
+ *
+ * It helps having the same codepath for actors being evaluated in the same
+ * process *and* in a remote one.
+ */
+function MessageManagerMock() {
+  this._listeners = new Map();
+}
+MessageManagerMock.prototype = {
+  addMessageListener(name, listener) {
+    let listeners = this._listeners.get(name);
+    if (!listeners) {
+      listeners = [];
+      this._listeners.set(name, listeners);
+    }
+    if (!listeners.includes(listener)) {
+      listeners.push(listener);
+    }
+  },
+  removeMessageListener(name, listener) {
+    const listeners = this._listeners.get(name);
+    const idx = listeners.indexOf(listener);
+    listeners.splice(idx, 1);
+  },
+  sendAsyncMessage(name, data) {
+    this.other.internalSendAsyncMessage(name, data);
+  },
+  internalSendAsyncMessage(name, data) {
+    const listeners = this._listeners.get(name);
+    if (!listeners) {
+      return;
+    }
+    const message = {
+      target: this,
+      data
+    };
+    for (const listener of listeners) {
+      if (typeof listener === "object" &&
+          typeof listener.receiveMessage === "function") {
+        listener.receiveMessage(message);
+      } else if (typeof listener === "function") {
+        listener(message);
+      }
+    }
+  },
+};
+
+/**
+ * Create two MessageManager mocks, connected to each others.
+ * Calling sendAsyncMessage on the first will dispatch messages on the second one,
+ * and the other way around
+ */
+exports.createMessageManagerMocks = function() {
+  const a = new MessageManagerMock();
+  const b = new MessageManagerMock();
+  a.other = b;
+  b.other = a;
+  return [a, b];
+};
--- a/devtools/server/actors/webconsole/moz.build
+++ b/devtools/server/actors/webconsole/moz.build
@@ -3,12 +3,13 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'commands.js',
     'content-process-forward.js',
     'listeners.js',
+    'message-manager-mock.js',
     'screenshot.js',
     'utils.js',
     'worker-listeners.js',
 )
--- a/devtools/shared/webconsole/network-monitor.js
+++ b/devtools/shared/webconsole/network-monitor.js
@@ -172,43 +172,43 @@ ChannelEventSinkFactory.unregister = fun
 
 ChannelEventSinkFactory.getService = function() {
   // Make sure the ChannelEventSink service is registered before accessing it
   ChannelEventSinkFactory.register();
 
   return Cc[SINK_CONTRACT_ID].getService(Ci.nsIChannelEventSink).wrappedJSObject;
 };
 
-function StackTraceCollector(filters, messageManager) {
+function StackTraceCollector(filters, netmonitors) {
   this.filters = filters;
   this.stacktracesById = new Map();
-  this.messageManager = messageManager;
+  this.netmonitors = netmonitors;
 }
 
 StackTraceCollector.prototype = {
   init() {
     Services.obs.addObserver(this, "http-on-opening-request");
     ChannelEventSinkFactory.getService().registerCollector(this);
-    if (this.messageManager) {
-      this.onGetStack = this.onGetStack.bind(this);
-      this.messageManager.addMessageListener("debug:request-stack", this.onGetStack);
+    this.onGetStack = this.onGetStack.bind(this);
+    for (const { messageManager } of this.netmonitors) {
+      messageManager.addMessageListener("debug:request-stack", this.onGetStack);
     }
   },
 
   destroy() {
     Services.obs.removeObserver(this, "http-on-opening-request");
     ChannelEventSinkFactory.getService().unregisterCollector(this);
-    if (this.messageManager) {
-      this.messageManager.removeMessageListener("debug:request-stack", this.onGetStack);
+    for (const { messageManager } of this.netmonitors) {
+      messageManager.removeMessageListener("debug:request-stack", this.onGetStack);
     }
   },
 
   _saveStackTrace(channel, stacktrace) {
-    if (this.messageManager) {
-      this.messageManager.sendAsyncMessage("debug:request-stack-available", {
+    for (const { messageManager } of this.netmonitors) {
+      messageManager.sendAsyncMessage("debug:request-stack-available", {
         channelId: channel.channelId,
         stacktrace: stacktrace && stacktrace.length > 0
       });
     }
     this.stacktracesById.set(channel.channelId, stacktrace);
   },
 
   observe(subject) {
@@ -258,19 +258,20 @@ StackTraceCollector.prototype = {
 
   getStackTrace(channelId) {
     const trace = this.stacktracesById.get(channelId);
     this.stacktracesById.delete(channelId);
     return trace;
   },
 
   onGetStack(msg) {
+    const messageManager = msg.target;
     const channelId = msg.data;
     const stack = this.getStackTrace(channelId);
-    this.messageManager.sendAsyncMessage("debug:request-stack", {
+    messageManager.sendAsyncMessage("debug:request-stack", {
       channelId,
       stack,
     });
   },
 };
 
 exports.StackTraceCollector = StackTraceCollector;