Bug 717749 - Part 2: Hook up the debugger to the slow script debug service. (r=past)
authorShu-yu Guo <shu@rfrn.org>
Tue, 20 May 2014 18:27:25 -0700
changeset 184039 ae9ea46cbe269a706e177f676f6e550182808cde
parent 184038 3ea744b43a24a9bf7e666026b9a15ee9d5927c2e
child 184040 94e7e63ae385c87cbb1de52a1641b628b72e8401
push id26810
push usercbook@mozilla.com
push dateWed, 21 May 2014 11:46:36 +0000
treeherdermozilla-central@50fb8c4db2fd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast
bugs717749
milestone32.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 717749 - Part 2: Hook up the debugger to the slow script debug service. (r=past)
browser/devtools/debugger/debugger-controller.js
browser/devtools/framework/gDevTools.jsm
toolkit/devtools/client/dbg-client.jsm
toolkit/devtools/server/actors/script.js
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -457,16 +457,23 @@ ThreadState.prototype = {
     dumpn("Handling tab navigation in the ThreadState");
     this._update();
   },
 
   /**
    * Update the UI after a thread state change.
    */
   _update: function(aEvent) {
+    // Ignore "interrupted" events, which are generated by the slow script
+    // dialog and internal events such as setting breakpoints, to avoid UI
+    // flicker.
+    if (aEvent == "interrupted") {
+      return;
+    }
+
     DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state);
 
     if (gTarget && (aEvent == "paused" || aEvent == "resumed")) {
       gTarget.emit("thread-" + aEvent);
     }
   }
 };
 
@@ -563,16 +570,21 @@ StackFrames.prototype = {
         if (!aPacket.why.frameFinished) {
           break;
         } else if (aPacket.why.frameFinished.throw) {
           this._currentException = aPacket.why.frameFinished.throw;
         } else if (aPacket.why.frameFinished.return) {
           this._currentReturnedValue = aPacket.why.frameFinished.return;
         }
         break;
+      // If paused by an explicit interrupt, which are generated by the slow
+      // script dialog and internal events such as setting breakpoints, ignore
+      // the event to avoid UI flicker.
+      case "interrupted":
+        return;
     }
 
     this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
     DebuggerView.editor.focus();
   },
 
   /**
    * Handler for the thread client's resumed notification.
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -575,16 +575,89 @@ let gDevToolsBrowser = {
       devtoolsKeyset = doc.createElement("keyset");
       devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
     }
     devtoolsKeyset.appendChild(keys);
     let mainKeyset = doc.getElementById("mainKeyset");
     mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
   },
 
+  /**
+   * Hook the JS debugger tool to the "Debug Script" button of the slow script
+   * dialog.
+   */
+  setSlowScriptDebugHandler: function DT_setSlowScriptDebugHandler() {
+    let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
+                         .getService(Ci.nsISlowScriptDebug);
+    let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+
+    debugService.activationHandler = function(aWindow) {
+      let chromeWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                                .getInterface(Ci.nsIWebNavigation)
+                                .QueryInterface(Ci.nsIDocShellTreeItem)
+                                .rootTreeItem
+                                .QueryInterface(Ci.nsIInterfaceRequestor)
+                                .getInterface(Ci.nsIDOMWindow)
+                                .QueryInterface(Ci.nsIDOMChromeWindow);
+      let target = devtools.TargetFactory.forTab(chromeWindow.gBrowser.selectedTab);
+
+      let setupFinished = false;
+      gDevTools.showToolbox(target, "jsdebugger").then(toolbox => {
+        let threadClient = toolbox.getCurrentPanel().panelWin.gThreadClient;
+
+        // Break in place, which means resuming the debuggee thread and pausing
+        // right before the next step happens.
+        switch (threadClient.state) {
+          case "paused":
+            // When the debugger is already paused.
+            threadClient.breakOnNext();
+            setupFinished = true;
+            break;
+          case "attached":
+            // When the debugger is already open.
+            threadClient.interrupt(() => {
+              threadClient.breakOnNext();
+              setupFinished = true;
+            });
+            break;
+          case "resuming":
+            // The debugger is newly opened.
+            threadClient.addOneTimeListener("resumed", () => {
+              threadClient.interrupt(() => {
+                threadClient.breakOnNext();
+                setupFinished = true;
+              });
+            });
+            break;
+          default:
+            throw Error("invalid thread client state in slow script debug handler: " +
+                        threadClient.state);
+          }
+      });
+
+      // Don't return from the interrupt handler until the debugger is brought
+      // up; no reason to continue executing the slow script.
+      let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIDOMWindowUtils);
+      utils.enterModalState();
+      while (!setupFinished) {
+        tm.currentThread.processNextEvent(true);
+      }
+      utils.leaveModalState();
+    };
+  },
+
+  /**
+   * Unset the slow script debug handler.
+   */
+  unsetSlowScriptDebugHandler: function DT_unsetSlowScriptDebugHandler() {
+    let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
+                         .getService(Ci.nsISlowScriptDebug);
+    debugService.activationHandler = undefined;
+  },
 
   /**
    * Detect the presence of a Firebug.
    *
    * @return promise
    */
   _isFirebugInstalled: function DT_isFirebugInstalled() {
     let bootstrappedAddons = Services.prefs.getCharPref("extensions.bootstrappedAddons");
@@ -664,16 +737,20 @@ let gDevToolsBrowser = {
           ref = doc.getElementById("menu_devtools_separator");
         }
 
         if (ref) {
           mp.insertBefore(elements.menuitem, ref);
         }
       }
     }
+
+    if (toolDefinition.id === "jsdebugger") {
+      gDevToolsBrowser.setSlowScriptDebugHandler();
+    }
   },
 
   /**
    * Add all tools to the developer tools menu of a window.
    *
    * @param {XULDocument} doc
    *        The document to which the tool items are to be added.
    */
@@ -839,16 +916,20 @@ let gDevToolsBrowser = {
    *
    * @param {string} toolId
    *        id of the tool to remove
    */
   _removeToolFromWindows: function DT_removeToolFromWindows(toolId) {
     for (let win of gDevToolsBrowser._trackedBrowserWindows) {
       gDevToolsBrowser._removeToolFromMenu(toolId, win.document);
     }
+
+    if (toolId === "jsdebugger") {
+      gDevToolsBrowser.unsetSlowScriptDebugHandler();
+    }
   },
 
   /**
    * Remove a tool's menuitem from a window
    *
    * @param {string} toolId
    *        Id of the tool to add a menu entry for
    * @param {XULDocument} doc
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -1489,16 +1489,26 @@ ThreadClient.prototype = {
   /**
    * Resume a paused thread.
    */
   resume: function (aOnResponse) {
     this._doResume(null, aOnResponse);
   },
 
   /**
+   * Resume then pause without stepping.
+   *
+   * @param function aOnResponse
+   *        Called with the response packet.
+   */
+  breakOnNext: function (aOnResponse) {
+    this._doResume({ type: "break" }, aOnResponse);
+  },
+
+  /**
    * Step over a function call.
    *
    * @param function aOnResponse
    *        Called with the response packet.
    */
   stepOver: function (aOnResponse) {
     this._doResume({ type: "next" }, aOnResponse);
   },
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -946,17 +946,25 @@ ThreadActor.prototype = {
           aPacket.why.frameFinished.throw = createValueGrip(aCompletion.throw);
         }
         return aPacket;
       });
     };
   },
 
   _makeOnStep: function ({ thread, pauseAndRespond, startFrame,
-                           startLocation }) {
+                           startLocation, steppingType }) {
+    // Breaking in place: we should always pause.
+    if (steppingType === "break") {
+      return function () {
+        return pauseAndRespond(this);
+      };
+    }
+
+    // Otherwise take what a "step" means into consideration.
     return function () {
       // onStep is called with 'this' set to the current frame.
 
       const generatedLocation = getFrameLocation(this);
       const newLocation = thread.synchronize(thread.sources.getOriginalLocation(
         generatedLocation));
 
       // Cases when we should pause because we have executed enough to consider
@@ -991,29 +999,30 @@ ThreadActor.prototype = {
       // consider this a "step" yet).
       return undefined;
     };
   },
 
   /**
    * Define the JS hook functions for stepping.
    */
-  _makeSteppingHooks: function (aStartLocation) {
+  _makeSteppingHooks: function (aStartLocation, steppingType) {
     // Bind these methods and state because some of the hooks are called
     // with 'this' set to the current frame. Rather than repeating the
     // binding in each _makeOnX method, just do it once here and pass it
     // in to each function.
     const steppingHookState = {
       pauseAndRespond: (aFrame, onPacket=(k)=>k) => {
         this._pauseAndRespond(aFrame, { type: "resumeLimit" }, onPacket);
       },
       createValueGrip: this.createValueGrip.bind(this),
       thread: this,
       startFrame: this.youngestFrame,
-      startLocation: aStartLocation
+      startLocation: aStartLocation,
+      steppingType: steppingType
     };
 
     return {
       onEnterFrame: this._makeOnEnterFrame(steppingHookState),
       onPop: this._makeOnPop(steppingHookState),
       onStep: this._makeOnStep(steppingHookState)
     };
   },
@@ -1024,34 +1033,36 @@ ThreadActor.prototype = {
    *
    * @param Object aRequest
    *        The request packet received over the RDP.
    * @returns A promise that resolves to true once the hooks are attached, or is
    *          rejected with an error packet.
    */
   _handleResumeLimit: function (aRequest) {
     let steppingType = aRequest.resumeLimit.type;
-    if (["step", "next", "finish"].indexOf(steppingType) == -1) {
+    if (["break", "step", "next", "finish"].indexOf(steppingType) == -1) {
       return reject({ error: "badParameterType",
                       message: "Unknown resumeLimit type" });
     }
 
     const generatedLocation = getFrameLocation(this.youngestFrame);
     return this.sources.getOriginalLocation(generatedLocation)
       .then(originalLocation => {
-        const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks(originalLocation);
+        const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks(originalLocation,
+                                                                        steppingType);
 
         // Make sure there is still a frame on the stack if we are to continue
         // stepping.
         let stepFrame = this._getNextStepFrame(this.youngestFrame);
         if (stepFrame) {
           switch (steppingType) {
             case "step":
               this.dbg.onEnterFrame = onEnterFrame;
               // Fall through.
+            case "break":
             case "next":
               if (stepFrame.script) {
                   stepFrame.onStep = onStep;
               }
               stepFrame.onPop = onPop;
               break;
             case "finish":
               stepFrame.onPop = onPop;