Merge latest green inbound changeset and mozilla-central
authorEd Morley <emorley@mozilla.com>
Mon, 17 Jun 2013 09:33:03 +0100
changeset 146717 cfb4d238a7fc11a3c6503a15dd00c80063910319
parent 146716 efbe3b2ea841ee0fd2c18b70f7deb4e4432e49b7 (current diff)
parent 146705 19b3d7dc3d7cedee923686eb6e93b860846080f8 (diff)
child 146722 834c8941ae247786babd19c668b8de380041dda5
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)
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
Merge latest green inbound changeset and mozilla-central
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -19,16 +19,17 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 Cu.import("resource:///modules/source-editor.jsm");
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
+Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
   "resource:///modules/devtools/Parser.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
   "resource://gre/modules/devtools/NetworkHelper.jsm");
 
@@ -68,16 +69,34 @@ let DebuggerController = {
     this._isInitialized = true;
     window.removeEventListener("DOMContentLoaded", this.startupDebugger, true);
 
     let deferred = this._startup = Promise.defer();
 
     DebuggerView.initialize(() => {
       DebuggerView._isInitialized = true;
 
+      VariablesViewController.attach(DebuggerView.Variables, {
+        getGripClient: aObject => {
+          return this.activeThread.pauseGrip(aObject);
+        }
+      });
+
+      // Relay events from the VariablesView.
+      DebuggerView.Variables.on("fetched", (aEvent, aType) => {
+        switch (aType) {
+          case "variables":
+            window.dispatchEvent(document, "Debugger:FetchedVariables");
+            break;
+          case "properties":
+            window.dispatchEvent(document, "Debugger:FetchedProperties");
+            break;
+        }
+      });
+
       // Chrome debugging needs to initiate the connection by itself.
       if (window._isChromeDebugger) {
         this.connect().then(deferred.resolve);
       } else {
         deferred.resolve();
       }
     });
 
@@ -398,29 +417,27 @@ ThreadState.prototype = {
     DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state);
 
     if (DebuggerController._target && (aEvent == "paused" || aEvent == "resumed")) {
       DebuggerController._target.emit("thread-" + aEvent);
     }
   }
 };
 
+
 /**
  * Keeps the stack frame list up-to-date, using the thread client's
  * stack frame cache.
  */
 function StackFrames() {
   this._onPaused = this._onPaused.bind(this);
   this._onResumed = this._onResumed.bind(this);
   this._onFrames = this._onFrames.bind(this);
   this._onFramesCleared = this._onFramesCleared.bind(this);
   this._afterFramesCleared = this._afterFramesCleared.bind(this);
-  this._fetchScopeVariables = this._fetchScopeVariables.bind(this);
-  this._fetchVarProperties = this._fetchVarProperties.bind(this);
-  this._addVarExpander = this._addVarExpander.bind(this);
   this.evaluate = this.evaluate.bind(this);
 }
 
 StackFrames.prototype = {
   get activeThread() DebuggerController.activeThread,
   autoScopeExpand: false,
   currentFrame: null,
   syncedWatchExpressions: null,
@@ -583,17 +600,22 @@ StackFrames.prototype = {
 
     // Make sure the debugger view panes are visible.
     DebuggerView.showInstrumentsPane();
 
     // Make sure all the previous stackframes are removed before re-adding them.
     DebuggerView.StackFrames.empty();
 
     for (let frame of this.activeThread.cachedFrames) {
-      this._addFrame(frame);
+      let depth = frame.depth;
+      let { url, line } = frame.where;
+      let frameLocation = NetworkHelper.convertToUnicode(unescape(url));
+      let frameTitle = StackFrameUtils.getFrameTitle(frame);
+
+      DebuggerView.StackFrames.addFrame(frameTitle, frameLocation, line, depth);
     }
     if (this.currentFrame == null) {
       DebuggerView.StackFrames.selectedDepth = 0;
     }
     if (this.activeThread.moreFrames) {
       DebuggerView.StackFrames.dirty = true;
     }
   },
@@ -656,16 +678,17 @@ StackFrames.prototype = {
     DebuggerView.Sources.highlightBreakpoint(url, line);
     // Don't display the watch expressions textbox inputs in the pane.
     DebuggerView.WatchExpressions.toggleContents(false);
     // Start recording any added variables or properties in any scope.
     DebuggerView.Variables.createHierarchy();
     // Clear existing scopes and create each one dynamically.
     DebuggerView.Variables.empty();
 
+
     // If watch expressions evaluation results are available, create a scope
     // to contain all the values.
     if (this.syncedWatchExpressions && watchExpressionsEvaluation) {
       let label = L10N.getStr("watchExpressionsScopeLabel");
       let scope = DebuggerView.Variables.addScope(label);
 
       // Customize the scope for holding watch expressions evaluations.
       scope.descriptorTooltip = false;
@@ -679,80 +702,39 @@ StackFrames.prototype = {
       this._fetchWatchExpressions(scope, watchExpressionsEvaluation);
       scope.expand();
     }
 
     do {
       // Create a scope to contain all the inspected variables.
       let label = StackFrameUtils.getScopeLabel(environment);
       let scope = DebuggerView.Variables.addScope(label);
+      let innermost = environment == frame.environment;
 
-      // Handle additions to the innermost scope.
-      if (environment == frame.environment) {
+      // Handle special additions to the innermost scope.
+      if (innermost) {
         this._insertScopeFrameReferences(scope, frame);
-        this._addScopeExpander(scope, environment);
-        // Always expand the innermost scope by default.
+      }
+
+      DebuggerView.Variables.controller.addExpander(scope, environment);
+
+      // The innermost scope is always automatically expanded, because it
+      // contains the variables in the current stack frame which are likely to
+      // be inspected.
+      if (innermost || this.autoScopeExpand) {
         scope.expand();
       }
-      // Lazily add nodes for every other environment scope.
-      else {
-        this._addScopeExpander(scope, environment);
-        this.autoScopeExpand && scope.expand();
-      }
     } while ((environment = environment.parent));
 
     // Signal that variables have been fetched.
     window.dispatchEvent(document, "Debugger:FetchedVariables");
     DebuggerView.Variables.commitHierarchy();
   },
 
   /**
-   * Adds an 'onexpand' callback for a scope, lazily handling
-   * the addition of new variables.
-   *
-   * @param Scope aScope
-   *        The scope where the variables will be placed into.
-   * @param object aEnv
-   *        The scope's environment.
-   */
-  _addScopeExpander: function(aScope, aEnv) {
-    aScope._sourceEnvironment = aEnv;
-
-    // It's a good idea to be prepared in case of an expansion.
-    aScope.addEventListener("mouseover", this._fetchScopeVariables, false);
-    // Make sure that variables are always available on expansion.
-    aScope.onexpand = this._fetchScopeVariables;
-  },
-
-  /**
-   * Adds an 'onexpand' callback for a variable, lazily handling
-   * the addition of new properties.
-   *
-   * @param Variable aVar
-   *        The variable where the properties will be placed into.
-   * @param any aGrip
-   *        The grip of the variable.
-   */
-  _addVarExpander: function(aVar, aGrip) {
-    // No need for expansion for primitive values.
-    if (VariablesView.isPrimitive({ value: aGrip })) {
-      return;
-    }
-    aVar._sourceGrip = aGrip;
-
-    // Some variables are likely to contain a very large number of properties.
-    // It's a good idea to be prepared in case of an expansion.
-    if (aVar.name == "window" || aVar.name == "this") {
-      aVar.addEventListener("mouseover", this._fetchVarProperties, false);
-    }
-    // Make sure that properties are always available on expansion.
-    aVar.onexpand = this._fetchVarProperties;
-  },
-
-  /**
    * Adds the watch expressions evaluation results to a scope in the view.
    *
    * @param Scope aScope
    *        The scope where the watch expressions will be placed into.
    * @param object aExp
    *        The grip of the evaluation results.
    */
   _fetchWatchExpressions: function(aScope, aExp) {
@@ -765,234 +747,56 @@ StackFrames.prototype = {
     // Add nodes for every watch expression in scope.
     this.activeThread.pauseGrip(aExp).getPrototypeAndProperties((aResponse) => {
       let ownProperties = aResponse.ownProperties;
       let totalExpressions = DebuggerView.WatchExpressions.itemCount;
 
       for (let i = 0; i < totalExpressions; i++) {
         let name = DebuggerView.WatchExpressions.getExpression(i);
         let expVal = ownProperties[i].value;
-        let expRef = aScope.addVar(name, ownProperties[i]);
-        this._addVarExpander(expRef, expVal);
+        let expRef = aScope.addItem(name, ownProperties[i]);
+        DebuggerView.Variables.controller.addExpander(expRef, expVal);
 
         // Revert some of the custom watch expressions scope presentation flags.
         expRef.switch = null;
         expRef.delete = null;
         expRef.descriptorTooltip = true;
         expRef.separatorStr = L10N.getStr("variablesSeparatorLabel");
       }
 
       // Signal that watch expressions have been fetched.
       window.dispatchEvent(document, "Debugger:FetchedWatchExpressions");
       DebuggerView.Variables.commitHierarchy();
     });
   },
 
   /**
-   * Adds variables to a scope in the view. Triggered when a scope is
-   * expanded or is hovered. It does not expand the scope.
-   *
-   * @param Scope aScope
-   *        The scope where the variables will be placed into.
-   */
-  _fetchScopeVariables: function(aScope) {
-    // Fetch the variables only once.
-    if (aScope._fetched) {
-      return;
-    }
-    aScope._fetched = true;
-    let env = aScope._sourceEnvironment;
-
-    switch (env.type) {
-      case "with":
-      case "object":
-        // Add nodes for every variable in scope.
-        this.activeThread.pauseGrip(env.object).getPrototypeAndProperties((aResponse) => {
-          let { ownProperties, safeGetterValues } = aResponse;
-          this._mergeSafeGetterValues(ownProperties, safeGetterValues);
-          this._insertScopeVariables(ownProperties, aScope);
-
-          // Signal that variables have been fetched.
-          window.dispatchEvent(document, "Debugger:FetchedVariables");
-          DebuggerView.Variables.commitHierarchy();
-        });
-        break;
-      case "block":
-      case "function":
-        // Add nodes for every argument and every other variable in scope.
-        this._insertScopeArguments(env.bindings.arguments, aScope);
-        this._insertScopeVariables(env.bindings.variables, aScope);
-
-        // No need to signal that variables have been fetched, since
-        // the scope arguments and variables are already attached to the
-        // environment bindings, so pausing the active thread is unnecessary.
-        break;
-      default:
-        Cu.reportError("Unknown Debugger.Environment type: " + env.type);
-        break;
-    }
-  },
-
-  /**
    * Add nodes for special frame references in the innermost scope.
    *
    * @param Scope aScope
    *        The scope where the references will be placed into.
    * @param object aFrame
    *        The frame to get some references from.
    */
   _insertScopeFrameReferences: function(aScope, aFrame) {
     // Add any thrown exception.
     if (this.currentException) {
-      let excRef = aScope.addVar("<exception>", { value: this.currentException });
-      this._addVarExpander(excRef, this.currentException);
+      let excRef = aScope.addItem("<exception>", { value: this.currentException });
+      DebuggerView.Variables.controller.addExpander(excRef, this.currentException);
     }
     // Add any returned value.
     if (this.currentReturnedValue) {
-      let retRef = aScope.addVar("<return>", { value: this.currentReturnedValue });
-      this._addVarExpander(retRef, this.currentReturnedValue);
+      let retRef = aScope.addItem("<return>", { value: this.currentReturnedValue });
+      DebuggerView.Variables.controller.addExpander(retRef, this.currentReturnedValue);
     }
     // Add "this".
     if (aFrame.this) {
-      let thisRef = aScope.addVar("this", { value: aFrame.this });
-      this._addVarExpander(thisRef, aFrame.this);
-    }
-  },
-
-  /**
-   * Add nodes for every argument in scope.
-   *
-   * @param object aArguments
-   *        The map of names to arguments, as specified in the protocol.
-   * @param Scope aScope
-   *        The scope where the nodes will be placed into.
-   */
-  _insertScopeArguments: function(aArguments, aScope) {
-    if (!aArguments) {
-      return;
-    }
-    for (let argument of aArguments) {
-      let name = Object.getOwnPropertyNames(argument)[0];
-      let argRef = aScope.addVar(name, argument[name]);
-      let argVal = argument[name].value;
-      this._addVarExpander(argRef, argVal);
-    }
-  },
-
-  /**
-   * Add nodes for every variable in scope.
-   *
-   * @param object aVariables
-   *        The map of names to variables, as specified in the protocol.
-   * @param Scope aScope
-   *        The scope where the nodes will be placed into.
-   */
-  _insertScopeVariables: function(aVariables, aScope) {
-    if (!aVariables) {
-      return;
-    }
-    let variableNames = Object.keys(aVariables);
-
-    // Sort all of the variables before adding them, if preferred.
-    if (Prefs.variablesSortingEnabled) {
-      variableNames.sort();
-    }
-    // Add the variables to the specified scope.
-    for (let name of variableNames) {
-      let varRef = aScope.addVar(name, aVariables[name]);
-      let varVal = aVariables[name].value;
-      this._addVarExpander(varRef, varVal);
-    }
-  },
-
-  /**
-   * Adds properties to a variable in the view. Triggered when a variable is
-   * expanded or certain variables are hovered. It does not expand the variable.
-   *
-   * @param Variable aVar
-   *        The variable where the properties will be placed into.
-   */
-  _fetchVarProperties: function(aVar) {
-    // Fetch the properties only once.
-    if (aVar._fetched) {
-      return;
+      let thisRef = aScope.addItem("this", { value: aFrame.this });
+      DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this);
     }
-    aVar._fetched = true;
-    let grip = aVar._sourceGrip;
-
-    this.activeThread.pauseGrip(grip).getPrototypeAndProperties((aResponse) => {
-      let { ownProperties, prototype, safeGetterValues } = aResponse;
-      let sortable = VariablesView.NON_SORTABLE_CLASSES.indexOf(grip.class) == -1;
-
-      this._mergeSafeGetterValues(ownProperties, safeGetterValues);
-
-      // Add all the variable properties.
-      if (ownProperties) {
-        aVar.addProperties(ownProperties, {
-          // Not all variables need to force sorted properties.
-          sorted: sortable,
-          // Expansion handlers must be set after the properties are added.
-          callback: this._addVarExpander
-        });
-      }
-
-      // Add the variable's __proto__.
-      if (prototype && prototype.type != "null") {
-        aVar.addProperty("__proto__", { value: prototype });
-        // Expansion handlers must be set after the properties are added.
-        this._addVarExpander(aVar.get("__proto__"), prototype);
-      }
-
-      // Mark the variable as having retrieved all its properties.
-      aVar._retrieved = true;
-
-      // Signal that properties have been fetched.
-      window.dispatchEvent(document, "Debugger:FetchedProperties");
-      DebuggerView.Variables.commitHierarchy();
-    });
-  },
-
-  /**
-   * Merge the safe getter values descriptors into the "own properties" object
-   * that comes from a "prototypeAndProperties" response packet. This is needed
-   * for Variables View.
-   *
-   * @private
-   * @param object aOwnProperties
-   *        The |ownProperties| object that will get the new safe getter values.
-   * @param object aSafeGetterValues
-   *        The |safeGetterValues| object.
-   */
-  _mergeSafeGetterValues: function(aOwnProperties, aSafeGetterValues) {
-    // Merge the safe getter values into one object such that we can use it
-    // in VariablesView.
-    for (let name of Object.keys(aSafeGetterValues)) {
-      if (name in aOwnProperties) {
-        aOwnProperties[name].getterValue = aSafeGetterValues[name].getterValue;
-        aOwnProperties[name].getterPrototypeLevel =
-          aSafeGetterValues[name].getterPrototypeLevel;
-      } else {
-        aOwnProperties[name] = aSafeGetterValues[name];
-      }
-    }
-  },
-
-  /**
-   * Adds the specified stack frame to the list.
-   *
-   * @param object aFrame
-   *        The new frame to add.
-   */
-  _addFrame: function(aFrame) {
-    let depth = aFrame.depth;
-    let { url, line } = aFrame.where;
-    let frameLocation = NetworkHelper.convertToUnicode(unescape(url));
-    let frameTitle = StackFrameUtils.getFrameTitle(aFrame);
-
-    DebuggerView.StackFrames.addFrame(frameTitle, frameLocation, line, depth);
   },
 
   /**
    * Loads more stack frames from the debugger server cache.
    */
   addMoreFrames: function() {
     this.activeThread.fillFrames(
       this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE);
--- a/browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug786070_hide_nonenums.js
@@ -19,19 +19,19 @@ function test() {
   });
 }
 
 function testNonEnumProperties() {
   gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let testScope = gDebugger.DebuggerView.Variables.addScope("test-scope");
-      let testVar = testScope.addVar("foo");
+      let testVar = testScope.addItem("foo");
 
-      testVar.addProperties({
+      testVar.addItems({
         foo: {
           value: "bar",
           enumerable: true
         },
         bar: {
           value: "foo",
           enumerable: false
         }
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-03.js
@@ -19,18 +19,18 @@ function test() {
   });
 }
 
 function testSimpleCall() {
   gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let testScope = gDebugger.DebuggerView.Variables.addScope("test-scope");
-      let testVar = testScope.addVar("something");
-      let duplVar = testScope.addVar("something");
+      let testVar = testScope.addItem("something");
+      let duplVar = testScope.addItem("something");
 
       info("Scope id: " + testScope.target.id);
       info("Scope name: " + testScope.target.name);
       info("Variable id: " + testVar.target.id);
       info("Variable name: " + testVar.target.name);
 
       ok(testScope,
         "Should have created a scope.");
@@ -56,18 +56,18 @@ function testSimpleCall() {
 
       is(testVar.target.querySelector(".name").getAttribute("value"), "something",
         "Any new variable should have the designated title.");
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 0,
         "Any new variable should have a details container with no child nodes.");
 
 
-      let properties = testVar.addProperties({ "child": { "value": { "type": "object",
-                                                                     "class": "Object" } } });
+      let properties = testVar.addItems({ "child": { "value": { "type": "object",
+                                                                "class": "Object" } } });
 
 
       ok(!testVar.expanded,
         "Any new created variable should be initially collapsed.");
 
       ok(testVar.visible,
         "Any new created variable should be initially visible.");
 
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-04.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-04.js
@@ -19,36 +19,36 @@ function test() {
   });
 }
 
 function testSimpleCall() {
   gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let testScope = gDebugger.DebuggerView.Variables.addScope("test");
-      let testVar = testScope.addVar("something");
+      let testVar = testScope.addItem("something");
 
-      let properties = testVar.addProperties({
+      let properties = testVar.addItems({
         "child": {
           "value": {
             "type": "object",
             "class": "Object"
           },
           "enumerable": true
         }
       });
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1,
         "A new detail node should have been added in the variable tree.");
 
       ok(testVar.get("child"),
         "The added detail property should be accessible from the variable.");
 
 
-      let properties2 = testVar.get("child").addProperties({
+      let properties2 = testVar.get("child").addItems({
         "grandchild": {
           "value": {
             "type": "object",
             "class": "Object"
           },
           "enumerable": true
         }
       });
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-05.js
@@ -19,17 +19,17 @@ function test() {
   });
 }
 
 function testSimpleCall() {
   gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let testScope = gDebugger.DebuggerView.Variables.addScope("test");
-      let testVar = testScope.addVar("something");
+      let testVar = testScope.addItem("something");
 
       testVar.setGrip(1.618);
 
       is(testVar.target.querySelector(".value").getAttribute("value"), "1.618",
         "The grip information for the variable wasn't set correctly.");
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 0,
         "Adding a value property shouldn't add any new tree nodes.");
@@ -39,42 +39,42 @@ function testSimpleCall() {
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 0,
         "Adding type and class properties shouldn't add any new tree nodes.");
 
       is(testVar.target.querySelector(".value").getAttribute("value"), "[object Window]",
         "The information for the variable wasn't set correctly.");
 
 
-      testVar.addProperties({ "helloWorld": { "value": "hello world", "enumerable": true } });
+      testVar.addItems({ "helloWorld": { "value": "hello world", "enumerable": true } });
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1,
         "A new detail node should have been added in the variable tree.");
 
 
-      testVar.addProperties({ "helloWorld": { "value": "hello jupiter", "enumerable": true } });
+      testVar.addItems({ "helloWorld": { "value": "hello jupiter", "enumerable": true } });
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 1,
         "Shouldn't be able to duplicate nodes added in the variable tree.");
 
 
-      testVar.addProperties({ "someProp0": { "value": "random string", "enumerable": true },
-                              "someProp1": { "value": "another string", "enumerable": true } });
+      testVar.addItems({ "someProp0": { "value": "random string", "enumerable": true },
+                         "someProp1": { "value": "another string", "enumerable": true } });
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 3,
         "Two new detail nodes should have been added in the variable tree.");
 
 
-      testVar.addProperties({ "someProp2": { "value": { "type": "null" }, "enumerable": true },
-                              "someProp3": { "value": { "type": "undefined" }, "enumerable": true },
-                              "someProp4": {
-                                "value": { "type": "object", "class": "Object" },
-                                "enumerable": true
-                              }
-                            });
+      testVar.addItems({ "someProp2": { "value": { "type": "null" }, "enumerable": true },
+                         "someProp3": { "value": { "type": "undefined" }, "enumerable": true },
+                         "someProp4": {
+                           "value": { "type": "object", "class": "Object" },
+                           "enumerable": true
+                         }
+                       });
 
       is(testVar.target.querySelector(".variables-view-element-details").childNodes.length, 6,
         "Three new detail nodes should have been added in the variable tree.");
 
 
       gDebugger.DebuggerController.activeThread.resume(function() {
         closeDebuggerAndFinish();
       });
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-06.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-06.js
@@ -21,63 +21,63 @@ function test() {
 
 function testSimpleCall() {
   gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
     Services.tm.currentThread.dispatch({ run: function() {
 
       let globalScope = gDebugger.DebuggerView.Variables.addScope("Test-Global");
       let localScope = gDebugger.DebuggerView.Variables.addScope("Test-Local");
 
-      let windowVar = globalScope.addVar("window");
-      let documentVar = globalScope.addVar("document");
-      let localVar0 = localScope.addVar("localVariable");
-      let localVar1 = localScope.addVar("localVar1");
-      let localVar2 = localScope.addVar("localVar2");
-      let localVar3 = localScope.addVar("localVar3");
-      let localVar4 = localScope.addVar("localVar4");
-      let localVar5 = localScope.addVar("localVar5");
+      let windowVar = globalScope.addItem("window");
+      let documentVar = globalScope.addItem("document");
+      let localVar0 = localScope.addItem("localVariable");
+      let localVar1 = localScope.addItem("localVar1");
+      let localVar2 = localScope.addItem("localVar2");
+      let localVar3 = localScope.addItem("localVar3");
+      let localVar4 = localScope.addItem("localVar4");
+      let localVar5 = localScope.addItem("localVar5");
 
       localVar0.setGrip(42);
       localVar1.setGrip(true);
       localVar2.setGrip("nasu");
 
       localVar3.setGrip({ "type": "undefined" });
       localVar4.setGrip({ "type": "null" });
       localVar5.setGrip({ "type": "object", "class": "Object" });
 
-      localVar5.addProperties({ "someProp0": { "value": 42, "enumerable": true },
-                                "someProp1": { "value": true , "enumerable": true},
-                                "someProp2": { "value": "nasu", "enumerable": true},
-                                "someProp3": { "value": { "type": "undefined" }, "enumerable": true},
-                                "someProp4": { "value": { "type": "null" }, "enumerable": true },
-                                "someProp5": {
-                                  "value": { "type": "object", "class": "Object" },
-                                  "enumerable": true
-                                }
-                              });
+      localVar5.addItems({ "someProp0": { "value": 42, "enumerable": true },
+                           "someProp1": { "value": true , "enumerable": true},
+                           "someProp2": { "value": "nasu", "enumerable": true},
+                           "someProp3": { "value": { "type": "undefined" }, "enumerable": true},
+                           "someProp4": { "value": { "type": "null" }, "enumerable": true },
+                           "someProp5": {
+                             "value": { "type": "object", "class": "Object" },
+                             "enumerable": true
+                           }
+                         });
 
-      localVar5.get("someProp5").addProperties({ "someProp0": { "value": 42, "enumerable": true },
-                                                 "someProp1": { "value": true, "enumerable": true },
-                                                 "someProp2": { "value": "nasu", "enumerable": true },
-                                                 "someProp3": { "value": { "type": "undefined" }, "enumerable": true },
-                                                 "someProp4": { "value": { "type": "null" }, "enumerable": true },
-                                                 "someAccessor": { "get": { "type": "object", "class": "Function" },
-                                                                   "set": { "type": "undefined" },
-                                                                   "enumerable": true } });
+      localVar5.get("someProp5").addItems({ "someProp0": { "value": 42, "enumerable": true },
+                                            "someProp1": { "value": true, "enumerable": true },
+                                            "someProp2": { "value": "nasu", "enumerable": true },
+                                            "someProp3": { "value": { "type": "undefined" }, "enumerable": true },
+                                            "someProp4": { "value": { "type": "null" }, "enumerable": true },
+                                            "someAccessor": { "get": { "type": "object", "class": "Function" },
+                                                              "set": { "type": "undefined" }, "enumerable": true }
+                                          });
 
       windowVar.setGrip({ "type": "object", "class": "Window" });
-      windowVar.addProperties({ "helloWorld": { "value": "hello world" } });
+      windowVar.addItems({ "helloWorld": { "value": "hello world" } });
 
       documentVar.setGrip({ "type": "object", "class": "HTMLDocument" });
-      documentVar.addProperties({ "onload": { "value": { "type": "null" } },
-                                  "onunload": { "value": { "type": "null" } },
-                                  "onfocus": { "value": { "type": "null" } },
-                                  "onblur": { "value": { "type": "null" } },
-                                  "onclick": { "value": { "type": "null" } },
-                                  "onkeypress": { "value": { "type": "null" } } });
+      documentVar.addItems({ "onload": { "value": { "type": "null" } },
+                             "onunload": { "value": { "type": "null" } },
+                             "onfocus": { "value": { "type": "null" } },
+                             "onblur": { "value": { "type": "null" } },
+                             "onclick": { "value": { "type": "null" } },
+                             "onkeypress": { "value": { "type": "null" } } });
 
 
       ok(windowVar, "The windowVar hasn't been created correctly.");
       ok(documentVar, "The documentVar hasn't been created correctly.");
       ok(localVar0, "The localVar0 hasn't been created correctly.");
       ok(localVar1, "The localVar1 hasn't been created correctly.");
       ok(localVar2, "The localVar2 hasn't been created correctly.");
       ok(localVar3, "The localVar3 hasn't been created correctly.");
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-data.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-data.js
@@ -70,21 +70,21 @@ function testVariablesView()
   testHierarchy();
   testHeader();
   testFirstLevelContents();
   testSecondLevelContents();
   testThirdLevelContents();
   testIntegrity(arr, obj);
 
   let fooScope = gVariablesView.addScope("foo");
-  let anonymousVar = fooScope.addVar();
+  let anonymousVar = fooScope.addItem();
 
   let anonymousScope = gVariablesView.addScope();
-  let barVar = anonymousScope.addVar("bar");
-  let bazProperty = barVar.addProperty("baz");
+  let barVar = anonymousScope.addItem("bar");
+  let bazProperty = barVar.addItem("baz");
 
   testAnonymousHeaders(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);
   testPropertyInheritance(fooScope, anonymousVar, anonymousScope, barVar, bazProperty);
 
   executeSoon(function() {
     testKeyboardAccessibility(function() {
       testClearHierarchy();
 
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js
@@ -105,18 +105,19 @@ function testVariablesFiltering()
 
   function test3()
   {
     backspace(3);
 
     is(gSearchBox.value, "*",
       "Searchbox value is incorrect after 3 backspaces");
 
-    is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 3,
-      "There should be 3 variables displayed in the inner scope");
+    // variable count includes `__proto__` for object scopes
+    is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 4,
+      "There should be 4 variables displayed in the inner scope");
     isnot(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the math scope");
     isnot(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the test scope");
     isnot(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the load scope");
     isnot(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the global scope");
@@ -135,18 +136,19 @@ function testVariablesFiltering()
 
   function test4()
   {
     backspace(1);
 
     is(gSearchBox.value, "",
       "Searchbox value is incorrect after 1 backspace");
 
-    is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 3,
-      "There should be 3 variables displayed in the inner scope");
+    // variable count includes `__proto__` for object scopes
+    is(innerScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 4,
+      "There should be 4 variables displayed in the inner scope");
     isnot(mathScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the math scope");
     isnot(testScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the test scope");
     isnot(loadScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the load scope");
     isnot(globalScope.querySelectorAll(".variables-view-variable:not([non-match])").length, 0,
       "There should be some variables displayed in the global scope");
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -1441,17 +1441,17 @@ NetworkDetailsView.prototype = {
   _addHeaders: function(aName, aResponse) {
     let kb = aResponse.headersSize / 1024;
     let size = L10N.numberWithDecimals(kb, HEADERS_SIZE_DECIMALS);
     let text = L10N.getFormatStr("networkMenu.sizeKB", size);
     let headersScope = this._headers.addScope(aName + " (" + text + ")");
     headersScope.expanded = true;
 
     for (let header of aResponse.headers) {
-      let headerVar = headersScope.addVar(header.name, { null: true }, true);
+      let headerVar = headersScope.addItem(header.name, { null: true }, true);
       gNetwork.getString(header.value).then((aString) => headerVar.setGrip(aString));
     }
   },
 
   /**
    * Sets the network request cookies shown in this view.
    *
    * @param object aResponse
@@ -1484,17 +1484,17 @@ NetworkDetailsView.prototype = {
    * @param object aResponse
    *        The message received from the server.
    */
   _addCookies: function(aName, aResponse) {
     let cookiesScope = this._cookies.addScope(aName);
     cookiesScope.expanded = true;
 
     for (let cookie of aResponse.cookies) {
-      let cookieVar = cookiesScope.addVar(cookie.name, { null: true }, true);
+      let cookieVar = cookiesScope.addItem(cookie.name, { null: true }, true);
       gNetwork.getString(cookie.value).then((aString) => cookieVar.setGrip(aString));
 
       // By default the cookie name and value are shown. If this is the only
       // information available, then nothing else is to be displayed.
       let cookieProps = Object.keys(cookie);
       if (cookieProps.length == 2) {
         continue;
       }
@@ -1586,17 +1586,17 @@ NetworkDetailsView.prototype = {
         name: NetworkHelper.convertToUnicode(unescape(param[0])),
         value: NetworkHelper.convertToUnicode(unescape(param[1]))
       });
 
     let paramsScope = this._params.addScope(aName);
     paramsScope.expanded = true;
 
     for (let param of paramsArray) {
-      let headerVar = paramsScope.addVar(param.name, { null: true }, true);
+      let headerVar = paramsScope.addItem(param.name, { null: true }, true);
       headerVar.setGrip(param.value);
     }
   },
 
   /**
    * Sets the network response body shown in this view.
    *
    * @param string aUrl
@@ -1629,17 +1629,17 @@ NetworkDetailsView.prototype = {
         // Valid JSON.
         if (jsonObject) {
           $("#response-content-json-box").hidden = false;
           let jsonScopeName = callbackPadding
             ? L10N.getFormatStr("jsonpScopeName", callbackPadding[0].slice(0, -1))
             : L10N.getStr("jsonScopeName");
 
           let jsonScope = this._json.addScope(jsonScopeName);
-          jsonScope.addVar().populate(jsonObject, { expanded: true });
+          jsonScope.addItem().populate(jsonObject, { expanded: true });
           jsonScope.expanded = true;
         }
         // Malformed JSON.
         else {
           $("#response-content-textarea-box").hidden = false;
           NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
             aEditor.setMode(SourceEditor.MODES.JAVASCRIPT);
             aEditor.setText(aString);
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -15,16 +15,18 @@ const LAZY_APPEND_DELAY = 100; // ms
 const LAZY_APPEND_BATCH = 100; // nodes
 const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
 const PAGE_SIZE_MAX_JUMPS = 30;
 const SEARCH_ACTION_MAX_DELAY = 300; // ms
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper",
   "resource://gre/modules/devtools/NetworkHelper.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
   "resource://gre/modules/devtools/WebConsoleUtils.jsm");
 
 this.EXPORTED_SYMBOLS = ["VariablesView"];
@@ -69,29 +71,31 @@ this.VariablesView = function VariablesV
   this._list.setAttribute("orient", "vertical");
   this._list.addEventListener("keypress", this._onViewKeyPress, false);
   this._parent.appendChild(this._list);
   this._boxObject = this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
 
   for (let name in aFlags) {
     this[name] = aFlags[name];
   }
+
+  EventEmitter.decorate(this);
 };
 
 VariablesView.prototype = {
   /**
    * Helper setter for populating this container with a raw object.
    *
    * @param object aData
    *        The raw object to display. You can only provide this object
    *        if you want the variables view to work in sync mode.
    */
   set rawObject(aObject) {
     this.empty();
-    this.addScope().addVar().populate(aObject);
+    this.addScope().addItem().populate(aObject);
   },
 
   /**
    * Adds a scope to contain any inspected variables.
    *
    * @param string aName
    *        The scope's name (e.g. "Local", "Global" etc.).
    * @return Scope
@@ -176,16 +180,21 @@ VariablesView.prototype = {
       if (!this._store.length) {
         this._appendEmptyNotice();
         this._toggleSearchVisibility(false);
       }
     }, aTimeout);
   },
 
   /**
+   * The controller for this VariablesView, if it has one.
+   */
+  controller: null,
+
+  /**
    * The amount of time (in milliseconds) it takes to empty this view lazily.
    */
   lazyEmptyDelay: LAZY_EMPTY_DELAY,
 
   /**
    * Specifies if this view may be emptied lazily.
    * @see VariablesView.prototype.empty
    */
@@ -582,17 +591,18 @@ VariablesView.prototype = {
    *
    * @param nsIDOMNode aNode
    *        The node to search for.
    * @return Scope
    *         The matched scope, or null if nothing is found.
    */
   getScopeForNode: function(aNode) {
     let item = this._itemsByElement.get(aNode);
-    if (item && !(item instanceof Variable) && !(item instanceof Property)) {
+    // Match only Scopes, not Variables or Properties.
+    if (item && !(item instanceof Variable)) {
       return item;
     }
     return null;
   },
 
   /**
    * Recursively searches this container for the scope, variable or property
    * displayed by the specified node.
@@ -785,32 +795,30 @@ VariablesView.prototype = {
         return;
 
       case e.DOM_VK_END:
         this.focusLastVisibleItem();
         return;
 
       case e.DOM_VK_RETURN:
       case e.DOM_VK_ENTER:
-        // Start editing the value or name of the variable or property.
-        if (item instanceof Variable ||
-            item instanceof Property) {
+        // Start editing the value or name of the Variable or Property.
+        if (item instanceof Variable) {
           if (e.metaKey || e.altKey || e.shiftKey) {
             item._activateNameInput();
           } else {
             item._activateValueInput();
           }
         }
         return;
 
       case e.DOM_VK_DELETE:
       case e.DOM_VK_BACK_SPACE:
-        // Delete the variable or property if allowed.
-        if (item instanceof Variable ||
-            item instanceof Property) {
+        // Delete the Variable or Property if allowed.
+        if (item instanceof Variable) {
           item._onDelete(e);
         }
         return;
     }
   },
 
   /**
    * The number of elements in this container to jump when Page Up or Page Down
@@ -897,91 +905,108 @@ VariablesView.prototype = {
   _emptyTextNode: null,
   _emptyTextValue: ""
 };
 
 VariablesView.NON_SORTABLE_CLASSES = [
   "Array",
   "Int8Array",
   "Uint8Array",
+  "Uint8ClampedArray",
   "Int16Array",
   "Uint16Array",
   "Int32Array",
   "Uint32Array",
   "Float32Array",
   "Float64Array"
 ];
 
 /**
+ * Determine whether an object's properties should be sorted based on its class.
+ *
+ * @param string aClassName
+ *        The class of the object.
+ */
+VariablesView.isSortable = function(aClassName) {
+  return VariablesView.NON_SORTABLE_CLASSES.indexOf(aClassName) == -1;
+};
+
+/**
  * Generates the string evaluated when performing simple value changes.
  *
  * @param Variable | Property aItem
  *        The current variable or property.
  * @param string aCurrentString
  *        The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ *        Prefix for the symbolic name.
  * @return string
  *         The string to be evaluated.
  */
-VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString) {
-  return aItem._symbolicName + "=" + aCurrentString;
+VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
+  return aPrefix + aItem._symbolicName + "=" + aCurrentString;
 };
 
 /**
  * Generates the string evaluated when overriding getters and setters with
  * plain values.
  *
  * @param Property aItem
  *        The current getter or setter property.
  * @param string aCurrentString
  *        The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ *        Prefix for the symbolic name.
  * @return string
  *         The string to be evaluated.
  */
-VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString) {
+VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
   let property = "\"" + aItem._nameString + "\"";
-  let parent = aItem.ownerView._symbolicName || "this";
+  let parent = aPrefix + aItem.ownerView._symbolicName || "this";
 
   return "Object.defineProperty(" + parent + "," + property + "," +
     "{ value: " + aCurrentString +
     ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
     ", configurable: true" +
     ", writable: true" +
     "})";
 };
 
 /**
  * Generates the string evaluated when performing getters and setters changes.
  *
  * @param Property aItem
  *        The current getter or setter property.
  * @param string aCurrentString
  *        The trimmed user inputted string.
+ * @param string aPrefix [optional]
+ *        Prefix for the symbolic name.
  * @return string
  *         The string to be evaluated.
  */
-VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString) {
+VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString, aPrefix = "") {
   let type = aItem._nameString;
   let propertyObject = aItem.ownerView;
   let parentObject = propertyObject.ownerView;
   let property = "\"" + propertyObject._nameString + "\"";
-  let parent = parentObject._symbolicName || "this";
+  let parent = aPrefix + parentObject._symbolicName || "this";
 
   switch (aCurrentString) {
     case "":
     case "null":
     case "undefined":
       let mirrorType = type == "get" ? "set" : "get";
       let mirrorLookup = type == "get" ? "__lookupSetter__" : "__lookupGetter__";
 
       // If the parent object will end up without any getter or setter,
       // morph it into a plain value.
       if ((type == "set" && propertyObject.getter.type == "undefined") ||
           (type == "get" && propertyObject.setter.type == "undefined")) {
         // Make sure the right getter/setter to value override macro is applied to the target object.
-        return propertyObject.evaluationMacro(propertyObject, "undefined");
+        return propertyObject.evaluationMacro(propertyObject, "undefined", aPrefix);
       }
 
       // Construct and return the getter/setter removal evaluation string.
       // e.g: Object.defineProperty(foo, "bar", {
       //   get: foo.__lookupGetter__("bar"),
       //   set: undefined,
       //   enumerable: true,
       //   configurable: true
@@ -990,26 +1015,26 @@ VariablesView.getterOrSetterEvalMacro = 
         "{" + mirrorType + ":" + parent + "." + mirrorLookup + "(" + property + ")" +
         "," + type + ":" + undefined +
         ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" +
         ", configurable: true" +
         "})";
 
     default:
       // Wrap statements inside a function declaration if not already wrapped.
-      if (aCurrentString.indexOf("function") != 0) {
+      if (!aCurrentString.startsWith("function")) {
         let header = "function(" + (type == "set" ? "value" : "") + ")";
         let body = "";
         // If there's a return statement explicitly written, always use the
         // standard function definition syntax
-        if (aCurrentString.indexOf("return ") != -1) {
+        if (aCurrentString.contains("return ")) {
           body = "{" + aCurrentString + "}";
         }
         // If block syntax is used, use the whole string as the function body.
-        else if (aCurrentString.indexOf("{") == 0) {
+        else if (aCurrentString.startsWith("{")) {
           body = aCurrentString;
         }
         // Prefer an expression closure.
         else {
           body = "(" + aCurrentString + ")";
         }
         aCurrentString = header + body;
       }
@@ -1037,16 +1062,17 @@ VariablesView.getterOrSetterDeleteCallba
 
   // Make sure the right getter/setter to value override macro is applied
   // to the target object.
   aItem.ownerView.eval(aItem.evaluationMacro(aItem, ""));
 
   return true; // Don't hide the element.
 };
 
+
 /**
  * A Scope is an object holding Variable instances.
  * Iterable via "for (let [name, variable] in instance) { }".
  *
  * @param VariablesView aView
  *        The view to contain this scope.
  * @param string aName
  *        The scope's name.
@@ -1078,48 +1104,106 @@ function Scope(aView, aName, aFlags = {}
   this._store = new Map();
   this._enumItems = [];
   this._nonEnumItems = [];
   this._init(aName.trim(), aFlags);
 }
 
 Scope.prototype = {
   /**
-   * Adds a variable to contain any inspected properties.
+   * Whether this Scope should be prefetched when it is remoted.
+   */
+  shouldPrefetch: true,
+
+  /**
+   * Create a new Variable that is a child of this Scope.
    *
    * @param string aName
-   *        The variable's name.
+   *        The name of the new Property.
    * @param object aDescriptor
-   *        Specifies the value and/or type & class of the variable,
+   *        The variable's descriptor.
+   * @return Variable
+   *         The newly created child Variable.
+   */
+  _createChild: function(aName, aDescriptor) {
+    return new Variable(this, aName, aDescriptor);
+  },
+
+  /**
+   * Adds a child to contain any inspected properties.
+   *
+   * @param string aName
+   *        The child's name.
+   * @param object aDescriptor
+   *        Specifies the value and/or type & class of the child,
    *        or 'get' & 'set' accessor properties. If the type is implicit,
    *        it will be inferred from the value.
    *        e.g. - { value: 42 }
    *             - { value: true }
    *             - { value: "nasu" }
    *             - { value: { type: "undefined" } }
    *             - { value: { type: "null" } }
    *             - { value: { type: "object", class: "Object" } }
    *             - { get: { type: "object", class: "Function" },
    *                 set: { type: "undefined" } }
    * @param boolean aRelaxed
    *        True if name duplicates should be allowed.
    * @return Variable
    *         The newly created Variable instance, null if it already exists.
    */
-  addVar: function(aName = "", aDescriptor = {}, aRelaxed = false) {
+  addItem: function(aName = "", aDescriptor = {}, aRelaxed = false) {
     if (this._store.has(aName) && !aRelaxed) {
       return null;
     }
 
-    let variable = new Variable(this, aName, aDescriptor);
-    this._store.set(aName, variable);
-    this._variablesView._itemsByElement.set(variable._target, variable);
-    this._variablesView._currHierarchy.set(variable._absoluteName, variable);
-    variable.header = !!aName;
-    return variable;
+    let child = this._createChild(aName, aDescriptor);
+    this._store.set(aName, child);
+    this._variablesView._itemsByElement.set(child._target, child);
+    this._variablesView._currHierarchy.set(child._absoluteName, child);
+    child.header = !!aName;
+    return child;
+  },
+
+  /**
+   * Adds items for this variable.
+   *
+   * @param object aItems
+   *        An object containing some { name: descriptor } data properties,
+   *        specifying the value and/or type & class of the variable,
+   *        or 'get' & 'set' accessor properties. If the type is implicit,
+   *        it will be inferred from the value.
+   *        e.g. - { someProp0: { value: 42 },
+   *                 someProp1: { value: true },
+   *                 someProp2: { value: "nasu" },
+   *                 someProp3: { value: { type: "undefined" } },
+   *                 someProp4: { value: { type: "null" } },
+   *                 someProp5: { value: { type: "object", class: "Object" } },
+   *                 someProp6: { get: { type: "object", class: "Function" },
+   *                              set: { type: "undefined" } } }
+   * @param object aOptions [optional]
+   *        Additional options for adding the properties. Supported options:
+   *        - sorted: true to sort all the properties before adding them
+   *        - callback: function invoked after each item is added
+   */
+  addItems: function(aItems, aOptions = {}) {
+    let names = Object.keys(aItems);
+
+    // Sort all of the properties before adding them, if preferred.
+    if (aOptions.sorted) {
+      names.sort();
+    }
+    // Add the properties to the current scope.
+    for (let name of names) {
+      let descriptor = aItems[name];
+      let item = this.addItem(name, descriptor);
+
+      if (aOptions.callback) {
+        aOptions.callback(item, descriptor.value);
+      }
+    }
   },
 
   /**
    * Gets the variable in this container having the specified name.
    *
    * @param string aName
    *        The name of the variable to get.
    * @return Variable
@@ -1174,21 +1258,23 @@ Scope.prototype = {
    *        The parent to check.
    * @return boolean
    *         True if the specified item is a descendant, false otherwise.
    */
   isDescendantOf: function(aParent) {
     if (this.isChildOf(aParent)) {
       return true;
     }
-    if (this.ownerView instanceof Scope ||
-        this.ownerView instanceof Variable ||
-        this.ownerView instanceof Property) {
+
+    // Recurse to parent if it is a Scope, Variable, or Property.
+    if (this.ownerView instanceof Scope) {
       return this.ownerView.isDescendantOf(aParent);
     }
+
+    return false;
   },
 
   /**
    * Shows the scope.
    */
   show: function() {
     this._target.hidden = false;
     this._isContentVisible = true;
@@ -1400,20 +1486,19 @@ Scope.prototype = {
     if (!this._nameString ||
         !this._isContentVisible ||
         !this._isHeaderVisible ||
         !this._isMatch) {
       return false;
     }
     // Check if all parent objects are expanded.
     let item = this;
-    while ((item = item.ownerView) &&  /* Parent object exists. */
-           (item instanceof Scope ||
-            item instanceof Variable ||
-            item instanceof Property)) {
+
+    // Recurse while parent is a Scope, Variable, or Property
+    while ((item = item.ownerView) && item instanceof Scope) {
       if (!item._isExpanded) {
         return false;
       }
     }
     return true;
   },
 
   /**
@@ -1717,24 +1802,21 @@ Scope.prototype = {
 
         if (variable._wasToggled && aLowerCaseQuery) {
           variable.expand();
         }
         if (variable._isExpanded && !aLowerCaseQuery) {
           variable._wasToggled = true;
         }
 
-        // If the variable is contained in another scope (variable or property),
+        // If the variable is contained in another Scope, Variable, or Property,
         // the parent may not be a match, thus hidden. It should be visible
         // ("expand upwards").
-
         while ((variable = variable.ownerView) &&  /* Parent object exists. */
-               (variable instanceof Scope ||
-                variable instanceof Variable ||
-                variable instanceof Property)) {
+               variable instanceof Scope) {
 
           // Show and expand the parent, as it is certainly accessible.
           variable._matched = true;
           aLowerCaseQuery && variable.expand();
         }
       }
 
       // Proceed with the search recursively inside this variable or property.
@@ -1966,89 +2048,34 @@ function Variable(aScope, aName, aDescri
   Scope.call(this, aScope, aName, this._initialDescriptor = aDescriptor);
   this.setGrip(aDescriptor.value);
   this._symbolicName = aName;
   this._absoluteName = aScope.name + "[\"" + aName + "\"]";
 }
 
 ViewHelpers.create({ constructor: Variable, proto: Scope.prototype }, {
   /**
-   * Adds a property for this variable.
-   *
-   * @param string aName
-   *        The property's name.
-   * @param object aDescriptor
-   *        Specifies the value and/or type & class of the property,
-   *        or 'get' & 'set' accessor properties. If the type is implicit,
-   *        it will be inferred from the value.
-   *        e.g. - { value: 42 }
-   *             - { value: true }
-   *             - { value: "nasu" }
-   *             - { value: { type: "undefined" } }
-   *             - { value: { type: "null" } }
-   *             - { value: { type: "object", class: "Object" } }
-   *             - { get: { type: "object", class: "Function" },
-   *                 set: { type: "undefined" } }
-   *             - { get: { type "object", class: "Function" },
-   *                 getterValue: "foo", getterPrototypeLevel: 2 }
-   * @param boolean aRelaxed
-   *        True if name duplicates should be allowed.
-   * @return Property
-   *         The newly created Property instance, null if it already exists.
+   * Whether this Scope should be prefetched when it is remoted.
    */
-  addProperty: function(aName = "", aDescriptor = {}, aRelaxed = false) {
-    if (this._store.has(aName) && !aRelaxed) {
-      return null;
-    }
-
-    let property = new Property(this, aName, aDescriptor);
-    this._store.set(aName, property);
-    this._variablesView._itemsByElement.set(property._target, property);
-    this._variablesView._currHierarchy.set(property._absoluteName, property);
-    property.header = !!aName;
-    return property;
+  get shouldPrefetch(){
+    return this.name == "window" || this.name == "this";
   },
 
   /**
-   * Adds properties for this variable.
+   * Create a new Property that is a child of Variable.
    *
-   * @param object aProperties
-   *        An object containing some { name: descriptor } data properties,
-   *        specifying the value and/or type & class of the variable,
-   *        or 'get' & 'set' accessor properties. If the type is implicit,
-   *        it will be inferred from the value.
-   *        e.g. - { someProp0: { value: 42 },
-   *                 someProp1: { value: true },
-   *                 someProp2: { value: "nasu" },
-   *                 someProp3: { value: { type: "undefined" } },
-   *                 someProp4: { value: { type: "null" } },
-   *                 someProp5: { value: { type: "object", class: "Object" } },
-   *                 someProp6: { get: { type: "object", class: "Function" },
-   *                              set: { type: "undefined" } } }
-   * @param object aOptions [optional]
-   *        Additional options for adding the properties. Supported options:
-   *        - sorted: true to sort all the properties before adding them
-   *        - callback: function invoked after each property is added
+   * @param string aName
+   *        The name of the new Property.
+   * @param object aDescriptor
+   *        The property's descriptor.
+   * @return Property
+   *         The newly created child Property.
    */
-  addProperties: function(aProperties, aOptions = {}) {
-    let propertyNames = Object.keys(aProperties);
-
-    // Sort all of the properties before adding them, if preferred.
-    if (aOptions.sorted) {
-      propertyNames.sort();
-    }
-    // Add the properties to the current scope.
-    for (let name of propertyNames) {
-      let descriptor = aProperties[name];
-      let property = this.addProperty(name, descriptor);
-
-      if (aOptions.callback) {
-        aOptions.callback(property, descriptor.value);
-      }
-    }
+  _createChild: function(aName, aDescriptor) {
+    return new Property(this, aName, aDescriptor);
   },
 
   /**
    * Populates this variable to contain all the properties of an object.
    *
    * @param object aObject
    *        The raw object you want to display.
    * @param object aOptions [optional]
@@ -2117,17 +2144,17 @@ ViewHelpers.create({ constructor: Variab
    *        The raw property value you want to display.
    * @return Property
    *         The newly added property instance.
    */
   _addRawValueProperty: function(aName, aDescriptor, aValue) {
     let descriptor = Object.create(aDescriptor);
     descriptor.value = VariablesView.getGrip(aValue);
 
-    let propertyItem = this.addProperty(aName, descriptor);
+    let propertyItem = this.addItem(aName, descriptor);
     propertyItem._sourceValue = aValue;
 
     // Add an 'onexpand' callback for the property, lazily handling
     // the addition of new child properties.
     if (!VariablesView.isPrimitive(descriptor)) {
       propertyItem.onexpand = this._populateTarget;
     }
     return propertyItem;
@@ -2144,17 +2171,17 @@ ViewHelpers.create({ constructor: Variab
    * @return Property
    *         The newly added property instance.
    */
   _addRawNonValueProperty: function(aName, aDescriptor) {
     let descriptor = Object.create(aDescriptor);
     descriptor.get = VariablesView.getGrip(aDescriptor.get);
     descriptor.set = VariablesView.getGrip(aDescriptor.set);
 
-    return this.addProperty(aName, descriptor);
+    return this.addItem(aName, descriptor);
   },
 
   /**
    * Gets this variable's path to the topmost scope.
    * For example, a symbolic name may look like "arguments['0']['foo']['bar']".
    * @return string
    */
   get symbolicName() this._symbolicName,
@@ -2306,18 +2333,18 @@ ViewHelpers.create({ constructor: Variab
       }
       // Deleting getters and setters individually is not allowed if no
       // evaluation method is provided.
       else {
         this.delete = null;
         this.evaluationMacro = null;
       }
 
-      let getter = this.addProperty("get", { value: descriptor.get });
-      let setter = this.addProperty("set", { value: descriptor.set });
+      let getter = this.addItem("get", { value: descriptor.get });
+      let setter = this.addItem("set", { value: descriptor.set });
       getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
       setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro;
 
       getter.hideArrow();
       setter.hideArrow();
       this.expand();
     }
   },
@@ -2847,19 +2874,18 @@ VariablesView.prototype.commitHierarchy 
     let changed = false;
 
     // If the inspected variable existed in a previous hierarchy, check if
     // the displayed value (a representation of the grip) has changed and if
     // it was previously expanded.
     if (prevVariable) {
       expanded = prevVariable._isExpanded;
 
-      // Only analyze variables and properties for displayed value changes.
-      if (currVariable instanceof Variable ||
-          currVariable instanceof Property) {
+      // Only analyze Variables and Properties for displayed value changes.
+      if (currVariable instanceof Variable) {
         changed = prevVariable._valueString != currVariable._valueString;
       }
     }
 
     // Make sure this variable is not handled in ulteror commits for the
     // same hierarchy.
     currVariable._committed = true;
 
@@ -2970,16 +2996,26 @@ VariablesView.isFalsy = function(aDescri
   if (type == "undefined" || type == "null") {
     return true;
   }
 
   return false;
 };
 
 /**
+ * Returns true if the value is an instance of Variable or Property.
+ *
+ * @param any aValue
+ *        The value to test.
+ */
+VariablesView.isVariable = function(aValue) {
+  return aValue instanceof Variable;
+};
+
+/**
  * Returns a standard grip for a value.
  *
  * @param any aValue
  *        The raw value to get a grip for.
  * @return any
  *         The value's grip.
  */
 VariablesView.getGrip = function(aValue) {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/VariablesViewController.jsm
@@ -0,0 +1,350 @@
+/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
+Cu.import("resource:///modules/devtools/VariablesView.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "VARIABLES_SORTING_ENABLED", () =>
+  Services.prefs.getBoolPref("devtools.debugger.ui.variables-sorting-enabled")
+);
+
+const MAX_LONG_STRING_LENGTH = 200000;
+
+this.EXPORTED_SYMBOLS = ["VariablesViewController"];
+
+
+/**
+ * Controller for a VariablesView that handles interfacing with the debugger
+ * protocol. Is able to populate scopes and variables via the protocol as well
+ * as manage actor lifespans.
+ *
+ * @param VariablesView aView
+ *        The view to attach to.
+ * @param object aOptions
+ *        Options for configuring the controller. Supported options:
+ *        - getGripClient: callback for creating an object grip client
+ *        - getLongStringClient: callback for creating a long string grip client
+ *        - releaseActor: callback for releasing an actor when it's no longer needed
+ *        - overrideValueEvalMacro: callback for creating an overriding eval macro
+ *        - getterOrSetterEvalMacro: callback for creating a getter/setter eval macro
+ *        - simpleValueEvalMacro: callback for creating a simple value eval macro
+ */
+function VariablesViewController(aView, aOptions) {
+  this.addExpander = this.addExpander.bind(this);
+
+  this._getGripClient = aOptions.getGripClient;
+  this._getLongStringClient = aOptions.getLongStringClient;
+  this._releaseActor = aOptions.releaseActor;
+
+  if (aOptions.overrideValueEvalMacro) {
+    this._overrideValueEvalMacro = aOptions.overrideValueEvalMacro;
+  }
+  if (aOptions.getterOrSetterEvalMacro) {
+    this._getterOrSetterEvalMacro = aOptions.getterOrSetterEvalMacro;
+  }
+  if (aOptions.simpleValueEvalMacro) {
+    this._simpleValueEvalMacro = aOptions.simpleValueEvalMacro;
+  }
+
+  this._actors = new Set();
+  this.view = aView;
+  this.view.controller = this;
+}
+
+VariablesViewController.prototype = {
+  /**
+   * The default getter/setter evaluation macro.
+   */
+  _getterOrSetterEvalMacro: VariablesView.getterOrSetterEvalMacro,
+
+  /**
+   * The default override value evaluation macro.
+   */
+  _overrideValueEvalMacro: VariablesView.overrideValueEvalMacro,
+
+  /**
+   * The default simple value evaluation macro.
+   */
+  _simpleValueEvalMacro: VariablesView.simpleValueEvalMacro,
+
+  /**
+   * Populate a long string into a target using a grip.
+   *
+   * @param Variable aTarget
+   *        The target Variable/Property to put the retrieved string into.
+   * @param LongStringActor aGrip
+   *        The long string grip that use to retrieve the full string.
+   * @return Promise
+   *         The promise that will be resolved when the string is retrieved.
+   */
+  _populateFromLongString: function(aTarget, aGrip){
+    let deferred = Promise.defer();
+
+    let from = aGrip.initial.length;
+    let to = Math.min(aGrip.length, MAX_LONG_STRING_LENGTH);
+
+    this._getLongStringClient(aGrip).substring(from, to, aResponse => {
+      // Stop tracking the actor because it's no longer needed.
+      this.releaseActor(aGrip);
+
+      // Replace the preview with the full string and make it non-expandable.
+      aTarget.onexpand = null;
+      aTarget.setGrip(aGrip.initial + aResponse.substring);
+      aTarget.hideArrow();
+
+      // Mark the string as having retrieved.
+      aTarget._retrieved = true;
+      deferred.resolve();
+    });
+
+    return deferred.promise;
+  },
+
+  /**
+   * Adds properties to a Scope, Variable, or Property in the view. Triggered
+   * when a scope is expanded or certain variables are hovered.
+   *
+   * @param Scope aTarget
+   *        The Scope where the properties will be placed into.
+   * @param object aGrip
+   *        The grip to use to populate the target.
+   */
+  _populateFromObject: function(aTarget, aGrip) {
+    let deferred = Promise.defer();
+
+    this._getGripClient(aGrip).getPrototypeAndProperties(aResponse => {
+      let { ownProperties, prototype, safeGetterValues } = aResponse;
+      let sortable = VariablesView.isSortable(aGrip.class);
+
+      // Merge the safe getter values into one object such that we can use it
+      // in VariablesView.
+      for (let name of Object.keys(safeGetterValues)) {
+        if (name in ownProperties) {
+          ownProperties[name].getterValue = safeGetterValues[name].getterValue;
+          ownProperties[name].getterPrototypeLevel = safeGetterValues[name]
+                                                     .getterPrototypeLevel;
+        } else {
+          ownProperties[name] = safeGetterValues[name];
+        }
+      }
+
+      // Add all the variable properties.
+      if (ownProperties) {
+        aTarget.addItems(ownProperties, {
+          // Not all variables need to force sorted properties.
+          sorted: sortable,
+          // Expansion handlers must be set after the properties are added.
+          callback: this.addExpander
+        });
+      }
+
+      // Add the variable's __proto__.
+      if (prototype && prototype.type != "null") {
+        let proto = aTarget.addItem("__proto__", { value: prototype });
+        // Expansion handlers must be set after the properties are added.
+        this.addExpander(proto, prototype);
+      }
+
+      // Mark the variable as having retrieved all its properties.
+      aTarget._retrieved = true;
+      this.view.commitHierarchy();
+      deferred.resolve();
+    });
+
+    return deferred.promise;
+  },
+
+  /**
+   * Adds an 'onexpand' callback for a variable, lazily handling
+   * the addition of new properties.
+   *
+   * @param Variable aVar
+   *        The variable where the properties will be placed into.
+   * @param any aSource
+   *        The source to use to populate the target.
+   */
+  addExpander: function(aTarget, aSource) {
+    // Attach evaluation macros as necessary.
+    if (aTarget.getter || aTarget.setter) {
+      aTarget.evaluationMacro = this._overrideValueEvalMacro;
+
+      let getter = aTarget.get("get");
+      if (getter) {
+        getter.evaluationMacro = this._getterOrSetterEvalMacro;
+      }
+
+      let setter = aTarget.get("set");
+      if (setter) {
+        setter.evaluationMacro = this._getterOrSetterEvalMacro;
+      }
+    } else {
+      aTarget.evaluationMacro = this._simpleValueEvalMacro;
+    }
+
+    // If the source is primitive then an expander is not needed.
+    if (VariablesView.isPrimitive({ value: aSource })) {
+      return;
+    }
+
+    // If the source is a long string then show the arrow.
+    if (WebConsoleUtils.isActorGrip(aSource) && aSource.type == "longString") {
+      aTarget.showArrow();
+    }
+
+    // Make sure that properties are always available on expansion.
+    aTarget.onexpand = () => this.expand(aTarget, aSource);
+
+    // Some variables are likely to contain a very large number of properties.
+    // It's a good idea to be prepared in case of an expansion.
+    if (aTarget.shouldPrefetch) {
+      aTarget.addEventListener("mouseover", aTarget.onexpand, false);
+    }
+
+    // Register all the actors that this controller now depends on.
+    for (let grip of [aTarget.value, aTarget.getter, aTarget.setter]) {
+      if (WebConsoleUtils.isActorGrip(grip)) {
+        this._actors.add(grip.actor);
+      }
+    }
+  },
+
+  /**
+   * Adds properties to a Scope, Variable, or Property in the view. Triggered
+   * when a scope is expanded or certain variables are hovered.
+   *
+   * @param Scope aTarget
+   *        The Scope to be expanded.
+   * @param object aSource
+   *        The source to use to populate the target.
+   * @return Promise
+   *         The promise that is resolved once the target has been expanded.
+   */
+  expand: function(aTarget, aSource) {
+    // Fetch the variables only once.
+    if (aTarget._fetched) {
+      return aTarget._fetched;
+    }
+
+    let deferred = Promise.defer();
+    aTarget._fetched = deferred.promise;
+
+    if (!aSource) {
+      throw new Error("No actor grip was given for the variable.");
+    }
+
+    // If the target a Variable or Property then we're fetching properties
+    if (VariablesView.isVariable(aTarget)) {
+      this._populateFromObject(aTarget, aSource).then(() => {
+        deferred.resolve();
+        // Signal that properties have been fetched.
+        this.view.emit("fetched", "properties", aTarget);
+      });
+      return deferred.promise;
+    }
+
+    switch (aSource.type) {
+      case "longString":
+        this._populateFromLongString(aTarget, aSource).then(() => {
+          deferred.resolve();
+          // Signal that a long string has been fetched.
+          this.view.emit("fetched", "longString", aTarget);
+        });
+        break;
+      case "with":
+      case "object":
+        this._populateFromObject(aTarget, aSource.object).then(() => {
+          deferred.resolve();
+          // Signal that variables have been fetched.
+          this.view.emit("fetched", "variables", aTarget);
+        });
+        break;
+      case "block":
+      case "function":
+        // Add nodes for every argument and every other variable in scope.
+        let args = aSource.bindings.arguments;
+        if (args) {
+          for (let arg of args) {
+            let name = Object.getOwnPropertyNames(arg)[0];
+            let ref = aTarget.addItem(name, arg[name]);
+            let val = arg[name].value;
+            this.addExpander(ref, val);
+          }
+        }
+
+        aTarget.addItems(aSource.bindings.variables, {
+          // Not all variables need to force sorted properties.
+          sorted: VARIABLES_SORTING_ENABLED,
+          // Expansion handlers must be set after the properties are added.
+          callback: this.addExpander
+        });
+
+        // No need to signal that variables have been fetched, since
+        // the scope arguments and variables are already attached to the
+        // environment bindings, so pausing the active thread is unnecessary.
+
+        deferred.resolve();
+        break;
+      default:
+        let error = "Unknown Debugger.Environment type: " + aSource.type;
+        Cu.reportError(error);
+        deferred.reject(error);
+    }
+
+    return deferred.promise;
+  },
+
+  /**
+   * Release an actor from the controller.
+   *
+   * @param object aActor
+   *        The actor to release.
+   */
+  releaseActor: function(aActor){
+    if (this._releaseActor) {
+      this._releaseActor(aActor);
+    }
+    this._actors.delete(aActor);
+  },
+
+  /**
+   * Release all the actors referenced by the controller, optionally filtered.
+   *
+   * @param function aFilter [optional]
+   *        Callback to filter which actors are released.
+   */
+  releaseActors: function(aFilter) {
+    for (let actor of this._actors) {
+      if (!aFilter || aFilter(actor)) {
+        this.releaseActor(actor);
+      }
+    }
+  },
+};
+
+
+/**
+ * Attaches a VariablesViewController to a VariablesView if it doesn't already
+ * have one.
+ *
+ * @param VariablesView aView
+ *        The view to attach to.
+ * @param object aOptions
+ *        The options to use in creating the controller.
+ * @return VariablesViewController
+ */
+VariablesViewController.attach = function(aView, aOptions) {
+  if (aView.controller) {
+    return aView.controller;
+  }
+  return new VariablesViewController(aView, aOptions);
+};
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -32,16 +32,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/devtools/WebConsoleUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
                                   "resource:///modules/devtools/VariablesView.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
+                                  "resource:///modules/devtools/VariablesViewController.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
                                   "resource:///modules/devtools/shared/event-emitter.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
                                   "resource://gre/modules/devtools/Loader.jsm");
 
 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
@@ -2099,23 +2102,19 @@ WebConsoleFrame.prototype = {
     }
     else if (aNode._connectionId &&
              aNode.classList.contains("webconsole-msg-network")) {
       delete this._networkRequests[aNode._connectionId];
       this._releaseObject(aNode._connectionId);
     }
     else if (aNode.classList.contains("webconsole-msg-inspector")) {
       let view = aNode._variablesView;
-      let actors = view ?
-                   this.jsterm._objectActorsInVariablesViews.get(view) :
-                   new Set();
-      for (let actor of actors) {
-        this._releaseObject(actor);
+      if (view) {
+        view.controller.releaseActors();
       }
-      actors.clear();
       aNode._variablesView = null;
     }
 
     if (aNode.parentNode) {
       aNode.parentNode.removeChild(aNode);
     }
   },
 
@@ -2738,16 +2737,45 @@ WebConsoleFrame.prototype = {
     else {
       onDestroy();
     }
 
     return this._destroyer.promise;
   },
 };
 
+
+/**
+ * @see VariablesView.simpleValueEvalMacro
+ */
+function simpleValueEvalMacro(aItem, aCurrentString)
+{
+  return VariablesView.simpleValueEvalMacro(aItem, aCurrentString, "_self");
+};
+
+
+/**
+ * @see VariablesView.overrideValueEvalMacro
+ */
+function overrideValueEvalMacro(aItem, aCurrentString)
+{
+  return VariablesView.overrideValueEvalMacro(aItem, aCurrentString, "_self");
+};
+
+
+/**
+ * @see VariablesView.getterOrSetterEvalMacro
+ */
+function getterOrSetterEvalMacro(aItem, aCurrentString)
+{
+  return VariablesView.getterOrSetterEvalMacro(aItem, aCurrentString, "_self");
+}
+
+
+
 /**
  * Create a JSTerminal (a JavaScript command line). This is attached to an
  * existing HeadsUpDisplay (a Web Console instance). This code is responsible
  * with handling command line input, code evaluation and result output.
  *
  * @constructor
  * @param object aWebConsoleFrame
  *        The WebConsoleFrame object that owns this JSTerm instance.
@@ -2766,18 +2794,16 @@ function JSTerm(aWebConsoleFrame)
 
   // Holds the index of the history entry that the user is currently viewing.
   // This is reset to this.history.length when this.execute() is invoked.
   this.historyPlaceHolder = 0;
   this._objectActorsInVariablesViews = new Map();
 
   this._keyPress = this.keyPress.bind(this);
   this._inputEventHandler = this.inputEventHandler.bind(this);
-  this._fetchVarProperties = this._fetchVarProperties.bind(this);
-  this._fetchVarLongString = this._fetchVarLongString.bind(this);
   this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this);
 
   EventEmitter.decorate(this);
 }
 
 JSTerm.prototype = {
   SELECTED_FRAME: -1,
 
@@ -3277,17 +3303,37 @@ JSTerm.prototype = {
   _createVariablesView: function JST__createVariablesView(aOptions)
   {
     let view = new VariablesView(aOptions.container);
     view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder");
     view.emptyText = l10n.getStr("emptyPropertiesList");
     view.searchEnabled = !aOptions.hideFilterInput;
     view.lazyEmpty = this._lazyVariablesView;
     view.lazyAppend = this._lazyVariablesView;
-    this._objectActorsInVariablesViews.set(view, new Set());
+
+    VariablesViewController.attach(view, {
+      getGripClient: aGrip => {
+        return new GripClient(this.hud.proxy.client, aGrip);
+      },
+      getLongStringClient: aGrip => {
+        return this.webConsoleClient.longString(aGrip);
+      },
+      releaseActor: aActor => {
+        this.hud._releaseObject(aActor);
+      },
+      simpleValueEvalMacro: simpleValueEvalMacro,
+      overrideValueEvalMacro: overrideValueEvalMacro,
+      getterOrSetterEvalMacro: getterOrSetterEvalMacro,
+    });
+
+    // Relay events from the VariablesView.
+    view.on("fetched", (aEvent, aType, aVar) => {
+      this.emit("variablesview-fetched", aVar);
+    });
+
     return view;
   },
 
   /**
    * Update the variables view.
    *
    * @private
    * @param object aOptions
@@ -3299,26 +3345,21 @@ JSTerm.prototype = {
    *        - label: the new label for the inspected object.
    */
   _updateVariablesView: function JST__updateVariablesView(aOptions)
   {
     let view = aOptions.view;
     view.createHierarchy();
     view.empty();
 
-    let actors = this._objectActorsInVariablesViews.get(view);
-    for (let actor of actors) {
-      // We need to avoid pruning the object inspection starting point.
-      // That one is pruned when the console message is removed.
-      if (view._consoleLastObjectActor != actor) {
-        this.hud._releaseObject(actor);
-      }
-    }
-
-    actors.clear();
+    // We need to avoid pruning the object inspection starting point.
+    // That one is pruned when the console message is removed.
+    view.controller.releaseActors(aActor => {
+      return view._consoleLastObjectActor != aActor;
+    });
 
     if (aOptions.objectActor) {
       // Make sure eval works in the correct context.
       view.eval = this._variablesViewEvaluate.bind(this, aOptions);
       view.switch = this._variablesViewSwitch.bind(this, aOptions);
       view.delete = this._variablesViewDelete.bind(this, aOptions);
     }
     else {
@@ -3326,21 +3367,21 @@ JSTerm.prototype = {
       view.switch = null;
       view.delete = null;
     }
 
     let scope = view.addScope(aOptions.label);
     scope.expanded = true;
     scope.locked = true;
 
-    let container = scope.addVar();
-    container.evaluationMacro = this._variablesViewSimpleValueEvalMacro;
+    let container = scope.addItem();
+    container.evaluationMacro = simpleValueEvalMacro;
 
     if (aOptions.objectActor) {
-      this._fetchVarProperties(container, aOptions.objectActor);
+      view.controller.expand(container, aOptions.objectActor);
       view._consoleLastObjectActor = aOptions.objectActor.actor;
     }
     else if (aOptions.rawObject) {
       container.populate(aOptions.rawObject);
       view.commitHierarchy();
       view._consoleLastObjectActor = null;
     }
     else {
@@ -3370,90 +3411,16 @@ JSTerm.prototype = {
       frame: this.SELECTED_FRAME,
       bindObjectActor: aOptions.objectActor.actor,
     };
 
     this.requestEvaluation(aString, evalOptions).then(onEval, onEval);
   },
 
   /**
-   * Generates the string evaluated when performing simple value changes in the
-   * variables view.
-   *
-   * @private
-   * @param Variable | Property aItem
-   *        The current variable or property.
-   * @param string aCurrentString
-   *        The trimmed user inputted string.
-   * @return string
-   *         The string to be evaluated.
-   */
-  _variablesViewSimpleValueEvalMacro:
-  function JST__variablesViewSimpleValueEvalMacro(aItem, aCurrentString)
-  {
-    return "_self" + aItem.symbolicName + "=" + aCurrentString;
-  },
-
-
-  /**
-   * Generates the string evaluated when overriding getters and setters with
-   * plain values in the variables view.
-   *
-   * @private
-   * @param Property aItem
-   *        The current getter or setter property.
-   * @param string aCurrentString
-   *        The trimmed user inputted string.
-   * @return string
-   *         The string to be evaluated.
-   */
-  _variablesViewOverrideValueEvalMacro:
-  function JST__variablesViewOverrideValueEvalMacro(aItem, aCurrentString)
-  {
-    let parent = aItem.ownerView;
-    let symbolicName = parent.symbolicName;
-    if (symbolicName.indexOf("_self") != 0) {
-      parent._symbolicName = "_self" + symbolicName;
-    }
-
-    let result = VariablesView.overrideValueEvalMacro.apply(this, arguments);
-
-    parent._symbolicName = symbolicName;
-
-    return result;
-  },
-
-  /**
-   * Generates the string evaluated when performing getters and setters changes
-   * in the variables view.
-   *
-   * @private
-   * @param Property aItem
-   *        The current getter or setter property.
-   * @param string aCurrentString
-   *        The trimmed user inputted string.
-   * @return string
-   *         The string to be evaluated.
-   */
-  _variablesViewGetterOrSetterEvalMacro:
-  function JST__variablesViewGetterOrSetterEvalMacro(aItem, aCurrentString)
-  {
-    let propertyObject = aItem.ownerView;
-    let parentObject = propertyObject.ownerView;
-    let parent = parentObject.symbolicName;
-    parentObject._symbolicName = "_self" + parent;
-
-    let result = VariablesView.getterOrSetterEvalMacro.apply(this, arguments);
-
-    parentObject._symbolicName = parent;
-
-    return result;
-  },
-
-  /**
    * The property deletion function used by the variables view when a property
    * is deleted.
    *
    * @private
    * @param object aOptions
    *        The options used for |this._updateVariablesView()|.
    * @param object aVar
    *        The Variable object instance for the deleted property.
@@ -3551,154 +3518,17 @@ JSTerm.prototype = {
       if (WebConsoleUtils.isActorGrip(grip)) {
         this.hud._releaseObject(grip.actor);
       }
     }
 
     aCallback && aCallback(aResponse);
   },
 
-  /**
-   * Adds properties to a variable in the view. Triggered when a variable is
-   * expanded. It does not expand the variable.
-   *
-   * @param object aVar
-   *        The VariablseView Variable instance where the properties get added.
-   * @param object [aGrip]
-   *        Optional, the object actor grip of the variable. If the grip is not
-   *        provided, then the aVar.value is used as the object actor grip.
-   */
-  _fetchVarProperties: function JST__fetchVarProperties(aVar, aGrip)
-  {
-    // Retrieve the properties only once.
-    if (aVar._fetched) {
-      return;
-    }
-    aVar._fetched = true;
-
-    let grip = aGrip || aVar.value;
-    if (!grip) {
-      throw new Error("No object actor grip was given for the variable.");
-    }
-
-    let view = aVar._variablesView;
-    let actors = this._objectActorsInVariablesViews.get(view);
-
-    function addActorForDescriptor(aGrip) {
-      if (WebConsoleUtils.isActorGrip(aGrip)) {
-        actors.add(aGrip.actor);
-      }
-    }
-
-    let onNewProperty = (aProperty) => {
-      if (aProperty.getter || aProperty.setter) {
-        aProperty.evaluationMacro = this._variablesViewOverrideValueEvalMacro;
-        let getter = aProperty.get("get");
-        let setter = aProperty.get("set");
-        if (getter) {
-          getter.evaluationMacro = this._variablesViewGetterOrSetterEvalMacro;
-        }
-        if (setter) {
-          setter.evaluationMacro = this._variablesViewGetterOrSetterEvalMacro;
-        }
-      }
-      else {
-        aProperty.evaluationMacro = this._variablesViewSimpleValueEvalMacro;
-      }
-
-      let grips = [aProperty.value, aProperty.getter, aProperty.setter];
-      grips.forEach(addActorForDescriptor);
-
-      let inspectable = !VariablesView.isPrimitive({ value: aProperty.value });
-      let longString = WebConsoleUtils.isActorGrip(aProperty.value) &&
-                       aProperty.value.type == "longString";
-      if (inspectable) {
-        aProperty.onexpand = this._fetchVarProperties;
-      }
-      else if (longString) {
-        aProperty.onexpand = this._fetchVarLongString;
-        aProperty.showArrow();
-      }
-    };
-
-    let client = new GripClient(this.hud.proxy.client, grip);
-    client.getPrototypeAndProperties((aResponse) => {
-      let { ownProperties, prototype, safeGetterValues } = aResponse;
-      let sortable = VariablesView.NON_SORTABLE_CLASSES.indexOf(grip.class) == -1;
-
-      // Merge the safe getter values into one object such that we can use it
-      // in VariablesView.
-      for (let name of Object.keys(safeGetterValues)) {
-        if (name in ownProperties) {
-          ownProperties[name].getterValue = safeGetterValues[name].getterValue;
-          ownProperties[name].getterPrototypeLevel = safeGetterValues[name]
-                                                     .getterPrototypeLevel;
-        }
-        else {
-          ownProperties[name] = safeGetterValues[name];
-        }
-      }
-
-      // Add all the variable properties.
-      if (ownProperties) {
-        aVar.addProperties(ownProperties, {
-          sorted: sortable,
-          callback: onNewProperty,
-        });
-      }
-
-      // Add the variable's __proto__.
-      if (prototype && prototype.type != "null") {
-        let proto = aVar.addProperty("__proto__", { value: prototype });
-        onNewProperty(proto);
-      }
-
-      aVar._retrieved = true;
-      view.commitHierarchy();
-      this.emit("variablesview-fetched", aVar);
-    });
-  },
-
-  /**
-   * Fetch the full string for a given variable that displays a long string.
-   *
-   * @param object aVar
-   *        The VariablesView Variable instance where the properties get added.
-   */
-  _fetchVarLongString: function JST__fetchVarLongString(aVar)
-  {
-    if (aVar._fetched) {
-      return;
-    }
-    aVar._fetched = true;
-
-    let grip = aVar.value;
-    if (!grip) {
-      throw new Error("No long string actor grip was given for the variable.");
-    }
-
-    let client = this.webConsoleClient.longString(grip);
-    let toIndex = Math.min(grip.length, MAX_LONG_STRING_LENGTH);
-    client.substring(grip.initial.length, toIndex, (aResponse) => {
-      if (aResponse.error) {
-        Cu.reportError("JST__fetchVarLongString substring failure: " +
-                       aResponse.error + ": " + aResponse.message);
-        return;
-      }
-
-      aVar.onexpand = null;
-      aVar.setGrip(grip.initial + aResponse.substring);
-      aVar.hideArrow();
-      aVar._retrieved = true;
-
-      if (toIndex != grip.length) {
-        this.hud.logWarningAboutStringTooLong();
-      }
-    });
-  },
+
 
   /**
    * Writes a JS object to the JSTerm outputNode.
    *
    * @param string aOutputMessage
    *        The message to display.
    * @param function [aCallback]
    *        Optional function to invoke when users click the message.
@@ -4353,21 +4183,17 @@ JSTerm.prototype = {
 
   /**
    * Destroy the sidebar.
    * @private
    */
   _sidebarDestroy: function JST__sidebarDestroy()
   {
     if (this._variablesView) {
-      let actors = this._objectActorsInVariablesViews.get(this._variablesView);
-      for (let actor of actors) {
-        this.hud._releaseObject(actor);
-      }
-      actors.clear();
+      this._variablesView.controller.releaseActors();
       this._variablesView = null;
     }
 
     if (this.sidebar) {
       this.sidebar.hide();
       this.sidebar.destroy();
       this.sidebar = null;
     }
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -0,0 +1,1582 @@
+/* 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";
+
+/**
+ * Here's the server side of the remote inspector.
+ *
+ * The WalkerActor is the client's view of the debuggee's DOM.  It's gives
+ * the client a tree of NodeActor objects.
+ *
+ * The walker presents the DOM tree mostly unmodified from the source DOM
+ * tree, but with a few key differences:
+ *
+ *  - Empty text nodes are ignored.  This is pretty typical of developer
+ *    tools, but maybe we should reconsider that on the server side.
+ *  - iframes with documents loaded have the loaded document as the child,
+ *    the walker provides one big tree for the whole document tree.
+ *
+ * There are a few ways to get references to NodeActors:
+ *
+ *   - When you first get a WalkerActor reference, it comes with a free
+ *     reference to the root document's node.
+ *   - Given a node, you can ask for children, siblings, and parents.
+ *   - You can issue querySelector and querySelectorAll requests to find
+ *     other elements.
+ *   - Requests that return arbitrary nodes from the tree (like querySelector
+ *     and querySelectorAll) will also return any nodes the client hasn't
+ *     seen in order to have a complete set of parents.
+ *
+ * Once you have a NodeFront, you should be able to answer a few questions
+ * without further round trips, like the node's name, namespace/tagName,
+ * attributes, etc.  Other questions (like a text node's full nodeValue)
+ * might require another round trip.
+ *
+ * The protocol guarantees that the client will always know the parent of
+ * any node that is returned by the server.  This means that some requests
+ * (like querySelector) will include the extra nodes needed to satisfy this
+ * requirement.  The client keeps track of this parent relationship, so the
+ * node fronts form a tree that is a subset of the actual DOM tree.
+ */
+
+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) {
+  handle.removeTabActor(InspectorActor);
+};
+
+// XXX: A poor man's makeInfallible until we move it out of transport.js
+// Which should be very soon.
+function makeInfallible(handler) {
+  return function(...args) {
+    try {
+      return handler.apply(this, args);
+    } catch(ex) {
+      console.error(ex);
+    }
+    return undefined;
+  }
+}
+
+// A resolve that hits the main loop first.
+function delayedResolve(value) {
+  let deferred = promise.defer();
+  Services.tm.mainThread.dispatch(makeInfallible(function delayedResolveHandler() {
+    deferred.resolve(value);
+  }), 0);
+  return deferred.promise;
+}
+
+/**
+ * We only send nodeValue up to a certain size by default.  This stuff
+ * controls that size.
+ */
+exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
+var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;
+
+exports.getValueSummaryLength = function() {
+  return gValueSummaryLength;
+};
+
+exports.setValueSummaryLength = function(val) {
+  gValueSummaryLength = val;
+};
+
+/**
+ * Server side of the node actor.
+ */
+var NodeActor = protocol.ActorClass({
+  typeName: "domnode",
+
+  initialize: function(walker, node) {
+    protocol.Actor.prototype.initialize.call(this, null);
+    this.walker = walker;
+    this.rawNode = node;
+  },
+
+  toString: function() {
+    return "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]";
+  },
+
+  /**
+   * Instead of storing a connection object, the NodeActor gets its connection
+   * from its associated walker.
+   */
+  get conn() this.walker.conn,
+
+  // Returns the JSON representation of this object over the wire.
+  form: function(detail) {
+    let parentNode = this.walker.parentNode(this);
+
+    let form = {
+      actor: this.actorID,
+      parent: parentNode ? parentNode.actorID : undefined,
+      nodeType: this.rawNode.nodeType,
+      namespaceURI: this.namespaceURI,
+      nodeName: this.rawNode.nodeName,
+      numChildren: this.rawNode.childNodes.length,
+
+      // doctype attributes
+      name: this.rawNode.name,
+      publicId: this.rawNode.publicId,
+      systemId: this.rawNode.systemId,
+
+      attrs: this.writeAttrs()
+    };
+
+    if (this.rawNode.nodeValue) {
+      // We only include a short version of the value if it's longer than
+      // gValueSummaryLength
+      if (this.rawNode.nodeValue.length > gValueSummaryLength) {
+        form.shortValue = this.rawNode.nodeValue.substring(0, gValueSummaryLength);
+        form.incompleteValue = true;
+      } else {
+        form.shortValue = this.rawNode.nodeValue;
+      }
+    }
+
+    return form;
+  },
+
+  writeAttrs: function() {
+    if (!this.rawNode.attributes) {
+      return undefined;
+    }
+    return [{namespace: attr.namespace, name: attr.name, value: attr.value }
+            for (attr of this.rawNode.attributes)];
+  },
+
+  /**
+   * Returns a LongStringActor with the node's value.
+   */
+  getNodeValue: method(function() {
+    return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
+  }, {
+    request: {},
+    response: {
+      value: RetVal("longstring")
+    }
+  }),
+
+  /**
+   * Set the node's value to a given string.
+   */
+  setNodeValue: method(function(value) {
+    this.rawNode.nodeValue = value;
+  }, {
+    request: { value: Arg(0) },
+    response: {}
+  }),
+});
+
+/**
+ * Client side of the node actor.
+ *
+ * Node fronts are strored in a tree that mirrors the DOM tree on the
+ * server, but with a few key differences:
+ *  - Not all children will be necessary loaded for each node.
+ *  - The order of children isn't guaranteed to be the same as the DOM.
+ * Children are stored in a doubly-linked list, to make addition/removal
+ * and traversal quick.
+ *
+ * Due to the order/incompleteness of the child list, it is safe to use
+ * the parent node from clients, but the `children` request should be used
+ * to traverse children.
+ */
+let NodeFront = protocol.FrontClass(NodeActor, {
+  initialize: function(conn, form, detail, ctx) {
+    this._parent = null; // The parent node
+    this._child = null;  // The first child of this node.
+    this._next = null;   // The next sibling of this node.
+    this._prev = null;   // The previous sibling of this node.
+    protocol.Front.prototype.initialize.call(this, conn, form, detail, ctx);
+  },
+
+  destroy: function() {
+    // If an observer was added on this node, shut it down.
+    if (this.observer) {
+      this._observer.disconnect();
+      this._observer = null;
+    }
+
+    // Disconnect this item and from the ownership tree and destroy
+    // all of its children.
+    this.reparent(null);
+    for (let child of this.treeChildren()) {
+      child.destroy();
+    }
+    protocol.Front.prototype.destroy.call(this);
+  },
+
+  // Update the object given a form representation off the wire.
+  form: function(form, detail, ctx) {
+    // Shallow copy of the form.  We could just store a reference, but
+    // eventually we'll want to update some of the data.
+    this._form = object.merge(form);
+    this._form.attrs = this._form.attrs ? this._form.attrs.slice() : [];
+
+    if (form.parent) {
+      // Get the owner actor for this actor (the walker), and find the
+      // parent node of this actor from it, creating a standin node if
+      // necessary.
+      let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent);
+      this.reparent(parentNodeFront);
+    }
+  },
+
+  /**
+   * Returns the parent NodeFront for this NodeFront.
+   */
+  parentNode: function() {
+    return this._parent;
+  },
+
+  /**
+   * Process a mutation entry as returned from the walker's `getMutations`
+   * request.  Only tries to handle changes of the node's contents
+   * themselves (character data and attribute changes), the walker itself
+   * will keep the ownership tree up to date.
+   */
+  updateMutation: function(change) {
+    if (change.type === "attributes") {
+      // We'll need to lazily reparse the attributes after this change.
+      this._attrMap = undefined;
+
+      // Update any already-existing attributes.
+      let found = false;
+      for (let i = 0; i < this.attributes.length; i++) {
+        let attr = this.attributes[i];
+        if (attr.name == change.attributeName &&
+            attr.namespace == change.attributeNamespace) {
+          if (change.newValue !== null) {
+            attr.value = change.newValue;
+          } else {
+            this.attributes.splice(i, 1);
+          }
+          found = true;
+          break;
+        }
+      }
+      // This is a new attribute.
+      if (!found)  {
+        this.attributes.push({
+          name: change.attributeName,
+          namespace: change.attributeNamespace,
+          value: change.newValue
+        });
+      }
+    } else if (change.type === "characterData") {
+      this._form.shortValue = change.newValue;
+      this._form.incompleteValue = change.incompleteValue;
+    }
+  },
+
+  // Some accessors to make NodeFront feel more like an nsIDOMNode
+
+  get id() this.getAttribute("id"),
+
+  get nodeType() this._form.nodeType,
+  get namespaceURI() this._form.namespaceURI,
+  get nodeName() this._form.nodeName,
+
+  get className() {
+    return this.getAttribute("class") || '';
+  },
+
+  get hasChildren() this._form.numChildren > 0,
+  get numChildren() this._form.numChildren,
+
+  get tagName() this.nodeType === Ci.nsIDOMNode.ELEMENT_NODE ? this.nodeName : null,
+  get shortValue() this._form.shortValue,
+  get incompleteValue() !!this._form.incompleteValue,
+
+  // doctype properties
+  get name() this._form.name,
+  get publicId() this._form.publicId,
+  get systemId() this._form.systemId,
+
+  getAttribute: function(name) {
+    let attr = this._getAttribute(name);
+    return attr ? attr.value : null;
+  },
+  hasAttribute: function(name) {
+    this._cacheAttributes();
+    return (name in this._attrMap);
+  },
+
+  get attributes() this._form.attrs,
+
+  getNodeValue: protocol.custom(function() {
+    if (!this.incompleteValue) {
+      return delayedResolve(new ShortLongString(this.shortValue));
+    } else {
+      return this._getNodeValue();
+    }
+  }, {
+    impl: "_getNodeValue"
+  }),
+
+  _cacheAttributes: function() {
+    if (typeof(this._attrMap) != "undefined") {
+      return;
+    }
+    this._attrMap = {};
+    for (let attr of this.attributes) {
+      this._attrMap[attr.name] = attr;
+    }
+  },
+
+  _getAttribute: function(name) {
+    this._cacheAttributes();
+    return this._attrMap[name] || undefined;
+  },
+
+  /**
+   * Set this node's parent.  Note that the children saved in
+   * this tree are unordered and incomplete, so shouldn't be used
+   * instead of a `children` request.
+   */
+  reparent: function(parent) {
+    if (this._parent === parent) {
+      return;
+    }
+
+    if (this._parent && this._parent._child === this) {
+      this._parent._child = this._next;
+    }
+    if (this._prev) {
+      this._prev._next = this._next;
+    }
+    if (this._next) {
+      this._next._prev = this._prev;
+    }
+    this._next = null;
+    this._prev = null;
+    this._parent = parent;
+    if (!parent) {
+      // Subtree is disconnected, we're done
+      return;
+    }
+    this._next = parent._child;
+    if (this._next) {
+      this._next._prev = this;
+    }
+    parent._child = this;
+  },
+
+  /**
+   * Return all the known children of this node.
+   */
+  treeChildren: function() {
+    let ret = [];
+    for (let child = this._child; child != null; child = child._next) {
+      ret.push(child);
+    }
+    return ret;
+  },
+
+  /**
+   * Get an nsIDOMNode for the given node front.  This only works locally,
+   * and is only intended as a stopgap during the transition to the remote
+   * protocol.  If you depend on this you're likely to break soon.
+   */
+  rawNode: function(rawNode) {
+    if (!this.conn._transport._serverConnection) {
+      throw new Error("Tried to use rawNode on a remote connection.");
+    }
+    let actor = this.conn._transport._serverConnection.getActor(this.actorID);
+    if (!actor) {
+      throw new Error("Could not find client side for actor " + this.actorID);
+    }
+    return actor.rawNode;
+  }
+});
+
+/**
+ * Returned from any call that might return a node that isn't connected to root by
+ * nodes the child has seen, such as querySelector.
+ */
+types.addDictType("disconnectedNode", {
+  // The actual node to return
+  node: "domnode",
+
+  // Nodes that are needed to connect the node to a node the client has already seen
+  newNodes: "array:domnode"
+});
+
+types.addDictType("disconnectedNodeArray", {
+  // The actual node list to return
+  nodes: "array:domnode",
+
+  // Nodes that are needed to connect those nodes to the root.
+  newNodes: "array:domnode"
+});
+
+types.addDictType("dommutation", {});
+
+/**
+ * Server side of a node list as returned by querySelectorAll()
+ */
+var NodeListActor = exports.NodeListActor = protocol.ActorClass({
+  typeName: "domnodelist",
+
+  initialize: function(walker, nodeList) {
+    protocol.Actor.prototype.initialize.call(this);
+    this.walker = walker;
+    this.nodeList = nodeList;
+  },
+
+  destroy: function() {
+    protocol.Actor.prototype.destroy.call(this);
+  },
+
+  /**
+   * Instead of storing a connection object, the NodeActor gets its connection
+   * from its associated walker.
+   */
+  get conn() {
+    return this.walker.conn;
+  },
+
+  /**
+   * Items returned by this actor should belong to the parent walker.
+   */
+  marshallPool: function() {
+    return this.walker;
+  },
+
+  // Returns the JSON representation of this object over the wire.
+  form: function() {
+    return {
+      actor: this.actorID,
+      length: this.nodeList.length
+    }
+  },
+
+  /**
+   * Get a single node from the node list.
+   */
+  item: method(function(index) {
+    let node = this.walker._ref(this.nodeList[index]);
+    let newNodes = [node for (node of this.walker.ensurePathToRoot(node))];
+    return {
+      node: node,
+      newNodes: newNodes
+    }
+  }, {
+    request: { item: Arg(0) },
+    response: RetVal("disconnectedNode")
+  }),
+
+  /**
+   * Get a range of the items from the node list.
+   */
+  items: method(function(start=0, end=this.nodeList.length) {
+    let items = [this.walker._ref(item) for (item of Array.prototype.slice.call(this.nodeList, start, end))];
+    let newNodes = new Set();
+    for (let item of items) {
+      this.walker.ensurePathToRoot(item, newNodes);
+    }
+    return {
+      nodes: items,
+      newNodes: [node for (node of newNodes)]
+    }
+  }, {
+    request: {
+      start: Arg(0, "number", { optional: true }),
+      end: Arg(1, "number", { optional: true })
+    },
+    response: { nodes: RetVal("disconnectedNodeArray") }
+  }),
+
+  release: method(function() {}, { release: true })
+});
+
+/**
+ * Client side of a node list as returned by querySelectorAll()
+ */
+var NodeListFront = exports.NodeLIstFront = protocol.FrontClass(NodeListActor, {
+  initialize: function(client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+  },
+
+  destroy: function() {
+    protocol.Front.prototype.destroy.call(this);
+  },
+
+  marshallPool: function() {
+    return this.parent();
+  },
+
+  // Update the object given a form representation off the wire.
+  form: function(json) {
+    this.length = json.length;
+  },
+
+  item: protocol.custom(function(index) {
+    return this._item(index).then(response => {
+      return response.node;
+    });
+  }, {
+    impl: "_item"
+  }),
+
+  items: protocol.custom(function(start, end) {
+    return this._items(start, end).then(response => {
+      return response.nodes;
+    });
+  }, {
+    impl: "_items"
+  })
+});
+
+// Some common request/response templates for the dom walker
+
+let nodeArrayMethod = {
+  request: {
+    node: Arg(0, "domnode"),
+    maxNodes: Option(1),
+    center: Option(1, "domnode"),
+    start: Option(1, "domnode"),
+    whatToShow: Option(1)
+  },
+  response: RetVal(types.addDictType("domtraversalarray", {
+    nodes: "array:domnode"
+  }))
+};
+
+let traversalMethod = {
+  request: {
+    node: Arg(0, "domnode"),
+    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, 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() {
+    return {
+      actor: this.actorID,
+      root: this.rootNode.form()
+    }
+  },
+
+  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);
+    }
+    protocol.Actor.prototype.unmanage.call(this, actor);
+  },
+
+  _ref: function(node) {
+    let actor = this._refMap.get(node);
+    if (actor) return actor;
+
+    actor = new NodeActor(this, node);
+
+    // Add the node actor as a child of this walker actor, assigning
+    // it an actorID.
+    this.manage(actor);
+    this._refMap.set(node, actor);
+
+    if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
+      this._watchDocument(actor);
+    }
+    return actor;
+  },
+
+  /**
+   * Watch the given document node for mutations using the DOM observer
+   * API.
+   */
+  _watchDocument: function(actor) {
+    let node = actor.rawNode;
+    // Create the observer on the node's actor.  The node will make sure
+    // the observer is cleaned up when the actor is released.
+    actor.observer = actor.rawNode.defaultView.MutationObserver(this.onMutations);
+    actor.observer.observe(node, {
+      attributes: true,
+      characterData: true,
+      childList: true,
+      subtree: true
+    });
+  },
+
+  /**
+   * Return the document node that contains the given node,
+   * or the root node if no node is specified.
+   * @param NodeActor node
+   *        The node whose document is needed, or null to
+   *        return the root.
+   */
+  document: method(function(node) {
+    let doc = node ? nodeDocument(node.rawNode) : this.rootDoc;
+    return this._ref(doc);
+  }, {
+    request: { node: Arg(0, "domnode", {optional: true}) },
+    response: { node: RetVal("domnode") },
+  }),
+
+  /**
+   * Return the documentElement for the document containing the
+   * given node.
+   * @param NodeActor node
+   *        The node whose documentElement is requested, or null
+   *        to use the root document.
+   */
+  documentElement: method(function(node) {
+    let elt = node ? nodeDocument(node.rawNode).documentElement : this.rootDoc.documentElement;
+    return this._ref(elt);
+  }, {
+    request: { node: Arg(0, "domnode", {optional: true}) },
+    response: { node: RetVal("domnode") },
+  }),
+
+  /**
+   * Return all parents of the given node, ordered from immediate parent
+   * to root.
+   * @param NodeActor node
+   *    The node whose parents are requested.
+   * @param object options
+   *    Named options, including:
+   *    `sameDocument`: If true, parents will be restricted to the same
+   *      document as the node.
+   */
+  parents: method(function(node, options={}) {
+    let walker = documentWalker(node.rawNode);
+    let parents = [];
+    let cur;
+    while((cur = walker.parentNode())) {
+      if (options.sameDocument && cur.ownerDocument != node.rawNode.ownerDocument) {
+        break;
+      }
+      parents.push(this._ref(cur));
+    }
+    return parents;
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      sameDocument: Option(1)
+    },
+    response: {
+      nodes: RetVal("array:domnode")
+    },
+  }),
+
+  parentNode: function(node) {
+    let walker = documentWalker(node.rawNode);
+    let parent = walker.parentNode();
+    if (parent) {
+      return this._ref(parent);
+    }
+    return null;
+  },
+
+  /**
+   * Release actors for a node and all child nodes.
+   */
+  releaseNode: method(function(node) {
+    let walker = documentWalker(node.rawNode);
+
+    let child = walker.firstChild();
+    while (child) {
+      let childActor = this._refMap.get(child);
+      if (childActor) {
+        this.releaseNode(childActor);
+      }
+      child = walker.nextSibling();
+    }
+
+    node.destroy();
+  }, {
+    request: { node: Arg(0, "domnode") }
+  }),
+
+  /**
+   * Add any nodes between `node` and the walker's root node that have not
+   * yet been seen by the client.
+   */
+  ensurePathToRoot: function(node, newParents=new Set()) {
+    if (!node) {
+      return newParents;
+    }
+    let walker = documentWalker(node.rawNode);
+    let cur;
+    while ((cur = walker.parentNode())) {
+      let parent = this._refMap.get(cur);
+      if (!parent) {
+        // This parent didn't exist, so hasn't been seen by the client yet.
+        newParents.add(this._ref(cur));
+      } else {
+        // This parent did exist, so the client knows about it.
+        return newParents;
+      }
+    }
+    return newParents;
+  },
+
+  /**
+   * Return children of the given node.  By default this method will return
+   * all children of the node, but there are options that can restrict this
+   * to a more manageable subset.
+   *
+   * @param NodeActor node
+   *    The node whose children you're curious about.
+   * @param object options
+   *    Named options:
+   *    `maxNodes`: The set of nodes returned by the method will be no longer
+   *       than maxNodes.
+   *    `start`: If a node is specified, the list of nodes will start
+   *       with the given child.  Mutally exclusive with `center`.
+   *    `center`: If a node is specified, the given node will be as centered
+   *       as possible in the list, given how close to the ends of the child
+   *       list it is.  Mutually exclusive with `start`.
+   *    `whatToShow`: A bitmask of node types that should be included.  See
+   *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
+   *
+   * @returns an object with three items:
+   *    hasFirst: true if the first child of the node is included in the list.
+   *    hasLast: true if the last child of the node is included in the list.
+   *    nodes: Child nodes returned by the request.
+   */
+  children: method(function(node, options={}) {
+    if (options.center && options.start) {
+      throw Error("Can't specify both 'center' and 'start' options.");
+    }
+    let maxNodes = options.maxNodes || -1;
+    if (maxNodes == -1) {
+      maxNodes = Number.MAX_VALUE;
+    }
+
+    // We're going to create a few document walkers with the same filter,
+    // make it easier.
+    let filteredWalker = function(node) {
+      return documentWalker(node, options.whatToShow);
+    }
+
+    // Need to know the first and last child.
+    let rawNode = node.rawNode;
+    let firstChild = filteredWalker(rawNode).firstChild();
+    let lastChild = filteredWalker(rawNode).lastChild();
+
+    if (!firstChild) {
+      // No children, we're done.
+      return { hasFirst: true, hasLast: true, nodes: [] };
+    }
+
+    let start;
+    if (options.center) {
+      start = options.center.rawNode;
+    } else if (options.start) {
+      start = options.start.rawNode;
+    } else {
+      start = firstChild;
+    }
+
+    let nodes = [];
+
+    // Start by reading backward from the starting point if we're centering...
+    let backwardWalker = filteredWalker(start);
+    if (start != firstChild && options.center) {
+      backwardWalker.previousSibling();
+      let backwardCount = Math.floor(maxNodes / 2);
+      let backwardNodes = this._readBackward(backwardWalker, backwardCount);
+      nodes = backwardNodes;
+    }
+
+    // Then read forward by any slack left in the max children...
+    let forwardWalker = filteredWalker(start);
+    let forwardCount = maxNodes - nodes.length;
+    nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
+
+    // If there's any room left, it means we've run all the way to the end.
+    // If we're centering, check if there are more items to read at the front.
+    let remaining = maxNodes - nodes.length;
+    if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
+      let firstNodes = this._readBackward(backwardWalker, remaining);
+
+      // Then put it all back together.
+      nodes = firstNodes.concat(nodes);
+    }
+
+    return {
+      hasFirst: nodes[0].rawNode == firstChild,
+      hasLast: nodes[nodes.length - 1].rawNode == lastChild,
+      nodes: nodes
+    };
+  }, nodeArrayMethod),
+
+  /**
+   * Return siblings of the given node.  By default this method will return
+   * all siblings of the node, but there are options that can restrict this
+   * to a more manageable subset.
+   *
+   * If `start` or `center` are not specified, this method will center on the
+   * node whose siblings are requested.
+   *
+   * @param NodeActor node
+   *    The node whose children you're curious about.
+   * @param object options
+   *    Named options:
+   *    `maxNodes`: The set of nodes returned by the method will be no longer
+   *       than maxNodes.
+   *    `start`: If a node is specified, the list of nodes will start
+   *       with the given child.  Mutally exclusive with `center`.
+   *    `center`: If a node is specified, the given node will be as centered
+   *       as possible in the list, given how close to the ends of the child
+   *       list it is.  Mutually exclusive with `start`.
+   *    `whatToShow`: A bitmask of node types that should be included.  See
+   *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
+   *
+   * @returns an object with three items:
+   *    hasFirst: true if the first child of the node is included in the list.
+   *    hasLast: true if the last child of the node is included in the list.
+   *    nodes: Child nodes returned by the request.
+   */
+  siblings: method(function(node, options={}) {
+    let parentNode = documentWalker(node.rawNode).parentNode();
+    if (!parentNode) {
+      return {
+        hasFirst: true,
+        hasLast: true,
+        nodes: [node]
+      };
+    }
+
+    if (!(options.start || options.center)) {
+      options.center = node;
+    }
+
+    return this.children(this._ref(parentNode), options);
+  }, nodeArrayMethod),
+
+  /**
+   * Get the next sibling of a given node.  Getting nodes one at a time
+   * might be inefficient, be careful.
+   *
+   * @param object options
+   *    Named options:
+   *    `whatToShow`: A bitmask of node types that should be included.  See
+   *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
+   */
+  nextSibling: method(function(node, options={}) {
+    let walker = documentWalker(node.rawNode, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
+    return this._ref(walker.nextSibling());
+  }, traversalMethod),
+
+  /**
+   * Get the previous sibling of a given node.  Getting nodes one at a time
+   * might be inefficient, be careful.
+   *
+   * @param object options
+   *    Named options:
+   *    `whatToShow`: A bitmask of node types that should be included.  See
+   *       https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
+   */
+  previousSibling: method(function(node, options={}) {
+    let walker = documentWalker(node.rawNode, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
+    return this._ref(walker.previousSibling());
+  }, traversalMethod),
+
+  /**
+   * Helper function for the `children` method: Read forward in the sibling
+   * list into an array with `count` items, including the current node.
+   */
+  _readForward: function(walker, count)
+  {
+    let ret = [];
+    let node = walker.currentNode;
+    do {
+      ret.push(this._ref(node));
+      node = walker.nextSibling();
+    } while (node && --count);
+    return ret;
+  },
+
+  /**
+   * Helper function for the `children` method: Read backward in the sibling
+   * list into an array with `count` items, including the current node.
+   */
+  _readBackward: function(walker, count)
+  {
+    let ret = [];
+    let node = walker.currentNode;
+    do {
+      ret.push(this._ref(node));
+      node = walker.previousSibling();
+    } while(node && --count);
+    ret.reverse();
+    return ret;
+  },
+
+  /**
+   * Return the first node in the document that matches the given selector.
+   * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
+   *
+   * @param NodeActor baseNode
+   * @param string selector
+   */
+  querySelector: method(function(baseNode, selector) {
+    let node = baseNode.rawNode.querySelector(selector);
+
+    if (!node) {
+      return {
+      }
+    };
+
+    let node = this._ref(node);
+    let newParents = this.ensurePathToRoot(node);
+    return {
+      node: node,
+      newNodes: [parent for (parent of newParents)]
+    }
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      selector: Arg(1)
+    },
+    response: RetVal("disconnectedNode")
+  }),
+
+  /**
+   * Return a NodeListActor with all nodes that match the given selector.
+   * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
+   *
+   * @param NodeActor baseNode
+   * @param string selector
+   */
+  querySelectorAll: method(function(baseNode, selector) {
+    return new NodeListActor(this, baseNode.rawNode.querySelectorAll(selector));
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      selector: Arg(1)
+    },
+    response: {
+      list: RetVal("domnodelist")
+    }
+  }),
+
+  /**
+   * Get any pending mutation records.  Must be called by the client after
+   * the `new-mutations` notification is received.  Returns an array of
+   * mutation records.
+   *
+   * Mutation records have a basic structure:
+   *
+   * {
+   *   type: attributes|characterData|childList,
+   *   target: <domnode actor ID>,
+   * }
+   *
+   * And additional attributes based on the mutation type:
+   *
+   * `attributes` type:
+   *   attributeName: <string> - the attribute that changed
+   *   attributeNamespace: <string> - the attribute's namespace URI, if any.
+   *   newValue: <string> - The new value of the attribute, if any.
+   *
+   * `characterData` type:
+   *   newValue: <string> - the new shortValue for the node
+   *   [incompleteValue: true] - True if the shortValue was truncated.
+   *
+   * `childList` type is returned when the set of children for a node
+   * has changed.  Includes extra data, which can be used by the client to
+   * maintain its ownership subtree.
+   *
+   *   added: array of <domnode actor ID> - The list of actors *previously
+   *     seen by the client* that were added to the target node.
+   *   removed: array of <domnode actor ID> The list of actors *previously
+   *     seen by the client* that were removed from the target node.
+   *
+   * Actors that are included in a MutationRecord's `removed` but
+   * not in an `added` have been removed from the client's ownership
+   * tree (either by being moved under a node the client has seen yet
+   * or by being removed from the tree entirely), and is considered
+   * 'orphaned'.
+   *
+   * Keep in mind that if a node that the client hasn't seen is moved
+   * into or out of the target node, it will not be included in the
+   * removedNodes and addedNodes list, so if the client is interested
+   * in the new set of children it needs to issue a `children` request.
+   */
+  getMutations: method(function(options={}) {
+    let pending = this._pendingMutations || [];
+    this._pendingMutations = [];
+
+    if (options.cleanup) {
+      for (let node of this._orphaned) {
+        this.releaseNode(node);
+      }
+      this._orphaned = new Set();
+    }
+
+    return pending;
+  }, {
+    request: {
+      cleanup: Option(0)
+    },
+    response: {
+      mutations: RetVal("array:dommutation")
+    }
+  }),
+
+  /**
+   * Handles mutations from the DOM mutation observer API.
+   *
+   * @param array[MutationRecord] mutations
+   *    See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
+   */
+  onMutations: function(mutations) {
+    // We only send the `new-mutations` notification once, until the client
+    // fetches mutations with the `getMutations` packet.
+    let needEvent = this._pendingMutations.length === 0;
+
+    for (let change of mutations) {
+      let targetActor = this._refMap.get(change.target);
+      if (!targetActor) {
+        continue;
+      }
+      let targetNode = change.target;
+      let mutation = {
+        type: change.type,
+        target: targetActor.actorID,
+      }
+
+      if (mutation.type === "attributes") {
+        mutation.attributeName = change.attributeName;
+        mutation.attributeNamespace = change.attributeNamespace || undefined;
+        mutation.newValue = targetNode.getAttribute(mutation.attributeName);
+      } else if (mutation.type === "characterData") {
+        if (targetNode.nodeValue.length > gValueSummaryLength) {
+          mutation.newValue = targetNode.nodeValue.substring(0, gValueSummaryLength);
+          mutation.incompleteValue = true;
+        } else {
+          mutation.newValue = targetNode.nodeValue;
+        }
+      } else if (mutation.type === "childList") {
+        // Get the list of removed and added actors that the client has seen
+        // so that it can keep its ownership tree up to date.
+        let removedActors = [];
+        let addedActors = [];
+        for (let removed of change.removedNodes) {
+          let removedActor = this._refMap.get(removed);
+          if (!removedActor) {
+            // If the client never encountered this actor we don't need to
+            // mention that it was removed.
+            continue;
+          }
+          // While removed from the tree, nodes are saved as orphaned.
+          this._orphaned.add(removedActor);
+          removedActors.push(removedActor.actorID);
+        }
+        for (let added of change.addedNodes) {
+          let addedActor = this._refMap.get(added);
+          if (!addedActor) {
+            // If the client never encounted this actor we don't need to tell
+            // it about its addition for ownership tree purposes - if the
+            // client wants to see the new nodes it can ask for children.
+            continue;
+          }
+          // The actor is reconnected to the ownership tree, unorphan
+          // it and let the client know so that its ownership tree is up
+          // to date.
+          this._orphaned.delete(addedActor);
+          addedActors.push(addedActor.actorID);
+        }
+        mutation.numChildren = change.target.childNodes.length;
+        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, {
+  // Set to true if cleanup should be requested after every mutation list.
+  autoCleanup: true,
+
+  initialize: function(client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+    this._orphaned = new Set();
+  },
+
+  destroy: function() {
+    protocol.Front.prototype.destroy.call(this);
+  },
+
+  // Update the object given a form representation off the wire.
+  form: function(json) {
+    this.actorID = json.actorID;
+    this.rootNode = types.getType("domnode").read(json.root, this);
+  },
+
+  /**
+   * When reading an actor form off the wire, we want to hook it up to its
+   * parent front.  The protocol guarantees that the parent will be seen
+   * by the client in either a previous or the current request.
+   * So if we've already seen this parent return it, otherwise create
+   * a bare-bones stand-in node.  The stand-in node will be updated
+   * with a real form by the end of the deserialization.
+   */
+  ensureParentFront: function(id) {
+    let front = this.get(id);
+    if (front) {
+      return front;
+    }
+
+    return types.getType("domnode").read({ actor: id }, this, "standin");
+  },
+
+  releaseNode: protocol.custom(function(node) {
+    // NodeFront.destroy will destroy children in the ownership tree too,
+    // mimicking what the server will do here.
+    let actorID = node.actorID;
+    node.destroy();
+    return this._releaseNode({ actorID: actorID });
+  }, {
+    impl: "_releaseNode"
+  }),
+
+  querySelector: protocol.custom(function(queryNode, selector) {
+    return this._querySelector(queryNode, selector).then(response => {
+      return response.node;
+    });
+  }, {
+    impl: "_querySelector"
+  }),
+
+  /**
+   * Get any unprocessed mutation records and process them.
+   */
+  getMutations: protocol.custom(function(options={}) {
+    return this._getMutations(options).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.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 = [];
+          let removedFronts = [];
+          for (let removed of change.removed) {
+            let removedFront = this.get(removed);
+            if (!removedFront) {
+              console.error("Got a removal of an actor we didn't know about: " + removed);
+              continue;
+            }
+            // Remove from the ownership tree
+            removedFront.reparent(null);
+
+            // This node is orphaned unless we get it in the 'added' list
+            // eventually.
+            this._orphaned.add(removedFront);
+            removedFronts.push(removedFront);
+          }
+          for (let added of change.added) {
+            let addedFront = this.get(added);
+            if (!addedFront) {
+              console.error("Got an addition of an actor we didn't know about: " + added);
+              continue;
+            }
+            addedFront.reparent(targetFront)
+
+            // The actor is reconnected to the ownership tree, unorphan
+            // it.
+            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);
+      }
+
+      if (options.cleanup) {
+        for (let node of this._orphaned) {
+          node.destroy();
+        }
+        this._orphaned = new Set();
+      }
+
+      events.emit(this, "mutations", emitMutations);
+    });
+  }, {
+    impl: "_getMutations"
+  }),
+
+  /**
+   * Handle the `new-mutations` notification by fetching the
+   * available mutation records.
+   */
+  onMutations: protocol.preEvent("new-mutations", function() {
+    // Fetch and process the mutations.
+    this.getMutations({cleanup: this.autoCleanup}).then(null, console.error);
+  }),
+
+  // XXX hack during transition to remote inspector: get a proper NodeFront
+  // for a given local node.  Only works locally.
+  frontForRawNode: function(rawNode){
+    if (!this.conn._transport._serverConnection) {
+      throw Error("Tried to use frontForRawNode on a remote connection.");
+    }
+    let actor = this.conn._transport._serverConnection.getActor(this.actorID);
+    if (!actor) {
+      throw Error("Could not find client side for actor " + this.actorID);
+    }
+    let nodeActor = actor._ref(rawNode);
+
+    // Pass the node through a read/write pair to create the client side actor.
+    let nodeType = types.getType("domnode");
+    return nodeType.read(nodeType.write(nodeActor, actor), this);
+  }
+});
+
+/**
+ * Server side of the inspector actor, which is used to create
+ * inspector-related actors, including the walker.
+ */
+var InspectorActor = protocol.ActorClass({
+  typeName: "inspector",
+  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, this.webProgress, options);
+  }, {
+    request: {},
+    response: {
+      walker: RetVal("domwalker")
+    }
+  })
+});
+
+/**
+ * Client side of the inspector actor, which is used to create
+ * inspector-related actors, including the walker.
+ */
+var InspectorFront = exports.InspectorFront = protocol.FrontClass(InspectorActor, {
+  initialize: function(client, tabForm) {
+    protocol.Front.prototype.initialize.call(this, client);
+    this.actorID = tabForm.inspectorActor;
+
+    // XXX: This is the first actor type in its hierarchy to use the protocol
+    // library, so we're going to self-own on the client side for now.
+    client.addActorPool(this);
+    this.manage(this);
+  }
+});
+
+function documentWalker(node, whatToShow=Ci.nsIDOMNodeFilter.SHOW_ALL) {
+  return new DocumentWalker(node, whatToShow, whitespaceTextFilter, false);
+}
+
+// Exported for test purposes.
+exports._documentWalker = documentWalker;
+
+function nodeDocument(node) {
+  return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
+}
+
+/**
+ * Similar to a TreeWalker, except will dig in to iframes and it doesn't
+ * implement the good methods like previousNode and nextNode.
+ *
+ * See TreeWalker documentation for explanations of the methods.
+ */
+function DocumentWalker(aNode, aShow, aFilter, aExpandEntityReferences)
+{
+  let doc = nodeDocument(aNode);
+  this.walker = doc.createTreeWalker(nodeDocument(aNode),
+    aShow, aFilter, aExpandEntityReferences);
+  this.walker.currentNode = aNode;
+  this.filter = aFilter;
+}
+
+DocumentWalker.prototype = {
+  get node() this.walker.node,
+  get whatToShow() this.walker.whatToShow,
+  get expandEntityReferences() this.walker.expandEntityReferences,
+  get currentNode() this.walker.currentNode,
+  set currentNode(aVal) this.walker.currentNode = aVal,
+
+  /**
+   * Called when the new node is in a different document than
+   * the current node, creates a new treewalker for the document we've
+   * run in to.
+   */
+  _reparentWalker: function DW_reparentWalker(aNewNode) {
+    if (!aNewNode) {
+      return null;
+    }
+    let doc = nodeDocument(aNewNode);
+    let walker = doc.createTreeWalker(doc,
+      this.whatToShow, this.filter, this.expandEntityReferences);
+    walker.currentNode = aNewNode;
+    this.walker = walker;
+    return aNewNode;
+  },
+
+  parentNode: function DW_parentNode()
+  {
+    let currentNode = this.walker.currentNode;
+    let parentNode = this.walker.parentNode();
+
+    if (!parentNode) {
+      if (currentNode && currentNode.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE
+          && currentNode.defaultView) {
+        let embeddingFrame = currentNode.defaultView.frameElement;
+        if (embeddingFrame) {
+          return this._reparentWalker(embeddingFrame);
+        }
+      }
+      return null;
+    }
+
+    return parentNode;
+  },
+
+  firstChild: function DW_firstChild()
+  {
+    let node = this.walker.currentNode;
+    if (!node)
+      return null;
+    if (node.contentDocument) {
+      return this._reparentWalker(node.contentDocument);
+    } else if (node.getSVGDocument) {
+      return this._reparentWalker(node.getSVGDocument());
+    }
+    return this.walker.firstChild();
+  },
+
+  lastChild: function DW_lastChild()
+  {
+    let node = this.walker.currentNode;
+    if (!node)
+      return null;
+    if (node.contentDocument) {
+      return this._reparentWalker(node.contentDocument);
+    } else if (node.getSVGDocument) {
+      return this._reparentWalker(node.getSVGDocument());
+    }
+    return this.walker.lastChild();
+  },
+
+  previousSibling: function DW_previousSibling() this.walker.previousSibling(),
+  nextSibling: function DW_nextSibling() this.walker.nextSibling()
+}
+
+/**
+ * A tree walker filter for avoiding empty whitespace text nodes.
+ */
+function whitespaceTextFilter(aNode)
+{
+    if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
+        !/[^\s]/.exec(aNode.nodeValue)) {
+      return Ci.nsIDOMNodeFilter.FILTER_SKIP;
+    } else {
+      return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
+    }
+}
+
+loader.lazyGetter(this, "DOMUtils", function () {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -288,16 +288,17 @@ var DebuggerServer = {
     this.addGlobalActor(this.ChromeDebuggerActor, "chromeDebugger");
     this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
     this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
     if ("nsIProfiler" in Ci)
       this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
 
     this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
     this.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
+    this.registerModule("devtools/server/actors/inspector");
   },
 
   /**
    * Listens on the given port for remote debugger connections.
    *
    * @param aPort int
    *        The port to listen on.
    */
--- a/toolkit/devtools/server/tests/mochitest/Makefile.in
+++ b/toolkit/devtools/server/tests/mochitest/Makefile.in
@@ -6,14 +6,22 @@
 DEPTH		= @DEPTH@
 topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir	= @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
-MOCHITEST_CHROME_FILES =			\
-	test_unsafeDereference.html		\
-	nonchrome_unsafeDereference.html	\
+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/inspector-helpers.js
@@ -0,0 +1,185 @@
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Always log packets when running tests.
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+SimpleTest.registerCleanupFunction(function() {
+  Services.prefs.clearUserPref("devtools.debugger.log");
+});
+
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+
+const {_documentWalker} = devtools.require("devtools/server/actors/inspector");
+
+if (!DebuggerServer.initialized) {
+  DebuggerServer.init(() => true);
+  DebuggerServer.addBrowserActors();
+  SimpleTest.registerCleanupFunction(function() {
+    DebuggerServer.destroy();
+  });
+}
+
+var gAttachCleanups = [];
+
+SimpleTest.registerCleanupFunction(function() {
+  for (let cleanup of gAttachCleanups) {
+    cleanup();
+  }
+});
+
+/**
+ * Open a tab, load the url, wait for it to signal its readiness,
+ * find the tab with the debugger server, and call the callback.
+ *
+ * Returns a function which can be called to close the opened ta
+ * and disconnect its debugger client.
+ */
+function attachURL(url, callback) {
+  var win = window.open(url, "_blank");
+  var client = null;
+
+  let cleanup = () => {
+    if (client) {
+      client.close();
+      client = null;
+    }
+    if (win) {
+      win.close();
+      win = null;
+    }
+  };
+  gAttachCleanups.push(cleanup);
+
+  window.addEventListener("message", function loadListener(event) {
+    if (event.data === "ready") {
+      client = new DebuggerClient(DebuggerServer.connectPipe());
+      client.connect((applicationType, traits) => {
+        client.listTabs(response => {
+          for (let tab of response.tabs) {
+            if (tab.url === url) {
+              window.removeEventListener("message", loadListener, false);
+              try {
+                callback(null, client, tab, win.document);
+              } catch(ex) {
+                Cu.reportError(ex);
+                dump(ex);
+              }
+              break;
+            }
+          }
+        });
+      });
+    }
+  }, false);
+
+  return cleanup;
+}
+
+function sortOwnershipChildren(children) {
+  return children.sort((a, b) => a.name.localeCompare(b.name));
+}
+
+function serverOwnershipSubtree(walker, node) {
+  let actor = walker._refMap.get(node);
+  if (!actor) {
+    return undefined;
+  }
+
+  let children = [];
+  let docwalker = _documentWalker(node);
+  let child = docwalker.firstChild();
+  while (child) {
+    let item = serverOwnershipSubtree(walker, child);
+    if (item) {
+      children.push(item);
+    }
+    child = docwalker.nextSibling();
+  }
+  return {
+    name: actor.actorID,
+    children: sortOwnershipChildren(children)
+  }
+}
+
+function serverOwnershipTree(walker) {
+  let serverConnection = walker.conn._transport._serverConnection;
+  let serverWalker = serverConnection.getActor(walker.actorID);
+
+  return {
+    root: serverOwnershipSubtree(serverWalker, serverWalker.rootDoc ),
+    orphaned: [serverOwnershipSubtree(serverWalker, o.rawNode) for (o of serverWalker._orphaned)]
+  };
+}
+
+function clientOwnershipSubtree(node) {
+  return {
+    name: node.actorID,
+    children: sortOwnershipChildren([clientOwnershipSubtree(child) for (child of node.treeChildren())])
+  }
+}
+
+function clientOwnershipTree(walker) {
+  return {
+    root: clientOwnershipSubtree(walker.rootNode),
+    orphaned: [clientOwnershipSubtree(o) for (o of walker._orphaned)]
+  }
+}
+
+function ownershipTreeSize(tree) {
+  let size = 1;
+  for (let child of tree.children) {
+    size += ownershipTreeSize(child);
+  }
+  return size;
+}
+
+function assertOwnershipTrees(walker) {
+  let serverTree = serverOwnershipTree(walker);
+  let clientTree = clientOwnershipTree(walker);
+  is(JSON.stringify(clientTree, null, ' '), JSON.stringify(serverTree, null, ' '), "Server and client ownership trees should match.");
+
+  return ownershipTreeSize(clientTree.root);
+}
+
+// Verify that an actorID is inaccessible both from the client library and the server.
+function checkMissing(client, actorID) {
+  let deferred = Promise.defer();
+  let front = client.getActor(actorID);
+  ok(!front, "Front shouldn't be accessible from the client for actorID: " + actorID);
+
+  let deferred = Promise.defer();
+  client.request({
+    to: actorID,
+    type: "request",
+  }, response => {
+    is(response.error, "noSuchActor", "node list actor should no longer be contactable.");
+    deferred.resolve(undefined);
+  });
+  return deferred.promise;
+}
+
+function promiseDone(promise) {
+  promise.then(null, err => {
+    ok(false, "Promise failed: " + err);
+    if (err.stack) {
+      dump(err.stack);
+    }
+    SimpleTest.finish();
+  });
+}
+
+var _tests = [];
+function addTest(test) {
+  _tests.push(test);
+}
+
+function runNextTest() {
+  if (_tests.length == 0) {
+    SimpleTest.finish()
+    return;
+  }
+  _tests.shift()();
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/inspector-traversal-data.html
@@ -0,0 +1,54 @@
+<html>
+<head>
+  <script type="text/javascript">
+    window.onload = function() {
+      // Put a copy of the body in an iframe to test frame traversal.
+      var body = document.querySelector("body");
+      var data = "data:text/html,<html>" + body.outerHTML + "<html>";
+      var iframe = document.createElement("iframe");
+      iframe.setAttribute("id", "childFrame");
+      iframe.onload = function() {
+        window.opener.postMessage('ready', '*')
+      };
+      iframe.src = data;
+      body.appendChild(iframe);
+    }
+  </script>
+<body>
+  <h1>Inspector Actor Tests</h1>
+  <span id="longstring">longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong</span>
+  <span id="shortstring">short</span>
+  <span id="empty"></span>
+  <div id="longlist" data-test="exists">
+    <div id="a">a</div>
+    <div id="b">b</div>
+    <div id="c">c</div>
+    <div id="d">d</div>
+    <div id="e">e</div>
+    <div id="f">f</div>
+    <div id="g">g</div>
+    <div id="h">h</div>
+    <div id="i">i</div>
+    <div id="j">j</div>
+    <div id="k">k</div>
+    <div id="l">l</div>
+    <div id="m">m</div>
+    <div id="n">n</div>
+    <div id="o">o</div>
+    <div id="p">p</div>
+    <div id="q">q</div>
+    <div id="r">r</div>
+    <div id="s">s</div>
+    <div id="t">t</div>
+    <div id="u">u</div>
+    <div id="v">v</div>
+    <div id="w">w</div>
+    <div id="x">x</div>
+    <div id="y">y</div>
+    <div id="z">z</div>
+  </div>
+  <div id="longlist-sibling">
+    <div id="longlist-sibling-firstchild"></div>
+  </div>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector-mutations-attr.html
@@ -0,0 +1,128 @@
+<!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 attrNode;
+var attrFront;
+
+addTest(function setup() {
+  let url = document.getElementById("inspectorContent").href;
+  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 => {
+      ok(walker, "getWalker() should return an actor.");
+      gClient = client;
+      gWalker = walker;
+    }).then(runNextTest));
+  });
+});
+
+addTest(setupAttrTest);
+addTest(testAddAttribute);
+addTest(testChangeAttribute);
+addTest(testRemoveAttribute);
+addTest(setupFrameAttrTest);
+addTest(testAddAttribute);
+addTest(testChangeAttribute);
+addTest(testRemoveAttribute);
+
+
+function setupAttrTest() {
+  attrNode = gInspectee.querySelector("#a")
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#a").then(node => {
+    attrFront = node;
+  }).then(runNextTest));
+}
+
+function setupFrameAttrTest() {
+  let frame = gInspectee.querySelector('#childFrame');
+  attrNode = frame.contentDocument.querySelector("#a");
+
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#childFrame").then(childFrame => {
+    return gWalker.children(childFrame);
+  }).then(children => {
+    let nodes = children.nodes;
+    ok(nodes.length, 1, "There should be only one child of the iframe");
+    is(nodes[0].nodeType, Node.DOCUMENT_NODE, "iframe child should be a document node");
+    return gWalker.querySelector(nodes[0], "#a");
+  }).then(node => {
+    attrFront = node;
+  }).then(runNextTest));
+}
+
+function testAddAttribute() {
+  attrNode.setAttribute("data-newattr", "newvalue");
+  attrNode.setAttribute("data-newattr2", "newvalue");
+  gWalker.once("mutations", () => {
+    is(attrFront.attributes.length, 3, "Should have id and two new attributes.");
+    is(attrFront.getAttribute("data-newattr"), "newvalue", "Node front should have the first new attribute");
+    is(attrFront.getAttribute("data-newattr2"), "newvalue", "Node front should have the second new attribute.");
+    runNextTest();
+  });
+}
+
+function testChangeAttribute() {
+  attrNode.setAttribute("data-newattr", "changedvalue");
+  gWalker.once("mutations", () => {
+    is(attrFront.attributes.length, 3, "Should have id and two new attributes.");
+    is(attrFront.getAttribute("data-newattr"), "changedvalue", "Node front should have the changed first value");
+    is(attrFront.getAttribute("data-newattr2"), "newvalue", "Second value should remain unchanged.");
+    runNextTest();
+  });
+}
+
+function testRemoveAttribute() {
+  attrNode.removeAttribute("data-newattr2");
+  gWalker.once("mutations", () => {
+    is(attrFront.attributes.length, 2, "Should have id and one remaining attribute.");
+    is(attrFront.getAttribute("data-newattr"), "changedvalue", "Node front should still have the first value");
+    ok(!attrFront.hasAttribute("data-newattr2"), "Second value should be removed.");
+    runNextTest();
+  })
+}
+
+addTest(function cleanup() {
+  delete gInspectee;
+  delete gWalker;
+  delete gClient;
+  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>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector-mutations-childlist.html
@@ -0,0 +1,313 @@
+<!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 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;
+  if (gCleanupConnection) {
+    gCleanupConnection();
+    gCleanupConnection = null;
+  }
+}
+
+function assertOwnership() {
+  let num = assertOwnershipTrees(gWalker);
+}
+
+function setParent(nodeSelector, newParentSelector) {
+  let node = gInspectee.querySelector(nodeSelector);
+  if (newParentSelector) {
+    let newParent = gInspectee.querySelector(newParentSelector);
+    newParent.appendChild(node);
+  } else {
+    node.parentNode.removeChild(node);
+  }
+}
+
+function loadSelector(selector) {
+  return gWalker.querySelectorAll(gWalker.rootNode, selector).then(nodeList => {
+    return nodeList.items();
+  });
+}
+
+function loadSelectors(selectors) {
+  return Promise.all([loadSelector(sel) for (sel of selectors)]);
+}
+
+function doMoves(moves) {
+  for (let move of moves) {
+    setParent(move[0], move[1]);
+  }
+}
+
+/**
+ * Test a set of tree rearrangements and make sure they cause the expected changes.
+ */
+
+var gDummySerial = 0;
+
+function mutationTest(testSpec) {
+  return function() {
+    setup(() => {
+      promiseDone(loadSelectors(testSpec.load || ["html"]).then(() => {
+        gWalker.autoCleanup = !!testSpec.autoCleanup;
+        if (testSpec.preCheck) {
+          testSpec.preCheck();
+        }
+        doMoves(testSpec.moves || []);
+
+        // Some of these moves will trigger no mutation events,
+        // so do a dummy change to the root node to trigger
+        // a mutation event anyway.
+        gInspectee.documentElement.setAttribute("data-dummy", gDummySerial++);
+
+        gWalker.once("mutations", (mutations) => {
+          // Filter out our dummy mutation.
+          let mutations = mutations.filter(change => {
+            if (change.type == "attributes" &&
+                change.attributeName == "data-dummy") {
+              return false;
+            }
+            return true;
+          });
+          assertOwnership();
+          if (testSpec.postCheck) {
+            testSpec.postCheck(mutations);
+          }
+          teardown();
+          runNextTest();
+        });
+      }));
+    })
+  }
+}
+
+// Verify that our dummy mutation works.
+addTest(mutationTest({
+  autoCleanup: false,
+  postCheck: function(mutations) {
+    is(mutations.length, 0, "Dummy mutation is filtered out.");
+  }
+}));
+
+// Test a simple move to a different location in the sibling list for the same
+// parent.
+addTest(mutationTest({
+  autoCleanup: false,
+  load: ["#longlist div"],
+  moves: [
+    ["#a", "#longlist"]
+  ],
+  postCheck: function(mutations) {
+    let remove = mutations[0];
+    is(remove.type, "childList", "First mutation should be a childList.")
+    ok(remove.removed.length > 0, "First mutation should be a removal.")
+    let add = mutations[1];
+    is(add.type, "childList", "Second mutation should be a childList removal.")
+    ok(add.added.length > 0, "Second mutation should be an addition.")
+    let a = add.added[0];
+    is(a.id, "a", "Added node should be #a");
+    is(a.parentNode(), remove.target, "Should still be a child of longlist.");
+    is(remove.target, add.target, "First and second mutations should be against the same node.");
+  }
+}));
+
+// Test a move to another location that is within our ownership tree.
+addTest(mutationTest({
+  autoCleanup: false,
+  load: ["#longlist div", "#longlist-sibling"],
+  moves: [
+    ["#a", "#longlist-sibling"]
+  ],
+  postCheck: function(mutations) {
+    let remove = mutations[0];
+    is(remove.type, "childList", "First mutation should be a childList.")
+    ok(remove.removed.length > 0, "First mutation should be a removal.")
+    let add = mutations[1];
+    is(add.type, "childList", "Second mutation should be a childList removal.")
+    ok(add.added.length > 0, "Second mutation should be an addition.")
+    let a = add.added[0];
+    is(a.id, "a", "Added node should be #a");
+    is(a.parentNode(), add.target, "Should still be a child of longlist.");
+    is(add.target.id, "longlist-sibling", "long-sibling should be the target.");
+  }
+}));
+
+// Move an unseen node with a seen parent into our ownership tree - should generate a
+// childList pair with no adds or removes.
+addTest(mutationTest({
+  autoCleanup: false,
+  load: ["#longlist"],
+  moves: [
+    ["#longlist-sibling", "#longlist"]
+  ],
+  postCheck: function(mutations) {
+    is(mutations.length, 2, "Should generate two mutations");
+    is(mutations[0].type, "childList", "Should be childList mutations.");
+    is(mutations[0].added.length, 0, "Should have no adds.");
+    is(mutations[0].removed.length, 0, "Should have no removes.");
+    is(mutations[1].type, "childList", "Should be childList mutations.");
+    is(mutations[1].added.length, 0, "Should have no adds.");
+    is(mutations[1].removed.length, 0, "Should have no removes.");
+  }
+}));
+
+// Move an unseen node with an unseen parent into our ownership tree.  Should only
+// generate one childList mutation with no adds or removes.
+addTest(mutationTest({
+  autoCleanup: false,
+  load: ["#longlist div"],
+  moves: [
+    ["#longlist-sibling-firstchild", "#longlist"]
+  ],
+  postCheck: function(mutations) {
+    is(mutations.length, 1, "Should generate two mutations");
+    is(mutations[0].type, "childList", "Should be childList mutations.");
+    is(mutations[0].added.length, 0, "Should have no adds.");
+    is(mutations[0].removed.length, 0, "Should have no removes.");
+  }
+}));
+
+// Move a node between unseen nodes, should generate no mutations.
+addTest(mutationTest({
+  autoCleanup: false,
+  load: ["html"],
+  moves: [
+    ["#longlist-sibling", "#longlist"]
+  ],
+  postCheck: function(mutations) {
+    is(mutations.length, 0, "Should generate no mutations.");
+  }
+}));
+
+// Orphan a node and don't clean it up
+addTest(mutationTest({
+  autoCleanup: false,
+  load: ["#longlist div"],
+  moves: [
+    ["#longlist", null]
+  ],
+  postCheck: function(mutations) {
+    is(mutations.length, 1, "Should generate one mutation.");
+    let change = mutations[0];
+    is(change.type, "childList", "Should be a childList.");
+    is(change.removed.length, 1, "Should have removed a child.");
+    let ownership = clientOwnershipTree(gWalker);
+    is(ownership.orphaned.length, 1, "Should have one orphaned subtree.");
+    is(ownershipTreeSize(ownership.orphaned[0]), 27, "Should have orphaned longlist and 26 children.");
+  }
+}));
+
+// Orphan a node, and do clean it up.
+addTest(mutationTest({
+  autoCleanup: true,
+  load: ["#longlist div"],
+  moves: [
+    ["#longlist", null]
+  ],
+  postCheck: function(mutations) {
+    is(mutations.length, 1, "Should generate one mutation.");
+    let change = mutations[0];
+    is(change.type, "childList", "Should be a childList.");
+    is(change.removed.length, 1, "Should have removed a child.");
+    let ownership = clientOwnershipTree(gWalker);
+    is(ownership.orphaned.length, 0, "Should have no orphaned subtrees.");
+  }
+}));
+
+// Orphan a node by moving it into the tree but out of our visible subtree.
+addTest(mutationTest({
+  autoCleanup: false,
+  load: ["#longlist div"],
+  moves: [
+    ["#longlist", "#longlist-sibling"]
+  ],
+  postCheck: function(mutations) {
+    is(mutations.length, 1, "Should generate one mutation.");
+    let change = mutations[0];
+    is(change.type, "childList", "Should be a childList.");
+    is(change.removed.length, 1, "Should have removed a child.");
+    let ownership = clientOwnershipTree(gWalker);
+    is(ownership.orphaned.length, 1, "Should have one orphaned subtree.");
+    is(ownershipTreeSize(ownership.orphaned[0]), 27, "Should have orphaned longlist and 26 children.");
+  }
+}));
+
+// Orphan a node by moving it into the tree but out of our visible subtree, and clean it up.
+addTest(mutationTest({
+  autoCleanup: true,
+  load: ["#longlist div"],
+  moves: [
+    ["#longlist", "#longlist-sibling"]
+  ],
+  postCheck: function(mutations) {
+    is(mutations.length, 1, "Should generate one mutation.");
+    let change = mutations[0];
+    is(change.type, "childList", "Should be a childList.");
+    is(change.removed.length, 1, "Should have removed a child.");
+    let ownership = clientOwnershipTree(gWalker);
+    is(ownership.orphaned.length, 0, "Should have no orphaned subtrees.");
+  }
+}));
+
+
+addTest(function cleanup() {
+  delete gInspectee;
+  delete gWalker;
+  delete gClient;
+  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>
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>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector-mutations-value.html
@@ -0,0 +1,151 @@
+<!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();
+}
+
+const testSummaryLength = 10;
+inspector.setValueSummaryLength(testSummaryLength);
+SimpleTest.registerCleanupFunction(function() {
+  inspector.setValueSummaryLength(inspector.DEFAULT_VALUE_SUMMARY_LENGTH);
+});
+
+var gInspectee = null;
+var gWalker = null;
+var gClient = null;
+var valueNode;
+var valueFront;
+var longString = "stringstringstringstringstringstringstringstringstringstringstring";
+var truncatedLongString = longString.substring(0, testSummaryLength);
+var shortString = "str";
+var shortString2 = "str2";
+
+addTest(function setup() {
+  let url = document.getElementById("inspectorContent").href;
+  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 => {
+      ok(walker, "getWalker() should return an actor.");
+      gClient = client;
+      gWalker = walker;
+    }).then(runNextTest));
+  });
+});
+
+addTest(setupValueTest);
+addTest(testKeepLongValue);
+addTest(testSetShortValue);
+addTest(testKeepShortValue);
+addTest(testSetLongValue);
+addTest(setupFrameValueTest);
+addTest(testKeepLongValue);
+addTest(testSetShortValue);
+addTest(testKeepShortValue);
+addTest(testSetLongValue);
+
+function setupValueTest() {
+  valueNode = gInspectee.querySelector("#longstring").firstChild;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#longstring").then(node => {
+    return gWalker.children(node);
+  }).then(children => {
+    valueFront = children.nodes[0];
+  }).then(runNextTest));
+}
+
+function setupFrameValueTest() {
+  let frame = gInspectee.querySelector('#childFrame');
+  valueNode = frame.contentDocument.querySelector("#longstring").firstChild;
+
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#childFrame").then(childFrame => {
+    return gWalker.children(childFrame);
+  }).then(children => {
+    let nodes = children.nodes;
+    ok(nodes.length, 1, "There should be only one child of the iframe");
+    is(nodes[0].nodeType, Node.DOCUMENT_NODE, "iframe child should be a document node");
+    return gWalker.querySelector(nodes[0], "#longstring");
+  }).then(node => {
+    return gWalker.children(node);
+  }).then(children => {
+    valueFront = children.nodes[0];
+  }).then(runNextTest));
+}
+
+function testKeepLongValue() {
+  // After first setup we should have a long string in the node
+  is(valueFront.shortValue.length, testSummaryLength, "After setup the test node should be truncated.");
+  ok(valueFront.incompleteValue, "After setup the node value should be incomplete.");
+  valueNode.nodeValue = longString;
+  gWalker.once("mutations", () => {
+    is(valueFront.shortValue, truncatedLongString, "Value should have changed to a truncated value");
+    ok(valueFront.incompleteValue, "Node value should stay incomplete.");
+    runNextTest();
+  });
+}
+
+function testSetShortValue() {
+  valueNode.nodeValue = shortString;
+  gWalker.once("mutations", () => {
+    is(valueFront.shortValue, shortString, "Value should not be truncated.");
+    ok(!valueFront.incompleteValue, "Value should not be incomplete.");
+    runNextTest();
+  });
+}
+
+function testKeepShortValue() {
+  valueNode.nodeValue = shortString2;
+  gWalker.once("mutations", () => {
+    is(valueFront.shortValue, shortString2, "Value should not be truncated.");
+    ok(!valueFront.incompleteValue, "Value should not be incomplete.");
+    runNextTest();
+  });
+}
+
+function testSetLongValue() {
+  valueNode.nodeValue = longString;
+  gWalker.once("mutations", () => {
+    is(valueFront.shortValue, truncatedLongString, "Value should have changed to a truncated value");
+    ok(valueFront.incompleteValue, "Node value should stay incomplete.");
+    runNextTest();
+  });
+}
+
+addTest(function cleanup() {
+  delete gInspectee;
+  delete gWalker;
+  delete gClient;
+  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>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector-release.html
@@ -0,0 +1,94 @@
+<!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 gWalker = null;
+var gClient = null;
+
+function assertOwnership() {
+  return assertOwnershipTrees(gWalker);
+}
+
+addTest(function setup() {
+  let url = document.getElementById("inspectorContent").href;
+  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 => {
+      ok(walker, "getWalker() should return an actor.");
+      gClient = client;
+      gWalker = walker;
+    }).then(runNextTest));
+  });
+});
+
+addTest(function testReleaseSubtree() {
+  let originalOwnershipSize = 0;
+  let longlist = null;
+  let firstChild = null;
+  promiseDone(gWalker.querySelectorAll(gWalker.rootNode, "#longlist div").then(list => {
+    // Make sure we have the 26 children of longlist in our ownership tree.
+    is(list.length, 26, "Expect 26 div children.");
+    // Make sure we've read in all those children and incorporated them in our ownership tree.
+    return list.items();
+  }).then((items)=> {
+    originalOwnershipSize = assertOwnership();
+    ok(originalOwnershipSize > 26, "Should have at least 26 items in our ownership tree");
+    firstChild = items[0].actorID;
+  }).then(() => {
+    // Now get the longlist and release it from the ownership tree.
+    return gWalker.querySelector(gWalker.rootNode, "#longlist");
+  }).then(node => {
+    longlist = node.actorID;
+    return gWalker.releaseNode(node);
+  }).then(() => {
+    // Our ownership size should now be 27 fewer (we forgot about #longlist + 26 children)
+    let newOwnershipSize = assertOwnership();
+    is(newOwnershipSize, originalOwnershipSize - 27, "Ownership tree should have dropped by 27 nodes");
+    // Now verify that some nodes have gone away
+    return checkMissing(gClient, longlist);
+  }).then(() => {
+    return checkMissing(gClient, firstChild);
+  }).then(runNextTest));
+});
+
+addTest(function cleanup() {
+  delete gWalker;
+  delete gClient;
+  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>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector-traversal.html
@@ -0,0 +1,287 @@
+<!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 gClient = null;
+var gWalker = null;
+var checkActorIDs = [];
+
+function assertOwnership() {
+  assertOwnershipTrees(gWalker);
+}
+addTest(function setup() {
+  let url = document.getElementById("inspectorContent").href;
+  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 => {
+      ok(walker, "getWalker() should return an actor.");
+      gClient = client;
+      gWalker = walker;
+    }).then(runNextTest));
+  });
+});
+
+addTest(function testWalkerRoot() {
+  // Make sure that refetching the root document of the walker returns the same
+  // actor as the getWalker returned.
+  promiseDone(gWalker.document().then(root => {
+    ok(root === gWalker.rootNode, "Re-fetching the document node should match the root document node.");
+    checkActorIDs.push(root.actorID);
+    assertOwnership();
+  }).then(runNextTest));
+});
+
+addTest(function testQuerySelector() {
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#longlist").then(node => {
+    is(node.getAttribute("data-test"), "exists", "should have found the right node");
+    assertOwnership();
+  }).then(() => {
+    return gWalker.querySelector(gWalker.rootNode, "unknownqueryselector").then(node => {
+      ok(!node, "Should not find a node here.");
+      assertOwnership();
+    });
+  }).then(runNextTest));
+});
+
+addTest(function testQuerySelectors() {
+  let nodeList = null;
+  let firstNode = null;
+  let nodeListID = null;
+  promiseDone(gWalker.querySelectorAll(gWalker.rootNode, "#longlist div").then(list => {
+    nodeList = list;
+    is(nodeList.length, 26, "Expect 26 div children.");
+    assertOwnership();
+    return nodeList.item(0);
+  }).then(node => {
+    firstNode = node;
+    checkActorIDs.push(node.actorID);
+    is(node.id, "a", "First child should be a");
+    assertOwnership();
+    return nodeList.items();
+  }).then(nodes => {
+    is(nodes.length, 26, "Expect 26 nodes");
+    is(nodes[0], firstNode, "First node should be reused.");
+    ok(nodes[0]._parent, "Parent node should be set.");
+    ok(nodes[0]._next || nodes[0]._prev, "Siblings should be set.");
+    ok(nodes[25]._next || nodes[25]._prev, "Siblings of " + nodes[25] + " should be set.");
+    assertOwnership();
+    return nodeList.items(-1);
+  }).then(nodes => {
+    is(nodes.length, 1, "Expect 1 node")
+    is(nodes[0].id, "z", "Expect it to be the last node.");
+    checkActorIDs.push(nodes[0].actorID);
+    // Save the node list ID so we can ensure it was destroyed.
+    nodeListID = nodeList.actorID;
+    assertOwnership();
+    return nodeList.release();
+  }).then(() => {
+    ok(!nodeList.actorID, "Actor should have been destroyed.");
+    assertOwnership();
+    return checkMissing(gClient, nodeListID);
+  }).then(runNextTest));
+});
+
+// Helper to check the response of requests that return hasFirst/hasLast/nodes
+// node lists (like `children` and `siblings`)
+function nodeArrayChecker(first, last, ids) {
+  return function(response) {
+    is(response.hasFirst, first, "Should " + (first ? "" : "not ") + " have the first node.");
+    is(response.hasLast, last, "Should " + (last ? "" : "not ") + " have the last node.");
+    is(response.nodes.length, ids.length, "Should have " + ids.length + " children listed.");
+    let responseIds = '';
+    for (node of response.nodes) {
+      responseIds += node.id;
+    }
+    is(responseIds, ids, "Correct nodes were returned.");
+    assertOwnership();
+  }
+}
+
+addTest(function testNoChildren() {
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#empty").then(empty => {
+    assertOwnership();
+    return gWalker.children(empty).then(nodeArrayChecker(true, true, ""));
+  }).then(runNextTest));
+});
+
+addTest(function testLongListTraversal() {
+  var longList;
+  var allChildren;
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#longlist").then(node => {
+    longList = node;
+    // First call with no options, expect all children.
+    assertOwnership();
+    return gWalker.children(longList).then(response => {
+      nodeArrayChecker(true, true, "abcdefghijklmnopqrstuvwxyz")(response);
+      allChildren = response.nodes;
+      assertOwnership();
+    });
+  }).then(() => {
+    // maxNodes should limit us to the first 5 nodes.
+    assertOwnership();
+    return gWalker.children(longList, { maxNodes: 5 }).then(nodeArrayChecker(true, false, 'abcde'));
+  }).then(() => {
+    assertOwnership();
+    // maxNodes with the second item centered should still give us the first 5 nodes.
+    return gWalker.children(longList, { maxNodes: 5, center: allChildren[1] }).then(
+      nodeArrayChecker(true, false, 'abcde')
+    );
+  }).then(() => {
+    // maxNodes with a center in the middle of the list should put that item in the middle
+    let center = allChildren[13];
+    is(center.id, 'n', "Make sure I know how to count letters.");
+    return gWalker.children(longList, { maxNodes: 5, center: center }).then(
+      nodeArrayChecker(false, false, 'lmnop')
+    );
+  }).then(() => {
+    // maxNodes with the second-to-last item centered should give us the last 5 nodes.
+    return gWalker.children(longList, { maxNodes: 5, center: allChildren[24] }).then(
+      nodeArrayChecker(false, true, 'vwxyz')
+    );
+  }).then(() => {
+    // maxNodes with a start in the middle should start at that node and fetch 5
+    let start = allChildren[13];
+    is(start.id, 'n', "Make sure I know how to count letters.")
+    return gWalker.children(longList, { maxNodes: 5, start: start }).then(
+      nodeArrayChecker(false, false, 'nopqr')
+    );
+  }).then(() => {
+    // maxNodes near the end should only return what's left
+    return gWalker.children(longList, { maxNodes: 5, start: allChildren[24] }).then(
+      nodeArrayChecker(false, true, 'yz')
+    );
+  }).then(runNextTest));
+});
+
+addTest(function testSiblings() {
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#a").then(a => {
+    return gWalker.siblings(a, { maxNodes: 5, center: a }).then(nodeArrayChecker(true, false, "abcde"));
+  }).then(() => {
+    return gWalker.siblings(gWalker.rootNode).then(response => {
+      ok(response.hasFirst && response.hasLast, "Has first and last.");
+      is(response.nodes.length, 1, "Has only the document element.");
+      ok(response.nodes[0] === gWalker.rootNode, "Document element is its own sibling.");
+    });
+  }).then(runNextTest));
+})
+
+addTest(function testFrameTraversal() {
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#childFrame").then(childFrame => {
+    return gWalker.children(childFrame);
+  }).then(children => {
+    let nodes = children.nodes;
+    ok(nodes.length, 1, "There should be only one child of the iframe");
+    is(nodes[0].nodeType, Node.DOCUMENT_NODE, "iframe child should be a document node");
+    return gWalker.querySelector(nodes[0], "#z");
+  }).then(childDocumentZ => {
+    return gWalker.parents(childDocumentZ);
+  }).then(parents => {
+    // Expected set of parent tag names for this item:
+    let expectedParents = ['DIV', 'BODY', 'HTML', '#document', 'IFRAME', 'BODY', 'HTML', '#document'];
+    for (let parent of parents) {
+      let expected = expectedParents.shift();
+      is(parent.nodeName, expected, "Got expected parent");
+    }
+  }).then(runNextTest));
+});
+
+addTest(function testLongValue() {
+  const testSummaryLength = 10;
+  inspector.setValueSummaryLength(testSummaryLength);
+  SimpleTest.registerCleanupFunction(function() {
+    inspector.setValueSummaryLength(inspector.DEFAULT_VALUE_SUMMARY_LENGTH);
+  });
+
+  let longstringText = gInspectee.getElementById("longstring").firstChild.nodeValue;
+
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#longstring").then(node => {
+    // Now we need to get the text node child...
+    return gWalker.children(node, { maxNodes: 1 });
+  }).then(children => {
+    let textNode = children.nodes[0];
+    is(textNode.nodeType, Node.TEXT_NODE, "Value should be a text node");
+    is(textNode.shortValue.length, 10, "Value summary should be limited to the summary value length");
+    ok(textNode.incompleteValue, "Value should be incomplete");
+    return textNode;
+  }).then(textNode => {
+    return textNode.getNodeValue();
+  }).then(value => {
+    return value.string();
+  }).then(valueStr => {
+    is(valueStr, longstringText, "Full node value should match the string from the document.");
+  }).then(runNextTest));
+});
+
+addTest(function testShortValue() {
+  let shortstringText = gInspectee.getElementById("shortstring").firstChild.nodeValue;
+
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#shortstring").then(node => {
+    // Now we need to get the text node child...
+    return gWalker.children(node, { maxNodes: 1 });
+  }).then(children => {
+    let textNode = children.nodes[0];
+    is(textNode.nodeType, Node.TEXT_NODE, "Value should be a text node");
+    is(textNode.shortValue, shortstringText, "Value should be complete");
+    ok(!textNode.incompleteValue, "Value should be complete");
+    return textNode;
+  }).then(textNode => {
+    return textNode.getNodeValue();
+  }).then(value => {
+    return value.string();
+  }).then(valueStr => {
+    is(valueStr, shortstringText, "Full node value should match the string from the document.");
+  }).then(runNextTest));
+});
+
+addTest(function testReleaseWalker() {
+  checkActorIDs.push(gWalker.actorID);
+
+  promiseDone(gWalker.release().then(() => {
+    let promises = [checkMissing(gClient, id) for (id of checkActorIDs)];
+    return Promise.all(promises)
+  }).then(runNextTest));
+});
+
+addTest(function cleanup() {
+  delete gWalker;
+  delete gInspectee;
+  delete gClient;
+  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>