merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 21 Jul 2016 16:23:11 +0200
changeset 345922 e28e856b987380f55d699092f11f6997378f79a6
parent 345888 29ead859749af91a4e70d10a278a0ca3fca9d2b4 (current diff)
parent 345921 cab3629ad5fd8f7d6c960bdf966b14cfb06e7eb3 (diff)
child 346117 6b180266ac16e3226be33319ff710ddfa85f5836
push id6389
push userraliiev@mozilla.com
push dateMon, 19 Sep 2016 13:38:22 +0000
treeherdermozilla-beta@01d67bfe6c81 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone50.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
browser/themes/linux/customizableui/panelUIOverlay.css
browser/themes/osx/customizableui/panelUIOverlay.css
browser/themes/shared/customizableui/panelUIOverlay.inc.css
browser/themes/windows/customizableui/panelUIOverlay.css
devtools/client/framework/source-location.js
devtools/client/framework/test/browser_source-location-01.js
devtools/client/framework/test/browser_source-location-02.js
devtools/server/actors/utils/ScriptStore.js
devtools/server/tests/unit/test_ScriptStore.js
toolkit/components/telemetry/Histograms.json
--- a/b2g/chrome/content/devtools/hud.js
+++ b/b2g/chrome/content/devtools/hud.js
@@ -20,17 +20,17 @@ XPCOMUtils.defineLazyGetter(this, 'Debug
   return devtools.require('devtools/shared/client/main').DebuggerClient;
 });
 
 XPCOMUtils.defineLazyGetter(this, 'WebConsoleUtils', function() {
   return devtools.require('devtools/shared/webconsole/utils').Utils;
 });
 
 XPCOMUtils.defineLazyGetter(this, 'EventLoopLagFront', function() {
-  return devtools.require('devtools/server/actors/eventlooplag').EventLoopLagFront;
+  return devtools.require('devtools/shared/fronts/eventlooplag').EventLoopLagFront;
 });
 
 XPCOMUtils.defineLazyGetter(this, 'PerformanceEntriesFront', function() {
   return devtools.require('devtools/server/actors/performance-entries').PerformanceEntriesFront;
 });
 
 XPCOMUtils.defineLazyGetter(this, 'MemoryFront', function() {
   return devtools.require('devtools/server/actors/memory').MemoryFront;
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -6,17 +6,17 @@
 # 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/.
 
 <?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/places/places.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/usercontext/usercontext.css" type="text/css"?>
 <?xml-stylesheet href="chrome://devtools/skin/devtools-browser.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/controlcenter/panel.css" type="text/css"?>
-<?xml-stylesheet href="chrome://browser/skin/customizableui/panelUIOverlay.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/customizableui/panelUI.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/browser-lightweightTheme.css" type="text/css"?>
 
 <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
 <?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?>
 <?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
 
 # All DTD information is stored in a separate file so that it can be shared by
--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -94,27 +94,25 @@ var _observer;
 
 function getObserver() {
   if (!_observer) {
     _observer = {
       onDeleteURI: function(uri, guid, reason) {
         this.emit("visitRemoved", {allHistory: false, urls: [uri.spec]});
       },
       onVisit: function(uri, visitId, time, sessionId, referringId, transitionType, guid, hidden, visitCount, typed) {
-        PlacesUtils.promisePlaceInfo(guid).then(placeInfo => {
-          let data = {
-            id: guid,
-            url: uri.spec,
-            title: placeInfo.title,
-            lastVisitTime: time / 1000,  // time from Places is microseconds,
-            visitCount,
-            typedCount: typed,
-          };
-          this.emit("visited", data);
-        });
+        let data = {
+          id: guid,
+          url: uri.spec,
+          title: "",
+          lastVisitTime: time / 1000,  // time from Places is microseconds,
+          visitCount,
+          typedCount: typed,
+        };
+        this.emit("visited", data);
       },
       onBeginUpdateBatch: function() {},
       onEndUpdateBatch: function() {},
       onTitleChanged: function() {},
       onClearHistory: function() {
         this.emit("visitRemoved", {allHistory: true});
       },
       onPageChanged: function() {},
--- a/browser/components/extensions/test/browser/browser_ext_history.js
+++ b/browser/components/extensions/test/browser/browser_ext_history.js
@@ -454,17 +454,19 @@ add_task(function* test_on_visited() {
   yield PlacesUtils.history.insertMany(PAGE_INFOS);
 
   let onVisitedData = yield extension.awaitMessage("on-visited-data");
 
   function checkOnVisitedData(index, expected) {
     let onVisited = onVisitedData[index];
     ok(PlacesUtils.isValidGuid(onVisited.id), "onVisited received a valid id");
     is(onVisited.url, expected.url, "onVisited received the expected url");
-    is(onVisited.title, expected.title, "onVisited received the expected title");
+    // Title will be blank until bug 1287928 lands
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=1287928
+    is(onVisited.title, "", "onVisited received a blank title");
     is(onVisited.lastVisitTime, expected.time, "onVisited received the expected time");
     is(onVisited.visitCount, expected.visitCount, "onVisited received the expected visitCount");
   }
 
   let expected = {
     url: PAGE_INFOS[0].url,
     title: PAGE_INFOS[0].title,
     time: PAGE_INFOS[0].visits[0].date.getTime(),
rename from browser/themes/linux/customizableui/panelUIOverlay.css
rename to browser/themes/linux/customizableui/panelUI.css
--- a/browser/themes/linux/customizableui/panelUIOverlay.css
+++ b/browser/themes/linux/customizableui/panelUI.css
@@ -1,13 +1,13 @@
 /* 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/. */
 
-%include ../../shared/customizableui/panelUIOverlay.inc.css
+%include ../../shared/customizableui/panelUI.inc.css
 
 .panel-subviews {
   background-color: -moz-dialog;
 }
 
 #BMB_bookmarksPopup > menuitem[type="checkbox"] {
   -moz-appearance: none !important; /* important, to override toolkit rule */
 }
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -46,17 +46,17 @@ browser.jar:
   skin/classic/browser/Toolbar-inverted@2x.png
   skin/classic/browser/Toolbar-small.png
   skin/classic/browser/webRTC-indicator.css
 * skin/classic/browser/controlcenter/panel.css        (controlcenter/panel.css)
   skin/classic/browser/customizableui/background-noise-toolbar.png  (customizableui/background-noise-toolbar.png)
   skin/classic/browser/customizableui/customizeMode-gridTexture.png  (customizableui/customizeMode-gridTexture.png)
   skin/classic/browser/customizableui/customizeMode-separatorHorizontal.png  (customizableui/customizeMode-separatorHorizontal.png)
   skin/classic/browser/customizableui/customizeMode-separatorVertical.png  (customizableui/customizeMode-separatorVertical.png)
-* skin/classic/browser/customizableui/panelUIOverlay.css (customizableui/panelUIOverlay.css)
+* skin/classic/browser/customizableui/panelUI.css (customizableui/panelUI.css)
 * skin/classic/browser/downloads/allDownloadsViewOverlay.css   (downloads/allDownloadsViewOverlay.css)
   skin/classic/browser/downloads/buttons.png          (downloads/buttons.png)
   skin/classic/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png)
   skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png)
   skin/classic/browser/downloads/download-notification-start.png (downloads/download-notification-start.png)
   skin/classic/browser/downloads/download-summary.png (downloads/download-summary.png)
 * skin/classic/browser/downloads/downloads.css        (downloads/downloads.css)
   skin/classic/browser/feeds/feedIcon.png             (feeds/feedIcon.png)
rename from browser/themes/osx/customizableui/panelUIOverlay.css
rename to browser/themes/osx/customizableui/panelUI.css
--- a/browser/themes/osx/customizableui/panelUIOverlay.css
+++ b/browser/themes/osx/customizableui/panelUI.css
@@ -1,13 +1,13 @@
 /* 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/. */
 
-%include ../../shared/customizableui/panelUIOverlay.inc.css
+%include ../../shared/customizableui/panelUI.inc.css
 
 .panel-subviews {
   background-color: hsla(0,0%,100%,.97);
 }
 
 .panelUI-grid .toolbarbutton-1 {
   margin-right: 0;
   margin-left: 0;
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -67,17 +67,17 @@ browser.jar:
   skin/classic/browser/webRTC-indicator.css
 * skin/classic/browser/controlcenter/panel.css        (controlcenter/panel.css)
   skin/classic/browser/customizableui/background-noise-toolbar.png  (customizableui/background-noise-toolbar.png)
   skin/classic/browser/customizableui/customize-titleBar-toggle.png  (customizableui/customize-titleBar-toggle.png)
   skin/classic/browser/customizableui/customize-titleBar-toggle@2x.png  (customizableui/customize-titleBar-toggle@2x.png)
   skin/classic/browser/customizableui/customizeMode-gridTexture.png  (customizableui/customizeMode-gridTexture.png)
   skin/classic/browser/customizableui/customizeMode-separatorHorizontal.png  (customizableui/customizeMode-separatorHorizontal.png)
   skin/classic/browser/customizableui/customizeMode-separatorVertical.png  (customizableui/customizeMode-separatorVertical.png)
-* skin/classic/browser/customizableui/panelUIOverlay.css    (customizableui/panelUIOverlay.css)
+* skin/classic/browser/customizableui/panelUI.css    (customizableui/panelUI.css)
 * skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css)
   skin/classic/browser/downloads/buttons.png                (downloads/buttons.png)
   skin/classic/browser/downloads/buttons@2x.png             (downloads/buttons@2x.png)
   skin/classic/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png)
   skin/classic/browser/downloads/download-glow-menuPanel@2x.png (downloads/download-glow-menuPanel@2x.png)
   skin/classic/browser/downloads/download-notification-finish.png  (downloads/download-notification-finish.png)
   skin/classic/browser/downloads/download-notification-finish@2x.png  (downloads/download-notification-finish@2x.png)
   skin/classic/browser/downloads/download-notification-start.png  (downloads/download-notification-start.png)
rename from browser/themes/shared/customizableui/panelUIOverlay.inc.css
rename to browser/themes/shared/customizableui/panelUI.inc.css
rename from browser/themes/windows/customizableui/panelUIOverlay.css
rename to browser/themes/windows/customizableui/panelUI.css
--- a/browser/themes/windows/customizableui/panelUIOverlay.css
+++ b/browser/themes/windows/customizableui/panelUI.css
@@ -1,13 +1,13 @@
 /* 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/. */
 
-%include ../../shared/customizableui/panelUIOverlay.inc.css
+%include ../../shared/customizableui/panelUI.inc.css
 
 .panel-subviews {
   background-color: -moz-field;
 }
 
 #PanelUI-contents #zoom-out-btn {
   padding-left: 12px;
   padding-right: 12px;
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -81,17 +81,17 @@ browser.jar:
 * skin/classic/browser/controlcenter/panel.css                 (controlcenter/panel.css)
   skin/classic/browser/customizableui/background-noise-toolbar.png  (customizableui/background-noise-toolbar.png)
   skin/classic/browser/customizableui/customize-titleBar-toggle.png  (customizableui/customize-titleBar-toggle.png)
   skin/classic/browser/customizableui/customize-titleBar-toggle@2x.png  (customizableui/customize-titleBar-toggle@2x.png)
   skin/classic/browser/customizableui/customizeMode-gridTexture.png  (customizableui/customizeMode-gridTexture.png)
   skin/classic/browser/customizableui/customizeMode-separatorHorizontal.png  (customizableui/customizeMode-separatorHorizontal.png)
   skin/classic/browser/customizableui/customizeMode-separatorVertical.png  (customizableui/customizeMode-separatorVertical.png)
   skin/classic/browser/customizableui/menu-arrow.svg           (customizableui/menu-arrow.svg)
-* skin/classic/browser/customizableui/panelUIOverlay.css       (customizableui/panelUIOverlay.css)
+* skin/classic/browser/customizableui/panelUI.css       (customizableui/panelUI.css)
 * skin/classic/browser/downloads/allDownloadsViewOverlay.css   (downloads/allDownloadsViewOverlay.css)
   skin/classic/browser/downloads/buttons.png                   (downloads/buttons.png)
   skin/classic/browser/downloads/buttons-XP.png                (downloads/buttons-XP.png)
   skin/classic/browser/downloads/download-glow-menuPanel.png   (downloads/download-glow-menuPanel.png)
   skin/classic/browser/downloads/download-glow-menuPanel-XPVista7.png   (downloads/download-glow-menuPanel-XPVista7.png)
   skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png)
   skin/classic/browser/downloads/download-notification-start.png (downloads/download-notification-start.png)
   skin/classic/browser/downloads/download-summary.png          (downloads/download-summary.png)
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/location-store.js
@@ -0,0 +1,103 @@
+/* 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 SOURCE_TOKEN = "<:>";
+
+function LocationStore (store) {
+  this._store = store || new Map();
+}
+
+/**
+ * Method to get a promised location from the Store.
+ * @param location
+ * @returns Promise<Object>
+ */
+LocationStore.prototype.get = function (location) {
+  this._safeAccessInit(location.url);
+  return this._store.get(location.url).get(location);
+};
+
+/**
+ * Method to set a promised location to the Store
+ * @param location
+ * @param promisedLocation
+ */
+LocationStore.prototype.set = function (location, promisedLocation = null) {
+  this._safeAccessInit(location.url);
+  this._store.get(location.url).set(serialize(location), promisedLocation);
+};
+
+/**
+ * Utility method to verify if key exists in Store before accessing it.
+ * If not, initializing it.
+ * @param url
+ * @private
+ */
+LocationStore.prototype._safeAccessInit = function (url) {
+  if (!this._store.has(url)) {
+    this._store.set(url, new Map());
+  }
+};
+
+/**
+ * Utility proxy method to Map.clear() method
+ */
+LocationStore.prototype.clear = function () {
+  this._store.clear();
+};
+
+/**
+ * Retrieves an object containing all locations to be resolved when `source-updated`
+ * event is triggered.
+ * @param url
+ * @returns {Array<String>}
+ */
+LocationStore.prototype.getByURL = function (url){
+  if (this._store.has(url)) {
+    return [...this._store.get(url).keys()];
+  }
+  return [];
+};
+
+/**
+ * Invalidates the stale location promises from the store when `source-updated`
+ * event is triggered, and when FrameView unsubscribes from a location.
+ * @param url
+ */
+LocationStore.prototype.clearByURL = function (url) {
+  this._safeAccessInit(url);
+  this._store.set(url, new Map());
+};
+
+exports.LocationStore = LocationStore;
+exports.serialize = serialize;
+exports.deserialize = deserialize;
+
+/**
+ * Utility method to serialize the source
+ * @param source
+ * @returns {string}
+ */
+function serialize(source) {
+  let { url, line, column } = source;
+  line = line || 0;
+  column = column || 0;
+  return `${url}${SOURCE_TOKEN}${line}${SOURCE_TOKEN}${column}`;
+};
+
+/**
+ * Utility method to serialize the source
+ * @param source
+ * @returns Object
+ */
+function deserialize(source) {
+  let [ url, line, column ] = source.split(SOURCE_TOKEN);
+  line = parseInt(line);
+  column = parseInt(column);
+  if (column === 0) {
+    return { url, line };
+  }
+  return { url, line, column };
+};
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -11,21 +11,22 @@ TEST_HARNESS_FILES.xpcshell.devtools.cli
 
 DevToolsModules(
     'about-devtools-toolbox.js',
     'attach-thread.js',
     'browser-menus.js',
     'devtools-browser.js',
     'devtools.js',
     'gDevTools.jsm',
+    'location-store.js',
     'menu-item.js',
     'menu.js',
     'selection.js',
     'sidebar.js',
-    'source-location.js',
+    'source-map-service.js',
     'target-from-url.js',
     'target.js',
     'toolbox-highlighter-utils.js',
     'toolbox-hosts.js',
     'toolbox-options.js',
     'toolbox.js',
     'ToolboxProcess.jsm',
 )
rename from devtools/client/framework/source-location.js
rename to devtools/client/framework/source-map-service.js
--- a/devtools/client/framework/source-location.js
+++ b/devtools/client/framework/source-map-service.js
@@ -1,98 +1,171 @@
 /* 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 { Task } = require("devtools/shared/task");
-const { assert } = require("devtools/shared/DevToolsUtils");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { LocationStore, serialize, deserialize } = require("./location-store");
 
 /**
  * A manager class that wraps a TabTarget and listens to source changes
  * from source maps and resolves non-source mapped locations to the source mapped
  * versions and back and forth, and creating smart elements with a location that
  * auto-update when the source changes (from pretty printing, source maps loading, etc)
  *
  * @param {TabTarget} target
  */
-function SourceLocationController(target) {
-  this.target = target;
-  this.locations = new Set();
+
+function SourceMapService(target) {
+  this._target = target;
+  this._locationStore = new LocationStore();
+  this._isInitialResolve = true;
+
+  EventEmitter.decorate(this);
 
   this._onSourceUpdated = this._onSourceUpdated.bind(this);
+  this._resolveLocation = this._resolveLocation.bind(this);
+  this._resolveAndUpdate = this._resolveAndUpdate.bind(this);
+  this.subscribe = this.subscribe.bind(this);
+  this.unsubscribe = this.unsubscribe.bind(this);
   this.reset = this.reset.bind(this);
   this.destroy = this.destroy.bind(this);
 
   target.on("source-updated", this._onSourceUpdated);
   target.on("navigate", this.reset);
   target.on("will-navigate", this.reset);
   target.on("close", this.destroy);
 }
 
-SourceLocationController.prototype.reset = function () {
-  this.locations.clear();
+/**
+ * Clears the store containing the cached resolved locations and promises
+ */
+SourceMapService.prototype.reset = function () {
+  this._isInitialResolve = true;
+  this._locationStore.clear();
+};
+
+SourceMapService.prototype.destroy = function () {
+  this.reset();
+  this._target.off("source-updated", this._onSourceUpdated);
+  this._target.off("navigate", this.reset);
+  this._target.off("will-navigate", this.reset);
+  this._target.off("close", this.destroy);
+  this._isInitialResolve = null;
+  this._target = this._locationStore = null;
 };
 
-SourceLocationController.prototype.destroy = function () {
-  this.locations.clear();
-  this.target.off("source-updated", this._onSourceUpdated);
-  this.target.off("navigate", this.reset);
-  this.target.off("will-navigate", this.reset);
-  this.target.off("close", this.destroy);
-  this.target = this.locations = null;
+/**
+ * Sets up listener for the callback to update the FrameView and tries to resolve location
+ * @param location
+ * @param callback
+ */
+SourceMapService.prototype.subscribe = function (location, callback) {
+  this.on(serialize(location), callback);
+  this._locationStore.set(location);
+  if (this._isInitialResolve) {
+    this._resolveAndUpdate(location);
+    this._isInitialResolve = false;
+  }
+};
+
+/**
+ * Removes the listener for the location and clears cached locations
+ * @param location
+ * @param callback
+ */
+SourceMapService.prototype.unsubscribe = function (location, callback) {
+  this.off(serialize(location), callback);
+  this._locationStore.clearByURL(location.url);
 };
 
 /**
- * Add this `location` to be observed and register a callback
- * whenever the underlying source is updated.
- *
- * @param {Object} location
- *        An object with a {String} url, {Number} line, and optionally
- *        a {Number} column.
- * @param {Function} callback
+ * Tries to resolve the location and if successful,
+ * emits the resolved location and caches it
+ * @param location
+ * @private
  */
-SourceLocationController.prototype.bindLocation = function (location, callback) {
-  assert(location.url, "Location must have a url.");
-  assert(location.line, "Location must have a line.");
-  this.locations.add({ location, callback });
+SourceMapService.prototype._resolveAndUpdate = function (location) {
+  this._resolveLocation(location).then(resolvedLocation => {
+    // We try to source map the first console log to initiate the source-updated event from
+    // target. The isSameLocation check is to make sure we don't update the frame, if the
+    // location is not source-mapped.
+    if (resolvedLocation) {
+      if (this._isInitialResolve) {
+        if (!isSameLocation(location, resolvedLocation)) {
+          this.emit(serialize(location), location, resolvedLocation);
+          return;
+        }
+      }
+      this.emit(serialize(location), location, resolvedLocation);
+    }
+  });
 };
 
 /**
- * Called when a new source occurs (a normal source, source maps) or an updated
- * source (pretty print) occurs.
- *
- * @param {String} eventName
- * @param {Object} sourceEvent
+ * Validates the location model,
+ * checks if there is existing promise to resolve location, if so returns cached promise
+ * if not promised to resolve,
+ * tries to resolve location and returns a promised location
+ * @param location
+ * @return Promise<Object>
+ * @private
  */
-SourceLocationController.prototype._onSourceUpdated = function (_, sourceEvent) {
+SourceMapService.prototype._resolveLocation = Task.async(function* (location) {
+  // Location must have a url and a line
+  if (!location.url || !location.line) {
+    return null;
+  }
+  const cachedLocation = this._locationStore.get(location);
+  if (cachedLocation) {
+    return cachedLocation;
+  } else {
+    const promisedLocation = resolveLocation(this._target, location);
+    if (promisedLocation) {
+      this._locationStore.set(location, promisedLocation);
+      return promisedLocation;
+    }
+  }
+});
+
+/**
+ * Checks if the `source-updated` event is fired from the target.
+ * Checks to see if location store has the source url in its cache,
+ * if so, tries to update each stale location in the store.
+ * @param _
+ * @param sourceEvent
+ * @private
+ */
+SourceMapService.prototype._onSourceUpdated = function (_, sourceEvent) {
   let { type, source } = sourceEvent;
   // If we get a new source, and it's not a source map, abort;
-  // we can ahve no actionable updates as this is just a new normal source.
+  // we can have no actionable updates as this is just a new normal source.
   // Also abort if there's no `url`, which means it's unsourcemappable anyway,
   // like an eval script.
   if (!source.url || type === "newSource" && !source.isSourceMapped) {
     return;
   }
-
-  for (let locationItem of this.locations) {
-    if (isSourceRelated(locationItem.location, source)) {
-      this._updateSource(locationItem);
+  let sourceUrl = null;
+  if (source.generatedUrl && source.isSourceMapped) {
+    sourceUrl = source.generatedUrl;
+  } else if (source.url && source.isPrettyPrinted) {
+    sourceUrl = source.url;
+  }
+  const locationsToResolve = this._locationStore.getByURL(sourceUrl);
+  if (locationsToResolve.length) {
+    this._locationStore.clearByURL(sourceUrl);
+    for (let location of locationsToResolve) {
+      this._resolveAndUpdate(deserialize(location));
     }
   }
 };
 
-SourceLocationController.prototype._updateSource = Task.async(function* (locationItem) {
-  let newLocation = yield resolveLocation(this.target, locationItem.location);
-  if (newLocation) {
-    let previousLocation = Object.assign({}, locationItem.location);
-    Object.assign(locationItem.location, newLocation);
-    locationItem.callback(previousLocation, newLocation);
-  }
-});
+exports.SourceMapService = SourceMapService;
 
 /**
  * Take a TabTarget and a location, containing a `url`, `line`, and `column`, resolve
  * the location to the latest location (so a source mapped location, or if pretty print
  * status has been updated)
  *
  * @param {TabTarget} target
  * @param {Object} location
@@ -100,38 +173,28 @@ SourceLocationController.prototype._upda
  */
 function resolveLocation(target, location) {
   return Task.spawn(function* () {
     let newLocation = yield target.resolveLocation({
       url: location.url,
       line: location.line,
       column: location.column || Infinity
     });
-
     // Source or mapping not found, so don't do anything
     if (newLocation.error) {
       return null;
     }
 
     return newLocation;
   });
 }
 
 /**
- * Takes a serialized SourceActor form and returns a boolean indicating
- * if this source is related to this location, like if a location is a generated source,
- * and the source map is loaded subsequently, the new source mapped SourceActor
- * will be considered related to this location. Same with pretty printing new sources.
- *
- * @param {Object} location
- * @param {Object} source
- * @return {Boolean}
+ * Returns if the original location and resolved location are the same
+ * @param location
+ * @param resolvedLocation
+ * @returns {boolean}
  */
-function isSourceRelated(location, source) {
-         // Mapping location to subsequently loaded source map
-  return source.generatedUrl === location.url ||
-         // Mapping source map loc to source map
-         source.url === location.url;
-}
-
-exports.SourceLocationController = SourceLocationController;
-exports.resolveLocation = resolveLocation;
-exports.isSourceRelated = isSourceRelated;
+function isSameLocation(location, resolvedLocation) {
+  return location.url === resolvedLocation.url &&
+    location.line === resolvedLocation.line &&
+    location.column === resolvedLocation.column;
+};
\ No newline at end of file
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -3,18 +3,22 @@ tags = devtools
 subsuite = devtools
 support-files =
   browser_toolbox_options_disable_js.html
   browser_toolbox_options_disable_js_iframe.html
   browser_toolbox_options_disable_cache.sjs
   browser_toolbox_sidebar_tool.xul
   browser_toolbox_window_title_changes_page.html
   browser_toolbox_window_title_frame_select_page.html
+  code_binary_search.coffee
+  code_binary_search.js
+  code_binary_search.map
   code_math.js
   code_ugly.js
+  doc_empty-tab-01.html
   head.js
   shared-head.js
   shared-redux-head.js
   helper_disable_cache.js
   doc_theme.css
   doc_viewsource.html
   browser_toolbox_options_enable_serviceworkers_testing_frame_script.js
   browser_toolbox_options_enable_serviceworkers_testing.html
@@ -26,18 +30,18 @@ support-files =
 [browser_devtools_api_destroy.js]
 [browser_dynamic_tool_enabling.js]
 [browser_ignore_toolbox_network_requests.js]
 [browser_keybindings_01.js]
 [browser_keybindings_02.js]
 [browser_keybindings_03.js]
 [browser_menu_api.js]
 [browser_new_activation_workflow.js]
-[browser_source-location-01.js]
-[browser_source-location-02.js]
+[browser_source_map-01.js]
+[browser_source_map-02.js]
 [browser_target_from_url.js]
 [browser_target_events.js]
 [browser_target_remote.js]
 [browser_target_support.js]
 [browser_toolbox_custom_host.js]
 [browser_toolbox_dynamic_registration.js]
 [browser_toolbox_getpanelwhenready.js]
 [browser_toolbox_highlight.js]
rename from devtools/client/framework/test/browser_source-location-01.js
rename to devtools/client/framework/test/browser_source_map-01.js
--- a/devtools/client/framework/test/browser_source-location-01.js
+++ b/devtools/client/framework/test/browser_source_map-01.js
@@ -1,66 +1,65 @@
 /* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
+ http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Whitelisting this test.
 // As part of bug 1077403, the leaking uncaught rejections should be fixed.
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("[object Object]");
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed(
   "TypeError: this.transport is null");
 
 /**
- * Tests the SourceMapController updates generated sources when source maps
+ * Tests the SourceMapService updates generated sources when source maps
  * are subsequently found. Also checks when no column is provided, and
  * when tagging an already source mapped location initially.
  */
 
 const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
 // Empty page
 const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
-const JS_URL = `${DEBUGGER_ROOT}code_binary_search.js`;
-const COFFEE_URL = `${DEBUGGER_ROOT}code_binary_search.coffee`;
-const { SourceLocationController } = require("devtools/client/framework/source-location");
+const JS_URL = `${URL_ROOT}code_binary_search.js`;
+const COFFEE_URL = `${URL_ROOT}code_binary_search.coffee`;
+const { SourceMapService } = require("devtools/client/framework/source-map-service");
 
 add_task(function* () {
-  let toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
+  const toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
 
-  let controller = new SourceLocationController(toolbox.target);
+  const service = new SourceMapService(toolbox.target);
 
-  let aggregator = [];
+  const aggregator = [];
 
-  function onUpdate(oldLoc, newLoc) {
+  function onUpdate(e, oldLoc, newLoc) {
     if (oldLoc.line === 6) {
       checkLoc1(oldLoc, newLoc);
     } else if (oldLoc.line === 8) {
       checkLoc2(oldLoc, newLoc);
     } else if (oldLoc.line === 2) {
       checkLoc3(oldLoc, newLoc);
     } else {
       throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
     }
     aggregator.push(newLoc);
   }
 
   let loc1 = { url: JS_URL, line: 6 };
   let loc2 = { url: JS_URL, line: 8, column: 3 };
-  let loc3 = { url: COFFEE_URL, line: 2, column: 0 };
 
-  controller.bindLocation(loc1, onUpdate);
-  controller.bindLocation(loc2, onUpdate);
-  controller.bindLocation(loc3, onUpdate);
+  service.subscribe(loc1, onUpdate);
+  service.subscribe(loc2, onUpdate);
 
   // Inject JS script
+  let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_binary_search");
   yield createScript(JS_URL);
+  yield sourceShown;
 
-  yield waitUntil(() => aggregator.length === 3);
+  yield waitUntil(() => aggregator.length === 2);
 
   ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 4), "found first updated location");
   ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 6), "found second updated location");
-  ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 2), "found third updated location");
 
   yield toolbox.destroy();
   gBrowser.removeCurrentTab();
   finish();
 });
 
 function checkLoc1(oldLoc, newLoc) {
   is(oldLoc.line, 6, "Correct line for JS:6");
@@ -75,28 +74,37 @@ function checkLoc2(oldLoc, newLoc) {
   is(oldLoc.line, 8, "Correct line for JS:8:3");
   is(oldLoc.column, 3, "Correct column for JS:8:3");
   is(oldLoc.url, JS_URL, "Correct url for JS:8:3");
   is(newLoc.line, 6, "Correct line for JS:8:3 -> COFFEE");
   is(newLoc.column, 10, "Correct column for JS:8:3 -> COFFEE");
   is(newLoc.url, COFFEE_URL, "Correct url for JS:8:3 -> COFFEE");
 }
 
-function checkLoc3(oldLoc, newLoc) {
-  is(oldLoc.line, 2, "Correct line for COFFEE:2:0");
-  is(oldLoc.column, 0, "Correct column for COFFEE:2:0");
-  is(oldLoc.url, COFFEE_URL, "Correct url for COFFEE:2:0");
-  is(newLoc.line, 2, "Correct line for COFFEE:2:0 -> COFFEE");
-  is(newLoc.column, 0, "Correct column for COFFEE:2:0 -> COFFEE");
-  is(newLoc.url, COFFEE_URL, "Correct url for COFFEE:2:0 -> COFFEE");
-}
-
 function createScript(url) {
   info(`Creating script: ${url}`);
   let mm = getFrameScript();
   let command = `
     let script = document.createElement("script");
     script.setAttribute("src", "${url}");
     document.body.appendChild(script);
     null;
   `;
   return evalInDebuggee(mm, command);
 }
+
+function waitForSourceShown(debuggerPanel, url) {
+  let { panelWin } = debuggerPanel;
+  let deferred = defer();
+
+  info(`Waiting for source ${url} to be shown in the debugger...`);
+  panelWin.on(panelWin.EVENTS.SOURCE_SHOWN, function onSourceShown(_, source) {
+
+    let sourceUrl = source.url || source.generatedUrl;
+    if (sourceUrl.includes(url)) {
+      panelWin.off(panelWin.EVENTS.SOURCE_SHOWN, onSourceShown);
+      info(`Source shown for ${url}`);
+      deferred.resolve(source);
+    }
+  });
+
+  return deferred.promise;
+}
rename from devtools/client/framework/test/browser_source-location-02.js
rename to devtools/client/framework/test/browser_source_map-02.js
--- a/devtools/client/framework/test/browser_source-location-02.js
+++ b/devtools/client/framework/test/browser_source_map-02.js
@@ -1,63 +1,63 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
+ http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Tests the SourceLocationController updates generated sources when pretty printing
+ * Tests the SourceMapService updates generated sources when pretty printing
  * and un pretty printing.
  */
 
 const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
 // Empty page
 const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
 const JS_URL = `${URL_ROOT}code_ugly.js`;
-const { SourceLocationController } = require("devtools/client/framework/source-location");
+const { SourceMapService } = require("devtools/client/framework/source-map-service");
 
 add_task(function* () {
   let toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
 
-  let controller = new SourceLocationController(toolbox.target);
+  let service = new SourceMapService(toolbox.target);
 
   let checkedPretty = false;
   let checkedUnpretty = false;
 
-  function onUpdate(oldLoc, newLoc) {
+  function onUpdate(e, oldLoc, newLoc) {
     if (oldLoc.line === 3) {
       checkPrettified(oldLoc, newLoc);
       checkedPretty = true;
     } else if (oldLoc.line === 9) {
       checkUnprettified(oldLoc, newLoc);
       checkedUnpretty = true;
     } else {
       throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
     }
   }
-
-  controller.bindLocation({ url: JS_URL, line: 3 }, onUpdate);
+  const loc1 = { url: JS_URL, line: 3 };
+  service.subscribe(loc1, onUpdate);
 
   // Inject JS script
   let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
   yield createScript(JS_URL);
   yield sourceShown;
 
   let ppButton = toolbox.getCurrentPanel().panelWin.document.getElementById("pretty-print");
   sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
   ppButton.click();
   yield sourceShown;
   yield waitUntil(() => checkedPretty);
 
   // TODO check unprettified change once bug 1177446 fixed
-  /*
-  sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
-  ppButton.click();
-  yield sourceShown;
-  yield waitUntil(() => checkedUnpretty);
-  */
+  // info("Testing un-pretty printing.");
+  // sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
+  // ppButton.click();
+  // yield sourceShown;
+  // yield waitUntil(() => checkedUnpretty);
+
 
   yield toolbox.destroy();
   gBrowser.removeCurrentTab();
   finish();
 });
 
 function checkPrettified(oldLoc, newLoc) {
   is(oldLoc.line, 3, "Correct line for JS:3");
copy from devtools/client/debugger/test/mochitest/code_binary_search.coffee
copy to devtools/client/framework/test/code_binary_search.coffee
copy from devtools/client/debugger/test/mochitest/code_binary_search.js
copy to devtools/client/framework/test/code_binary_search.js
copy from devtools/client/debugger/test/mochitest/code_binary_search.map
copy to devtools/client/framework/test/code_binary_search.map
copy from devtools/client/debugger/test/mochitest/doc_empty-tab-01.html
copy to devtools/client/framework/test/doc_empty-tab-01.html
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -6,16 +6,17 @@
 
 const MAX_ORDINAL = 99;
 const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
 const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
 const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER";
 const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER";
 const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
 const HTML_NS = "http://www.w3.org/1999/xhtml";
+const { SourceMapService } = require("./source-map-service");
 
 var {Cc, Ci, Cu} = require("chrome");
 var promise = require("promise");
 var defer = require("devtools/shared/defer");
 var Services = require("Services");
 var {Task} = require("devtools/shared/task");
 var {gDevTools} = require("devtools/client/framework/devtools");
 var EventEmitter = require("devtools/shared/event-emitter");
@@ -113,16 +114,19 @@ const ToolboxButtons = exports.ToolboxBu
  *        Type of host that will host the toolbox (e.g. sidebar, window)
  * @param {object} hostOptions
  *        Options for host specifically
  */
 function Toolbox(target, selectedTool, hostType, hostOptions) {
   this._target = target;
   this._toolPanels = new Map();
   this._telemetry = new Telemetry();
+  if (Services.prefs.getBoolPref("devtools.sourcemap.locations.enabled")) {
+    this._sourceMapService = new SourceMapService(this._target);
+  }
 
   this._initInspector = null;
   this._inspector = null;
 
   // Map of frames (id => frame-info) and currently selected frame id.
   this.frameMap = new Map();
   this.selectedFrameId = null;
 
@@ -2026,16 +2030,20 @@ Toolbox.prototype = {
     this.off("ready", this._showDevEditionPromo);
 
     gDevTools.off("tool-registered", this._toolRegistered);
     gDevTools.off("tool-unregistered", this._toolUnregistered);
 
     gDevTools.off("pref-changed", this._prefChanged);
 
     this._lastFocusedElement = null;
+    if (this._sourceMapService) {
+      this._sourceMapService.destroy();
+      this._sourceMapService = null;
+    }
 
     if (this.webconsolePanel) {
       this._saveSplitConsoleHeight();
       this.webconsolePanel.removeEventListener("resize",
         this._saveSplitConsoleHeight);
     }
     this.closeButton.removeEventListener("click", this.destroy, true);
     this.textboxContextMenuPopup.removeEventListener("popupshowing",
--- a/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_01.js
@@ -22,29 +22,29 @@ add_task(function* () {
   markup.isDragging = true;
 
   info("Simulate a mousemove on the view, at the bottom, and expect scrolling");
   let onScrolled = waitForScrollStop(markup.doc);
 
   markup._onMouseMove({
     preventDefault: () => {},
     target: markup.doc.body,
-    pageY: viewHeight
+    pageY: viewHeight + markup.doc.defaultView.scrollY
   });
 
   let bottomScrollPos = yield onScrolled;
   ok(bottomScrollPos > 0, "The view was scrolled down");
 
   info("Simulate a mousemove at the top and expect more scrolling");
   onScrolled = waitForScrollStop(markup.doc);
 
   markup._onMouseMove({
     preventDefault: () => {},
     target: markup.doc.body,
-    pageY: 0
+    pageY: markup.doc.defaultView.scrollY
   });
 
   let topScrollPos = yield onScrolled;
   ok(topScrollPos < bottomScrollPos, "The view was scrolled up");
   is(topScrollPos, 0, "The view was scrolled up to the top");
 
   info("Simulate a mouseup to stop dragging");
   markup._onMouseUp();
--- a/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js
+++ b/devtools/client/inspector/markup/test/browser_markup_dragdrop_autoscroll_02.js
@@ -21,29 +21,28 @@ add_task(function* () {
   markup.isDragging = true;
 
   info("Simulate a mousemove on the view, at the bottom, and expect scrolling");
   let onScrolled = waitForScrollStop(markup.doc);
 
   markup._onMouseMove({
     preventDefault: () => {},
     target: markup.doc.body,
-    pageY: viewHeight
+    pageY: viewHeight + markup.doc.defaultView.scrollY
   });
 
   let bottomScrollPos = yield onScrolled;
   ok(bottomScrollPos > 0, "The view was scrolled down");
-
   info("Simulate a mousemove at the top and expect more scrolling");
   onScrolled = waitForScrollStop(markup.doc);
 
   markup._onMouseMove({
     preventDefault: () => {},
     target: markup.doc.body,
-    pageY: 0
+    pageY: markup.doc.defaultView.scrollY
   });
 
   let topScrollPos = yield onScrolled;
   ok(topScrollPos < bottomScrollPos, "The view was scrolled up");
   is(topScrollPos, 0, "The view was scrolled up to the top");
 
   info("Simulate a mouseup to stop dragging");
   markup._onMouseUp();
--- a/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html
+++ b/devtools/client/inspector/markup/test/doc_markup_dragdrop_autoscroll_02.html
@@ -31,33 +31,10 @@ https://bugzilla.mozilla.org/show_bug.cg
   <div></div>
   <div></div>
   <div></div>
   <div></div>
   <div></div>
   <div></div>
   <div></div>
   <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
-  <div></div>
 </body>
 </html>
--- a/devtools/client/jsonview/components/text-panel.js
+++ b/devtools/client/jsonview/components/text-panel.js
@@ -66,29 +66,29 @@ define(function (require, exports, modul
     onCopy: function (event) {
       this.props.actions.onCopyJson();
     },
 
     render: function () {
       return (
         Toolbar({},
           ToolbarButton({
-            className: "btn prettyprint",
-            onClick: this.onPrettify},
-            Locale.$STR("jsonViewer.PrettyPrint")
-          ),
-          ToolbarButton({
             className: "btn save",
             onClick: this.onSave},
             Locale.$STR("jsonViewer.Save")
           ),
           ToolbarButton({
             className: "btn copy",
             onClick: this.onCopy},
             Locale.$STR("jsonViewer.Copy")
+          ),
+          ToolbarButton({
+            className: "btn prettyprint",
+            onClick: this.onPrettify},
+            Locale.$STR("jsonViewer.PrettyPrint")
           )
         )
       );
     },
   }));
 
   // Exports from this module
   exports.TextPanel = TextPanel;
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -291,16 +291,19 @@ pref("devtools.webconsole.timestampMessa
 
 // Web Console automatic multiline mode: |true| if you want incomplete statements
 // to automatically trigger multiline editing (equivalent to shift + enter).
 pref("devtools.webconsole.autoMultiline", true);
 
 // Enable the experimental webconsole frontend (work in progress)
 pref("devtools.webconsole.new-frontend-enabled", false);
 
+// Enable the experimental support for source maps in console (work in progress)
+pref("devtools.sourcemap.locations.enabled", false);
+
 // The number of lines that are displayed in the web console.
 pref("devtools.hud.loglimit", 1000);
 
 // The number of lines that are displayed in the web console for the Net,
 // CSS, JS and Web Developer categories. These defaults should be kept in sync
 // with DEFAULT_LOG_LIMIT in the webconsole frontend.
 pref("devtools.hud.loglimit.network", 1000);
 pref("devtools.hud.loglimit.cssparser", 1000);
--- a/devtools/client/shared/components/frame.js
+++ b/devtools/client/shared/components/frame.js
@@ -29,59 +29,136 @@ module.exports = createClass({
     // Option to display a function name even if it's anonymous.
     showAnonymousFunctionName: PropTypes.bool,
     // Option to display a host name after the source link.
     showHost: PropTypes.bool,
     // Option to display a host name if the filename is empty or just '/'
     showEmptyPathAsHost: PropTypes.bool,
     // Option to display a full source instead of just the filename.
     showFullSourceUrl: PropTypes.bool,
+    // Service to enable the source map feature for console.
+    sourceMapService: PropTypes.object,
   },
 
   getDefaultProps() {
     return {
       showFunctionName: false,
       showAnonymousFunctionName: false,
       showHost: false,
       showEmptyPathAsHost: false,
       showFullSourceUrl: false,
     };
   },
 
+  componentWillMount() {
+    const sourceMapService = this.props.sourceMapService;
+    if (sourceMapService) {
+      const source = this.getSource();
+      sourceMapService.subscribe(source, this.onSourceUpdated);
+    }
+  },
+
+  componentWillUnmount() {
+    const sourceMapService = this.props.sourceMapService;
+    if (sourceMapService) {
+      const source = this.getSource();
+      sourceMapService.unsubscribe(source, this.onSourceUpdated);
+    }
+  },
+
+  /**
+   * Component method to update the FrameView when a resolved location is available
+   * @param event
+   * @param location
+   */
+  onSourceUpdated(event, location, resolvedLocation) {
+    const frame = this.getFrame(resolvedLocation);
+    this.setState({
+      frame,
+      isSourceMapped: true,
+    });
+  },
+
+  /**
+   * Utility method to convert the Frame object to the
+   * Source Object model required by SourceMapService
+   * @param frame
+   * @returns {{url: *, line: *, column: *}}
+   */
+  getSource(frame) {
+    frame = frame || this.props.frame;
+    const { source, line, column } = frame;
+    return {
+      url: source,
+      line,
+      column,
+    };
+  },
+
+  /**
+   * Utility method to convert the Source object model to the
+   * Frame object model required by FrameView class.
+   * @param source
+   * @returns {{source: *, line: *, column: *, functionDisplayName: *}}
+   */
+  getFrame(source) {
+    const { url, line, column } = source;
+    return {
+      source: url,
+      line,
+      column,
+      functionDisplayName: this.props.frame.functionDisplayName,
+    };
+  },
+
   render() {
+    let frame, isSourceMapped;
     let {
       onClick,
-      frame,
       showFunctionName,
       showAnonymousFunctionName,
       showHost,
       showEmptyPathAsHost,
       showFullSourceUrl
     } = this.props;
 
+    if (this.state && this.state.isSourceMapped) {
+      frame = this.state.frame;
+      isSourceMapped = this.state.isSourceMapped;
+    } else {
+      frame = this.props.frame;
+    }
+
     let source = frame.source ? String(frame.source) : "";
     let line = frame.line != void 0 ? Number(frame.line) : null;
     let column = frame.column != void 0 ? Number(frame.column) : null;
 
     const { short, long, host } = getSourceNames(source);
     // Reparse the URL to determine if we should link this; `getSourceNames`
     // has already cached this indirectly. We don't want to attempt to
     // link to "self-hosted" and "(unknown)". However, we do want to link
     // to Scratchpad URIs.
-    const isLinkable = !!(isScratchpadScheme(source) || parseURL(source));
+    // Source mapped sources might not necessary linkable, but they
+    // are still valid in the debugger.
+    const isLinkable = !!(isScratchpadScheme(source) || parseURL(source))
+      || isSourceMapped;
     const elements = [];
     const sourceElements = [];
     let sourceEl;
 
     let tooltip = long;
+
+    // If the source is linkable and line > 0
+    const shouldDisplayLine = isLinkable && line;
+
     // Exclude all falsy values, including `0`, as even
     // a number 0 for line doesn't make sense, and should not be displayed.
     // If source isn't linkable, don't attempt to append line and column
     // info, as this probably doesn't make sense.
-    if (isLinkable && line) {
+    if (shouldDisplayLine) {
       tooltip += `:${line}`;
       // Intentionally exclude 0
       if (column) {
         tooltip += `:${column}`;
       }
     }
 
     let attributes = {
@@ -99,26 +176,35 @@ module.exports = createClass({
         elements.push(
           dom.span({ className: "frame-link-function-display-name" },
             functionDisplayName)
         );
       }
     }
 
     let displaySource = showFullSourceUrl ? long : short;
-    if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) {
+    // SourceMapped locations might not be parsed properly by parseURL.
+    // Eg: sourcemapped location could be /folder/file.coffee instead of a url
+    // and so the url parser would not parse non-url locations properly
+    // Check for "/" in displaySource. If "/" is in displaySource,
+    // take everything after last "/".
+    if (isSourceMapped) {
+      displaySource = displaySource.lastIndexOf("/") < 0 ?
+        displaySource :
+        displaySource.slice(displaySource.lastIndexOf("/") + 1);
+    } else if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) {
       displaySource = host;
     }
 
     sourceElements.push(dom.span({
       className: "frame-link-filename",
     }, displaySource));
 
     // If source is linkable, and we have a line number > 0
-    if (isLinkable && line) {
+    if (shouldDisplayLine) {
       let lineInfo = `:${line}`;
       // Add `data-line` attribute for testing
       attributes["data-line"] = line;
 
       // Intentionally exclude 0
       if (column) {
         lineInfo += `:${column}`;
         // Add `data-column` attribute for testing
@@ -129,17 +215,17 @@ module.exports = createClass({
     }
 
     // If source is not a URL (self-hosted, eval, etc.), don't make
     // it an anchor link, as we can't link to it.
     if (isLinkable) {
       sourceEl = dom.a({
         onClick: e => {
           e.preventDefault();
-          onClick(frame);
+          onClick(this.getSource(frame));
         },
         href: source,
         className: "frame-link-source",
         draggable: false,
         title: l10n.getFormatStr("frame.viewsourceindebugger", tooltip)
       }, sourceElements);
     } else {
       sourceEl = dom.span({
--- a/devtools/client/shared/components/reps/array.js
+++ b/devtools/client/shared/components/reps/array.js
@@ -122,17 +122,18 @@ define(function (require, exports, modul
     },
 
     render: function () {
       let mode = this.props.mode || "short";
       let object = this.props.object;
       let items;
 
       if (mode == "tiny") {
-        items = DOM.span({className: "length"}, object.length);
+        let isEmpty = object.length === 0;
+        items = DOM.span({className: "length"}, isEmpty ? "" : object.length);
       } else {
         let max = (mode == "short") ? 3 : 300;
         items = this.arrayIterator(object, max);
       }
 
       let objectLink = this.props.objectLink || DOM.span;
 
       return (
--- a/devtools/client/shared/components/reps/grip-array.js
+++ b/devtools/client/shared/components/reps/grip-array.js
@@ -103,17 +103,19 @@ define(function (require, exports, modul
 
     render: function () {
       let mode = this.props.mode || "short";
       let object = this.props.object;
 
       let items;
 
       if (mode == "tiny") {
-        items = span({className: "length"}, this.getLength(object));
+        let objectLength = this.getLength(object);
+        let isEmpty = objectLength === 0;
+        items = span({className: "length"}, isEmpty ? "" : objectLength);
       } else {
         let max = (mode == "short") ? 3 : 300;
         items = this.arrayIterator(object, max);
       }
 
       let objectLink = this.props.objectLink || span;
 
       return (
--- a/devtools/client/shared/components/test/mochitest/test_reps_array.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_array.html
@@ -52,17 +52,17 @@ window.onload = Task.async(function* () 
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
-        expectedOutput: `[0]`,
+        expectedOutput: `[]`,
       },
       {
         mode: "short",
         expectedOutput: defaultOutput,
       },
       {
         mode: "long",
         expectedOutput: defaultOutput,
--- a/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
@@ -52,17 +52,17 @@ window.onload = Task.async(function* () 
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
-        expectedOutput: `[0]`,
+        expectedOutput: `[]`,
       },
       {
         mode: "short",
         expectedOutput: defaultOutput,
       },
       {
         mode: "long",
         expectedOutput: defaultOutput,
--- a/devtools/client/shared/components/tree/tree-view.css
+++ b/devtools/client/shared/components/tree/tree-view.css
@@ -18,16 +18,21 @@
 /* TreeView Table*/
 
 .treeTable .treeLabelCell {
   padding: 2px 0 2px 0px;
   vertical-align: top;
   white-space: nowrap;
 }
 
+.treeTable .treeLabelCell::after {
+  content: ":";
+  color: var(--object-color);
+}
+
 .treeTable .treeValueCell {
   padding: 2px 0 2px 5px;
   overflow: hidden;
 }
 
 .treeTable .treeLabel {
   cursor: default;
   overflow: hidden;
--- a/devtools/client/shared/widgets/TableWidget.js
+++ b/devtools/client/shared/widgets/TableWidget.js
@@ -47,35 +47,38 @@ const MAX_VISIBLE_STRING_SIZE = 100;
  *
  * @param {nsIDOMNode} node
  *        The container element for the table widget.
  * @param {object} options
  *        - initialColumns: map of key vs display name for initial columns of
  *                          the table. See @setupColumns for more info.
  *        - uniqueId: the column which will be the unique identifier of each
  *                    entry in the table. Default: name.
+ *        - wrapTextInElements: Don't ever use 'value' attribute on labels.
+ *                              Default: false.
  *        - emptyText: text to display when no entries in the table to display.
  *        - highlightUpdated: true to highlight the changed/added row.
  *        - removableColumns: Whether columns are removeable. If set to false,
  *                            the context menu in the headers will not appear.
  *        - firstColumn: key of the first column that should appear.
  *        - cellContextMenuId: ID of a <menupopup> element to be set as a
  *                             context menu of every cell.
  */
 function TableWidget(node, options = {}) {
   EventEmitter.decorate(this);
 
   this.document = node.ownerDocument;
   this.window = this.document.defaultView;
   this._parent = node;
 
   let {initialColumns, emptyText, uniqueId, highlightUpdated, removableColumns,
-       firstColumn, cellContextMenuId} = options;
+       firstColumn, wrapTextInElements, cellContextMenuId} = options;
   this.emptyText = emptyText || "";
   this.uniqueId = uniqueId || "name";
+  this.wrapTextInElements = wrapTextInElements || false;
   this.firstColumn = firstColumn || "";
   this.highlightUpdated = highlightUpdated || false;
   this.removableColumns = removableColumns !== false;
   this.cellContextMenuId = cellContextMenuId;
 
   this.tbody = this.document.createElementNS(XUL_NS, "hbox");
   this.tbody.className = "table-widget-body theme-body";
   this.tbody.setAttribute("flex", "1");
@@ -959,16 +962,17 @@ module.exports.TableWidget = TableWidget
  *        The displayed string on the column's header.
  */
 function Column(table, id, header) {
   this.tbody = table.tbody;
   this.document = table.document;
   this.window = table.window;
   this.id = id;
   this.uniqueId = table.uniqueId;
+  this.wrapTextInElements = table.wrapTextInElements;
   this.table = table;
   this.cells = [];
   this.items = {};
 
   this.highlightUpdated = table.highlightUpdated;
 
   // This wrapping element is required solely so that position:sticky works on
   // the headers of the columns.
@@ -1441,16 +1445,17 @@ Column.prototype = {
  *        can be a DOMNode that is appended or a string value.
  * @param {Cell} nextCell
  *        The cell object which is next to this cell. null if this cell is last
  *        cell of the column
  */
 function Cell(column, item, nextCell) {
   let document = column.document;
 
+  this.wrapTextInElements = column.wrapTextInElements;
   this.label = document.createElementNS(XUL_NS, "label");
   this.label.setAttribute("crop", "end");
   this.label.className = "plain table-widget-cell";
 
   if (nextCell) {
     column.column.insertBefore(this.label, nextCell.label);
   } else {
     column.column.appendChild(this.label);
@@ -1494,16 +1499,22 @@ Cell.prototype = {
 
   set value(value) {
     this._value = value;
     if (value == null) {
       this.label.setAttribute("value", "");
       return;
     }
 
+    if (this.wrapTextInElements && !(value instanceof Ci.nsIDOMNode)) {
+      let span = this.label.ownerDocument.createElementNS(HTML_NS, "span");
+      span.textContent = value;
+      value = span;
+    }
+
     if (!(value instanceof Ci.nsIDOMNode) &&
         value.length > MAX_VISIBLE_STRING_SIZE) {
       value = value .substr(0, MAX_VISIBLE_STRING_SIZE) + "\u2026";
     }
 
     if (value instanceof Ci.nsIDOMNode) {
       this.label.removeAttribute("value");
 
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -493,16 +493,26 @@ a {
   border: 1px solid var(--theme-splitter-color);
   border-radius: 3px;
 }
 
 .consoletable {
   margin: 5px 0 0 0;
 }
 
+/* Force cells to only show one row of contents.  Getting normal ellipses
+   behavior has proven impossible so far, so this is better than letting
+   rows get out of vertical alignment when one cell has a lot of content. */
+.consoletable .table-widget-cell > span {
+  overflow: hidden;
+  display: flex;
+  height: 1.25em;
+  line-height: 1.25em;
+}
+
 .theme-light .message[severity=error] .stacktrace {
   background-color: rgba(255, 255, 255, 0.5);
 }
 
 .theme-dark .message[severity=error] .stacktrace {
   background-color: rgba(0, 0, 0, 0.5);
 }
 
--- a/devtools/client/webconsole/console-output.js
+++ b/devtools/client/webconsole/console-output.js
@@ -3615,16 +3615,17 @@ Widgets.Table.prototype = extend(Widgets
     if (this.element) {
       return this;
     }
 
     let result = this.element = this.document.createElementNS(XHTML_NS, "div");
     result.className = "consoletable devtools-monospace";
 
     this.table = new TableWidget(result, {
+      wrapTextInElements: true,
       initialColumns: this.columns,
       uniqueId: "_index",
       firstColumn: "_index"
     });
 
     for (let row of this.data) {
       this.table.push(row);
     }
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -2526,55 +2526,62 @@ WebConsoleFrame.prototype = {
     locationNode.className = "message-location devtools-monospace";
 
     if (!url) {
       url = "";
     }
 
     let fullURL = url.split(" -> ").pop();
     // Make the location clickable.
-    let onClick = () => {
+    let onClick = ({ url, line }) => {
       let category = locationNode.closest(".message").category;
       let target = null;
 
       if (/^Scratchpad\/\d+$/.test(url)) {
         target = "scratchpad";
       } else if (category === CATEGORY_CSS) {
         target = "styleeditor";
       } else if (category === CATEGORY_JS || category === CATEGORY_WEBDEV) {
         target = "jsdebugger";
-      } else if (/\.js$/.test(fullURL)) {
+      } else if (/\.js$/.test(url)) {
         // If it ends in .js, let's attempt to open in debugger
         // anyway, as this falls back to normal view-source.
         target = "jsdebugger";
+      } else {
+        // Point everything else to debugger, if source not available,
+        // it will fall back to view-source.
+        target = "jsdebugger";
       }
 
       switch (target) {
         case "scratchpad":
           this.owner.viewSourceInScratchpad(url, line);
           return;
         case "jsdebugger":
-          this.owner.viewSourceInDebugger(fullURL, line);
+          this.owner.viewSourceInDebugger(url, line);
           return;
         case "styleeditor":
-          this.owner.viewSourceInStyleEditor(fullURL, line);
+          this.owner.viewSourceInStyleEditor(url, line);
           return;
       }
       // No matching tool found; use old school view-source
-      this.owner.viewSource(fullURL, line);
+      this.owner.viewSource(url, line);
     };
 
+    const toolbox = gDevTools.getToolbox(this.owner.target);
+
     this.ReactDOM.render(this.FrameView({
       frame: {
         source: fullURL,
         line,
         column
       },
       showEmptyPathAsHost: true,
       onClick,
+      sourceMapService: toolbox ? toolbox._sourceMapService : null,
     }), locationNode);
 
     return locationNode;
   },
 
   /**
    * Adjusts the category and severity of the given message.
    *
--- a/devtools/server/actors/eventlooplag.js
+++ b/devtools/server/actors/eventlooplag.js
@@ -8,74 +8,53 @@
  * The eventLoopLag actor emits "event-loop-lag" events when the event
  * loop gets unresponsive. The event comes with a "time" property (the
  * duration of the lag in milliseconds).
  */
 
 const {Ci} = require("chrome");
 const Services = require("Services");
 const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
-const protocol = require("devtools/shared/protocol");
-const {method, Arg, RetVal} = protocol;
+const {Actor, ActorClassWithSpec} = require("devtools/shared/protocol");
 const events = require("sdk/event/core");
-
-var EventLoopLagActor = exports.EventLoopLagActor = protocol.ActorClass({
-
-  typeName: "eventLoopLag",
+const {eventLoopLagSpec} = require("devtools/shared/specs/eventlooplag");
 
+var EventLoopLagActor = exports.EventLoopLagActor = ActorClassWithSpec(eventLoopLagSpec, {
   _observerAdded: false,
 
-  events: {
-    "event-loop-lag" : {
-      type: "event-loop-lag",
-      time: Arg(0, "number") // duration of the lag in milliseconds.
-    }
-  },
-
   /**
    * Start tracking the event loop lags.
    */
-  start: method(function () {
+  start: function () {
     if (!this._observerAdded) {
       Services.obs.addObserver(this, "event-loop-lag", false);
       this._observerAdded = true;
     }
     return Services.appShell.startEventLoopLagTracking();
-  }, {
-    request: {},
-    response: {success: RetVal("number")}
-  }),
+  },
 
   /**
    * Stop tracking the event loop lags.
    */
-  stop: method(function () {
+  stop: function () {
     if (this._observerAdded) {
       Services.obs.removeObserver(this, "event-loop-lag");
       this._observerAdded = false;
     }
     Services.appShell.stopEventLoopLagTracking();
-  }, {request: {}, response: {}}),
+  },
 
   destroy: function () {
     this.stop();
-    protocol.Actor.prototype.destroy.call(this);
+    Actor.prototype.destroy.call(this);
   },
 
   // nsIObserver
 
   observe: function (subject, topic, data) {
     if (topic == "event-loop-lag") {
       // Forward event loop lag event
       events.emit(this, "event-loop-lag", data);
     }
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 });
-
-exports.EventLoopLagFront = protocol.FrontClass(EventLoopLagActor, {
-  initialize: function (client, form) {
-    protocol.Front.prototype.initialize.call(this, client);
-    this.actorID = form.eventLoopLagActor;
-    this.manage(this);
-  },
-});
--- a/devtools/server/actors/script.js
+++ b/devtools/server/actors/script.js
@@ -16,17 +16,16 @@ const { ObjectActor, createValueGrip, lo
 const { SourceActor, getSourceURL } = require("devtools/server/actors/source");
 const { DebuggerServer } = require("devtools/server/main");
 const { ActorClassWithSpec } = require("devtools/shared/protocol");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { assert, dumpn, update, fetch } = DevToolsUtils;
 const promise = require("promise");
 const PromiseDebugging = require("PromiseDebugging");
 const xpcInspector = require("xpcInspector");
-const ScriptStore = require("./utils/ScriptStore");
 const { DevToolsWorker } = require("devtools/shared/worker/worker");
 const object = require("sdk/util/object");
 const { threadSpec } = require("devtools/shared/specs/script");
 
 const { defer, resolve, reject, all } = promise;
 
 loader.lazyGetter(this, "Debugger", () => {
   let Debugger = require("Debugger");
@@ -489,24 +488,16 @@ const ThreadActor = ActorClassWithSpec(t
     if (!this._threadLifetimePool) {
       this._threadLifetimePool = new ActorPool(this.conn);
       this.conn.addActorPool(this._threadLifetimePool);
       this._threadLifetimePool.objectActors = new WeakMap();
     }
     return this._threadLifetimePool;
   },
 
-  get scripts() {
-    if (!this._scripts) {
-      this._scripts = new ScriptStore();
-      this._scripts.addScripts(this.dbg.findScripts());
-    }
-    return this._scripts;
-  },
-
   get sources() {
     return this._parent.sources;
   },
 
   get youngestFrame() {
     if (this.state != "paused") {
       return null;
     }
@@ -640,18 +631,16 @@ const ThreadActor = ActorClassWithSpec(t
     try {
       // Put ourselves in the paused state.
       let packet = this._paused();
       if (!packet) {
         return { error: "notAttached" };
       }
       packet.why = { type: "attached" };
 
-      this._restoreBreakpoints();
-
       // Send the response to the attach request now (rather than
       // returning it), because we're going to start a nested event loop
       // here.
       this.conn.send(packet);
 
       // Start a nested event loop.
       this._pushThreadPause();
 
@@ -1179,17 +1168,18 @@ const ThreadActor = ActorClassWithSpec(t
   _breakOnEnter: function (script) {
     let offsets = script.getAllOffsets();
     for (let line = 0, n = offsets.length; line < n; line++) {
       if (offsets[line]) {
         // N.B. Hidden breakpoints do not have an original location, and are not
         // stored in the breakpoint actor map.
         let actor = new BreakpointActor(this);
         this.threadLifetimePool.addActor(actor);
-        let scripts = this.scripts.getScriptsBySourceAndLine(script.source, line);
+
+        let scripts = this.dbg.findScripts({ source: script.source, line: line });
         let entryPoints = findEntryPointsForLine(scripts, line);
         setBreakpointAtEntryPoints(actor, entryPoints);
         this._hiddenBreakpoints.set(actor.actorID, actor);
         break;
       }
     }
   },
 
@@ -1313,17 +1303,18 @@ const ThreadActor = ActorClassWithSpec(t
   },
 
   /**
    * Get the script and source lists from the debugger.
    */
   _discoverSources: function () {
     // Only get one script per Debugger.Source.
     const sourcesToScripts = new Map();
-    const scripts = this.scripts.getAllScripts();
+    const scripts = this.dbg.findScripts();
+
     for (let i = 0, len = scripts.length; i < len; i++) {
       let s = scripts[i];
       if (s.source) {
         sourcesToScripts.set(s.source, s);
       }
     }
 
     return all([...sourcesToScripts.values()].map(script => {
@@ -1919,46 +1910,27 @@ const ThreadActor = ActorClassWithSpec(t
         from: this.actorID,
         type: name,
         source: source.form()
       });
     }
   },
 
   /**
-   * Restore any pre-existing breakpoints to the sources that we have access to.
-   */
-  _restoreBreakpoints: function () {
-    if (this.breakpointActorMap.size === 0) {
-      return;
-    }
-
-    for (let s of this.scripts.getSources()) {
-      this._addSource(s);
-    }
-  },
-
-  /**
    * Add the provided source to the server cache.
    *
    * @param aSource Debugger.Source
    *        The source that will be stored.
    * @returns true, if the source was added; false otherwise.
    */
   _addSource: function (aSource) {
     if (!this.sources.allowSource(aSource) || this._debuggerSourcesSeen.has(aSource)) {
       return false;
     }
 
-    // The scripts must be added to the ScriptStore before restoring
-    // breakpoints. If we try to add them to the ScriptStore any later, we can
-    // accidentally set a breakpoint in a top level script as a "closest match"
-    // because we wouldn't have added the child scripts to the ScriptStore yet.
-    this.scripts.addScripts(this.dbg.findScripts({ source: aSource }));
-
     let sourceActor = this.sources.createNonSourceMappedActor(aSource);
     let bpActors = [...this.breakpointActorMap.findActors()];
 
     if (this._options.useSourceMaps) {
       let promises = [];
 
       // Go ahead and establish the source actors for this script, which
       // fetches sourcemaps if available and sends onNewSource
--- a/devtools/server/actors/source.js
+++ b/devtools/server/actors/source.js
@@ -180,17 +180,16 @@ let SourceActor = ActorClassWithSpec(sou
 
   get isInlineSource() {
     return this._isInlineSource;
   },
 
   get threadActor() { return this._threadActor; },
   get sources() { return this._threadActor.sources; },
   get dbg() { return this.threadActor.dbg; },
-  get scripts() { return this.threadActor.scripts; },
   get source() { return this._source; },
   get generatedSource() { return this._generatedSource; },
   get breakpointActorMap() { return this.threadActor.breakpointActorMap; },
   get url() {
     if (this.source) {
       return getSourceURL(this.source, this.threadActor._parent.window);
     }
     return this._originalUrl;
@@ -439,17 +438,17 @@ let SourceActor = ActorClassWithSpec(sou
   /**
    * Extract all executable offsets from the given script
    * @param String url - extract offsets of the script with this url
    * @param Boolean onlyLine - will return only the line number
    * @return Set - Executable offsets/lines of the script
    **/
   getExecutableOffsets: function (source, onlyLine) {
     let offsets = new Set();
-    for (let s of this.threadActor.scripts.getScriptsBySource(source)) {
+    for (let s of this.dbg.findScripts({ source })) {
       for (let offset of s.getAllColumnOffsets()) {
         offsets.add(onlyLine ? offset.lineNumber : offset);
       }
     }
 
     return offsets;
   },
 
@@ -713,20 +712,27 @@ let SourceActor = ActorClassWithSpec(sou
     const { originalLocation } = actor;
     const { originalLine, originalSourceActor } = originalLocation;
 
     if (!this.isSourceMapped) {
       if (!this._setBreakpointAtGeneratedLocation(
         actor,
         GeneratedLocation.fromOriginalLocation(originalLocation)
       )) {
-        const scripts = this.scripts.getScriptsBySourceActorAndLine(
-          this,
-          originalLine
-        );
+        const query = { line: originalLine };
+        // For most cases, we have a real source to query for. The
+        // only time we don't is for HTML pages. In that case we want
+        // to query for scripts in an HTML page based on its URL, as
+        // there could be several sources within an HTML page.
+        if (this.source) {
+          query.source = this.source;
+        } else {
+          query.url = this.url;
+        }
+        const scripts = this.dbg.findScripts(query);
 
         // Never do breakpoint sliding for column breakpoints.
         // Additionally, never do breakpoint sliding if no scripts
         // exist on this line.
         //
         // Sliding can go horribly wrong if we always try to find the
         // next line with valid entry points in the entire file.
         // Scripts may be completely GCed and we never knew they
@@ -831,21 +837,25 @@ let SourceActor = ActorClassWithSpec(sou
   _setBreakpointAtGeneratedLocation: function (actor, generatedLocation) {
     let {
       generatedSourceActor,
       generatedLine,
       generatedColumn,
       generatedLastColumn
     } = generatedLocation;
 
-    // Find all scripts that match the given source actor and line number.
-    let scripts = this.scripts.getScriptsBySourceActorAndLine(
-      generatedSourceActor,
-      generatedLine
-    );
+    // Find all scripts that match the given source actor and line
+    // number.
+    const query = { line: generatedLine };
+    if (generatedSourceActor.source) {
+      query.source = generatedSourceActor.source;
+    } else {
+      query.url = generatedSourceActor.url;
+    }
+    let scripts = this.dbg.findScripts(query);
 
     scripts = scripts.filter((script) => !actor.hasScript(script));
 
     // Find all entry points that correspond to the given location.
     let entryPoints = [];
     if (generatedColumn === undefined) {
       // This is a line breakpoint, so we are interested in all offsets
       // that correspond to the given line number.
--- a/devtools/server/actors/stylesheets.js
+++ b/devtools/server/actors/stylesheets.js
@@ -455,17 +455,20 @@ var StyleSheetActor = protocol.ActorClas
       charset: this._getCSSCharset()
     };
 
     // Bug 1282660 - We use the system principal to load the default internal
     // stylesheets instead of the content principal since such stylesheets
     // require system principal to load. At meanwhile, we strip the loadGroup
     // for preventing the assertion of the userContextId mismatching.
     // The default internal stylesheets load from the 'resource:' URL.
-    if (!/^resource:\/\//.test(this.href)) {
+    // Bug 1287607 - The 'chrome:' URL will be also loaded from here, so we do
+    // the same thing for such URLs as well.
+    if (!/^resource:\/\//.test(this.href) &&
+        !/^chrome:\/\//.test(this.href)) {
       options.window = this.window;
       options.principal = this.document.nodePrincipal;
     }
 
     return fetch(this.href, options).then(({ content }) => {
       this.text = content;
       return content;
     });
deleted file mode 100644
--- a/devtools/server/actors/utils/ScriptStore.js
+++ /dev/null
@@ -1,219 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const { noop } = require("devtools/shared/DevToolsUtils");
-
-/**
- * A `ScriptStore` is a cache of `Debugger.Script` instances. It holds strong
- * references to the cached scripts to alleviate the GC-sensitivity issues that
- * plague `Debugger.prototype.findScripts`, but this means that its lifetime
- * must be managed carefully. It is the `ScriptStore` user's responsibility to
- * ensure that the `ScriptStore` stays up to date.
- *
- * Implementation Notes:
- *
- * The ScriptStore's prototype methods are very hot, in general. To help the
- * JIT, they avoid ES6-isms and higher-order iteration functions, for the most
- * part. You might be wondering why we don't maintain indices on, say,
- * Debugger.Source for faster querying, if these methods are so hot. First, the
- * hottest method is actually just getting all scripts; second, populating the
- * store becomes prohibitively expensive. So we fall back to linear queries
- * (which isn't so bad, because Debugger.prototype.findScripts is also linear).
- */
-function ScriptStore() {
-  // Set of every Debugger.Script in the cache.
-  this._scripts = new NoDeleteSet;
-}
-
-module.exports = ScriptStore;
-
-ScriptStore.prototype = {
-  // Populating a ScriptStore.
-
-  /**
-   * Add one script to the cache.
-   *
-   * @param Debugger.Script script
-   */
-  addScript(script) {
-    this._scripts.add(script);
-  },
-
-  /**
-   * Add many scripts to the cache at once.
-   *
-   * @param Array scripts
-   *        The set of Debugger.Scripts to add to the cache.
-   */
-  addScripts(scripts) {
-    for (var i = 0, len = scripts.length; i < len; i++) {
-      this.addScript(scripts[i]);
-    }
-  },
-
-  // Querying a ScriptStore.
-
-  /**
-   * Get all the sources for which we have scripts cached.
-   *
-   * @returns Array of Debugger.Source
-   */
-  getSources() {
-    return [...new Set(this._scripts.items.map(s => s.source))];
-  },
-
-  /**
-   * Get all the scripts in the cache.
-   *
-   * @returns read-only Array of Debugger.Script.
-   *
-   * NB: The ScriptStore retains ownership of the returned array, and the
-   * ScriptStore's consumers MUST NOT MODIFY its contents!
-   */
-  getAllScripts() {
-    return this._scripts.items;
-  },
-
-  getScriptsBySourceActor(sourceActor) {
-    return sourceActor.source ?
-           this.getScriptsBySource(sourceActor.source) :
-           this.getScriptsByURL(sourceActor._originalUrl);
-  },
-
-  getScriptsBySourceActorAndLine(sourceActor, line) {
-    return sourceActor.source ?
-           this.getScriptsBySourceAndLine(sourceActor.source, line) :
-           this.getScriptsByURLAndLine(sourceActor._originalUrl, line);
-  },
-
-  /**
-   * Get all scripts produced from the given source.
-   *
-   * @oaram Debugger.Source source
-   * @returns Array of Debugger.Script
-   */
-  getScriptsBySource(source) {
-    var results = [];
-    var scripts = this._scripts.items;
-    var length = scripts.length;
-    for (var i = 0; i < length; i++) {
-      if (scripts[i].source === source) {
-        results.push(scripts[i]);
-      }
-    }
-    return results;
-  },
-
-  /**
-   * Get all scripts produced from the given source whose source code definition
-   * spans the given line.
-   *
-   * @oaram Debugger.Source source
-   * @param Number line
-   * @returns Array of Debugger.Script
-   */
-  getScriptsBySourceAndLine(source, line) {
-    var results = [];
-    var scripts = this._scripts.items;
-    var length = scripts.length;
-    for (var i = 0; i < length; i++) {
-      var script = scripts[i];
-      if (script.source === source &&
-          script.startLine <= line &&
-          (script.startLine + script.lineCount) > line) {
-        results.push(script);
-      }
-    }
-    return results;
-  },
-
-  /**
-   * Get all scripts defined by a source at the given URL.
-   *
-   * @param String url
-   * @returns Array of Debugger.Script
-   */
-  getScriptsByURL(url) {
-    var results = [];
-    var scripts = this._scripts.items;
-    var length = scripts.length;
-    for (var i = 0; i < length; i++) {
-      if (scripts[i].url === url) {
-        results.push(scripts[i]);
-      }
-    }
-    return results;
-  },
-
-  /**
-   * Get all scripts defined by a source a the given URL and whose source code
-   * definition spans the given line.
-   *
-   * @param String url
-   * @param Number line
-   * @returns Array of Debugger.Script
-   */
-  getScriptsByURLAndLine(url, line) {
-    var results = [];
-    var scripts = this._scripts.items;
-    var length = scripts.length;
-    for (var i = 0; i < length; i++) {
-      var script = scripts[i];
-      if (script.url === url &&
-          script.startLine <= line &&
-          (script.startLine + script.lineCount) > line) {
-        results.push(script);
-      }
-    }
-    return results;
-  },
-};
-
-
-/**
- * A set which can only grow, and does not support the delete operation.
- * Provides faster iteration than the native Set by maintaining an array of all
- * items, in addition to the internal set of all items, which allows direct
- * iteration (without the iteration protocol and calling into C++, which are
- * both expensive).
- */
-function NoDeleteSet() {
-  this._set = new Set();
-  this.items = [];
-}
-
-NoDeleteSet.prototype = {
-  /**
-   * An array containing every item in the set for convenience and faster
-   * iteration. This is public for reading only, and consumers MUST NOT modify
-   * this array!
-   */
-  items: null,
-
-  /**
-   * Add an item to the set.
-   *
-   * @param any item
-   */
-  add(item) {
-    if (!this._set.has(item)) {
-      this._set.add(item);
-      this.items.push(item);
-    }
-  },
-
-  /**
-   * Return true if the item is in the set, false otherwise.
-   *
-   * @param any item
-   * @returns Boolean
-   */
-  has(item) {
-    return this._set.has(item);
-  }
-};
--- a/devtools/server/actors/utils/TabSources.js
+++ b/devtools/server/actors/utils/TabSources.js
@@ -240,17 +240,17 @@ TabSources.prototype = {
         }
       }
 
       if (url in this._sourceMappedSourceActors) {
         return this._sourceMappedSourceActors[url];
       }
     }
 
-    throw new Error("getSourceByURL: could not find source for " + url);
+    throw new Error("getSourceActorByURL: could not find source for " + url);
     return null;
   },
 
   /**
    * Returns true if the URL likely points to a minified resource, false
    * otherwise.
    *
    * @param String aURL
--- a/devtools/server/actors/utils/moz.build
+++ b/devtools/server/actors/utils/moz.build
@@ -5,13 +5,12 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'actor-registry-utils.js',
     'audionodes.json',
     'automation-timeline.js',
     'make-debugger.js',
     'map-uri-to-addon-id.js',
-    'ScriptStore.js',
     'stack.js',
     'TabSources.js',
     'walker-search.js'
 )
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -2086,51 +2086,45 @@ TabActor.prototype = {
    * @param {String} request.url
    * @param {Number} request.line
    * @param {Number?} request.column
    * @return {Promise<Object>}
    */
   onResolveLocation(request) {
     let { url, line } = request;
     let column = request.column || 0;
-    let actor = this.sources.getSourceActorByURL(url);
-
-    if (actor) {
-      // Get the generated source actor if this is source mapped
-      let generatedActor = actor.generatedSource ?
-        this.sources.createNonSourceMappedActor(actor.generatedSource) :
-        actor;
-      let generatedLocation = new GeneratedLocation(
-        generatedActor, line, column);
+    const scripts = this.threadActor.dbg.findScripts({ url });
 
-      return this.sources.getOriginalLocation(generatedLocation).then(loc => {
-        // If no map found, return this packet
-        if (loc.originalLine == null) {
-          return {
-            from: this.actorID,
-            type: "resolveLocation",
-            error: "MAP_NOT_FOUND"
-          };
-        }
-
-        loc = loc.toJSON();
-        return {
-          from: this.actorID,
-          url: loc.source.url,
-          column: loc.column,
-          line: loc.line
-        };
+    if (!scripts[0] || !scripts[0].source) {
+      return promise.resolve({
+        from: this.actorID,
+        type: "resolveLocation",
+        error: "SOURCE_NOT_FOUND"
       });
     }
+    const source = scripts[0].source;
+    const generatedActor = this.sources.createNonSourceMappedActor(source);
+    let generatedLocation = new GeneratedLocation(
+      generatedActor, line, column);
+    return this.sources.getOriginalLocation(generatedLocation).then(loc => {
+      // If no map found, return this packet
+      if (loc.originalLine == null) {
+        return {
+          type: "resolveLocation",
+          error: "MAP_NOT_FOUND"
+        };
+      }
 
-    // Fall back to this packet when source is not found
-    return promise.resolve({
-      from: this.actorID,
-      type: "resolveLocation",
-      error: "SOURCE_NOT_FOUND"
+      loc = loc.toJSON();
+      return {
+        from: this.actorID,
+        url: loc.source.url,
+        column: loc.column,
+        line: loc.line
+      };
     });
   },
 };
 
 /**
  * The request types this actor can handle.
  */
 TabActor.prototype.requestTypes = {
deleted file mode 100644
--- a/devtools/server/tests/unit/test_ScriptStore.js
+++ /dev/null
@@ -1,168 +0,0 @@
-/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// Test the functionality of ScriptStore.
-
-const ScriptStore = require("devtools/server/actors/utils/ScriptStore");
-
-// Fixtures
-
-const firstSource = "firstSource";
-const secondSource = "secondSource";
-const thirdSource = "thirdSource";
-
-const scripts = new Set([
-  {
-    url: "a.js",
-    source: firstSource,
-    startLine: 1,
-    lineCount: 100,
-    global: "g1"
-  },
-  {
-    url: "a.js",
-    source: firstSource,
-    startLine: 1,
-    lineCount: 40,
-    global: "g1"
-  },
-  {
-    url: "a.js",
-    source: firstSource,
-    startLine: 50,
-    lineCount: 100,
-    global: "g1"
-  },
-  {
-    url: "a.js",
-    source: firstSource,
-    startLine: 60,
-    lineCount: 90,
-    global: "g1"
-  },
-  {
-    url: "index.html",
-    source: secondSource,
-    startLine: 150,
-    lineCount: 1,
-    global: "g2"
-  },
-  {
-    url: "index.html",
-    source: thirdSource,
-    startLine: 200,
-    lineCount: 100,
-    global: "g2"
-  },
-  {
-    url: "index.html",
-    source: thirdSource,
-    startLine: 250,
-    lineCount: 10,
-    global: "g2"
-  },
-  {
-    url: "index.html",
-    source: thirdSource,
-    startLine: 275,
-    lineCount: 5,
-    global: "g2"
-  }
-]);
-
-function contains(script, line) {
-  return script.startLine <= line &&
-    line < script.startLine + script.lineCount;
-}
-
-function run_test() {
-  testAddScript();
-  testAddScripts();
-  testGetSources();
-  testGetScriptsBySource();
-  testGetScriptsBySourceAndLine();
-  testGetScriptsByURL();
-  testGetScriptsByURLAndLine();
-}
-
-function testAddScript() {
-  const ss = new ScriptStore();
-
-  for (let s of scripts) {
-    ss.addScript(s);
-  }
-
-  equal(ss.getAllScripts().length, scripts.size);
-
-  for (let s of ss.getAllScripts()) {
-    ok(scripts.has(s));
-  }
-}
-
-function testAddScripts() {
-  const ss = new ScriptStore();
-  ss.addScripts([...scripts]);
-
-  equal(ss.getAllScripts().length, scripts.size);
-
-  for (let s of ss.getAllScripts()) {
-    ok(scripts.has(s));
-  }
-}
-
-function testGetSources() {
-  const ss = new ScriptStore();
-  ss.addScripts([...scripts]);
-
-  const expected = new Set([firstSource, secondSource, thirdSource]);
-  const actual = ss.getSources();
-  equal(expected.size, actual.length);
-
-  for (let s of actual) {
-    ok(expected.has(s));
-    expected.delete(s);
-  }
-}
-
-function testGetScriptsBySource() {
-  const ss = new ScriptStore();
-  ss.addScripts([...scripts]);
-
-  const expected = [...scripts].filter(s => s.source === thirdSource);
-  const actual = ss.getScriptsBySource(thirdSource);
-
-  deepEqual(actual, expected);
-}
-
-function testGetScriptsBySourceAndLine() {
-  const ss = new ScriptStore();
-  ss.addScripts([...scripts]);
-
-  const expected = [...scripts].filter(
-    s => s.source === firstSource && contains(s, 65));
-  const actual = ss.getScriptsBySourceAndLine(firstSource, 65);
-
-  deepEqual(actual, expected);
-}
-
-function testGetScriptsByURL() {
-  const ss = new ScriptStore();
-  ss.addScripts([...scripts]);
-
-  const expected = [...scripts].filter(s => s.url === "index.html");
-  const actual = ss.getScriptsByURL("index.html");
-
-  deepEqual(actual, expected);
-}
-
-function testGetScriptsByURLAndLine() {
-  const ss = new ScriptStore();
-  ss.addScripts([...scripts]);
-
-  const expected = [...scripts].filter(
-    s => s.url === "index.html" && contains(s, 250));
-  const actual = ss.getScriptsByURLAndLine("index.html", 250);
-
-  deepEqual(actual, expected);
-}
--- a/devtools/server/tests/unit/test_eventlooplag_actor.js
+++ b/devtools/server/tests/unit/test_eventlooplag_actor.js
@@ -4,17 +4,17 @@
 /**
  * Test the eventLoopLag actor.
  */
 
 "use strict";
 
 function run_test()
 {
-  let {EventLoopLagFront} = require("devtools/server/actors/eventlooplag");
+  let {EventLoopLagFront} = require("devtools/shared/fronts/eventlooplag");
 
   DebuggerServer.init();
   DebuggerServer.addBrowserActors();
 
   // As seen in EventTracer.cpp
   let threshold = 20;
   let interval = 10;
 
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -32,17 +32,16 @@ support-files =
   setBreakpoint-on-line-with-no-offsets-in-gcd-script.js
   addons/web-extension/manifest.json
   addons/web-extension2/manifest.json
 
 [test_addon_reload.js]
 [test_addons_actor.js]
 [test_animation_name.js]
 [test_animation_type.js]
-[test_ScriptStore.js]
 [test_actor-registry-actor.js]
 [test_nesting-01.js]
 [test_nesting-02.js]
 [test_nesting-03.js]
 [test_forwardingprefix.js]
 [test_getyoungestframe.js]
 [test_nsjsinspector.js]
 [test_dbgactor.js]
new file mode 100644
--- /dev/null
+++ b/devtools/shared/fronts/eventlooplag.js
@@ -0,0 +1,15 @@
+/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol");
+const { eventLoopLagSpec } = require("devtools/shared/specs/eventlooplag");
+
+exports.EventLoopLagFront = FrontClassWithSpec(eventLoopLagSpec, {
+  initialize: function (client, form) {
+    Front.prototype.initialize.call(this, client);
+    this.actorID = form.eventLoopLagActor;
+    this.manage(this);
+  },
+});
--- a/devtools/shared/fronts/moz.build
+++ b/devtools/shared/fronts/moz.build
@@ -10,16 +10,17 @@ DevToolsModules(
     'animation.js',
     'call-watcher.js',
     'canvas.js',
     'css-properties.js',
     'csscoverage.js',
     'device.js',
     'director-manager.js',
     'director-registry.js',
+    'eventlooplag.js',
     'framerate.js',
     'gcli.js',
     'highlighters.js',
     'inspector.js',
     'layout.js',
     'memory.js',
     'performance-recording.js',
     'performance.js',
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/eventlooplag.js
@@ -0,0 +1,31 @@
+/* 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 { Arg, RetVal, generateActorSpec } = require("devtools/shared/protocol");
+
+const eventLoopLagSpec = generateActorSpec({
+  typeName: "eventLoopLag",
+
+  events: {
+    "event-loop-lag": {
+      type: "event-loop-lag",
+      // duration of the lag in milliseconds.
+      time: Arg(0, "number")
+    }
+  },
+
+  methods: {
+    start: {
+      request: {},
+      response: {success: RetVal("number")}
+    },
+    stop: {
+      request: {},
+      response: {}
+    }
+  }
+});
+
+exports.eventLoopLagSpec = eventLoopLagSpec;
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -12,16 +12,17 @@ DevToolsModules(
     'call-watcher.js',
     'canvas.js',
     'css-properties.js',
     'csscoverage.js',
     'device.js',
     'director-manager.js',
     'director-registry.js',
     'environment.js',
+    'eventlooplag.js',
     'frame.js',
     'framerate.js',
     'gcli.js',
     'heap-snapshot-file.js',
     'highlighters.js',
     'inspector.js',
     'layout.js',
     'memory.js',
--- a/devtools/shared/webconsole/test/common.js
+++ b/devtools/shared/webconsole/test/common.js
@@ -92,16 +92,19 @@ function _attachConsole(aListeners, aCal
           aCallback(aState, aResponse);
           return;
         }
         let tab = aResponse.tabs[aResponse.selected];
         aState.dbgClient.attachTab(tab.actor, function (response, tabClient) {
           if (aAttachToWorker) {
             let workerName = "console-test-worker.js#" + new Date().getTime();
             var worker = new Worker(workerName);
+            // Keep a strong reference to the Worker to avoid it being
+            // GCd during the test (bug 1237492).
+            aState._worker_ref = worker;
             worker.addEventListener("message", function listener() {
               worker.removeEventListener("message", listener);
               tabClient.listWorkers(function (response) {
                 let worker = response.workers.filter(w => w.url == workerName)[0];
                 if (!worker) {
                   console.error("listWorkers failed. Unable to find the " +
                                 "worker actor\n");
                   return;
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -3897,30 +3897,35 @@ HTMLInputElement::CancelRangeThumbDrag(b
   }
 }
 
 void
 HTMLInputElement::SetValueOfRangeForUserEvent(Decimal aValue)
 {
   MOZ_ASSERT(aValue.isFinite());
 
+  Decimal oldValue = GetValueAsDecimal();
+
   nsAutoString val;
   ConvertNumberToString(aValue, val);
   // TODO: What should we do if SetValueInternal fails?  (The allocation
   // is small, so we should be fine here.)
   SetValueInternal(val, nsTextEditorState::eSetValue_BySetUserInput |
                         nsTextEditorState::eSetValue_Notify);
   nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame());
   if (frame) {
     frame->UpdateForValueChange();
   }
-  nsContentUtils::DispatchTrustedEvent(OwnerDoc(),
-                                       static_cast<nsIDOMHTMLInputElement*>(this),
-                                       NS_LITERAL_STRING("input"), true,
-                                       false);
+
+  if (GetValueAsDecimal() != oldValue) {
+    nsContentUtils::DispatchTrustedEvent(OwnerDoc(),
+                                         static_cast<nsIDOMHTMLInputElement*>(this),
+                                         NS_LITERAL_STRING("input"), true,
+                                         false);
+  }
 }
 
 void
 HTMLInputElement::StartNumberControlSpinnerSpin()
 {
   MOZ_ASSERT(!mNumberControlSpinnerIsSpinning);
 
   mNumberControlSpinnerIsSpinning = true;
--- a/dom/html/test/mochitest.ini
+++ b/dom/html/test/mochitest.ini
@@ -452,16 +452,17 @@ skip-if = (toolkit == 'gonk' && debug) #
 [test_bug893537.html]
 [test_bug95530.html]
 [test_bug969346.html]
 [test_bug982039.html]
 [test_bug1003539.html]
 [test_bug1045270.html]
 [test_bug1146116.html]
 [test_bug1264157.html]
+[test_bug1287321.html]
 [test_change_crossorigin.html]
 [test_checked.html]
 [test_dir_attributes_reflection.html]
 [test_dl_attributes_reflection.html]
 [test_element_prototype.html]
 [test_embed_attributes_reflection.html]
 [test_formData.html]
 [test_formSubmission.html]
new file mode 100644
--- /dev/null
+++ b/dom/html/test/test_bug1287321.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1287321
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1287321</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript">
+
+  /** Test for Bug 1287321 **/
+
+  function test() {
+    var r = document.getElementById("range");
+    var rect = r.getBoundingClientRect();
+    var y = parseInt((rect.height / 2));
+    var movement = parseInt(rect.width / 10);
+    var x = movement;
+    synthesizeMouse(r, x, y, { type: "mousedown" });
+    x += movement;
+    var eventCount = 0;
+    r.oninput = function() {
+      ++eventCount;
+    }
+    synthesizeMouse(r, x, y, { type: "mousemove" });
+    is(eventCount, 1, "Got the expected input event");
+
+    x += movement;
+    synthesizeMouse(r, x, y, { type: "mousemove" });
+    is(eventCount, 2, "Got the expected input event");
+
+    synthesizeMouse(r, x, y, { type: "mousemove" });
+    is(eventCount, 2, "Got the expected input event");
+
+    x += movement;
+    synthesizeMouse(r, x, y, { type: "mousemove" });
+    is(eventCount, 3, "Got the expected input event");
+
+    synthesizeMouse(r, x, y, { type: "mouseup" });
+    is(eventCount, 3, "Got the expected input event");
+
+    SimpleTest.finish();
+  }
+
+  SimpleTest.waitForExplicitFinish();
+  SimpleTest.waitForFocus(test);
+
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1287321">Mozilla Bug 1287321</a>
+<input type="range" id="range">
+</body>
+</html>
--- a/gfx/gl/GLContext.cpp
+++ b/gfx/gl/GLContext.cpp
@@ -6,16 +6,20 @@
 
 #include "GLContext.h"
 
 #include <algorithm>
 #include <stdio.h>
 #include <string.h>
 #include <ctype.h>
 #include <vector>
+#ifdef MOZ_WIDGET_ANDROID
+#include <fcntl.h>
+#include <sys/mman.h>
+#endif
 
 #include "GLBlitHelper.h"
 #include "GLReadTexImageHelper.h"
 #include "GLScreenBuffer.h"
 
 #include "gfxCrashReporterUtils.h"
 #include "gfxEnv.h"
 #include "gfxUtils.h"
@@ -27,29 +31,34 @@
 #include "prlink.h"
 #include "ScopedGLHelpers.h"
 #include "SharedSurfaceGL.h"
 #include "GfxTexturesReporter.h"
 #include "TextureGarbageBin.h"
 #include "gfx2DGlue.h"
 #include "gfxPrefs.h"
 #include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/gfx/Logging.h"
 
 #include "OGLShaderProgram.h" // for ShaderProgramType
 
 #include "mozilla/DebugOnly.h"
 
 #ifdef XP_MACOSX
 #include <CoreServices/CoreServices.h>
 #endif
 
 #if defined(MOZ_WIDGET_COCOA)
 #include "nsCocoaFeatures.h"
 #endif
 
+#ifdef MOZ_WIDGET_ANDROID
+#include "AndroidBridge.h"
+#endif
+
 namespace mozilla {
 namespace gl {
 
 using namespace mozilla::gfx;
 using namespace mozilla::layers;
 
 #ifdef MOZ_GL_DEBUG
 unsigned GLContext::sCurrentGLContextTLS = -1;
@@ -456,16 +465,17 @@ GLContext::GLContext(CreateContextFlags 
     mLockedSurface(nullptr),
     mMaxTextureSize(0),
     mMaxCubeMapTextureSize(0),
     mMaxTextureImageSize(0),
     mMaxRenderbufferSize(0),
     mMaxSamples(0),
     mNeedsTextureSizeChecks(false),
     mNeedsFlushBeforeDeleteFB(false),
+    mTextureAllocCrashesOnMapFailure(false),
     mWorkAroundDriverBugs(true),
     mHeavyGLCallsSinceLastFlush(false)
 {
     mMaxViewportDims[0] = 0;
     mMaxViewportDims[1] = 0;
     mOwningThreadId = PlatformThread::CurrentId();
 }
 
@@ -800,17 +810,19 @@ GLContext::InitWithPrefixImpl(const char
 
     // The order of these strings must match up with the order of the enum
     // defined in GLContext.h for renderer IDs.
     const char* rendererMatchStrings[size_t(GLRenderer::Other)] = {
         "Adreno 200",
         "Adreno 205",
         "Adreno (TM) 200",
         "Adreno (TM) 205",
+        "Adreno (TM) 305",
         "Adreno (TM) 320",
+        "Adreno (TM) 330",
         "Adreno (TM) 420",
         "Mali-400 MP",
         "PowerVR SGX 530",
         "PowerVR SGX 540",
         "NVIDIA Tegra",
         "Android Emulator",
         "Gallium 0.4 on llvmpipe",
         "Intel HD Graphics 3000 OpenGL Engine",
@@ -1043,16 +1055,27 @@ GLContext::InitWithPrefixImpl(const char
     }
 #endif
     if (mWorkAroundDriverBugs &&
         Renderer() == GLRenderer::AdrenoTM420) {
         // see bug 1194923. Calling glFlush before glDeleteFramebuffers
         // prevents occasional driver crash.
         mNeedsFlushBeforeDeleteFB = true;
     }
+#ifdef MOZ_WIDGET_ANDROID
+    if (mWorkAroundDriverBugs &&
+        (Renderer() == GLRenderer::AdrenoTM305 ||
+         Renderer() == GLRenderer::AdrenoTM320 ||
+         Renderer() == GLRenderer::AdrenoTM330) &&
+        AndroidBridge::Bridge()->GetAPIVersion() < 21) {
+        // Bug 1164027. Driver crashes when functions such as
+        // glTexImage2D fail due to virtual memory exhaustion.
+        mTextureAllocCrashesOnMapFailure = true;
+    }
+#endif
 
     mMaxTextureImageSize = mMaxTextureSize;
 
     if (IsSupported(GLFeature::framebuffer_multisample)) {
         fGetIntegerv(LOCAL_GL_MAX_SAMPLES, (GLint*)&mMaxSamples);
     }
 
     ////////////////////////////////////////////////////////////////////////////
@@ -2845,16 +2868,68 @@ GLContext::fDeleteFramebuffers(GLsizei n
     if (n == 1 && *names == 0) {
         // Deleting framebuffer 0 causes hangs on the DROID. See bug 623228.
     } else {
         raw_fDeleteFramebuffers(n, names);
     }
     TRACKING_CONTEXT(DeletedFramebuffers(this, n, names));
 }
 
+#ifdef MOZ_WIDGET_ANDROID
+/**
+ * Conservatively estimate whether there is enough available
+ * contiguous virtual address space to map a newly allocated texture.
+ */
+static bool
+WillTextureMapSucceed(GLsizei width, GLsizei height, GLenum format, GLenum type)
+{
+    bool willSucceed = false;
+    // Some drivers leave large gaps between textures, so require
+    // there to be double the actual size of the texture available.
+    size_t size = width * height * GetBytesPerTexel(format, type) * 2;
+
+    int fd = open("/dev/zero", O_RDONLY);
+
+    void *p = mmap(nullptr, size, PROT_NONE, MAP_SHARED, fd, 0);
+    if (p != MAP_FAILED) {
+        willSucceed = true;
+        munmap(p, size);
+    }
+
+    close(fd);
+
+    return willSucceed;
+}
+#endif // MOZ_WIDGET_ANDROID
+
+void
+GLContext::fTexImage2D(GLenum target, GLint level, GLint internalformat,
+                       GLsizei width, GLsizei height, GLint border,
+                       GLenum format, GLenum type, const GLvoid* pixels) {
+    if (!IsTextureSizeSafeToPassToDriver(target, width, height)) {
+        // pass wrong values to cause the GL to generate GL_INVALID_VALUE.
+        // See bug 737182 and the comment in IsTextureSizeSafeToPassToDriver.
+        level = -1;
+        width = -1;
+        height = -1;
+        border = -1;
+    }
+#if MOZ_WIDGET_ANDROID
+    if (mTextureAllocCrashesOnMapFailure) {
+        // We have no way of knowing whether this texture already has
+        // storage allocated for it, and therefore whether this check
+        // is necessary. We must therefore assume it does not and
+        // always perform the check.
+        if (!WillTextureMapSucceed(width, height, internalformat, type)) {
+            return;
+        }
+    }
+#endif
+    raw_fTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
+}
 
 GLuint
 GLContext::GetDrawFB()
 {
     if (mScreen)
         return mScreen->GetDrawFB();
 
     GLuint ret = 0;
@@ -2963,11 +3038,55 @@ CreateTextureForOffscreen(GLContext* aGL
         MOZ_ASSERT(unpackType == LOCAL_GL_UNSIGNED_BYTE);
         internalFormat = LOCAL_GL_BGRA_EXT;
         unpackFormat = LOCAL_GL_BGRA_EXT;
     }
 
     return CreateTexture(aGL, internalFormat, unpackFormat, unpackType, aSize);
 }
 
+uint32_t
+GetBytesPerTexel(GLenum format, GLenum type)
+{
+    // If there is no defined format or type, we're not taking up any memory
+    if (!format || !type) {
+        return 0;
+    }
+
+    if (format == LOCAL_GL_DEPTH_COMPONENT) {
+        if (type == LOCAL_GL_UNSIGNED_SHORT)
+            return 2;
+        else if (type == LOCAL_GL_UNSIGNED_INT)
+            return 4;
+    } else if (format == LOCAL_GL_DEPTH_STENCIL) {
+        if (type == LOCAL_GL_UNSIGNED_INT_24_8_EXT)
+            return 4;
+    }
+
+    if (type == LOCAL_GL_UNSIGNED_BYTE || type == LOCAL_GL_FLOAT || type == LOCAL_GL_UNSIGNED_INT_8_8_8_8_REV) {
+        uint32_t multiplier = type == LOCAL_GL_UNSIGNED_BYTE ? 1 : 4;
+        switch (format) {
+            case LOCAL_GL_ALPHA:
+            case LOCAL_GL_LUMINANCE:
+                return 1 * multiplier;
+            case LOCAL_GL_LUMINANCE_ALPHA:
+                return 2 * multiplier;
+            case LOCAL_GL_RGB:
+                return 3 * multiplier;
+            case LOCAL_GL_RGBA:
+                return 4 * multiplier;
+            default:
+                break;
+        }
+    } else if (type == LOCAL_GL_UNSIGNED_SHORT_4_4_4_4 ||
+               type == LOCAL_GL_UNSIGNED_SHORT_5_5_5_1 ||
+               type == LOCAL_GL_UNSIGNED_SHORT_5_6_5)
+    {
+        return 2;
+    }
+
+    gfxCriticalError() << "Unknown texture type " << type << " or format " << format;
+    MOZ_CRASH();
+    return 0;
+}
 
 } /* namespace gl */
 } /* namespace mozilla */
--- a/gfx/gl/GLContext.h
+++ b/gfx/gl/GLContext.h
@@ -165,17 +165,19 @@ enum class GLVendor {
     Other
 };
 
 enum class GLRenderer {
     Adreno200,
     Adreno205,
     AdrenoTM200,
     AdrenoTM205,
+    AdrenoTM305,
     AdrenoTM320,
+    AdrenoTM330,
     AdrenoTM420,
     Mali400MP,
     SGX530,
     SGX540,
     Tegra,
     AndroidEmulator,
     GalliumLlvmpipe,
     IntelHD3000,
@@ -1616,27 +1618,19 @@ private:
         ASSERT_NOT_PASSING_STACK_BUFFER_TO_GL(pixels);
         BEFORE_GL_CALL;
         mSymbols.fTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
         AFTER_GL_CALL;
         mHeavyGLCallsSinceLastFlush = true;
     }
 
 public:
-    void fTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid* pixels) {
-        if (!IsTextureSizeSafeToPassToDriver(target, width, height)) {
-            // pass wrong values to cause the GL to generate GL_INVALID_VALUE.
-            // See bug 737182 and the comment in IsTextureSizeSafeToPassToDriver.
-            level = -1;
-            width = -1;
-            height = -1;
-            border = -1;
-        }
-        raw_fTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
-    }
+    void fTexImage2D(GLenum target, GLint level, GLint internalformat,
+                     GLsizei width, GLsizei height, GLint border,
+                     GLenum format, GLenum type, const GLvoid* pixels);
 
     void fTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid* pixels) {
         ASSERT_NOT_PASSING_STACK_BUFFER_TO_GL(pixels);
         BEFORE_GL_CALL;
         mSymbols.fTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
         AFTER_GL_CALL;
         mHeavyGLCallsSinceLastFlush = true;
     }
@@ -3526,16 +3520,17 @@ protected:
     GLint mMaxTextureSize;
     GLint mMaxCubeMapTextureSize;
     GLint mMaxTextureImageSize;
     GLint mMaxRenderbufferSize;
     GLint mMaxViewportDims[2];
     GLsizei mMaxSamples;
     bool mNeedsTextureSizeChecks;
     bool mNeedsFlushBeforeDeleteFB;
+    bool mTextureAllocCrashesOnMapFailure;
     bool mWorkAroundDriverBugs;
 
     bool IsTextureSizeSafeToPassToDriver(GLenum target, GLsizei width, GLsizei height) const {
         if (mNeedsTextureSizeChecks) {
             // some drivers incorrectly handle some large texture sizes that are below the
             // max texture size that they report. So we check ourselves against our own values
             // (mMax[CubeMap]TextureSize).
             // see bug 737182 for Mac Intel 2D textures
@@ -3694,12 +3689,18 @@ GLuint CreateTextureForOffscreen(GLConte
  *    GL_TEXTURE_MIN_FILTER = GL_LINEAR
  *    GL_TEXTURE_MAG_FILTER = GL_LINEAR
  *    GL_TEXTURE_WRAP_S = GL_CLAMP_TO_EDGE
  *    GL_TEXTURE_WRAP_T = GL_CLAMP_TO_EDGE
  */
 GLuint CreateTexture(GLContext* aGL, GLenum aInternalFormat, GLenum aFormat,
                      GLenum aType, const gfx::IntSize& aSize, bool linear = true);
 
+/**
+ * Helper function that calculates the number of bytes required per
+ * texel for a texture from its format and type.
+ */
+uint32_t GetBytesPerTexel(GLenum format, GLenum type);
+
 } /* namespace gl */
 } /* namespace mozilla */
 
 #endif /* GLCONTEXT_H_ */
--- a/gfx/gl/GLUploadHelpers.cpp
+++ b/gfx/gl/GLUploadHelpers.cpp
@@ -376,61 +376,16 @@ TexImage2DHelper(GLContext* gl,
                         format,
                         type,
                         pixels);
         gl->fPixelStorei(LOCAL_GL_UNPACK_ROW_LENGTH, 0);
         gl->fPixelStorei(LOCAL_GL_UNPACK_ALIGNMENT, 4);
     }
 }
 
-static uint32_t
-GetBytesPerTexel(GLenum format, GLenum type)
-{
-    // If there is no defined format or type, we're not taking up any memory
-    if (!format || !type) {
-        return 0;
-    }
-
-    if (format == LOCAL_GL_DEPTH_COMPONENT) {
-        if (type == LOCAL_GL_UNSIGNED_SHORT)
-            return 2;
-        else if (type == LOCAL_GL_UNSIGNED_INT)
-            return 4;
-    } else if (format == LOCAL_GL_DEPTH_STENCIL) {
-        if (type == LOCAL_GL_UNSIGNED_INT_24_8_EXT)
-            return 4;
-    }
-
-    if (type == LOCAL_GL_UNSIGNED_BYTE || type == LOCAL_GL_FLOAT || type == LOCAL_GL_UNSIGNED_INT_8_8_8_8_REV) {
-        uint32_t multiplier = type == LOCAL_GL_UNSIGNED_BYTE ? 1 : 4;
-        switch (format) {
-            case LOCAL_GL_ALPHA:
-            case LOCAL_GL_LUMINANCE:
-                return 1 * multiplier;
-            case LOCAL_GL_LUMINANCE_ALPHA:
-                return 2 * multiplier;
-            case LOCAL_GL_RGB:
-                return 3 * multiplier;
-            case LOCAL_GL_RGBA:
-                return 4 * multiplier;
-            default:
-                break;
-        }
-    } else if (type == LOCAL_GL_UNSIGNED_SHORT_4_4_4_4 ||
-               type == LOCAL_GL_UNSIGNED_SHORT_5_5_5_1 ||
-               type == LOCAL_GL_UNSIGNED_SHORT_5_6_5)
-    {
-        return 2;
-    }
-
-    gfxCriticalError() << "Unknown texture type " << type << " or format " << format;
-    MOZ_CRASH();
-    return 0;
-}
-
 SurfaceFormat
 UploadImageDataToTexture(GLContext* gl,
                          unsigned char* aData,
                          int32_t aStride,
                          SurfaceFormat aFormat,
                          const nsIntRegion& aDstRegion,
                          GLuint& aTexture,
                          size_t* aOutUploadSize,
--- a/testing/mozbase/mozfile/mozfile/mozfile.py
+++ b/testing/mozbase/mozfile/mozfile/mozfile.py
@@ -137,16 +137,18 @@ def rmtree(dir):
     return remove(dir)
 
 
 def _call_windows_retry(func, args=(), retry_max=5, retry_delay=0.5):
     """
     It's possible to see spurious errors on Windows due to various things
     keeping a handle to the directory open (explorer, virus scanners, etc)
     So we try a few times if it fails with a known error.
+    retry_delay is multiplied by the number of failed attempts to increase
+    the likelihood of success in subsequent attempts.
     """
     retry_count = 0
     while True:
         try:
             func(*args)
         except OSError as e:
             # Error codes are defined in:
             # http://docs.python.org/2/library/errno.html#module-errno
@@ -155,34 +157,34 @@ def _call_windows_retry(func, args=(), r
 
             if retry_count == retry_max:
                 raise
 
             retry_count += 1
 
             print '%s() failed for "%s". Reason: %s (%s). Retrying...' % \
                     (func.__name__, args, e.strerror, e.errno)
-            time.sleep(retry_delay)
+            time.sleep(retry_count * retry_delay)
         else:
             # If no exception has been thrown it should be done
             break
 
 
 def remove(path):
     """Removes the specified file, link, or directory tree.
 
     This is a replacement for shutil.rmtree that works better under
     windows. It does the following things:
 
      - check path access for the current user before trying to remove
      - retry operations on some known errors due to various things keeping
        a handle on file paths - like explorer, virus scanners, etc. The
        known errors are errno.EACCES and errno.ENOTEMPTY, and it will
-       retry up to 5 five times with a delay of 0.5 seconds between each
-       attempt.
+       retry up to 5 five times with a delay of (failed_attempts * 0.5) seconds
+       between each attempt.
 
     Note that no error will be raised if the given path does not exists.
 
     :param path: path to be removed
     """
 
     import shutil
 
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5075,16 +5075,42 @@
     "kind": "count",
     "description": "a testing histogram; not meant to be touched"
   },
   "TELEMETRY_TEST_COUNT_INIT_NO_RECORD": {
     "expires_in_version": "never",
     "kind": "count",
     "description": "a testing histogram; not meant to be touched - initially not recording"
   },
+  "TELEMETRY_TEST_CATEGORICAL": {
+    "alert_emails": ["telemetry-client-dev@mozilla.com"],
+    "bug_numbers": [1188888],
+    "expires_in_version": "never",
+    "kind": "categorical",
+    "labels": [
+      "CommonLabel",
+      "Label2",
+      "Label3"
+    ],
+    "description": "a testing histogram; not meant to be touched"
+  },
+  "TELEMETRY_TEST_CATEGORICAL_OPTOUT": {
+    "alert_emails": ["telemetry-client-dev@mozilla.com"],
+    "bug_numbers": [1188888],
+    "expires_in_version": "never",
+    "releaseChannelCollection": "opt-out",
+    "kind": "categorical",
+    "labels": [
+      "CommonLabel",
+      "Label4",
+      "Label5",
+      "Label6"
+    ],
+    "description": "a testing histogram; not meant to be touched"
+  },
   "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD": {
     "expires_in_version": "never",
     "kind": "count",
     "keyed": true,
     "description": "a testing histogram; not meant to be touched - initially not recording"
   },
   "TELEMETRY_TEST_KEYED_FLAG": {
     "expires_in_version": "never",
--- a/toolkit/components/telemetry/Telemetry.cpp
+++ b/toolkit/components/telemetry/Telemetry.cpp
@@ -2795,16 +2795,22 @@ Accumulate(const char* name, uint32_t sa
 
 void
 Accumulate(const char *name, const nsCString& key, uint32_t sample)
 {
   TelemetryHistogram::Accumulate(name, key, sample);
 }
 
 void
+AccumulateCategorical(ID id, const nsCString& label)
+{
+  TelemetryHistogram::AccumulateCategorical(id, label);
+}
+
+void
 AccumulateTimeDelta(ID aHistogram, TimeStamp start, TimeStamp end)
 {
   Accumulate(aHistogram,
              static_cast<uint32_t>((end - start).ToMilliseconds()));
 }
 
 void
 ClearHistogram(ID aId)
--- a/toolkit/components/telemetry/Telemetry.h
+++ b/toolkit/components/telemetry/Telemetry.h
@@ -46,66 +46,92 @@ void CreateStatisticsRecorder();
 void DestroyStatisticsRecorder();
 
 /**
  * Initialize the Telemetry service on the main thread at startup.
  */
 void Init();
 
 /**
- * Adds sample to a histogram defined in TelemetryHistograms.h
+ * Adds sample to a histogram defined in TelemetryHistogramEnums.h
  *
  * @param id - histogram id
  * @param sample - value to record.
  */
 void Accumulate(ID id, uint32_t sample);
 
 /**
- * Adds sample to a keyed histogram defined in TelemetryHistograms.h
+ * Adds sample to a keyed histogram defined in TelemetryHistogramEnums.h
  *
  * @param id - keyed histogram id
  * @param key - the string key
  * @param sample - (optional) value to record, defaults to 1.
  */
 void Accumulate(ID id, const nsCString& key, uint32_t sample = 1);
 
 /**
- * Adds a sample to a histogram defined in TelemetryHistograms.h.
+ * Adds a sample to a histogram defined in TelemetryHistogramEnums.h.
  * This function is here to support telemetry measurements from Java,
  * where we have only names and not numeric IDs.  You should almost
  * certainly be using the by-enum-id version instead of this one.
  *
  * @param name - histogram name
  * @param sample - value to record
  */
 void Accumulate(const char* name, uint32_t sample);
 
 /**
- * Adds a sample to a histogram defined in TelemetryHistograms.h.
+ * Adds a sample to a histogram defined in TelemetryHistogramEnums.h.
  * This function is here to support telemetry measurements from Java,
  * where we have only names and not numeric IDs.  You should almost
  * certainly be using the by-enum-id version instead of this one.
  *
  * @param name - histogram name
  * @param key - the string key
  * @param sample - sample - (optional) value to record, defaults to 1.
  */
 void Accumulate(const char *name, const nsCString& key, uint32_t sample = 1);
 
 /**
- * Adds time delta in milliseconds to a histogram defined in TelemetryHistograms.h
+ * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h
+ * This is the typesafe - and preferred - way to use the categorical histograms
+ * by passing values from the corresponding Telemetry::LABELS_* enum.
+ *
+ * @param enumValue - Label value from one of the Telemetry::LABELS_* enums.
+ */
+template<class E>
+void AccumulateCategorical(E enumValue) {
+  static_assert(IsCategoricalLabelEnum<E>::value,
+                "Only categorical label enum types are supported.");
+  Accumulate(static_cast<ID>(CategoricalLabelId<E>::value),
+             static_cast<uint32_t>(enumValue));
+};
+
+/**
+ * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h
+ * This string will be matched against the labels defined in Histograms.json.
+ * If the string does not match a label defined for the histogram, nothing will
+ * be recorded.
+ *
+ * @param id - The histogram id.
+ * @param label - A string label value that is defined in Histograms.json for this histogram.
+ */
+void AccumulateCategorical(ID id, const nsCString& label);
+
+/**
+ * Adds time delta in milliseconds to a histogram defined in TelemetryHistogramEnums.h
  *
  * @param id - histogram id
  * @param start - start time
  * @param end - end time
  */
 void AccumulateTimeDelta(ID id, TimeStamp start, TimeStamp end = TimeStamp::Now());
 
 /**
- * This clears the data for a histogram in TelemetryHistograms.h.
+ * This clears the data for a histogram in TelemetryHistogramEnums.h.
  *
  * @param id - histogram id
  */
 void ClearHistogram(ID id);
 
 /**
  * Enable/disable recording for this histogram at runtime.
  * Recording is enabled by default, unless listed at kRecordingInitiallyDisabledIDs[].
--- a/toolkit/components/telemetry/TelemetryHistogram.cpp
+++ b/toolkit/components/telemetry/TelemetryHistogram.cpp
@@ -114,20 +114,23 @@ typedef nsClassHashtable<nsCStringHashKe
 struct HistogramInfo {
   uint32_t min;
   uint32_t max;
   uint32_t bucketCount;
   uint32_t histogramType;
   uint32_t id_offset;
   uint32_t expiration_offset;
   uint32_t dataset;
+  uint32_t label_index;
+  uint32_t label_count;
   bool keyed;
 
   const char *id() const;
   const char *expiration() const;
+  nsresult label_id(const char* label, uint32_t* labelId) const;
 };
 
 struct AddonHistogramInfo {
   uint32_t min;
   uint32_t max;
   uint32_t bucketCount;
   uint32_t histogramType;
   Histogram *h;
@@ -284,16 +287,41 @@ HistogramInfo::id() const
 }
 
 const char *
 HistogramInfo::expiration() const
 {
   return &gHistogramStringTable[this->expiration_offset];
 }
 
+nsresult
+HistogramInfo::label_id(const char* label, uint32_t* labelId) const
+{
+  MOZ_ASSERT(label);
+  MOZ_ASSERT(this->histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL);
+  if (this->histogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) {
+    return NS_ERROR_FAILURE;
+  }
+
+  for (uint32_t i = 0; i < this->label_count; ++i) {
+    // gHistogramLabelTable contains the indices of the label strings in the
+    // gHistogramStringTable.
+    // They are stored in-order and consecutively, from the offset label_index
+    // to (label_index + label_count).
+    uint32_t string_offset = gHistogramLabelTable[this->label_index + i];
+    const char* const str = &gHistogramStringTable[string_offset];
+    if (::strcmp(label, str) == 0) {
+      *labelId = i;
+      return NS_OK;
+    }
+  }
+
+  return NS_ERROR_FAILURE;
+}
+
 } // namespace
 
 
 ////////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////////
 //
 // PRIVATE: Histogram Get, Add, Clone, Clear functions
 
@@ -349,16 +377,17 @@ internal_HistogramGet(const char *name, 
     histogramType = nsITelemetry::HISTOGRAM_LINEAR;
   }
 
   switch (histogramType) {
   case nsITelemetry::HISTOGRAM_EXPONENTIAL:
     *result = Histogram::FactoryGet(name, min, max, bucketCount, Histogram::kUmaTargetedHistogramFlag);
     break;
   case nsITelemetry::HISTOGRAM_LINEAR:
+  case nsITelemetry::HISTOGRAM_CATEGORICAL:
     *result = LinearHistogram::FactoryGet(name, min, max, bucketCount, Histogram::kUmaTargetedHistogramFlag);
     break;
   case nsITelemetry::HISTOGRAM_BOOLEAN:
     *result = BooleanHistogram::FactoryGet(name, Histogram::kUmaTargetedHistogramFlag);
     break;
   case nsITelemetry::HISTOGRAM_FLAG:
     *result = FlagHistogram::FactoryGet(name, Histogram::kUmaTargetedHistogramFlag);
     break;
@@ -563,16 +592,33 @@ internal_HistogramAdd(Histogram& histogr
       return NS_OK;
     }
     dataset = gHistograms[id].dataset;
   }
 
   return internal_HistogramAdd(histogram, value, dataset);
 }
 
+nsresult
+internal_HistogramAddCategorical(mozilla::Telemetry::ID id, const nsCString& label)
+{
+  uint32_t labelId = 0;
+  if (NS_FAILED(gHistograms[id].label_id(label.get(), &labelId))) {
+    return NS_ERROR_ILLEGAL_VALUE;
+  }
+
+  Histogram* h = nullptr;
+  nsresult rv = internal_GetHistogramByEnumId(id, &h);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  return internal_HistogramAdd(*h, labelId);
+}
+
 void
 internal_HistogramClear(Histogram& aHistogram, bool onlySubsession)
 {
   if (!onlySubsession) {
     aHistogram.Clear();
   }
 
 #if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
@@ -1042,49 +1088,77 @@ internal_GetKeyedHistogramById(const nsA
 // that seems preferable to risking deadlock.
 
 namespace {
 
 bool
 internal_JSHistogram_Add(JSContext *cx, unsigned argc, JS::Value *vp)
 {
   JSObject *obj = JS_THIS_OBJECT(cx, vp);
+  MOZ_ASSERT(obj);
   if (!obj) {
     return false;
   }
 
   Histogram *h = static_cast<Histogram*>(JS_GetPrivate(obj));
   MOZ_ASSERT(h);
   Histogram::ClassType type = h->histogram_type();
 
   JS::CallArgs args = CallArgsFromVp(argc, vp);
 
+  if (!internal_CanRecordBase()) {
+    return true;
+  }
+
   // If we don't have an argument for the count histogram, assume an increment of 1.
   // Otherwise, make sure to run some sanity checks on the argument.
-  int32_t value = 1;
-  if ((type != base::CountHistogram::COUNT_HISTOGRAM) || args.length()) {
-    if (!args.length()) {
-      JS_ReportError(cx, "Expected one argument");
+  if ((type == base::CountHistogram::COUNT_HISTOGRAM) && (args.length() == 0)) {
+    internal_HistogramAdd(*h, 1);
+    return true;
+  }
+
+  // For categorical histograms we allow passing a string argument that specifies the label.
+  mozilla::Telemetry::ID id;
+  if (type == base::LinearHistogram::LINEAR_HISTOGRAM &&
+      (args.length() > 0) && args[0].isString() &&
+      NS_SUCCEEDED(internal_GetHistogramEnumId(h->histogram_name().c_str(), &id)) &&
+      gHistograms[id].histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL) {
+    nsAutoJSString label;
+    if (!label.init(cx, args[0])) {
+      JS_ReportError(cx, "Invalid string parameter");
       return false;
     }
 
-    if (!(args[0].isNumber() || args[0].isBoolean())) {
-      JS_ReportError(cx, "Not a number");
+    nsresult rv = internal_HistogramAddCategorical(id, NS_ConvertUTF16toUTF8(label));
+    if (NS_FAILED(rv)) {
+      JS_ReportError(cx, "Unknown label for categorical histogram");
       return false;
     }
 
-    if (!JS::ToInt32(cx, args[0], &value)) {
-      return false;
-    }
+    return true;
+  }
+
+  // All other accumulations expect one numerical argument.
+  int32_t value = 0;
+  if (!args.length()) {
+    JS_ReportError(cx, "Expected one argument");
+    return false;
   }
 
-  if (internal_CanRecordBase()) {
-    internal_HistogramAdd(*h, value);
+  if (!(args[0].isNumber() || args[0].isBoolean())) {
+    JS_ReportError(cx, "Not a number");
+    return false;
   }
 
+  if (!JS::ToInt32(cx, args[0], &value)) {
+    JS_ReportError(cx, "Failed to convert argument");
+    return false;
+  }
+
+  internal_HistogramAdd(*h, value);
   return true;
 }
 
 bool
 internal_JSHistogram_Snapshot(JSContext *cx, unsigned argc, JS::Value *vp)
 {
   JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
   JSObject *obj = JS_THIS_OBJECT(cx, vp);
@@ -1872,16 +1946,24 @@ TelemetryHistogram::Accumulate(const cha
   mozilla::Telemetry::ID id;
   nsresult rv = internal_GetHistogramEnumId(name, &id);
   if (NS_SUCCEEDED(rv)) {
     internal_Accumulate(id, key, sample);
   }
 }
 
 void
+TelemetryHistogram::AccumulateCategorical(mozilla::Telemetry::ID aId,
+                                          const nsCString& label)
+{
+  StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+  internal_HistogramAddCategorical(aId, label);
+}
+
+void
 TelemetryHistogram::ClearHistogram(mozilla::Telemetry::ID aId)
 {
   StaticMutexAutoLock locker(gTelemetryHistogramMutex);
   if (!internal_CanRecordBase()) {
     return;
   }
 
   Histogram *h;
--- a/toolkit/components/telemetry/TelemetryHistogram.h
+++ b/toolkit/components/telemetry/TelemetryHistogram.h
@@ -35,16 +35,18 @@ void SetHistogramRecordingEnabled(mozill
 nsresult SetHistogramRecordingEnabled(const nsACString &id, bool aEnabled);
 
 void Accumulate(mozilla::Telemetry::ID aHistogram, uint32_t aSample);
 void Accumulate(mozilla::Telemetry::ID aID, const nsCString& aKey,
                                             uint32_t aSample);
 void Accumulate(const char* name, uint32_t sample);
 void Accumulate(const char* name, const nsCString& key, uint32_t sample);
 
+void AccumulateCategorical(mozilla::Telemetry::ID aId, const nsCString& aLabel);
+
 void
 ClearHistogram(mozilla::Telemetry::ID aId);
 
 nsresult
 GetHistogramById(const nsACString &name, JSContext *cx,
                  JS::MutableHandle<JS::Value> ret);
 
 nsresult
--- a/toolkit/components/telemetry/gen-histogram-data.py
+++ b/toolkit/components/telemetry/gen-histogram-data.py
@@ -10,42 +10,66 @@ from shared_telemetry_utils import Strin
 
 import sys
 import histogram_tools
 import itertools
 
 banner = """/* This file is auto-generated, see gen-histogram-data.py.  */
 """
 
-def print_array_entry(output, histogram, name_index, exp_index):
+def print_array_entry(output, histogram, name_index, exp_index, label_index, label_count):
     cpp_guard = histogram.cpp_guard()
     if cpp_guard:
         print("#if defined(%s)" % cpp_guard, file=output)
-    print("  { %s, %s, %s, %s, %d, %d, %s, %s }," \
-        % (histogram.low(), histogram.high(),
-           histogram.n_buckets(), histogram.nsITelemetry_kind(),
-           name_index, exp_index, histogram.dataset(),
+    print("  { %s, %s, %s, %s, %d, %d, %s, %d, %d, %s }," \
+        % (histogram.low(),
+           histogram.high(),
+           histogram.n_buckets(),
+           histogram.nsITelemetry_kind(),
+           name_index,
+           exp_index,
+           histogram.dataset(),
+           label_index,
+           label_count,
            "true" if histogram.keyed() else "false"), file=output)
     if cpp_guard:
         print("#endif", file=output)
 
 def write_histogram_table(output, histograms):
-    table = StringTable()
+    string_table = StringTable()
+    label_table = []
+    label_count = 0
 
     print("const HistogramInfo gHistograms[] = {", file=output)
     for histogram in histograms:
-        name_index = table.stringIndex(histogram.name())
-        exp_index = table.stringIndex(histogram.expiration())
-        print_array_entry(output, histogram, name_index, exp_index)
+        name_index = string_table.stringIndex(histogram.name())
+        exp_index = string_table.stringIndex(histogram.expiration())
+
+        labels = histogram.labels()
+        label_index = 0
+        if len(labels) > 0:
+            label_index = label_count
+            label_table.append((histogram.name(), string_table.stringIndexes(labels)))
+            label_count += len(labels)
+
+        print_array_entry(output, histogram,
+                          name_index, exp_index,
+                          label_index, len(labels))
+    print("};\n", file=output)
+
+    strtab_name = "gHistogramStringTable"
+    string_table.writeDefinition(output, strtab_name)
+    static_assert(output, "sizeof(%s) <= UINT32_MAX" % strtab_name,
+                  "index overflow")
+
+    print("\nconst uint32_t gHistogramLabelTable[] = {", file=output)
+    for name,indexes in label_table:
+        print("/* %s */ %s," % (name, ", ".join(map(str, indexes))), file=output)
     print("};", file=output)
 
-    strtab_name = "gHistogramStringTable"
-    table.writeDefinition(output, strtab_name)
-    static_assert(output, "sizeof(%s) <= UINT32_MAX" % strtab_name,
-                  "index overflow")
 
 # Write out static asserts for histogram data.  We'd prefer to perform
 # these checks in this script itself, but since several histograms
 # (generally enumerated histograms) use compile-time constants for
 # their upper bounds, we have to let the compiler do the checking.
 
 def static_asserts_for_boolean(output, histogram):
     pass
@@ -84,16 +108,17 @@ def write_histogram_static_asserts(outpu
 // compile time, so that incorrect histogram definitions
 // give compile-time errors, not runtime errors.""", file=output)
 
     table = {
         'boolean' : static_asserts_for_boolean,
         'flag' : static_asserts_for_flag,
         'count': static_asserts_for_count,
         'enumerated' : static_asserts_for_enumerated,
+        'categorical' : static_asserts_for_enumerated,
         'linear' : static_asserts_for_linear,
         'exponential' : static_asserts_for_exponential,
         }
 
     for histogram in histograms:
         histogram_tools.table_dispatch(histogram.kind(), table,
                                        lambda f: f(output, histogram))
 
--- a/toolkit/components/telemetry/gen-histogram-enum.py
+++ b/toolkit/components/telemetry/gen-histogram-enum.py
@@ -16,31 +16,47 @@ from __future__ import print_function
 
 import histogram_tools
 import itertools
 import sys
 
 banner = """/* This file is auto-generated, see gen-histogram-enum.py.  */
 """
 
+header = """
+#ifndef mozilla_TelemetryHistogramEnums_h
+#define mozilla_TelemetryHistogramEnums_h
+
+#include "mozilla/TemplateLib.h"
+
+namespace mozilla {
+namespace Telemetry {
+"""
+
+footer = """
+} // namespace mozilla
+} // namespace Telemetry
+#endif // mozilla_TelemetryHistogramEnums_h"""
+
 def main(output, *filenames):
+    # Print header.
     print(banner, file=output)
-    print("#ifndef mozilla_TelemetryHistogramEnums_h", file=output);
-    print("#define mozilla_TelemetryHistogramEnums_h", file=output);
-    print("namespace mozilla {", file=output)
-    print("namespace Telemetry {", file=output)
-    print("enum ID : uint32_t {", file=output)
+    print(header, file=output)
 
-    groups = itertools.groupby(histogram_tools.from_files(filenames),
+    # Load the histograms.
+    all_histograms = list(histogram_tools.from_files(filenames))
+    groups = itertools.groupby(all_histograms,
                                lambda h: h.name().startswith("USE_COUNTER2_"))
-    seen_use_counters = False
 
+    # Print the histogram enums.
     # Note that histogram_tools.py guarantees that all of the USE_COUNTER2_*
     # histograms are defined in a contiguous block.  We therefore assume
     # that there's at most one group for which use_counter_group is true.
+    print("enum ID : uint32_t {", file=output)
+    seen_use_counters = False
     for (use_counter_group, histograms) in groups:
         if use_counter_group:
             seen_use_counters = True
 
         # The HistogramDUMMY* enum variables are used to make the computation
         # of Histogram{First,Last}UseCounter easier.  Otherwise, we'd have to
         # special case the first and last histogram in the group.
         if use_counter_group:
@@ -62,14 +78,30 @@ def main(output, *filenames):
     print("  HistogramCount,", file=output)
     if seen_use_counters:
         print("  HistogramUseCounterCount = HistogramLastUseCounter - HistogramFirstUseCounter + 1", file=output)
     else:
         print("  HistogramFirstUseCounter = 0,", file=output)
         print("  HistogramLastUseCounter = 0,", file=output)
         print("  HistogramUseCounterCount = 0", file=output)
     print("};", file=output)
-    print("} // namespace mozilla", file=output)
-    print("} // namespace Telemetry", file=output)
-    print("#endif // mozilla_TelemetryHistogramEnums_h", file=output);
+
+    # Write categorical label enums.
+    categorical = filter(lambda h: h.kind() == "categorical", all_histograms)
+    enums = [("LABELS_" + h.name(), h.labels(), h.name()) for h in categorical]
+    for name,labels,_ in enums:
+        print("\nenum class %s : uint32_t {" % name, file=output)
+        print("  %s" % ",\n  ".join(labels), file=output)
+        print("};", file=output)
+
+    print("\ntemplate<class T> struct IsCategoricalLabelEnum : FalseType {};", file=output)
+    for name,_,_ in enums:
+        print("template<> struct IsCategoricalLabelEnum<%s> : TrueType {};" % name, file=output)
+
+    print("\ntemplate<class T> struct CategoricalLabelId {};", file=output)
+    for name,_,id in enums:
+        print("template<> struct CategoricalLabelId<%s> : IntegralConstant<uint32_t, %s> {};" % (name, id), file=output)
+
+    # Footer.
+    print(footer, file=output)
 
 if __name__ == '__main__':
     main(sys.stdout, *sys.argv[1:])
--- a/toolkit/components/telemetry/histogram_tools.py
+++ b/toolkit/components/telemetry/histogram_tools.py
@@ -4,16 +4,20 @@
 
 import collections
 import json
 import math
 import os
 import re
 import sys
 
+# Constants.
+MAX_LABEL_LENGTH = 20
+MAX_LABEL_COUNT = 100
+
 # histogram_tools.py is used by scripts from a mozilla-central build tree
 # and also by outside consumers, such as the telemetry server.  We need
 # to ensure that importing things works in both contexts.  Therefore,
 # unconditionally importing things that are local to the build tree, such
 # as buildconfig, is a no-no.
 try:
     import buildconfig
 
@@ -102,23 +106,27 @@ symbol that should guard C/C++ definitio
         self._is_use_counter = name.startswith("USE_COUNTER2_")
         self.verify_attributes(name, definition)
         self._name = name
         self._description = definition['description']
         self._kind = definition['kind']
         self._cpp_guard = definition.get('cpp_guard')
         self._keyed = definition.get('keyed', False)
         self._expiration = definition.get('expires_in_version')
+        self._labels = definition.get('labels', [])
         self.compute_bucket_parameters(definition)
-        table = { 'boolean': 'BOOLEAN',
-                  'flag': 'FLAG',
-                  'count': 'COUNT',
-                  'enumerated': 'LINEAR',
-                  'linear': 'LINEAR',
-                  'exponential': 'EXPONENTIAL' }
+        table = {
+            'boolean': 'BOOLEAN',
+            'flag': 'FLAG',
+            'count': 'COUNT',
+            'enumerated': 'LINEAR',
+            'categorical': 'CATEGORICAL',
+            'linear': 'LINEAR',
+            'exponential': 'EXPONENTIAL',
+        }
         table_dispatch(self.kind(), table,
                        lambda k: self._set_nsITelemetry_kind(k))
         datasets = { 'opt-in': 'DATASET_RELEASE_CHANNEL_OPTIN',
                      'opt-out': 'DATASET_RELEASE_CHANNEL_OPTOUT' }
         value = definition.get('releaseChannelCollection', 'opt-in')
         if not value in datasets:
             raise DefinitionException, "unknown release channel collection policy for " + name
         self._dataset = "nsITelemetry::" + datasets[value]
@@ -128,17 +136,18 @@ symbol that should guard C/C++ definitio
         return self._name
 
     def description(self):
         """Return the description of the histogram."""
         return self._description
 
     def kind(self):
         """Return the kind of the histogram.
-Will be one of 'boolean', 'flag', 'count', 'enumerated', 'linear', or 'exponential'."""
+Will be one of 'boolean', 'flag', 'count', 'enumerated', 'categorical', 'linear',
+or 'exponential'."""
         return self._kind
 
     def expiration(self):
         """Return the expiration version of the histogram."""
         return self._expiration
 
     def nsITelemetry_kind(self):
         """Return the nsITelemetry constant corresponding to the kind of
@@ -168,123 +177,161 @@ associated with the histogram.  Returns 
     def keyed(self):
         """Returns True if this a keyed histogram, false otherwise."""
         return self._keyed
 
     def dataset(self):
         """Returns the dataset this histogram belongs into."""
         return self._dataset
 
+    def labels(self):
+        """Returns a list of labels for a categorical histogram, [] for others."""
+        return self._labels
+
     def ranges(self):
         """Return an array of lower bounds for each bucket in the histogram."""
-        table = { 'boolean': linear_buckets,
-                  'flag': linear_buckets,
-                  'count': linear_buckets,
-                  'enumerated': linear_buckets,
-                  'linear': linear_buckets,
-                  'exponential': exponential_buckets }
+        table = {
+            'boolean': linear_buckets,
+            'flag': linear_buckets,
+            'count': linear_buckets,
+            'enumerated': linear_buckets,
+            'categorical': linear_buckets,
+            'linear': linear_buckets,
+            'exponential': exponential_buckets,
+        }
         return table_dispatch(self.kind(), table,
                               lambda p: p(self.low(), self.high(), self.n_buckets()))
 
     def compute_bucket_parameters(self, definition):
         table = {
             'boolean': Histogram.boolean_flag_bucket_parameters,
             'flag': Histogram.boolean_flag_bucket_parameters,
             'count': Histogram.boolean_flag_bucket_parameters,
             'enumerated': Histogram.enumerated_bucket_parameters,
+            'categorical': Histogram.categorical_bucket_parameters,
             'linear': Histogram.linear_bucket_parameters,
-            'exponential': Histogram.exponential_bucket_parameters
-            }
+            'exponential': Histogram.exponential_bucket_parameters,
+        }
         table_dispatch(self.kind(), table,
                        lambda p: self.set_bucket_parameters(*p(definition)))
 
     def verify_attributes(self, name, definition):
         global always_allowed_keys
         general_keys = always_allowed_keys + ['low', 'high', 'n_buckets']
 
         table = {
             'boolean': always_allowed_keys,
             'flag': always_allowed_keys,
             'count': always_allowed_keys,
             'enumerated': always_allowed_keys + ['n_values'],
+            'categorical': always_allowed_keys + ['labels'],
             'linear': general_keys,
-            'exponential': general_keys
-            }
+            'exponential': general_keys,
+        }
         # We removed extended_statistics_ok on the client, but the server-side,
         # where _strict_type_checks==False, has to deal with historical data.
         if not self._strict_type_checks:
             table['exponential'].append('extended_statistics_ok')
 
         table_dispatch(definition['kind'], table,
                        lambda allowed_keys: Histogram.check_keys(name, definition, allowed_keys))
 
-        # Check for the alert_emails field. Use counters don't have any mechanism
-        # to add them, so skip the check for them.
-        if not self._is_use_counter:
-            if 'alert_emails' not in definition:
-                if whitelists is not None and name not in whitelists['alert_emails']:
-                    raise KeyError, 'New histogram "%s" must have an alert_emails field.' % name
-            elif not isinstance(definition['alert_emails'], list):
-                raise KeyError, 'alert_emails must be an array (in histogram "%s")' % name
+        self.check_name(name)
+        self.check_field_types(name, definition)
+        self.check_whitelistable_fields(name, definition)
+        self.check_expiration(name, definition)
+        self.check_label_values(name, definition)
 
-        Histogram.check_name(name)
-        self.check_field_types(name, definition)
-        Histogram.check_expiration(name, definition)
-        self.check_bug_numbers(name, definition)
-
-    @staticmethod
-    def check_name(name):
+    def check_name(self, name):
         if '#' in name:
             raise ValueError, '"#" not permitted for %s' % (name)
 
-    @staticmethod
-    def check_expiration(name, definition):
+        # Avoid C++ identifier conflicts between histogram enums and label enum names.
+        if name.startswith("LABELS_"):
+            raise ValueError, "Histogram name '%s' can not start with LABELS_" % (name)
+
+        # To make it easier to generate C++ identifiers from this etc., we restrict
+        # the histogram names to a strict pattern.
+        # We skip this on the server to avoid failures with old Histogram.json revisions.
+        if self._strict_type_checks:
+            pattern = '^[a-z][a-z0-9_]+[a-z0-9]$'
+            if not re.match(pattern, name, re.IGNORECASE):
+                raise ValueError, "Histogram name '%s' doesn't confirm to '%s'" % (name, pattern)
+
+    def check_expiration(self, name, definition):
         expiration = definition.get('expires_in_version')
 
         if not expiration:
             return
 
         if re.match(r'^[1-9][0-9]*$', expiration):
             expiration = expiration + ".0a1"
         elif re.match(r'^[1-9][0-9]*\.0$', expiration):
             expiration = expiration + "a1"
 
         definition['expires_in_version'] = expiration
 
-    def check_bug_numbers(self, name, definition):
-        # Use counters don't have any mechanism to add the bug numbers field.
+    def check_label_values(self, name, definition):
+        labels = definition.get('labels')
+        if not labels:
+            return
+
+        invalid = filter(lambda l: len(l) > MAX_LABEL_LENGTH, labels)
+        if len(invalid) > 0:
+            raise ValueError, 'Label values for %s exceed length limit of %d: %s' % \
+                              (name, MAX_LABEL_LENGTH, ', '.join(invalid))
+
+        if len(labels) > MAX_LABEL_COUNT:
+            raise ValueError, 'Label count for %s exceeds limit of %d' % \
+                              (name, MAX_LABEL_COUNT)
+
+        # To make it easier to generate C++ identifiers from this etc., we restrict
+        # the label values to a strict pattern.
+        pattern = '^[a-z][a-z0-9_]+[a-z0-9]$'
+        invalid = filter(lambda l: not re.match(pattern, l, re.IGNORECASE), labels)
+        if len(invalid) > 0:
+            raise ValueError, 'Label values for %s are not matching pattern "%s": %s' % \
+                              (name, pattern, ', '.join(invalid))
+
+    # Check for the presence of fields that old histograms are whitelisted for.
+    def check_whitelistable_fields(self, name, definition):
+        # Use counters don't have any mechanism to add the fields checked here,
+        # so skip the check for them.
         if self._is_use_counter:
             return
-        bug_numbers = definition.get('bug_numbers')
-        if not bug_numbers:
-            if whitelists is None or name in whitelists['bug_numbers']:
-                return
-            else:
-                raise KeyError, 'New histogram "%s" must have a bug_numbers field.' % name
 
-        if not isinstance(bug_numbers, list):
-            raise ValueError, 'bug_numbers field for "%s" should be an array' % (name)
+        # In the pipeline we don't have whitelists available.
+        if whitelists is None:
+            return
 
-        if not all(type(num) is int for num in bug_numbers):
-            raise ValueError, 'bug_numbers array for "%s" should only contain integers' % (name)
+        for field in ['alert_emails', 'bug_numbers']:
+            if field not in definition and name not in whitelists[field]:
+                raise KeyError, 'New histogram "%s" must have a %s field.' % (name, field)
 
     def check_field_types(self, name, definition):
         # Define expected types for the histogram properties.
         type_checked_fields = {
-                "n_buckets": int,
-                "n_values": int,
-                "low": int,
-                "high": int,
-                "keyed": bool,
-                "expires_in_version": basestring,
-                "kind": basestring,
-                "description": basestring,
-                "cpp_guard": basestring,
-                "releaseChannelCollection": basestring
-            }
+            "n_buckets": int,
+            "n_values": int,
+            "low": int,
+            "high": int,
+            "keyed": bool,
+            "expires_in_version": basestring,
+            "kind": basestring,
+            "description": basestring,
+            "cpp_guard": basestring,
+            "releaseChannelCollection": basestring,
+        }
+
+        # For list fields we check the items types.
+        type_checked_list_fields = {
+            "bug_numbers": int,
+            "alert_emails": basestring,
+            "labels": basestring,
+        }
 
         # For the server-side, where _strict_type_checks==False, we want to
         # skip the stricter type checks for these fields for dealing with
         # historical data.
         coerce_fields = ["low", "high", "n_values", "n_buckets"]
         if not self._strict_type_checks:
             def try_to_coerce_to_number(v):
                 try:
@@ -292,26 +339,34 @@ associated with the histogram.  Returns 
                 except:
                     return v
             for key in [k for k in coerce_fields if k in definition]:
                 definition[key] = try_to_coerce_to_number(definition[key])
             # This handles old "keyed":"true" definitions (bug 1271986).
             if definition.get("keyed", None) == "true":
                 definition["keyed"] = True
 
+        def nice_type_name(t):
+            if t is basestring:
+                return "string"
+            return t.__name__
+
         for key, key_type in type_checked_fields.iteritems():
             if not key in definition:
                 continue
             if not isinstance(definition[key], key_type):
-                if key_type is basestring:
-                    type_name = "string"
-                else:
-                    type_name = key_type.__name__
                 raise ValueError, ('value for key "{0}" in Histogram "{1}" '
-                        'should be {2}').format(key, name, type_name)
+                        'should be {2}').format(key, name, nice_type_name(key_type))
+
+        for key, key_type in type_checked_list_fields.iteritems():
+            if not key in definition:
+                continue
+            if not all(isinstance(x, key_type) for x in definition[key]):
+                raise ValueError, ('all values for list "{0}" in Histogram "{1}" '
+                        'should be {2}').format(key, name, nice_type_name(key_type))
 
     @staticmethod
     def check_keys(name, definition, allowed_keys):
         for key in definition.iterkeys():
             if key not in allowed_keys:
                 raise KeyError, '%s not permitted for %s' % (key, name)
 
     def set_bucket_parameters(self, low, high, n_buckets):
@@ -335,16 +390,21 @@ associated with the histogram.  Returns 
                 definition['n_buckets'])
 
     @staticmethod
     def enumerated_bucket_parameters(definition):
         n_values = definition['n_values']
         return (1, n_values, n_values + 1)
 
     @staticmethod
+    def categorical_bucket_parameters(definition):
+        n_values = len(definition['labels'])
+        return (1, n_values, n_values + 1)
+
+    @staticmethod
     def exponential_bucket_parameters(definition):
         return (definition.get('low', 1),
                 definition['high'],
                 definition['n_buckets'])
 
 # We support generating histograms from multiple different input files, not
 # just Histograms.json.  For each file's basename, we have a specific
 # routine to parse that file, and return a dictionary mapping histogram
--- a/toolkit/components/telemetry/nsITelemetry.idl
+++ b/toolkit/components/telemetry/nsITelemetry.idl
@@ -17,22 +17,24 @@ interface nsITelemetry : nsISupports
 {
   /**
    * Histogram types:
    * HISTOGRAM_EXPONENTIAL - buckets increase exponentially
    * HISTOGRAM_LINEAR - buckets increase linearly
    * HISTOGRAM_BOOLEAN - For storing 0/1 values
    * HISTOGRAM_FLAG - For storing a single value; its count is always == 1.
    * HISTOGRAM_COUNT - For storing counter values without bucketing.
+   * HISTOGRAM_CATEGORICAL - For storing enumerated values by label.
    */
   const unsigned long HISTOGRAM_EXPONENTIAL = 0;
   const unsigned long HISTOGRAM_LINEAR = 1;
   const unsigned long HISTOGRAM_BOOLEAN = 2;
   const unsigned long HISTOGRAM_FLAG = 3;
   const unsigned long HISTOGRAM_COUNT = 4;
+  const unsigned long HISTOGRAM_CATEGORICAL = 5;
 
   /**
    * Scalar types:
    * SCALAR_COUNT - for storing a numeric value
    * SCALAR_STRING - for storing a string value
    * SCALAR_BOOLEAN - for storing a boolean value
    */
   const unsigned long SCALAR_COUNT = 0;
--- a/toolkit/components/telemetry/shared_telemetry_utils.py
+++ b/toolkit/components/telemetry/shared_telemetry_utils.py
@@ -30,42 +30,59 @@ class StringTable:
         if string in self.table:
             return self.table[string]
         else:
             result = self.current_index
             self.table[string] = result
             self.current_index += self.c_strlen(string)
             return result
 
+    def stringIndexes(self, strings):
+        """ Returns a list of indexes for the provided list of strings.
+        Adds the strings to the table if they are not in it yet.
+        :param strings: list of strings to put into the table.
+        """
+        return [self.stringIndex(s) for s in strings]
+
     def writeDefinition(self, f, name):
         """Writes the string table to a file as a C const char array.
+
+        This writes out the string table as one single C char array for memory
+        size reasons, separating the individual strings with '\0' characters.
+        This way we can index directly into the string array and avoid the additional
+        storage costs for the pointers to them (and potential extra relocations for those).
+
         :param f: the output stream.
         :param name: the name of the output array.
         """
         entries = self.table.items()
         entries.sort(key=lambda x:x[1])
+
         # Avoid null-in-string warnings with GCC and potentially
         # overlong string constants; write everything out the long way.
         def explodeToCharArray(string):
             def toCChar(s):
                 if s == "'":
                     return "'\\''"
                 else:
                     return "'%s'" % s
             return ", ".join(map(toCChar, string))
+
         f.write("const char %s[] = {\n" % name)
-        for (string, offset) in entries[:-1]:
+        for (string, offset) in entries:
+            if "*/" in string:
+                raise ValueError, "String in string table contains unexpected sequence '*/': %s" % string
+
             e = explodeToCharArray(string)
             if e:
-                f.write("  /* %5d */ %s, '\\0',\n"
-                        % (offset, explodeToCharArray(string)))
+                f.write("  /* %5d - \"%s\" */ %s, '\\0',\n"
+                        % (offset, string, explodeToCharArray(string)))
             else:
-                f.write("  /* %5d */ '\\0',\n" % offset)
-        f.write("  /* %5d */ %s, '\\0' };\n\n"
-                % (entries[-1][1], explodeToCharArray(entries[-1][0])))
+                f.write("  /* %5d - \"%s\" */ '\\0',\n" % (offset, string))
+        f.write("};\n\n")
 
 def static_assert(output, expression, message):
     """Writes a C++ compile-time assertion expression to a file.
     :param output: the output stream.
     :param expression: the expression to check.
     :param message: the string literal that will appear if the expression evaluates to
         false.
     """
--- a/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js
+++ b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js
@@ -1,35 +1,67 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const INT_MAX = 0x7FFFFFFF;
 
 Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
 
-function test_expired_histogram() {
-  var histogram_id = "FOOBAR";
-  var test_expired_id = "TELEMETRY_TEST_EXPIRED";
-  var clone_id = "ExpiredClone";
-  var dummy = Telemetry.newHistogram(histogram_id, "28.0a1", Telemetry.HISTOGRAM_EXPONENTIAL, 1, 2, 3);
-  var dummy_clone = Telemetry.histogramFrom(clone_id, test_expired_id);
-  var rh = Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []);
-  Assert.ok(!!rh);
+// Return an array of numbers from lower up to, excluding, upper
+function numberRange(lower, upper)
+{
+  let a = [];
+  for (let i=lower; i<upper; ++i) {
+    a.push(i);
+  }
+  return a;
+}
 
-  dummy.add(1);
-  dummy_clone.add(1);
-
-  do_check_eq(Telemetry.histogramSnapshots["__expired__"], undefined);
-  do_check_eq(Telemetry.histogramSnapshots[histogram_id], undefined);
-  do_check_eq(Telemetry.histogramSnapshots[test_expired_id], undefined);
-  do_check_eq(Telemetry.histogramSnapshots[clone_id], undefined);
-  do_check_eq(rh[test_expired_id], undefined);
+function expect_fail(f) {
+  let failed = false;
+  try {
+    f();
+    failed = false;
+  } catch (e) {
+    failed = true;
+  }
+  do_check_true(failed);
 }
 
-function test_histogram(histogram_type, name, min, max, bucket_count) {
+function expect_success(f) {
+  let succeeded = false;
+  try {
+    f();
+    succeeded = true;
+  } catch (e) {
+    succeeded = false;
+  }
+  do_check_true(succeeded);
+}
+
+function compareHistograms(h1, h2) {
+  let s1 = h1.snapshot();
+  let s2 = h2.snapshot();
+
+  do_check_eq(s1.histogram_type, s2.histogram_type);
+  do_check_eq(s1.min, s2.min);
+  do_check_eq(s1.max, s2.max);
+  do_check_eq(s1.sum, s2.sum);
+
+  do_check_eq(s1.counts.length, s2.counts.length);
+  for (let i = 0; i < s1.counts.length; i++)
+    do_check_eq(s1.counts[i], s2.counts[i]);
+
+  do_check_eq(s1.ranges.length, s2.ranges.length);
+  for (let i = 0; i < s1.ranges.length; i++)
+    do_check_eq(s1.ranges[i], s2.ranges[i]);
+}
+
+function check_histogram(histogram_type, name, min, max, bucket_count) {
   var h = Telemetry.newHistogram(name, "never", histogram_type, min, max, bucket_count);
   var r = h.snapshot().ranges;
   var sum = 0;
   for(let i=0;i<r.length;i++) {
     var v = r[i];
     sum += v;
     h.add(v);
   }
@@ -65,40 +97,56 @@ function test_histogram(histogram_type, 
 
   h.add(0);
   h.add(1);
   var c = h.snapshot().counts;
   do_check_eq(c[0], 1);
   do_check_eq(c[1], 1);
 }
 
-function expect_fail(f) {
-  let failed = false;
-  try {
-    f();
-    failed = false;
-  } catch (e) {
-    failed = true;
-  }
-  do_check_true(failed);
-}
+// This MUST be the very first test of this file.
+add_task({
+  skip_if: () => gIsAndroid
+},
+function* test_instantiate() {
+  const ID = "TELEMETRY_TEST_COUNT";
+  let h = Telemetry.getHistogramById(ID);
+
+  // Instantiate the subsession histogram through |add| and make sure they match.
+  // This MUST be the first use of "TELEMETRY_TEST_COUNT" in this file, otherwise
+  // |add| will not instantiate the histogram.
+  h.add(1);
+  let snapshot = h.snapshot();
+  let subsession = Telemetry.snapshotSubsessionHistograms();
+  Assert.equal(snapshot.sum, subsession[ID].sum,
+               "Histogram and subsession histogram sum must match.");
+  // Clear the histogram, so we don't void the assumptions from the other tests.
+  h.clear();
+});
 
-function expect_success(f) {
-  let succeeded = false;
-  try {
-    f();
-    succeeded = true;
-  } catch (e) {
-    succeeded = false;
+add_task(function* test_parameterChecks() {
+  let kinds = [Telemetry.HISTOGRAM_EXPONENTIAL, Telemetry.HISTOGRAM_LINEAR]
+  for (let histogram_type of kinds) {
+    let [min, max, bucket_count] = [1, INT_MAX - 1, 10]
+    check_histogram(histogram_type, "test::"+histogram_type, min, max, bucket_count);
+
+    const nh = Telemetry.newHistogram;
+    expect_fail(() => nh("test::min", "never", histogram_type, 0, max, bucket_count));
+    expect_fail(() => nh("test::bucket_count", "never", histogram_type, min, max, 1));
   }
-  do_check_true(succeeded);
-}
+});
 
-function test_boolean_histogram()
-{
+add_task(function* test_noSerialization() {
+  // Instantiate the storage for this histogram and make sure it doesn't
+  // get reflected into JS, as it has no interesting data in it.
+  Telemetry.getHistogramById("NEWTAB_PAGE_PINNED_SITES_COUNT");
+  do_check_false("NEWTAB_PAGE_PINNED_SITES_COUNT" in Telemetry.histogramSnapshots);
+});
+
+add_task(function* test_boolean_histogram() {
   var h = Telemetry.newHistogram("test::boolean histogram", "never", Telemetry.HISTOGRAM_BOOLEAN);
   var r = h.snapshot().ranges;
   // boolean histograms ignore numeric parameters
   do_check_eq(uneval(r), uneval([0, 1, 2]))
   var sum = 0
   for(var i=0;i<r.length;i++) {
     var v = r[i];
     sum += v;
@@ -107,20 +155,19 @@ function test_boolean_histogram()
   h.add(true);
   h.add(false);
   var s = h.snapshot();
   do_check_eq(s.histogram_type, Telemetry.HISTOGRAM_BOOLEAN);
   // last bucket should always be 0 since .add parameters are normalized to either 0 or 1
   do_check_eq(s.counts[2], 0);
   do_check_eq(s.sum, 3);
   do_check_eq(s.counts[0], 2);
-}
+});
 
-function test_flag_histogram()
-{
+add_task(function* test_flag_histogram() {
   var h = Telemetry.newHistogram("test::flag histogram", "never", Telemetry.HISTOGRAM_FLAG);
   var r = h.snapshot().ranges;
   // Flag histograms ignore numeric parameters.
   do_check_eq(uneval(r), uneval([0, 1, 2]));
   // Should already have a 0 counted.
   var c = h.snapshot().counts;
   var s = h.snapshot().sum;
   do_check_eq(uneval(c), uneval([1, 0, 0]));
@@ -133,68 +180,78 @@ function test_flag_histogram()
   do_check_eq(s2, 1);
   // Should only switch counts once.
   h.add(1);
   var c3 = h.snapshot().counts;
   var s3 = h.snapshot().sum;
   do_check_eq(uneval(c3), uneval([0, 1, 0]));
   do_check_eq(s3, 1);
   do_check_eq(h.snapshot().histogram_type, Telemetry.HISTOGRAM_FLAG);
-}
+});
 
-function test_count_histogram()
-{
+add_task(function* test_count_histogram() {
   let h = Telemetry.newHistogram("test::count histogram", "never", Telemetry.HISTOGRAM_COUNT, 1, 2, 3);
   let s = h.snapshot();
   do_check_eq(uneval(s.ranges), uneval([0, 1, 2]));
   do_check_eq(uneval(s.counts), uneval([0, 0, 0]));
   do_check_eq(s.sum, 0);
   h.add();
   s = h.snapshot();
   do_check_eq(uneval(s.counts), uneval([1, 0, 0]));
   do_check_eq(s.sum, 1);
   h.add();
   s = h.snapshot();
   do_check_eq(uneval(s.counts), uneval([2, 0, 0]));
   do_check_eq(s.sum, 2);
-}
+});
+
+add_task(function* test_categorical_histogram()
+{
+  let h1 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL");
+  for (let v of ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1]) {
+    h1.add(v);
+  }
+  for (let s of ["", "Label4", "1234"]) {
+    Assert.throws(() => h1.add(s));
+  }
 
-function test_getHistogramById() {
+  let snapshot = h1.snapshot();
+  Assert.equal(snapshot.sum, 6);
+  Assert.deepEqual(snapshot.ranges, [0, 1, 2, 3]);
+  Assert.deepEqual(snapshot.counts, [3, 2, 2, 0]);
+
+  let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL_OPTOUT");
+  for (let v of ["CommonLabel", "CommonLabel", "Label4", "Label5", "Label6", 0, 1]) {
+    h2.add(v);
+  }
+  for (let s of ["", "Label3", "1234"]) {
+    Assert.throws(() => h2.add(s));
+  }
+
+  snapshot = h2.snapshot();
+  Assert.equal(snapshot.sum, 7);
+  Assert.deepEqual(snapshot.ranges, [0, 1, 2, 3, 4]);
+  Assert.deepEqual(snapshot.counts, [3, 2, 1, 1, 0]);
+});
+
+add_task(function* test_getHistogramById() {
   try {
     Telemetry.getHistogramById("nonexistent");
     do_throw("This can't happen");
   } catch (e) {
 
   }
   var h = Telemetry.getHistogramById("CYCLE_COLLECTOR");
   var s = h.snapshot();
   do_check_eq(s.histogram_type, Telemetry.HISTOGRAM_EXPONENTIAL);
   do_check_eq(s.min, 1);
   do_check_eq(s.max, 10000);
-}
-
-function compareHistograms(h1, h2) {
-  let s1 = h1.snapshot();
-  let s2 = h2.snapshot();
-
-  do_check_eq(s1.histogram_type, s2.histogram_type);
-  do_check_eq(s1.min, s2.min);
-  do_check_eq(s1.max, s2.max);
-  do_check_eq(s1.sum, s2.sum);
+});
 
-  do_check_eq(s1.counts.length, s2.counts.length);
-  for (let i = 0; i < s1.counts.length; i++)
-    do_check_eq(s1.counts[i], s2.counts[i]);
-
-  do_check_eq(s1.ranges.length, s2.ranges.length);
-  for (let i = 0; i < s1.ranges.length; i++)
-    do_check_eq(s1.ranges[i], s2.ranges[i]);
-}
-
-function test_histogramFrom() {
+add_task(function* test_histogramFrom() {
   // Test one histogram of each type.
   let names = [
       "CYCLE_COLLECTOR",      // EXPONENTIAL
       "GC_REASON_2",          // LINEAR
       "GC_RESET",             // BOOLEAN
       "TELEMETRY_TEST_FLAG",  // FLAG
       "TELEMETRY_TEST_COUNT", // COUNT
   ];
@@ -211,31 +268,97 @@ function test_histogramFrom() {
   let testFlag = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
   testFlag.add(1);
   let testCount = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
   testCount.add();
   let clone = Telemetry.histogramFrom("FlagClone", "TELEMETRY_TEST_FLAG");
   compareHistograms(testFlag, clone);
   clone = Telemetry.histogramFrom("CountClone", "TELEMETRY_TEST_COUNT");
   compareHistograms(testCount, clone);
-}
+});
 
-function test_getSlowSQL() {
+add_task(function* test_getSlowSQL() {
   var slow = Telemetry.slowSQL;
   do_check_true(("mainThread" in slow) && ("otherThreads" in slow));
-}
+});
 
-function test_getWebrtc() {
+add_task(function* test_getWebrtc() {
   var webrtc = Telemetry.webrtcStats;
   do_check_true("IceCandidatesStats" in webrtc);
   var icestats = webrtc.IceCandidatesStats;
   do_check_true(("webrtc" in icestats) && ("loop" in icestats));
-}
+});
+
+// Check that telemetry doesn't record in private mode
+add_task(function* test_privateMode() {
+  var h = Telemetry.newHistogram("test::private_mode_boolean", "never", Telemetry.HISTOGRAM_BOOLEAN);
+  var orig = h.snapshot();
+  Telemetry.canRecordExtended = false;
+  h.add(1);
+  do_check_eq(uneval(orig), uneval(h.snapshot()));
+  Telemetry.canRecordExtended = true;
+  h.add(1);
+  do_check_neq(uneval(orig), uneval(h.snapshot()));
+});
+
+// Check that telemetry records only when it is suppose to.
+add_task(function* test_histogramRecording() {
+  // Check that no histogram is recorded if both base and extended recording are off.
+  Telemetry.canRecordBase = false;
+  Telemetry.canRecordExtended = false;
+
+  let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+  h.clear();
+  let orig = h.snapshot();
+  h.add(1);
+  Assert.equal(orig.sum, h.snapshot().sum);
+
+  // Check that only base histograms are recorded.
+  Telemetry.canRecordBase = true;
+  h.add(1);
+  Assert.equal(orig.sum + 1, h.snapshot().sum,
+               "Histogram value should have incremented by 1 due to recording.");
 
-function test_addons() {
+  // Extended histograms should not be recorded.
+  h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN");
+  orig = h.snapshot();
+  h.add(1);
+  Assert.equal(orig.sum, h.snapshot().sum,
+               "Histograms should be equal after recording.");
+
+  // Runtime created histograms should not be recorded.
+  h = Telemetry.newHistogram("test::runtime_created_boolean", "never", Telemetry.HISTOGRAM_BOOLEAN);
+  orig = h.snapshot();
+  h.add(1);
+  Assert.equal(orig.sum, h.snapshot().sum,
+               "Histograms should be equal after recording.");
+
+  // Check that extended histograms are recorded when required.
+  Telemetry.canRecordExtended = true;
+
+  h.add(1);
+  Assert.equal(orig.sum + 1, h.snapshot().sum,
+               "Runtime histogram value should have incremented by 1 due to recording.");
+
+  h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN");
+  orig = h.snapshot();
+  h.add(1);
+  Assert.equal(orig.sum + 1, h.snapshot().sum,
+               "Histogram value should have incremented by 1 due to recording.");
+
+  // Check that base histograms are still being recorded.
+  h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+  h.clear();
+  orig = h.snapshot();
+  h.add(1);
+  Assert.equal(orig.sum + 1, h.snapshot().sum,
+               "Histogram value should have incremented by 1 due to recording.");
+});
+
+add_task(function* test_addons() {
   var addon_id = "testing-addon";
   var fake_addon_id = "fake-addon";
   var name1 = "testing-histogram1";
   var register = Telemetry.registerAddonHistogram;
   expect_success(() =>
                  register(addon_id, name1, Telemetry.HISTOGRAM_LINEAR, 1, 5, 6));
   // Can't register the same histogram multiple times.
   expect_fail(() =>
@@ -311,96 +434,60 @@ function test_addons() {
   do_check_false(name2 in snapshots[flag_addon]);
 
   // Check that we can remove addon histograms.
   Telemetry.unregisterAddonHistograms(addon_id);
   snapshots = Telemetry.addonHistogramSnapshots;
   do_check_false(addon_id in snapshots);
   // Make sure other addons are unaffected.
   do_check_true(extra_addon in snapshots);
-}
-
-// Check that telemetry doesn't record in private mode
-function test_privateMode() {
-  var h = Telemetry.newHistogram("test::private_mode_boolean", "never", Telemetry.HISTOGRAM_BOOLEAN);
-  var orig = h.snapshot();
-  Telemetry.canRecordExtended = false;
-  h.add(1);
-  do_check_eq(uneval(orig), uneval(h.snapshot()));
-  Telemetry.canRecordExtended = true;
-  h.add(1);
-  do_check_neq(uneval(orig), uneval(h.snapshot()));
-}
+});
 
-// Check that telemetry records only when it is suppose to.
-function test_histogramRecording() {
-  // Check that no histogram is recorded if both base and extended recording are off.
-  Telemetry.canRecordBase = false;
-  Telemetry.canRecordExtended = false;
-
-  let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
-  h.clear();
-  let orig = h.snapshot();
-  h.add(1);
-  Assert.equal(orig.sum, h.snapshot().sum);
+add_task(function* test_expired_histogram() {
+  var histogram_id = "FOOBAR";
+  var test_expired_id = "TELEMETRY_TEST_EXPIRED";
+  var clone_id = "ExpiredClone";
+  var dummy = Telemetry.newHistogram(histogram_id, "28.0a1", Telemetry.HISTOGRAM_EXPONENTIAL, 1, 2, 3);
+  var dummy_clone = Telemetry.histogramFrom(clone_id, test_expired_id);
+  var rh = Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []);
+  Assert.ok(!!rh);
 
-  // Check that only base histograms are recorded.
-  Telemetry.canRecordBase = true;
-  h.add(1);
-  Assert.equal(orig.sum + 1, h.snapshot().sum,
-               "Histogram value should have incremented by 1 due to recording.");
+  dummy.add(1);
+  dummy_clone.add(1);
 
-  // Extended histograms should not be recorded.
-  h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN");
-  orig = h.snapshot();
-  h.add(1);
-  Assert.equal(orig.sum, h.snapshot().sum,
-               "Histograms should be equal after recording.");
+  do_check_eq(Telemetry.histogramSnapshots["__expired__"], undefined);
+  do_check_eq(Telemetry.histogramSnapshots[histogram_id], undefined);
+  do_check_eq(Telemetry.histogramSnapshots[test_expired_id], undefined);
+  do_check_eq(Telemetry.histogramSnapshots[clone_id], undefined);
+  do_check_eq(rh[test_expired_id], undefined);
+});
 
-  // Runtime created histograms should not be recorded.
-  h = Telemetry.newHistogram("test::runtime_created_boolean", "never", Telemetry.HISTOGRAM_BOOLEAN);
-  orig = h.snapshot();
-  h.add(1);
-  Assert.equal(orig.sum, h.snapshot().sum,
-               "Histograms should be equal after recording.");
-
-  // Check that extended histograms are recorded when required.
-  Telemetry.canRecordExtended = true;
+add_task(function* test_keyed_histogram() {
+  // Check that invalid names get rejected.
 
-  h.add(1);
-  Assert.equal(orig.sum + 1, h.snapshot().sum,
-               "Runtime histogram value should have incremented by 1 due to recording.");
-
-  h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN");
-  orig = h.snapshot();
-  h.add(1);
-  Assert.equal(orig.sum + 1, h.snapshot().sum,
-               "Histogram value should have incremented by 1 due to recording.");
+  let threw = false;
+  try {
+    Telemetry.newKeyedHistogram("test::invalid # histogram", "never", Telemetry.HISTOGRAM_BOOLEAN);
+  } catch (e) {
+    // This should throw as we reject names with the # separator
+    threw = true;
+  }
+  Assert.ok(threw, "newKeyedHistogram should have thrown");
 
-  // Check that base histograms are still being recorded.
-  h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
-  h.clear();
-  orig = h.snapshot();
-  h.add(1);
-  Assert.equal(orig.sum + 1, h.snapshot().sum,
-               "Histogram value should have incremented by 1 due to recording.");
-}
+  threw = false;
+  try {
+    Telemetry.getKeyedHistogramById("test::unknown histogram", "never", Telemetry.HISTOGRAM_BOOLEAN);
+  } catch (e) {
+    // This should throw as it is an unknown ID
+    threw = true;
+  }
+  Assert.ok(threw, "getKeyedHistogramById should have thrown");
+});
 
-// Return an array of numbers from lower up to, excluding, upper
-function numberRange(lower, upper)
-{
-  let a = [];
-  for (let i=lower; i<upper; ++i) {
-    a.push(i);
-  }
-  return a;
-}
-
-function test_keyed_boolean_histogram()
-{
+add_task(function* test_keyed_boolean_histogram() {
   const KEYED_ID = "test::keyed::boolean";
   let KEYS = numberRange(0, 2).map(i => "key" + (i + 1));
   KEYS.push("漢語");
   let histogramBase = {
     "min": 1,
     "max": 2,
     "histogram_type": 2,
     "sum": 1,
@@ -436,20 +523,19 @@ function test_keyed_boolean_histogram()
   Assert.deepEqual(h.snapshot(), testSnapShot);
 
   let allSnapshots = Telemetry.keyedHistogramSnapshots;
   Assert.deepEqual(allSnapshots[KEYED_ID], testSnapShot);
 
   h.clear();
   Assert.deepEqual(h.keys(), []);
   Assert.deepEqual(h.snapshot(), {});
-}
+});
 
-function test_keyed_count_histogram()
-{
+add_task(function* test_keyed_count_histogram() {
   const KEYED_ID = "test::keyed::count";
   const KEYS = numberRange(0, 5).map(i => "key" + (i + 1));
   let histogramBase = {
     "min": 1,
     "max": 2,
     "histogram_type": 4,
     "sum": 0,
     "ranges": [0, 1, 2],
@@ -492,20 +578,19 @@ function test_keyed_count_histogram()
   Assert.deepEqual(h.snapshot(), testSnapShot);
 
   let allSnapshots = Telemetry.keyedHistogramSnapshots;
   Assert.deepEqual(allSnapshots[KEYED_ID], testSnapShot);
 
   h.clear();
   Assert.deepEqual(h.keys(), []);
   Assert.deepEqual(h.snapshot(), {});
-}
+});
 
-function test_keyed_flag_histogram()
-{
+add_task(function* test_keyed_flag_histogram() {
   const KEYED_ID = "test::keyed::flag";
   let h = Telemetry.newKeyedHistogram(KEYED_ID, "never", Telemetry.HISTOGRAM_FLAG);
 
   const KEY = "default";
   h.add(KEY, true);
 
   let testSnapshot = {};
   testSnapshot[KEY] = {
@@ -521,19 +606,19 @@ function test_keyed_flag_histogram()
   Assert.deepEqual(h.snapshot(), testSnapshot);
 
   let allSnapshots = Telemetry.keyedHistogramSnapshots;
   Assert.deepEqual(allSnapshots[KEYED_ID], testSnapshot);
 
   h.clear();
   Assert.deepEqual(h.keys(), []);
   Assert.deepEqual(h.snapshot(), {});
-}
+});
 
-function test_keyed_histogram_recording() {
+add_task(function* test_keyed_histogram_recording() {
   // Check that no histogram is recorded if both base and extended recording are off.
   Telemetry.canRecordBase = false;
   Telemetry.canRecordExtended = false;
 
   const TEST_KEY = "record_foo";
   let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
   h.clear();
   h.add(TEST_KEY, 1);
@@ -571,19 +656,19 @@ function test_keyed_histogram_recording(
   Assert.equal(h.snapshot(TEST_KEY).sum, 1,
                "The keyed histogram should record the correct value.");
 
   // Check that base histograms are still being recorded.
   h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
   h.clear();
   h.add(TEST_KEY, 1);
   Assert.equal(h.snapshot(TEST_KEY).sum, 1);
-}
+});
 
-function test_histogram_recording_enabled() {
+add_task(function* test_histogram_recording_enabled() {
   Telemetry.canRecordBase = true;
   Telemetry.canRecordExtended = true;
 
   // Check that a "normal" histogram respects recording-enabled on/off
   var h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
   var orig = h.snapshot();
 
   h.add(1);
@@ -620,20 +705,19 @@ function test_histogram_recording_enable
   Assert.equal(orig.sum + 1, h.snapshot().sum,
                "When recording is enabled add should record.");
 
   // Restore to disabled
   Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT_INIT_NO_RECORD", false);
   h.add(1);
   Assert.equal(orig.sum + 1, h.snapshot().sum,
                "When recording is disabled add should not record.");
+});
 
-}
-
-function test_keyed_histogram_recording_enabled() {
+add_task(function* test_keyed_histogram_recording_enabled() {
   Telemetry.canRecordBase = true;
   Telemetry.canRecordExtended = true;
 
   // Check RecordingEnabled for keyed histograms which are recording by default
   const TEST_KEY = "record_foo";
   let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
 
   h.clear();
@@ -665,49 +749,19 @@ function test_keyed_histogram_recording_
   Assert.equal(h.snapshot(TEST_KEY).sum, 1,
     "Keyed histogram add should record when recording is enabled");
 
   // Restore to disabled
   Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD", false);
   h.add(TEST_KEY, 1);
   Assert.equal(h.snapshot(TEST_KEY).sum, 1,
     "Keyed histogram add should not record when recording is disabled");
-}
-
-function test_keyed_histogram() {
-  // Check that invalid names get rejected.
-
-  let threw = false;
-  try {
-    Telemetry.newKeyedHistogram("test::invalid # histogram", "never", Telemetry.HISTOGRAM_BOOLEAN);
-  } catch (e) {
-    // This should throw as we reject names with the # separator
-    threw = true;
-  }
-  Assert.ok(threw, "newKeyedHistogram should have thrown");
+});
 
-  threw = false;
-  try {
-    Telemetry.getKeyedHistogramById("test::unknown histogram", "never", Telemetry.HISTOGRAM_BOOLEAN);
-  } catch (e) {
-    // This should throw as it is an unknown ID
-    threw = true;
-  }
-  Assert.ok(threw, "getKeyedHistogramById should have thrown");
-
-  // Check specific keyed histogram types working properly.
-
-  test_keyed_boolean_histogram();
-  test_keyed_count_histogram();
-  test_keyed_flag_histogram();
-  test_keyed_histogram_recording();
-}
-
-function test_datasets()
-{
+add_task(function* test_datasets() {
   // Check that datasets work as expected.
 
   const RELEASE_CHANNEL_OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT;
   const RELEASE_CHANNEL_OPTIN  = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN;
 
   // Histograms should default to the extended dataset
   let h = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
   Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTIN);
@@ -740,45 +794,22 @@ function test_datasets()
   registered = Telemetry.registeredKeyedHistograms(RELEASE_CHANNEL_OPTIN, []);
   registered = new Set(registered);
   Assert.ok(registered.has("TELEMETRY_TEST_KEYED_FLAG"));
   Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"));
   registered = Telemetry.registeredKeyedHistograms(RELEASE_CHANNEL_OPTOUT, []);
   registered = new Set(registered);
   Assert.ok(!registered.has("TELEMETRY_TEST_KEYED_FLAG"));
   Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"));
-}
-
-function test_instantiate() {
-  if (gIsAndroid) {
-    // We don't support subsessions yet on Android.
-    return;
-  }
-
-  const ID = "TELEMETRY_TEST_COUNT";
-  let h = Telemetry.getHistogramById(ID);
+});
 
-  // Instantiate the subsession histogram through |add| and make sure they match.
-  // This MUST be the first use of "TELEMETRY_TEST_COUNT" in this file, otherwise
-  // |add| will not instantiate the histogram.
-  h.add(1);
-  let snapshot = h.snapshot();
-  let subsession = Telemetry.snapshotSubsessionHistograms();
-  Assert.equal(snapshot.sum, subsession[ID].sum,
-               "Histogram and subsession histogram sum must match.");
-  // Clear the histogram, so we don't void the assumptions from the other tests.
-  h.clear();
-}
-
-function test_subsession() {
-  if (gIsAndroid) {
-    // We don't support subsessions yet on Android.
-    return;
-  }
-
+add_task({
+  skip_if: () => gIsAndroid
+},
+function* test_subsession() {
   const ID = "TELEMETRY_TEST_COUNT";
   const FLAG = "TELEMETRY_TEST_FLAG";
   let h = Telemetry.getHistogramById(ID);
   let flag = Telemetry.getHistogramById(FLAG);
 
   // Both original and duplicate should start out the same.
   h.clear();
   let snapshot = Telemetry.histogramSnapshots;
@@ -849,24 +880,22 @@ function test_subsession() {
   Assert.ok(ID in snapshot);
   Assert.ok(ID in subsession);
   Assert.ok(FLAG in snapshot);
   Assert.ok(FLAG in subsession);
   Assert.equal(snapshot[ID].sum, 1);
   Assert.equal(subsession[ID].sum, 0);
   Assert.equal(snapshot[FLAG].sum, 1);
   Assert.equal(subsession[FLAG].sum, 0);
-}
+});
 
-function test_keyed_subsession() {
-  if (gIsAndroid) {
-    // We don't support subsessions yet on Android.
-    return;
-  }
-
+add_task({
+  skip_if: () => gIsAndroid
+},
+function* test_keyed_subsession() {
   let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG");
   const KEY = "foo";
 
   // Both original and subsession should start out the same.
   h.clear();
   Assert.ok(!(KEY in h.snapshot()));
   Assert.ok(!(KEY in h.subsessionSnapshot()));
   Assert.equal(h.snapshot(KEY).sum, 0);
@@ -899,54 +928,9 @@ function test_keyed_subsession() {
   Assert.ok(KEY in snapshot);
   Assert.ok(KEY in subsession);
   Assert.equal(snapshot[KEY].sum, 1);
   Assert.equal(subsession[KEY].sum, 1);
 
   subsession = h.subsessionSnapshot();
   Assert.ok(!(KEY in subsession));
   Assert.equal(h.subsessionSnapshot(KEY).sum, 0);
-}
-
-function generateUUID() {
-  let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
-  // strip {}
-  return str.substring(1, str.length - 1);
-}
-
-function run_test()
-{
-  // This MUST be the very first test of this file.
-  test_instantiate();
-
-  let kinds = [Telemetry.HISTOGRAM_EXPONENTIAL, Telemetry.HISTOGRAM_LINEAR]
-  for (let histogram_type of kinds) {
-    let [min, max, bucket_count] = [1, INT_MAX - 1, 10]
-    test_histogram(histogram_type, "test::"+histogram_type, min, max, bucket_count);
-
-    const nh = Telemetry.newHistogram;
-    expect_fail(() => nh("test::min", "never", histogram_type, 0, max, bucket_count));
-    expect_fail(() => nh("test::bucket_count", "never", histogram_type, min, max, 1));
-  }
-
-  // Instantiate the storage for this histogram and make sure it doesn't
-  // get reflected into JS, as it has no interesting data in it.
-  let h = Telemetry.getHistogramById("NEWTAB_PAGE_PINNED_SITES_COUNT");
-  do_check_false("NEWTAB_PAGE_PINNED_SITES_COUNT" in Telemetry.histogramSnapshots);
-
-  test_boolean_histogram();
-  test_flag_histogram();
-  test_count_histogram();
-  test_getHistogramById();
-  test_histogramFrom();
-  test_getSlowSQL();
-  test_getWebrtc();
-  test_privateMode();
-  test_histogramRecording();
-  test_addons();
-  test_expired_histogram();
-  test_keyed_histogram();
-  test_datasets();
-  test_subsession();
-  test_keyed_subsession();
-  test_histogram_recording_enabled();
-  test_keyed_histogram_recording_enabled();
-}
+});