Bug 878614 - Handle subdocument loads/unloads in the inspector actor. r=jwalker
authorDave Camp <dcamp@mozilla.com>
Fri, 07 Jun 2013 11:02:32 -0700
changeset 146703 97ca769b1ed6568a5ecf7737489afaa42d95c8b7
parent 146702 cccfd3161714be699bc2dd6c3bccc289bd3eece7
child 146704 88cd07a112ae43a74d5f3cd15d25ca2cae5bfcdc
push id2697
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 18:49:53 +0000
treeherdermozilla-beta@dfec938c7b63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs878614
milestone24.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 878614 - Handle subdocument loads/unloads in the inspector actor. r=jwalker
toolkit/devtools/server/actors/inspector.js
toolkit/devtools/server/tests/mochitest/Makefile.in
toolkit/devtools/server/tests/mochitest/test_inspector-mutations-frameload.html
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -44,16 +44,18 @@
 const {Cc, Ci, Cu} = require("chrome");
 
 const protocol = require("devtools/server/protocol");
 const {Arg, Option, method, RetVal, types} = protocol;
 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
 const promise = require("sdk/core/promise");
 const object = require("sdk/util/object");
 const events = require("sdk/event/core");
+const { Unknown } = require("sdk/platform/xpcom");
+const { Class } = require("sdk/core/heritage");
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 exports.register = function(handle) {
   handle.addTabActor(InspectorActor, "inspectorActor");
 };
 
 exports.unregister = function(handle) {
@@ -566,44 +568,96 @@ let traversalMethod = {
     whatToShow: Option(1)
   },
   response: {
     node: RetVal("domnode")
   }
 }
 
 /**
+ * We need to know when a document is navigating away so that we can kill
+ * the nodes underneath it.  We also need to know when a document is
+ * navigated to so that we can send a mutation event for the iframe node.
+ *
+ * The nsIWebProgressListener is the easiest/best way to watch these
+ * loads that works correctly with the bfcache.
+ *
+ * See nsIWebProgressListener for details
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIWebProgressListener
+ */
+var ProgressListener = Class({
+  extends: Unknown,
+  interfaces: ["nsIWebProgressListener", "nsISupportsWeakReference"],
+
+  initialize: function(webProgress) {
+    Unknown.prototype.initialize.call(this);
+    this.webProgress = webProgress;
+    this.webProgress.addProgressListener(this);
+  },
+
+  destroy: function() {
+    this.webProgress.removeProgressListener(this);
+  },
+
+  onStateChange: makeInfallible(function stateChange(progress, request, flag, status) {
+    let isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
+    let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+    if (!(isWindow || isDocument)) {
+      return;
+    }
+
+    if (isDocument && (flag & Ci.nsIWebProgressListener.STATE_START)) {
+      events.emit(this, "windowchange-start", progress.DOMWindow);
+    }
+    if (isWindow && (flag & Ci.nsIWebProgressListener.STATE_STOP)) {
+      events.emit(this, "windowchange-stop", progress.DOMWindow);
+    }
+  }),
+  onProgressChange: function() {},
+  onSecurityChange: function() {},
+  onStatusChange: function() {},
+  onLocationChange: function() {},
+});
+
+/**
  * Server side of the DOM walker.
  */
 var WalkerActor = protocol.ActorClass({
   typeName: "domwalker",
 
   events: {
     "new-mutations" : {
       type: "newMutations"
     }
   },
 
   /**
    * Create the WalkerActor
    * @param DebuggerServerConnection conn
    *    The server connection.
    */
-  initialize: function(conn, document, options) {
+  initialize: function(conn, document, webProgress, options) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.rootDoc = document;
     this._refMap = new Map();
     this._pendingMutations = [];
 
     // Nodes which have been removed from the client's known
     // ownership tree are considered "orphaned", and stored in
     // this set.
     this._orphaned = new Set();
 
     this.onMutations = this.onMutations.bind(this);
+    this.onFrameLoad = this.onFrameLoad.bind(this);
+    this.onFrameUnload = this.onFrameUnload.bind(this);
+
+    this.progressListener = ProgressListener(webProgress);
+
+    events.on(this.progressListener, "windowchange-start", this.onFrameUnload);
+    events.on(this.progressListener, "windowchange-stop", this.onFrameLoad);
 
     // Ensure that the root document node actor is ready and
     // managed.
     this.rootNode = this.document();
   },
 
   // Returns the JSON representation of this object over the wire.
   form: function() {
@@ -614,16 +668,17 @@ var WalkerActor = protocol.ActorClass({
   },
 
   toString: function() {
     return "[WalkerActor " + this.actorID + "]";
   },
 
   destroy: function() {
     protocol.Actor.prototype.destroy.call(this);
+    this.progressListener.destroy();
     this.rootDoc = null;
   },
 
   release: method(function() {}, { release: true }),
 
   unmanage: function(actor) {
     if (actor instanceof NodeActor) {
       this._refMap.delete(actor.rawNode);
@@ -1141,18 +1196,54 @@ var WalkerActor = protocol.ActorClass({
         mutation.removed = removedActors;
         mutation.added = addedActors;
       }
       this._pendingMutations.push(mutation);
     }
     if (needEvent) {
       events.emit(this, "new-mutations");
     }
+  },
+
+  onFrameLoad: function(window) {
+    let frame = window.frameElement;
+    let frameActor = this._refMap.get(frame);
+    if (!frameActor) {
+      return;
+    }
+    let needEvent = this._pendingMutations.length === 0;
+    this._pendingMutations.push({
+      type: "frameLoad",
+      target: frameActor.actorID,
+      added: [],
+      removed: []
+    });
+
+    if (needEvent) {
+      events.emit(this, "new-mutations");
+    }
+  },
+
+  onFrameUnload: function(window) {
+    let doc = window.document;
+    let documentActor = this._refMap.get(doc);
+    if (!documentActor) {
+      return;
+    }
+
+    let needEvent = this._pendingMutations.length === 0;
+    this._pendingMutations.push({
+      type: "documentUnload",
+      target: documentActor.actorID
+    });
+    this.releaseNode(documentActor);
+    if (needEvent) {
+      events.emit(this, "new-mutations");
+    }
   }
-
 });
 
 /**
  * Client side of the DOM walker.
  */
 var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
   initialize: function(client, form) {
     protocol.Front.prototype.initialize.call(this, client, form);
@@ -1210,17 +1301,17 @@ var WalkerFront = exports.WalkerFront = 
   getMutations: protocol.custom(function() {
     return this._getMutations().then(mutations => {
       let emitMutations = [];
       for (let change of mutations) {
         // The target is only an actorID, get the associated front.
         let targetID = change.target;
         let targetFront = this.get(targetID);
         if (!targetFront) {
-          console.error("Got a mutation for an unexpected actor: " + targetID);
+          console.trace("Got a mutation for an unexpected actor: " + targetID + ", please file a bug on bugzilla.mozilla.org!");
           continue;
         }
 
         let emittedMutation = object.merge(change, { target: targetFront });
 
         if (change.type === "childList") {
           // Update the ownership tree according to the mutation record.
           let addedFronts = [];
@@ -1252,16 +1343,30 @@ var WalkerFront = exports.WalkerFront = 
             this._orphaned.delete(addedFront);
             addedFronts.push(addedFront);
           }
           // Before passing to users, replace the added and removed actor
           // ids with front in the mutation record.
           emittedMutation.added = addedFronts;
           emittedMutation.removed = removedFronts;
           targetFront._form.numChildren = change.numChildren;
+        } else if (change.type === "frameLoad") {
+          // Nothing we need to do here, except verify that we don't have any
+          // document children, because we should have gotten a documentUnload
+          // first.
+          for (let child of targetFront.treeChildren()) {
+            if (child.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
+              console.trace("Got an unexpected frameLoad in the inspector, please file a bug on bugzilla.mozilla.org!");
+            }
+          }
+        } else if (change.type === "documentUnload") {
+          // We try to give fronts instead of actorIDs, but these fronts need
+          // to be destroyed now.
+          emittedMutation.target = targetFront.actorID;
+          targetFront.destroy();
         } else {
           targetFront.updateMutation(change);
         }
 
         emitMutations.push(emittedMutation);
       }
       events.emit(this, "mutations", emitMutations);
     });
@@ -1305,20 +1410,21 @@ var InspectorActor = protocol.ActorClass
   initialize: function(conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.tabActor = tabActor;
     if (tabActor.browser instanceof Ci.nsIDOMWindow) {
       this.window = tabActor.browser;
     } else if (tabActor.browser instanceof Ci.nsIDOMElement) {
       this.window = tabActor.browser.contentWindow;
     }
+    this.webProgress = tabActor._tabbrowser;
   },
 
   getWalker: method(function(options={}) {
-    return WalkerActor(this.conn, this.window.document, options);
+    return WalkerActor(this.conn, this.window.document, this.webProgress, options);
   }, {
     request: {},
     response: {
       walker: RetVal("domwalker")
     }
   })
 });
 
--- a/toolkit/devtools/server/tests/mochitest/Makefile.in
+++ b/toolkit/devtools/server/tests/mochitest/Makefile.in
@@ -11,16 +11,17 @@ relativesrcdir	= @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_CHROME_FILES	= \
 	inspector-helpers.js \
 	inspector-traversal-data.html \
 	test_inspector-mutations-attr.html \
 	test_inspector-mutations-childlist.html \
+	test_inspector-mutations-frameload.html \
 	test_inspector-mutations-value.html \
 	test_inspector-release.html \
 	test_inspector-traversal.html \
 	test_unsafeDereference.html \
 	nonchrome_unsafeDereference.html \
 	$(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector-mutations-frameload.html
@@ -0,0 +1,273 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug </title>
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
+
+const Promise = devtools.require("sdk/core/promise");
+const inspector = devtools.require("devtools/server/actors/inspector");
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+}
+
+var gInspectee = null;
+var gWalker = null;
+var gClient = null;
+var gChildFrame = null;
+var gChildDocument = null;
+var gCleanupConnection = null;
+
+function setup(callback) {
+  let url = document.getElementById("inspectorContent").href;
+  gCleanupConnection = attachURL(url, function(err, client, tab, doc) {
+    gInspectee = doc;
+    let {InspectorFront} = devtools.require("devtools/server/actors/inspector");
+    let inspector = InspectorFront(client, tab);
+    promiseDone(inspector.getWalker().then(walker => {
+      gClient = client;
+      gWalker = walker;
+    }).then(callback));
+  });
+}
+
+function teardown() {
+  gWalker = null;
+  gClient = null;
+  gInspectee = null;
+  gChildFrame = null;
+  if (gCleanupConnection) {
+    gCleanupConnection();
+    gCleanupConnection = null;
+  }
+}
+
+function assertOwnership() {
+  return assertOwnershipTrees(gWalker);
+}
+
+function loadChildSelector(selector) {
+  return gWalker.querySelector(gWalker.rootNode, "#childFrame").then(frame => {
+    gChildFrame = frame;
+    return gWalker.children(frame);
+  }).then(children => {
+    return gWalker.querySelectorAll(children.nodes[0], selector);
+  }).then(nodeList => {
+    return nodeList.items();
+  });
+}
+
+
+function isSrcChange(change) {
+  return (change.type === "attributes" && change.attributeName === "src");
+}
+
+function assertAndStrip(mutations, message, test) {
+  let size = mutations.length;
+  mutations = mutations.filter(test);
+  ok((mutations.size != size), message);
+  return mutations;
+}
+
+function isSrcChange(change) {
+  return change.type === "attributes" && change.attributeName === "src";
+}
+
+function isUnload(change) {
+  return change.type === "documentUnload";
+}
+
+function isFrameLoad(change) {
+  return change.type === "frameLoad";
+}
+
+// Make sure an iframe's src attribute changed and then
+// strip that mutation out of the list.
+function assertSrcChange(mutations) {
+  return assertAndStrip(mutations, "Should have had an iframe source change.", isSrcChange);
+}
+
+// Make sure there's an unload in the mutation list and strip
+// that mutation out of the list
+function assertUnload(mutations) {
+  return assertAndStrip(mutations, "Should have had a document unload change.", isUnload);
+}
+
+// Make sure there's a frame load in the mutation list and strip
+// that mutation out of the list
+function assertFrameLoad(mutations) {
+  return assertAndStrip(mutations, "Should have had a frame load change.", isFrameLoad);
+}
+
+// Load mutations aren't predictable, so keep accumulating mutations until
+// the one we're looking for shows up.
+function waitForMutation(walker, test, mutations=[]) {
+  let deferred = Promise.defer();
+  for (let change of mutations) {
+    if (test(change)) {
+      deferred.resolve(mutations);
+    }
+  }
+
+  walker.once("mutations", newMutations => {
+    waitForMutation(walker, test, mutations.concat(newMutations)).then(finalMutations => {
+      deferred.resolve(finalMutations);
+    })
+  });
+
+  return deferred.promise;
+}
+
+function getUnloadedDoc(mutations) {
+  for (let change of mutations) {
+    if (isUnload(change)) {
+      return change.target;
+    }
+  }
+  return null;
+}
+
+addTest(function loadNewChild() {
+  setup(() => {
+    let beforeUnloadSize = 0;
+    // Load a bunch of fronts for actors inside the child frame.
+    promiseDone(loadChildSelector("#longlist div").then(() => {
+      let childFrame = gInspectee.querySelector("#childFrame");
+      childFrame.src = "data:text/html,<html>new child</html>";
+      return waitForMutation(gWalker, isFrameLoad);
+    }).then(mutations => {
+      let unloaded = getUnloadedDoc(mutations);
+      mutations = assertSrcChange(mutations);
+      mutations = assertUnload(mutations);
+      mutations = assertFrameLoad(mutations);
+
+      is(mutations.length, 0, "Got the expected mutations.");
+
+      assertOwnership();
+
+      return checkMissing(gClient, unloaded);
+    }).then(() => {
+      teardown();
+    }).then(runNextTest));
+  });
+});
+
+addTest(function loadNewChildTwice() {
+  setup(() => {
+    let beforeUnloadSize = 0;
+    // Load a bunch of fronts for actors inside the child frame.
+    promiseDone(loadChildSelector("#longlist div").then(() => {
+      let childFrame = gInspectee.querySelector("#childFrame");
+      childFrame.src = "data:text/html,<html>new child</html>";
+      return waitForMutation(gWalker, isFrameLoad);
+    }).then(mutations => {
+      // The first load went through as expected (as tested in loadNewChild)
+      // Now change the source again, but this time we *don't* expect
+      // an unload, because we haven't seen the new child document yet.
+      let childFrame = gInspectee.querySelector("#childFrame");
+      childFrame.src = "data:text/html,<html>second new child</html>";
+      return waitForMutation(gWalker, isFrameLoad);
+    }).then(mutations => {
+      mutations = assertSrcChange(mutations);
+      mutations = assertFrameLoad(mutations);
+      ok(!getUnloadedDoc(mutations), "Should not have gotten an unload.");
+
+      is(mutations.length, 0, "Got the expected mutations.");
+
+      assertOwnership();
+    }).then(() => {
+      teardown();
+    }).then(runNextTest));
+  });
+});
+
+
+addTest(function loadNewChildTwiceAndCareAboutIt() {
+  setup(() => {
+    let beforeUnloadSize = 0;
+    // Load a bunch of fronts for actors inside the child frame.
+    promiseDone(loadChildSelector("#longlist div").then(() => {
+      let childFrame = gInspectee.querySelector("#childFrame");
+      childFrame.src = "data:text/html,<html>new child</html>";
+      return waitForMutation(gWalker, isFrameLoad);
+    }).then(mutations => {
+      // Read the new child
+      return loadChildSelector("#longlist div");
+    }).then(() => {
+      // Now change the source again, and expect the same results as loadNewChild.
+      let childFrame = gInspectee.querySelector("#childFrame");
+      childFrame.src = "data:text/html,<html>second new child</html>";
+      return waitForMutation(gWalker, isFrameLoad);
+    }).then(mutations => {
+      let unloaded = getUnloadedDoc(mutations);
+
+      mutations = assertSrcChange(mutations);
+      mutations = assertUnload(mutations);
+      mutations = assertFrameLoad(mutations);
+
+      is(mutations.length, 0, "Got the expected mutations.");
+
+      assertOwnership();
+
+      return checkMissing(gClient, unloaded);
+    }).then(() => {
+      teardown();
+    }).then(runNextTest));
+  });
+});
+
+addTest(function testBack() {
+  setup(() => {
+    let beforeUnloadSize = 0;
+    // Load a bunch of fronts for actors inside the child frame.
+    promiseDone(loadChildSelector("#longlist div").then(() => {
+      let childFrame = gInspectee.querySelector("#childFrame");
+      childFrame.src = "data:text/html,<html>new child</html>";
+      return waitForMutation(gWalker, isFrameLoad);
+    }).then(mutations => {
+      // Read the new child
+      return loadChildSelector("#longlist div");
+    }).then(() => {
+      // Now use history.back to change the source, and expect the same results as loadNewChild.
+      let childFrame = gInspectee.querySelector("#childFrame");
+      childFrame.contentWindow.history.back();
+      return waitForMutation(gWalker, isFrameLoad);
+    }).then(mutations => {
+      let unloaded = getUnloadedDoc(mutations);
+      mutations = assertSrcChange(mutations);
+      mutations = assertUnload(mutations);
+      mutations = assertFrameLoad(mutations);
+      is(mutations.length, 0, "Got the expected mutations.");
+
+      assertOwnership();
+
+      return checkMissing(gClient, unloaded);
+    }).then(() => {
+      teardown();
+    }).then(runNextTest));
+  });
+});
+
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>