merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 28 Aug 2014 16:02:15 +0200
changeset 223753 47c9418fbc28608e6b9b2c0b1f1664b183bd5b30
parent 223748 2a15dc07ddaa262b994e9bbd6623773714226b30 (current diff)
parent 223752 141dc6c20917c7fcd027d6c44df09a1058880525 (diff)
child 223796 61c5ca15dd597d9738422554f08595e662f67acd
child 223816 f6beabe7cfb60c8a405199cff263b5e7ecd5cd93
child 223840 cd4d6b7e1ef1fd78dfed45a46324b007316ef8f6
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone34.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 fx-team to mozilla-central a=merge
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -1166,16 +1166,17 @@ SourceScripts.prototype = {
         }
       });
     }
 
     // If there are any stored breakpoints for this source, display them again,
     // both in the editor and the breakpoints pane.
     DebuggerController.Breakpoints.updatePaneBreakpoints();
     DebuggerController.Breakpoints.updateEditorBreakpoints();
+    DebuggerController.HitCounts.updateEditorHitCounts();
 
     // Make sure the events listeners are up to date.
     if (DebuggerView.instrumentsPaneTab == "events-tab") {
       DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch();
     }
 
     // Signal that a new source has been added.
     window.emit(EVENTS.NEW_SOURCE);
@@ -1218,16 +1219,17 @@ SourceScripts.prototype = {
     else if (!DebuggerView.Sources.selectedValue) {
       DebuggerView.Sources.selectedIndex = 0;
     }
 
     // If there are any stored breakpoints for the sources, display them again,
     // both in the editor and the breakpoints pane.
     DebuggerController.Breakpoints.updatePaneBreakpoints();
     DebuggerController.Breakpoints.updateEditorBreakpoints();
+    DebuggerController.HitCounts.updateEditorHitCounts();
 
     // Signal that sources have been added.
     window.emit(EVENTS.SOURCES_ADDED);
   },
 
   /**
    * Handler for the debugger client's 'blackboxchange' notification.
    */
@@ -1483,16 +1485,17 @@ Tracer.prototype = {
     }
 
     DebuggerView.Tracer.selectTab();
 
     let id = this._trace = "dbg.trace" + Math.random();
     let fields = [
       "name",
       "location",
+      "hitCount",
       "parameterNames",
       "depth",
       "arguments",
       "return",
       "throw",
       "yield"
     ];
 
@@ -1516,40 +1519,50 @@ Tracer.prototype = {
     }
     this.traceClient.stopTrace(this._trace, aResponse => {
       const { error } = aResponse;
       if (error) {
         DevToolsUtils.reportException("Tracer.prototype.stopTracing", error);
       }
 
       this._trace = null;
+      DebuggerController.HitCounts.clear();
       aCallback(aResponse);
     });
   },
 
   onTraces: function (aEvent, { traces }) {
     const tracesLength = traces.length;
     let tracesToShow;
 
+    // Update hit counts.
+    for (let t of traces) {
+      if (t.type == "enteredFrame") {
+        DebuggerController.HitCounts.set(t.location, t.hitCount);
+      }
+    }
+    DebuggerController.HitCounts.updateEditorHitCounts();
+
+    // Limit number of traces to be shown in the log.
     if (tracesLength > TracerView.MAX_TRACES) {
       tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES, tracesLength);
       this._stack.splice(0, this._stack.length);
       DebuggerView.Tracer.empty();
     } else {
       tracesToShow = traces;
     }
 
+    // Show traces in the log.
     for (let t of tracesToShow) {
       if (t.type == "enteredFrame") {
         this._onCall(t);
       } else {
         this._onReturn(t);
       }
     }
-
     DebuggerView.Tracer.commit();
   },
 
   /**
    * Callback for handling a new call frame.
    */
   _onCall: function({ name, location, blackBoxed, parameterNames, depth, arguments: args }) {
     const item = {
@@ -2220,16 +2233,94 @@ Breakpoints.prototype = {
 Object.defineProperty(Breakpoints.prototype, "_addedOrDisabled", {
   get: function* () {
     yield* this._added.values();
     yield* this._disabled.values();
   }
 });
 
 /**
+ * Handles Tracer's hit counts.
+ */
+function HitCounts() {
+  /**
+   * Storage of hit counts for every location
+   * hitCount = _locations[url][line][column]
+   */
+  this._hitCounts = Object.create(null);
+}
+
+HitCounts.prototype = {
+  set: function({url, line, column}, aHitCount) {
+    if (!this._hitCounts[url]) {
+      this._hitCounts[url] = Object.create(null);
+    }
+    if (!this._hitCounts[url][line]) {
+      this._hitCounts[url][line] = Object.create(null);
+    }
+    this._hitCounts[url][line][column] = aHitCount;
+  },
+
+  /**
+   * Update all the hit counts in the editor view. This is invoked when the
+   * selected script is changed, or when new sources are received via the
+   * _onNewSource and _onSourcesAdded event listeners.
+   */
+  updateEditorHitCounts: function() {
+    // First, remove all hit counters.
+    DebuggerView.editor.removeAllMarkers("hit-counts");
+
+    // Then, add new hit counts, just for the current source.
+    for (let url in this._hitCounts) {
+      for (let line in this._hitCounts[url]) {
+        for (let column in this._hitCounts[url][line]) {
+          this._updateEditorHitCount({url, line, column});
+        }
+      }
+    }
+  },
+
+  /**
+   * Update a hit counter on a certain line.
+   */
+  _updateEditorHitCount: function({url, line, column}) {
+    // Editor must be initialized.
+    if (!DebuggerView.editor) {
+      return;
+    }
+
+    // No need to do anything if the counter's source is not being shown in the
+    // editor.
+    if (DebuggerView.Sources.selectedValue != url) {
+      return;
+    }
+
+    // There might be more counters on the same line. We need to combine them
+    // into one.
+    let content = Object.keys(this._hitCounts[url][line])
+                    .sort() // Sort by key (column).
+                    .map(a => this._hitCounts[url][line][a]) // Extract values.
+                    .map(a => a + "\u00D7") // Format hit count (e.g. 146×).
+                    .join("|");
+
+    // CodeMirror's lines are indexed from 0, while traces start from 1
+    DebuggerView.editor.addContentMarker(line - 1, "hit-counts", "hit-count",
+                                         content);
+  },
+
+  /**
+   * Remove all hit couters and clear the storage
+   */
+  clear: function() {
+    DebuggerView.editor.removeAllMarkers("hit-counts");
+    this._hitCounts = Object.create(null);
+  }
+}
+
+/**
  * Localization convenience methods.
  */
 let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);
 
 /**
  * Shortcuts for accessing various debugger preferences.
  */
 let Prefs = new ViewHelpers.Prefs("devtools", {
@@ -2260,16 +2351,17 @@ EventEmitter.decorate(this);
 DebuggerController.initialize();
 DebuggerController.Parser = new Parser();
 DebuggerController.ThreadState = new ThreadState();
 DebuggerController.StackFrames = new StackFrames();
 DebuggerController.SourceScripts = new SourceScripts();
 DebuggerController.Breakpoints = new Breakpoints();
 DebuggerController.Breakpoints.DOM = new EventListeners();
 DebuggerController.Tracer = new Tracer();
+DebuggerController.HitCounts = new HitCounts();
 
 /**
  * Export some properties to the global scope for easier access.
  */
 Object.defineProperties(window, {
   "gTarget": {
     get: function() DebuggerController._target
   },
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -216,22 +216,27 @@ let DebuggerView = {
     extraKeys["Esc"] = false;
 
     function bindKey(func, key, modifiers = {}) {
       let key = document.getElementById(key).getAttribute("key");
       let shortcut = Editor.accel(key, modifiers);
       extraKeys[shortcut] = () => DebuggerView.Filtering[func]();
     }
 
+    let gutters = ["breakpoints"];
+    if (Services.prefs.getBoolPref("devtools.debugger.tracer")) {
+      gutters.unshift("hit-counts");
+    }
+
     this.editor = new Editor({
       mode: Editor.modes.text,
       readOnly: true,
       lineNumbers: true,
       showAnnotationRuler: true,
-      gutters: [ "breakpoints" ],
+      gutters: gutters,
       extraKeys: extraKeys,
       contextMenu: "sourceEditorContextMenu"
     });
 
     this.editor.appendTo(document.getElementById("editor")).then(() => {
       this.editor.extend(DebuggerEditor);
       this._loadingText = L10N.getStr("loadingText");
       this._onEditorLoad(aCallback);
@@ -405,16 +410,17 @@ let DebuggerView = {
       }
 
       this._setEditorText(aText);
       this._setEditorMode(aSource.url, aContentType, aText);
 
       // Synchronize any other components with the currently displayed source.
       DebuggerView.Sources.selectedValue = aSource.url;
       DebuggerController.Breakpoints.updateEditorBreakpoints();
+      DebuggerController.HitCounts.updateEditorHitCounts();
 
       histogram.add(Date.now() - startTime);
 
       // Resolve and notify that a source file was shown.
       window.emit(EVENTS.SOURCE_SHOWN, aSource);
       deferred.resolve([aSource, aText, aContentType]);
     },
     ([, aError]) => {
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -18,16 +18,17 @@ support-files =
   code_function-search-01.js
   code_function-search-02.js
   code_function-search-03.js
   code_location-changes.js
   code_math.js
   code_math.map
   code_math.min.js
   code_math_bogus_map.js
+  code_same-line-functions.js
   code_script-switching-01.js
   code_script-switching-02.js
   code_test-editor-mode
   code_tracing-01.js
   code_ugly.js
   code_ugly-2.js
   code_ugly-3.js
   code_ugly-4.js
@@ -69,16 +70,17 @@ support-files =
   doc_no-page-sources.html
   doc_pause-exceptions.html
   doc_pretty-print.html
   doc_pretty-print-2.html
   doc_pretty-print-3.html
   doc_pretty-print-on-paused.html
   doc_random-javascript.html
   doc_recursion-stack.html
+  doc_same-line-functions.html
   doc_scope-variable.html
   doc_scope-variable-2.html
   doc_scope-variable-3.html
   doc_scope-variable-4.html
   doc_script-switching-01.html
   doc_script-switching-02.html
   doc_split-console-paused-reload.html
   doc_step-out.html
@@ -156,16 +158,18 @@ skip-if = true # Bug 933950 (leaky test)
 [browser_dbg_editor-mode.js]
 [browser_dbg_event-listeners-01.js]
 [browser_dbg_event-listeners-02.js]
 [browser_dbg_event-listeners-03.js]
 [browser_dbg_file-reload.js]
 [browser_dbg_function-display-name.js]
 [browser_dbg_global-method-override.js]
 [browser_dbg_globalactor.js]
+[browser_dbg_hit-counts-01.js]
+[browser_dbg_hit-counts-02.js]
 [browser_dbg_host-layout.js]
 [browser_dbg_iframes.js]
 [browser_dbg_instruments-pane-collapse.js]
 [browser_dbg_interrupts.js]
 [browser_dbg_listaddons.js]
 [browser_dbg_listtabs-01.js]
 [browser_dbg_listtabs-02.js]
 [browser_dbg_listtabs-03.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_hit-counts-01.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Evaluating two functions on the same line and checking for correct hit count
+ * for both of them in CodeMirror's gutter.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_same-line-functions.html";
+const CODE_URL = "code_same-line-functions.js";
+
+let gTab, gDebuggee, gPanel, gDebugger;
+let gEditor;
+
+function test() {
+  Task.async(function* () {
+    yield pushPrefs(["devtools.debugger.tracer", true]);
+
+    initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
+      gTab = aTab;
+      gDebuggee = aDebuggee;
+      gPanel = aPanel;
+      gDebugger = gPanel.panelWin;
+      gEditor = gDebugger.DebuggerView.editor;
+
+      Task.async(function* () {
+        yield waitForSourceShown(gPanel, CODE_URL);
+        yield startTracing(gPanel);
+
+        clickButton();
+
+        yield waitForClientEvents(aPanel, "traces");
+
+        testHitCounts();
+
+        yield stopTracing(gPanel);
+        yield popPrefs();
+        yield closeDebuggerAndFinish(gPanel);
+      })();
+    });
+  })().catch(e => {
+    ok(false, "Got an error: " + e.message + "\n" + e.stack);
+  });
+}
+
+function clickButton() {
+  EventUtils.sendMouseEvent({ type: "click" },
+                            gDebuggee.document.querySelector("button"),
+                            gDebuggee);
+}
+
+function testHitCounts() {
+  let marker = gEditor.getMarker(0, 'hit-counts');
+
+  is(marker.innerHTML, "1\u00D7|1\u00D7",
+    "Both functions should be hit only once.");
+}
+
+registerCleanupFunction(function() {
+  gTab = null;
+  gDebuggee = null;
+  gPanel = null;
+  gDebugger = null;
+  gEditor = null;
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_hit-counts-02.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * When tracing is stopped all hit counters should be cleared.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_same-line-functions.html";
+const CODE_URL = "code_same-line-functions.js";
+
+let gTab, gDebuggee, gPanel, gDebugger;
+let gEditor;
+
+function test() {
+  Task.async(function* () {
+    yield pushPrefs(["devtools.debugger.tracer", true]);
+
+    initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
+      gTab = aTab;
+      gDebuggee = aDebuggee;
+      gPanel = aPanel;
+      gDebugger = gPanel.panelWin;
+      gEditor = gDebugger.DebuggerView.editor;
+
+      Task.async(function* () {
+        yield waitForSourceShown(gPanel, CODE_URL);
+        yield startTracing(gPanel);
+
+        clickButton();
+
+        yield waitForClientEvents(aPanel, "traces");
+
+        testHitCountsBeforeStopping();
+
+        yield stopTracing(gPanel);
+
+        testHitCountsAfterStopping();
+
+        yield popPrefs();
+        yield closeDebuggerAndFinish(gPanel);
+      })();
+    });
+  })().catch(e => {
+    ok(false, "Got an error: " + e.message + "\n" + e.stack);
+  });
+}
+
+function clickButton() {
+  EventUtils.sendMouseEvent({ type: "click" },
+                            gDebuggee.document.querySelector("button"),
+                            gDebuggee);
+}
+
+function testHitCountsBeforeStopping() {
+  let marker = gEditor.getMarker(0, 'hit-counts');
+  ok(marker, "A counter should exists.");
+}
+
+function testHitCountsAfterStopping() {
+  let marker = gEditor.getMarker(0, 'hit-counts');
+  is(marker, undefined, "A counter should be cleared.");
+}
+
+registerCleanupFunction(function() {
+  gTab = null;
+  gDebuggee = null;
+  gPanel = null;
+  gDebugger = null;
+  gEditor = null;
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/code_same-line-functions.js
@@ -0,0 +1,1 @@
+function first() { var a = "first"; second(); function second() { var a = "second"; } }
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/doc_same-line-functions.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Debugger Tracer test page</title>
+  </head>
+
+  <body>
+    <script src="code_same-line-functions.js"></script>
+    <button onclick="first()">Click me!</button>
+  </body>
+</html>
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -921,8 +921,19 @@ function doResume(aPanel) {
   return rdpInvoke(threadClient, threadClient.resume);
 }
 
 function doInterrupt(aPanel) {
   const threadClient = aPanel.panelWin.gThreadClient;
   return rdpInvoke(threadClient, threadClient.interrupt);
 }
 
+function pushPrefs(...aPrefs) {
+  let deferred = promise.defer();
+  SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve);
+  return deferred.promise;
+}
+
+function popPrefs() {
+  let deferred = promise.defer();
+  SpecialPowers.popPrefEnv(deferred.resolve);
+  return deferred.promise;
+}
\ No newline at end of file
--- a/browser/devtools/sourceeditor/codemirror/mozilla.css
+++ b/browser/devtools/sourceeditor/codemirror/mozilla.css
@@ -2,26 +2,41 @@
  * 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/. */
 
 .errors,
 .breakpoints {
   width: 16px;
 }
 
+.hit-counts {
+  width: 6px;
+}
+
 .error, .breakpoint, .debugLocation, .breakpoint-debugLocation {
   display: inline-block;
   margin-left: 5px;
   width: 12px;
   height: 12px;
   background-repeat: no-repeat;
   background-position: center;
   background-size: contain;
 }
 
+.hit-count {
+  display: inline-block;
+  height: 12px;
+  border: solid rgba(0,0,0,0.2);
+  border-width: 1px 1px 1px 0;
+  border-radius: 0 3px 3px 0;
+  padding: 0 3px;
+  font-size: 10px;
+  pointer-events: none;
+}
+
 .error {
   background-image: url("chrome://browser/skin/devtools/editor-error.png");
   opacity: 0.75;
 }
 
 .breakpoint {
   background-image: url("chrome://browser/skin/devtools/editor-breakpoint.png");
   position: relative;
--- a/browser/devtools/sourceeditor/debugger.js
+++ b/browser/devtools/sourceeditor/debugger.js
@@ -110,16 +110,17 @@ function hasBreakpoint(ctx, line) {
   // In some rare occasions CodeMirror might not be properly initialized yet, so
   // return an exceptional value in that case.
   if (cm.lineInfo(line) === null) {
     return null;
   }
   let markers = cm.lineInfo(line).gutterMarkers;
 
   return markers != null &&
+    markers.breakpoints &&
     markers.breakpoints.classList.contains("breakpoint");
 }
 
 /**
  * Adds a visual breakpoint for a specified line. Third
  * parameter 'cond' can hold any object.
  *
  * After adding a breakpoint, this function makes Editor to
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -613,16 +613,42 @@ Editor.prototype = {
   removeMarker: function (line, gutterName, markerClass) {
     if (!this.hasMarker(line, gutterName, markerClass))
       return;
 
     let cm = editors.get(this);
     cm.lineInfo(line).gutterMarkers[gutterName].classList.remove(markerClass);
   },
 
+  /**
+   * Adds a marker with a specified class and an HTML content to a line's
+   * gutter. If another marker exists on that line, it is overwritten by a new
+   * marker.
+   */
+  addContentMarker: function (line, gutterName, markerClass, content) {
+    let cm = editors.get(this);
+    let info = cm.lineInfo(line);
+    if (!info)
+      return;
+
+    let marker = cm.getWrapperElement().ownerDocument.createElement("div");
+    marker.className = markerClass;
+    marker.innerHTML = content;
+    cm.setGutterMarker(info.line, gutterName, marker);
+  },
+
+  /**
+   * The reverse of addContentMarker. Removes any line's markers in the
+   * specified gutter.
+   */
+  removeContentMarker: function (line, gutterName) {
+    let cm = editors.get(this);
+    cm.setGutterMarker(info.line, gutterName, null);
+  },
+
   getMarker: function(line, gutterName) {
     let cm = editors.get(this);
     let info = cm.lineInfo(line);
     if (!info)
       return null;
 
     let gutterMarkers = info.gutterMarkers;
     if (!gutterMarkers)
--- a/toolkit/components/places/ColorAnalyzer_worker.js
+++ b/toolkit/components/places/ColorAnalyzer_worker.js
@@ -114,17 +114,17 @@ onmessage = function(event) {
     weight *= chroma / CHROMA_WEIGHT_MIDDLE;
 
     metadata.desirability *= weight;
     return metadata;
   });
 
   // only send back the most desirable colors
   mergedColors.sort(function(a, b) {
-    return b.desirability - a.desirability;
+    return b.desirability != a.desirability ? b.desirability - a.desirability : b.color - a.color;
   });
   mergedColors = mergedColors.map(function(metadata) {
     return metadata.color;
   }).slice(0, maxColors);
   postMessage({ colors: mergedColors });
 };
 
 /**
@@ -211,17 +211,17 @@ function mergeColors(colorFrequencies, n
     };
   });
 
   let merged = clusterlib.hcluster(items, distance, merge, threshold);
   return merged;
 }
 
 function descendingFreqSort(a, b) {
-  return b.freq - a.freq;
+  return b.freq != a.freq ? b.freq - a.freq : b.color - a.color;
 }
 
 /**
  * Given two items for a pair of clusters (as created in mergeColors above),
  * determine the distance between them so we know if we should merge or not.
  * Uses the euclidean distance between their mean colors in the lab color
  * space, weighted so larger items are harder to merge.
  *
--- a/toolkit/components/places/tests/browser/browser_colorAnalyzer.js
+++ b/toolkit/components/places/tests/browser/browser_colorAnalyzer.js
@@ -325,21 +325,21 @@ tests.push(function test_perfBigImage() 
 const filePrefix = getRootDirectory(gTestPath) + "colorAnalyzer/";
 
 tests.push(function test_categoryDiscover() {
   frcTest(filePrefix + "category-discover.png", 0xB28D3A,
           "category-discover analysis returns red");
 });
 
 tests.push(function test_localeGeneric() {
-  frcTest(filePrefix + "localeGeneric.png", 0x00A400,
-          "localeGeneric analysis returns orange");
+  frcTest(filePrefix + "localeGeneric.png", 0x3EC23E,
+          "localeGeneric analysis returns green");
 });
 
 tests.push(function test_dictionaryGeneric() {
-  frcTest(filePrefix + "dictionaryGeneric-16.png", 0x502E1E,
-          "dictionaryGeneric-16 analysis returns blue");
+  frcTest(filePrefix + "dictionaryGeneric-16.png", 0x854C30,
+          "dictionaryGeneric-16 analysis returns brown");
 });
 
 tests.push(function test_extensionGeneric() {
   frcTest(filePrefix + "extensionGeneric-16.png", 0x53BA3F,
           "extensionGeneric-16 analysis returns green");
 });
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -1787,17 +1787,23 @@ ThreadClient.prototype = {
     }
     // Otherwise, force a pause in order to set the breakpoint.
     this.interrupt((aResponse) => {
       if (aResponse.error) {
         // Can't set the breakpoint if pausing failed.
         aOnResponse(aResponse);
         return;
       }
-      doSetBreakpoint(this.resume.bind(this));
+
+      const { type, why } = aResponse;
+      const cleanUp = type == "paused" && why.type == "interrupted"
+        ? () => this.resume()
+        : noop;
+
+      doSetBreakpoint(cleanUp);
     });
   },
 
   /**
    * Release multiple thread-lifetime object actors. If any pause-lifetime
    * actors are included in the request, a |notReleasable| error will return,
    * but all the thread-lifetime ones will have been released.
    *
--- a/toolkit/devtools/server/actors/common.js
+++ b/toolkit/devtools/server/actors/common.js
@@ -161,8 +161,32 @@ ActorPool.prototype = {
       actor.disconnect();
     }
     this._cleanups = {};
   }
 }
 
 exports.ActorPool = ActorPool;
 
+// TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when it is
+// implemented.
+exports.getOffsetColumn = function getOffsetColumn(aOffset, aScript) {
+  let bestOffsetMapping = null;
+  for (let offsetMapping of aScript.getAllColumnOffsets()) {
+    if (!bestOffsetMapping ||
+        (offsetMapping.offset <= aOffset &&
+         offsetMapping.offset > bestOffsetMapping.offset)) {
+      bestOffsetMapping = offsetMapping;
+    }
+  }
+
+  if (!bestOffsetMapping) {
+    // XXX: Try not to completely break the experience of using the debugger for
+    // the user by assuming column 0. Simultaneously, report the error so that
+    // there is a paper trail if the assumption is bad and the debugging
+    // experience becomes wonky.
+    reportError(new Error("Could not find a column for offset " + aOffset
+                          + " in the script " + aScript));
+    return 0;
+  }
+
+  return bestOffsetMapping.columnNumber;
+}
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -3,17 +3,17 @@
 /* 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 Services = require("Services");
 const { Cc, Ci, Cu, components, ChromeWorker } = require("chrome");
-const { ActorPool } = require("devtools/server/actors/common");
+const { ActorPool, getOffsetColumn } = require("devtools/server/actors/common");
 const { DebuggerServer } = require("devtools/server/main");
 const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 const { dbg_assert, dumpn, update } = DevToolsUtils;
 const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
 const promise = require("promise");
 const Debugger = require("Debugger");
 const xpcInspector = require("xpcInspector");
 const mapURIToAddonID = require("./utils/map-uri-to-addon-id");
@@ -5221,41 +5221,16 @@ ThreadSources.prototype = {
     }
   }
 };
 
 exports.ThreadSources = ThreadSources;
 
 // Utility functions.
 
-// TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when it is
-// implemented.
-function getOffsetColumn(aOffset, aScript) {
-  let bestOffsetMapping = null;
-  for (let offsetMapping of aScript.getAllColumnOffsets()) {
-    if (!bestOffsetMapping ||
-        (offsetMapping.offset <= aOffset &&
-         offsetMapping.offset > bestOffsetMapping.offset)) {
-      bestOffsetMapping = offsetMapping;
-    }
-  }
-
-  if (!bestOffsetMapping) {
-    // XXX: Try not to completely break the experience of using the debugger for
-    // the user by assuming column 0. Simultaneously, report the error so that
-    // there is a paper trail if the assumption is bad and the debugging
-    // experience becomes wonky.
-    reportError(new Error("Could not find a column for offset " + aOffset
-                          + " in the script " + aScript));
-    return 0;
-  }
-
-  return bestOffsetMapping.columnNumber;
-}
-
 /**
  * Return the non-source-mapped location of the given Debugger.Frame. If the
  * frame does not have a script, the location's properties are all null.
  *
  * @param Debugger.Frame aFrame
  *        The frame whose location we are getting.
  * @returns Object
  *          Returns an object of the form { url, line, column }
--- a/toolkit/devtools/server/actors/tracer.js
+++ b/toolkit/devtools/server/actors/tracer.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Cu } = require("chrome");
 const { DebuggerServer } = require("devtools/server/main");
 const { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
 const Debugger = require("Debugger");
+const { getOffsetColumn } = require("devtools/server/actors/common");
 
 // TODO bug 943125: remove this polyfill and use Debugger.Frame.prototype.depth
 // once it is implemented.
 if (!Object.getOwnPropertyDescriptor(Debugger.Frame.prototype, "depth")) {
   Debugger.Frame.prototype._depth = null;
   Object.defineProperty(Debugger.Frame.prototype, "depth", {
     get: function () {
       if (this._depth === null) {
@@ -55,16 +56,17 @@ const MAX_PROPERTIES = 3;
  */
 const TRACE_TYPES = new Set([
   "time",
   "return",
   "throw",
   "yield",
   "name",
   "location",
+  "hitCount",
   "callsite",
   "parameterNames",
   "arguments",
   "depth"
 ]);
 
 /**
  * Creates a TracerActor. TracerActor provides a stream of function
@@ -76,16 +78,17 @@ function TracerActor(aConn, aParent)
   this._parent = aParent;
   this._attached = false;
   this._activeTraces = new MapStack();
   this._totalTraces = 0;
   this._startTime = 0;
   this._sequence = 0;
   this._bufferSendTimer = null;
   this._buffer = [];
+  this._hitCounts = new WeakMap();
 
   // Keep track of how many different trace requests have requested what kind of
   // tracing info. This way we can minimize the amount of data we are collecting
   // at any given time.
   this._requestsForTraceType = Object.create(null);
   for (let type of TRACE_TYPES) {
     this._requestsForTraceType[type] = 0;
   }
@@ -231,16 +234,21 @@ TracerActor.prototype = {
       name = this._activeTraces.peekKey();
       stoppedTraceTypes = this._activeTraces.pop();
     }
 
     for (let traceType of stoppedTraceTypes) {
       this._requestsForTraceType[traceType]--;
     }
 
+    // Clear hit counts if no trace is requesting them.
+    if (!this._requestsForTraceType.hitCount) {
+      this._hitCounts.clear();
+    }
+
     if (this.idle) {
       this.dbg.enabled = false;
     }
 
     return {
       type: "stoppedTrace",
       why: "requested",
       name
@@ -267,26 +275,36 @@ TracerActor.prototype = {
     };
 
     if (this._requestsForTraceType.name) {
       packet.name = aFrame.callee
         ? aFrame.callee.displayName || "(anonymous function)"
         : "(" + aFrame.type + ")";
     }
 
-    if (this._requestsForTraceType.location && aFrame.script) {
-      // We should return the location of the start of the script, but
-      // Debugger.Script does not provide complete start locations (bug
-      // 901138). Instead, return the current offset (the location of the first
-      // statement in the function).
-      packet.location = {
-        url: aFrame.script.url,
-        line: aFrame.script.startLine,
-        column: getOffsetColumn(aFrame.offset, aFrame.script)
-      };
+    if (aFrame.script) {
+      if (this._requestsForTraceType.hitCount) {
+        // Increment hit count.
+        let previousHitCount = this._hitCounts.get(aFrame.script) || 0;
+        this._hitCounts.set(aFrame.script, previousHitCount + 1);
+
+        packet.hitCount = this._hitCounts.get(aFrame.script);
+      }
+
+      if (this._requestsForTraceType.location) {
+        // We should return the location of the start of the script, but
+        // Debugger.Script does not provide complete start locations (bug
+        // 901138). Instead, return the current offset (the location of the first
+        // statement in the function).
+        packet.location = {
+          url: aFrame.script.url,
+          line: aFrame.script.startLine,
+          column: getOffsetColumn(aFrame.offset, aFrame.script)
+        };
+      }
     }
 
     if (this._parent.threadActor && aFrame.script) {
       packet.blackBoxed = this._parent.threadActor.sources.isBlackBoxed(aFrame.script.url);
     } else {
       packet.blackBoxed = false;
     }
 
@@ -492,22 +510,16 @@ MapStack.prototype = {
       let keyIndex = this._stack.lastIndexOf(aKey);
       this._stack.splice(keyIndex, 1);
       delete this._map[aKey];
     }
     return value;
   }
 };
 
-// TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when
-// it is implemented.
-function getOffsetColumn(aOffset, aScript) {
-  return 0;
-}
-
 // Serialization helper functions. Largely copied from script.js and modified
 // for use in serialization rather than object actor requests.
 
 /**
  * Create a grip for the given debuggee value.
  *
  * @param aValue Debugger.Object|primitive
  *        The value to describe with the created grip.
--- a/toolkit/devtools/server/tests/unit/test_trace_actor-05.js
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-05.js
@@ -83,19 +83,20 @@ function test_enter_exit_frame()
     .then(function() {
       let url = getFileUrl("tracerlocations.js");
 
       check_location(traces[0].location, { url: url, line: 1, column: 0 });
 
       do_check_eq(traces[1].name, "foo");
 
       // XXX: foo's definition is at tracerlocations.js:3:0, but Debugger.Script
-      // does not provide complete definition locations (bug 901138). |column|
-      // will always be 0 until we can get bug 863089 fixed.
-      check_location(traces[1].location, { url: url, line: 3, column: 0 });
+      // does not provide complete definition locations (bug 901138). Therefore,
+      // we use the first statement in the function (tracerlocations.js:4:2) for
+      // a column approximation.
+      check_location(traces[1].location, { url: url, line: 3, column: 2 });
       check_location(traces[1].callsite, { url: url, line: 8, column: 0 });
 
       do_check_eq(typeof traces[1].parameterNames, "object");
       do_check_eq(traces[1].parameterNames.length, 1);
       do_check_eq(traces[1].parameterNames[0], "x");
 
       do_check_eq(typeof traces[1].arguments, "object");
       do_check_true(Array.isArray(traces[1].arguments));
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-11.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that hit counts from tracer count function frames correctly, even after
+ * restarting the trace.
+ */
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+
+function run_test()
+{
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-tracer-actor");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTab(gClient, "test-tracer-actor", function(aResponse, aTabClient) {
+      gClient.attachTracer(aResponse.traceActor, function(aResponse, aTraceClient) {
+        gTraceClient = aTraceClient;
+        test_hit_counts();
+      });
+    });
+  });
+  do_test_pending();
+}
+
+function test_hit_counts()
+{
+  start_trace()
+    .then(eval_code)
+    .then(listen_to_traces)
+    .then(stop_trace)
+    .then(start_trace) // Restart tracing.
+    .then(eval_code)
+    .then(listen_to_traces)
+    .then(stop_trace)
+    .then(function() {
+      finishClient(gClient);
+    }).then(null, error => {
+      do_check_true(false, "Should not get an error, got: " + DevToolsUtils.safeErrorString(error));
+    });
+}
+
+function listen_to_traces() {
+  const tracesStopped = promise.defer();
+  gClient.addListener("traces", (aEvent, { traces }) => {
+    for (let t of traces) {
+      check_trace(t);
+    }
+    tracesStopped.resolve();
+  });
+  return tracesStopped.promise;
+}
+
+function start_trace()
+{
+  let deferred = promise.defer();
+  gTraceClient.startTrace(["depth", "name", "location", "hitCount"], null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function eval_code()
+{
+  gDebuggee.eval("(" + function iife() {
+    [1, 2, 3].forEach(function noop() {
+      for (let x of [1]) {}
+    });
+  } + ")()");
+}
+
+function stop_trace()
+{
+  let deferred = promise.defer();
+  gTraceClient.stopTrace(null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function check_trace({ type, sequence, depth, name, location, hitCount })
+{
+  if (location) {
+    do_check_true(location.url !== "self-hosted");
+  }
+
+  switch(sequence) {
+  case 0:
+    do_check_eq(name, "(eval)");
+    do_check_eq(hitCount, 1);
+    break;
+
+  case 1:
+    do_check_eq(name, "iife");
+    do_check_eq(hitCount, 1);
+    break;
+
+  case 2:
+    do_check_eq(hitCount, 1);
+    do_check_eq(name, "noop");
+    break;
+
+  case 4:
+    do_check_eq(hitCount, 2);
+    do_check_eq(name, "noop");
+    break;
+
+  case 6:
+    do_check_eq(hitCount, 3);
+    do_check_eq(name, "noop");
+    break;
+
+  case 3:
+  case 5:
+  case 7:
+  case 8:
+  case 9:
+    do_check_eq(type, "exitedFrame");
+    break;
+
+  default:
+    // Should have covered all sequences.
+    do_check_true(false);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-12.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that hit counts are correct even if we start tracing other things first
+ * and then start tracing hit counts.
+ */
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+
+function run_test()
+{
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-tracer-actor");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTab(gClient, "test-tracer-actor", function(aResponse, aTabClient) {
+      gClient.attachTracer(aResponse.traceActor, function(aResponse, aTraceClient) {
+        gTraceClient = aTraceClient;
+        test_hit_counts();
+      });
+    });
+  });
+  do_test_pending();
+}
+
+function test_hit_counts()
+{
+  const tracesStopped = promise.defer();
+  gClient.addListener("traces", (aEvent, { traces }) => {
+    for (let t of traces) {
+      check_trace(t);
+    }
+    tracesStopped.resolve();
+  });
+
+  start_trace_without_hit_counts()
+    .then(eval_code)
+    .then(start_trace_hit_counts)
+    .then(eval_code)
+    .then(() => tracesStopped.promise)
+    .then(stop_trace)
+    .then(function() {
+      finishClient(gClient);
+    }).then(null, error => {
+      do_check_true(false,
+        "Should not get an error, got: " + DevToolsUtils.safeErrorString(error));
+    });
+}
+
+function listen_to_traces() {
+  const tracesStopped = promise.defer();
+  gClient.addListener("traces", (aEvent, { traces }) => {
+    for (let t of traces) {
+      check_trace(t);
+    }
+    tracesStopped.resolve();
+  });
+  return tracesStopped;
+}
+
+function start_trace_without_hit_counts()
+{
+  let deferred = promise.defer();
+  gTraceClient.startTrace(["depth", "name", "location"], null,
+    function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function start_trace_hit_counts()
+{
+  let deferred = promise.defer();
+  gTraceClient.startTrace(["hitCount"], null,
+    function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function eval_code()
+{
+  gDebuggee.eval("(" + function iife() {
+    [1, 2, 3].forEach(function noop() {
+      for (let x of [1]) {}
+    });
+  } + ")()");
+}
+
+function stop_trace()
+{
+  let deferred = promise.defer();
+  gTraceClient.stopTrace(null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function check_trace({ type, sequence, depth, name, location, hitCount })
+{
+  if (location) {
+    do_check_true(location.url !== "self-hosted");
+  }
+
+  switch(sequence) {
+
+  // First evaluation (before tracing hit counts).
+  case 0:
+  case 1:
+  case 2:
+  case 4:
+  case 6:
+    do_check_eq(hitCount, undefined);
+    break;
+
+  // Second evaluation (after tracing hit counts).
+  case 10:
+    do_check_eq(name, "(eval)");
+    do_check_eq(hitCount, 1);
+    break;
+
+  case 11:
+    do_check_eq(name, "iife");
+    do_check_eq(hitCount, 1);
+    break;
+
+  case 12:
+    do_check_eq(hitCount, 1);
+    do_check_eq(name, "noop");
+    break;
+
+  case 14:
+    do_check_eq(hitCount, 2);
+    do_check_eq(name, "noop");
+    break;
+
+  case 16:
+    do_check_eq(hitCount, 3);
+    do_check_eq(name, "noop");
+    break;
+
+  case 3:
+  case 5:
+  case 7:
+  case 8:
+  case 9:
+  case 13:
+  case 15:
+  case 17:
+  case 18:
+  case 19:
+    do_check_eq(type, "exitedFrame");
+    break;
+
+  default:
+    // Should have covered all sequences.
+    do_check_true(false);
+  }
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -192,16 +192,18 @@ reason = bug 820380
 [test_trace_actor-03.js]
 [test_trace_actor-04.js]
 [test_trace_actor-05.js]
 [test_trace_actor-06.js]
 [test_trace_actor-07.js]
 [test_trace_actor-08.js]
 [test_trace_actor-09.js]
 [test_trace_actor-10.js]
+[test_trace_actor-11.js]
+[test_trace_actor-12.js]
 [test_ignore_caught_exceptions.js]
 [test_requestTypes.js]
 reason = bug 937197
 [test_layout-reflows-observer.js]
 [test_protocolSpec.js]
 [test_registerClient.js]
 [test_client_request.js]
 [test_monitor_actor.js]
--- a/toolkit/mozapps/extensions/test/browser/browser_plugin_enabled_state_locked.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_plugin_enabled_state_locked.js
@@ -52,20 +52,24 @@ function getTestPlugin(aPlugins) {
 
   return pluginElement;
 }
 
 function checkStateMenu(locked) {
   Assert.equal(Services.prefs.prefIsLocked(getTestPluginPref()), locked,
     "Preference lock state should be correct.");
   let menuList = gManagerWindow.document.getAnonymousElementByAttribute(gPluginElement, "anonid", "state-menulist");
+  //  State menu should always have a selected item which must be visible
+  let selectedMenuItem = menuList.querySelector(".addon-control[selected=\"true\"]");
 
   is_element_visible(menuList, "State menu should be visible.");
   Assert.equal(menuList.disabled, locked,
     "State menu should" + (locked === true ? "" : " not") + " be disabled.");
+
+  is_element_visible(selectedMenuItem, "State menu's selected item should be visible.");
 }
 
 function checkStateMenuDetail(locked) {
   Assert.equal(Services.prefs.prefIsLocked(getTestPluginPref()), locked,
     "Preference should be " + (locked === true ? "" : "un") + "locked.");
 
   // open details menu
   let details = gManagerWindow.document.getAnonymousElementByAttribute(gPluginElement, "anonid", "details-btn");
--- a/toolkit/themes/linux/mozapps/extensions/extensions.css
+++ b/toolkit/themes/linux/mozapps/extensions/extensions.css
@@ -875,22 +875,22 @@ setting[type="radio"] > radiogroup {
 
 #update-selected {
   margin: 12px;
 }
 
 
 /*** buttons ***/
 
-.addon-control[disabled="true"] {
+.addon-control[disabled="true"]:not(.no-auto-hide) {
   display: none;
 }
 
-.addon-control.no-auto-hide {
-  display: block;
+.no-auto-hide .addon-control {
+  display: block !important;
 }
 
 .addon-control.enable {
   list-style-image: url("moz-icon://stock/gtk-yes?size=button");
 }
 
 .addon-control.disable {
   list-style-image: url("moz-icon://stock/gtk-no?size=button");
--- a/toolkit/themes/osx/mozapps/extensions/extensions.css
+++ b/toolkit/themes/osx/mozapps/extensions/extensions.css
@@ -1099,22 +1099,26 @@ setting[type="radio"] > radiogroup {
 
 #update-selected {
   margin: 12px;
 }
 
 
 /*** buttons ***/
 
-.addon-control[disabled="true"] {
+.addon-control[disabled="true"]:not(.no-auto-hide) {
   display: none;
 }
 
-.addon-control.no-auto-hide {
-  display: block;
+.no-auto-hide .addon-control {
+  display: block !important;
+}
+
+.no-auto-hide > .menulist-dropmarker {
+  -moz-padding-start: 0px !important;
 }
 
 button.button-link {
   -moz-appearance: none;
   background: transparent;
   border: none;
   box-shadow: none;
   text-decoration: underline;
--- a/toolkit/themes/windows/mozapps/extensions/extensions.css
+++ b/toolkit/themes/windows/mozapps/extensions/extensions.css
@@ -1115,22 +1115,22 @@ menulist { /* Fixes some styling inconsi
 
 #update-selected {
   margin: 12px;
 }
 
 
 /*** buttons ***/
 
-.addon-control[disabled="true"] {
+.addon-control[disabled="true"]:not(.no-auto-hide) {
   display: none;
 }
 
-.addon-control.no-auto-hide {
-  display: block;
+.no-auto-hide .addon-control {
+  display: block !important;
 }
 
 button.button-link {
   -moz-appearance: none;
   background: transparent;
   border: none;
   box-shadow: none;
   text-decoration: underline;