Bug 1240912 - Filter requests by top frame's outerWindowID. r=ochameau draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Thu, 04 Aug 2016 17:42:27 -0500
changeset 402162 7f4043a6226d5ba95b51b96ae487297fe446a699
parent 398324 814af2431bb94ed2a9c4ac267233a39fce145a0b
child 402163 193b55a8beda265514bde4f4530e8f4daf13fc2f
push id26614
push userbmo:jryans@gmail.com
push dateThu, 18 Aug 2016 01:07:09 +0000
reviewersochameau
bugs1240912
milestone51.0a1
Bug 1240912 - Filter requests by top frame's outerWindowID. r=ochameau MozReview-Commit-ID: B5Y65EVG8Tz
devtools/client/netmonitor/test/browser.ini
devtools/client/netmonitor/test/browser_net_frame.js
devtools/client/netmonitor/test/head.js
devtools/client/netmonitor/test/html_frame-subdocument.html
devtools/client/netmonitor/test/html_frame-test-page.html
devtools/server/actors/webbrowser.js
devtools/server/actors/webconsole.js
devtools/shared/webconsole/network-monitor.js
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -4,31 +4,33 @@ subsuite = devtools
 support-files =
   dropmarker.svg
   head.js
   html_cause-test-page.html
   html_content-type-test-page.html
   html_content-type-without-cache-test-page.html
   html_cors-test-page.html
   html_custom-get-page.html
-  html_single-get-page.html
   html_cyrillic-test-page.html
+  html_frame-test-page.html
+  html_frame-subdocument.html
   html_filter-test-page.html
   html_infinite-get-page.html
   html_json-custom-mime-test-page.html
   html_json-long-test-page.html
   html_json-malformed-test-page.html
   html_json-text-mime-test-page.html
   html_jsonp-test-page.html
   html_navigate-test-page.html
   html_params-test-page.html
   html_post-data-test-page.html
   html_post-raw-test-page.html
   html_post-raw-with-headers-test-page.html
   html_simple-test-page.html
+  html_single-get-page.html
   html_send-beacon.html
   html_sorting-test-page.html
   html_statistics-test-page.html
   html_status-codes-test-page.html
   html_api-calls-test-page.html
   html_copy-as-curl.html
   html_curl-utils.html
   sjs_content-type-test-server.sjs
@@ -78,16 +80,17 @@ subsuite = clipboard
 [browser_net_copy_as_curl.js]
 subsuite = clipboard
 skip-if = e10s # Bug 1091596
 [browser_net_cors_requests.js]
 [browser_net_cyrillic-01.js]
 [browser_net_cyrillic-02.js]
 [browser_net_details-no-duplicated-content.js]
 skip-if = (os == 'linux' && e10s && debug) # Bug 1242204
+[browser_net_frame.js]
 [browser_net_filter-01.js]
 [browser_net_filter-02.js]
 [browser_net_filter-03.js]
 [browser_net_filter-04.js]
 [browser_net_footer-summary.js]
 [browser_net_html-preview.js]
 [browser_net_icon-preview.js]
 [browser_net_image-tooltip.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_frame.js
@@ -0,0 +1,218 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for all expected requests when an iframe is loading a subdocument.
+ */
+
+const TOP_FILE_NAME = "html_frame-test-page.html";
+const SUB_FILE_NAME = "html_frame-subdocument.html";
+const TOP_URL = EXAMPLE_URL + TOP_FILE_NAME;
+const SUB_URL = EXAMPLE_URL + SUB_FILE_NAME;
+
+const EXPECTED_REQUESTS_TOP = [
+  {
+    method: "GET",
+    url: TOP_URL,
+    causeType: "document",
+    causeUri: "",
+    stack: true
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "stylesheet_request",
+    causeType: "stylesheet",
+    causeUri: TOP_URL,
+    stack: false
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "img_request",
+    causeType: "img",
+    causeUri: TOP_URL,
+    stack: false
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "xhr_request",
+    causeType: "xhr",
+    causeUri: TOP_URL,
+    stack: [{ fn: "performXhrRequest", file: TOP_FILE_NAME, line: 23 }]
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "fetch_request",
+    causeType: "fetch",
+    causeUri: TOP_URL,
+    stack: [{ fn: "performFetchRequest", file: TOP_FILE_NAME, line: 27 }]
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "promise_fetch_request",
+    causeType: "fetch",
+    causeUri: TOP_URL,
+    stack: [
+      { fn: "performPromiseFetchRequest", file: TOP_FILE_NAME, line: 39 },
+      { fn: null, file: TOP_FILE_NAME, line: 38, asyncCause: "promise callback" },
+    ]
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "timeout_fetch_request",
+    causeType: "fetch",
+    causeUri: TOP_URL,
+    stack: [
+      { fn: "performTimeoutFetchRequest", file: TOP_FILE_NAME, line: 41 },
+      { fn: "performPromiseFetchRequest", file: TOP_FILE_NAME, line: 40,
+        asyncCause: "setTimeout handler" },
+    ]
+  },
+  {
+    method: "POST",
+    url: EXAMPLE_URL + "beacon_request",
+    causeType: "beacon",
+    causeUri: TOP_URL,
+    stack: [{ fn: "performBeaconRequest", file: TOP_FILE_NAME, line: 31 }]
+  },
+];
+
+const EXPECTED_REQUESTS_SUB = [
+  {
+    method: "GET",
+    url: SUB_URL,
+    causeType: "subdocument",
+    causeUri: TOP_URL,
+    stack: false
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "stylesheet_request",
+    causeType: "stylesheet",
+    causeUri: SUB_URL,
+    stack: false
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "img_request",
+    causeType: "img",
+    causeUri: SUB_URL,
+    stack: false
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "xhr_request",
+    causeType: "xhr",
+    causeUri: SUB_URL,
+    stack: [{ fn: "performXhrRequest", file: SUB_FILE_NAME, line: 22 }]
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "fetch_request",
+    causeType: "fetch",
+    causeUri: SUB_URL,
+    stack: [{ fn: "performFetchRequest", file: SUB_FILE_NAME, line: 26 }]
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "promise_fetch_request",
+    causeType: "fetch",
+    causeUri: SUB_URL,
+    stack: [
+      { fn: "performPromiseFetchRequest", file: SUB_FILE_NAME, line: 38 },
+      { fn: null, file: SUB_FILE_NAME, line: 37, asyncCause: "promise callback" },
+    ]
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + "timeout_fetch_request",
+    causeType: "fetch",
+    causeUri: SUB_URL,
+    stack: [
+      { fn: "performTimeoutFetchRequest", file: SUB_FILE_NAME, line: 40 },
+      { fn: "performPromiseFetchRequest", file: SUB_FILE_NAME, line: 39,
+        asyncCause: "setTimeout handler" },
+    ]
+  },
+  {
+    method: "POST",
+    url: EXAMPLE_URL + "beacon_request",
+    causeType: "beacon",
+    causeUri: SUB_URL,
+    stack: [{ fn: "performBeaconRequest", file: SUB_FILE_NAME, line: 30 }]
+  },
+];
+
+const REQUEST_COUNT = EXPECTED_REQUESTS_TOP.length + EXPECTED_REQUESTS_SUB.length;
+
+add_task(function* () {
+  // the initNetMonitor function clears the network request list after the
+  // page is loaded. That's why we first load a bogus page from SIMPLE_URL,
+  // and only then load the real thing from TOP_URL - we want to catch
+  // all the requests the page is making, not only the XHRs.
+  // We can't use about:blank here, because initNetMonitor checks that the
+  // page has actually made at least one request.
+  let [ tab, , monitor ] = yield initNetMonitor(SIMPLE_URL);
+  let { NetMonitorView } = monitor.panelWin;
+  let { RequestsMenu } = NetMonitorView;
+  RequestsMenu.lazyUpdate = false;
+
+  tab.linkedBrowser.loadURI(TOP_URL, null, null);
+
+  yield waitForNetworkEvents(monitor, REQUEST_COUNT);
+
+  is(RequestsMenu.itemCount, REQUEST_COUNT,
+    "All the page events should be recorded.");
+
+  // While there is a defined order for requests in each document separately, the requests
+  // from different documents may interleave in various ways that change per test run, so
+  // there is not a single order when considering all the requests together.
+  let currentTop = 0;
+  let currentSub = 0;
+  for (let i = 0; i < REQUEST_COUNT; i++) {
+    let requestItem = RequestsMenu.getItemAtIndex(i);
+
+    let itemUrl = requestItem.attachment.url;
+    let itemCauseUri = requestItem.target.querySelector(".requests-menu-cause-label")
+                                         .getAttribute("tooltiptext");
+    let spec;
+    if (itemUrl == SUB_URL || itemCauseUri == SUB_URL) {
+      spec = EXPECTED_REQUESTS_SUB[currentSub++];
+    } else {
+      spec = EXPECTED_REQUESTS_TOP[currentTop++];
+    }
+    let { method, url, causeType, causeUri, stack } = spec;
+
+    verifyRequestItemTarget(requestItem,
+      method, url, { cause: { type: causeType, loadingDocumentUri: causeUri } }
+    );
+
+    let { stacktrace } = requestItem.attachment.cause;
+    let stackLen = stacktrace ? stacktrace.length : 0;
+
+    if (stack) {
+      ok(stacktrace, `Request #${i} has a stacktrace`);
+      ok(stackLen > 0,
+        `Request #${i} (${causeType}) has a stacktrace with ${stackLen} items`);
+
+      // if "stack" is array, check the details about the top stack frames
+      if (Array.isArray(stack)) {
+        stack.forEach((frame, j) => {
+          is(stacktrace[j].functionName, frame.fn,
+            `Request #${i} has the correct function on JS stack frame #${j}`);
+          is(stacktrace[j].filename.split("/").pop(), frame.file,
+            `Request #${i} has the correct file on JS stack frame #${j}`);
+          is(stacktrace[j].lineNumber, frame.line,
+            `Request #${i} has the correct line number on JS stack frame #${j}`);
+          is(stacktrace[j].asyncCause, frame.asyncCause,
+            `Request #${i} has the correct async cause on JS stack frame #${j}`);
+        });
+      }
+    } else {
+      is(stackLen, 0, `Request #${i} (${causeType}) has an empty stacktrace`);
+    }
+  }
+
+  yield teardown(monitor);
+});
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -151,27 +151,28 @@ function initNetMonitor(aUrl, aWindow, a
     info("Net tab added successfully: " + aUrl);
 
     let target = TargetFactory.forTab(tab);
 
     yield target.makeRemote();
     info("Target remoted.");
 
     if (!aEnableCache) {
+      info("Disabling cache and reloading page.");
       yield toggleCache(target, true);
       info("Cache disabled when the current and all future toolboxes are open.");
       // Remove any requests generated by the reload while toggling the cache to
       // avoid interfering with the test.
       isnot([...target.activeConsole.getNetworkEvents()].length, 0,
          "Request to reconfigure the tab was recorded.");
       target.activeConsole.clearNetworkRequests();
     }
 
     let toolbox = yield gDevTools.showToolbox(target, "netmonitor");
-    info("Netork monitor pane shown successfully.");
+    info("Network monitor pane shown successfully.");
 
     let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject;
     let monitor = toolbox.getCurrentPanel();
     return [tab, debuggee, monitor];
   });
 }
 
 function restartNetMonitor(aMonitor, aNewUrl) {
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_frame-subdocument.html
@@ -0,0 +1,48 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+    <meta http-equiv="Pragma" content="no-cache" />
+    <meta http-equiv="Expires" content="0" />
+    <title>Network Monitor test page</title>
+    <link rel="stylesheet" type="text/css" href="stylesheet_request" />
+  </head>
+
+  <body>
+    <p>Request frame test</p>
+    <img src="img_request" />
+    <script type="text/javascript">
+      function performXhrRequest() {
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET", "xhr_request", true);
+        xhr.send();
+      }
+
+      function performFetchRequest() {
+        fetch("fetch_request");
+      }
+
+      function performBeaconRequest() {
+        navigator.sendBeacon("beacon_request");
+      }
+
+      performXhrRequest();
+      performFetchRequest();
+
+      // Perform some requests with async stacks
+      Promise.resolve().then(function performPromiseFetchRequest() {
+        fetch("promise_fetch_request");
+        setTimeout(function performTimeoutFetchRequest() {
+          fetch("timeout_fetch_request");
+
+          // Finally, send a beacon request
+          performBeaconRequest();
+        }, 0);
+      });
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_frame-test-page.html
@@ -0,0 +1,49 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+    <meta http-equiv="Pragma" content="no-cache" />
+    <meta http-equiv="Expires" content="0" />
+    <title>Network Monitor test page</title>
+    <link rel="stylesheet" type="text/css" href="stylesheet_request" />
+  </head>
+
+  <body>
+    <p>Request frame test</p>
+    <img src="img_request" />
+    <iframe src="html_frame-subdocument.html"></iframe>
+    <script type="text/javascript">
+      function performXhrRequest() {
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET", "xhr_request", true);
+        xhr.send();
+      }
+
+      function performFetchRequest() {
+        fetch("fetch_request");
+      }
+
+      function performBeaconRequest() {
+        navigator.sendBeacon("beacon_request");
+      }
+
+      performXhrRequest();
+      performFetchRequest();
+
+      // Perform some requests with async stacks
+      Promise.resolve().then(function performPromiseFetchRequest() {
+        fetch("promise_fetch_request");
+        setTimeout(function performTimeoutFetchRequest() {
+          fetch("timeout_fetch_request");
+
+          // Finally, send a beacon request
+          performBeaconRequest();
+        }, 0);
+      });
+    </script>
+  </body>
+</html>
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -864,19 +864,18 @@ function TabActor(connection) {
   this.makeDebugger = makeDebugger.bind(null, {
     findDebuggees: () => {
       return this.windows.concat(this.webextensionsContentScriptGlobals);
     },
     shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee
   });
 
   // Flag eventually overloaded by sub classes in order to watch new docshells
-  // Used on b2g to catch activity frames and in chrome to list all frames
-  this.listenForNewDocShells =
-    Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+  // Used by the ChromeActor to list all frames in the Browser Toolbox
+  this.listenForNewDocShells = false;
 
   this.traits = {
     reconfigure: true,
     // Supports frame listing via `listFrames` request and `frameUpdate` events
     // as well as frame switching via `switchToFrame` request
     frames: true,
     // Do not require to send reconfigure request to reset the document state
     // to what it was before using the TabActor
@@ -933,19 +932,23 @@ TabActor.prototype = {
   get chromeEventHandler() {
     return getDocShellChromeEventHandler(this.docShell);
   },
 
   /**
    * Getter for the nsIMessageManager associated to the tab.
    */
   get messageManager() {
-    return this.docShell
-      .QueryInterface(Ci.nsIInterfaceRequestor)
-      .getInterface(Ci.nsIContentFrameMessageManager);
+    try {
+      return this.docShell
+        .QueryInterface(Ci.nsIInterfaceRequestor)
+        .getInterface(Ci.nsIContentFrameMessageManager);
+    } catch (e) {
+      return null;
+    }
   },
 
   /**
    * Getter for the tab's doc shell.
    */
   get docShell() {
     throw new Error(
       "The docShell getter should be implemented by a subclass of TabActor");
@@ -967,16 +970,25 @@ TabActor.prototype = {
     if (this.docShell) {
       return this.docShell
         .QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIDOMWindow);
     }
     return null;
   },
 
+  get outerWindowID() {
+    if (this.window) {
+      return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
+                        .getInterface(Ci.nsIDOMWindowUtils)
+                        .outerWindowID;
+    }
+    return null;
+  },
+
   /**
    * Getter for the WebExtensions ContentScript globals related to the
    * current tab content's DOM window.
    */
   get webextensionsContentScriptGlobals() {
     // Ignore xpcshell runtime which spawn TabActors without a window.
     if (this.window) {
       return ExtensionContent.getContentScriptGlobalsForWindow(this.window);
@@ -1099,20 +1111,17 @@ TabActor.prototype = {
     };
 
     // We may try to access window while the document is closing, then
     // accessing window throws. Also on xpcshell we are using tabactor even if
     // there is no valid document.
     if (this.docShell && !this.docShell.isBeingDestroyed()) {
       response.title = this.title;
       response.url = this.url;
-      let windowUtils = this.window
-        .QueryInterface(Ci.nsIInterfaceRequestor)
-        .getInterface(Ci.nsIDOMWindowUtils);
-      response.outerWindowID = windowUtils.outerWindowID;
+      response.outerWindowID = this.outerWindowID;
     }
 
     // Always use the same ActorPool, so existing actor instances
     // (created in createExtraActors) are not lost.
     if (!this._tabActorPool) {
       this._tabActorPool = new ActorPool(this.conn);
       this.conn.addActorPool(this._tabActorPool);
     }
@@ -1396,39 +1405,41 @@ TabActor.prototype = {
   _notifyDocShellsUpdate(docshells) {
     let windows = this._docShellsToWindows(docshells);
 
     // Do not send the `frameUpdate` event if the windows array is empty.
     if (windows.length == 0) {
       return;
     }
 
-    this.conn.send({ from: this.actorID,
-                     type: "frameUpdate",
-                     frames: windows
-                   });
+    this.conn.send({
+      from: this.actorID,
+      type: "frameUpdate",
+      frames: windows
+    });
   },
 
   _updateChildDocShells() {
     this._notifyDocShellsUpdate(this.docShells);
   },
 
   _notifyDocShellDestroy(webProgress) {
     webProgress = webProgress.QueryInterface(Ci.nsIWebProgress);
     let id = webProgress.DOMWindow
                         .QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDOMWindowUtils)
                         .outerWindowID;
-    this.conn.send({ from: this.actorID,
-                     type: "frameUpdate",
-                     frames: [{
-                       id: id,
-                       destroy: true
-                     }]
-                   });
+    this.conn.send({
+      from: this.actorID,
+      type: "frameUpdate",
+      frames: [{
+        id,
+        destroy: true
+      }]
+    });
 
     // Stop watching this docshell (the unwatch() method will check if we
     // started watching it before).
     webProgress.QueryInterface(Ci.nsIDocShell);
     this._progressListener.unwatch(webProgress);
 
     if (webProgress.DOMWindow == this._originalWindow) {
       // If the original top level document we connected to is removed,
@@ -1457,20 +1468,21 @@ TabActor.prototype = {
     // we have to switch to the top-level one.
     if (webProgress.DOMWindow == this.window &&
         this.window != this._originalWindow) {
       this._changeTopLevelDocument(this._originalWindow);
     }
   },
 
   _notifyDocShellDestroyAll() {
-    this.conn.send({ from: this.actorID,
-                     type: "frameUpdate",
-                     destroyAll: true
-                   });
+    this.conn.send({
+      from: this.actorID,
+      type: "frameUpdate",
+      destroyAll: true
+    });
   },
 
   /**
    * Creates a thread actor and a pool for context-lifetime actors. It then sets
    * up the content window for debugging.
    */
   _pushContext() {
     assert(!this._contextPool, "Can't push multiple contexts");
@@ -1867,23 +1879,21 @@ TabActor.prototype = {
     // targeted context (it will indirectly update this.window and
     // many other attributes defined from docShell).
     Object.defineProperty(this, "docShell", {
       value: docShell,
       enumerable: true,
       configurable: true
     });
     events.emit(this, "changed-toplevel-document");
-    let id = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                   .getInterface(Ci.nsIDOMWindowUtils)
-                   .outerWindowID;
-    this.conn.send({ from: this.actorID,
-                     type: "frameUpdate",
-                     selected: id
-                   });
+    this.conn.send({
+      from: this.actorID,
+      type: "frameUpdate",
+      selected: this.outerWindowID
+    });
   },
 
   /**
    * Handle location changes, by clearing the previous debuggees and enabling
    * debugging, which may have been disabled temporarily by the
    * DebuggerProgressListener.
    */
   _windowReady(window, isFrameSwitching = false) {
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -610,18 +610,18 @@ WebConsoleActor.prototype =
             this.stackTraceCollector.init();
 
             let processBoundary = Services.appinfo.processType !=
                                   Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
             if ((appId || messageManager) && processBoundary) {
               // Start a network monitor in the parent process to listen to
               // most requests than happen in parent
               this.networkMonitor =
-                new NetworkMonitorChild(appId, messageManager,
-                                        this.parentActor.actorID, this);
+                new NetworkMonitorChild(appId, this.parentActor.outerWindowID,
+                                        messageManager, this.parentActor.actorID, this);
               this.networkMonitor.init();
               // Spawn also one in the child to listen to service workers
               this.networkMonitorChild = new NetworkMonitor({ window }, this);
               this.networkMonitorChild.init();
             } else {
               this.networkMonitor = new NetworkMonitor({ window }, this);
               this.networkMonitor.init();
             }
--- a/devtools/shared/webconsole/network-monitor.js
+++ b/devtools/shared/webconsole/network-monitor.js
@@ -45,17 +45,17 @@ const RESPONSE_BODY_LIMIT = 1048576;
  *        Request to check.
  * @param filters
  *        NetworkMonitor filters to match against.
  * @return boolean
  *         True if the network request should be logged, false otherwise.
  */
 function matchRequest(channel, filters) {
   // Log everything if no filter is specified
-  if (!filters.topFrame && !filters.window && !filters.appId) {
+  if (!filters.outerWindowID && !filters.window && !filters.appId) {
     return true;
   }
 
   // Ignore requests from chrome or add-on code when we are monitoring
   // content.
   // TODO: one particular test (browser_styleeditor_fetch-from-cache.js) needs
   // the flags.testing check. We will move to a better way to serve
   // its needs in bug 1167188, where this check should be removed.
@@ -76,37 +76,21 @@ function matchRequest(channel, filters) 
       }
       if (win.parent == win) {
         break;
       }
       win = win.parent;
     }
   }
 
-  if (filters.topFrame) {
+  if (filters.outerWindowID) {
     let topFrame = NetworkHelper.getTopFrameForRequest(channel);
-    while (topFrame) {
-      // In the normal case, a topFrame filter should match the request's topFrame if it
-      // will match at all.
-      if (topFrame === filters.topFrame) {
-        return true;
-      }
-      // As a stop gap approach for RDM, where `filters.topFrame` will be the
-      // <xul:browser> frame for an entire tab and the request's `topFrame` is the
-      // <iframe mozbrower> that triggered the request, we try to climb up parent frames
-      // above the request's `topFrame` to see if they might also match the filter.
-      // In bug 1240912, we want to rework this, since we don't really want to be passing
-      // a frame down to the network monitor.
-      if (!topFrame.ownerGlobal) {
-        break;
-      }
-      topFrame = topFrame.ownerGlobal
-                         .QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindowUtils)
-                         .containerElement;
+    if (topFrame && topFrame.outerWindowID &&
+        topFrame.outerWindowID == filters.outerWindowID) {
+      return true;
     }
   }
 
   if (filters.appId) {
     let appId = NetworkHelper.getAppIdForRequest(channel);
     if (appId && appId == filters.appId) {
       return true;
     }
@@ -684,17 +668,17 @@ NetworkResponseListener.prototype = {
  * routed to the remote Web Console.
  *
  * @constructor
  * @param object filters
  *        Object with the filters to use for network requests:
  *        - window (nsIDOMWindow): filter network requests by the associated
  *          window object.
  *        - appId (number): filter requests by the appId.
- *        - topFrame (nsIDOMElement): filter requests by their topFrameElement.
+ *        - outerWindowID (number): filter requests by their top frame's outerWindowID.
  *        Filters are optional. If any of these filters match the request is
  *        logged (OR is applied). If no filter is provided then all requests are
  *        logged.
  * @param object owner
  *        The network monitor owner. This object needs to hold:
  *        - onNetworkEvent(requestInfo, channel, networkMonitor).
  *          This method is invoked once for every new network request and it is
  *          given the following arguments: the initial network request
@@ -1395,25 +1379,28 @@ NetworkMonitor.prototype = {
  *
  * The main process creates NetworkEventActorProxy instances per request. These
  * send the data to this object using the nsIMessageManager. Here we proxy the
  * data to the WebConsoleActor or to a NetworkEventActor.
  *
  * @constructor
  * @param number appId
  *        The web appId of the child process.
+ * @param number outerWindowID
+ *        The outerWindowID of the TabActor's main window.
  * @param nsIMessageManager messageManager
  *        The nsIMessageManager to use to communicate with the parent process.
  * @param string connID
  *        The connection ID to use for send messages to the parent process.
  * @param object owner
  *        The WebConsoleActor that is listening for the network requests.
  */
-function NetworkMonitorChild(appId, messageManager, connID, owner) {
+function NetworkMonitorChild(appId, outerWindowID, messageManager, connID, owner) {
   this.appId = appId;
+  this.outerWindowID = outerWindowID;
   this.connID = connID;
   this.owner = owner;
   this._messageManager = messageManager;
   this._onNewEvent = this._onNewEvent.bind(this);
   this._onUpdateEvent = this._onUpdateEvent.bind(this);
   this._netEvents = new Map();
 }
 
@@ -1443,16 +1430,17 @@ NetworkMonitorChild.prototype = {
   init: function () {
     let mm = this._messageManager;
     mm.addMessageListener("debug:netmonitor:" + this.connID + ":newEvent",
                           this._onNewEvent);
     mm.addMessageListener("debug:netmonitor:" + this.connID + ":updateEvent",
                           this._onUpdateEvent);
     mm.sendAsyncMessage("debug:netmonitor:" + this.connID, {
       appId: this.appId,
+      outerWindowID: this.outerWindowID,
       action: "start",
     });
   },
 
   _onNewEvent: DevToolsUtils.makeInfallible(function _onNewEvent(msg) {
     let {id, event} = msg.data;
 
     // Try to add stack trace to the event data received from parent
@@ -1582,46 +1570,44 @@ NetworkEventActorProxy.prototype = {
  *        Instance identifier to use for messages.
  */
 function NetworkMonitorManager(frame, id) {
   this.id = id;
   // Get messageManager from XUL browser (which might be a specialized tunnel for RDM)
   // or else fallback to asking the frameLoader itself.
   let mm = frame.messageManager || frame.frameLoader.messageManager;
   this.messageManager = mm;
-  this.frame = frame;
   this.onNetMonitorMessage = this.onNetMonitorMessage.bind(this);
   this.onNetworkEvent = this.onNetworkEvent.bind(this);
 
   mm.addMessageListener("debug:netmonitor:" + id, this.onNetMonitorMessage);
 }
 exports.NetworkMonitorManager = NetworkMonitorManager;
 
 NetworkMonitorManager.prototype = {
   netMonitor: null,
-  frame: null,
   messageManager: null,
 
   /**
    * Handler for "debug:monitor" messages received through the message manager
    * from the content process.
    *
    * @param object msg
    *        Message from the content.
    */
   onNetMonitorMessage: DevToolsUtils.makeInfallible(function (msg) {
     let {action} = msg.json;
     // Pipe network monitor data from parent to child via the message manager.
     switch (action) {
       case "start":
         if (!this.netMonitor) {
-          let {appId} = msg.json;
+          let {appId, outerWindowID} = msg.json;
           this.netMonitor = new NetworkMonitor({
-            topFrame: this.frame,
-            appId: appId,
+            outerWindowID,
+            appId,
           }, this);
           this.netMonitor.init();
         }
         break;
       case "setPreferences": {
         let {preferences} = msg.json;
         for (let key of Object.keys(preferences)) {
           if (key == "saveRequestAndResponseBodies" && this.netMonitor) {
@@ -1655,21 +1641,21 @@ NetworkMonitorManager.prototype = {
    *         data about the request is available.
    */
   onNetworkEvent: DevToolsUtils.makeInfallible(function _onNetworkEvent(event) {
     return new NetworkEventActorProxy(this.messageManager, this.id).init(event);
   }),
 
   destroy: function () {
     if (this.messageManager) {
-      this.messageManager.removeMessageListener("debug:netmonitor:" + this.id,
-                                                this.onNetMonitorMessage);
+      let mm = this.messageManager;
+      this.messageManager = null;
+      mm.removeMessageListener("debug:netmonitor:" + this.id, this.onNetMonitorMessage);
     }
-    this.messageManager = null;
-    this.filters = null;
+    this.frames = null;
 
     if (this.netMonitor) {
       this.netMonitor.destroy();
       this.netMonitor = null;
     }
   },
 };