Bug 1574159 - implement listRemoteFrames for browsingContextTarget and listRemoteTab for content process; r=ochameau
authoryulia <ystartsev@mozilla.com>
Tue, 27 Aug 2019 09:07:46 +0000
changeset 553774 9a85c28fd033545e63ac5851f71c4d34a9436d28
parent 553773 5c69342869b413c4249304f8fd016603652fc809
child 553775 2187d722dc389e70f1f0f93ac0195f31e3fab8c8
push id2165
push userffxbld-merge
push dateMon, 14 Oct 2019 16:30:58 +0000
treeherdermozilla-release@0eae18af659f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1574159
milestone70.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 1574159 - implement listRemoteFrames for browsingContextTarget and listRemoteTab for content process; r=ochameau Differential Revision: https://phabricator.services.mozilla.com/D42327
devtools/server/actors/descriptors/frame.js
devtools/server/actors/targets/browsing-context.js
devtools/server/actors/targets/parent-process.js
devtools/server/tests/browser/browser.ini
devtools/server/tests/browser/browser_resource_list-remote-frames.js
devtools/server/tests/browser/doc_iframe.html
devtools/shared/fronts/descriptors/frame.js
devtools/shared/specs/targets/browsing-context.js
--- a/devtools/server/actors/descriptors/frame.js
+++ b/devtools/server/actors/descriptors/frame.js
@@ -67,20 +67,24 @@ const FrameDescriptorActor = ActorClassW
     } else {
       form = await this._connectBrowsingContext();
     }
 
     return form;
   },
 
   form() {
+    const url = this._browsingContext.currentWindowGlobal
+      ? this._browsingContext.currentWindowGlobal.documentURI.displaySpec
+      : null;
     return {
       actor: this.actorID,
       id: this.id,
-      parentId: this._browsingContext.parent
+      url,
+      parentID: this._browsingContext.parent
         ? this._browsingContext.parent.id
         : null,
     };
   },
 
   destroy() {
     this._browsingContext = null;
     this._embedderElement = null;
--- a/devtools/server/actors/targets/browsing-context.js
+++ b/devtools/server/actors/targets/browsing-context.js
@@ -40,16 +40,22 @@ const L10N = new LocalizationHelper(STRI
 const { ActorClassWithSpec, Actor, Pool } = require("devtools/shared/protocol");
 const {
   LazyPool,
   createExtraActors,
 } = require("devtools/shared/protocol/lazy-pool");
 const {
   browsingContextTargetSpec,
 } = require("devtools/shared/specs/targets/browsing-context");
+loader.lazyRequireGetter(
+  this,
+  "FrameDescriptorActor",
+  "devtools/server/actors/descriptors/frame",
+  true
+);
 
 loader.lazyRequireGetter(
   this,
   "ThreadActor",
   "devtools/server/actors/thread",
   true
 );
 loader.lazyRequireGetter(
@@ -279,16 +285,17 @@ const browsingContextTargetPrototype = {
       // Supports the logInPage request.
       logInPage: true,
       // Supports requests related to rewinding.
       canRewind,
     };
 
     this._workerTargetActorList = null;
     this._workerTargetActorPool = null;
+    this._frameDescriptorActorPool = null;
     this._onWorkerTargetActorListChanged = this._onWorkerTargetActorListChanged.bind(
       this
     );
   },
 
   traits: null,
 
   // Optional console API listener options (e.g. used by the WebExtensionActor to
@@ -352,16 +359,20 @@ const browsingContextTargetPrototype = {
    */
   get docShell() {
     throw new Error(
       "`docShell` getter should be overridden by a subclass of " +
         "`BrowsingContextTargetActor`"
     );
   },
 
+  get childBrowsingContexts() {
+    return this.docShell.browsingContext.getChildren();
+  },
+
   /**
    * Getter for the list of all `docShell`s in the browsing context.
    * @return {Array}
    */
   get docShells() {
     return getChildDocShells(this.docShell);
   },
 
@@ -659,16 +670,59 @@ const browsingContextTargetPrototype = {
     return {};
   },
 
   listFrames(request) {
     const windows = this._docShellsToWindows(this.docShells);
     return { frames: windows };
   },
 
+  listRemoteFrames() {
+    const frames = [];
+    const contextsToWalk = this.childBrowsingContexts;
+
+    if (contextsToWalk == 0) {
+      return { frames };
+    }
+
+    const pool = new Pool(this.conn);
+    while (contextsToWalk.length) {
+      const currentContext = contextsToWalk.pop();
+      let frameDescriptor = this._getKnownFrameDescriptor(currentContext.id);
+      if (!frameDescriptor) {
+        frameDescriptor = new FrameDescriptorActor(this.conn, currentContext);
+      }
+      pool.manage(frameDescriptor);
+      frames.push(frameDescriptor);
+      contextsToWalk.push(...currentContext.getChildren());
+    }
+    // Do not destroy the pool before transfering ownership to the newly created
+    // pool, so that we do not accidently destroy actors that are still in use.
+    if (this._frameDescriptorActorPool) {
+      this._frameDescriptorActorPool.destroy();
+    }
+
+    this._frameDescriptorActorPool = pool;
+
+    return { frames };
+  },
+
+  _getKnownFrameDescriptor(id) {
+    // if there is no pool, then we do not have any descriptors
+    if (!this._frameDescriptorActorPool) {
+      return null;
+    }
+    for (const descriptor of this._frameDescriptorActorPool.poolChildren()) {
+      if (descriptor.id === id) {
+        return descriptor;
+      }
+    }
+    return null;
+  },
+
   ensureWorkerTargetActorList() {
     if (this._workerTargetActorList === null) {
       this._workerTargetActorList = new WorkerTargetActorList(this.conn, {
         type: Ci.nsIWorkerDebugger.TYPE_DEDICATED,
         window: this.window,
       });
     }
     return this._workerTargetActorList;
@@ -956,16 +1010,21 @@ const browsingContextTargetPrototype = {
       this._workerTargetActorList = null;
     }
 
     if (this._workerTargetActorPool !== null) {
       this._workerTargetActorPool.destroy();
       this._workerTargetActorPool = null;
     }
 
+    if (this._frameDescriptorActorPool !== null) {
+      this._frameDescriptorActorPool.destroy();
+      this._frameDescriptorActorPool = null;
+    }
+
     this._attached = false;
 
     this.emit("tabDetached");
 
     return true;
   },
 
   // Protocol Request Handlers
--- a/devtools/server/actors/targets/parent-process.js
+++ b/devtools/server/actors/targets/parent-process.js
@@ -97,16 +97,35 @@ Object.defineProperty(parentProcessTarge
     for (const { docShell } of Services.ww.getWindowEnumerator()) {
       docShells = docShells.concat(getChildDocShells(docShell));
     }
 
     return docShells;
   },
 });
 
+/**
+ * Getter for the list of all browsingContexts in the parent process.
+ * We use specialized code in order to retrieve <browser>'s browsing context for
+ * each browser's tab. BrowsingContext.getChildren method doesn't return the
+ * tab's BrowsingContext because they are of "content" type, while the root
+ * BrowsingContext of the parent process target is of "chrome" type.
+ *
+ * @return {Array}
+ */
+Object.defineProperty(parentProcessTargetPrototype, "childBrowsingContexts", {
+  get: function() {
+    // Iterate over all `browser` elements that are remote, and return their
+    // browsing context.
+    return [
+      ...this.window.document.querySelectorAll(`browser[remote="true"]`),
+    ].map(browser => browser.browsingContext);
+  },
+});
+
 parentProcessTargetPrototype.observe = function(subject, topic, data) {
   BrowsingContextTargetActor.prototype.observe.call(this, subject, topic, data);
   if (!this.attached) {
     return;
   }
 
   subject.QueryInterface(Ci.nsIDocShell);
 
--- a/devtools/server/tests/browser/browser.ini
+++ b/devtools/server/tests/browser/browser.ini
@@ -14,16 +14,17 @@ support-files =
   doc_accessibility_infobar.html
   doc_accessibility_text_label_audit_frame.html
   doc_accessibility_text_label_audit.html
   doc_accessibility.html
   doc_allocations.html
   doc_force_cc.html
   doc_force_gc.html
   doc_innerHTML.html
+  doc_iframe.html
   doc_perf.html
   doc_promise-get-allocation-stack.html
   doc_promise-get-fulfillment-stack.html
   doc_promise-get-rejection-stack.html
   error-actor.js
   grid.html
   inspectedwindow-reload-target.sjs
   inspector-search-data.html
@@ -148,13 +149,14 @@ fail-if = fission
 fail-if = fission
 [browser_storage_updates.js]
 fail-if = fission
 [browser_storage_webext_storage_local.js]
 [browser_styles_getRuleText.js]
 [browser_stylesheets_getTextEmpty.js]
 [browser_stylesheets_nested-iframes.js]
 [browser_register_actor.js]
+[browser_resource_list-remote-frames.js]
 [browser_webextension_inspected_window.js]
 [browser_dbg_promises-allocation-stack.js]
 [browser_dbg_promises-chrome-allocation-stack.js]
 [browser_dbg_promises-fulfillment-stack.js]
 [browser_dbg_promises-rejection-stack.js]
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/browser_resource_list-remote-frames.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that we can get a stack to a promise's allocation point in the chrome
+ * process.
+ */
+
+"use strict";
+
+add_task(async function() {
+  await pushPref("fission.autostart", true);
+  const tabTarget = await addTabTarget(MAIN_DOMAIN + "doc_iframe.html");
+  await testLocalListFrames(tabTarget);
+  await testBrowserListFrames(tabTarget);
+});
+
+async function testLocalListFrames(tabTarget) {
+  // at this point, tabTarget is the tab with two iframes, one nested inside
+  // the other.
+  const { frames } = await tabTarget.listRemoteFrames();
+  is(frames.length, 2, "Got two frames");
+
+  // Since we do not have access to remote frames yet this will return null.
+  // This test should be updated when we have access to remote frames.
+  for (const frame of frames) {
+    const frameTarget = await frame.getTarget();
+    is(frameTarget, null, "We cannot get remote iframe fronts yet");
+  }
+
+  // However we can confirm that the newly created iframe is there.
+  const browser = gBrowser.selectedBrowser;
+  const oopID = await ContentTask.spawn(browser, {}, async () => {
+    const oop = content.document.querySelector("iframe");
+    return oop.frameLoader.browsingContext.id;
+  });
+  ok(
+    frames.find(f => f.id === oopID),
+    "tabTarget.listRemoteFrames returns the oop frame descriptor"
+  );
+}
+async function testBrowserListFrames(tabTarget) {
+  // Now, we can test against the entire browser. getMainProcess will return
+  // a target for the parentProcess, and will be able to enumerate over all
+  // the tabs, the remote iframe, and the pair of frames, one nested inside the other.
+  const target = await tabTarget.client.mainRoot.getMainProcess();
+  await getFrames(target, tabTarget);
+}
+
+async function getFrames(target) {
+  const { frames } = await target.listRemoteFrames();
+
+  // Connect to the tab which is being debugged, this is equivilant to the
+  // browsing context of the tabTarget. The difference between tabTarget's browsing
+  // context, and this browsing context, is we have less information in tabTarget,
+  // since it is requesting information from a content process. From the parent
+  // we can access information like the URL.
+  const descriptor = frames.find(f => f.url && f.url.includes("doc_iframe"));
+
+  const front = await descriptor.getTarget();
+  ok(front.hasActor("console"), "Got the console actor");
+  ok(front.hasActor("thread"), "Got the thread actor");
+  // Ensure sending at least one request to an actor...
+  const consoleFront = await front.getFront("console");
+  const { result } = await consoleFront.evaluateJS("var a = 42; a");
+  is(result, 42, "console.eval worked");
+
+  // Although we can get metadata about the child frames,
+  // since we do not have access to remote frames yet, this will return null.
+  // This test should be updated when we have access to remote frames.
+  const childFrames = frames.filter(d => d.parentID === descriptor.id);
+  for (const frame of childFrames) {
+    const frameTarget = await frame.getTarget();
+    is(frameTarget, null, "We cannot get remote iframe fronts yet");
+  }
+
+  getFirstFrameAgain(front, descriptor, target);
+}
+
+// Assert that calling descriptor.getTarget returns the same actor.
+async function getFirstFrameAgain(firstTargetFront, descriptor, target) {
+  const targetFront = await descriptor.getTarget();
+
+  is(
+    targetFront,
+    firstTargetFront,
+    "Second call to getTarget with the same id returns the same form"
+  );
+
+  const { frames } = await target.listRemoteFrames();
+  const secondDescriptor = frames.find(f => f.id === descriptor.id);
+  const secondTargetFront = await secondDescriptor.getTarget();
+  is(
+    secondTargetFront,
+    firstTargetFront,
+    "Second call to listFrames with the same id returns the same form"
+  );
+}
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/browser/doc_iframe.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>iframe test page</title>
+  </head>
+
+  <body>
+    <iframe id="better-not-ask" src="data:text/html,<iframe src='data:text/html,foo'></iframe>"></iframe>
+  </body>
+
+</html>
--- a/devtools/shared/fronts/descriptors/frame.js
+++ b/devtools/shared/fronts/descriptors/frame.js
@@ -24,16 +24,18 @@ class FrameDescriptorFront extends Front
     super(client);
     this._frameDescriptorFront = null;
     this._targetFrontPromise = null;
     this._client = client;
   }
 
   form(json) {
     this.id = json.id;
+    this.url = json.url;
+    this.parentID = json.parentID;
   }
 
   async _createFrameTarget(form) {
     let front = null;
     front = new BrowsingContextTargetFront(this._client);
     front.actorID = form.actor;
     front.form(form);
     this.manage(front);
--- a/devtools/shared/specs/targets/browsing-context.js
+++ b/devtools/shared/specs/targets/browsing-context.js
@@ -27,16 +27,20 @@ types.addDictType("browsingContextTarget
   error: "nullable:string",
   message: "nullable:string",
 });
 
 types.addDictType("browsingContextTarget.listframes", {
   frames: "array:browsingContextTarget.window",
 });
 
+types.addDictType("browsingContextTarget.listRemoteFrames", {
+  frames: "array:frameDescriptor",
+});
+
 types.addDictType("browsingContextTarget.window", {
   id: "string",
   parentID: "nullable:string",
   url: "nullable:string", // should be present if not destroying
   title: "nullable:string", // should be present if not destroying
   destroy: "nullable:boolean", // not present if not destroying
 });
 
@@ -99,16 +103,20 @@ const browsingContextTargetSpecPrototype
         windowId: Option(0, "string"),
       },
       response: RetVal("browsingContextTarget.switchtoframe"),
     },
     listFrames: {
       request: {},
       response: RetVal("browsingContextTarget.listframes"),
     },
+    listRemoteFrames: {
+      request: {},
+      response: RetVal("browsingContextTarget.listRemoteFrames"),
+    },
     listWorkers: {
       request: {},
       response: RetVal("browsingContextTarget.workers"),
     },
     logInPage: {
       request: {
         text: Option(0, "string"),
         category: Option(0, "string"),