Bug 1479524 - Always use message manager with NetworkMonitorActor. r=jdescottes draft
authorAlexandre Poirot <poirot.alex@gmail.com>
Fri, 27 Jul 2018 02:05:14 -0700
changeset 828962 5d6e043af66874e2dc4ba9626f9daf357e958078
parent 828961 a62c083163d60a523e2d3cae38c6b1a8adac0817
child 828963 720e2d5dc1a63776c2890d65909c72d1b81004f2
push id118734
push userbmo:poirot.alex@gmail.com
push dateTue, 14 Aug 2018 14:53:41 +0000
reviewersjdescottes
bugs1479524
milestone63.0a1
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":
@@ -1246,41 +1246,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.
 
@@ -1812,36 +1800,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.
    */
@@ -1877,40 +1869,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;