Merge m-c to inbound, a=merge CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Tue, 22 Mar 2016 16:57:35 -0700
changeset 289895 eb528d042c851c297f40543d63dfb8d1ed5361ce
parent 289871 f5ee47a13b2d08676f82e3f5b62fb84c6430b199 (current diff)
parent 289894 3381aa98edf72e02b9d6b4db6efa0865063a2329 (diff)
child 289896 f66ffb0d7984b2673cd57cca1d618bdf29d88a09
push id74027
push userkwierso@gmail.com
push dateTue, 22 Mar 2016 23:57:42 +0000
treeherdermozilla-inbound@eb528d042c85 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound, a=merge CLOSED TREE MozReview-Commit-ID: KCev5FqbsuD
build/moz.configure/init.configure
build/moz.configure/old.configure
devtools/client/performance/modules/logic/marker-formatters.js
mobile/android/config/tooltool-manifests/android-armv6/releng.manifest
--- a/build.gradle
+++ b/build.gradle
@@ -4,28 +4,32 @@ allprojects {
     // Expose the per-object-directory configuration to all projects.
     ext {
         mozconfig = gradle.mozconfig
         topsrcdir = gradle.mozconfig.topsrcdir
         topobjdir = gradle.mozconfig.topobjdir
     }
 
     repositories {
-        maven {
-            url gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORY
+        if (gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORY) {
+            maven {
+                url gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORY
+            }
         }
     }
 }
 
 buildDir "${topobjdir}/gradle/build"
 
 buildscript {
     repositories {
-        maven {
-            url gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORY
+        if (gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORY) {
+            maven {
+                url gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORY
+            }
         }
         // For android-sdk-manager SNAPSHOT releases.
         maven {
             url "file://${gradle.mozconfig.topsrcdir}/mobile/android/gradle/m2repo"
         }
     }
 
     dependencies {
--- a/build/docs/toolchains.rst
+++ b/build/docs/toolchains.rst
@@ -46,8 +46,63 @@ 2. Select ``Programming Languages`` -> `
 3. Under ``Windows and Web Development`` uncheck everything except
    ``Universal Windows App Development Tools`` and the items under it
    (should be ``Tools (1.2)...`` and the ``Windows 10 SDK``).
 
 Once Visual Studio 2015 Community has been installed, from a checkout
 of mozilla-central, run the following to produce a ZIP archive::
 
    $ ./mach python build/windows_toolchain.py create-zip vs2015.zip
+
+Firefox for Android with Gradle
+===============================
+
+To build Firefox for Android with Gradle in automation, archives
+containing both the Gradle executable and a Maven repository
+comprising the exact build dependencies are produced and uploaded to
+an internal Mozilla server.  The build automation will download,
+verify, and extract these archive before building.  These archives
+provide a self-contained Gradle and Maven repository so that machines
+don't need to fetch additional Maven dependencies at build time.
+(Gradle and the downloaded Maven dependencies can be both
+redistributed publicly.)
+
+Archiving the Gradle executable is straight-forward, but archiving a
+local Maven repository is not.  Therefore a special Task Cluster
+Docker image and job exist for producing the required archives.  The
+Docker image definition is rooted in
+``taskcluster/docker/android-gradle-build``.  The Task Cluster job
+definition is in
+``testing/taskcluster/tasks/builds/android_api_15_gradle_dependencies.yml``.
+The job runs in a container based on the custom Docker image and
+spawns a Sonatype Nexus proxying Maven repository process in the
+background.  The job builds Firefox for Android using Gradle and the
+in-tree Gradle configuration rooted at ``build.gradle``.  The spawned
+proxying Maven repository downloads external dependencies and collects
+them.  After the Gradle build completes, the job archives the Gradle
+version used to build, and the downloaded Maven repository, and
+exposes them as Task Cluster artifacts.
+
+Here is `an example try job fetching these dependencies
+<https://treeherder.mozilla.org/#/jobs?repo=try&revision=75bc98935147&selectedJob=17793653>`_.
+The resulting task produced a `Gradle archive
+<https://queue.taskcluster.net/v1/task/CeYMgAP3Q-KF8h37nMhJjg/runs/0/artifacts/public%2Fbuild%2Fgradle.tar.xz>`_
+and a `Maven repository archive
+<https://queue.taskcluster.net/v1/task/CeYMgAP3Q-KF8h37nMhJjg/runs/0/artifacts/public%2Fbuild%2Fjcentral.tar.xz>`_.
+These archives were then uploaded (manually) to Mozilla automation
+using tooltool for consumption in Gradle builds.
+
+To update the version of Gradle in the archive produced, update
+``gradle/wrapper/gradle-wrapper.properties``.  Be sure to also update
+the SHA256 checksum to prevent poisoning the build machines!
+
+To update the versions of Gradle dependencies used, update
+``dependencies`` sections in the in-tree Gradle configuration rooted
+at ``build.gradle``.  Once you are confident your changes build
+locally, push a fresh try build with an invocation like::
+
+   $ hg push-to-try -m "try: -b o -p android-api-15-gradle-dependencies"
+
+Then `upload your archives to tooltool
+<https://wiki.mozilla.org/ReleaseEngineering/Applications/Tooltool#How_To_Upload_To_Tooltool>`_,
+update the in-tree manifests in
+``mobile/android/config/tooltool-manifests``, and push a fresh try
+build.
--- a/build/moz.configure/init.configure
+++ b/build/moz.configure/init.configure
@@ -259,21 +259,25 @@ def wanted_mozconfig_variables(help):
          'AUTOCONF',
          'AWK',
          'DISABLE_EXPORT_JS',
          'DISABLE_SHARED_JS',
          'DOXYGEN',
          'DSYMUTIL',
          'EXTERNAL_SOURCE_DIR',
          'GENISOIMAGE',
+         'GRADLE',
+         'GRADLE_FLAGS',
+         'GRADLE_MAVEN_REPOSITORY',
          'JS_STANDALONE',
          'L10NBASEDIR',
          'MOZILLABUILD',
          'MOZ_ARTIFACT_BUILDS',
          'MOZ_BUILD_APP',
+         'MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE',
          'MOZ_CALLGRIND',
          'MOZ_DMD',
          'MOZ_FMP4',
          'MOZ_INSTRUMENT_EVENT_LOOP',
          'MOZ_INSTRUMENTS',
          'MOZ_JPROF',
          'MOZ_PROFILING',
          'MOZ_USE_SYSTRACE',
--- a/build/moz.configure/old.configure
+++ b/build/moz.configure/old.configure
@@ -318,17 +318,16 @@ def old_configure_options(*options):
     '--with-doc-include-dirs',
     '--with-doc-input-dirs',
     '--with-doc-output-dir',
     '--with-float-abi',
     '--with-fpu',
     '--with-gonk-toolchain-prefix',
     '--with-google-api-keyfile',
     '--with-google-oauth-api-keyfile',
-    '--with-gradle',
     '--with-intl-api',
     '--with-ios-sdk',
     '--with-java-bin-path',
     '--with-jitreport-granularity',
     '--with-linux-headers',
     '--with-macbundlename-prefix',
     '--with-macos-private-frameworks',
     '--with-macos-sdk',
--- a/config/android-common.mk
+++ b/config/android-common.mk
@@ -19,18 +19,25 @@ ifdef MOZ_SIGN_CMD
 RELEASE_JARSIGNER := $(MOZ_SIGN_CMD) -f jar
 else
 RELEASE_JARSIGNER := $(DEBUG_JARSIGNER)
 endif
 
 # $(1) is the full path to input:  foo-debug-unsigned-unaligned.apk.
 # $(2) is the full path to output: foo.apk.
 # Use this like: $(call RELEASE_SIGN_ANDROID_APK,foo-debug-unsigned-unaligned.apk,foo.apk)
+#
+# The |zip -d| there to handle re-signing previously signed APKs.  Gradle
+# produces signed, unaligned APK files, but this expects unsigned, unaligned
+# APK files.  The |zip -d| discards any existing signature, turning a signed,
+# unaligned APK into an unsigned, unaligned APK.  Sadly |zip -q| doesn't
+# silence a warning about "nothing to do" so we pipe to /dev/null.
 RELEASE_SIGN_ANDROID_APK = \
   cp $(1) $(2)-unaligned.apk && \
+  ($(ZIP) -d $(2)-unaligned.apk 'META-INF/*' > /dev/null || true) && \
   $(RELEASE_JARSIGNER) $(2)-unaligned.apk && \
   $(ZIPALIGN) -f -v 4 $(2)-unaligned.apk $(2) && \
   $(RM) $(2)-unaligned.apk
 
 # For Android, we default to 1.7
 ifndef JAVA_VERSION
   JAVA_VERSION = 1.7
 endif
--- a/devtools/client/animationinspector/animation-controller.js
+++ b/devtools/client/animationinspector/animation-controller.js
@@ -18,18 +18,20 @@ Cu.import("resource://gre/modules/Task.j
 var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm");
 Cu.import("resource://gre/modules/Console.jsm");
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 
 loader.lazyRequireGetter(this, "promise");
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 loader.lazyRequireGetter(this, "AnimationsFront", "devtools/server/actors/animation", true);
 
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+
 const STRINGS_URI = "chrome://devtools/locale/animationinspector.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
 
 // Global toolbox/inspector, set when startup is called.
 var gToolbox, gInspector;
 
 /**
  * Startup the animationinspector controller and view, called by the sidebar
  * widget when loading/unloading the iframe into the tab.
  */
--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -5,18 +5,20 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Cu} = require("chrome");
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
 
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+
 const STRINGS_URI = "chrome://devtools/locale/animationinspector.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
 
 /**
  * UI component responsible for displaying a single animation timeline, which
  * basically looks like a rectangle that shows the delay and iterations.
  */
 function AnimationTimeBlock() {
   EventEmitter.decorate(this);
   this.onClick = this.onClick.bind(this);
--- a/devtools/client/animationinspector/components/rate-selector.js
+++ b/devtools/client/animationinspector/components/rate-selector.js
@@ -4,18 +4,21 @@
  * 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 {Cu} = require("chrome");
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 const {createNode} = require("devtools/client/animationinspector/utils");
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+
 const STRINGS_URI = "chrome://devtools/locale/animationinspector.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
+
 // List of playback rate presets displayed in the timeline toolbar.
 const PLAYBACK_RATES = [.1, .25, .5, 1, 2, 5, 10];
 
 /**
  * UI component responsible for displaying a playback rate selector UI.
  * The rendering logic is such that a predefined list of rates is generated.
  * If *all* animations passed to render share the same rate, then that rate is
  * selected in the <select> element, otherwise, the empty value is selected.
--- a/devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js
+++ b/devtools/client/animationinspector/test/browser_animation_empty_on_invalid_nodes.js
@@ -2,18 +2,20 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 requestLongerTimeout(2);
 
 // Test that the panel shows no animation data for invalid or not animated nodes
 
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+
 const STRINGS_URI = "chrome://devtools/locale/animationinspector.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
 
 add_task(function*() {
   yield addTab(URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel, window} = yield openAnimationInspector();
   let {document} = window;
 
   info("Select node .still and check that the panel is empty");
   let stillNode = yield getNodeFront(".still", inspector);
--- a/devtools/client/animationinspector/test/browser_animation_running_on_compositor.js
+++ b/devtools/client/animationinspector/test/browser_animation_running_on_compositor.js
@@ -4,18 +4,20 @@
 
 "use strict";
 
 requestLongerTimeout(2);
 
 // Test that when animations displayed in the timeline are running on the
 // compositor, they get a special icon and information in the tooltip.
 
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+
 const STRINGS_URI = "chrome://devtools/locale/animationinspector.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
 
 add_task(function*() {
   yield addTab(URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel} = yield openAnimationInspector();
   let timeline = panel.animationsTimelineComponent;
 
   info("Select a test node we know has an animation running on the compositor");
   yield selectNodeAndWaitForAnimations(".animated", inspector);
--- a/devtools/client/animationinspector/utils.js
+++ b/devtools/client/animationinspector/utils.js
@@ -6,18 +6,21 @@
 
 "use strict";
 
 const {Cu} = require("chrome");
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 var {loader} = Cu.import("resource://devtools/shared/Loader.jsm");
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+
 const STRINGS_URI = "chrome://devtools/locale/animationinspector.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
+
 // How many times, maximum, can we loop before we find the optimal time
 // interval in the timeline graph.
 const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
 // Time graduations should be multiple of one of these number.
 const OPTIMAL_TIME_INTERVAL_MULTIPLES = [1, 2.5, 5];
 
 // RGB color for the time interval background.
 const TIME_INTERVAL_COLOR = [128, 136, 144];
--- a/devtools/client/canvasdebugger/canvasdebugger.js
+++ b/devtools/client/canvasdebugger/canvasdebugger.js
@@ -12,16 +12,17 @@ Cu.import("resource://gre/modules/Consol
 
 const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const promise = require("promise");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
 const { CallWatcherFront } = require("devtools/server/actors/call-watcher");
 const { CanvasFront } = require("devtools/server/actors/canvas");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 
 const CANVAS_ACTOR_RECORDING_ATTEMPT = DevToolsUtils.testing ? 500 : 5000;
 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
   "resource://gre/modules/PluralForm.jsm");
@@ -170,18 +171,18 @@ var EventsHandler = {
 
     window.emit(EVENTS.UI_RESET);
   }
 };
 
 /**
  * Localization convenience methods.
  */
-var L10N = new ViewHelpers.L10N(STRINGS_URI);
-var SHARED_L10N = new ViewHelpers.L10N(SHARED_STRINGS_URI);
+var L10N = new LocalizationHelper(STRINGS_URI);
+var SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
 
 /**
  * Convenient way of emitting events from the panel window.
  */
 EventEmitter.decorate(this);
 
 /**
  * DOM query helpers.
--- a/devtools/client/debugger/debugger-controller.js
+++ b/devtools/client/debugger/debugger-controller.js
@@ -100,21 +100,16 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://devtools/shared/event-emitter.js");
 Cu.import("resource://devtools/client/shared/widgets/SimpleListWidget.jsm");
 Cu.import("resource://devtools/client/shared/widgets/BreadcrumbsWidget.jsm");
 Cu.import("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
 Cu.import("resource://devtools/client/shared/widgets/VariablesView.jsm");
 Cu.import("resource://devtools/client/shared/widgets/VariablesViewController.jsm");
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 
-/**
- * Localization convenience methods.
- */
-var L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);
-
 Cu.import("resource://devtools/client/shared/browser-loader.js");
 const { require } = BrowserLoader({
   baseURI: "resource://devtools/client/debugger/",
   window,
 });
 XPCOMUtils.defineConstant(this, "require", require);
 const { gDevTools } = require("devtools/client/framework/devtools");
 
@@ -146,16 +141,18 @@ var Services = require("Services");
 var {TargetFactory} = require("devtools/client/framework/target");
 var {Toolbox} = require("devtools/client/framework/toolbox");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 var promise = require("devtools/shared/deprecated-sync-thenables");
 var Editor = require("devtools/client/sourceeditor/editor");
 var DebuggerEditor = require("devtools/client/sourceeditor/debugger");
 var {Tooltip} = require("devtools/client/shared/widgets/Tooltip");
 var FastListWidget = require("devtools/client/shared/widgets/FastListWidget");
+var {LocalizationHelper} = require("devtools/client/shared/l10n");
+var {PrefsHelper} = require("devtools/client/shared/prefs");
 
 XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
   "resource://devtools/shared/Parser.jsm");
@@ -170,16 +167,21 @@ Object.defineProperty(this, "NetworkHelp
   get: function() {
     return require("devtools/shared/webconsole/network-helper");
   },
   configurable: true,
   enumerable: true
 });
 
 /**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(DBG_STRINGS_URI);
+
+/**
  * Object defining the debugger controller components.
  */
 var DebuggerController = {
   /**
    * Initializes the debugger controller.
    */
   initialize: function() {
     dumpn("Initializing the DebuggerController");
@@ -1190,17 +1192,17 @@ StackFrames.prototype = {
     this.currentFrameDepth = -1;
     return this._onFrames();
   }
 };
 
 /**
  * Shortcuts for accessing various debugger preferences.
  */
-var Prefs = new ViewHelpers.Prefs("devtools", {
+var Prefs = new PrefsHelper("devtools", {
   workersAndSourcesWidth: ["Int", "debugger.ui.panes-workers-and-sources-width"],
   instrumentsWidth: ["Int", "debugger.ui.panes-instruments-width"],
   panesVisibleOnStartup: ["Bool", "debugger.ui.panes-visible-on-startup"],
   variablesSortingEnabled: ["Bool", "debugger.ui.variables-sorting-enabled"],
   variablesOnlyEnumVisible: ["Bool", "debugger.ui.variables-only-enum-visible"],
   variablesSearchboxVisible: ["Bool", "debugger.ui.variables-searchbox-visible"],
   pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"],
   ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"],
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -13,24 +13,23 @@ var { loader, require } = Cu.import("res
 // devtools-browser is special as it loads main module
 // To be cleaned up in bug 1247203.
 require("devtools/client/framework/devtools-browser");
 var { gDevTools } = require("devtools/client/framework/devtools");
 var { TargetFactory } = require("devtools/client/framework/target");
 var { Toolbox } = require("devtools/client/framework/toolbox");
 var Services = require("Services");
 var { DebuggerClient } = require("devtools/shared/client/main");
-var { ViewHelpers } =
-  Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm", {});
+var { PrefsHelper } = require("devtools/client/shared/prefs");
 var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
 /**
  * Shortcuts for accessing various debugger preferences.
  */
-var Prefs = new ViewHelpers.Prefs("devtools.debugger", {
+var Prefs = new PrefsHelper("devtools.debugger", {
   chromeDebuggingHost: ["Char", "chrome-debugging-host"],
   chromeDebuggingPort: ["Int", "chrome-debugging-port"]
 });
 
 var gToolbox, gClient;
 
 var connect = Task.async(function*() {
   window.removeEventListener("load", connect);
--- a/devtools/client/inspector/layout/layout.js
+++ b/devtools/client/inspector/layout/layout.js
@@ -5,23 +5,24 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const {InplaceEditor, editableItem} =
       require("devtools/client/shared/inplace-editor");
 const {ReflowFront} = require("devtools/server/actors/layout");
+const {LocalizationHelper} = require("devtools/client/shared/l10n");
 
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Console.jsm");
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 
 const STRINGS_URI = "chrome://devtools/locale/shared.properties";
-const SHARED_L10N = new ViewHelpers.L10N(STRINGS_URI);
+const SHARED_L10N = new LocalizationHelper(STRINGS_URI);
 const NUMERIC = /^-?[\d\.]+$/;
 const LONG_TEXT_ROTATE_LIMIT = 3;
 
 /**
  * An instance of EditingSession tracks changes that have been made during the
  * modification of box model values. All of these changes can be reverted by
  * calling revert.
  *
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -42,17 +42,16 @@ support-files =
   lib_jquery_2.1.1_min.js
 
 [browser_markup_anonymous_01.js]
 [browser_markup_anonymous_02.js]
 skip-if = e10s # scratchpad.xul is not loading in e10s window
 [browser_markup_anonymous_03.js]
 [browser_markup_anonymous_04.js]
 [browser_markup_copy_image_data.js]
-skip-if = (e10s && os == 'mac') # bug 1252345
 [browser_markup_css_completion_style_attribute_01.js]
 [browser_markup_css_completion_style_attribute_02.js]
 [browser_markup_dragdrop_autoscroll.js]
 skip-if = e10s && os == 'win'
 [browser_markup_dragdrop_distance.js]
 [browser_markup_dragdrop_draggable.js]
 [browser_markup_dragdrop_dragRootNode.js]
 [browser_markup_dragdrop_escapeKeyPress.js]
@@ -79,17 +78,16 @@ skip-if = e10s && os == 'win'
 [browser_markup_links_05.js]
 [browser_markup_links_06.js]
 [browser_markup_links_07.js]
 [browser_markup_load_01.js]
 [browser_markup_html_edit_01.js]
 [browser_markup_html_edit_02.js]
 [browser_markup_html_edit_03.js]
 [browser_markup_image_tooltip.js]
-skip-if = (e10s && os == 'mac') # bug 1252345
 [browser_markup_image_tooltip_mutations.js]
 [browser_markup_keybindings_01.js]
 [browser_markup_keybindings_02.js]
 [browser_markup_keybindings_03.js]
 [browser_markup_keybindings_04.js]
 [browser_markup_keybindings_delete_attributes.js]
 [browser_markup_keybindings_scrolltonode.js]
 [browser_markup_mutation_01.js]
--- a/devtools/client/inspector/shared/dom-node-preview.js
+++ b/devtools/client/inspector/shared/dom-node-preview.js
@@ -2,19 +2,20 @@
  * 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 {Cu} = require("chrome");
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 const {createNode} = require("devtools/client/animationinspector/utils");
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 
 const STRINGS_URI = "chrome://devtools/locale/inspector.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
 
 /**
  * UI component responsible for displaying a preview of a dom node.
  * @param {InspectorPanel} inspector Requires a reference to the inspector-panel
  * to highlight and select the node, as well as refresh it when there are
  * mutations.
  * @param {Object} options Supported properties are:
  * - compact {Boolean} Defaults to false.
--- a/devtools/client/locales/en-US/markers.properties
+++ b/devtools/client/locales/en-US/markers.properties
@@ -40,54 +40,59 @@ marker.label.unknown=Unknown
 # reasons that can be translated.
 marker.label.javascript.scriptElement=Script Tag
 marker.label.javascript.promiseCallback=Promise Callback
 marker.label.javascript.promiseInit=Promise Init
 marker.label.javascript.workerRunnable=Worker
 marker.label.javascript.jsURI=JavaScript URI
 marker.label.javascript.eventHandler=Event Handler
 
-# LOCALIZATION NOTE (marker.fieldFormat):
-# Some timeline markers come with details, like a size, a name, a js function.
-# %1$S is replaced with one of the above label (marker.label.*) and %2$S
-# with the details. For examples: Paint (200x100), or console.time (FOO)
-marker.fieldFormat=%1$S (%2$S)
-
 # LOCALIZATION NOTE (marker.field.*):
 # Strings used in the waterfall sidebar as property names.
 
 # General marker fields
 marker.field.start=Start:
 marker.field.end=End:
 marker.field.duration=Duration:
+
+# General "reason" for a marker (JavaScript, Garbage Collection)
+marker.field.causeName=Cause:
+# General "type" for a marker (Cycle Collection, Garbage Collection)
+marker.field.type=Type:
+# General "label" for a marker (user defined)
+marker.field.label=Label:
+
 # Field names for stack values
 marker.field.stack=Stack:
 marker.field.startStack=Stack at start:
 marker.field.endStack=Stack at end:
+
 # %S is the "Async Cause" of a marker, and this signifies that the cause
 # was an asynchronous one in a displayed stack.
 marker.field.asyncStack=(Async: %S)
+
 # For console.time markers
 marker.field.consoleTimerName=Timer Name:
+
 # For DOM Event markers
 marker.field.DOMEventType=Event Type:
 marker.field.DOMEventPhase=Phase:
+
 # Non-incremental cause for a Garbage Collection marker
 marker.field.nonIncrementalCause=Non-incremental Cause:
+
 # For "Recalculate Style" markers
 marker.field.restyleHint=Restyle Hint:
-# General "reason" for a marker (JavaScript, Garbage Collection)
-marker.field.causeName=Cause:
-# General "type" for a marker (Cycle Collection, Garbage Collection)
-marker.field.type=Type:
+
 # The type of operation performed by a Worker.
 marker.worker.serializeDataOffMainThread=Serialize data in Worker
 marker.worker.serializeDataOnMainThread=Serialize data on the main thread
 marker.worker.deserializeDataOffMainThread=Deserialize data in Worker
 marker.worker.deserializeDataOnMainThread=Deserialize data on the main thread
+
 # The type of operation performed by a MessagePort
 marker.messagePort.serializeData=Serialize data
 marker.messagePort.deserializeData=Deserialize data
 
 # Strings used in the waterfall sidebar as values.
 marker.value.unknownFrame=<unknown location>
 marker.value.DOMEventTargetPhase=Target
 marker.value.DOMEventCapturingPhase=Capture
--- a/devtools/client/memory/utils.js
+++ b/devtools/client/memory/utils.js
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const { Cu, Cc, Ci } = require("chrome");
 
-Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 const STRINGS_URI = "chrome://devtools/locale/memory.properties"
-const L10N = exports.L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = exports.L10N = new LocalizationHelper(STRINGS_URI);
 
 const { OS } = require("resource://gre/modules/osfile.jsm");
 const { assert } = require("devtools/shared/DevToolsUtils");
 const { Preferences } = require("resource://gre/modules/Preferences.jsm");
 const CUSTOM_CENSUS_DISPLAY_PREF = "devtools.memory.custom-census-displays";
 const CUSTOM_DOMINATOR_TREE_DISPLAY_PREF = "devtools.memory.custom-dominator-tree-displays";
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const {
--- a/devtools/client/netmonitor/har/har-builder.js
+++ b/devtools/client/netmonitor/har/har-builder.js
@@ -1,25 +1,26 @@
 /* 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 { Ci, Cc } = require("chrome");
 const { defer, all } = require("promise");
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 
 loader.lazyImporter(this, "ViewHelpers", "resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 loader.lazyRequireGetter(this, "NetworkHelper", "devtools/shared/webconsole/network-helper");
 
 loader.lazyGetter(this, "appInfo", () => {
   return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
 });
 
 loader.lazyGetter(this, "L10N", () => {
-  return new ViewHelpers.L10N("chrome://devtools/locale/har.properties");
+  return new LocalizationHelper("chrome://devtools/locale/har.properties");
 });
 
 const HAR_VERSION = "1.1";
 
 /**
  * This object is responsible for building HAR file. See HAR spec:
  * https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html
  * http://www.softwareishard.com/blog/har-12-spec/
--- a/devtools/client/netmonitor/netmonitor-view.js
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -17,39 +17,41 @@ XPCOMUtils.defineLazyGetter(this, "HarEx
 
 XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function() {
   return require("devtools/shared/webconsole/network-helper");
 });
 
 const {ToolSidebar} = require("devtools/client/framework/sidebar");
 const {Tooltip} = require("devtools/client/shared/widgets/Tooltip");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const {LocalizationHelper} = require("devtools/client/shared/l10n");
+const {PrefsHelper} = require("devtools/client/shared/prefs");
 
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 
 /**
  * Localization convenience methods.
  */
 const NET_STRINGS_URI = "chrome://devtools/locale/netmonitor.properties";
-var L10N = new ViewHelpers.L10N(NET_STRINGS_URI);
+var L10N = new LocalizationHelper(NET_STRINGS_URI);
 
 // ms
 const WDA_DEFAULT_VERIFY_INTERVAL = 50;
 
 // Use longer timeout during testing as the tests need this process to succeed
 // and two seconds is quite short on slow debug builds. The timeout here should
 // be at least equal to the general mochitest timeout of 45 seconds so that this
 // never gets hit during testing.
 // ms
 const WDA_DEFAULT_GIVE_UP_TIMEOUT = DevToolsUtils.testing ? 45000 : 2000;
 
 /**
  * Shortcuts for accessing various network monitor preferences.
  */
-var Prefs = new ViewHelpers.Prefs("devtools.netmonitor", {
+var Prefs = new PrefsHelper("devtools.netmonitor", {
   networkDetailsWidth: ["Int", "panes-network-details-width"],
   networkDetailsHeight: ["Int", "panes-network-details-height"],
   statistics: ["Bool", "statistics"],
   filters: ["Json", "filters"]
 });
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const EPSILON = 0.001;
--- a/devtools/client/performance/components/jit-optimizations-item.js
+++ b/devtools/client/performance/components/jit-optimizations-item.js
@@ -1,16 +1,18 @@
 /* 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/. */
 
 const { Cu } = require("chrome");
-Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
+
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 const STRINGS_URI = "chrome://devtools/locale/jit-optimizations.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
+
 const { PluralForm } = require("resource://gre/modules/PluralForm.jsm");
 const { DOM: dom, PropTypes, createClass, createFactory } = require("devtools/client/shared/vendor/react");
 const {
   JITOptimizations, hasSuccessfulOutcome, isSuccessfulOutcome
 } = require("devtools/client/performance/modules/logic/jit");
 const Frame = createFactory(require("devtools/client/shared/components/frame"));
 const OPTIMIZATION_FAILURE = L10N.getStr("jit.optimizationFailure");
 const JIT_SAMPLES = L10N.getStr("jit.samples");
--- a/devtools/client/performance/components/jit-optimizations.js
+++ b/devtools/client/performance/components/jit-optimizations.js
@@ -1,16 +1,18 @@
 /* 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/. */
 
 const { Cu } = require("chrome");
-Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
+
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 const STRINGS_URI = "chrome://devtools/locale/jit-optimizations.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
+
 const { assert } = require("devtools/shared/DevToolsUtils");
 const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
 const Tree = createFactory(require("../../shared/components/tree"));
 const OptimizationsItem = createFactory(require("./jit-optimizations-item"));
 const FrameView = createFactory(require("../../shared/components/frame"));
 
 const onClickTooltipString = frame =>
   L10N.getFormatStr("viewsourceindebugger",`${frame.source}:${frame.line}:${frame.column}`);
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/modules/categories.js
@@ -0,0 +1,121 @@
+/* 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 { L10N } = require("devtools/client/performance/modules/global");
+
+/**
+ * Details about each profile pseudo-stack entry cateogry.
+ * @see CATEGORY_MAPPINGS.
+ */
+const CATEGORIES = [{
+  color: "#5e88b0",
+  abbrev: "other",
+  label: L10N.getStr("category.other")
+}, {
+  color: "#46afe3",
+  abbrev: "css",
+  label: L10N.getStr("category.css")
+}, {
+  color: "#d96629",
+  abbrev: "js",
+  label: L10N.getStr("category.js")
+}, {
+  color: "#eb5368",
+  abbrev: "gc",
+  label: L10N.getStr("category.gc")
+}, {
+  color: "#df80ff",
+  abbrev: "network",
+  label: L10N.getStr("category.network")
+}, {
+  color: "#70bf53",
+  abbrev: "graphics",
+  label: L10N.getStr("category.graphics")
+}, {
+  color: "#8fa1b2",
+  abbrev: "storage",
+  label: L10N.getStr("category.storage")
+}, {
+  color: "#d99b28",
+  abbrev: "events",
+  label: L10N.getStr("category.events")
+}, {
+  color: "#8fa1b2",
+  abbrev: "tools",
+  label: L10N.getStr("category.tools")
+}];
+
+/**
+ * Mapping from category bitmasks in the profiler data to additional details.
+ * To be kept in sync with the js::ProfileEntry::Category in ProfilingStack.h
+ */
+const CATEGORY_MAPPINGS = {
+  "16": CATEGORIES[0],    // js::ProfileEntry::Category::OTHER
+  "32": CATEGORIES[1],    // js::ProfileEntry::Category::CSS
+  "64": CATEGORIES[2],    // js::ProfileEntry::Category::JS
+  "128": CATEGORIES[3],   // js::ProfileEntry::Category::GC
+  "256": CATEGORIES[3],   // js::ProfileEntry::Category::CC
+  "512": CATEGORIES[4],   // js::ProfileEntry::Category::NETWORK
+  "1024": CATEGORIES[5],  // js::ProfileEntry::Category::GRAPHICS
+  "2048": CATEGORIES[6],  // js::ProfileEntry::Category::STORAGE
+  "4096": CATEGORIES[7],  // js::ProfileEntry::Category::EVENTS
+
+  // non-bitmasks for specially-assigned categories
+  "9000": CATEGORIES[8],
+};
+
+/**
+ * Get the numeric bitmask (or set of masks) for the given category
+ * abbreviation. See `CATEGORIES` and `CATEGORY_MAPPINGS` above.
+ *
+ * CATEGORY_MASK can be called with just a name if it is expected that the
+ * category is mapped to by exactly one bitmask. If the category is mapped
+ * to by multiple masks, CATEGORY_MASK for that name must be called with
+ * an additional argument specifying the desired id (in ascending order).
+ */
+const [CATEGORY_MASK, CATEGORY_MASK_LIST] = (() => {
+  let bitmasksForCategory = {};
+  let all = Object.keys(CATEGORY_MAPPINGS);
+
+  for (let category of CATEGORIES) {
+    bitmasksForCategory[category.abbrev] = all
+      .filter(mask => CATEGORY_MAPPINGS[mask] == category)
+      .map(mask => +mask)
+      .sort();
+  }
+
+  return [
+    function (name, index) {
+      if (!(name in bitmasksForCategory)) {
+        throw new Error(`Category abbreviation "${name}" does not exist.`);
+      }
+      if (arguments.length == 1) {
+        if (bitmasksForCategory[name].length != 1) {
+          throw new Error(`Expected exactly one category number for "${name}".`);
+        } else {
+          return bitmasksForCategory[name][0];
+        }
+      } else {
+        if (index > bitmasksForCategory[name].length) {
+          throw new Error(`Index "${index}" too high for category "${name}".`);
+        } else {
+          return bitmasksForCategory[name][index - 1];
+        }
+      }
+    },
+
+    function (name) {
+      if (!(name in bitmasksForCategory)) {
+        throw new Error(`Category abbreviation "${name}" does not exist.`);
+      }
+      return bitmasksForCategory[name];
+    }
+  ];
+})();
+
+exports.CATEGORIES = CATEGORIES;
+exports.CATEGORY_MAPPINGS = CATEGORY_MAPPINGS;
+exports.CATEGORY_MASK = CATEGORY_MASK;
+exports.CATEGORY_MASK_LIST = CATEGORY_MASK_LIST;
--- a/devtools/client/performance/modules/global.js
+++ b/devtools/client/performance/modules/global.js
@@ -1,168 +1,36 @@
 /* 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 { ViewHelpers } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
+const { MultiLocalizationHelper } = require("devtools/client/shared/l10n");
+const { PrefsHelper } = require("devtools/client/shared/prefs");
 
 /**
  * Localization convenience methods.
  */
-const L10N = new ViewHelpers.MultiL10N([
+exports.L10N = new MultiLocalizationHelper(
   "chrome://devtools/locale/markers.properties",
   "chrome://devtools/locale/performance.properties"
-]);
+);
 
 /**
  * A list of preferences for this tool. The values automatically update
  * if somebody edits edits about:config or the prefs change somewhere else.
  *
- * This needs to be registered and unregistered when used; the PerformanceController
- * handles this automatically, but if you just use this module in a test independently,
- * ensure you call `registerObserver()` and `unregisterUnobserver()`.
+ * This needs to be registered and unregistered when used for the auto-update
+ * functionality to work. The PerformanceController handles this, but if you
+ * just use this module in a test independently, ensure you call
+ * `registerObserver()` and `unregisterUnobserver()`.
  */
-const PREFS = new ViewHelpers.Prefs("devtools.performance", {
+exports.PREFS = new PrefsHelper("devtools.performance", {
   "show-triggers-for-gc-types": ["Char", "ui.show-triggers-for-gc-types"],
   "show-platform-data": ["Bool", "ui.show-platform-data"],
   "hidden-markers": ["Json", "timeline.hidden-markers"],
   "memory-sample-probability": ["Float", "memory.sample-probability"],
   "memory-max-log-length": ["Int", "memory.max-log-length"],
   "profiler-buffer-size": ["Int", "profiler.buffer-size"],
   "profiler-sample-frequency": ["Int", "profiler.sample-frequency-khz"],
-  // TODO re-enable once we flame charts via bug 1148663
+  // TODO: re-enable once we flame charts via bug 1148663.
   "enable-memory-flame": ["Bool", "ui.enable-memory-flame"],
 });
-
-/**
- * Details about each profile entry cateogry.
- * @see CATEGORY_MAPPINGS.
- */
-const CATEGORIES = [{
-  color: "#5e88b0",
-  abbrev: "other",
-  label: L10N.getStr("category.other")
-}, {
-  color: "#46afe3",
-  abbrev: "css",
-  label: L10N.getStr("category.css")
-}, {
-  color: "#d96629",
-  abbrev: "js",
-  label: L10N.getStr("category.js")
-}, {
-  color: "#eb5368",
-  abbrev: "gc",
-  label: L10N.getStr("category.gc")
-}, {
-  color: "#df80ff",
-  abbrev: "network",
-  label: L10N.getStr("category.network")
-}, {
-  color: "#70bf53",
-  abbrev: "graphics",
-  label: L10N.getStr("category.graphics")
-}, {
-  color: "#8fa1b2",
-  abbrev: "storage",
-  label: L10N.getStr("category.storage")
-}, {
-  color: "#d99b28",
-  abbrev: "events",
-  label: L10N.getStr("category.events")
-}, {
-  color: "#8fa1b2",
-  abbrev: "tools",
-  label: L10N.getStr("category.tools")
-}];
-
-/**
- * Mapping from category bitmasks in the profiler data to additional details.
- * To be kept in sync with the js::ProfileEntry::Category in ProfilingStack.h
- */
-const CATEGORY_MAPPINGS = {
-  "16": CATEGORIES[0],    // js::ProfileEntry::Category::OTHER
-  "32": CATEGORIES[1],    // js::ProfileEntry::Category::CSS
-  "64": CATEGORIES[2],    // js::ProfileEntry::Category::JS
-  "128": CATEGORIES[3],   // js::ProfileEntry::Category::GC
-  "256": CATEGORIES[3],   // js::ProfileEntry::Category::CC
-  "512": CATEGORIES[4],   // js::ProfileEntry::Category::NETWORK
-  "1024": CATEGORIES[5],  // js::ProfileEntry::Category::GRAPHICS
-  "2048": CATEGORIES[6],  // js::ProfileEntry::Category::STORAGE
-  "4096": CATEGORIES[7],  // js::ProfileEntry::Category::EVENTS
-
-  // non-bitmasks for specially-assigned categories
-  "9000": CATEGORIES[8],
-};
-
-/**
- * Get the numeric bitmask (or set of masks) for the given category
- * abbreviation. See CATEGORIES and CATEGORY_MAPPINGS above.
- *
- * CATEGORY_MASK can be called with just a name if it is expected that the
- * category is mapped to by exactly one bitmask.  If the category is mapped
- * to by multiple masks, CATEGORY_MASK for that name must be called with
- * an additional argument specifying the desired id (in ascending order).
- */
-const [CATEGORY_MASK, CATEGORY_MASK_LIST] = (function () {
-  let bitmasksForCategory = {};
-  let all = Object.keys(CATEGORY_MAPPINGS);
-
-  for (let category of CATEGORIES) {
-    bitmasksForCategory[category.abbrev] = all
-      .filter(mask => CATEGORY_MAPPINGS[mask] == category)
-      .map(mask => +mask)
-      .sort();
-  }
-
-  return [
-    function (name, index) {
-      if (!(name in bitmasksForCategory)) {
-        throw new Error(`Category abbreviation "${name}" does not exist.`);
-      }
-      if (arguments.length == 1) {
-        if (bitmasksForCategory[name].length != 1) {
-          throw new Error(`Expected exactly one category number for "${name}".`);
-        } else {
-          return bitmasksForCategory[name][0];
-        }
-      } else {
-        if (index > bitmasksForCategory[name].length) {
-          throw new Error(`Index "${index}" too high for category "${name}".`);
-        } else {
-          return bitmasksForCategory[name][index - 1];
-        }
-      }
-    },
-
-    function (name) {
-      if (!(name in bitmasksForCategory)) {
-        throw new Error(`Category abbreviation "${name}" does not exist.`);
-      }
-      return bitmasksForCategory[name];
-    }
-  ];
-})();
-
-// Human-readable "other" category bitmask. Older Geckos don't have all the
-// necessary instrumentation in the sampling profiler backend for creating
-// a categories graph, in which case we default to the "other" category.
-const CATEGORY_OTHER = CATEGORY_MASK("other");
-
-// Human-readable JIT category bitmask. Certain pseudo-frames in a sample,
-// like "EnterJIT", don't have any associated `category` information.
-const CATEGORY_JIT = CATEGORY_MASK("js");
-
-// Human-readable "devtools" category bitmask. Not emitted from frames themselves,
-// but used manually in the client.
-const CATEGORY_DEVTOOLS = CATEGORY_MASK("tools");
-
-// Exported symbols.
-exports.L10N = L10N;
-exports.PREFS = PREFS;
-exports.CATEGORIES = CATEGORIES;
-exports.CATEGORY_MAPPINGS = CATEGORY_MAPPINGS;
-exports.CATEGORY_MASK = CATEGORY_MASK;
-exports.CATEGORY_MASK_LIST = CATEGORY_MASK_LIST;
-exports.CATEGORY_OTHER = CATEGORY_OTHER;
-exports.CATEGORY_JIT = CATEGORY_JIT;
-exports.CATEGORY_DEVTOOLS = CATEGORY_DEVTOOLS;
--- a/devtools/client/performance/modules/io.js
+++ b/devtools/client/performance/modules/io.js
@@ -1,16 +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 { Cc, Ci, Cu, Cr } = require("chrome");
+const { Cc, Ci } = require("chrome");
 
-const promise = require("promise");
 const RecordingUtils = require("devtools/shared/performance/recording-utils");
 const { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
 const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
 
 // This identifier string is used to tentatively ascertain whether or not
 // a JSON loaded from disk is actually something generated by this tool.
 // It isn't, of course, a definitive verification, but a Good Enoughâ„¢
 // approximation before continuing the import. Don't localize this.
@@ -22,18 +21,18 @@ const PERF_TOOL_SERIALIZER_CURRENT_VERSI
  * Helpers for importing/exporting JSON.
  */
 
 /**
  * Gets a nsIScriptableUnicodeConverter instance with a default UTF-8 charset.
  * @return object
  */
 function getUnicodeConverter () {
-  let className = "@mozilla.org/intl/scriptableunicodeconverter";
-  let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
+  let cname = "@mozilla.org/intl/scriptableunicodeconverter";
+  let converter = Cc[cname].createInstance(Ci.nsIScriptableUnicodeConverter);
   converter.charset = "UTF-8";
   return converter;
 }
 
 /**
  * Saves a recording as JSON to a file. The provided data is assumed to be
  * acyclical, so that it can be properly serialized.
  *
@@ -41,77 +40,84 @@ function getUnicodeConverter () {
  *        The recording data to stream as JSON.
  * @param nsILocalFile file
  *        The file to stream the data into.
  * @return object
  *         A promise that is resolved once streaming finishes, or rejected
  *         if there was an error.
  */
 function saveRecordingToFile (recordingData, file) {
-  let deferred = promise.defer();
-
   recordingData.fileType = PERF_TOOL_SERIALIZER_IDENTIFIER;
   recordingData.version = PERF_TOOL_SERIALIZER_CURRENT_VERSION;
 
   let string = JSON.stringify(recordingData);
-  let inputStream = this.getUnicodeConverter().convertToInputStream(string);
+  let inputStream = getUnicodeConverter().convertToInputStream(string);
   let outputStream = FileUtils.openSafeFileOutputStream(file);
 
-  NetUtil.asyncCopy(inputStream, outputStream, deferred.resolve);
-  return deferred.promise;
+  return new Promise(resolve => {
+    NetUtil.asyncCopy(inputStream, outputStream, resolve);
+  });
 }
 
 /**
  * Loads a recording stored as JSON from a file.
  *
  * @param nsILocalFile file
  *        The file to import the data from.
  * @return object
  *         A promise that is resolved once importing finishes, or rejected
  *         if there was an error.
  */
 function loadRecordingFromFile (file) {
-  let deferred = promise.defer();
-
   let channel = NetUtil.newChannel({
     uri: NetUtil.newURI(file),
-    loadUsingSystemPrincipal: true});
+    loadUsingSystemPrincipal: true
+  });
 
   channel.contentType = "text/plain";
 
-  NetUtil.asyncFetch(channel, (inputStream, status) => {
-    try {
-      let string = NetUtil.readInputStreamToString(inputStream, inputStream.available());
-      var recordingData = JSON.parse(string);
-    } catch (e) {
-      deferred.reject(new Error("Could not read recording data file."));
-      return;
-    }
-    if (recordingData.fileType != PERF_TOOL_SERIALIZER_IDENTIFIER) {
-      deferred.reject(new Error("Unrecognized recording data file."));
-      return;
-    }
-    if (!isValidSerializerVersion(recordingData.version)) {
-      deferred.reject(new Error("Unsupported recording data file version."));
-      return;
-    }
-    if (recordingData.version === PERF_TOOL_SERIALIZER_LEGACY_VERSION) {
-      recordingData = convertLegacyData(recordingData);
-    }
-    if (recordingData.profile.meta.version === 2) {
-      RecordingUtils.deflateProfile(recordingData.profile);
-    }
-    if (!recordingData.label) {
-      // set the label to the filename without its extension
-      recordingData.label = file.leafName.replace(/\..+$/, "");
-    }
-    deferred.resolve(recordingData);
+  return new Promise((resolve, reject) => {
+    NetUtil.asyncFetch(channel, (inputStream) => {
+      let recordingData;
+
+      try {
+        let string = NetUtil.readInputStreamToString(inputStream, inputStream.available());
+        recordingData = JSON.parse(string);
+      } catch (e) {
+        reject(new Error("Could not read recording data file."));
+        return;
+      }
+
+      if (recordingData.fileType != PERF_TOOL_SERIALIZER_IDENTIFIER) {
+        reject(new Error("Unrecognized recording data file."));
+        return;
+      }
+
+      if (!isValidSerializerVersion(recordingData.version)) {
+        reject(new Error("Unsupported recording data file version."));
+        return;
+      }
+
+      if (recordingData.version === PERF_TOOL_SERIALIZER_LEGACY_VERSION) {
+        recordingData = convertLegacyData(recordingData);
+      }
+
+      if (recordingData.profile.meta.version === 2) {
+        RecordingUtils.deflateProfile(recordingData.profile);
+      }
+
+      // If the recording has no label, set it to be the
+      // filename without its extension.
+      if (!recordingData.label) {
+        recordingData.label = file.leafName.replace(/\..+$/, "");
+      }
+
+      resolve(recordingData);
+    });
   });
-
-  return deferred.promise;
 }
 
 /**
  * Returns a boolean indicating whether or not the passed in `version`
  * is supported by this serializer.
  *
  * @param number version
  * @return boolean
@@ -119,19 +125,19 @@ function loadRecordingFromFile (file) {
 function isValidSerializerVersion (version) {
   return !!~[
     PERF_TOOL_SERIALIZER_LEGACY_VERSION,
     PERF_TOOL_SERIALIZER_CURRENT_VERSION
   ].indexOf(version);
 }
 
 /**
- * Takes recording data (with version `1`, from the original profiler tool), and
- * massages the data to be line with the current performance tool's property names
- * and values.
+ * Takes recording data (with version `1`, from the original profiler tool),
+ * and massages the data to be line with the current performance tool's
+ * property names and values.
  *
  * @param object legacyData
  * @return object
  */
 function convertLegacyData (legacyData) {
   let { profilerData, ticksData, recordingDuration } = legacyData;
 
   // The `profilerData` and `ticksData` stay, but the previously unrecorded
@@ -141,25 +147,24 @@ function convertLegacyData (legacyData) 
     duration: recordingDuration,
     markers: [],
     frames: [],
     memory: [],
     ticks: ticksData,
     allocations: { sites: [], timestamps: [], frames: [], sizes: [] },
     profile: profilerData.profile,
     // Fake a configuration object here if there's tick data,
-    // so that it can be rendered
+    // so that it can be rendered.
     configuration: {
       withTicks: !!ticksData.length,
       withMarkers: false,
       withMemory: false,
       withAllocations: false
     },
     systemHost: {},
     systemClient: {},
   };
 
   return data;
 }
 
-exports.getUnicodeConverter = getUnicodeConverter;
 exports.saveRecordingToFile = saveRecordingToFile;
 exports.loadRecordingFromFile = loadRecordingFromFile;
--- a/devtools/client/performance/modules/logic/frame-utils.js
+++ b/devtools/client/performance/modules/logic/frame-utils.js
@@ -4,16 +4,18 @@
 "use strict";
 
 const global = require("devtools/client/performance/modules/global");
 const demangle = require("devtools/client/shared/demangle");
 const { assert } = require("devtools/shared/DevToolsUtils");
 const { isChromeScheme, isContentScheme, parseURL } =
   require("devtools/client/shared/source-utils");
 
+const { CATEGORY_MASK, CATEGORY_MAPPINGS } = require("devtools/client/performance/modules/categories");
+
 // Character codes used in various parsing helper functions.
 const CHAR_CODE_R = "r".charCodeAt(0);
 const CHAR_CODE_0 = "0".charCodeAt(0);
 const CHAR_CODE_9 = "9".charCodeAt(0);
 const CHAR_CODE_CAP_Z = "Z".charCodeAt(0);
 
 const CHAR_CODE_LPAREN = "(".charCodeAt(0);
 const CHAR_CODE_RPAREN = ")".charCodeAt(0);
@@ -224,28 +226,28 @@ function computeIsContentAndCategory(fra
   }
 
   if (schemeStartIndex !== 0) {
     for (let j = schemeStartIndex; j < location.length; j++) {
       if (location.charCodeAt(j) === CHAR_CODE_R &&
           isChromeScheme(location, j) &&
           (location.indexOf("resource://devtools") !== -1 ||
            location.indexOf("resource://devtools") !== -1)) {
-        frame.category = global.CATEGORY_DEVTOOLS;
+        frame.category = CATEGORY_MASK("tools");
         return;
       }
     }
   }
 
   if (location === "EnterJIT") {
-    frame.category = global.CATEGORY_JIT;
+    frame.category = CATEGORY_MASK("js");
     return;
   }
 
-  frame.category = global.CATEGORY_OTHER;
+  frame.category = CATEGORY_MASK("other");
 }
 
 /**
  * Get caches to cache inflated frames and computed frame keys of a frame
  * table.
  *
  * @param object framesTable
  * @return object
@@ -323,18 +325,17 @@ function InflatedFrame(index, frameTable
 InflatedFrame.prototype.getFrameKey = function getFrameKey(options) {
   if (this.isContent || !options.contentOnly || options.isRoot) {
     options.isMetaCategoryOut = false;
     return this.location;
   }
 
   if (options.isLeaf) {
     // We only care about leaf platform frames if we are displaying content
-    // only. If no category is present, give the default category of
-    // CATEGORY_OTHER.
+    // only. If no category is present, give the default category of "other".
     //
     // 1. The leaf is where time is _actually_ being spent, so we _need_ to
     // show it to developers in some way to give them accurate profiling
     // data. We decide to split the platform into various category buckets
     // and just show time spent in each bucket.
     //
     // 2. The calls leading to the leaf _aren't_ where we are spending time,
     // but _do_ give the developer context for how they got to the leaf
@@ -384,17 +385,17 @@ function getFrameInfo (node, options) {
       data.functionName = global.L10N.getStr("table.root");
     } else {
       data = parseLocation(node.location, node.line, node.column);
       data.hasOptimizations = node.hasOptimizations();
       data.isContent = node.isContent;
       data.isMetaCategory = node.isMetaCategory;
     }
     data.samples = node.youngestFrameSamples;
-    data.categoryData = global.CATEGORY_MAPPINGS[node.category] || {};
+    data.categoryData = CATEGORY_MAPPINGS[node.category] || {};
     data.nodeType = node.nodeType;
 
     // Frame name (function location or some meta information)
     data.name = data.isMetaCategory ? data.categoryData.label :
                 shouldDemangle(data.functionName) ? demangle(data.functionName) : data.functionName;
 
     data.tooltiptext = data.isMetaCategory ? data.categoryData.label : node.location || "";
 
--- a/devtools/client/performance/modules/logic/moz.build
+++ b/devtools/client/performance/modules/logic/moz.build
@@ -1,14 +1,13 @@
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
     'frame-utils.js',
     'jit.js',
-    'marker-formatters.js',
     'marker-utils.js',
     'telemetry.js',
     'tree-model.js',
     'waterfall-utils.js',
 )
rename from devtools/client/performance/modules/logic/marker-formatters.js
rename to devtools/client/performance/modules/marker-formatters.js
--- a/devtools/client/performance/modules/logic/marker-formatters.js
+++ b/devtools/client/performance/modules/marker-formatters.js
@@ -3,19 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 /**
  * This file contains utilities for creating elements for markers to be displayed,
  * and parsing out the blueprint to generate correct values for markers.
  */
 const { Ci } = require("chrome");
-const Services = require("Services");
-const { L10N } = require("devtools/client/performance/modules/global");
-const PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data";
+const { L10N, PREFS } = require("devtools/client/performance/modules/global");
 
 // String used to fill in platform data when it should be hidden.
 const GECKO_SYMBOL = "(Gecko)";
 
 /**
  * Mapping of JS marker causes to a friendlier form. Only
  * markers that are considered "from content" should be labeled here.
  */
@@ -33,128 +31,166 @@ const JS_MARKER_MAP = {
   "setInterval handler":       "setInterval",
   "setTimeout handler":        "setTimeout",
   "FrameRequestCallback":      "requestAnimationFrame",
 };
 
 /**
  * A series of formatters used by the blueprint.
  */
-const Formatters = {
+exports.Formatters = {
   /**
    * Uses the marker name as the label for markers that do not have
    * a blueprint entry. Uses "Other" in the marker filter menu.
    */
-  UnknownLabel: function (marker={}) {
+  UnknownLabel: function (marker = {}) {
     return marker.name || L10N.getStr("marker.label.unknown");
   },
 
+  /* Group 0 - Reflow and Rendering pipeline */
+
+  StylesFields: function (marker) {
+    if ("restyleHint" in marker) {
+      let label = marker.restyleHint.replace(/eRestyle_/g, "");
+      return {
+        [L10N.getStr("marker.field.restyleHint")]: label
+      };
+    }
+  },
+
+  /* Group 1 - JS */
+
+  DOMEventFields: function (marker) {
+    let fields = Object.create(null);
+
+    if ("type" in marker) {
+      fields[L10N.getStr("marker.field.DOMEventType")] = marker.type;
+    }
+
+    if ("eventPhase" in marker) {
+      let label;
+      switch (marker.eventPhase) {
+        case Ci.nsIDOMEvent.AT_TARGET:
+          label = L10N.getStr("marker.value.DOMEventTargetPhase");
+          break;
+        case Ci.nsIDOMEvent.CAPTURING_PHASE:
+          label = L10N.getStr("marker.value.DOMEventCapturingPhase");
+          break;
+        case Ci.nsIDOMEvent.BUBBLING_PHASE:
+          label = L10N.getStr("marker.value.DOMEventBubblingPhase");
+          break;
+      }
+      fields[L10N.getStr("marker.field.DOMEventPhase")] = label;
+    }
+
+    return fields;
+  },
+
+  JSLabel: function (marker = {}) {
+    let generic = L10N.getStr("marker.label.javascript");
+    if ("causeName" in marker) {
+      return JS_MARKER_MAP[marker.causeName] || generic;
+    }
+    return generic;
+  },
+
+  JSFields: function (marker) {
+    if ("causeName" in marker && !JS_MARKER_MAP[marker.causeName]) {
+      let label = PREFS["show-platform-data"] ? marker.causeName : GECKO_SYMBOL;
+      return {
+        [L10N.getStr("marker.field.causeName")]: label
+      };
+    }
+  },
+
   GCLabel: function (marker) {
     if (!marker) {
       return L10N.getStr("marker.label.garbageCollection2");
     }
     // Only if a `nonincrementalReason` exists, do we want to label
     // this as a non incremental GC event.
     if ("nonincrementalReason" in marker) {
       return L10N.getStr("marker.label.garbageCollection.nonIncremental");
-    }
-    return L10N.getStr("marker.label.garbageCollection.incremental");
-  },
-
-  JSLabel: function (marker={}) {
-    let generic = L10N.getStr("marker.label.javascript");
-    if ("causeName" in marker) {
-      return JS_MARKER_MAP[marker.causeName] || generic;
-    }
-    return generic;
-  },
-
-  DOMJSLabel: function (marker={}) {
-    return `Event (${marker.type})`;
-  },
-
-  /**
-   * Returns a hash for computing a fields object for a JS marker. If the cause
-   * is considered content (so an entry exists in the JS_MARKER_MAP), do not display it
-   * since it's redundant with the label. Otherwise for Gecko code, either display
-   * the cause, or "(Gecko)", depending on if "show-platform-data" is set.
-   */
-  JSFields: function (marker) {
-    if ("causeName" in marker && !JS_MARKER_MAP[marker.causeName]) {
-      let cause = Services.prefs.getBoolPref(PLATFORM_DATA_PREF) ? marker.causeName : GECKO_SYMBOL;
-      return {
-        [L10N.getStr("marker.field.causeName")]: cause
-      };
+    } else {
+      return L10N.getStr("marker.label.garbageCollection.incremental");
     }
   },
 
   GCFields: function (marker) {
     let fields = Object.create(null);
-    let cause = marker.causeName;
-    let label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
 
-    fields[L10N.getStr("marker.field.causeName")] = label;
+    if ("causeName" in marker) {
+      let cause = marker.causeName;
+      let label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
+      fields[L10N.getStr("marker.field.causeName")] = label;
+    }
 
     if ("nonincrementalReason" in marker) {
-      fields[L10N.getStr("marker.field.nonIncrementalCause")] = marker.nonincrementalReason;
+      let label = marker.nonincrementalReason;
+      fields[L10N.getStr("marker.field.nonIncrementalCause")] = label;
     }
 
     return fields;
   },
 
   MinorGCFields: function (marker) {
-    const cause = marker.causeName;
-    const label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
+    let fields = Object.create(null);
+
+    if ("causeName" in marker) {
+      let cause = marker.causeName;
+      let label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
+      fields[L10N.getStr("marker.field.causeName")] = label;
+    }
+
+    fields[L10N.getStr("marker.field.type")] = L10N.getStr("marker.nurseryCollection");
+
+    return fields;
+  },
+
+  CycleCollectionFields: function (marker) {
+    let label = marker.name.replace(/nsCycleCollector::/g, "");
     return {
-      [L10N.getStr("marker.field.type")]: L10N.getStr("marker.nurseryCollection"),
-      [L10N.getStr("marker.field.causeName")]: label,
+      [L10N.getStr("marker.field.type")]: label
     };
   },
 
-  DOMEventFields: function (marker) {
-    let fields = Object.create(null);
-    if ("type" in marker) {
-      fields[L10N.getStr("marker.field.DOMEventType")] = marker.type;
-    }
-    if ("eventPhase" in marker) {
-      let phase;
-      if (marker.eventPhase === Ci.nsIDOMEvent.AT_TARGET) {
-        phase = L10N.getStr("marker.value.DOMEventTargetPhase");
-      } else if (marker.eventPhase === Ci.nsIDOMEvent.CAPTURING_PHASE) {
-        phase = L10N.getStr("marker.value.DOMEventCapturingPhase");
-      } else if (marker.eventPhase === Ci.nsIDOMEvent.BUBBLING_PHASE) {
-        phase = L10N.getStr("marker.value.DOMEventBubblingPhase");
-      }
-      fields[L10N.getStr("marker.field.DOMEventPhase")] = phase;
-    }
-    return fields;
-  },
-
-  StylesFields: function (marker) {
-    if ("restyleHint" in marker) {
+  WorkerFields: function (marker) {
+    if ("workerOperation" in marker) {
+      let label = L10N.getStr(`marker.worker.${marker.workerOperation}`);
       return {
-        [L10N.getStr("marker.field.restyleHint")]: marker.restyleHint.replace(/eRestyle_/g, "")
+        [L10N.getStr("marker.field.type")]: label
       };
     }
   },
 
-  CycleCollectionFields: function (marker) {
-    return {
-      [L10N.getStr("marker.field.type")]: marker.name.replace(/nsCycleCollector::/g, "")
-    };
+  MessagePortFields: function (marker) {
+    if ("messagePortOperation" in marker) {
+      let label = L10N.getStr(`marker.messagePort.${marker.messagePortOperation}`);
+      return {
+        [L10N.getStr("marker.field.type")]: label
+      };
+    }
   },
 
-  WorkerFields: function(marker) {
-    return {
-      [L10N.getStr("marker.field.type")]:
-        L10N.getStr(`marker.worker.${marker.workerOperation}`)
-    };
-  },
+  /* Group 2 - User Controlled */
 
-  MessagePortFields: function(marker) {
-    return {
-      [L10N.getStr("marker.field.type")]:
-        L10N.getStr(`marker.messagePort.${marker.messagePortOperation}`)
-    };
-  }
+  ConsoleTimeFields: [{
+    label: L10N.getStr("marker.field.consoleTimerName"),
+    property: "causeName",
+  }],
+
+  TimeStampFields: [{
+    label: L10N.getStr("marker.field.label"),
+    property: "causeName",
+  }]
 };
 
-exports.Formatters = Formatters;
+/**
+ * Takes a main label (e.g. "Timestamp") and a property name (e.g. "causeName"),
+ * and returns a string that represents that property value for a marker if it
+ * exists (e.g. "Timestamp (rendering)"), or just the main label if it does not.
+ *
+ * @param string mainLabel
+ * @param string propName
+ */
+exports.Formatters.labelForProperty = function (mainLabel, propName) {
+  return (marker={}) => marker[propName] ? `${mainLabel} (${marker[propName]})` : mainLabel;
+};
--- a/devtools/client/performance/modules/markers.js
+++ b/devtools/client/performance/modules/markers.js
@@ -1,67 +1,65 @@
 /* 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 { L10N } = require("devtools/client/performance/modules/global");
-const { Formatters } = require("devtools/client/performance/modules/logic/marker-formatters");
+const { Formatters, labelForProperty } = require("devtools/client/performance/modules/marker-formatters");
 
 /**
  * A simple schema for mapping markers to the timeline UI. The keys correspond
  * to marker names, while the values are objects with the following format:
  *
  * - group: The row index in the overview graph; multiple markers
  *          can be added on the same row. @see <overview.js/buildGraphImage>
  * - label: The label used in the waterfall to identify the marker. Can be a
  *          string or just a function that accepts the marker and returns a
- *          string, if you want to use a dynamic property for the main label.
+ *          string (if you want to use a dynamic property for the main label).
  *          If you use a function for a label, it *must* handle the case where
- *          no marker is provided for a main label to describe all markers of
- *          this type.
+ *          no marker is provided, to get a generic label used to describe
+ *          all markers of this type.
  * - colorName: The label of the DevTools color used for this marker. If
  *              adding a new color, be sure to check that there's an entry
- *              for `.marker-details-bullet.{COLORNAME}` for the equivilent
- *              entry in ./devtools/client/themes/performance.inc.css
+ *              for `.marker-color-graphs-{COLORNAME}` for the equivilent
+ *              entry in "./devtools/client/themes/performance.css"
  *              https://developer.mozilla.org/en-US/docs/Tools/DevToolsColors
  * - collapsible: Whether or not this marker can contain other markers it
- *                eclipses, and becomes collapsible to reveal its nestable children.
- *                Defaults to true.
+ *                eclipses, and becomes collapsible to reveal its nestable
+ *                children. Defaults to true.
  * - nestable: Whether or not this marker can be nested inside an eclipsing
  *             collapsible marker. Defaults to true.
- * - fields: An optional array of marker properties you wish to display in the
- *           marker details view. For example, a field in the array such as
- *           { property: "aCauseName", label: "Cause" } would render a string
- *           like `Cause: ${marker.aCauseName}` in the marker details view.
- *           Each `field` item may take the following properties:
+ * - fields: An optional array of fields you wish to display in the marker
+ *           details view. For example, a field in the array such as
+ *           { label: "Cause", property: "causeName" } would render a string
+ *           like `Cause: ${marker.causeName}` in the marker details view.
+ *           Each field may take the following properties:
+ *           - label: The name of the property that should be displayed.
  *           - property: The property that must exist on the marker to render,
  *                       and the value of the property will be displayed.
- *           - label: The name of the property that should be displayed.
- *           - formatter: If a formatter is provided, instead of directly using
- *                        the `property` property on the marker, the marker is
- *                        passed into the formatter function to determine the
- *                        displayed value.
- *            Can also be a function that returns an object. Each key in the object
- *            will be rendered as a field, with its value rendering as the value.
+ *           Alternatively, this also be a function that returns an object.
+ *           Each key in that object will be rendered as one field's label,
+ *           with its value rendering as one field's value.
  *
  * Whenever this is changed, browser_timeline_waterfall-styles.js *must* be
  * updated as well.
  */
 const TIMELINE_BLUEPRINT = {
-  /* Default definition used for markers that occur but
-   * are not defined here. Should ultimately be defined, but this gives
-   * us room to work on the front end separately from the platform. */
+  /* Default definition used for markers that occur but are not defined here.
+   * Should ultimately be defined, but this gives us room to work on the
+   * front end separately from the platform. */
   "UNKNOWN": {
     group: 2,
     colorName: "graphs-grey",
     label: Formatters.UnknownLabel,
   },
 
   /* Group 0 - Reflow and Rendering pipeline */
+
   "Styles": {
     group: 0,
     colorName: "graphs-purple",
     label: L10N.getStr("marker.label.styles"),
     fields: Formatters.StylesFields,
   },
   "Reflow": {
     group: 0,
@@ -80,16 +78,17 @@ const TIMELINE_BLUEPRINT = {
   },
   "CompositeForwardTransaction": {
     group: 0,
     colorName: "graphs-bluegrey",
     label: L10N.getStr("marker.label.compositeForwardTransaction"),
   },
 
   /* Group 1 - JS */
+
   "DOMEvent": {
     group: 1,
     colorName: "graphs-yellow",
     label: L10N.getStr("marker.label.domevent"),
     fields: Formatters.DOMEventFields,
   },
   "document::DOMContentLoaded": {
     group: 1,
@@ -150,43 +149,28 @@ const TIMELINE_BLUEPRINT = {
   "MessagePort": {
     group: 1,
     colorName: "graphs-orange",
     label: L10N.getStr("marker.label.messagePort"),
     fields: Formatters.MessagePortFields
   },
 
   /* Group 2 - User Controlled */
+
   "ConsoleTime": {
     group: 2,
     colorName: "graphs-blue",
-    label: sublabelForProperty(L10N.getStr("marker.label.consoleTime"), "causeName"),
-    fields: [{
-      property: "causeName",
-      label: L10N.getStr("marker.field.consoleTimerName")
-    }],
+    label: Formatters.labelForProperty(L10N.getStr("marker.label.consoleTime"), "causeName"),
+    fields: Formatters.ConsoleTimeFields,
     nestable: false,
     collapsible: false,
   },
   "TimeStamp": {
     group: 2,
     colorName: "graphs-blue",
-    label: sublabelForProperty(L10N.getStr("marker.label.timestamp"), "causeName"),
-    fields: [{
-      property: "causeName",
-      label: "Label:"
-    }],
+    label: Formatters.labelForProperty(L10N.getStr("marker.label.timestamp"), "causeName"),
+    fields: Formatters.TimeStampFields,
     collapsible: false,
   },
 };
 
-/**
- * Takes a main label (like "Timestamp") and a property,
- * and returns a marker that will print out the property
- * value for a marker if it exists ("Timestamp (rendering)"),
- * or just the main label if it does not.
- */
-function sublabelForProperty (mainLabel, prop) {
-  return (marker={}) => marker[prop] ? `${mainLabel} (${marker[prop]})` : mainLabel;
-}
-
 // Exported symbols.
 exports.TIMELINE_BLUEPRINT = TIMELINE_BLUEPRINT;
--- a/devtools/client/performance/modules/moz.build
+++ b/devtools/client/performance/modules/moz.build
@@ -4,13 +4,15 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
     'logic',
     'widgets',
 ]
 
 DevToolsModules(
+    'categories.js',
     'constants.js',
     'global.js',
     'io.js',
+    'marker-formatters.js',
     'markers.js',
 )
--- a/devtools/client/performance/test/browser_perf-tree-view-08.js
+++ b/devtools/client/performance/test/browser_perf-tree-view-08.js
@@ -4,17 +4,17 @@
 
 /**
  * Tests that the profiler's tree view renders generalized platform data
  * when `contentOnly` is on correctly.
  */
 
 const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
 const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
-const { CATEGORY_MASK } = require("devtools/client/performance/modules/global");
+const { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
 const RecordingUtils = require("devtools/shared/performance/recording-utils");
 
 add_task(function() {
   let threadNode = new ThreadNode(gProfile.threads[0], { startTime: 0, endTime: 20, contentOnly: true });
 
   // Don't display the synthesized (root) and the real (root) node twice.
   threadNode.calls = threadNode.calls[0].calls;
 
--- a/devtools/client/performance/test/browser_perf-tree-view-11.js
+++ b/devtools/client/performance/test/browser_perf-tree-view-11.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that if `show-jit-optimizations` is true, then an
  * icon is next to the frame with optimizations
  */
 
-var { CATEGORY_MASK } = require("devtools/client/performance/modules/global");
+var { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
 
 function* spawnTest() {
   let { panel } = yield initPerformance(SIMPLE_URL);
   let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
   let { OverviewView, DetailsView, JsCallTreeView, RecordingsView } = panel.panelWin;
 
   let profilerData = { threads: [gThread] };
 
--- a/devtools/client/performance/test/helpers/synth-utils.js
+++ b/devtools/client/performance/test/helpers/synth-utils.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 /**
  * Generates a generalized profile with some samples.
  */
 exports.synthesizeProfile = () => {
-  const { CATEGORY_MASK } = require("devtools/client/performance/modules/global");
+  const { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
   const RecordingUtils = require("devtools/shared/performance/recording-utils");
 
   return RecordingUtils.deflateProfile({
     meta: { version: 2 },
     threads: [{
       samples: [{
         time: 1,
         frames: [
--- a/devtools/client/performance/test/unit/test_marker-utils.js
+++ b/devtools/client/performance/test/unit/test_marker-utils.js
@@ -6,18 +6,21 @@
  */
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function () {
   let { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
+  let { PREFS } = require("devtools/client/performance/modules/global");
   let Utils = require("devtools/client/performance/modules/logic/marker-utils");
 
+  PREFS.registerObserver();
+
   Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);
 
   equal(Utils.getMarkerLabel({ name: "DOMEvent" }), "DOM Event",
     "getMarkerLabel() returns a simple label");
   equal(Utils.getMarkerLabel({ name: "Javascript", causeName: "setTimeout handler" }), "setTimeout",
     "getMarkerLabel() returns a label defined via function");
   equal(Utils.getMarkerLabel({ name: "GarbageCollection", causeName: "ALLOC_TRIGGER" }), "Incremental GC",
     "getMarkerLabel() returns a label for a function that is generalizable");
@@ -88,9 +91,11 @@ add_task(function () {
       label: "MyCustom"
     }
   };
 
   equal(Utils.getBlueprintFor({ name: "Reflow" }).label, "Layout",
     "Utils.getBlueprintFor() should return marker def for passed in marker.");
   equal(Utils.getBlueprintFor({ name: "Not sure!" }).label(), "Unknown",
     "Utils.getBlueprintFor() should return a default marker def if the marker is undefined.");
+
+  PREFS.unregisterObserver();
 });
--- a/devtools/client/performance/test/unit/test_profiler-categories.js
+++ b/devtools/client/performance/test/unit/test_profiler-categories.js
@@ -5,32 +5,30 @@
  * Tests if the profiler categories are mapped correctly.
  */
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function () {
-  let global = require("devtools/client/performance/modules/global");
-  let l10n = global.L10N;
-  let categories = global.CATEGORIES;
-  let mappings = global.CATEGORY_MAPPINGS;
-  let count = categories.length;
+  let { CATEGORIES, CATEGORY_MAPPINGS } = require("devtools/client/performance/modules/categories");
+  let { L10N } = require("devtools/client/performance/modules/global");
+  let count = CATEGORIES.length;
 
   ok(count,
     "Should have a non-empty list of categories available.");
 
-  ok(categories.some(e => e.color),
+  ok(CATEGORIES.some(e => e.color),
     "All categories have an associated color.");
 
-  ok(categories.every(e => e.label),
+  ok(CATEGORIES.every(e => e.label),
     "All categories have an associated label.");
 
-  ok(categories.every(e => e.label === l10n.getStr("category." + e.abbrev)),
+  ok(CATEGORIES.every(e => e.label === L10N.getStr("category." + e.abbrev)),
     "All categories have a correctly localized label.");
 
-  ok(Object.keys(mappings).every(e => (Number(e) >= 9000 && Number(e) <= 9999) || Number.isInteger(Math.log2(e))),
+  ok(Object.keys(CATEGORY_MAPPINGS).every(e => (Number(e) >= 9000 && Number(e) <= 9999) || Number.isInteger(Math.log2(e))),
     "All bitmask mappings keys are powers of 2, or between 9000-9999 for special categories.");
 
-  ok(Object.keys(mappings).every(e => categories.indexOf(mappings[e]) !== -1),
+  ok(Object.keys(CATEGORY_MAPPINGS).every(e => CATEGORIES.indexOf(CATEGORY_MAPPINGS[e]) !== -1),
     "All bitmask mappings point to a category.");
 });
--- a/devtools/client/performance/test/unit/test_tree-model-07.js
+++ b/devtools/client/performance/test/unit/test_tree-model-07.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that when displaying only content nodes, platform nodes are generalized.
  */
 
-var { CATEGORY_MASK } = require("devtools/client/performance/modules/global");
+var { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function test() {
   let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
   let url = (n) => `http://content/${n}`;
--- a/devtools/client/performance/test/unit/test_tree-model-08.js
+++ b/devtools/client/performance/test/unit/test_tree-model-08.js
@@ -7,17 +7,17 @@
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function test() {
   let FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
   let { FrameNode } = require("devtools/client/performance/modules/logic/tree-model");
-  let { CATEGORY_OTHER } = require("devtools/client/performance/modules/global");
+  let { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
   let compute = frame => {
     FrameUtils.computeIsContentAndCategory(frame);
     return frame;
   };
 
   let frames = [
     new FrameNode("hello/<.world (http://foo/bar.js:123:987)", compute({
       location: "hello/<.world (http://foo/bar.js:123:987)",
@@ -37,17 +37,17 @@ add_task(function test() {
     }), false),
     new FrameNode("hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)", compute({
       location: "hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)",
       line: 456,
     }), false),
     new FrameNode("Foo::Bar::Baz", compute({
       location: "Foo::Bar::Baz",
       line: 456,
-      category: CATEGORY_OTHER,
+      category: CATEGORY_MASK("other"),
     }), false),
     new FrameNode("EnterJIT", compute({
       location: "EnterJIT",
     }), false),
     new FrameNode("chrome://browser/content/content.js", compute({
       location: "chrome://browser/content/content.js",
       line: 456,
       column: 123
--- a/devtools/client/performance/test/unit/test_tree-model-09.js
+++ b/devtools/client/performance/test/unit/test_tree-model-09.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests that when displaying only content nodes, platform nodes are generalized.
  */
 
-var { CATEGORY_MASK } = require("devtools/client/performance/modules/global");
+var { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function test() {
   let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
   let url = (n) => `http://content/${n}`;
--- a/devtools/client/projecteditor/lib/helpers/l10n.js
+++ b/devtools/client/projecteditor/lib/helpers/l10n.js
@@ -4,19 +4,19 @@
  * 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/. */
 
 /**
  * This file contains helper functions for internationalizing projecteditor strings
  */
 
 const { Cu, Cc, Ci } = require("chrome");
-const { ViewHelpers } = Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm", {});
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 const ITCHPAD_STRINGS_URI = "chrome://devtools/locale/projecteditor.properties";
-const L10N = new ViewHelpers.L10N(ITCHPAD_STRINGS_URI).stringBundle;
+const L10N = new LocalizationHelper(ITCHPAD_STRINGS_URI).stringBundle;
 
 function getLocalizedString (name) {
   try {
     return L10N.GetStringFromName(name);
   } catch (ex) {
     console.log("Error reading '" + name + "'");
     throw new Error("l10n error with " + name);
   }
--- a/devtools/client/responsive.html/components/utils/l10n.js
+++ b/devtools/client/responsive.html/components/utils/l10n.js
@@ -1,20 +1,16 @@
 /* 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 { LocalizationHelper } = require("devtools/client/shared/l10n");
 const STRINGS_URI = "chrome://devtools/locale/responsive.properties";
-
-const {
-  ViewHelpers
-} = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
-
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
 
 module.exports = {
   getStr: (...args) => L10N.getStr(...args),
   getFormatStr: (...args) => L10N.getFormatStr(...args),
   getFormatStrWithNumbers: (...args) => L10N.getFormatStrWithNumbers(...args),
   numberWithDecimals: (...args) => L10N.numberWithDecimals(...args),
 };
--- a/devtools/client/responsivedesign/responsivedesign.jsm
+++ b/devtools/client/responsivedesign/responsivedesign.jsm
@@ -12,16 +12,18 @@ var Telemetry = require("devtools/client
 var {showDoorhanger} = require("devtools/client/shared/doorhanger");
 var {TouchEventSimulator} = require("devtools/shared/touch/simulator");
 var {Task} = require("resource://gre/modules/Task.jsm");
 var promise = require("promise");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 var Services = require("Services");
 var EventEmitter = require("devtools/shared/event-emitter");
 var {ViewHelpers} = require("devtools/client/shared/widgets/ViewHelpers.jsm");
+var { LocalizationHelper } = require("devtools/client/shared/l10n");
+
 loader.lazyImporter(this, "SystemAppProxy",
                     "resource://gre/modules/SystemAppProxy.jsm");
 loader.lazyRequireGetter(this, "DebuggerClient",
                          "devtools/shared/client/main", true);
 loader.lazyRequireGetter(this, "DebuggerServer",
                          "devtools/server/main", true);
 
 this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"];
@@ -32,17 +34,17 @@ const MIN_HEIGHT = 50;
 const MAX_WIDTH = 10000;
 const MAX_HEIGHT = 10000;
 
 const SLOW_RATIO = 6;
 const ROUND_RATIO = 10;
 
 const INPUT_PARSER = /(\d+)[^\d]+(\d+)/;
 
-const SHARED_L10N = new ViewHelpers.L10N("chrome://devtools/locale/shared.properties");
+const SHARED_L10N = new LocalizationHelper("chrome://devtools/locale/shared.properties");
 
 function debug(msg) {
   // dump(`RDM UI: ${msg}\n`);
 }
 
 var ActiveTabs = new Map();
 
 var Manager = {
--- a/devtools/client/shadereditor/shadereditor.js
+++ b/devtools/client/shadereditor/shadereditor.js
@@ -12,16 +12,17 @@ Cu.import("resource://devtools/client/sh
 Cu.import("resource://gre/modules/Console.jsm");
 
 const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const promise = require("promise");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {Tooltip} = require("devtools/client/shared/widgets/Tooltip");
 const Editor = require("devtools/client/sourceeditor/editor");
+const {LocalizationHelper} = require("devtools/client/shared/l10n");
 
 // The panel's window global is an EventEmitter firing the following events:
 const EVENTS = {
   // When new programs are received from the server.
   NEW_PROGRAM: "ShaderEditor:NewProgram",
   PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded",
 
   // When the vertex and fragment sources were shown in the editor.
@@ -613,17 +614,17 @@ var ShadersEditorsView = {
     vs: [],
     fs: []
   }
 };
 
 /**
  * Localization convenience methods.
  */
-var L10N = new ViewHelpers.L10N(STRINGS_URI);
+var L10N = new LocalizationHelper(STRINGS_URI);
 
 /**
  * Convenient way of emitting events from the panel window.
  */
 EventEmitter.decorate(this);
 
 /**
  * DOM query helper.
--- a/devtools/client/shared/components/frame.js
+++ b/devtools/client/shared/components/frame.js
@@ -1,18 +1,19 @@
 /* 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 { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
 const { getSourceNames, parseURL, isScratchpadScheme } = require("devtools/client/shared/source-utils");
-const { L10N } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm").ViewHelpers;
-const l10n = new L10N("chrome://devtools/locale/components.properties");
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+
+const l10n = new LocalizationHelper("chrome://devtools/locale/components.properties");
 
 module.exports = createClass({
   propTypes: {
     // SavedFrame, or an object containing all the required properties.
     frame: PropTypes.shape({
       functionDisplayName: PropTypes.string,
       source: PropTypes.string.isRequired,
       line: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/l10n.js
@@ -0,0 +1,122 @@
+/* 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 { Ci } = require("chrome");
+const Services = require("Services");
+
+/**
+ * Localization convenience methods.
+ *
+ * @param string stringBundleName
+ *        The desired string bundle's name.
+ */
+function LocalizationHelper(stringBundleName) {
+  loader.lazyGetter(this, "stringBundle", () =>
+    Services.strings.createBundle(stringBundleName));
+  loader.lazyGetter(this, "ellipsis", () =>
+    Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data);
+}
+
+LocalizationHelper.prototype = {
+  /**
+   * L10N shortcut function.
+   *
+   * @param string name
+   * @return string
+   */
+  getStr: function(name) {
+    return this.stringBundle.GetStringFromName(name);
+  },
+
+  /**
+   * L10N shortcut function.
+   *
+   * @param string name
+   * @param array args
+   * @return string
+   */
+  getFormatStr: function(name, ...args) {
+    return this.stringBundle.formatStringFromName(name, args, args.length);
+  },
+
+  /**
+   * L10N shortcut function for numeric arguments that need to be formatted.
+   * All numeric arguments will be fixed to 2 decimals and given a localized
+   * decimal separator. Other arguments will be left alone.
+   *
+   * @param string name
+   * @param array args
+   * @return string
+   */
+  getFormatStrWithNumbers: function(name, ...args) {
+    let newArgs = args.map(x => typeof x == "number" ? this.numberWithDecimals(x, 2) : x);
+    return this.stringBundle.formatStringFromName(name, newArgs, newArgs.length);
+  },
+
+  /**
+   * Converts a number to a locale-aware string format and keeps a certain
+   * number of decimals.
+   *
+   * @param number number
+   *        The number to convert.
+   * @param number decimals [optional]
+   *        Total decimals to keep.
+   * @return string
+   *         The localized number as a string.
+   */
+  numberWithDecimals: function(number, decimals = 0) {
+    // If this is an integer, don't do anything special.
+    if (number === (number|0)) {
+      return number;
+    }
+    // If this isn't a number (and yes, `isNaN(null)` is false), return zero.
+    if (isNaN(number) || number === null) {
+      return "0";
+    }
+
+    let localized = number.toLocaleString();
+
+    // If no grouping or decimal separators are available, bail out, because
+    // padding with zeros at the end of the string won't make sense anymore.
+    if (!localized.match(/[^\d]/)) {
+      return localized;
+    }
+
+    return number.toLocaleString(undefined, {
+      maximumFractionDigits: decimals,
+      minimumFractionDigits: decimals
+    });
+  }
+};
+
+/**
+ * A helper for having the same interface as LocalizationHelper, but for more than
+ * one file. Useful for abstracting l10n string locations.
+ */
+function MultiLocalizationHelper(...stringBundleNames) {
+  let instances = stringBundleNames.map(bundle => new LocalizationHelper(bundle));
+
+  // Get all function members of the LocalizationHelper class, making sure we're not
+  // executing any potential getters while doing so, and wrap all the
+  // methods we've found to work on all given string bundles.
+  Object.getOwnPropertyNames(LocalizationHelper.prototype)
+    .map(name => ({
+      name: name,
+      descriptor: Object.getOwnPropertyDescriptor(LocalizationHelper.prototype, name)
+    }))
+    .filter(({ descriptor }) => descriptor.value instanceof Function)
+    .forEach(method => {
+      this[method.name] = (...args) => {
+        for (let l10n of instances) {
+          try {
+            return method.descriptor.value.apply(l10n, args);
+          } catch (e) {}
+        }
+      };
+    });
+}
+
+exports.LocalizationHelper = LocalizationHelper;
+exports.MultiLocalizationHelper = MultiLocalizationHelper;
--- a/devtools/client/shared/moz.build
+++ b/devtools/client/shared/moz.build
@@ -27,20 +27,22 @@ DevToolsModules(
     'DOMHelpers.jsm',
     'doorhanger.js',
     'file-watcher-worker.js',
     'file-watcher.js',
     'frame-script-utils.js',
     'getjson.js',
     'inplace-editor.js',
     'Jsbeautify.jsm',
+    'l10n.js',
     'node-attribute-parser.js',
     'options-view.js',
     'output-parser.js',
     'poller.js',
+    'prefs.js',
     'source-utils.js',
     'SplitView.jsm',
     'telemetry.js',
     'theme-switching.js',
     'theme.js',
     'undo.js',
     'view-source.js',
     'webgl-utils.js',
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/prefs.js
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * Shortcuts for lazily accessing and setting various preferences.
+ * Usage:
+ *   let prefs = new Prefs("root.path.to.branch", {
+ *     myIntPref: ["Int", "leaf.path.to.my-int-pref"],
+ *     myCharPref: ["Char", "leaf.path.to.my-char-pref"],
+ *     myJsonPref: ["Json", "leaf.path.to.my-json-pref"],
+ *     myFloatPref: ["Float", "leaf.path.to.my-float-pref"]
+ *     ...
+ *   });
+ *
+ * Get/set:
+ *   prefs.myCharPref = "foo";
+ *   let aux = prefs.myCharPref;
+ *
+ * Observe:
+ *   prefs.registerObserver();
+ *   prefs.on("pref-changed", (prefName, prefValue) => {
+ *     ...
+ *   });
+ *
+ * @param string prefsRoot
+ *        The root path to the required preferences branch.
+ * @param object prefsBlueprint
+ *        An object containing { accessorName: [prefType, prefName] } keys.
+ */
+function PrefsHelper(prefsRoot = "", prefsBlueprint = {}) {
+  EventEmitter.decorate(this);
+
+  let cache = new Map();
+
+  for (let accessorName in prefsBlueprint) {
+    let [prefType, prefName] = prefsBlueprint[accessorName];
+    map(this, cache, accessorName, prefType, prefsRoot, prefName);
+  }
+
+  let observer = makeObserver(this, cache, prefsRoot, prefsBlueprint);
+  this.registerObserver = () => observer.register();
+  this.unregisterObserver = () => observer.unregister();
+}
+
+/**
+ * Helper method for getting a pref value.
+ *
+ * @param Map cache
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @return any
+ */
+function get(cache, prefType, prefsRoot, prefName) {
+  let cachedPref = cache.get(prefName);
+  if (cachedPref !== undefined) {
+    return cachedPref;
+  }
+  let value = Services.prefs["get" + prefType + "Pref"]([prefsRoot, prefName].join("."));
+  cache.set(prefName, value);
+  return value;
+}
+
+/**
+ * Helper method for setting a pref value.
+ *
+ * @param Map cache
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @param any value
+ */
+function set(cache, prefType, prefsRoot, prefName, value) {
+  Services.prefs["set" + prefType + "Pref"]([prefsRoot, prefName].join("."), value);
+  cache.set(prefName, value);
+}
+
+/**
+ * Maps a property name to a pref, defining lazy getters and setters.
+ * Supported types are "Bool", "Char", "Int", "Float" (sugar around "Char"
+ * type and casting), and "Json" (which is basically just sugar for "Char"
+ * using the standard JSON serializer).
+ *
+ * @param PrefsHelper self
+ * @param Map cache
+ * @param string accessorName
+ * @param string prefType
+ * @param string prefsRoot
+ * @param string prefName
+ * @param array serializer [optional]
+ */
+function map(self, cache, accessorName, prefType, prefsRoot, prefName, serializer = { in: e => e, out: e => e }) {
+  if (prefName in self) {
+    throw new Error(`Can't use ${prefName} because it overrides a property on the instance.`);
+  }
+  if (prefType == "Json") {
+    map(self, cache, accessorName, "Char", prefsRoot, prefName, { in: JSON.parse, out: JSON.stringify });
+    return;
+  }
+  if (prefType == "Float") {
+    map(self, cache, accessorName, "Char", prefsRoot, prefName, { in: Number.parseFloat, out: (n) => n + ""});
+    return;
+  }
+
+  Object.defineProperty(self, accessorName, {
+    get: () => serializer.in(get(cache, prefType, prefsRoot, prefName)),
+    set: (e) => set(cache, prefType, prefsRoot, prefName, serializer.out(e))
+  });
+}
+
+/**
+ * Finds the accessor for the provided pref, based on the blueprint object
+ * used in the constructor.
+ *
+ * @param PrefsHelper self
+ * @param object prefsBlueprint
+ * @return string
+ */
+function accessorNameForPref(somePrefName, prefsBlueprint) {
+  for (let accessorName in prefsBlueprint) {
+    let [, prefName] = prefsBlueprint[accessorName];
+    if (somePrefName == prefName) {
+      return accessorName;
+    }
+  }
+  return "";
+}
+
+/**
+ * Creates a pref observer for `self`.
+ *
+ * @param PrefsHelper self
+ * @param Map cache
+ * @param string prefsRoot
+ * @param object prefsBlueprint
+ * @return object
+ */
+function makeObserver(self, cache, prefsRoot, prefsBlueprint) {
+  return {
+    register: function() {
+      this._branch = Services.prefs.getBranch(prefsRoot + ".");
+      this._branch.addObserver("", this, false);
+    },
+    unregister: function() {
+      this._branch.removeObserver("", this);
+    },
+    observe: function(subject, topic, prefName) {
+      // If this particular pref isn't handled by the blueprint object,
+      // even though it's in the specified branch, ignore it.
+      let accessorName = accessorNameForPref(prefName, prefsBlueprint);
+      if (!(accessorName in self)) {
+        return;
+      }
+      cache.delete(prefName);
+      self.emit("pref-changed", accessorName, self[accessorName]);
+    }
+  };
+}
+
+exports.PrefsHelper = PrefsHelper;
--- a/devtools/client/shared/source-utils.js
+++ b/devtools/client/shared/source-utils.js
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { URL } = require("sdk/url");
-const { L10N } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm").ViewHelpers;
-const l10n = new L10N("chrome://devtools/locale/components.properties");
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+
+const l10n = new LocalizationHelper("chrome://devtools/locale/components.properties");
 const UNKNOWN_SOURCE_STRING = l10n.getStr("frame.unknownSource");
 
 // Character codes used in various parsing helper functions.
 const CHAR_CODE_A = "a".charCodeAt(0);
 const CHAR_CODE_C = "c".charCodeAt(0);
 const CHAR_CODE_D = "d".charCodeAt(0);
 const CHAR_CODE_E = "e".charCodeAt(0);
 const CHAR_CODE_F = "f".charCodeAt(0);
--- a/devtools/client/shared/test/browser_filter-editor-02.js
+++ b/devtools/client/shared/test/browser_filter-editor-02.js
@@ -3,19 +3,19 @@
 
 "use strict";
 
 // Tests that the Filter Editor Widget renders filters correctly
 
 const TEST_URI = "chrome://devtools/content/shared/widgets/filter-frame.xhtml";
 const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
 
-const { ViewHelpers } = Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm", {});
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 const STRINGS_URI = "chrome://devtools/locale/filterwidget.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
 
 add_task(function*() {
   yield addTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
   const TEST_DATA = [
     {
       cssValue: "blur(2px) contrast(200%) hue-rotate(20.2deg) drop-shadow(5px 5px black)",
--- a/devtools/client/shared/test/browser_filter-editor-06.js
+++ b/devtools/client/shared/test/browser_filter-editor-06.js
@@ -4,19 +4,19 @@
 "use strict";
 
 // Tests the Filter Editor Widget's add button
 
 const TEST_URI = "chrome://devtools/content/shared/widgets/filter-frame.xhtml";
 
 const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
 
-const { ViewHelpers } = Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm", {});
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 const STRINGS_URI = "chrome://devtools/locale/filterwidget.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
 
 add_task(function*() {
   yield addTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
   const container = doc.querySelector("#container");
   let widget = new CSSFilterEditorWidget(container, "none");
 
--- a/devtools/client/shared/test/browser_filter-editor-07.js
+++ b/devtools/client/shared/test/browser_filter-editor-07.js
@@ -4,19 +4,19 @@
 "use strict";
 
 // Tests the Filter Editor Widget's remove button
 
 const TEST_URI = "chrome://devtools/content/shared/widgets/filter-frame.xhtml";
 
 const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
 
-const { ViewHelpers } = Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm", {});
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 const STRINGS_URI = "chrome://devtools/locale/filterwidget.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
 
 add_task(function*() {
   yield addTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
   const container = doc.querySelector("#container");
   let widget = new CSSFilterEditorWidget(container, "blur(2px) contrast(200%)");
 
--- a/devtools/client/shared/test/browser_flame-graph-04.js
+++ b/devtools/client/shared/test/browser_flame-graph-04.js
@@ -1,20 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that text metrics in the flame graph widget work properly.
 
 var HTML_NS = "http://www.w3.org/1999/xhtml";
-var {ViewHelpers} = Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm", {});
+var {LocalizationHelper} = require("devtools/client/shared/l10n");
 var {FlameGraph} = require("devtools/client/shared/widgets/FlameGraph");
 var {FLAME_GRAPH_BLOCK_TEXT_FONT_SIZE} = require("devtools/client/shared/widgets/FlameGraph");
 var {FLAME_GRAPH_BLOCK_TEXT_FONT_FAMILY} = require("devtools/client/shared/widgets/FlameGraph");
 
-var L10N = new ViewHelpers.L10N();
+var L10N = new LocalizationHelper();
 
 add_task(function*() {
   yield addTab("about:blank");
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
 function* performTest() {
--- a/devtools/client/shared/test/browser_num-l10n.js
+++ b/devtools/client/shared/test/browser_num-l10n.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Tests that ViewHelpers.Prefs work properly.
+// Tests that the localization utils work properly.
 
-var {ViewHelpers} = Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm", {});
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 
 function test() {
-  let l10n = new ViewHelpers.L10N();
+  let l10n = new LocalizationHelper();
 
   is(l10n.numberWithDecimals(1234.56789, 2), "1,234.57",
     "The first number was properly localized.");
   is(l10n.numberWithDecimals(0.0001, 2), "0",
     "The second number was properly localized.");
   is(l10n.numberWithDecimals(1.0001, 2), "1",
     "The third number was properly localized.");
   is(l10n.numberWithDecimals(NaN, 2), "0",
--- a/devtools/client/shared/test/browser_prefs-01.js
+++ b/devtools/client/shared/test/browser_prefs-01.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Tests that ViewHelpers.Prefs work properly.
+// Tests that the preference helpers work properly.
 
-var {ViewHelpers} = Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm", {});
+var { PrefsHelper } = require("devtools/client/shared/prefs");
 
 function test() {
-  let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
+  let Prefs = new PrefsHelper("devtools.debugger", {
     "foo": ["Bool", "enabled"]
   });
 
   let originalPrefValue = Services.prefs.getBoolPref("devtools.debugger.enabled");
   is(Prefs.foo, originalPrefValue, "The pref value was correctly fetched.");
 
   Prefs.foo = !originalPrefValue;
   is(Prefs.foo, !originalPrefValue,
--- a/devtools/client/shared/test/browser_prefs-02.js
+++ b/devtools/client/shared/test/browser_prefs-02.js
@@ -1,20 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// Tests that ViewHelpers.Prefs work properly with custom types of Float and Json.
+// Tests that preference helpers work properly with custom types of Float and Json.
 
-var {ViewHelpers} = Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm", {});
+var { PrefsHelper } = require("devtools/client/shared/prefs");
 
 function test() {
   let originalJson = Services.prefs.getCharPref("devtools.performance.timeline.hidden-markers");
   let originalFloat = Services.prefs.getCharPref("devtools.performance.memory.sample-probability");
 
-  let Prefs = new ViewHelpers.Prefs("devtools.performance", {
+  let Prefs = new PrefsHelper("devtools.performance", {
     "float": ["Float", "memory.sample-probability"],
     "json": ["Json", "timeline.hidden-markers"]
   });
 
   Prefs.registerObserver();
 
   // Float
   Services.prefs.setCharPref("devtools.performance.timeline.hidden-markers", "{\"a\":1}");
--- a/devtools/client/shared/widgets/Chart.jsm
+++ b/devtools/client/shared/widgets/Chart.jsm
@@ -17,22 +17,25 @@ const NAMED_SLICE_MIN_ANGLE = TAU / 8;
 const NAMED_SLICE_TEXT_DISTANCE_RATIO = 1.9;
 const HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO = 20;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 Cu.import("resource://devtools/shared/event-emitter.js");
 
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+
 this.EXPORTED_SYMBOLS = ["Chart"];
 
 /**
  * Localization convenience methods.
  */
-var L10N = new ViewHelpers.L10N(NET_STRINGS_URI);
+var L10N = new LocalizationHelper(NET_STRINGS_URI);
 
 /**
  * A factory for creating charts.
  * Example usage: let myChart = Chart.Pie(document, { ... });
  */
 var Chart = {
   Pie: createPieChart,
   Table: createTableChart,
--- a/devtools/client/shared/widgets/FilterWidget.js
+++ b/devtools/client/shared/widgets/FilterWidget.js
@@ -9,18 +9,21 @@
   * for Rule View's filter swatches
   */
 
 const EventEmitter = require("devtools/shared/event-emitter");
 const { Cu, Cc, Ci } = require("chrome");
 const { ViewHelpers } =
       Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm",
                 {});
+
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 const STRINGS_URI = "chrome://devtools/locale/filterwidget.properties";
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
+const L10N = new LocalizationHelper(STRINGS_URI);
+
 const {cssTokenizer} = require("devtools/client/shared/css-parsing-utils");
 
 loader.lazyGetter(this, "asyncStorage",
                   () => require("devtools/shared/async-storage"));
 
 loader.lazyGetter(this, "DOMUtils", () => {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
--- a/devtools/client/shared/widgets/FlameGraph.js
+++ b/devtools/client/shared/widgets/FlameGraph.js
@@ -1,42 +1,43 @@
 /* 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("resource://gre/modules/Task.jsm");
 const { ViewHelpers } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 const { setNamedTimeout, clearNamedTimeout } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 
 loader.lazyRequireGetter(this, "promise");
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/shared/event-emitter");
 
 loader.lazyRequireGetter(this, "getColor",
   "devtools/client/shared/theme", true);
 
 loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
-  "devtools/client/performance/modules/global", true);
+  "devtools/client/performance/modules/categories", true);
 loader.lazyRequireGetter(this, "FrameUtils",
   "devtools/client/performance/modules/logic/frame-utils");
 loader.lazyRequireGetter(this, "demangle",
   "devtools/client/shared/demangle");
 
 loader.lazyRequireGetter(this, "AbstractCanvasGraph",
   "devtools/client/shared/widgets/Graphs", true);
 loader.lazyRequireGetter(this, "GraphArea",
   "devtools/client/shared/widgets/Graphs", true);
 loader.lazyRequireGetter(this, "GraphAreaDragger",
   "devtools/client/shared/widgets/Graphs", true);
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://devtools/content/shared/widgets/graphs-frame.xhtml";
 
-const L10N = new ViewHelpers.L10N();
+const L10N = new LocalizationHelper();
 
 const GRAPH_RESIZE_EVENTS_DRAIN = 100; // ms
 
 const GRAPH_WHEEL_ZOOM_SENSITIVITY = 0.00035;
 const GRAPH_WHEEL_SCROLL_SENSITIVITY = 0.5;
 const GRAPH_KEYBOARD_ZOOM_SENSITIVITY = 20;
 const GRAPH_KEYBOARD_PAN_SENSITIVITY = 20;
 const GRAPH_KEYBOARD_ACCELERATION = 1.05;
--- a/devtools/client/shared/widgets/LineGraphWidget.js
+++ b/devtools/client/shared/widgets/LineGraphWidget.js
@@ -1,18 +1,19 @@
 "use strict";
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
 
 const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 const { ViewHelpers, Heritage } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 const { AbstractCanvasGraph, CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
-const L10N = new ViewHelpers.L10N("chrome://devtools/locale/graphs.properties");
+const L10N = new LocalizationHelper("chrome://devtools/locale/graphs.properties");
 
 // Line graph constants.
 
 const GRAPH_DAMPEN_VALUES_FACTOR = 0.85;
 const GRAPH_TOOLTIP_SAFE_BOUNDS = 8; // px
 const GRAPH_MIN_MAX_TOOLTIP_DISTANCE = 14; // px
 
 const GRAPH_BACKGROUND_COLOR = "#0088cc";
--- a/devtools/client/shared/widgets/SideMenuWidget.jsm
+++ b/devtools/client/shared/widgets/SideMenuWidget.jsm
@@ -8,22 +8,25 @@
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const SHARED_STRINGS_URI = "chrome://devtools/locale/shared.properties";
 
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 Cu.import("resource://devtools/shared/event-emitter.js");
 
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+
 this.EXPORTED_SYMBOLS = ["SideMenuWidget"];
 
 /**
  * Localization convenience methods.
  */
-var L10N = new ViewHelpers.L10N(SHARED_STRINGS_URI);
+var L10N = new LocalizationHelper(SHARED_STRINGS_URI);
 
 /**
  * A simple side menu, with the ability of grouping menu items.
  *
  * Note: this widget should be used in tandem with the WidgetMethods in
  * ViewHelpers.jsm.
  *
  * @param nsIDOMNode aNode
--- a/devtools/client/shared/widgets/VariablesViewController.jsm
+++ b/devtools/client/shared/widgets/VariablesViewController.jsm
@@ -8,16 +8,17 @@
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://devtools/client/shared/widgets/VariablesView.jsm");
 Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
 var Services = require("Services");
 var promise = require("promise");
+var {LocalizationHelper} = require("devtools/client/shared/l10n");
 
 Object.defineProperty(this, "WebConsoleUtils", {
   get: function() {
     return require("devtools/shared/webconsole/utils").Utils;
   },
   configurable: true,
   enumerable: true
 });
@@ -28,20 +29,22 @@ XPCOMUtils.defineLazyGetter(this, "VARIA
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
   "resource://gre/modules/Console.jsm");
 
 const MAX_LONG_STRING_LENGTH = 200000;
 const MAX_PROPERTY_ITEMS = 2000;
 const DBG_STRINGS_URI = "chrome://devtools/locale/debugger.properties";
 
-const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data
-
 this.EXPORTED_SYMBOLS = ["VariablesViewController", "StackFrameUtils"];
 
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(DBG_STRINGS_URI);
 
 /**
  * Controller for a VariablesView that handles interfacing with the debugger
  * protocol. Is able to populate scopes and variables via the protocol as well
  * as manage actor lifespans.
  *
  * @param VariablesView aView
  *        The view to attach to.
@@ -190,17 +193,17 @@ VariablesViewController.prototype = {
         propertyIterator: aIterator,
         start: start,
         count: count
       };
 
       // Query the name of the first and last items for this slice
       let deferred = promise.defer();
       aIterator.names([start, start + count - 1], ({ names }) => {
-        let label = "[" + names[0] + ELLIPSIS + names[1] + "]";
+        let label = "[" + names[0] + L10N.ellipsis + names[1] + "]";
         let item = aTarget.addItem(label);
         item.showArrow();
         this.addExpander(item, sliceGrip);
         deferred.resolve();
       });
       promises.push(deferred.promise);
     }
 
@@ -786,13 +789,8 @@ var StackFrameUtils = this.StackFrameUti
         label += " [" +
           (f.name || f.userDisplayName || f.displayName || "(anonymous)") +
         "]";
         break;
     }
     return label;
   }
 };
-
-/**
- * Localization convenience methods.
- */
-var L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);
--- a/devtools/client/shared/widgets/ViewHelpers.jsm
+++ b/devtools/client/shared/widgets/ViewHelpers.jsm
@@ -302,270 +302,16 @@ this.ViewHelpers = {
       aPane.ownerDocument.defaultView.setTimeout(doToggle, PANE_APPEARANCE_DELAY);
     } else {
       doToggle();
     }
   }
 };
 
 /**
- * Localization convenience methods.
- *
- * @param string aStringBundleName
- *        The desired string bundle's name.
- */
-ViewHelpers.L10N = function(aStringBundleName) {
-  XPCOMUtils.defineLazyGetter(this, "stringBundle", () =>
-    Services.strings.createBundle(aStringBundleName));
-
-  XPCOMUtils.defineLazyGetter(this, "ellipsis", () =>
-    Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data);
-};
-
-ViewHelpers.L10N.prototype = {
-  stringBundle: null,
-
-  /**
-   * L10N shortcut function.
-   *
-   * @param string aName
-   * @return string
-   */
-  getStr: function(aName) {
-    return this.stringBundle.GetStringFromName(aName);
-  },
-
-  /**
-   * L10N shortcut function.
-   *
-   * @param string aName
-   * @param array aArgs
-   * @return string
-   */
-  getFormatStr: function(aName, ...aArgs) {
-    return this.stringBundle.formatStringFromName(aName, aArgs, aArgs.length);
-  },
-
-  /**
-   * L10N shortcut function for numeric arguments that need to be formatted.
-   * All numeric arguments will be fixed to 2 decimals and given a localized
-   * decimal separator. Other arguments will be left alone.
-   *
-   * @param string aName
-   * @param array aArgs
-   * @return string
-   */
-  getFormatStrWithNumbers: function(aName, ...aArgs) {
-    let newArgs = aArgs.map(x => typeof x == "number" ? this.numberWithDecimals(x, 2) : x);
-    return this.stringBundle.formatStringFromName(aName, newArgs, newArgs.length);
-  },
-
-  /**
-   * Converts a number to a locale-aware string format and keeps a certain
-   * number of decimals.
-   *
-   * @param number aNumber
-   *        The number to convert.
-   * @param number aDecimals [optional]
-   *        Total decimals to keep.
-   * @return string
-   *         The localized number as a string.
-   */
-  numberWithDecimals: function(aNumber, aDecimals = 0) {
-    // If this is an integer, don't do anything special.
-    if (aNumber == (aNumber | 0)) {
-      return aNumber;
-    }
-    if (isNaN(aNumber) || aNumber == null) {
-      return "0";
-    }
-    let localized = aNumber.toLocaleString(); // localize
-
-    // If no grouping or decimal separators are available, bail out, because
-    // padding with zeros at the end of the string won't make sense anymore.
-    if (!localized.match(/[^\d]/)) {
-      return localized;
-    }
-
-    return aNumber.toLocaleString(undefined, {
-      maximumFractionDigits: aDecimals,
-      minimumFractionDigits: aDecimals
-    });
-  }
-};
-
-/**
- * A helper for having the same interface as ViewHelpers.L10N, but for
- * more than one file. Useful for abstracting l10n string locations.
- */
-ViewHelpers.MultiL10N = function(aStringBundleNames) {
-  let l10ns = aStringBundleNames.map(bundle => new ViewHelpers.L10N(bundle));
-  let proto = ViewHelpers.L10N.prototype;
-
-  Object.getOwnPropertyNames(proto)
-    .map(name => ({
-      name: name,
-      desc: Object.getOwnPropertyDescriptor(proto, name)
-    }))
-    .filter(property => property.desc.value instanceof Function)
-    .forEach(method => {
-      this[method.name] = function(...args) {
-        for (let l10n of l10ns) {
-          try { return method.desc.value.apply(l10n, args) } catch (e) {}
-        }
-      };
-    });
-};
-
-/**
- * Shortcuts for lazily accessing and setting various preferences.
- * Usage:
- *   let prefs = new ViewHelpers.Prefs("root.path.to.branch", {
- *     myIntPref: ["Int", "leaf.path.to.my-int-pref"],
- *     myCharPref: ["Char", "leaf.path.to.my-char-pref"],
- *     myJsonPref: ["Json", "leaf.path.to.my-json-pref"],
- *     myFloatPref: ["Float", "leaf.path.to.my-float-pref"]
- *     ...
- *   });
- *
- * Get/set:
- *   prefs.myCharPref = "foo";
- *   let aux = prefs.myCharPref;
- *
- * Observe:
- *   prefs.registerObserver();
- *   prefs.on("pref-changed", (prefName, prefValue) => {
- *     ...
- *   });
- *
- * @param string aPrefsRoot
- *        The root path to the required preferences branch.
- * @param object aPrefsBlueprint
- *        An object containing { accessorName: [prefType, prefName] } keys.
- * @param object aOptions
- *        Additional options for this constructor. Currently supported:
- *          - monitorChanges: true to update the stored values if they changed
- *                            when somebody edits about:config or the prefs
- *                            change somewhere else.
- */
-ViewHelpers.Prefs = function(aPrefsRoot = "", aPrefsBlueprint = {}, aOptions = {}) {
-  EventEmitter.decorate(this);
-
-  this._cache = new Map();
-  let self = this;
-
-  for (let [accessorName, [prefType, prefName]] of Iterator(aPrefsBlueprint)) {
-    this._map(accessorName, prefType, aPrefsRoot, prefName);
-  }
-
-  let observer = {
-    register: function() {
-      this.branch = Services.prefs.getBranch(aPrefsRoot + ".");
-      this.branch.addObserver("", this, false);
-    },
-    unregister: function() {
-      this.branch.removeObserver("", this);
-    },
-    observe: function(_, __, aPrefName) {
-      // If this particular pref isn't handled by the blueprint object,
-      // even though it's in the specified branch, ignore it.
-      let accessor = self._accessor(aPrefsBlueprint, aPrefName);
-      if (!(accessor in self)) {
-        return;
-      }
-      self._cache.delete(aPrefName);
-      self.emit("pref-changed", accessor, self[accessor]);
-    }
-  };
-
-  this.registerObserver = () => observer.register();
-  this.unregisterObserver = () => observer.unregister();
-
-  if (aOptions.monitorChanges) {
-    this.registerObserver();
-  }
-};
-
-ViewHelpers.Prefs.prototype = {
-  /**
-   * Helper method for getting a pref value.
-   *
-   * @param string aType
-   * @param string aPrefsRoot
-   * @param string aPrefName
-   * @return any
-   */
-  _get: function(aType, aPrefsRoot, aPrefName) {
-    let cachedPref = this._cache.get(aPrefName);
-    if (cachedPref !== undefined) {
-      return cachedPref;
-    }
-    let value = Services.prefs["get" + aType + "Pref"]([aPrefsRoot, aPrefName].join("."));
-    this._cache.set(aPrefName, value);
-    return value;
-  },
-
-  /**
-   * Helper method for setting a pref value.
-   *
-   * @param string aType
-   * @param string aPrefsRoot
-   * @param string aPrefName
-   * @param any aValue
-   */
-  _set: function(aType, aPrefsRoot, aPrefName, aValue) {
-    Services.prefs["set" + aType + "Pref"]([aPrefsRoot, aPrefName].join("."), aValue);
-    this._cache.set(aPrefName, aValue);
-  },
-
-  /**
-   * Maps a property name to a pref, defining lazy getters and setters.
-   * Supported types are "Bool", "Char", "Int", "Float" (sugar around "Char" type and casting),
-   * and "Json" (which is basically just sugar for "Char" using the standard JSON serializer).
-   *
-   * @param string aAccessorName
-   * @param string aType
-   * @param string aPrefsRoot
-   * @param string aPrefName
-   * @param array aSerializer
-   */
-  _map: function(aAccessorName, aType, aPrefsRoot, aPrefName, aSerializer = { in: e => e, out: e => e }) {
-    if (aPrefName in this) {
-      throw new Error(`Can't use ${aPrefName} because it's already a property.`);
-    }
-    if (aType == "Json") {
-      this._map(aAccessorName, "Char", aPrefsRoot, aPrefName, { in: JSON.parse, out: JSON.stringify });
-      return;
-    }
-    if (aType == "Float") {
-      this._map(aAccessorName, "Char", aPrefsRoot, aPrefName, { in: Number.parseFloat, out: (n) => n + ""});
-      return;
-    }
-
-    Object.defineProperty(this, aAccessorName, {
-      get: () => aSerializer.in(this._get(aType, aPrefsRoot, aPrefName)),
-      set: (e) => this._set(aType, aPrefsRoot, aPrefName, aSerializer.out(e))
-    });
-  },
-
-  /**
-   * Finds the accessor in this object for the provided property name,
-   * based on the blueprint object used in the constructor.
-   */
-  _accessor: function(aPrefsBlueprint, aPrefName) {
-    for (let [accessorName, [, prefName]] of Iterator(aPrefsBlueprint)) {
-      if (prefName == aPrefName) {
-        return accessorName;
-      }
-    }
-    return null;
-  }
-};
-
-/**
  * A generic Item is used to describe children present in a Widget.
  *
  * This is basically a very thin wrapper around an nsIDOMNode, with a few
  * characteristics, like a `value` and an `attachment`.
  *
  * The characteristics are optional, and their meaning is entirely up to you.
  * - The `value` should be a string, passed as an argument.
  * - The `attachment` is any kind of primitive or object, passed as an argument.
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -2,31 +2,32 @@
 /* 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 {Cu} = require("chrome");
 const EventEmitter = require("devtools/shared/event-emitter");
+const {LocalizationHelper} = require("devtools/client/shared/l10n");
 
 loader.lazyRequireGetter(this, "TreeWidget",
                          "devtools/client/shared/widgets/TreeWidget", true);
 loader.lazyRequireGetter(this, "TableWidget",
                          "devtools/client/shared/widgets/TableWidget", true);
 loader.lazyImporter(this, "ViewHelpers",
   "resource://devtools/client/shared/widgets/ViewHelpers.jsm");
 loader.lazyImporter(this, "VariablesView",
   "resource://devtools/client/shared/widgets/VariablesView.jsm");
 
 /**
  * Localization convenience methods.
  */
 const STORAGE_STRINGS = "chrome://devtools/locale/storage.properties";
-const L10N = new ViewHelpers.L10N(STORAGE_STRINGS);
+const L10N = new LocalizationHelper(STORAGE_STRINGS);
 
 const GENERIC_VARIABLES_VIEW_SETTINGS = {
   lazyEmpty: true,
    // ms
   lazyEmptyDelay: 10,
   searchEnabled: true,
   searchPlaceholder: L10N.getStr("storage.search.placeholder"),
   preventDescriptorModifiers: true
--- a/devtools/client/webaudioeditor/includes.js
+++ b/devtools/client/webaudioeditor/includes.js
@@ -11,21 +11,23 @@ Cu.import("resource://devtools/client/sh
 const { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 
 var { console } = Cu.import("resource://gre/modules/Console.jsm", {});
 var { EventTarget } = require("sdk/event/target");
 
 const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 const { Class } = require("sdk/core/heritage");
 const EventEmitter = require("devtools/shared/event-emitter");
-const STRINGS_URI = "chrome://devtools/locale/webaudioeditor.properties"
-const L10N = new ViewHelpers.L10N(STRINGS_URI);
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const Services = require("Services");
 const { gDevTools } = require("devtools/client/framework/devtools");
+const { LocalizationHelper } = require("devtools/client/shared/l10n");
+
+const STRINGS_URI = "chrome://devtools/locale/webaudioeditor.properties"
+const L10N = new LocalizationHelper(STRINGS_URI);
 
 loader.lazyRequireGetter(this, "LineGraphWidget",
   "devtools/client/shared/widgets/LineGraphWidget");
 
 // `AUDIO_NODE_DEFINITION` defined in the controller's initialization,
 // which describes all the properties of an AudioNode
 var AUDIO_NODE_DEFINITION;
 
--- a/devtools/server/actors/script.js
+++ b/devtools/server/actors/script.js
@@ -1255,43 +1255,45 @@ ThreadActor.prototype = {
     let i = 0;
     while (frame && (i < start)) {
       frame = frame.older;
       i++;
     }
 
     // Return request.count frames, or all remaining
     // frames if count is not defined.
-    let frames = [];
     let promises = [];
     for (; frame && (!count || i < (start + count)); i++, frame=frame.older) {
       let form = this._createFrameActor(frame).form();
       form.depth = i;
 
       let promise = this.sources.getOriginalLocation(new GeneratedLocation(
         this.sources.createNonSourceMappedActor(frame.script.source),
         form.where.line,
         form.where.column
       )).then((originalLocation) => {
-        if (originalLocation.originalSourceActor) {
-          let sourceForm = originalLocation.originalSourceActor.form();
-          form.where = {
-            source: sourceForm,
-            line: originalLocation.originalLine,
-            column: originalLocation.originalColumn
-          };
-          form.source = sourceForm;
-          frames.push(form);
+        if (!originalLocation.originalSourceActor) {
+          return null;
         }
+
+        let sourceForm = originalLocation.originalSourceActor.form();
+        form.where = {
+          source: sourceForm,
+          line: originalLocation.originalLine,
+          column: originalLocation.originalColumn
+        };
+        form.source = sourceForm;
+        return form;
       });
       promises.push(promise);
     }
 
-    return all(promises).then(function () {
-      return { frames: frames };
+    return all(promises).then(function (frames) {
+      // Filter null values because sourcemapping may have failed.
+      return { frames: frames.filter(x => !!x) };
     });
   },
 
   onReleaseMany: function (aRequest) {
     if (!aRequest.actors) {
       return { error: "missingParameter",
                message: "no actors were specified" };
     }
--- a/devtools/server/tests/unit/test_sourcemaps-17.js
+++ b/devtools/server/tests/unit/test_sourcemaps-17.js
@@ -22,33 +22,42 @@ function run_test() {
       test_source_map();
     });
   });
   do_test_pending();
 }
 
 function test_source_map() {
    // Set up debuggee code.
-  const a = new SourceNode(1, 1, "foo.js", "function a() { b(); }");
+  const a = new SourceNode(1, 1, "a.js", "function a() { b(); }");
   const b = new SourceNode(null, null, null, "function b() { c(); }");
-  const c = new SourceNode(2, 1, "foo.js", "function c() { debugger; }");
-  const { map, code } = (new SourceNode(null, null, null, [a,b,c])).toStringWithSourceMap({
-    file: "bar.js",
+  const c = new SourceNode(1, 1, "c.js", "function c() { d(); }");
+  const d = new SourceNode(null, null, null, "function d() { e(); }");
+  const e = new SourceNode(1, 1, "e.js", "function e() { debugger; }");
+  const { map, code } = (new SourceNode(null, null, null, [a,b,c,d,e])).toStringWithSourceMap({
+    file: "root.js",
     sourceRoot: "root",
   });
   Components.utils.evalInSandbox(
     code + "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString()),
     gDebuggee,
     "1.8",
     "http://example.com/www/js/abc.js",
     1
   );
 
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
-    gThreadClient.fillFrames(50, function(res) {
-      do_check_true(!res.error, "Should not get an error: " + res.error);
+    gThreadClient.getFrames(0, 50, function({ error, frames }) {
+      do_check_true(!error);
+      do_check_eq(frames.length, 4);
+      // b.js should be skipped
+      do_check_eq(frames[0].where.source.url, "http://example.com/www/root/e.js");
+      do_check_eq(frames[1].where.source.url, "http://example.com/www/root/c.js");
+      do_check_eq(frames[2].where.source.url, "http://example.com/www/root/a.js");
+      do_check_eq(frames[3].where.source.url, null);
+
       finishClient(gClient);
     })
   });
 
     // Trigger it.
   gDebuggee.eval("a()");
 }
--- a/js/public/ProfilingStack.h
+++ b/js/public/ProfilingStack.h
@@ -70,17 +70,17 @@ class ProfileEntry
 
         // Union of all flags.
         ALL = IS_CPP_ENTRY|FRAME_LABEL_COPY|BEGIN_PSEUDO_JS|OSR,
 
         // Mask for removing all flags except the category information.
         CATEGORY_MASK = ~ALL
     };
 
-    // Keep these in sync with devtools/client/performance/modules/global.js
+    // Keep these in sync with devtools/client/performance/modules/categories.js
     enum class Category : uint32_t {
         OTHER    = 0x10,
         CSS      = 0x20,
         JS       = 0x40,
         GC       = 0x80,
         CC       = 0x100,
         NETWORK  = 0x200,
         GRAPHICS = 0x400,
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -136,16 +136,28 @@
 
             <!-- For debugging -->
             <intent-filter>
                 <action android:name="org.mozilla.gecko.DEBUG" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
 
+        <!-- Bug 1256615: We published a .App alias and we need to maintain it
+             forever.  If we don't, dock icons (e.g., Samsung Touchwiz icons)
+             will disappear because the intent filter details change. -->
+        <activity-alias android:name=".App"
+                        android:label="@MOZ_APP_DISPLAYNAME@"
+                        android:targetActivity="@MOZ_ANDROID_BROWSER_INTENT_CLASS@">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity-alias>
+
         <service android:name="org.mozilla.gecko.GeckoService" />
 
         <activity android:name="org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt"
                   android:launchMode="singleTop"
                   android:theme="@style/OverlayActivity" />
 
         <!-- The main reason for the Tab Queue build flag is to not mess with the VIEW intent filter
              before the rest of the plumbing is in place -->
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -732,16 +732,18 @@
     </style>
 
     <style name="TextAppearance.ShareOverlay">
         <item name="android:fontFamily">sans-serif</item>
     </style>
 
     <style name="TextAppearance.ShareOverlay.Header">
         <item name="android:textColor">@android:color/white</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:textStyle">normal</item>
     </style>
 
     <style name="ShareOverlayRow">
         <item name="android:minHeight">60dp</item>
         <item name="android:gravity">center_vertical</item>
         <item name="android:background">@drawable/overlay_share_button_background</item>
         <item name="android:focusableInTouchMode">false</item>
     </style>
deleted file mode 100644
--- a/mobile/android/config/tooltool-manifests/android-armv6/releng.manifest
+++ /dev/null
@@ -1,47 +0,0 @@
-[
-{
-"version": "Android NDK r8e",
-"size": 78706854,
-"digest": "8ff42509ecebfd7e20f8fac9987ed2b2c04942641eead674ee66f74014c5153f1c20080cd3ccb243af76ca7432df3c3f5b5ae08a478fd2817e62661a4edb437c",
-"algorithm": "sha512",
-"filename": "android-ndk.tar.bz2",
-"unpack": true
-},
-{
-"versions": [
-  "Android SDK 6.0 / API 23",
-  "Android tools r24.4",
-  "Android build tools 23.0.1",
-  "Android Support Repository (Support Library 23.0.1)",
-  "Google Support Repository (Google Play Services 8.1.0)"
-],
-"size": 535625068,
-"visibility": "internal", 
-"digest": "0627515046a23c1d109e2782865b1b3b546c1d552955e4156317f76cbb195eb630aa25feea3f4edd1c685f129da0c2a5169d4d6349c1c31d8a95158a4569a478",
-"algorithm": "sha512",
-"filename": "android-sdk-linux.tar.xz",
-"unpack": true
-},
-{
-"size": 167175,
-"digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
-"algorithm": "sha512",
-"filename": "sccache.tar.bz2",
-"unpack": true
-},
-{
-"size": 4906080,
-"digest": "d735544e039da89382c53b2302b7408d4610247b4f8b5cdc5a4d5a8ec5470947b19e8ea7f7a37e78222e661347e394e0030d81f41534138b527b14e9c4e55634",
-"algorithm": "sha512",
-"filename": "jsshell.tar.xz",
-"unpack": true
-},
-{
-"version": "gcc 4.8.5",
-"size": 81065660,
-"digest": "db26f498ab56a3b5c65d7cda290cbb74174af9f2d021ca9c158f53b0382924ccf5ed9638d41eef449434aa9383a9113994d9729d9dd910321d1f35f9411eae38",
-"algorithm": "sha512",
-"filename": "gcc.tar.xz",
-"unpack": true
-}
-]
--- a/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
@@ -30,18 +30,18 @@
 "filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
 "unpack": true
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "jcentral.tar.xz",
 "unpack": true,
-"digest": "b5d85a917785e1c034318f7495fef27a6274b04d8640245726b0cf1331b7ac374f5757868901c3fadd930bf10603173a706be653d769dde8ddfdb8673b143363",
-"size": 38596168
+"digest": "31a8c573f002e5f9a09c5ae4fd768bebb3ea29006c78bb1d353ccbb48be9dcaaeffaf8a32778f0bbb516bea3112c59067f5d17535f31cc7e8abfd8945b139642",
+"size": 38606248
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "gradle.tar.xz",
 "unpack": true,
 "digest": "ef1d0038da879cc6840fced87671f8f6a18c51375498804f64d21fa48d7089ded4da2be36bd06a1457083e9110e59c0884f1e074dc609d29617c131caea8f234",
 "size": 50542140
--- a/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
@@ -39,10 +39,26 @@
 },
 {
 "version": "gcc 4.8.5",
 "size": 81065660,
 "digest": "db26f498ab56a3b5c65d7cda290cbb74174af9f2d021ca9c158f53b0382924ccf5ed9638d41eef449434aa9383a9113994d9729d9dd910321d1f35f9411eae38",
 "algorithm": "sha512",
 "filename": "gcc.tar.xz",
 "unpack": true
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "jcentral.tar.xz",
+"unpack": true,
+"digest": "31a8c573f002e5f9a09c5ae4fd768bebb3ea29006c78bb1d353ccbb48be9dcaaeffaf8a32778f0bbb516bea3112c59067f5d17535f31cc7e8abfd8945b139642",
+"size": 38606248
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "gradle.tar.xz",
+"unpack": true,
+"digest": "ef1d0038da879cc6840fced87671f8f6a18c51375498804f64d21fa48d7089ded4da2be36bd06a1457083e9110e59c0884f1e074dc609d29617c131caea8f234",
+"size": 50542140
 }
 ]
--- a/mobile/android/config/tooltool-manifests/android/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android/releng.manifest
@@ -50,10 +50,26 @@
 },
 {
 "size": 30899096,
 "visibility": "public",
 "digest": "ac9f5f95d11580d3dbeff87e80a585fe4d324b270dabb91b1165686acab47d99fa6651074ab0be09420239a5d6af38bb2c539506962a7b44e0ed4d080bba2953",
 "algorithm": "sha512",
 "filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
 "unpack": true
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "jcentral.tar.xz",
+"unpack": true,
+"digest": "31a8c573f002e5f9a09c5ae4fd768bebb3ea29006c78bb1d353ccbb48be9dcaaeffaf8a32778f0bbb516bea3112c59067f5d17535f31cc7e8abfd8945b139642",
+"size": 38606248
+},
+{
+"algorithm": "sha512",
+"visibility": "public",
+"filename": "gradle.tar.xz",
+"unpack": true,
+"digest": "ef1d0038da879cc6840fced87671f8f6a18c51375498804f64d21fa48d7089ded4da2be36bd06a1457083e9110e59c0884f1e074dc609d29617c131caea8f234",
+"size": 50542140
 }
 ]
new file mode 100644
--- /dev/null
+++ b/mobile/android/gradle.configure
@@ -0,0 +1,49 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# If --with-gradle is specified, build mobile/android with Gradle.  If no
+# Gradle binary is specified, or if --without-gradle is specified, use the in
+# tree Gradle wrapper.  The wrapper downloads and installs Gradle, which is
+# good for local developers but not good in automation.
+option('--with-gradle', nargs='?',
+       help='Enable building mobile/android with Gradle '
+            '(argument: location of binary or wrapper (gradle/gradlew))')
+
+@depends('--with-gradle', check_build_environment)
+def gradle(value, build_env):
+    if value:
+        set_config('MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE', '1')
+
+    gradle = value[0] if len(value) else \
+        os.path.join(build_env['TOPSRCDIR'], 'gradlew')
+
+    # TODO: verify that $GRADLE is executable.
+    if not os.path.isfile(gradle):
+        error('GRADLE must be executable: %s' % gradle)
+
+    set_config('GRADLE', gradle)
+
+    return gradle
+
+
+# Automation uses this to change log levels, not use the daemon, and use
+# offline mode.
+option(env='GRADLE_FLAGS', default='', help='Flags to pass to Gradle.')
+
+@depends('GRADLE_FLAGS')
+def gradle_flags(value):
+    set_config('GRADLE_FLAGS', value[0] if value else '')
+
+
+# Automation will set this to file:///path/to/local via the mozconfig.
+# Local developer default is jcenter.
+option(env='GRADLE_MAVEN_REPOSITORY', default='https://jcenter.bintray.com/',
+       help='Path to Maven repository containing Gradle dependencies.')
+
+@depends('GRADLE_MAVEN_REPOSITORY')
+def gradle_maven_repository(value):
+    if value:
+        set_config('GRADLE_MAVEN_REPOSITORY', value[0])
--- a/mobile/android/mach_commands.py
+++ b/mobile/android/mach_commands.py
@@ -10,16 +10,20 @@ import os
 
 import mozpack.path as mozpath
 
 from mozbuild.base import (
     MachCommandBase,
     MachCommandConditions as conditions,
 )
 
+from mozbuild.shellutil import (
+    split as shell_split,
+)
+
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
 
 # NOTE python/mach/mach/commands/commandinfo.py references this function
@@ -58,26 +62,28 @@ class MachCommands(MachCommandBase):
         self.log_manager.terminal_handler.setLevel(logging.CRITICAL)
 
 
         # In automation, JAVA_HOME is set via mozconfig, which needs
         # to be specially handled in each mach command. This turns
         # $JAVA_HOME/bin/java into $JAVA_HOME.
         java_home = os.path.dirname(os.path.dirname(self.substs['JAVA']))
 
+        gradle_flags = shell_split(self.substs.get('GRADLE_FLAGS', ''))
+
         # We force the Gradle JVM to run with the UTF-8 encoding, since we
         # filter strings.xml, which is really UTF-8; the ellipsis character is
         # replaced with ??? in some encodings (including ASCII).  It's not yet
         # possible to filter with encodings in Gradle
         # (https://github.com/gradle/gradle/pull/520) and it's challenging to
         # do our filtering with Gradle's Ant support.  Moreover, all of the
         # Android tools expect UTF-8: see
         # http://tools.android.com/knownissues/encoding.  See
         # http://stackoverflow.com/a/21267635 for discussion of this approach.
-        return self.run_process([self.substs['GRADLE']] + args,
+        return self.run_process([self.substs['GRADLE']] + gradle_flags + args,
             append_env={
                 'GRADLE_OPTS': '-Dfile.encoding=utf-8',
                 'JAVA_HOME': java_home,
             },
             pass_thru=True, # Allow user to run gradle interactively.
             ensure_exit_code=False, # Don't throw on non-zero exit code.
             cwd=mozpath.join(self.topsrcdir))
 
--- a/mobile/android/moz.build
+++ b/mobile/android/moz.build
@@ -24,14 +24,13 @@ DIRS += [
     'app',
     'fonts',
     'geckoview_library',
 ]
 
 if CONFIG['MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER']:
     DIRS += ['bouncer'] # No ordering implied with respect to base.
 
-if not CONFIG['MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE']:
-    TEST_DIRS += [
-        'tests',
-    ]
+TEST_DIRS += [
+    'tests',
+]
 
 SPHINX_TREES['fennec'] = 'docs'
--- a/mobile/android/moz.configure
+++ b/mobile/android/moz.configure
@@ -1,7 +1,8 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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('../../toolkit/moz.configure')
+include('gradle.configure')
--- a/mobile/android/tests/browser/moz.build
+++ b/mobile/android/tests/browser/moz.build
@@ -1,13 +1,17 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 MOCHITEST_CHROME_MANIFESTS += ['chrome/chrome.ini']
 
+if not CONFIG['MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE']:
+    TEST_DIRS += [
+        'junit3',
+    ]
+
 TEST_DIRS += [
-    'junit3',
     'robocop/roboextender',
     'robocop',
 ]
--- a/mobile/android/tests/browser/robocop/Makefile.in
+++ b/mobile/android/tests/browser/robocop/Makefile.in
@@ -49,17 +49,19 @@ GARBAGE += \
 
 JAVAFILES += \
   $(java-harness) \
   $(java-tests) \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
+ifndef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
 tools:: $(ANDROID_APK_NAME).apk
+endif
 
 # The test APK needs to know the contents of the target APK while not
 # being linked against them.  This is a best effort to avoid getting
 # out of sync with base's build config.
 jars_dir := $(DEPTH)/mobile/android/base
 stumbler_jars_dir := $(DEPTH)/mobile/android/stumbler
 ANDROID_CLASSPATH_JARS += \
   $(wildcard $(jars_dir)/*.jar) \
--- a/mobile/android/tests/moz.build
+++ b/mobile/android/tests/moz.build
@@ -1,14 +1,18 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
+if not CONFIG['MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE']:
+    TEST_DIRS += [
+        'background',
+    ]
+
 TEST_DIRS += [
-    'background',
     'browser',
     'javaaddons', # Must be built before browser/robocop/roboextender.
                   # This is enforced in config/recurse.mk.
 ]
 
 ANDROID_INSTRUMENTATION_MANIFESTS += ['browser/robocop/robocop.ini']
--- a/mobile/android/themes/core/aboutReaderControls.css
+++ b/mobile/android/themes/core/aboutReaderControls.css
@@ -57,16 +57,17 @@
   font-family: sans-serif;
   position: fixed;
   width: 100%;
   left: 0;
   margin: 0;
   padding: 0;
   bottom: 0;
   list-style: none;
+  pointer-events: none;
 }
 
 .toolbar > * {
   float: right;
 }
 
 .button {
   width: 56px;
@@ -91,16 +92,21 @@
 .dropdown-popup > div > button::-moz-focus-inner {
   border: 0;
 }
 
 .button[hidden] {
   display: none;
 }
 
+.dropdown-toggle,
+#reader-popup {
+  pointer-events: auto;
+}
+
 .dropdown {
   left: 0;
   text-align: center;
   display: inline-block;
   list-style: none;
   margin: 0px;
   padding: 0px;
 }
--- a/old-configure.in
+++ b/old-configure.in
@@ -2808,17 +2808,16 @@ MOZ_PEERCONNECTION=
 MOZ_SRTP=
 MOZ_WEBRTC_SIGNALING=
 MOZ_WEBRTC_ASSERT_ALWAYS=1
 MOZ_WEBRTC_HARDWARE_AEC_NS=
 MOZ_SCTP=
 MOZ_ANDROID_OMX=
 MOZ_MEDIA_NAVIGATOR=
 MOZ_OMX_PLUGIN=
-MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE=
 MOZ_VPX_ERROR_CONCEALMENT=
 MOZ_WEBSPEECH=1
 MOZ_WEBSPEECH_MODELS=
 MOZ_WEBSPEECH_POCKETSPHINX=
 MOZ_WEBSPEECH_TEST_BACKEND=1
 VPX_USE_YASM=
 VPX_ASFLAGS=
 VPX_AS_CONVERSION=
@@ -4135,78 +4134,16 @@ if test -n "$MOZ_OMX_PLUGIN"; then
         dnl Only allow building OMX plugin on Gonk (B2G) or Android
         AC_DEFINE(MOZ_OMX_PLUGIN)
     else
         dnl fail if we're not building on Gonk or Android
         AC_MSG_ERROR([OMX media plugin can only be built on B2G or Android])
     fi
 fi
 
-dnl ========================================================
-dnl Gradle support
-dnl
-dnl If --with-gradle is specified, build mobile/android with Gradle.
-dnl
-dnl If no Gradle binary is specified, use the in tree Gradle wrapper.
-dnl The wrapper downloads and installs Gradle, which is good for local
-dnl developers but not good in automation.
-dnl ========================================================
-
-GRADLE=
-MOZ_ARG_WITH_STRING(gradle,
-[  --with-gradle=/path/to/bin/gradle
-                          Enable building mobile/android with Gradle (argument: location of binary or wrapper (gradle/gradlew))],
-    if test "$withval" = "no" ; then
-        dnl --without-gradle => use the wrapper in |mach gradle|, don't build
-        dnl with Gradle by default.
-        GRADLE=$srcdir/gradlew
-        MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE=
-    elif test "$withval" = "yes" ; then
-        dnl --with-gradle => use the wrapper in |mach gradle|, build with
-        dnl Gradle by default.
-        GRADLE=$srcdir/gradlew
-        MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE=1
-    else
-        dnl --with-gradle=/path/to/gradle => use the given binary in |mach
-        dnl gradle|, build with Gradle by default.
-        GRADLE=$withval
-        MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE=1
-    fi
-    ,
-    dnl No --with{out}-gradle => use the wrapper in |mach gradle|, don't build
-    dnl with Gradle by default.
-    GRADLE=$srcdir/gradlew
-    MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE=
-    )
-
-if test "$OS_TARGET" = Android -a x"$MOZ_WIDGET_TOOLKIT" != x"gonk" ; then
-    if test -z "$GRADLE" -o ! -x "$GRADLE" ; then
-        AC_MSG_ERROR([The program gradlew/gradle was not found.  Use --with-gradle=/path/to/bin/gradle}])
-    fi
-fi
-AC_SUBST(GRADLE)
-
-dnl Path to Maven repository containing Gradle dependencies.  Automation will
-dnl set this to file:///path/to/local via the mozconfig.  Local developer
-dnl default is jcenter.
-if test -z "$GRADLE_MAVEN_REPOSITORY" ; then
-    GRADLE_MAVEN_REPOSITORY=https://jcenter.bintray.com/
-fi
-AC_SUBST(GRADLE_MAVEN_REPOSITORY)
-
-if test -n "$MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE"; then
-    if test "$OS_TARGET" = "Android" -a x"$MOZ_WIDGET_TOOLKIT" != x"gonk"; then
-        dnl Only allow building mobile/android with Gradle.
-        AC_DEFINE(MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE)
-    else
-        dnl fail if we're not building mobile/android.
-        AC_MSG_ERROR([Can only build mobile/android with Gradle])
-    fi
-fi
-
 dnl system libvpx Support
 dnl ========================================================
 MOZ_ARG_WITH_BOOL(system-libvpx,
 [  --with-system-libvpx    Use system libvpx (located with pkgconfig)],
     MOZ_SYSTEM_LIBVPX=1)
 
 MOZ_LIBVPX_CFLAGS=
 MOZ_LIBVPX_LIBS=
@@ -7624,17 +7561,16 @@ AC_SUBST(WIN32_GUI_EXE_LDFLAGS)
 
 AC_SUBST(MOZ_VORBIS)
 AC_SUBST(MOZ_TREMOR)
 AC_SUBST(MOZ_FFVPX)
 AC_SUBST_LIST(FFVPX_ASFLAGS)
 AC_SUBST(MOZ_DIRECTSHOW)
 AC_SUBST(MOZ_ANDROID_OMX)
 AC_SUBST(MOZ_OMX_PLUGIN)
-AC_SUBST(MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE)
 AC_SUBST(MOZ_VPX_ERROR_CONCEALMENT)
 AC_SUBST(VPX_USE_YASM)
 AC_SUBST_LIST(VPX_ASFLAGS)
 AC_SUBST(VPX_AS_CONVERSION)
 AC_SUBST(VPX_X86_ASM)
 AC_SUBST(VPX_ARM_ASM)
 AC_SUBST(VPX_NEED_OBJ_INT_EXTRACT)
 AC_SUBST(MOZ_CODE_COVERAGE)
--- a/toolkit/components/passwordmgr/test/browser/browser.ini
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -12,15 +12,15 @@ support-files =
 [browser_filldoorhanger.js]
 [browser_hasInsecureLoginForms.js]
 [browser_hasInsecureLoginForms_streamConverter.js]
 [browser_notifications.js]
 skip-if = true # Intermittent failures: Bug 1182296, bug 1148771
 [browser_passwordmgr_editing.js]
 skip-if = os == "linux"
 [browser_context_menu.js]
-skip-if = os == "linux"
+skip-if = e10s
 [browser_passwordmgr_contextmenu.js]
 [browser_passwordmgr_fields.js]
 [browser_passwordmgr_observers.js]
 [browser_passwordmgr_sort.js]
 [browser_passwordmgr_switchtab.js]
 [browser_passwordmgrdlg.js]