merge m-c to fx-team
authorTim Taubert <ttaubert@mozilla.com>
Thu, 23 Jan 2014 10:57:35 +0100
changeset 180926 b92275eaf6a749d1d74b8a2b09bcc36935923adf
parent 180836 1a75d37e1e39bf821513a907ab6ff50d7763333f (current diff)
parent 180925 445a480ba7d1e63af08091d0e1661b9c44c4b578 (diff)
child 180927 22737775ef328cbf76553a1f6c71df97517c17e4
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone29.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 m-c to fx-team
b2g/chrome/content/shell.js
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -1098,16 +1098,18 @@ let RemoteDebugger = {
         DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
         DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
         if ("nsIProfiler" in Ci) {
           DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
         }
         DebuggerServer.registerModule("devtools/server/actors/inspector");
         DebuggerServer.registerModule("devtools/server/actors/styleeditor");
         DebuggerServer.registerModule("devtools/server/actors/stylesheets");
+        DebuggerServer.registerModule("devtools/server/actors/tracer");
+        DebuggerServer.registerModule("devtools/server/actors/webgl");
       }
       DebuggerServer.addActors('chrome://browser/content/dbg-browser-actors.js');
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
       DebuggerServer.registerModule("devtools/server/actors/device");
 
 #ifdef MOZ_WIDGET_GONK
       DebuggerServer.onConnectionChange = function(what) {
         AdbController.updateState();
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -230,17 +230,17 @@ let DebuggerController = {
       target.on("close", this._onTabDetached);
       target.on("navigate", this._onTabNavigated);
       target.on("will-navigate", this._onTabNavigated);
       this.client = client;
 
       if (target.chrome) {
         this._startChromeDebugging(chromeDebugger, startedDebugging.resolve);
       } else {
-        this._startDebuggingTab(threadActor, startedDebugging.resolve);
+        this._startDebuggingTab(startedDebugging.resolve);
         const startedTracing = promise.defer();
         this._startTracingTab(traceActor, startedTracing.resolve);
 
         return promise.all([startedDebugging.promise, startedTracing.promise]);
       }
 
       return startedDebugging.promise;
     }
@@ -334,38 +334,40 @@ let DebuggerController = {
     if (aResponse.error == "wrongOrder") {
       DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl);
     }
   },
 
   /**
    * Sets up a debugging session.
    *
-   * @param string aThreadActor
-   *        The remote protocol grip of the tab.
    * @param function aCallback
    *        A function to invoke once the client attaches to the active thread.
    */
-  _startDebuggingTab: function(aThreadActor, aCallback) {
-    this.client.attachThread(aThreadActor, (aResponse, aThreadClient) => {
+  _startDebuggingTab: function(aCallback) {
+    this._target.activeTab.attachThread({
+      useSourceMaps: Prefs.sourceMapsEnabled
+    }, (aResponse, aThreadClient) => {
       if (!aThreadClient) {
         Cu.reportError("Couldn't attach to thread: " + aResponse.error);
         return;
       }
       this.activeThread = aThreadClient;
 
       this.ThreadState.connect();
       this.StackFrames.connect();
       this.SourceScripts.connect();
-      aThreadClient.resume(this._ensureResumptionOrder);
+      if (aThreadClient.paused) {
+        aThreadClient.resume(this._ensureResumptionOrder);
+      }
 
       if (aCallback) {
         aCallback();
       }
-    }, { useSourceMaps: Prefs.sourceMapsEnabled });
+    });
   },
 
   /**
    * Sets up a chrome debugging session.
    *
    * @param object aChromeDebugger
    *        The remote protocol grip of the chrome debugger.
    * @param function aCallback
@@ -377,17 +379,19 @@ let DebuggerController = {
         Cu.reportError("Couldn't attach to thread: " + aResponse.error);
         return;
       }
       this.activeThread = aThreadClient;
 
       this.ThreadState.connect();
       this.StackFrames.connect();
       this.SourceScripts.connect();
-      aThreadClient.resume(this._ensureResumptionOrder);
+      if (aThreadClient.paused) {
+        aThreadClient.resume(this._ensureResumptionOrder);
+      }
 
       if (aCallback) {
         aCallback();
       }
     }, { useSourceMaps: Prefs.sourceMapsEnabled });
   },
 
   /**
@@ -414,31 +418,33 @@ let DebuggerController = {
     });
   },
 
   /**
    * Detach and reattach to the thread actor with useSourceMaps true, blow
    * away old sources and get them again.
    */
   reconfigureThread: function(aUseSourceMaps) {
-    this.client.reconfigureThread({ useSourceMaps: aUseSourceMaps }, aResponse => {
+    this.activeThread.reconfigure({ useSourceMaps: aUseSourceMaps }, aResponse => {
       if (aResponse.error) {
         let msg = "Couldn't reconfigure thread: " + aResponse.message;
         Cu.reportError(msg);
         dumpn(msg);
         return;
       }
 
       // Reset the view and fetch all the sources again.
       DebuggerView.handleTabNavigation();
       this.SourceScripts.handleTabNavigation();
 
       // Update the stack frame list.
-      this.activeThread._clearFrames();
-      this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
+      if (this.activeThread.paused) {
+        this.activeThread._clearFrames();
+        this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
+      }
     });
   },
 
   /**
    * Attempts to quit the current process if allowed.
    *
    * @return object
    *         A promise that is resolved if the app will quit successfully.
--- a/browser/devtools/debugger/test/browser_dbg_break-on-dom-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_break-on-dom-01.js
@@ -27,17 +27,17 @@ function test() {
     is(gView.instrumentsPaneTab, "variables-tab",
       "The variables tab should be selected by default.");
 
     Task.spawn(function() {
       yield waitForSourceShown(aPanel, ".html");
       is(gEvents.itemCount, 0, "There should be no events before reloading.");
 
       let reloaded = waitForSourcesAfterReload();
-      gDebugger.gClient.activeTab.reload();
+      gDebugger.DebuggerController._target.activeTab.reload();
 
       is(gEvents.itemCount, 0, "There should be no events while reloading.");
       yield reloaded;
       is(gEvents.itemCount, 0, "There should be no events after reloading.");
 
       yield closeDebuggerAndFinish(aPanel);
     });
 
--- a/browser/devtools/debugger/test/browser_dbg_break-on-dom-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_break-on-dom-02.js
@@ -42,17 +42,17 @@ function test() {
     }
 
     function testFetchOnReloadWhenFocused() {
       return Task.spawn(function() {
         let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
 
         let reloading = once(gDebugger.gTarget, "will-navigate");
         let reloaded = waitForSourcesAfterReload();
-        gDebugger.gClient.activeTab.reload();
+        gDebugger.DebuggerController._target.activeTab.reload();
 
         yield reloading;
 
         is(gEvents.itemCount, 0,
           "There should be no events displayed in the view while reloading.");
         ok(true,
           "Event listeners were removed when the target started navigating.");
 
@@ -84,17 +84,17 @@ function test() {
         gView.toggleInstrumentsPane({ visible: true, animated: false }, 0);
         is(gView.instrumentsPaneHidden, false,
           "The instruments pane should still be visible.");
         is(gView.instrumentsPaneTab, "variables-tab",
           "The variables tab should be selected.");
 
         let reloading = once(gDebugger.gTarget, "will-navigate");
         let reloaded = waitForSourcesAfterReload();
-        gDebugger.gClient.activeTab.reload();
+        gDebugger.DebuggerController._target.activeTab.reload();
 
         yield reloading;
 
         is(gEvents.itemCount, 0,
           "There should be no events displayed in the view while reloading.");
         ok(true,
           "Event listeners were removed when the target started navigating.");
 
--- a/browser/devtools/debugger/test/browser_dbg_break-on-dom-event.js
+++ b/browser/devtools/debugger/test/browser_dbg_break-on-dom-event.js
@@ -63,17 +63,17 @@ function pauseDebuggee() {
 }
 
 // Test pause on all events.
 function testBreakOnAll() {
   let deferred = promise.defer();
 
   // Test calling pauseOnDOMEvents from a paused state.
   gThreadClient.pauseOnDOMEvents("*", (aPacket) => {
-    is(aPacket, undefined,
+    is(aPacket.error, undefined,
       "The pause-on-any-event request completed successfully.");
 
     gClient.addOneTimeListener("paused", (aEvent, aPacket) => {
       is(aPacket.why.type, "pauseOnDOMEvents",
         "A hidden breakpoint was hit.");
       is(aPacket.frame.callee.name, "keyupHandler",
         "The keyupHandler is entered.");
 
--- a/browser/devtools/debugger/test/browser_dbg_source-maps-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_source-maps-02.js
@@ -3,35 +3,33 @@
 
 /**
  * Test that we can toggle between the original and generated sources.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_binary_search.html";
 const JS_URL = EXAMPLE_URL + "code_binary_search.js";
 
-let gTab, gDebuggee, gPanel, gDebugger;
-let gEditor, gSources, gFrames, gPrefs, gOptions;
+let gDebuggee, gPanel, gDebugger, gEditor;
+let gSources, gFrames, gPrefs, gOptions;
 
 function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
-    gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
     gFrames = gDebugger.DebuggerView.StackFrames;
     gPrefs = gDebugger.Prefs;
     gOptions = gDebugger.DebuggerView.Options;
 
     waitForSourceShown(gPanel, ".coffee")
       .then(testToggleGeneratedSource)
       .then(testSetBreakpoint)
-      .then(testHitBreakpoint)
       .then(testToggleOnPause)
       .then(testResume)
       .then(() => closeDebuggerAndFinish(gPanel))
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
@@ -63,44 +61,33 @@ function testToggleGeneratedSource() {
 
 function testSetBreakpoint() {
   let deferred = promise.defer();
 
   gDebugger.gThreadClient.setBreakpoint({ url: JS_URL, line: 7 }, aResponse => {
     ok(!aResponse.error,
       "Should be able to set a breakpoint in a js file.");
 
-    deferred.resolve();
-  });
-
-  return deferred.promise;
-}
-
-function testHitBreakpoint() {
-  let deferred = promise.defer();
-
-  gDebugger.gThreadClient.resume(aResponse => {
-    ok(!aResponse.error, "Shouldn't get an error resuming.");
-    is(aResponse.type, "resumed", "Type should be 'resumed'.");
+    gDebugger.gClient.addOneTimeListener("resumed", () => {
+      waitForCaretAndScopes(gPanel, 7).then(() => {
+        // Make sure that we have JavaScript stack frames.
+        is(gFrames.itemCount, 1,
+          "Should have only one frame.");
+        is(gFrames.getItemAtIndex(0).attachment.url.indexOf(".coffee"), -1,
+          "First frame should not be a coffee source frame.");
+        isnot(gFrames.getItemAtIndex(0).attachment.url.indexOf(".js"), -1,
+          "First frame should be a JS frame.");
 
-    waitForCaretAndScopes(gPanel, 7).then(() => {
-      // Make sure that we have JavaScript stack frames.
-      is(gFrames.itemCount, 1,
-        "Should have only one frame.");
-      is(gFrames.getItemAtIndex(0).attachment.url.indexOf(".coffee"), -1,
-        "First frame should not be a coffee source frame.");
-      isnot(gFrames.getItemAtIndex(0).attachment.url.indexOf(".js"), -1,
-        "First frame should be a JS frame.");
+        deferred.resolve();
+      });
 
-      deferred.resolve();
+      // This will cause the breakpoint to be hit, and put us back in the
+      // paused state.
+      gDebuggee.binary_search([0, 2, 3, 5, 7, 10], 5);
     });
-
-    // This will cause the breakpoint to be hit, and put us back in the
-    // paused state.
-    gDebuggee.binary_search([0, 2, 3, 5, 7, 10], 5);
   });
 
   return deferred.promise;
 }
 
 function testToggleOnPause() {
   let finished = waitForSourceAndCaretAndScopes(gPanel, ".coffee", 5).then(() => {
     is(gPrefs.sourceMapsEnabled, true,
@@ -143,17 +130,16 @@ function testResume() {
 
     deferred.resolve();
   });
 
   return deferred.promise;
 }
 
 registerCleanupFunction(function() {
-  gTab = null;
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gEditor = null;
   gSources = null;
   gFrames = null;
   gPrefs = null;
   gOptions = null;
--- a/browser/devtools/debugger/test/browser_dbg_source-maps-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_source-maps-03.js
@@ -3,33 +3,31 @@
 
 /**
  * Test that we can debug minified javascript with source maps.
  */
 
 const TAB_URL = EXAMPLE_URL + "doc_minified.html";
 const JS_URL = EXAMPLE_URL + "code_math.js";
 
-let gTab, gDebuggee, gPanel, gDebugger;
+let gDebuggee, gPanel, gDebugger;
 let gEditor, gSources, gFrames;
 
 function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
-    gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
     gDebugger = gPanel.panelWin;
     gEditor = gDebugger.DebuggerView.editor;
     gSources = gDebugger.DebuggerView.Sources;
     gFrames = gDebugger.DebuggerView.StackFrames;
 
     waitForSourceShown(gPanel, JS_URL)
       .then(checkInitialSource)
       .then(testSetBreakpoint)
-      .then(testHitBreakpoint)
       .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
       .then(null, aError => {
         ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
       });
   });
 }
 
 function checkInitialSource() {
@@ -40,59 +38,44 @@ function checkInitialSource() {
   is(gEditor.getText().split("\n").length, 46,
     "The debugger's editor should have the original source displayed, " +
     "not the whitespace stripped minified version.");
 }
 
 function testSetBreakpoint() {
   let deferred = promise.defer();
 
-  gDebugger.gThreadClient.interrupt(aResponse => {
-    gDebugger.gThreadClient.setBreakpoint({ url: JS_URL, line: 30, column: 21 }, aResponse => {
-      ok(!aResponse.error,
-        "Should be able to set a breakpoint in a js file.");
-      ok(!aResponse.actualLocation,
-        "Should be able to set a breakpoint on line 30 and column 10.");
+  gDebugger.gThreadClient.setBreakpoint({ url: JS_URL, line: 30, column: 21 }, aResponse => {
+    ok(!aResponse.error,
+      "Should be able to set a breakpoint in a js file.");
+    ok(!aResponse.actualLocation,
+      "Should be able to set a breakpoint on line 30 and column 10.");
 
-      deferred.resolve();
+    gDebugger.gClient.addOneTimeListener("resumed", () => {
+      waitForCaretAndScopes(gPanel, 30).then(() => {
+        // Make sure that we have the right stack frames.
+        is(gFrames.itemCount, 9,
+          "Should have nine frames.");
+        is(gFrames.getItemAtIndex(0).attachment.url.indexOf(".min.js"), -1,
+          "First frame should not be a minified JS frame.");
+        isnot(gFrames.getItemAtIndex(0).attachment.url.indexOf(".js"), -1,
+          "First frame should be a JS frame.");
+
+        deferred.resolve();
+      });
+
+      // This will cause the breakpoint to be hit, and put us back in the
+      // paused state.
+      gDebuggee.arithmetic();
     });
   });
 
   return deferred.promise;
 }
 
-function testHitBreakpoint() {
-  let deferred = promise.defer();
-
-  gDebugger.gThreadClient.resume(aResponse => {
-    ok(!aResponse.error, "Shouldn't get an error resuming.");
-    is(aResponse.type, "resumed", "Type should be 'resumed'.");
-
-    waitForCaretAndScopes(gPanel, 30).then(() => {
-      // Make sure that we have the right stack frames.
-      is(gFrames.itemCount, 9,
-        "Should have nine frames.");
-      is(gFrames.getItemAtIndex(0).attachment.url.indexOf(".min.js"), -1,
-        "First frame should not be a minified JS frame.");
-      isnot(gFrames.getItemAtIndex(0).attachment.url.indexOf(".js"), -1,
-        "First frame should be a JS frame.");
-
-      deferred.resolve();
-    });
-
-    // This will cause the breakpoint to be hit, and put us back in the
-    // paused state.
-    gDebuggee.arithmetic();
-  });
-
-  return deferred.promise;
-}
-
-
 registerCleanupFunction(function() {
-  gTab = null;
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gEditor = null;
   gSources = null;
   gFrames = null;
 });
--- a/browser/devtools/debugger/test/browser_dbg_source-maps-04.js
+++ b/browser/devtools/debugger/test/browser_dbg_source-maps-04.js
@@ -96,17 +96,17 @@ function testSetBreakpoint() {
     deferred.resolve();
   });
 
   return deferred.promise;
 }
 
 function reloadPage() {
   let loaded = waitForSourceAndCaret(gPanel, ".js", 3);
-  gDebugger.gClient.activeTab.reload();
+  gDebugger.DebuggerController._target.activeTab.reload();
   return loaded.then(() => ok(true, "Page was reloaded and execution resumed."));
 }
 
 function testHitBreakpoint() {
   let deferred = promise.defer();
 
   gDebugger.gThreadClient.resume(aResponse => {
     ok(!aResponse.error, "Shouldn't get an error resuming.");
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -405,17 +405,17 @@ function ensureThreadClientState(aPanel,
     return promise.resolve(null);
   } else {
     return waitForThreadEvents(aPanel, aState);
   }
 }
 
 function navigateActiveTabTo(aPanel, aUrl, aWaitForEventName, aEventRepeat) {
   let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat);
-  let activeTab = aPanel.panelWin.gClient.activeTab;
+  let activeTab = aPanel.panelWin.DebuggerController._target.activeTab;
   aUrl ? activeTab.navigateTo(aUrl) : activeTab.reload();
   return finished;
 }
 
 function navigateActiveTabInHistory(aPanel, aDirection, aWaitForEventName, aEventRepeat) {
   let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat);
   content.history[aDirection]();
   return finished;
--- a/browser/devtools/framework/target.js
+++ b/browser/devtools/framework/target.js
@@ -279,16 +279,17 @@ TabTarget.prototype = {
     this._setupRemoteListeners();
 
     let attachTab = () => {
       this._client.attachTab(this._form.actor, (aResponse, aTabClient) => {
         if (!aTabClient) {
           this._remote.reject("Unable to attach to the tab");
           return;
         }
+        this.activeTab = aTabClient;
         this.threadActor = aResponse.threadActor;
         this._remote.resolve(null);
       });
     };
 
     if (this.isLocalTab) {
       this._client.connect((aType, aTraits) => {
         this._client.listTabs(aResponse => {
@@ -439,53 +440,57 @@ TabTarget.prototype = {
     // non-remoted targets.
     this.off("thread-resumed", this._handleThreadState);
     this.off("thread-paused", this._handleThreadState);
 
     if (this._tab) {
       this._teardownListeners();
     }
 
+    let cleanupAndResolve = () => {
+      this._cleanup();
+      this._destroyer.resolve(null);
+    };
     // If this target was not remoted, the promise will be resolved before the
     // function returns.
     if (this._tab && !this._client) {
-      this._cleanup();
-      this._destroyer.resolve(null);
+      cleanupAndResolve();
     } else if (this._client) {
       // If, on the other hand, this target was remoted, the promise will be
       // resolved after the remote connection is closed.
       this._teardownRemoteListeners();
 
       if (this.isLocalTab) {
         // We started with a local tab and created the client ourselves, so we
         // should close it.
-        this._client.close(() => {
-          this._cleanup();
-          this._destroyer.resolve(null);
-        });
+        this._client.close(cleanupAndResolve);
       } else {
         // The client was handed to us, so we are not responsible for closing
-        // it.
-        this._cleanup();
-        this._destroyer.resolve(null);
+        // it. We just need to detach from the tab, if already attached.
+        if (this.activeTab) {
+          this.activeTab.detach(cleanupAndResolve);
+        } else {
+          cleanupAndResolve();
+        }
       }
     }
 
     return this._destroyer.promise;
   },
 
   /**
    * Clean up references to what this target points to.
    */
   _cleanup: function TabTarget__cleanup() {
     if (this._tab) {
       targets.delete(this._tab);
     } else {
       promiseTargets.delete(this._form);
     }
+    this.activeTab = null;
     this._client = null;
     this._tab = null;
     this._form = null;
     this._remote = null;
   },
 
   toString: function() {
     return 'TabTarget:' + (this._tab ? this._tab : (this._form && this._form.actor));
--- a/browser/devtools/framework/toolbox-options.js
+++ b/browser/devtools/framework/toolbox-options.js
@@ -207,17 +207,17 @@ OptionsPanel.prototype = {
           newValue: this.value
         };
         data.oldValue = Services.prefs.getCharPref(data.pref);
         Services.prefs.setCharPref(data.pref, data.newValue);
         gDevTools.emit("pref-changed", data);
       }.bind(menulist));
     }
 
-    this.target.client.attachTab(this.target.client.activeTab._actor, (response) => {
+    this.target.client.attachTab(this.target.activeTab._actor, (response) => {
       this._origJavascriptEnabled = response.javascriptEnabled;
       this._origCacheEnabled = response.cacheEnabled;
 
       this._populateDisableJSCheckbox();
       this._populateDisableCacheCheckbox();
     });
   },
 
@@ -243,33 +243,33 @@ OptionsPanel.prototype = {
    */
   _disableJSClicked: function(event) {
     let checked = event.target.checked;
 
     let options = {
       "javascriptEnabled": !checked
     };
 
-    this.target.client.reconfigureTab(options);
+    this.target.activeTab.reconfigure(options);
   },
 
   /**
    * Disables the cache for the currently loaded tab.
    *
    * @param {Event} event
    *        The event sent by checking / unchecking the disable cache checkbox.
    */
   _disableCacheClicked: function(event) {
     let checked = event.target.checked;
 
     let options = {
       "cacheEnabled": !checked
     };
 
-    this.target.client.reconfigureTab(options);
+    this.target.activeTab.reconfigure(options);
   },
 
   destroy: function() {
     if (this.destroyPromise) {
       return this.destroyPromise;
     }
 
     let deferred = promise.defer();
@@ -286,16 +286,16 @@ OptionsPanel.prototype = {
     this._disableJSClicked = this._disableCacheClicked = null;
 
     // If the cache or JavaScript is disabled we need to revert them to their
     // original values.
     let options = {
       "cacheEnabled": this._origCacheEnabled,
       "javascriptEnabled": this._origJavascriptEnabled
     };
-    this.target.client.reconfigureTab(options, () => {
+    this.target.activeTab.reconfigure(options, () => {
       this.toolbox = null;
       deferred.resolve();
     }, true);
 
     return deferred.promise;
   }
 };
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -1212,17 +1212,17 @@ Toolbox.prototype = {
     gDevTools.off("tool-unregistered", this._toolUnregistered);
 
     let outstanding = [];
     for (let [id, panel] of this._toolPanels) {
       try {
         outstanding.push(panel.destroy());
       } catch (e) {
         // We don't want to stop here if any panel fail to close.
-        console.error(e);
+        console.error("Panel " + id + ":", e);
       }
     }
 
     // Destroying the walker and inspector fronts
     outstanding.push(this.destroyInspector());
 
     // Removing buttons
     this._pickerButton.removeEventListener("command", this.togglePicker, false);
--- a/browser/devtools/shadereditor/test/head.js
+++ b/browser/devtools/shadereditor/test/head.js
@@ -224,22 +224,22 @@ function ensurePixelIs(aDebuggee, aPosit
 }
 
 function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") {
   executeSoon(() => content.history[aDirection]());
   return once(aTarget, aWaitForTargetEvent);
 }
 
 function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") {
-  executeSoon(() => aTarget.client.activeTab.navigateTo(aUrl));
+  executeSoon(() => aTarget.activeTab.navigateTo(aUrl));
   return once(aTarget, aWaitForTargetEvent);
 }
 
 function reload(aTarget, aWaitForTargetEvent = "navigate") {
-  executeSoon(() => aTarget.client.activeTab.reload());
+  executeSoon(() => aTarget.activeTab.reload());
   return once(aTarget, aWaitForTargetEvent);
 }
 
 function initBackend(aUrl) {
   info("Initializing a shader editor front.");
 
   if (!DebuggerServer.initialized) {
     DebuggerServer.init(() => true);
--- a/browser/devtools/shared/widgets/BreadcrumbsWidget.jsm
+++ b/browser/devtools/shared/widgets/BreadcrumbsWidget.jsm
@@ -162,17 +162,19 @@ BreadcrumbsWidget.prototype = {
   ensureElementIsVisible: function(aElement) {
     if (!aElement) {
       return;
     }
 
     // Repeated calls to ensureElementIsVisible would interfere with each other
     // and may sometimes result in incorrect scroll positions.
     setNamedTimeout("breadcrumb-select", ENSURE_SELECTION_VISIBLE_DELAY, () => {
-      this._list.ensureElementIsVisible(aElement);
+      if (this._list.ensureElementIsVisible) {
+        this._list.ensureElementIsVisible(aElement);
+      }
     });
   },
 
   /**
    * The underflow and overflow listener for the arrowscrollbox container.
    */
   _onUnderflow: function({ target }) {
     if (target != this._list) {
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -4861,16 +4861,44 @@
   },
   "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_REMOTE_MS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000",
     "n_buckets": "1000",
     "description": "The time (in milliseconds) that it took to display a selected source to the user."
   },
+  "DEVTOOLS_DEBUGGER_RDP_LOCAL_RECONFIGURETAB_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'reconfigure tab' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_REMOTE_RECONFIGURETAB_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'reconfigure tab' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_LOCAL_RECONFIGURETHREAD_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'reconfigure thread' request to go round trip."
+  },
+  "DEVTOOLS_DEBUGGER_RDP_REMOTE_RECONFIGURETHREAD_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": "1000",
+    "description": "The time (in milliseconds) that it took a 'reconfigure thread' request to go round trip."
+  },
   "WEBRTC_ICE_SUCCESS_RATE": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "The number of failed ICE Connections (0) vs. number of successful ICE connections (1)."
   },
   "WEBRTC_CALL_DURATION": {
     "expires_in_version": "never",
     "kind": "exponential",
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -229,19 +229,22 @@ const UnsolicitedPauses = {
  * Creates a client for the remote debugging protocol server. This client
  * provides the means to communicate with the server and exchange the messages
  * required by the protocol in a traditional JavaScript API.
  */
 this.DebuggerClient = function (aTransport)
 {
   this._transport = aTransport;
   this._transport.hooks = this;
-  this._threadClients = {};
-  this._tabClients = {};
-  this._consoleClients = {};
+
+  // Map actor ID to client instance for each actor type.
+  this._threadClients = new Map;
+  this._tabClients = new Map;
+  this._tracerClients = new Map;
+  this._consoleClients = new Map;
 
   this._pendingRequests = [];
   this._activeRequests = new Map;
   this._eventsEnabled = true;
 
   this.compat = new ProtocolCompatibility(this, [
     new SourcesShim(),
   ]);
@@ -276,17 +279,17 @@ this.DebuggerClient = function (aTranspo
  * @param after
  *        The function to call after the response is received. It is passed the
  *        response, and the return value is considered the new response that
  *        will be passed to the callback. The |this| context is the instance of
  *        the client object we are defining a method for.
  */
 DebuggerClient.requester = function (aPacketSkeleton,
                                      { telemetry, before, after }) {
-  return function (...args) {
+  return DevToolsUtils.makeInfallible(function (...args) {
     let histogram, startTime;
     if (telemetry) {
       let transportType = this._transport.onOutputStreamReady === undefined
         ? "LOCAL_"
         : "REMOTE_";
       let histogramId = "DEVTOOLS_DEBUGGER_RDP_"
         + transportType + telemetry + "_MS";
       histogram = Services.telemetry.getHistogramById(histogramId);
@@ -306,41 +309,37 @@ DebuggerClient.requester = function (aPa
         outgoingPacket[k] = aPacketSkeleton[k];
       }
     }
 
     if (before) {
       outgoingPacket = before.call(this, outgoingPacket);
     }
 
-    this.request(outgoingPacket, function (aResponse) {
+    this.request(outgoingPacket, DevToolsUtils.makeInfallible(function (aResponse) {
       if (after) {
         let { from } = aResponse;
         aResponse = after.call(this, aResponse);
         if (!aResponse.from) {
           aResponse.from = from;
         }
       }
 
       // The callback is always the last parameter.
       let thisCallback = args[maxPosition + 1];
       if (thisCallback) {
-        try {
-          thisCallback(aResponse);
-        } catch (e) {
-          DevToolsUtils.reportException("DebuggerClient.requester callback", e);
-        }
+        thisCallback(aResponse);
       }
 
       if (histogram) {
         histogram.add(+new Date - startTime);
       }
-    }.bind(this));
+    }.bind(this), "DebuggerClient.requester request callback"));
 
-  };
+  }, "DebuggerClient.requester");
 };
 
 function args(aPos) {
   return new DebuggerClient.Argument(aPos);
 }
 
 DebuggerClient.Argument = function (aPosition) {
   this.position = aPosition;
@@ -385,53 +384,45 @@ DebuggerClient.prototype = {
     this._eventsEnabled = false;
 
     if (aOnClosed) {
       this.addOneTimeListener('closed', function (aEvent) {
         aOnClosed();
       });
     }
 
-    // In this function, we're using the hoisting behavior of nested
-    // function definitions to write the code in the order it will actually
-    // execute. So converting to arrow functions to get rid of 'self' would
-    // be unhelpful here.
-    let self = this;
+    const detachClients = (clientMap, next) => {
+      const clients = clientMap.values();
+      const total = clientMap.size;
+      let numFinished = 0;
 
-    let continuation = function () {
-      self._consoleClients = {};
-      detachThread();
-    }
-
-    for each (let client in this._consoleClients) {
-      continuation = client.close.bind(client, continuation);
-    }
-
-    continuation();
+      if (total == 0) {
+        next();
+        return;
+      }
 
-    function detachThread() {
-      if (self.activeThread) {
-        self.activeThread.detach(detachTab);
-      } else {
-        detachTab();
+      for (let client of clients) {
+        let method = client instanceof WebConsoleClient ? "close" : "detach";
+        client[method](() => {
+          if (++numFinished === total) {
+            clientMap.clear();
+            next();
+          }
+        });
       }
-    }
+    };
 
-    function detachTab() {
-      if (self.activeTab) {
-        self.activeTab.detach(closeTransport);
-      } else {
-        closeTransport();
-      }
-    }
-
-    function closeTransport() {
-      self._transport.close();
-      self._transport = null;
-    }
+    detachClients(this._consoleClients, () => {
+      detachClients(this._threadClients, () => {
+        detachClients(this._tabClients, () => {
+          this._transport.close();
+          this._transport = null;
+        });
+      });
+    });
   },
 
   /*
    * This function exists only to preserve DebuggerClient's interface;
    * new code should say 'client.mainRoot.listTabs()'.
    */
   listTabs: function (aOnResponse) { return this.mainRoot.listTabs(aOnResponse); },
 
@@ -446,26 +437,35 @@ DebuggerClient.prototype = {
    *
    * @param string aTabActor
    *        The actor ID for the tab to attach.
    * @param function aOnResponse
    *        Called with the response packet and a TabClient
    *        (which will be undefined on error).
    */
   attachTab: function (aTabActor, aOnResponse) {
+    if (this._tabClients.has(aTabActor)) {
+      let cachedTab = this._tabClients.get(aTabActor);
+      let cachedResponse = {
+        cacheEnabled: cachedTab.cacheEnabled,
+        javascriptEnabled: cachedTab.javascriptEnabled
+      };
+      setTimeout(() => aOnResponse(cachedResponse, cachedTab), 0);
+      return;
+    }
+
     let packet = {
       to: aTabActor,
       type: "attach"
     };
     this.request(packet, (aResponse) => {
       let tabClient;
       if (!aResponse.error) {
-        tabClient = new TabClient(this, aTabActor);
-        this._tabClients[aTabActor] = tabClient;
-        this.activeTab = tabClient;
+        tabClient = new TabClient(this, aResponse);
+        this._tabClients.set(aTabActor, tabClient);
       }
       aOnResponse(aResponse, tabClient);
     });
   },
 
   /**
    * Attach to a Web Console actor.
    *
@@ -474,117 +474,98 @@ DebuggerClient.prototype = {
    * @param array aListeners
    *        The console listeners you want to start.
    * @param function aOnResponse
    *        Called with the response packet and a WebConsoleClient
    *        instance (which will be undefined on error).
    */
   attachConsole:
   function (aConsoleActor, aListeners, aOnResponse) {
+    if (this._consoleClients.has(aConsoleActor)) {
+      setTimeout(() => aOnResponse({}, this._consoleClients.get(aConsoleActor)), 0);
+      return;
+    }
+
     let packet = {
       to: aConsoleActor,
       type: "startListeners",
       listeners: aListeners,
     };
 
     this.request(packet, (aResponse) => {
       let consoleClient;
       if (!aResponse.error) {
         consoleClient = new WebConsoleClient(this, aConsoleActor);
-        this._consoleClients[aConsoleActor] = consoleClient;
+        this._consoleClients.set(aConsoleActor, consoleClient);
       }
       aOnResponse(aResponse, consoleClient);
     });
   },
 
   /**
-   * Attach to a thread actor.
+   * Attach to a global-scoped thread actor for chrome debugging.
    *
    * @param string aThreadActor
    *        The actor ID for the thread to attach.
    * @param function aOnResponse
    *        Called with the response packet and a ThreadClient
    *        (which will be undefined on error).
    * @param object aOptions
    *        Configuration options.
    *        - useSourceMaps: whether to use source maps or not.
    */
   attachThread: function (aThreadActor, aOnResponse, aOptions={}) {
-    let packet = {
+    if (this._threadClients.has(aThreadActor)) {
+      setTimeout(() => aOnResponse({}, this._threadClients.get(aThreadActor)), 0);
+      return;
+    }
+
+   let packet = {
       to: aThreadActor,
       type: "attach",
       options: aOptions
     };
     this.request(packet, (aResponse) => {
       if (!aResponse.error) {
         var threadClient = new ThreadClient(this, aThreadActor);
-        this._threadClients[aThreadActor] = threadClient;
-        this.activeThread = threadClient;
+        this._threadClients.set(aThreadActor, threadClient);
       }
       aOnResponse(aResponse, threadClient);
     });
   },
 
   /**
    * Attach to a trace actor.
    *
    * @param string aTraceActor
    *        The actor ID for the tracer to attach.
    * @param function aOnResponse
    *        Called with the response packet and a TraceClient
    *        (which will be undefined on error).
    */
   attachTracer: function (aTraceActor, aOnResponse) {
+    if (this._tracerClients.has(aTraceActor)) {
+      setTimeout(() => aOnResponse({}, this._tracerClients.get(aTraceActor)), 0);
+      return;
+    }
+
     let packet = {
       to: aTraceActor,
       type: "attach"
     };
     this.request(packet, (aResponse) => {
       if (!aResponse.error) {
-        let traceClient = new TraceClient(this, aTraceActor);
-        aOnResponse(aResponse, traceClient);
+        var traceClient = new TraceClient(this, aTraceActor);
+        this._tracerClients.set(aTraceActor, traceClient);
       }
+      aOnResponse(aResponse, traceClient);
     });
   },
 
   /**
-   * Reconfigure a thread actor.
-   *
-   * @param object aOptions
-   *        A dictionary object of the new options to use in the thread actor.
-   * @param function aOnResponse
-   *        Called with the response packet.
-   */
-  reconfigureThread: function (aOptions, aOnResponse) {
-    let packet = {
-      to: this.activeThread._actor,
-      type: "reconfigure",
-      options: aOptions
-    };
-    this.request(packet, aOnResponse);
-  },
-
-  /**
-   * Reconfigure a tab actor.
-   *
-   * @param object aOptions
-   *        A dictionary object of the new options to use in the tab actor.
-   * @param function aOnResponse
-   *        Called with the response packet.
-   */
-  reconfigureTab: function (aOptions, aOnResponse) {
-    let packet = {
-      to: this.activeTab._actor,
-      type: "reconfigure",
-      options: aOptions
-    };
-    this.request(packet, aOnResponse);
-  },
-
-  /**
    * Release an object actor.
    *
    * @param string aActor
    *        The actor ID to send the request to.
    * @param aOnResponse function
    *        If specified, will be called with the response packet when
    *        debugging server responds.
    */
@@ -692,27 +673,28 @@ DebuggerClient.prototype = {
           !(aPacket.type == ThreadStateTypes.paused &&
             aPacket.why.type in UnsolicitedPauses)) {
         onResponse = this._activeRequests.get(aPacket.from);
         this._activeRequests.delete(aPacket.from);
       }
 
       // Packets that indicate thread state changes get special treatment.
       if (aPacket.type in ThreadStateTypes &&
-          aPacket.from in this._threadClients) {
-        this._threadClients[aPacket.from]._onThreadState(aPacket);
+          this._threadClients.has(aPacket.from)) {
+        this._threadClients.get(aPacket.from)._onThreadState(aPacket);
       }
       // On navigation the server resumes, so the client must resume as well.
       // We achieve that by generating a fake resumption packet that triggers
       // the client's thread state change listeners.
-      if (this.activeThread &&
-          aPacket.type == UnsolicitedNotifications.tabNavigated &&
-          aPacket.from in this._tabClients) {
-        let resumption = { from: this.activeThread._actor, type: "resumed" };
-        this.activeThread._onThreadState(resumption);
+      if (aPacket.type == UnsolicitedNotifications.tabNavigated &&
+          this._tabClients.has(aPacket.from) &&
+          this._tabClients.get(aPacket.from).thread) {
+        let thread = this._tabClients.get(aPacket.from).thread;
+        let resumption = { from: thread._actor, type: "resumed" };
+        thread._onThreadState(resumption);
       }
       // Only try to notify listeners on events, not responses to requests
       // that lack a packet type.
       if (aPacket.type) {
         this.notify(aPacket.type, aPacket);
       }
 
       if (onResponse) {
@@ -954,43 +936,80 @@ SSProto.translatePacket = function (aPac
 
 /**
  * Creates a tab client for the remote debugging protocol server. This client
  * is a front to the tab actor created in the server side, hiding the protocol
  * details in a traditional JavaScript API.
  *
  * @param aClient DebuggerClient
  *        The debugger client parent.
- * @param aActor string
- *        The actor ID for this tab.
+ * @param aForm object
+ *        The protocol form for this tab.
  */
-function TabClient(aClient, aActor) {
-  this._client = aClient;
-  this._actor = aActor;
-  this.request = this._client.request;
+function TabClient(aClient, aForm) {
+  this.client = aClient;
+  this._actor = aForm.from;
+  this._threadActor = aForm.threadActor;
+  this.javascriptEnabled = aForm.javascriptEnabled;
+  this.cacheEnabled = aForm.cacheEnabled;
+  this.thread = null;
+  this.request = this.client.request;
 }
 
 TabClient.prototype = {
   get actor() { return this._actor },
-  get _transport() { return this._client._transport; },
+  get _transport() { return this.client._transport; },
+
+  /**
+   * Attach to a thread actor.
+   *
+   * @param object aOptions
+   *        Configuration options.
+   *        - useSourceMaps: whether to use source maps or not.
+   * @param function aOnResponse
+   *        Called with the response packet and a ThreadClient
+   *        (which will be undefined on error).
+   */
+  attachThread: function(aOptions={}, aOnResponse) {
+    if (this.thread) {
+      setTimeout(() => aOnResponse({}, this.thread), 0);
+      return;
+    }
+
+    let packet = {
+      to: this._threadActor,
+      type: "attach",
+      options: aOptions
+    };
+    this.request(packet, (aResponse) => {
+      if (!aResponse.error) {
+        this.thread = new ThreadClient(this, this._threadActor);
+        this.client._threadClients.set(this._threadActor, this.thread);
+      }
+      aOnResponse(aResponse, this.thread);
+    });
+  },
 
   /**
    * Detach the client from the tab actor.
    *
    * @param function aOnResponse
    *        Called with the response packet.
    */
   detach: DebuggerClient.requester({
     type: "detach"
   }, {
+    before: function (aPacket) {
+      if (this.thread) {
+        this.thread.detach();
+      }
+      return aPacket;
+    },
     after: function (aResponse) {
-      if (this.activeTab === this._client._tabClients[this.actor]) {
-        this.activeTab = undefined;
-      }
-      delete this._client._tabClients[this.actor];
+      this.client._tabClients.delete(this.actor);
       return aResponse;
     },
     telemetry: "TABDETACH"
   }),
 
   /**
    * Reload the page in this tab.
    */
@@ -1007,16 +1026,31 @@ TabClient.prototype = {
    *        The URL to navigate to.
    */
   navigateTo: DebuggerClient.requester({
     type: "navigateTo",
     url: args(0)
   }, {
     telemetry: "NAVIGATETO"
   }),
+
+  /**
+   * Reconfigure the tab actor.
+   *
+   * @param object aOptions
+   *        A dictionary object of the new options to use in the tab actor.
+   * @param function aOnResponse
+   *        Called with the response packet.
+   */
+  reconfigure: DebuggerClient.requester({
+    type: "reconfigure",
+    options: args(0)
+  }, {
+    telemetry: "RECONFIGURETAB"
+  }),
 };
 
 eventSource(TabClient.prototype);
 
 /**
  * A RootClient object represents a root actor on the server. Each
  * DebuggerClient keeps a RootClient instance representing the root actor
  * for the initial connection; DebuggerClient's 'listTabs' and
@@ -1072,49 +1106,51 @@ RootClient.prototype = {
   get request()    { return this._client.request;    }
 };
 
 /**
  * Creates a thread client for the remote debugging protocol server. This client
  * is a front to the thread actor created in the server side, hiding the
  * protocol details in a traditional JavaScript API.
  *
- * @param aClient DebuggerClient
- *        The debugger client parent.
+ * @param aClient DebuggerClient|TabClient
+ *        The parent of the thread (tab for tab-scoped debuggers, DebuggerClient
+ *        for chrome debuggers).
  * @param aActor string
  *        The actor ID for this thread.
  */
 function ThreadClient(aClient, aActor) {
-  this._client = aClient;
+  this._parent = aClient;
+  this.client = aClient instanceof DebuggerClient ? aClient : aClient.client;
   this._actor = aActor;
   this._frameCache = [];
   this._scriptCache = {};
   this._pauseGrips = {};
   this._threadGrips = {};
-  this.request = this._client.request;
+  this.request = this.client.request;
 }
 
 ThreadClient.prototype = {
   _state: "paused",
   get state() { return this._state; },
   get paused() { return this._state === "paused"; },
 
   _pauseOnExceptions: false,
   _ignoreCaughtExceptions: false,
   _pauseOnDOMEvents: null,
 
   _actor: null,
   get actor() { return this._actor; },
 
-  get compat() { return this._client.compat; },
-  get _transport() { return this._client._transport; },
+  get compat() { return this.client.compat; },
+  get _transport() { return this.client._transport; },
 
   _assertPaused: function (aCommand) {
     if (!this.paused) {
-      throw Error(aCommand + " command sent while not paused.");
+      throw Error(aCommand + " command sent while not paused. Currently " + this._state);
     }
   },
 
   /**
    * Resume a paused thread. If the optional aLimit parameter is present, then
    * the thread will also pause when that limit is reached.
    *
    * @param [optional] object aLimit
@@ -1152,16 +1188,31 @@ ThreadClient.prototype = {
         this._state = "paused";
       }
       return aResponse;
     },
     telemetry: "RESUME"
   }),
 
   /**
+   * Reconfigure the thread actor.
+   *
+   * @param object aOptions
+   *        A dictionary object of the new options to use in the thread actor.
+   * @param function aOnResponse
+   *        Called with the response packet.
+   */
+  reconfigure: DebuggerClient.requester({
+    type: "reconfigure",
+    options: args(0)
+  }, {
+    telemetry: "RECONFIGURETHREAD"
+  }),
+
+  /**
    * Resume a paused thread.
    */
   resume: function (aOnResponse) {
     this._doResume(null, aOnResponse);
   },
 
   /**
    * Step over a function call.
@@ -1217,17 +1268,17 @@ ThreadClient.prototype = {
                                aIgnoreCaughtExceptions,
                                aOnResponse) {
     this._pauseOnExceptions = aPauseOnExceptions;
     this._ignoreCaughtExceptions = aIgnoreCaughtExceptions;
 
     // If the debuggee is paused, we have to send the flag via a reconfigure
     // request.
     if (this.paused) {
-      this._client.reconfigureThread({
+      this.reconfigure({
         pauseOnExceptions: aPauseOnExceptions,
         ignoreCaughtExceptions: aIgnoreCaughtExceptions
       }, aOnResponse);
       return;
     }
     // Otherwise send the flag using a standard resume request.
     this.interrupt(aResponse => {
       if (aResponse.error) {
@@ -1251,22 +1302,26 @@ ThreadClient.prototype = {
    * @param function onResponse
    *        Called with the response packet in a future turn of the event loop.
    */
   pauseOnDOMEvents: function (events, onResponse) {
     this._pauseOnDOMEvents = events;
     // If the debuggee is paused, the value of the array will be communicated in
     // the next resumption. Otherwise we have to force a pause in order to send
     // the array.
-    if (this.paused)
-      return void setTimeout(onResponse, 0);
+    if (this.paused) {
+      setTimeout(() => onResponse({}), 0);
+      return;
+    }
     this.interrupt(response => {
       // Can't continue if pausing failed.
-      if (response.error)
-        return void onResponse(response);
+      if (response.error) {
+        onResponse(response);
+        return;
+      }
       this.resume(onResponse);
     });
   },
 
   /**
    * Send a clientEvaluate packet to the debuggee. Response
    * will be a resume packet.
    *
@@ -1305,20 +1360,18 @@ ThreadClient.prototype = {
    *
    * @param function aOnResponse
    *        Called with the response packet.
    */
   detach: DebuggerClient.requester({
     type: "detach"
   }, {
     after: function (aResponse) {
-      if (this.activeThread === this._client._threadClients[this.actor]) {
-        this.activeThread = null;
-      }
-      delete this._client._threadClients[this.actor];
+      this.client._threadClients.delete(this.actor);
+      this._parent.thread = null;
       return aResponse;
     },
     telemetry: "THREADDETACH"
   }),
 
   /**
    * Request to set a breakpoint in the specified location.
    *
@@ -1327,21 +1380,21 @@ ThreadClient.prototype = {
    * @param function aOnResponse
    *        Called with the thread's response.
    */
   setBreakpoint: function (aLocation, aOnResponse) {
     // A helper function that sets the breakpoint.
     let doSetBreakpoint = function (aCallback) {
       let packet = { to: this._actor, type: "setBreakpoint",
                      location: aLocation };
-      this._client.request(packet, function (aResponse) {
+      this.client.request(packet, function (aResponse) {
         // Ignoring errors, since the user may be setting a breakpoint in a
         // dead script that will reappear on a page reload.
         if (aOnResponse) {
-          let bpClient = new BreakpointClient(this._client, aResponse.actor,
+          let bpClient = new BreakpointClient(this.client, aResponse.actor,
                                               aLocation);
           if (aCallback) {
             aCallback(aOnResponse(aResponse, bpClient));
           } else {
             aOnResponse(aResponse, bpClient);
           }
         }
       }.bind(this));
@@ -1565,17 +1618,17 @@ ThreadClient.prototype = {
    * @param aGrip object
    *        A pause-lifetime object grip returned by the protocol.
    */
   pauseGrip: function (aGrip) {
     if (aGrip.actor in this._pauseGrips) {
       return this._pauseGrips[aGrip.actor];
     }
 
-    let client = new ObjectClient(this._client, aGrip);
+    let client = new ObjectClient(this.client, aGrip);
     this._pauseGrips[aGrip.actor] = client;
     return client;
   },
 
   /**
    * Get or create a long string client, checking the grip client cache if it
    * already exists.
    *
@@ -1585,17 +1638,17 @@ ThreadClient.prototype = {
    *        The property name of the grip client cache to check for existing
    *        clients in.
    */
   _longString: function (aGrip, aGripCacheName) {
     if (aGrip.actor in this[aGripCacheName]) {
       return this[aGripCacheName][aGrip.actor];
     }
 
-    let client = new LongStringClient(this._client, aGrip);
+    let client = new LongStringClient(this.client, aGrip);
     this[aGripCacheName][aGrip.actor] = client;
     return client;
   },
 
   /**
    * Return an instance of LongStringClient for the given long string grip that
    * is scoped to the current pause.
    *
@@ -1650,36 +1703,35 @@ ThreadClient.prototype = {
    * Handle thread state change by doing necessary cleanup and notifying all
    * registered listeners.
    */
   _onThreadState: function (aPacket) {
     this._state = ThreadStateTypes[aPacket.type];
     this._clearFrames();
     this._clearPauseGrips();
     aPacket.type === ThreadStateTypes.detached && this._clearThreadGrips();
-    this._client._eventsEnabled && this.notify(aPacket.type, aPacket);
+    this.client._eventsEnabled && this.notify(aPacket.type, aPacket);
   },
 
   /**
    * Return an EnvironmentClient instance for the given environment actor form.
    */
   environment: function (aForm) {
-    return new EnvironmentClient(this._client, aForm);
+    return new EnvironmentClient(this.client, aForm);
   },
 
   /**
    * Return an instance of SourceClient for the given source actor form.
    */
   source: function (aForm) {
     if (aForm.actor in this._threadGrips) {
       return this._threadGrips[aForm.actor];
     }
 
-    return this._threadGrips[aForm.actor] = new SourceClient(this._client,
-                                                             aForm);
+    return this._threadGrips[aForm.actor] = new SourceClient(this, aForm);
   },
 
   /**
    * Request the prototype and own properties of mutlipleObjects.
    *
    * @param aOnResponse function
    *        Called with the request's response.
    * @param actors [string]
@@ -1719,18 +1771,25 @@ TraceClient.prototype = {
   get actor()   { return this._actor; },
   get tracing() { return this._activeTraces.size > 0; },
 
   get _transport() { return this._client._transport; },
 
   /**
    * Detach from the trace actor.
    */
-  detach: DebuggerClient.requester({ type: "detach" },
-                                   { telemetry: "TRACERDETACH" }),
+  detach: DebuggerClient.requester({
+    type: "detach"
+  }, {
+    after: function (aResponse) {
+      this._client._tracerClients.delete(this.actor);
+      return aResponse;
+    },
+    telemetry: "TRACERDETACH"
+  }),
 
   /**
    * Start a new trace.
    *
    * @param aTrace [string]
    *        An array of trace types to be recorded by the new trace.
    *
    * @param aName string
@@ -1959,31 +2018,31 @@ LongStringClient.prototype = {
   }, {
     telemetry: "SUBSTRING"
   }),
 };
 
 /**
  * A SourceClient provides a way to access the source text of a script.
  *
- * @param aClient DebuggerClient
- *        The debugger client parent.
+ * @param aClient ThreadClient
+ *        The thread client parent.
  * @param aForm Object
  *        The form sent across the remote debugging protocol.
  */
 function SourceClient(aClient, aForm) {
   this._form = aForm;
   this._isBlackBoxed = aForm.isBlackBoxed;
   this._isPrettyPrinted = aForm.isPrettyPrinted;
-  this._client = aClient;
+  this._activeThread = aClient;
+  this._client = aClient.client;
 }
 
 SourceClient.prototype = {
   get _transport() this._client._transport,
-  get _activeThread() this._client.activeThread,
   get isBlackBoxed() this._isBlackBoxed,
   get isPrettyPrinted() this._isPrettyPrinted,
   get actor() this._form.actor,
   get request() this._client.request,
   get url() this._form.url,
 
   /**
    * Black box this SourceClient's source.
@@ -2084,18 +2143,17 @@ SourceClient.prototype = {
     }
 
     if (typeof aResponse.source === "string") {
       aCallback(aResponse);
       return;
     }
 
     let { contentType, source } = aResponse;
-    let longString = this._client.activeThread.threadLongString(
-      source);
+    let longString = this._activeThread.threadLongString(source);
     longString.substring(0, longString.length, function (aResponse) {
       if (aResponse.error) {
         aCallback(aResponse);
         return;
       }
 
       aCallback({
         source: aResponse.substring,
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -650,18 +650,16 @@ ThreadActor.prototype = {
   },
 
   disconnect: function () {
     dumpn("in ThreadActor.prototype.disconnect");
     if (this._state == "paused") {
       this.onResume();
     }
 
-    this._state = "exited";
-
     this.clearDebuggees();
     this.conn.removeActorPool(this._threadLifetimePool);
     this._threadLifetimePool = null;
 
     if (this._prettyPrintWorker) {
       this._prettyPrintWorker.removeEventListener(
         "error", this._onPrettyPrintError, false);
       this._prettyPrintWorker.removeEventListener(
@@ -677,26 +675,28 @@ ThreadActor.prototype = {
     this.dbg = null;
   },
 
   /**
    * Disconnect the debugger and put the actor in the exited state.
    */
   exit: function () {
     this.disconnect();
+    this._state = "exited";
   },
 
   // Request handlers
   onAttach: function (aRequest) {
     if (this.state === "exited") {
       return { type: "exited" };
     }
 
     if (this.state !== "detached") {
-      return { error: "wrongState" };
+      return { error: "wrongState",
+               message: "Current state is " + this.state };
     }
 
     this._state = "attached";
 
     update(this._options, aRequest.options || {});
 
     // Initialize an event loop stack. This can't be done in the constructor,
     // because this.conn is not yet initialized by the actor pool at that time.
@@ -736,16 +736,18 @@ ThreadActor.prototype = {
     } catch (e) {
       reportError(e);
       return { error: "notAttached", message: e.toString() };
     }
   },
 
   onDetach: function (aRequest) {
     this.disconnect();
+    this._state = "detached";
+
     dumpn("ThreadActor.prototype.onDetach: returning 'detached' packet");
     return {
       type: "detached"
     };
   },
 
   onReconfigure: function (aRequest) {
     if (this.state == "exited") {
--- a/toolkit/devtools/server/actors/webapps.js
+++ b/toolkit/devtools/server/actors/webapps.js
@@ -672,17 +672,17 @@ WebappsActor.prototype = {
       req.open("GET", iconURL, false);
       req.responseType = "blob";
 
       try {
         req.send(null);
       } catch(e) {
         deferred.resolve({
           error: "noIcon",
-          message: "The icon file '" + iconURL + "' doesn't exists"
+          message: "The icon file '" + iconURL + "' doesn't exist"
         });
         return;
       }
 
       // Convert the blog to a base64 encoded data URI
       let reader = Cc["@mozilla.org/files/filereader;1"]
                      .createInstance(Ci.nsIDOMFileReader);
       reader.onload = function () {
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -885,36 +885,34 @@ BrowserTabActor.prototype = {
 
   /**
    * Handle location changes, by clearing the previous debuggees and enabling
    * debugging, which may have been disabled temporarily by the
    * DebuggerProgressListener.
    */
   onWindowCreated:
   makeInfallible(function BTA_onWindowCreated(evt) {
-    if (evt.target === this.browser.contentDocument) {
-      // pageshow events for non-persisted pages have already been handled by a
-      // prior DOMWindowCreated event.
-      if (evt.type == "pageshow" && !evt.persisted) {
-        return;
-      }
-      if (this._attached) {
-        this.threadActor.clearDebuggees();
-        if (this.threadActor.dbg) {
-          this.threadActor.dbg.enabled = true;
-          this.threadActor.maybePauseOnExceptions();
-        }
+    // pageshow events for non-persisted pages have already been handled by a
+    // prior DOMWindowCreated event.
+    if (!this._attached || (evt.type == "pageshow" && !evt.persisted)) {
+      return;
+    }
+    if (evt.target === this.browser.contentDocument ) {
+      this.threadActor.clearDebuggees();
+      if (this.threadActor.dbg) {
+        this.threadActor.dbg.enabled = true;
+        this.threadActor.global = evt.target.defaultView.wrappedJSObject;
+        this.threadActor.maybePauseOnExceptions();
       }
     }
 
-    if (this._attached) {
-      this.threadActor.global = evt.target.defaultView.wrappedJSObject;
-      if (this.threadActor.attached) {
-        this.threadActor.findGlobals();
-      }
+    // Refresh the debuggee list when a new window object appears (top window or
+    // iframe).
+    if (this.threadActor.attached) {
+      this.threadActor.findGlobals();
     }
   }, "BrowserTabActor.prototype.onWindowCreated"),
 
   /**
    * Tells if the window.console object is native or overwritten by script in
    * the page.
    *
    * @param nsIDOMWindow aWindow
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -371,16 +371,17 @@ var DebuggerServer = {
       this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
       this.addActors("resource://gre/modules/devtools/server/actors/script.js");
       this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
       this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
       this.registerModule("devtools/server/actors/inspector");
       this.registerModule("devtools/server/actors/webgl");
       this.registerModule("devtools/server/actors/stylesheets");
       this.registerModule("devtools/server/actors/styleeditor");
+      this.registerModule("devtools/server/actors/tracer");
     }
     if (!("ContentAppActor" in DebuggerServer)) {
       this.addActors("resource://gre/modules/devtools/server/actors/childtab.js");
     }
   },
 
   /**
    * Listens on the given port or socket file for remote debugger connections.
--- a/toolkit/devtools/server/tests/unit/head_dbg.js
+++ b/toolkit/devtools/server/tests/unit/head_dbg.js
@@ -149,19 +149,20 @@ function attachTestTab(aClient, aTitle, 
 }
 
 // Attach to |aClient|'s tab whose title is |aTitle|, and then attach to
 // that tab's thread. Pass |aCallback| the thread attach response packet, a
 // TabClient referring to the tab, and a ThreadClient referring to the
 // thread.
 function attachTestThread(aClient, aTitle, aCallback) {
   attachTestTab(aClient, aTitle, function (aResponse, aTabClient) {
-    aClient.attachThread(aResponse.threadActor, function (aResponse, aThreadClient) {
+    function onAttach(aResponse, aThreadClient) {
       aCallback(aResponse, aTabClient, aThreadClient);
-    }, { useSourceMaps: true });
+    }
+    aTabClient.attachThread({ useSourceMaps: true }, onAttach);
   });
 }
 
 // Attach to |aClient|'s tab whose title is |aTitle|, attach to the tab's
 // thread, and then resume it. Pass |aCallback| the thread's response to
 // the 'resume' packet, a TabClient for the tab, and a ThreadClient for the
 // thread.
 function attachTestTabAndResume(aClient, aTitle, aCallback) {
--- a/toolkit/devtools/server/tests/unit/test_attach.js
+++ b/toolkit/devtools/server/tests/unit/test_attach.js
@@ -9,25 +9,25 @@ function run_test()
   initTestDebuggerServer();
   gDebuggee = testGlobal("test-1");
   DebuggerServer.addTestGlobal(gDebuggee);
 
   let transport = DebuggerServer.connectPipe();
   gClient = new DebuggerClient(transport);
   gClient.connect(function(aType, aTraits) {
     attachTestTab(gClient, "test-1", function(aReply, aTabClient) {
-      test_attach(aReply.threadActor);
+      test_attach(aTabClient);
     });
   });
   do_test_pending();
 }
 
-function test_attach(aThreadActorID)
+function test_attach(aTabClient)
 {
-  gClient.attachThread(aThreadActorID, function(aResponse, aThreadClient) {
+  aTabClient.attachThread({}, function(aResponse, aThreadClient) {
     do_check_eq(aThreadClient.state, "paused");
     aThreadClient.resume(cleanup);
   });
 }
 
 function cleanup()
 {
   gClient.addListener("closed", function(aEvent) {
--- a/toolkit/devtools/server/tests/unit/test_breakpoint-18.js
+++ b/toolkit/devtools/server/tests/unit/test_breakpoint-18.js
@@ -38,22 +38,22 @@ function setUpCode() {
     },
     gDebuggee,
     "1.8",
     URL
   );
 }
 
 function setBreakpoint() {
+  gClient.addOneTimeListener("resumed", runCode);
   gThreadClient.setBreakpoint({
     url: URL,
     line: 1
   }, ({ error }) => {
     do_check_true(!error);
-    gThreadClient.resume(runCode);
   });
 }
 
 function runCode() {
   gClient.addOneTimeListener("paused", testBPHit);
   gDebuggee.test();
 }
 
--- a/toolkit/devtools/server/tests/unit/test_dbgclient_debuggerstatement.js
+++ b/toolkit/devtools/server/tests/unit/test_dbgclient_debuggerstatement.js
@@ -1,37 +1,39 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 
 var gClient;
+var gTabClient;
 var gDebuggee;
 
 function run_test()
 {
   initTestDebuggerServer();
   gDebuggee = testGlobal("test-1");
   DebuggerServer.addTestGlobal(gDebuggee);
 
   let transport = DebuggerServer.connectPipe();
   gClient = new DebuggerClient(transport);
   gClient.connect(function(aType, aTraits) {
     attachTestTab(gClient, "test-1", function(aReply, aTabClient) {
+      gTabClient = aTabClient;
       test_threadAttach(aReply.threadActor);
     });
   });
   do_test_pending();
 }
 
 function test_threadAttach(aThreadActorID)
 {
   do_print("Trying to attach to thread " + aThreadActorID);
-  gClient.attachThread(aThreadActorID, function(aResponse, aThreadClient) {
+  gTabClient.attachThread({}, function(aResponse, aThreadClient) {
     do_check_eq(aThreadClient.state, "paused");
     do_check_eq(aThreadClient.actor, aThreadActorID);
     aThreadClient.resume(function() {
       do_check_eq(aThreadClient.state, "attached");
       test_debugger_statement(aThreadClient);
     });
   });
 }
--- a/toolkit/devtools/server/tests/unit/test_interrupt.js
+++ b/toolkit/devtools/server/tests/unit/test_interrupt.js
@@ -15,31 +15,31 @@ function run_test()
   gClient.connect(function(aType, aTraits) {
     attachTestTab(gClient, "test-1", test_attach);
   });
   do_test_pending();
 }
 
 function test_attach(aResponse, aTabClient)
 {
-  gClient.attachThread(aResponse.threadActor, function(aResponse, aThreadClient) {
+  aTabClient.attachThread({}, function(aResponse, aThreadClient) {
     do_check_eq(aThreadClient.paused, true);
     aThreadClient.resume(function() {
-      test_interrupt();
+      test_interrupt(aThreadClient);
     });
   });
 }
 
-function test_interrupt()
+function test_interrupt(aThreadClient)
 {
-  do_check_eq(gClient.activeThread.paused, false);
-  gClient.activeThread.interrupt(function(aResponse) {
-    do_check_eq(gClient.activeThread.paused, true);
-    gClient.activeThread.resume(function() {
-      do_check_eq(gClient.activeThread.paused, false);
+  do_check_eq(aThreadClient.paused, false);
+  aThreadClient.interrupt(function(aResponse) {
+    do_check_eq(aThreadClient.paused, true);
+    aThreadClient.resume(function() {
+      do_check_eq(aThreadClient.paused, false);
       cleanup();
     });
   });
 }
 
 function cleanup()
 {
   gClient.addListener("closed", function(aEvent) {
--- a/toolkit/devtools/server/tests/unit/test_nesting-03.js
+++ b/toolkit/devtools/server/tests/unit/test_nesting-03.js
@@ -1,50 +1,52 @@
 /* -*- Mode: javascript; js-indent-level: 2; -*- */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that we can detect nested event loops in tabs with the same URL.
 
 const { defer } = devtools.require("sdk/core/promise");
-var gClient1, gClient2;
+var gClient1, gClient2, gThreadClient1, gThreadClient2;
 
 function run_test() {
   initTestDebuggerServer();
   addTestGlobal("test-nesting1");
   addTestGlobal("test-nesting1");
   // Conect the first client to the first debuggee.
   gClient1 = new DebuggerClient(DebuggerServer.connectPipe());
   gClient1.connect(function () {
     attachTestThread(gClient1, "test-nesting1", function (aResponse, aTabClient, aThreadClient) {
+      gThreadClient1 = aThreadClient;
       start_second_connection();
     });
   });
   do_test_pending();
 }
 
 function start_second_connection() {
   gClient2 = new DebuggerClient(DebuggerServer.connectPipe());
   gClient2.connect(function () {
     attachTestThread(gClient2, "test-nesting1", function (aResponse, aTabClient, aThreadClient) {
+      gThreadClient2 = aThreadClient;
       test_nesting();
     });
   });
 }
 
 function test_nesting() {
   const { resolve, reject, promise } = defer();
 
-  gClient1.activeThread.resume(aResponse => {
+  gThreadClient1.resume(aResponse => {
     do_check_eq(aResponse.error, "wrongOrder");
-    gClient2.activeThread.resume(aResponse => {
+    gThreadClient2.resume(aResponse => {
       do_check_true(!aResponse.error);
-      do_check_eq(aResponse.from, gClient2.activeThread.actor);
+      do_check_eq(aResponse.from, gThreadClient2.actor);
 
-      gClient1.activeThread.resume(aResponse => {
+      gThreadClient1.resume(aResponse => {
         do_check_true(!aResponse.error);
-        do_check_eq(aResponse.from, gClient1.activeThread.actor);
+        do_check_eq(aResponse.from, gThreadClient1.actor);
 
         gClient1.close(() => finishClient(gClient2));
       });
     });
   });
 }
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-13.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-13.js
@@ -54,13 +54,13 @@ function test_definition_site(func, obj)
     do_check_eq(column, 0);
 
     test_bad_definition_site(obj);
   });
 }
 
 function test_bad_definition_site(obj) {
   try {
-    obj.getDefinitionSite(() => do_check_true(false));
+    obj._client.request("definitionSite", () => do_check_true(false));
   } catch (e) {
     gThreadClient.resume(() => finishClient(gClient));
   }
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_reattach-thread.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that reattaching to a previously detached thread works.
+ */
+
+var gClient, gDebuggee, gThreadClient, gTabClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = testGlobal("test-reattach");
+  DebuggerServer.addTestGlobal(gDebuggee);
+
+  let transport = DebuggerServer.connectPipe();
+  gClient = new DebuggerClient(transport);
+  gClient.connect(() => {
+    attachTestTab(gClient, "test-reattach", (aReply, aTabClient) => {
+      gTabClient = aTabClient;
+      test_attach();
+    });
+  });
+  do_test_pending();
+}
+
+function test_attach()
+{
+  gTabClient.attachThread({}, (aResponse, aThreadClient) => {
+    do_check_eq(aThreadClient.state, "paused");
+    gThreadClient = aThreadClient;
+    aThreadClient.resume(test_detach);
+  });
+}
+
+function test_detach()
+{
+  gThreadClient.detach(() => {
+    do_check_eq(gThreadClient.state, "detached");
+    do_check_eq(gTabClient.thread, null);
+    test_reattach();
+  });
+}
+
+function test_reattach()
+{
+  gTabClient.attachThread({}, (aResponse, aThreadClient) => {
+    do_check_neq(gThreadClient, aThreadClient);
+    do_check_eq(aThreadClient.state, "paused");
+    do_check_eq(gTabClient.thread, aThreadClient);
+    aThreadClient.resume(cleanup);
+  });
+}
+
+function cleanup()
+{
+  gClient.close(do_test_finished);
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -25,16 +25,17 @@ support-files =
 [test_dbgsocket.js]
 skip-if = toolkit == "gonk"
 reason = bug 821285
 [test_dbgsocket_connection_drop.js]
 [test_dbgactor.js]
 [test_dbgglobal.js]
 [test_dbgclient_debuggerstatement.js]
 [test_attach.js]
+[test_reattach-thread.js]
 [test_blackboxing-01.js]
 [test_blackboxing-02.js]
 [test_blackboxing-03.js]
 [test_blackboxing-04.js]
 [test_blackboxing-05.js]
 [test_blackboxing-06.js]
 [test_frameactor-01.js]
 [test_frameactor-02.js]