merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 24 Feb 2016 12:04:15 +0100
changeset 285311 d848a5628d801a460a7244cbcdea22d328d8b310
parent 285290 d50ab673e9a16a34cee1124779d722225e9abbe7 (current diff)
parent 285310 9cf43731a81106da80a5ca3e3bd066d551666a9b (diff)
child 285312 3329b93589df2613f87fb475eace4b30c1cd0af4
child 285490 4f64ec5486f4694082a189d9b93730c444e0594a
child 285506 18ab16f5660e2040c7ae669112932a85217e1928
push id72311
push usercbook@mozilla.com
push dateWed, 24 Feb 2016 11:21:31 +0000
treeherdermozilla-inbound@3329b93589df [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone47.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
devtools/client/jsonview/components/reps/array.js
devtools/client/jsonview/components/reps/caption.js
devtools/client/jsonview/components/reps/null.js
devtools/client/jsonview/components/reps/number.js
devtools/client/jsonview/components/reps/object-box.js
devtools/client/jsonview/components/reps/object-link.js
devtools/client/jsonview/components/reps/object.js
devtools/client/jsonview/components/reps/rep-utils.js
devtools/client/jsonview/components/reps/rep.js
devtools/client/jsonview/components/reps/string.js
devtools/client/jsonview/components/reps/undefined.js
devtools/client/jsonview/css/reps.css
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2340,17 +2340,22 @@ function URLBarSetURI(aURI) {
     // 1. only if there's no opener (bug 370555).
     // 2. if remote newtab is enabled and it's the default remote newtab page
     let defaultRemoteURL = gAboutNewTabService.remoteEnabled &&
                            uri.spec === gAboutNewTabService.newTabURL;
     if ((gInitialPages.includes(uri.spec) || defaultRemoteURL) &&
         checkEmptyPageOrigin(gBrowser.selectedBrowser, uri)) {
       value = "";
     } else {
-      value = losslessDecodeURI(uri);
+      // We should deal with losslessDecodeURI throwing for exotic URIs
+      try {
+        value = losslessDecodeURI(uri);
+      } catch (ex) {
+        value = "about:blank";
+      }
     }
 
     valid = !isBlankPageURL(uri.spec);
   }
 
   gURLBar.value = value;
   gURLBar.valueIsTyped = !valid;
   SetPageProxyState(valid ? "valid" : "invalid");
--- a/browser/base/content/test/alerts/browser.ini
+++ b/browser/base/content/test/alerts/browser.ini
@@ -3,10 +3,11 @@ support-files =
   head.js
   file_dom_notifications.html
 
 [browser_notification_close.js]
 [browser_notification_do_not_disturb.js]
 [browser_notification_open_settings.js]
 [browser_notification_remove_permission.js]
 [browser_notification_permission_migration.js]
+[browser_notification_replace.js]
 [browser_notification_tab_switching.js]
 skip-if = buildapp == 'mulet'
--- a/browser/base/content/test/alerts/browser_notification_close.js
+++ b/browser/base/content/test/alerts/browser_notification_close.js
@@ -23,17 +23,17 @@ add_task(function* test_notificationClos
       ok(true, "Notifications don't use XUL windows on all platforms.");
       notification.close();
       return;
     }
 
     let alertTitleLabel = alertWindow.document.getElementById("alertTitleLabel");
     is(alertTitleLabel.value, "Test title", "Title text of notification should be present");
     let alertTextLabel = alertWindow.document.getElementById("alertTextLabel");
-    is(alertTextLabel.textContent, "Test body", "Body text of notification should be present");
+    is(alertTextLabel.textContent, "Test body 2", "Body text of notification should be present");
 
     let alertCloseButton = alertWindow.document.querySelector(".alertCloseButton");
     is(alertCloseButton.localName, "toolbarbutton", "close button found");
     let promiseBeforeUnloadEvent =
       BrowserTestUtils.waitForEvent(alertWindow, "beforeunload");
     let closedTime = alertWindow.Date.now();
     alertCloseButton.click();
     info("Clicked on close button");
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_replace.js
@@ -0,0 +1,38 @@
+"use strict";
+
+let notificationURL = "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+
+add_task(function* test_notificationReplace() {
+  let pm = Services.perms;
+  pm.add(makeURI(notificationURL), "desktop-notification", pm.ALLOW_ACTION);
+
+  yield BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: notificationURL
+  }, function* dummyTabTask(aBrowser) {
+    yield ContentTask.spawn(aBrowser, {}, function* () {
+      let win = content.window.wrappedJSObject;
+      let notification = win.showNotification1();
+      let promiseCloseEvent = ContentTaskUtils.waitForEvent(notification, "close");
+
+      let showEvent = yield ContentTaskUtils.waitForEvent(notification, "show");
+      is(showEvent.target.body, "Test body 1", "Showed tagged notification");
+
+      let newNotification = win.showNotification2();
+      let newShowEvent = yield ContentTaskUtils.waitForEvent(newNotification, "show");
+      is(newShowEvent.target.body, "Test body 2", "Showed new notification with same tag");
+
+      let closeEvent = yield promiseCloseEvent;
+      is(closeEvent.target.body, "Test body 1", "Closed previous tagged notification");
+
+      let promiseNewCloseEvent = ContentTaskUtils.waitForEvent(newNotification, "close");
+      newNotification.close();
+      let newCloseEvent = yield promiseNewCloseEvent;
+      is(newCloseEvent.target.body, "Test body 2", "Closed new notification");
+    });
+  });
+});
+
+add_task(function* cleanup() {
+  Services.perms.remove(makeURI(notificationURL), "desktop-notification");
+});
--- a/browser/base/content/test/alerts/file_dom_notifications.html
+++ b/browser/base/content/test/alerts/file_dom_notifications.html
@@ -3,32 +3,32 @@
 <meta charset="utf-8">
 <script>
 "use strict";
 
 function showNotification1() {
   var options = {
       dir: undefined,
       lang: undefined,
-      body: "Test body",
+      body: "Test body 1",
       tag: "Test tag",
       icon: undefined,
   };
   var n = new Notification("Test title", options);
   n.addEventListener("click", function(event) {
     event.preventDefault();
   });
   return n;
 }
 
 function showNotification2() {
   var options = {
       dir: undefined,
       lang: undefined,
-      body: "Test body",
+      body: "Test body 2",
       tag: "Test tag",
       icon: undefined,
   };
   return new Notification("Test title", options);
 }
 </script>
 </head>
 <body>
--- a/browser/base/content/test/general/browser_urlHighlight.js
+++ b/browser/base/content/test/general/browser_urlHighlight.js
@@ -55,16 +55,19 @@ function test() {
 
   testVal("<https://sub.>mozilla.org");
   testVal("<https://sub1.sub2.sub3.>mozilla.org");
   testVal("<https://user:pass@sub1.sub2.sub3.>mozilla.org");
   testVal("<https://user:pass@>mozilla.org");
   testVal("<user:pass@sub1.sub2.sub3.>mozilla.org");
   testVal("<user:pass@>mozilla.org");
 
+  testVal("<https://>mozilla.org<   >");
+  testVal("mozilla.org<   >");
+
   testVal("<https://>mozilla.org</file.ext>");
   testVal("<https://>mozilla.org</sub/file.ext>");
   testVal("<https://>mozilla.org</sub/file.ext?foo>");
   testVal("<https://>mozilla.org</sub/file.ext?foo&bar>");
   testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>");
   testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>");
   testVal("foo.bar<?q=test>");
   testVal("foo.bar<#mozilla.org>");
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -1,3 +1,4 @@
+[browser_moz_action_link.js]
 [browser_urlbar_blanking.js]
 support-files =
   file_blank_but_not_blank.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_moz_action_link.js
@@ -0,0 +1,31 @@
+"use strict";
+
+const kURIs = [
+  "moz-action:foo,",
+  "moz-action:foo",
+];
+
+add_task(function*() {
+  for (let uri of kURIs) {
+    let dataURI = `data:text/html,<a id=a href="${uri}" target=_blank>Link</a>`;
+    let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, dataURI);
+
+    let tabSwitchPromise = BrowserTestUtils.switchTab(gBrowser, function() {});
+    yield ContentTask.spawn(tab.linkedBrowser, null, function*() {
+      content.document.getElementById("a").click();
+    });
+    yield tabSwitchPromise;
+    isnot(gBrowser.selectedTab, tab, "Switched to new tab!");
+    is(gURLBar.value, "about:blank", "URL bar should be displaying about:blank");
+    let newTab = gBrowser.selectedTab;
+    yield BrowserTestUtils.switchTab(gBrowser, tab);
+    yield BrowserTestUtils.switchTab(gBrowser, newTab);
+    is(gBrowser.selectedTab, newTab, "Switched to new tab again!");
+    is(gURLBar.value, "about:blank", "URL bar should be displaying about:blank after tab switch");
+    // Finally, check that directly setting it produces the right results, too:
+    URLBarSetURI(makeURI(uri));
+    is(gURLBar.value, "about:blank", "URL bar should still be displaying about:blank");
+    yield BrowserTestUtils.removeTab(newTab);
+    yield BrowserTestUtils.removeTab(tab);
+  }
+});
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -256,17 +256,17 @@ file, You can obtain one at http://mozil
           // trimmedLength to ensure we don't count the length of a trimmed protocol
           // when determining which parts of the URL to highlight as "preDomain".
           let trimmedLength = 0;
           if (uriInfo.fixedURI.scheme == "http" && !value.startsWith("http://")) {
             value = "http://" + value;
             trimmedLength = "http://".length;
           }
 
-          let matchedURL = value.match(/^((?:[a-z]+:\/\/)(?:[^\/#?]+@)?)(.+?)(?::\d+)?(?:[\/#?]|$)/);
+          let matchedURL = value.match(/^((?:[a-z]+:\/\/)(?:[^\/#?]+@)?)(\S+?)(?::\d+)?\s*(?:[\/#?]|$)/);
           if (!matchedURL)
             return;
 
           // Strike out the "https" part if mixed active content is loaded.
           if (this.getAttribute("pageproxystate") == "valid" &&
               value.startsWith("https:") &&
               gBrowser.securityUI.state &
                 Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) {
@@ -834,22 +834,23 @@ file, You can obtain one at http://mozil
           return this.value;
           ]]>
         </setter>
       </property>
 
       <method name="_parseActionUrl">
         <parameter name="aUrl"/>
         <body><![CDATA[
-          if (!aUrl.startsWith("moz-action:"))
+          const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/;
+          if (!MOZ_ACTION_REGEX.test(aUrl))
             return null;
 
           // URL is in the format moz-action:ACTION,PARAMS
           // Where PARAMS is a JSON encoded object.
-          let [, type, params] = aUrl.match(/^moz-action:([^,]+),(.*)$/);
+          let [, type, params] = aUrl.match(MOZ_ACTION_REGEX);
 
           let action = {
             type: type,
           };
 
           try {
             action.params = JSON.parse(params);
             for (let key in action.params) {
--- a/browser/components/uitour/UITour-lib.js
+++ b/browser/components/uitour/UITour-lib.js
@@ -229,18 +229,31 @@ if (typeof Mozilla == 'undefined') {
 
   Mozilla.UITour.setConfiguration = function(configName, configValue) {
     _sendEvent('setConfiguration', {
       configuration: configName,
       value: configValue,
     });
   };
 
-  Mozilla.UITour.showFirefoxAccounts = function() {
-    _sendEvent('showFirefoxAccounts');
+  /**
+   * Request the browser open the Firefox Accounts page.
+   *
+   * @param {Object} extraURLCampaignParams - An object containing additional
+   * paramaters for the URL opened by the browser for reasons of promotional
+   * campaign tracking. Each attribute of the object must have a name that
+   * begins with "utm_" and a value that is a string. Both the name and value
+   * must contain only alphanumeric characters, dashes or underscores (meaning
+   * that you are limited to values that don't need encoding, as any such
+   * characters in the name or value will be rejected.)
+   */
+  Mozilla.UITour.showFirefoxAccounts = function(extraURLCampaignParams) {
+    _sendEvent('showFirefoxAccounts', {
+      extraURLCampaignParams: JSON.stringify(extraURLCampaignParams),
+    });
   };
 
   Mozilla.UITour.resetFirefox = function() {
     _sendEvent('resetFirefox');
   };
 
   Mozilla.UITour.addNavBarWidget= function(name, callback) {
     _sendEvent('addNavBarWidget', {
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -605,18 +605,25 @@ this.UITour = {
 
         window.openPreferences(data.pane);
         break;
       }
 
       case "showFirefoxAccounts": {
         // 'signup' is the only action that makes sense currently, so we don't
         // accept arbitrary actions just to be safe...
+        let p = new URLSearchParams("action=signup&entrypoint=uitour");
+        // Call our helper to validate extraURLCampaignParams and populate URLSearchParams
+        if (!this._populateCampaignParams(p, data.extraURLCampaignParams)) {
+          log.warn("showFirefoxAccounts: invalid campaign args specified");
+          return false;
+        }
+
         // We want to replace the current tab.
-        browser.loadURI("about:accounts?action=signup&entrypoint=uitour");
+        browser.loadURI("about:accounts?" + p.toString());
         break;
       }
 
       case "resetFirefox": {
         // Open a reset profile dialog window.
         ResetProfile.openConfirmationDialog(window);
         break;
       }
@@ -800,16 +807,62 @@ this.UITour = {
             return;
           }
         }
         break;
       }
     }
   },
 
+  // Given a string that is a JSONified represenation of an object with
+  // additional utm_* URL params that should be appended, validate and append
+  // them to the passed URLSearchParams object. Returns true if the params
+  // were validated and appended, and false if the request should be ignored.
+  _populateCampaignParams: function(urlSearchParams, extraURLCampaignParams) {
+    // We are extra paranoid about what params we allow to be appended.
+    if (typeof extraURLCampaignParams == "undefined") {
+      // no params, so it's all good.
+      return true;
+    }
+    if (typeof extraURLCampaignParams != "string") {
+      log.warn("_populateCampaignParams: extraURLCampaignParams is not a string");
+      return false;
+    }
+    let campaignParams;
+    try {
+      if (extraURLCampaignParams) {
+        campaignParams = JSON.parse(extraURLCampaignParams);
+        if (typeof campaignParams != "object") {
+          log.warn("_populateCampaignParams: extraURLCampaignParams is not a stringified object");
+          return false;
+        }
+      }
+    } catch (ex) {
+      log.warn("_populateCampaignParams: extraURLCampaignParams is not a JSON object");
+      return false;
+    }
+    if (campaignParams) {
+      // The regex that both the name and value of each param must match.
+      let reSimpleString = /^[-_a-zA-Z0-9]*$/;
+      for (let name in campaignParams) {
+        let value = campaignParams[name];
+        if (typeof name != "string" || typeof value != "string" ||
+            !name.startsWith("utm_") ||
+            value.length == 0 ||
+            !reSimpleString.test(name) ||
+            !reSimpleString.test(value)) {
+          log.warn("_populateCampaignParams: invalid campaign param specified");
+          return false;
+        }
+        urlSearchParams.append(name, value);
+      }
+    }
+    return true;
+  },
+
   setTelemetryBucket: function(aPageID) {
     let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID;
     BrowserUITelemetry.setBucket(bucket);
   },
 
   setExpiringTelemetryBucket: function(aPageID, aType) {
     let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID +
                  BrowserUITelemetry.BUCKET_SEPARATOR + aType;
--- a/browser/components/uitour/test/browser_UITour_sync.js
+++ b/browser/components/uitour/test/browser_UITour_sync.js
@@ -17,13 +17,55 @@ add_UITour_task(function* test_checkSync
 
 add_UITour_task(function* test_checkSyncSetup_enabled() {
   Services.prefs.setCharPref("services.sync.username", "uitour@tests.mozilla.org");
   let result = yield getConfigurationPromise("sync");
   is(result.setup, true, "Sync should be setup");
 });
 
 // The showFirefoxAccounts API is sync related, so we test that here too...
-add_UITour_task(function* test_firefoxAccounts() {
+add_UITour_task(function* test_firefoxAccountsNoParams() {
   yield gContentAPI.showFirefoxAccounts();
   yield BrowserTestUtils.browserLoaded(gTestTab.linkedBrowser, false,
                                        "about:accounts?action=signup&entrypoint=uitour");
 });
+
+add_UITour_task(function* test_firefoxAccountsValidParams() {
+  yield gContentAPI.showFirefoxAccounts({ utm_foo: "foo", utm_bar: "bar" });
+  yield BrowserTestUtils.browserLoaded(gTestTab.linkedBrowser, false,
+                                       "about:accounts?action=signup&entrypoint=uitour&utm_foo=foo&utm_bar=bar");
+});
+
+// A helper to check the request was ignored due to invalid params.
+function* checkAboutAccountsNotLoaded() {
+  try {
+    yield waitForConditionPromise(() => {
+      return gBrowser.selectedBrowser.currentURI.spec.startsWith("about:accounts");
+    }, "Check if about:accounts opened");
+    ok(false, "No about:accounts tab should have opened");
+  } catch (ex) {
+    ok(true, "No about:accounts tab opened");
+  }
+}
+
+add_UITour_task(function* test_firefoxAccountsNonObject() {
+  // non-string should be rejected.
+  yield gContentAPI.showFirefoxAccounts(99);
+  yield checkAboutAccountsNotLoaded();
+});
+
+add_UITour_task(function* test_firefoxAccountsNonUtmPrefix() {
+  // Any non "utm_" name should should be rejected.
+  yield gContentAPI.showFirefoxAccounts({ utm_foo: "foo", bar: "bar" });
+  yield checkAboutAccountsNotLoaded();
+});
+
+add_UITour_task(function* test_firefoxAccountsNonAlphaName() {
+  // Any "utm_" name which includes non-alpha chars should be rejected.
+  yield gContentAPI.showFirefoxAccounts({ utm_foo: "foo", "utm_bar=": "bar" });
+  yield checkAboutAccountsNotLoaded();
+});
+
+add_UITour_task(function* test_firefoxAccountsNonAlphaValue() {
+  // Any non-alpha value should be rejected.
+  yield gContentAPI.showFirefoxAccounts({ utm_foo: "foo&" });
+  yield checkAboutAccountsNotLoaded();
+});
--- a/devtools/client/aboutdebugging/aboutdebugging.css
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -38,23 +38,16 @@ button {
 .main-content {
   flex: 1;
 }
 
 .tab {
   max-width: 800px;
 }
 
-/* Prefs */
-
-label {
-  display: block;
-  margin-bottom: 5px;
-}
-
 /* Targets */
 
 .targets {
   margin-bottom: 25px;
 }
 
 .target {
   margin-top: 5px;
@@ -83,8 +76,13 @@ label {
 .addons-controls {
   display: flex;
   flex-direction: row;
 }
 
 .addons-options {
   flex: 1;
 }
+
+.addons-debugging-label {
+  display: inline-block;
+  margin: 0 5px 5px 0;
+}
\ No newline at end of file
--- a/devtools/client/aboutdebugging/components/addons-controls.js
+++ b/devtools/client/aboutdebugging/components/addons-controls.js
@@ -12,46 +12,55 @@ loader.lazyRequireGetter(this, "React", 
 loader.lazyRequireGetter(this, "Services");
 
 loader.lazyImporter(this, "AddonManager",
   "resource://gre/modules/AddonManager.jsm");
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
+const MORE_INFO_URL = "https://developer.mozilla.org/docs/Tools" +
+                      "/about:debugging#Enabling_add-on_debugging";
+
 exports.AddonsControls = React.createClass({
   displayName: "AddonsControls",
 
   render() {
     let { debugDisabled } = this.props;
 
     return React.createElement(
       "div", { className: "addons-controls" }, React.createElement(
         "div", { className: "addons-options" },
           React.createElement("input", {
             id: "enable-addon-debugging",
             type: "checkbox",
             checked: !debugDisabled,
             onChange: this.onEnableAddonDebuggingChange,
           }),
           React.createElement("label", {
+            className: "addons-debugging-label",
             htmlFor: "enable-addon-debugging",
             title: Strings.GetStringFromName("addonDebugging.tooltip")
-          }, Strings.GetStringFromName("addonDebugging.label"))
+          }, Strings.GetStringFromName("addonDebugging.label")),
+          "(",
+          React.createElement("a", { href: MORE_INFO_URL, target: "_blank" },
+            Strings.GetStringFromName("addonDebugging.moreInfo")),
+          ")"
         ),
         React.createElement("button", {
           id: "load-addon-from-file",
           onClick: this.loadAddonFromFile,
         }, Strings.GetStringFromName("loadTemporaryAddon"))
       );
   },
 
   onEnableAddonDebuggingChange(event) {
     let enabled = event.target.checked;
     Services.prefs.setBoolPref("devtools.chrome.enabled", enabled);
+    Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled);
   },
 
   loadAddonFromFile(event) {
     let win = event.target.ownerDocument.defaultView;
 
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     fp.init(win,
       Strings.GetStringFromName("selectAddonFromFile"),
--- a/devtools/client/aboutdebugging/components/addons-tab.js
+++ b/devtools/client/aboutdebugging/components/addons-tab.js
@@ -18,37 +18,46 @@ loader.lazyRequireGetter(this, "Services
 
 loader.lazyImporter(this, "AddonManager",
   "resource://gre/modules/AddonManager.jsm");
 
 const ExtensionIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
+const CHROME_ENABLED_PREF = "devtools.chrome.enabled";
+const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
+
 exports.AddonsTab = React.createClass({
   displayName: "AddonsTab",
 
   getInitialState() {
     return {
       extensions: [],
       debugDisabled: false,
     };
   },
 
   componentDidMount() {
     AddonManager.addAddonListener(this);
-    Services.prefs.addObserver("devtools.chrome.enabled",
+
+    Services.prefs.addObserver(CHROME_ENABLED_PREF,
       this.updateDebugStatus, false);
+    Services.prefs.addObserver(REMOTE_ENABLED_PREF,
+      this.updateDebugStatus, false);
+
     this.updateDebugStatus();
     this.updateAddonsList();
   },
 
   componentWillUnmount() {
     AddonManager.removeAddonListener(this);
-    Services.prefs.removeObserver("devtools.chrome.enabled",
+    Services.prefs.removeObserver(CHROME_ENABLED_PREF,
+      this.updateDebugStatus);
+    Services.prefs.removeObserver(REMOTE_ENABLED_PREF,
       this.updateDebugStatus);
   },
 
   render() {
     let { client } = this.props;
     let { debugDisabled, extensions: targets } = this.state;
     let name = Strings.GetStringFromName("extensions");
 
@@ -63,19 +72,21 @@ exports.AddonsTab = React.createClass({
           "div", { id: "addons" },
           React.createElement(TargetList,
             { name, targets, client, debugDisabled })
       )
     );
   },
 
   updateDebugStatus() {
-    this.setState({
-      debugDisabled: !Services.prefs.getBoolPref("devtools.chrome.enabled")
-    });
+    let debugDisabled =
+      !Services.prefs.getBoolPref(CHROME_ENABLED_PREF) ||
+      !Services.prefs.getBoolPref(REMOTE_ENABLED_PREF);
+
+    this.setState({ debugDisabled });
   },
 
   updateAddonsList() {
     AddonManager.getAllAddons(addons => {
       let extensions = addons.filter(addon => addon.isDebuggable).map(addon => {
         return {
           name: addon.name,
           icon: addon.iconURL || ExtensionIcon,
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -3,12 +3,13 @@ tags = devtools
 subsuite = devtools
 support-files =
   head.js
   addons/unpacked/bootstrap.js
   addons/unpacked/install.rdf
   service-workers/empty-sw.html
   service-workers/empty-sw.js
 
+[browser_addons_debugging_initial_state.js]
 [browser_addons_install.js]
 [browser_addons_toggle_debug.js]
 [browser_service_workers.js]
 [browser_service_workers_timeout.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that addons debugging controls are properly enabled/disabled depending
+// on the values of the relevant preferences:
+// - devtools.chrome.enabled
+// - devtools.debugger.remote-enabled
+
+const ADDON_ID = "test-devtools@mozilla.org";
+
+const TEST_DATA = [
+  {
+    chromeEnabled: false,
+    debuggerRemoteEnable: false,
+    expected: false,
+  }, {
+    chromeEnabled: false,
+    debuggerRemoteEnable: true,
+    expected: false,
+  }, {
+    chromeEnabled: true,
+    debuggerRemoteEnable: false,
+    expected: false,
+  }, {
+    chromeEnabled: true,
+    debuggerRemoteEnable: true,
+    expected: true,
+  }
+];
+
+add_task(function* () {
+  for (let testData of TEST_DATA) {
+    yield testCheckboxState(testData);
+  }
+});
+
+function* testCheckboxState(testData) {
+  info("Set preferences as defined by the current test data.");
+  yield new Promise(resolve => {
+    let options = {"set": [
+      ["devtools.chrome.enabled", testData.chromeEnabled],
+      ["devtools.debugger.remote-enabled", testData.debuggerRemoteEnable],
+    ]};
+    SpecialPowers.pushPrefEnv(options, resolve);
+  });
+
+  let { tab, document } = yield openAboutDebugging("addons");
+
+  info("Install a test addon.");
+  yield installAddon(document, "addons/unpacked/install.rdf", "test-devtools");
+
+  info("Test checkbox checked state.");
+  let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
+  is(addonDebugCheckbox.checked, testData.expected,
+    "Addons debugging checkbox should be in expected state.");
+
+  info("Test debug buttons disabled state.");
+  let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
+  ok(debugButtons.every(b => b.disabled != testData.expected),
+    "Debug buttons should be in the expected state");
+
+  info("Uninstall test addon installed earlier.");
+  yield uninstallAddon(ADDON_ID);
+
+  yield closeAboutDebugging(tab);
+}
--- a/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
@@ -8,16 +8,17 @@
 
 const ADDON_ID = "test-devtools@mozilla.org";
 
 add_task(function* () {
   info("Turn off addon debugging.");
   yield new Promise(resolve => {
     let options = {"set": [
       ["devtools.chrome.enabled", false],
+      ["devtools.debugger.remote-enabled", false],
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
 
   info("Install a test addon.");
   yield installAddon(document, "addons/unpacked/install.rdf", "test-devtools");
--- a/devtools/client/inspector/fonts/test/head.js
+++ b/devtools/client/inspector/fonts/test/head.js
@@ -4,16 +4,21 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 // Import the inspector's head.js first (which itself imports shared-head.js).
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
 
+Services.prefs.setBoolPref("devtools.fontinspector.enabled", true);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("devtools.fontinspector.enabled");
+});
+
 /**
  * Adds a new tab with the given URL, opens the inspector and selects the
  * font-inspector tab.
  * @return {Promise} resolves to a {toolbox, inspector, view} object
  */
 var openFontInspectorForURL = Task.async(function*(url) {
   yield addTab(url);
   let {toolbox, inspector} = yield openInspectorSidebarTab("fontinspector");
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -211,16 +211,17 @@ devtools.jar:
     skin/images/timeline-filter.svg (themes/images/timeline-filter.svg)
     skin/scratchpad.css (themes/scratchpad.css)
     skin/shadereditor.css (themes/shadereditor.css)
     skin/storage.css (themes/storage.css)
     skin/splitview.css (themes/splitview.css)
     skin/styleeditor.css (themes/styleeditor.css)
     skin/webaudioeditor.css (themes/webaudioeditor.css)
     skin/components-frame.css (themes/components-frame.css)
+    skin/components-h-split-box.css (themes/components-h-split-box.css)
     skin/jit-optimizations.css (themes/jit-optimizations.css)
     skin/images/magnifying-glass.png (themes/images/magnifying-glass.png)
     skin/images/magnifying-glass@2x.png (themes/images/magnifying-glass@2x.png)
     skin/images/magnifying-glass-light.png (themes/images/magnifying-glass-light.png)
     skin/images/magnifying-glass-light@2x.png (themes/images/magnifying-glass-light@2x.png)
     skin/images/itemToggle.png (themes/images/itemToggle.png)
     skin/images/itemToggle@2x.png (themes/images/itemToggle@2x.png)
     skin/images/itemArrow-dark-rtl.svg (themes/images/itemArrow-dark-rtl.svg)
--- a/devtools/client/jsonview/components/headers-panel.js
+++ b/devtools/client/jsonview/components/headers-panel.js
@@ -2,17 +2,17 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 define(function(require, exports, module) {
 
 const React = require("devtools/client/shared/vendor/react");
-const { createFactories } = require("./reps/rep-utils");
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
 const { Headers } = createFactories(require("./headers"));
 const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));
 
 const DOM = React.DOM;
 
 /**
  * This template represents the 'Headers' panel
  * s responsible for rendering its content.
--- a/devtools/client/jsonview/components/json-panel.js
+++ b/devtools/client/jsonview/components/json-panel.js
@@ -2,17 +2,17 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 define(function(require, exports, module) {
 
 const React = require("devtools/client/shared/vendor/react");
-const { createFactories } = require("./reps/rep-utils");
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
 const { TreeView } = createFactories(require("./reps/tree-view"));
 const { SearchBox } = createFactories(require("./search-box"));
 const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));
 const DOM = React.DOM;
 
 /**
  * This template represents the 'JSON' panel. The panel is
  * responsible for rendering an expandable tree that allows simple
--- a/devtools/client/jsonview/components/main-tabbed-area.js
+++ b/devtools/client/jsonview/components/main-tabbed-area.js
@@ -4,17 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 define(function(require, exports, module) {
 
 const React = require("devtools/client/shared/vendor/react");
-const { createFactories } = require("./reps/rep-utils");
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
 const { JsonPanel } = createFactories(require("./json-panel"));
 const { TextPanel } = createFactories(require("./text-panel"));
 const { HeadersPanel } = createFactories(require("./headers-panel"));
 const { Tabs, TabPanel } = createFactories(require("./reps/tabs"));
 
 /**
  * This object represents the root application template
  * responsible for rendering the basic tab layout.
--- a/devtools/client/jsonview/components/reps/moz.build
+++ b/devtools/client/jsonview/components/reps/moz.build
@@ -1,22 +1,11 @@
 # -*- 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/.
 
 DevToolsModules(
-    'array.js',
-    'caption.js',
-    'null.js',
-    'number.js',
-    'object-box.js',
-    'object-link.js',
-    'object.js',
-    'rep-utils.js',
-    'rep.js',
-    'string.js',
     'tabs.js',
     'toolbar.js',
     'tree-view.js',
-    'undefined.js',
 )
--- a/devtools/client/jsonview/components/reps/tree-view.js
+++ b/devtools/client/jsonview/components/reps/tree-view.js
@@ -3,19 +3,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/. */
 
 define(function(require, exports, module) {
 
 // Dependencies
 const React = require("devtools/client/shared/vendor/react");
-const { createFactories } = require("./rep-utils");
-const { Rep } = createFactories(require("./rep"));
-const { StringRep } = require("./string");
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
+const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
+const { StringRep } = require("devtools/client/shared/components/reps/string");
 const DOM = React.DOM;
 
 var uid = 0;
 
 /**
  * Renders a tree view with expandable/collapsible items.
  */
 var TreeView = React.createClass({
--- a/devtools/client/jsonview/components/text-panel.js
+++ b/devtools/client/jsonview/components/text-panel.js
@@ -2,17 +2,17 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 define(function(require, exports, module) {
 
 const React = require("devtools/client/shared/vendor/react");
-const { createFactories } = require("./reps/rep-utils");
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
 const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));
 const DOM = React.DOM;
 
 /**
  * This template represents the 'Raw Data' panel displaying
  * JSON as a text received from the server.
  */
 var TextPanel = React.createClass({
--- a/devtools/client/jsonview/css/main.css
+++ b/devtools/client/jsonview/css/main.css
@@ -1,15 +1,16 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
+@import "resource://devtools/client/shared/components/reps/reps.css";
+
 @import "general.css";
-@import "reps.css";
 @import "dom-tree.css";
 @import "search-box.css";
 @import "tabs.css";
 @import "toolbar.css";
 @import "json-panel.css";
 @import "text-panel.css";
 @import "headers-panel.css";
 
--- a/devtools/client/jsonview/css/moz.build
+++ b/devtools/client/jsonview/css/moz.build
@@ -9,17 +9,16 @@ DevToolsModules(
     'controls.png',
     'controls@2x.png',
     'dom-tree.css',
     'general.css',
     'headers-panel.css',
     'json-panel.css',
     'main.css',
     'read-only-prop.svg',
-    'reps.css',
     'search-box.css',
     'search.svg',
     'tabs.css',
     'text-panel.css',
     'toolbar.css',
     'twisty-closed.svg',
     'twisty-open.svg'
 )
deleted file mode 100644
--- a/devtools/client/jsonview/css/reps.css
+++ /dev/null
@@ -1,219 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* 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/. */
-
-.objectLink:hover {
-  cursor: pointer;
-  text-decoration: underline;
-}
-
-/******************************************************************************/
-
-.inline {
-  display: inline;
-  white-space: normal;
-}
-
-.objectBox-object {
-  font-family: Lucida Grande, sans-serif;
-  font-weight: bold;
-  color: DarkGreen;
-  white-space: pre-wrap;
-}
-
-.objectBox-string,
-.objectBox-text,
-.objectBox-number,
-.objectLink-element,
-.objectLink-textNode,
-.objectLink-function,
-.objectBox-stackTrace,
-.objectLink-profile,
-.objectBox-table {
-  font-family: monospace;
-}
-
-.objectBox-string,
-.objectBox-text,
-.objectLink-textNode,
-.objectBox-table {
-  white-space: pre-wrap;
-}
-
-.objectBox-number,
-.objectLink-styleRule,
-.objectLink-element,
-.objectLink-textNode,
-.objectBox-array > .length {
-  color: #000088;
-}
-
-.objectBox-string {
-  color: #FF0000;
-}
-
-.objectLink-function,
-.objectBox-stackTrace,
-.objectLink-profile {
-  color: DarkGreen;
-}
-
-.objectBox-null,
-.objectBox-undefined,
-.objectBox-hint,
-.logRowHint {
-  font-style: italic;
-  color: #787878;
-}
-
-.objectBox-scope {
-  color: #707070;
-}
-.objectBox-optimizedAway {
-  color: #909090;
-}
-
-.objectLink-sourceLink {
-  position: absolute;
-  right: 4px;
-  top: 2px;
-  padding-left: 8px;
-  font-family: Lucida Grande, sans-serif;
-  font-weight: bold;
-  color: #0000FF;
-}
-
-.objectLink-sourceLink > .systemLink {
-  float: right;
-  color: #FF0000;
-}
-
-/******************************************************************************/
-
-.objectLink-event,
-.objectLink-eventLog,
-.objectLink-regexp,
-.objectLink-object,
-.objectLink-Date {
-  font-family: Lucida Grande, sans-serif;
-  font-weight: bold;
-  color: DarkGreen;
-  white-space: pre-wrap;
-}
-
-/******************************************************************************/
-
-.objectLink-object .nodeName,
-.objectLink-NamedNodeMap .nodeName,
-.objectLink-NamedNodeMap .objectEqual,
-.objectLink-NamedNodeMap .arrayLeftBracket,
-.objectLink-NamedNodeMap .arrayRightBracket,
-.objectLink-Attr .attrEqual,
-.objectLink-Attr .attrTitle {
-  color: rgb(0, 0, 136)
-}
-
-.objectLink-object .nodeName {
-  font-weight: normal;
-}
-
-/******************************************************************************/
-
-.objectLeftBrace,
-.objectRightBrace,
-.objectEqual,
-.objectComma,
-.arrayLeftBracket,
-.arrayRightBracket,
-.arrayComma {
-  font-family: monospace;
-}
-
-.objectLeftBrace,
-.objectRightBrace,
-.arrayLeftBracket,
-.arrayRightBracket {
-  cursor: pointer;
-  font-weight: bold;
-}
-
-.objectLeftBrace,
-.arrayLeftBracket {
-  margin-right: 4px;
-}
-
-.objectRightBrace,
-.arrayRightBracket {
-  margin-left: 4px;
-}
-
-/******************************************************************************/
-/* Cycle reference*/
-
-.objectLink-Reference {
-  font-family: monospace;
-  font-weight: bold;
-  color: rgb(102, 102, 255);
-}
-
-.objectBox-array > .objectTitle {
-  font-weight: bold;
-  color: DarkGreen;
-}
-
-/******************************************************************************/
-
-.caption {
-  font-family: Lucida Grande, Tahoma, sans-serif;
-  font-weight: bold;
-  color:  #444444;
-}
-
-/******************************************************************************/
-/* Light Theme & Dark Theme */
-
-.theme-dark .domLabel,
-.theme-light .domLabel {
-  color: var(--theme-highlight-blue);
-}
-
-.theme-dark .objectBox-array .length,
-.theme-light .objectBox-array .length,
-.theme-dark .objectBox-number,
-.theme-light .objectBox-number {
-  color: var(--theme-highlight-green);
-}
-
-.theme-dark .objectBox-string,
-.theme-light .objectBox-string {
-  color: var(--theme-highlight-orange);
-}
-
-.theme-dark .objectBox-null,
-.theme-dark .objectBox-undefined,
-.theme-light .objectBox-null,
-.theme-light .objectBox-undefined {
-  font-style: normal;
-  color: var(--theme-comment);
-}
-
-.theme-dark .objectBox-array,
-.theme-light .objectBox-array {
-  color: var(--theme-body-color);
-}
-
-.theme-dark .objectBox-object,
-.theme-light .objectBox-object {
-  font-family: Lucida Grande, sans-serif;
-  font-weight: normal;
-  color: var(--theme-highlight-blue);
-  white-space: pre-wrap;
-}
-
-.theme-dark .caption,
-.theme-light .caption {
-  font-family: Lucida Grande, Tahoma, sans-serif;
-  font-weight: normal;
-  color: var(--theme-highlight-blue);
-}
--- a/devtools/client/jsonview/json-viewer.js
+++ b/devtools/client/jsonview/json-viewer.js
@@ -3,17 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 define(function(require, exports, module) {
 
 // ReactJS
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
-const { createFactories } = require("./components/reps/rep-utils");
+const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
 const { MainTabbedArea } = createFactories(require("./components/main-tabbed-area"));
 
 const json = document.getElementById("json");
 const headers = document.getElementById("headers");
 
 var jsonData;
 
 try {
--- a/devtools/client/locales/en-US/aboutdebugging.properties
+++ b/devtools/client/locales/en-US/aboutdebugging.properties
@@ -2,16 +2,17 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 debug = Debug
 
 addons = Add-ons
 addonDebugging.label = Enable add-on debugging
 addonDebugging.tooltip = Turning this on will allow you to debug add-ons and various other parts of the browser chrome
+addonDebugging.moreInfo = more info
 loadTemporaryAddon = Load Temporary Add-on
 extensions = Extensions
 selectAddonFromFile = Select Add-on Directory or XPI File
 
 workers = Workers
 serviceWorkers = Service Workers
 sharedWorkers = Shared Workers
 otherWorkers = Other Workers
--- a/devtools/client/locales/en-US/memory.properties
+++ b/devtools/client/locales/en-US/memory.properties
@@ -363,8 +363,16 @@ heapview.field.totalcount.tooltip=The nu
 
 # LOCALIZATION NOTE (heapview.field.name): The name of the column in the heap
 # view for name.
 heapview.field.name=Name
 
 # LOCALIZATION NOTE (heapview.field.name.tooltip): The tooltip for the column
 # header in the heap view for name.
 heapview.field.name.tooltip=The name of this group
+
+# LOCALIZATION NOTE (shortest-paths.header): The header label for the shortest
+# paths pane.
+shortest-paths.header=Retaining Paths from GC Roots
+
+# LOCALIZATION NOTE (shortest-paths.select-node): The message displayed in the
+# shortest paths pane when a node is not yet selected.
+shortest-paths.select-node=Select a node to view its retaining paths
--- a/devtools/client/memory/actions/moz.build
+++ b/devtools/client/memory/actions/moz.build
@@ -7,11 +7,12 @@ DevToolsModules(
     'allocations.js',
     'breakdown.js',
     'diffing.js',
     'dominatorTreeBreakdown.js',
     'filter.js',
     'inverted.js',
     'io.js',
     'refresh.js',
+    'sizes.js',
     'snapshot.js',
     'view.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/actions/sizes.js
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { actions } = require("../constants");
+
+exports.resizeShortestPaths = function (newSize) {
+  return {
+    type: actions.RESIZE_SHORTEST_PATHS,
+    size: newSize,
+  };
+};
--- a/devtools/client/memory/app.js
+++ b/devtools/client/memory/app.js
@@ -28,16 +28,17 @@ const {
   expandCensusNode,
   collapseCensusNode,
   focusCensusNode,
   expandDominatorTreeNode,
   collapseDominatorTreeNode,
   focusDominatorTreeNode,
 } = require("./actions/snapshot");
 const { changeViewAndRefresh } = require("./actions/view");
+const { resizeShortestPaths } = require("./actions/sizes");
 const {
   breakdownNameToSpec,
   getBreakdownDisplayData,
   dominatorTreeBreakdownNameToSpec,
   getDominatorTreeBreakdownDisplayData,
 } = require("./utils");
 const Toolbar = createFactory(require("./components/toolbar"));
 const List = createFactory(require("./components/list"));
@@ -108,17 +109,17 @@ const MemoryApp = createClass({
       heapWorker,
       breakdown,
       allocations,
       inverted,
       toolbox,
       filter,
       diffing,
       view,
-      dominatorTreeBreakdown
+      sizes,
     } = this.props;
 
     const selectedSnapshot = snapshots.find(s => s.selected);
 
     const onClickSnapshotListItem = diffing && diffing.state === diffingState.SELECTING
       ? snapshot => dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, snapshot))
       : snapshot => dispatch(selectSnapshotAndRefresh(heapWorker, snapshot.id));
 
@@ -232,16 +233,20 @@ const MemoryApp = createClass({
             onDominatorTreeFocus: node => {
               assert(view === viewState.DOMINATOR_TREE,
                      "If focusing dominator tree nodes, should be in dominator tree view");
               assert(selectedSnapshot, "...and we should have a selected snapshot");
               assert(selectedSnapshot.dominatorTree,
                      "...and that snapshot should have a dominator tree");
               dispatch(focusDominatorTreeNode(selectedSnapshot.id, node));
             },
+            onShortestPathsResize: newSize => {
+              dispatch(resizeShortestPaths(newSize));
+            },
+            sizes,
             view,
           })
         )
       )
     );
   },
 });
 
--- a/devtools/client/memory/components/census-tree-item.js
+++ b/devtools/client/memory/components/census-tree-item.js
@@ -20,51 +20,59 @@ const CensusTreeItem = module.exports = 
   },
 
   render() {
     let {
       item,
       depth,
       arrow,
       focused,
-      toolbox,
       getPercentBytes,
       getPercentCount,
       showSign,
       onViewSourceInDebugger,
+      inverted,
     } = this.props;
 
     const bytes = formatNumber(item.bytes, showSign);
     const percentBytes = formatPercent(getPercentBytes(item.bytes), showSign);
 
     const count = formatNumber(item.count, showSign);
     const percentCount = formatPercent(getPercentCount(item.count), showSign);
 
     const totalBytes = formatNumber(item.totalBytes, showSign);
     const percentTotalBytes = formatPercent(getPercentBytes(item.totalBytes), showSign);
 
     const totalCount = formatNumber(item.totalCount, showSign);
     const percentTotalCount = formatPercent(getPercentCount(item.totalCount), showSign);
 
-    return dom.div({ className: `heap-tree-item ${focused ? "focused" :""}` },
+    let pointer;
+    if (inverted && depth > 0) {
+      pointer = dom.span({ className: "children-pointer" }, "↖");
+    } else if (!inverted && item.children && item.children.length) {
+      pointer = dom.span({ className: "children-pointer" }, "↘");
+    }
+
+    return dom.div({ className: `heap-tree-item ${focused ? "focused" : ""}` },
       dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" },
                dom.span({ className: "heap-tree-number" }, bytes),
                dom.span({ className: "heap-tree-percent" }, percentBytes)),
       dom.span({ className: "heap-tree-item-field heap-tree-item-count" },
                dom.span({ className: "heap-tree-number" }, count),
                dom.span({ className: "heap-tree-percent" }, percentCount)),
       dom.span({ className: "heap-tree-item-field heap-tree-item-total-bytes" },
                dom.span({ className: "heap-tree-number" }, totalBytes),
                dom.span({ className: "heap-tree-percent" }, percentTotalBytes)),
       dom.span({ className: "heap-tree-item-field heap-tree-item-total-count" },
                dom.span({ className: "heap-tree-number" }, totalCount),
                dom.span({ className: "heap-tree-percent" }, percentTotalCount)),
                dom.span({ className: "heap-tree-item-field heap-tree-item-name",
                           style: { marginLeft: depth * TREE_ROW_HEIGHT }},
         arrow,
+        pointer,
         this.toLabel(item.name, onViewSourceInDebugger)
       )
     );
   },
 
   toLabel(name, linkToDebugger) {
     if (isSavedFrame(name)) {
       return Frame({
--- a/devtools/client/memory/components/census.js
+++ b/devtools/client/memory/components/census.js
@@ -61,15 +61,16 @@ const Census = module.exports = createCl
           item,
           depth,
           focused,
           arrow,
           expanded,
           getPercentBytes,
           getPercentCount,
           showSign: !!diffing,
+          inverted: census.inverted,
         }),
       getRoots: () => report.children || [],
       getKey: node => node.id,
       itemHeight: TREE_ROW_HEIGHT,
     });
   }
 });
--- a/devtools/client/memory/components/heap.js
+++ b/devtools/client/memory/components/heap.js
@@ -3,16 +3,18 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
 const { assert, safeErrorString } = require("devtools/shared/DevToolsUtils");
 const Census = createFactory(require("./census"));
 const CensusHeader = createFactory(require("./census-header"));
 const DominatorTree = createFactory(require("./dominator-tree"));
 const DominatorTreeHeader = createFactory(require("./dominator-tree-header"));
+const HSplitBox = createFactory(require("devtools/client/shared/components/h-split-box"));
+const ShortestPaths = createFactory(require("./shortest-paths"));
 const { getStatusTextFull, L10N } = require("../utils");
 const { snapshotState: states, diffingState, viewState, dominatorTreeState } = require("../constants");
 const { snapshot: snapshotModel, diffingModel } = require("../models");
 
 /**
  * Get the app state's current state atom.
  *
  * @see the relevant state string constants in `../constants.js`.
@@ -140,20 +142,22 @@ const Heap = module.exports = createClas
     onSnapshotClick: PropTypes.func.isRequired,
     onLoadMoreSiblings: PropTypes.func.isRequired,
     onCensusExpand: PropTypes.func.isRequired,
     onCensusCollapse: PropTypes.func.isRequired,
     onDominatorTreeExpand: PropTypes.func.isRequired,
     onDominatorTreeCollapse: PropTypes.func.isRequired,
     onCensusFocus: PropTypes.func.isRequired,
     onDominatorTreeFocus: PropTypes.func.isRequired,
+    onShortestPathsResize: PropTypes.func.isRequired,
     snapshot: snapshotModel,
     onViewSourceInDebugger: PropTypes.func.isRequired,
     diffing: diffingModel,
     view: PropTypes.string.isRequired,
+    sizes: PropTypes.object.isRequired,
   },
 
   render() {
     let {
       snapshot,
       diffing,
       onSnapshotClick,
       onLoadMoreSiblings,
@@ -272,22 +276,43 @@ const Heap = module.exports = createClas
       onCollapse: node => this.props.onCensusCollapse(census, node),
       onFocus: node => this.props.onCensusFocus(census, node),
     }));
 
     return this._renderHeapView(state, ...contents);
   },
 
   _renderDominatorTree(state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings) {
-    return this._renderHeapView(
-      state,
+    const tree = dom.div(
+      {
+        className: "vbox",
+        style: {
+          overflowY: "auto"
+        }
+      },
       DominatorTreeHeader(),
       DominatorTree({
         onViewSourceInDebugger,
         dominatorTree,
         onLoadMoreSiblings,
         onExpand: this.props.onDominatorTreeExpand,
         onCollapse: this.props.onDominatorTreeCollapse,
         onFocus: this.props.onDominatorTreeFocus,
       })
     );
+
+    const shortestPaths = ShortestPaths({
+      graph: dominatorTree.focused
+        ? dominatorTree.focused.shortestPaths
+        : null
+    });
+
+    return this._renderHeapView(
+      state,
+      HSplitBox({
+        start: tree,
+        end: shortestPaths,
+        startWidth: this.props.sizes.shortestPathsSize,
+        onResize: this.props.onShortestPathsResize,
+      })
+    );
   },
 });
--- a/devtools/client/memory/components/moz.build
+++ b/devtools/client/memory/components/moz.build
@@ -7,11 +7,12 @@ DevToolsModules(
     'census-header.js',
     'census-tree-item.js',
     'census.js',
     'dominator-tree-header.js',
     'dominator-tree-item.js',
     'dominator-tree.js',
     'heap.js',
     'list.js',
+    'shortest-paths.js',
     'snapshot-list-item.js',
     'toolbar.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/components/shortest-paths.js
@@ -0,0 +1,189 @@
+/* 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 { isSavedFrame } = require("devtools/shared/DevToolsUtils");
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+const { L10N } = require("../utils");
+
+const { ViewHelpers } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
+const COMPONENTS_STRINGS_URI = "chrome://devtools/locale/components.properties";
+const componentsL10N = new ViewHelpers.L10N(COMPONENTS_STRINGS_URI);
+const UNKNOWN_SOURCE_STRING = componentsL10N.getStr("frame.unknownSource");
+
+const GRAPH_DEFAULTS = {
+  translate: [20, 20],
+  scale: 1
+};
+
+const NO_STACK = "noStack";
+const NO_FILENAME = "noFilename";
+const ROOT_LIST = "JS::ubi::RootList";
+
+function stringifyLabel(label, id) {
+  const sanitized = [];
+
+  for (let i = 0, length = label.length; i < length; i++) {
+    const piece = label[i];
+
+    if (isSavedFrame(piece)) {
+      const { short } = getSourceNames(piece.source, UNKNOWN_SOURCE_STRING);
+      sanitized[i] = `${piece.functionDisplayName} @ ${short}:${piece.line}:${piece.column}`;
+    } else if (piece === NO_STACK) {
+      sanitized[i] = L10N.getStr("tree-item.nostack");
+    } else if (piece === NO_FILENAME) {
+      sanitized[i] = L10N.getStr("tree-item.nofilename");
+    } else if (piece === ROOT_LIST) {
+      // Don't use the usual labeling machinery for root lists: replace it
+      // with the "GC Roots" string.
+      sanitized.splice(0, label.length);
+      sanitized.push(L10N.getStr("tree-item.rootlist"));
+      break;
+    } else {
+      sanitized[i] = "" + piece;
+    }
+  }
+
+  return `${sanitized.join(" › ")} @ 0x${id.toString(16)}`;
+}
+
+module.exports = createClass({
+  displayName: "ShortestPaths",
+
+  propTypes: {
+    graph: PropTypes.shape({
+      nodes: PropTypes.arrayOf(PropTypes.object),
+      edges: PropTypes.arrayOf(PropTypes.object),
+    }),
+  },
+
+  getInitialState() {
+    return { zoom: null };
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.graph != nextProps.graph;
+  },
+
+  componentDidMount() {
+    if (this.props.graph) {
+      this._renderGraph(this.refs.container, this.props.graph);
+    }
+  },
+
+  componentDidUpdate() {
+    if (this.props.graph) {
+      this._renderGraph(this.refs.container, this.props.graph);
+    }
+  },
+
+  componentWillUnmount() {
+    if (this.state.zoom) {
+      this.state.zoom.on("zoom", null);
+    }
+  },
+
+  render() {
+    let contents;
+    if (this.props.graph) {
+      // Let the componentDidMount or componentDidUpdate method draw the graph
+      // with DagreD3. We just provide the container for the graph here.
+      contents = dom.div({
+        ref: "container",
+        style: {
+          flex: 1,
+          height: "100%",
+          width: "100%",
+        }
+      });
+    } else {
+      contents = dom.div(
+        {
+          id: "shortest-paths-select-node-msg"
+        },
+        L10N.getStr("shortest-paths.select-node")
+      );
+    }
+
+    return dom.div(
+      {
+        id: "shortest-paths",
+        className: "vbox",
+      },
+      dom.label(
+        {
+          id: "shortest-paths-header",
+          className: "header",
+        },
+        L10N.getStr("shortest-paths.header")
+      ),
+      contents
+    );
+  },
+
+  _renderGraph(container, { nodes, edges }) {
+    if (!container.firstChild) {
+      const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+      svg.setAttribute("id", "graph-svg");
+      svg.setAttribute("xlink", "http://www.w3.org/1999/xlink");
+      svg.style.width = "100%";
+      svg.style.height = "100%";
+
+      const target = document.createElementNS("http://www.w3.org/2000/svg", "g");
+      target.setAttribute("id", "graph-target");
+      target.style.width = "100%";
+      target.style.height = "100%";
+
+      svg.appendChild(target);
+      container.appendChild(svg);
+    }
+
+    const graph = new dagreD3.Digraph();
+
+    for (let i = 0; i < nodes.length; i++) {
+      graph.addNode(nodes[i].id, {
+        id: nodes[i].id,
+        label: stringifyLabel(nodes[i].label, nodes[i].id),
+      });
+    }
+
+    for (let i = 0; i < edges.length; i++) {
+      graph.addEdge(null, edges[i].from, edges[i].to, {
+        label: edges[i].name
+      });
+    }
+
+    const renderer = new dagreD3.Renderer();
+    renderer.drawNodes();
+    renderer.drawEdgePaths();
+
+    const svg = d3.select("#graph-svg");
+    const target = d3.select("#graph-target");
+
+    let zoom = this.state.zoom;
+    if (!zoom) {
+      zoom = d3.behavior.zoom().on("zoom", function() {
+        target.attr(
+          "transform",
+          `translate(${d3.event.translate}) scale(${d3.event.scale})`
+        );
+      });
+      svg.call(zoom);
+      this.setState({ zoom });
+    }
+
+    const { translate, scale } = GRAPH_DEFAULTS;
+    zoom.scale(scale);
+    zoom.translate(translate);
+    target.attr("transform", `translate(${translate}) scale(${scale})`);
+
+    const layout = dagreD3.layout();
+    renderer.layout(layout).run(graph, target);
+  },
+});
--- a/devtools/client/memory/constants.js
+++ b/devtools/client/memory/constants.js
@@ -96,16 +96,18 @@ actions.COMPUTE_DOMINATOR_TREE_END = "co
 actions.FETCH_DOMINATOR_TREE_START = "fetch-dominator-tree-start";
 actions.FETCH_DOMINATOR_TREE_END = "fetch-dominator-tree-end";
 actions.DOMINATOR_TREE_ERROR = "dominator-tree-error";
 actions.FETCH_IMMEDIATELY_DOMINATED_START = "fetch-immediately-dominated-start";
 actions.FETCH_IMMEDIATELY_DOMINATED_END = "fetch-immediately-dominated-end";
 actions.EXPAND_DOMINATOR_TREE_NODE = "expand-dominator-tree-node";
 actions.COLLAPSE_DOMINATOR_TREE_NODE = "collapse-dominator-tree-node";
 
+actions.RESIZE_SHORTEST_PATHS = "resize-shortest-paths";
+
 /*** Breakdowns ***************************************************************/
 
 const COUNT = { by: "count", count: true, bytes: true };
 const INTERNAL_TYPE = { by: "internalType", then: COUNT };
 const ALLOCATION_STACK = { by: "allocationStack", then: COUNT, noStack: COUNT };
 const OBJECT_CLASS = { by: "objectClass", then: COUNT, other: COUNT };
 
 const breakdowns = exports.breakdowns = {
--- a/devtools/client/memory/memory.xhtml
+++ b/devtools/client/memory/memory.xhtml
@@ -9,19 +9,34 @@
 <!-- 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/. -->
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <link rel="stylesheet" href="chrome://devtools/skin/widgets.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://devtools/skin/memory.css" type="text/css"/>
     <link rel="stylesheet" href="chrome://devtools/skin/components-frame.css" type="text/css"/>
+    <link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
+  </head>
+  <body class="theme-body">
+    <div id="app"></div>
+
+    <script type="application/javascript;version=1.8"
+            src="chrome://devtools/content/shared/theme-switching.js"
+            defer="true">
+    </script>
 
     <script type="application/javascript;version=1.8"
-            src="chrome://devtools/content/shared/theme-switching.js"/>
-    <script type="application/javascript;version=1.8"
-            src="initializer.js"></script>
-  </head>
-  <body class="theme-body">
-    <div id="app">
-    </div>
+            src="initializer.js"
+            defer="true">
+    </script>
+
+    <script type="application/javascript"
+            src="chrome://devtools/content/shared/vendor/d3.js"
+            defer="true">
+    </script>
+
+    <script type="application/javascript"
+            src="chrome://devtools/content/shared/vendor/dagre-d3.js"
+            defer="true">
+    </script>
   </body>
 </html>
--- a/devtools/client/memory/reducers.js
+++ b/devtools/client/memory/reducers.js
@@ -5,10 +5,11 @@
 
 exports.allocations = require("./reducers/allocations");
 exports.breakdown = require("./reducers/breakdown");
 exports.diffing = require("./reducers/diffing");
 exports.dominatorTreeBreakdown = require("./reducers/dominatorTreeBreakdown");
 exports.errors = require("./reducers/errors");
 exports.filter = require("./reducers/filter");
 exports.inverted = require("./reducers/inverted");
+exports.sizes = require("./reducers/sizes");
 exports.snapshots = require("./reducers/snapshots");
 exports.view = require("./reducers/view");
--- a/devtools/client/memory/reducers/moz.build
+++ b/devtools/client/memory/reducers/moz.build
@@ -6,11 +6,12 @@
 DevToolsModules(
     'allocations.js',
     'breakdown.js',
     'diffing.js',
     'dominatorTreeBreakdown.js',
     'errors.js',
     'filter.js',
     'inverted.js',
+    'sizes.js',
     'snapshots.js',
     'view.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/reducers/sizes.js
@@ -0,0 +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/. */
+"use strict";
+
+const { actions } = require("../constants");
+const { immutableUpdate } = require("devtools/shared/DevToolsUtils");
+
+const handlers = Object.create(null);
+
+handlers[actions.RESIZE_SHORTEST_PATHS] = function (sizes, { size }) {
+  return immutableUpdate(sizes, { shortestPathsSize: size });
+};
+
+module.exports = function (sizes = { shortestPathsSize: .5 }, action) {
+  const handler = handlers[action.type];
+  return handler ? handler(sizes, action) : sizes;
+};
--- a/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js
+++ b/devtools/client/memory/test/browser/browser_memory_no_auto_expand.js
@@ -21,18 +21,27 @@ this.test = makeMemoryTest(TEST_URL, fun
 
   is(getState().allocations.recording, false);
   const recordingCheckbox = doc.getElementById("record-allocation-stacks-checkbox");
   EventUtils.synthesizeMouseAtCenter(recordingCheckbox, {}, panel.panelWin);
   is(getState().allocations.recording, true);
 
   const nameElems = [...doc.querySelectorAll(".heap-tree-item-field.heap-tree-item-name")];
   is(nameElems.length, 4, "Should get 4 items, one for each coarse type");
-  ok(nameElems.some(e => e.textContent.trim() === "objects"), "One for coarse type 'objects'");
-  ok(nameElems.some(e => e.textContent.trim() === "scripts"), "One for coarse type 'scripts'");
-  ok(nameElems.some(e => e.textContent.trim() === "strings"), "One for coarse type 'strings'");
-  ok(nameElems.some(e => e.textContent.trim() === "other"), "One for coarse type 'other'");
+
+  for (let el of nameElems) {
+    dumpn(`Found ${el.textContent.trim()}`);
+  }
+
+  ok(nameElems.some(e => e.textContent.indexOf("objects") >= 0),
+     "One for coarse type 'objects'");
+  ok(nameElems.some(e => e.textContent.indexOf("scripts") >= 0),
+     "One for coarse type 'scripts'");
+  ok(nameElems.some(e => e.textContent.indexOf("strings") >= 0),
+     "One for coarse type 'strings'");
+  ok(nameElems.some(e => e.textContent.indexOf("other") >= 0),
+     "One for coarse type 'other'");
 
   for (let e of nameElems) {
     is(e.style.marginLeft, "0px",
        "None of the elements should be an indented/expanded child");
   }
 });
--- a/devtools/client/memory/test/browser/head.js
+++ b/devtools/client/memory/test/browser/head.js
@@ -67,16 +67,20 @@ function makeMemoryTest(url, generator) 
 
     yield closeMemoryPanel(tab);
     yield removeTab(tab);
 
     finish();
   });
 }
 
+function dumpn(msg) {
+  dump(`MEMORY-TEST: ${msg}\n`);
+}
+
 /**
  * Returns a promise that will resolve when the provided store matches
  * the expected array. expectedStates is an array of dominatorTree states.
  * Expectations :
  * - store.getState().snapshots.length == expected.length
  * - snapshots[i].dominatorTree.state == expected[i]
  *
  * @param  {Store} store
--- a/devtools/client/memory/test/chrome/chrome.ini
+++ b/devtools/client/memory/test/chrome/chrome.ini
@@ -1,14 +1,17 @@
 [DEFAULT]
 support-files =
   head.js
 
+[test_CensusTreeItem_01.html]
 [test_DominatorTree_01.html]
 [test_DominatorTree_02.html]
 [test_DominatorTree_03.html]
 [test_DominatorTreeItem_01.html]
 [test_Heap_01.html]
 [test_Heap_02.html]
 [test_Heap_03.html]
 [test_Heap_04.html]
+[test_ShortestPaths_01.html]
+[test_ShortestPaths_02.html]
 [test_Toolbar_01.html]
 [test_Toolbar_02.html]
--- a/devtools/client/memory/test/chrome/head.js
+++ b/devtools/client/memory/test/chrome/head.js
@@ -23,32 +23,62 @@ var {
   dominatorTreeState,
   snapshotState,
   viewState
 } = constants;
 
 const {
   getBreakdownDisplayData,
   getDominatorTreeBreakdownDisplayData,
+  L10N,
 } = require("devtools/client/memory/utils");
 
 var models = require("devtools/client/memory/models");
 
 var React = require("devtools/client/shared/vendor/react");
 var ReactDOM = require("devtools/client/shared/vendor/react-dom");
 var Heap = React.createFactory(require("devtools/client/memory/components/heap"));
+var CensusTreeItem = React.createFactory(require("devtools/client/memory/components/census-tree-item"));
 var DominatorTreeComponent = React.createFactory(require("devtools/client/memory/components/dominator-tree"));
 var DominatorTreeItem = React.createFactory(require("devtools/client/memory/components/dominator-tree-item"));
+var ShortestPaths = React.createFactory(require("devtools/client/memory/components/shortest-paths"));
 var Toolbar = React.createFactory(require("devtools/client/memory/components/toolbar"));
 
 // All tests are asynchronous.
 SimpleTest.waitForExplicitFinish();
 
 var noop = () => {};
 
+var TEST_CENSUS_TREE_ITEM_PROPS = Object.freeze({
+  item: Object.freeze({
+    bytes: 10,
+    count: 1,
+    totalBytes: 10,
+    totalCount: 1,
+    name: "foo",
+    children: [
+      Object.freeze({
+        bytes: 10,
+        count: 1,
+        totalBytes: 10,
+        totalCount: 1,
+        name: "bar",
+      })
+    ]
+  }),
+  depth: 0,
+  arrow: ">",
+  focused: true,
+  getPercentBytes: () => 50,
+  getPercentCount: () => 50,
+  showSign: false,
+  onViewSourceInDebugger: noop,
+  inverted: false,
+});
+
 // Counter for mock DominatorTreeNode ids.
 var TEST_NODE_ID_COUNTER = 0;
 
 /**
  * Create a mock DominatorTreeNode for testing, with sane defaults. Override any
  * property by providing it on `opts`. Optionally pass child nodes as well.
  *
  * @param {Object} opts
@@ -100,16 +130,31 @@ var TEST_DOMINATOR_TREE = Object.freeze(
 var TEST_DOMINATOR_TREE_PROPS = Object.freeze({
   dominatorTree: TEST_DOMINATOR_TREE,
   onLoadMoreSiblings: noop,
   onViewSourceInDebugger: noop,
   onExpand: noop,
   onCollapse: noop,
 });
 
+var TEST_SHORTEST_PATHS_PROPS = Object.freeze({
+  graph: Object.freeze({
+    nodes: [
+      { id: 1, label: ["other", "SomeType"] },
+      { id: 2, label: ["other", "SomeType"] },
+      { id: 3, label: ["other", "SomeType"] },
+    ],
+    edges: [
+      { from: 1, to: 2, name: "1->2" },
+      { from: 1, to: 3, name: "1->3" },
+      { from: 2, to: 3, name: "2->3" },
+    ],
+  }),
+});
+
 var TEST_HEAP_PROPS = Object.freeze({
   onSnapshotClick: noop,
   onLoadMoreSiblings: noop,
   onCensusExpand: noop,
   onCensusCollapse: noop,
   onDominatorTreeExpand: noop,
   onDominatorTreeCollapse: noop,
   onCensusFocus: noop,
@@ -141,16 +186,18 @@ var TEST_HEAP_PROPS = Object.freeze({
       focused: null,
     }),
     dominatorTree: TEST_DOMINATOR_TREE,
     error: null,
     imported: false,
     creationTime: 0,
     state: snapshotState.SAVED_CENSUS,
   }),
+  sizes: Object.freeze({ shortestPathsSize: .5 }),
+  onShortestPathsResize: noop,
 });
 
 var TEST_TOOLBAR_PROPS = Object.freeze({
   breakdowns: getBreakdownDisplayData(),
   onTakeSnapshotClick: noop,
   onImportClick: noop,
   onBreakdownChange: noop,
   onToggleRecordAllocationStacks: noop,
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_CensusTreeItem_01.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that children pointers show up at the correct times.
+-->
+<head>
+    <meta charset="utf-8">
+    <title>Tree component test</title>
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+    <!-- Give the container height so that the whole tree is rendered. -->
+    <div id="container" style="height: 900px;"></div>
+
+    <pre id="test">
+        <script src="head.js" type="application/javascript;version=1.8"></script>
+        <script type="application/javascript;version=1.8">
+         window.onload = Task.async(function* () {
+           try {
+             const container = document.getElementById("container");
+
+             yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+               inverted: true,
+               depth: 0,
+             })), container);
+
+             ok(!container.querySelector(".children-pointer"),
+                "Don't show children pointer for roots when we are inverted");
+
+             yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+               inverted: true,
+               depth: 1,
+             })), container);
+
+             ok(container.querySelector(".children-pointer"),
+                "Do show children pointer for non-roots when we are inverted");
+
+             yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+               inverted: false,
+               item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: undefined }),
+             })), container);
+
+             ok(!container.querySelector(".children-pointer"),
+                "Don't show children pointer when non-inverted and no children");
+
+             yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
+               inverted: false,
+               depth: 0,
+               item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: [{}] }),
+             })), container);
+
+             ok(container.querySelector(".children-pointer"),
+                "Do show children pointer when non-inverted and have children");
+
+           } catch(e) {
+             ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+           } finally {
+             SimpleTest.finish();
+           }
+         });
+        </script>
+    </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_ShortestPaths_01.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the ShortestPaths component properly renders a graph of the merged shortest paths.
+-->
+<head>
+    <meta charset="utf-8">
+    <title>Tree component test</title>
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+    <script type="application/javascript"
+            src="chrome://devtools/content/shared/vendor/d3.js">
+    </script>
+    <script type="application/javascript"
+            src="chrome://devtools/content/shared/vendor/dagre-d3.js">
+    </script>
+</head>
+<body>
+    <!-- Give the container height so that the whole tree is rendered. -->
+    <div id="container" style="height: 900px;"></div>
+
+    <pre id="test">
+        <script src="head.js" type="application/javascript;version=1.8"></script>
+        <script type="application/javascript;version=1.8">
+         window.onload = Task.async(function* () {
+           try {
+             const container = document.getElementById("container");
+
+             yield renderComponent(ShortestPaths(TEST_SHORTEST_PATHS_PROPS), container);
+
+             let found1 = false;
+             let found2 = false;
+             let found3 = false;
+
+             let found1to2 = false;
+             let found1to3 = false;
+             let found2to3 = false;
+
+             const tspans = [...container.querySelectorAll("tspan")];
+             for (let el of tspans) {
+               const text = el.textContent.trim();
+               dumpn("tspan's text = " + text);
+
+               switch (text) {
+                 // Nodes
+
+                 case "other › SomeType @ 0x1": {
+                   ok(!found1, "Should only find node 1 once");
+                   found1 = true;
+                   break;
+                 }
+
+                 case "other › SomeType @ 0x2": {
+                   ok(!found2, "Should only find node 2 once");
+                   found2 = true;
+                   break;
+                 }
+
+                 case "other › SomeType @ 0x3": {
+                   ok(!found3, "Should only find node 3 once");
+                   found3 = true;
+                   break;
+                 }
+
+                 // Edges
+
+                 case "1->2": {
+                   ok(!found1to2, "Should only find edge 1->2 once");
+                   found1to2 = true;
+                   break;
+                 }
+
+                 case "1->3": {
+                   ok(!found1to3, "Should only find edge 1->3 once");
+                   found1to3 = true;
+                   break;
+                 }
+
+                 case "2->3": {
+                   ok(!found2to3, "Should only find edge 2->3 once");
+                   found2to3 = true;
+                   break;
+                 }
+
+                 // Unexpected
+
+                 default: {
+                   ok(false, `Unexpected tspan: ${text}`);
+                   break;
+                 }
+               }
+             }
+
+             ok(found1, "Should have rendered node 1");
+             ok(found2, "Should have rendered node 2");
+             ok(found3, "Should have rendered node 3");
+
+             ok(found1to2, "Should have rendered edge 1->2");
+             ok(found1to3, "Should have rendered edge 1->3");
+             ok(found2to3, "Should have rendered edge 2->3");
+
+           } catch(e) {
+             ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+           } finally {
+             SimpleTest.finish();
+           }
+         });
+        </script>
+    </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/memory/test/chrome/test_ShortestPaths_02.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that the ShortestPaths component renders a suggestion to select a node when there is no graph.
+-->
+<head>
+    <meta charset="utf-8">
+    <title>Tree component test</title>
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+
+    <script type="application/javascript"
+            src="chrome://devtools/content/shared/vendor/d3.js">
+    </script>
+    <script type="application/javascript"
+            src="chrome://devtools/content/shared/vendor/dagre-d3.js">
+    </script>
+</head>
+<body>
+    <!-- Give the container height so that the whole tree is rendered. -->
+    <div id="container" style="height: 900px;"></div>
+
+    <pre id="test">
+        <script src="head.js" type="application/javascript;version=1.8"></script>
+        <script type="application/javascript;version=1.8">
+         window.onload = Task.async(function* () {
+           try {
+             const container = document.getElementById("container");
+
+             yield renderComponent(ShortestPaths(immutableUpdate(TEST_SHORTEST_PATHS_PROPS,
+                                                                 { graph: null })),
+                                   container);
+
+             ok(container.textContent.indexOf(L10N.getStr("shortest-paths.select-node")) !== -1,
+                "The node selection prompt is displayed");
+           } catch(e) {
+             ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+           } finally {
+             SimpleTest.finish();
+           }
+         });
+        </script>
+    </pre>
+</body>
+</html>
--- a/devtools/client/performance/modules/logic/marker-utils.js
+++ b/devtools/client/performance/modules/logic/marker-utils.js
@@ -207,18 +207,27 @@ const DOM = {
     let container = doc.createElement("vbox");
     let labelName = doc.createElement("label");
     labelName.className = "plain marker-details-labelname";
     labelName.setAttribute("value", L10N.getStr(`marker.field.${type}`));
     container.setAttribute("type", type);
     container.className = "marker-details-stack";
     container.appendChild(labelName);
 
+    // Workaround for profiles that have looping stack traces.  See
+    // bug 1246555.
     let wasAsyncParent = false;
+    let seen = new Set();
+
     while (frameIndex > 0) {
+      if (seen.has(frameIndex)) {
+        break;
+      }
+      seen.add(frameIndex);
+
       let frame = frames[frameIndex];
       let url = frame.source;
       let displayName = frame.functionDisplayName;
       let line = frame.line;
 
       // If the previous frame had an async parent, then the async
       // cause is in this frame and should be displayed.
       if (wasAsyncParent) {
--- a/devtools/client/performance/test/browser_perf-marker-details-01.js
+++ b/devtools/client/performance/test/browser_perf-marker-details-01.js
@@ -53,19 +53,31 @@ function* spawnTest() {
   markers.reduce((previous, m) => {
     if (m.start <= previous) {
       ok(false, "Markers are not in order");
       info(markers);
     }
     return m.start;
   }, 0);
 
+  // Override the timestamp marker's stack with our own recursive stack, which
+  // can happen for unknown reasons (bug 1246555); we should not cause a crash
+  // when attempting to render a recursive stack trace
+  let timestampMarker = markers.find(m => m.name === "ConsoleTime");
+  ok(typeof timestampMarker.stack === "number", "ConsoleTime marker has a stack before overwriting.");
+  let frames = PerformanceController.getCurrentRecording().getFrames();
+  let frameIndex = timestampMarker.stack = frames.length;
+  frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex + 1});
+  frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex + 2 });
+  frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex });
+
   const tests = {
     ConsoleTime: function (marker) {
       info("Got `ConsoleTime` marker with data: " + JSON.stringify(marker));
+      ok(marker.stack === frameIndex, "Should have the ConsoleTime marker with recursive stack");
       shouldHaveStack($, "startStack", marker);
       shouldHaveStack($, "endStack", marker);
       shouldHaveLabel($, "Timer Name:", "!!!", marker);
       return true;
     },
     TimeStamp: function (marker) {
       info("Got `TimeStamp` marker with data: " + JSON.stringify(marker));
       shouldHaveLabel($, "Label:", "go", marker);
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -320,17 +320,17 @@ pref("devtools.editor.tabsize", 2);
 pref("devtools.editor.expandtab", true);
 pref("devtools.editor.keymap", "default");
 pref("devtools.editor.autoclosebrackets", true);
 pref("devtools.editor.detectindentation", true);
 pref("devtools.editor.enableCodeFolding", true);
 pref("devtools.editor.autocomplete", true);
 
 // Enable the Font Inspector
-pref("devtools.fontinspector.enabled", true);
+pref("devtools.fontinspector.enabled", false);
 
 // Pref to store the browser version at the time of a telemetry ping for an
 // opened developer tool. This allows us to ping telemetry just once per browser
 // version for each user.
 pref("devtools.telemetry.tools.opened.version", "{}");
 
 // Enable the JSON View tool (an inspector for application/json documents)
 #ifdef MOZ_DEV_EDITION
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/.eslintrc
@@ -0,0 +1,5 @@
+{
+  "globals": {
+    "define": true,
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/h-split-box.js
@@ -0,0 +1,138 @@
+/* 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/. */
+
+// A box with a start and a end pane, separated by a dragable splitter that
+// allows the user to resize the relative widths of the panes.
+//
+//     +-----------------------+---------------------+
+//     |                       |                     |
+//     |                       |                     |
+//     |                       S                     |
+//     |      Start Pane       p     End Pane        |
+//     |                       l                     |
+//     |                       i                     |
+//     |                       t                     |
+//     |                       t                     |
+//     |                       e                     |
+//     |                       r                     |
+//     |                       |                     |
+//     |                       |                     |
+//     +-----------------------+---------------------+
+
+const {
+  DOM: dom,
+  createClass,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { assert } = require("devtools/shared/DevToolsUtils");
+
+module.exports = createClass({
+  displayName: "HSplitBox",
+
+  getDefaultProps() {
+    return {
+      startWidth: 0.5,
+      minStartWidth: "20px",
+      minEndWidth: "20px",
+    };
+  },
+
+  getInitialState() {
+    return {
+      mouseDown: false
+    };
+  },
+
+  propTypes: {
+    // The contents of the start pane.
+    start: PropTypes.any.isRequired,
+
+    // The contents of the end pane.
+    end: PropTypes.any.isRequired,
+
+    // The relative width of the start pane, expressed as a number between 0 and
+    // 1. The relative width of the end pane is 1 - startWidth. For example,
+    // with startWidth = .5, both panes are of equal width; with startWidth =
+    // .25, the start panel will take up 1/4 width and the end panel will take
+    // up 3/4 width.
+    startWidth: PropTypes.number,
+
+    // A minimum css width value for the start and end panes.
+    minStartWidth: PropTypes.any,
+    minEndWidth: PropTypes.any,
+
+    // A callback fired when the user drags the splitter to resize the relative
+    // pane widths. The function is passed the startWidth value that would put
+    // the splitter underneath the users mouse.
+    onResize: PropTypes.func.isRequired,
+  },
+
+  _onMouseDown(event) {
+    this.setState({ mouseDown: true });
+    event.preventDefault();
+  },
+
+  _onMouseUp(event) {
+    this.setState({ mouseDown: false });
+    event.preventDefault();
+  },
+
+  _onMouseMove(event) {
+    if (!this.state.mouseDown) {
+      return;
+    }
+
+    const rect = this.refs.box.getBoundingClientRect();
+    const { left, right } = rect;
+    const width = right - left;
+    const relative = event.clientX - left;
+    this.props.onResize(relative / width);
+
+    event.preventDefault();
+  },
+
+  componentDidMount() {
+    document.defaultView.top.addEventListener("mouseup", this._onMouseUp, false);
+    document.defaultView.top.addEventListener("mousemove", this._onMouseMove, false);
+  },
+
+  componentWillUnmount() {
+    document.defaultView.top.removeEventListener("mouseup", this._onMouseUp, false);
+    document.defaultView.top.removeEventListener("mousemove", this._onMouseMove, false);
+  },
+
+  render() {
+    const { start, end, startWidth, minStartWidth, minEndWidth } = this.props;
+    assert(0 <= startWidth && startWidth <= 1,
+           "0 <= this.props.startWidth <= 1");
+
+    return dom.div(
+      {
+        className: "h-split-box",
+        ref: "box",
+      },
+
+      dom.div(
+        {
+          className: "h-split-box-pane",
+          style: { flex: startWidth, minWidth: minStartWidth },
+        },
+        start
+      ),
+
+      dom.div({
+        className: "h-split-box-splitter",
+        onMouseDown: this._onMouseDown,
+      }),
+
+      dom.div(
+        {
+          className: "h-split-box-pane",
+          style: { flex: 1 - startWidth, minWidth: minEndWidth },
+        },
+        end
+      )
+    );
+  }
+});
--- a/devtools/client/shared/components/moz.build
+++ b/devtools/client/shared/components/moz.build
@@ -1,12 +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/.
 
+DIRS += [
+    'reps',
+]
+
 DevToolsModules(
     'frame.js',
+    'h-split-box.js',
     'tree.js',
 )
 
 MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
rename from devtools/client/jsonview/components/reps/array.js
rename to devtools/client/shared/components/reps/array.js
--- a/devtools/client/jsonview/components/reps/array.js
+++ b/devtools/client/shared/components/reps/array.js
@@ -1,189 +1,208 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Make this available to both AMD and CJS environments
 define(function(require, exports, module) {
-
-// Dependencies
-const React = require("devtools/client/shared/vendor/react");
-const { createFactories } = require("./rep-utils");
-const { Rep } = createFactories(require("./rep"));
-const { ObjectBox } = createFactories(require("./object-box"));
-const { Caption } = createFactories(require("./caption"));
-
-// Shortcuts
-const DOM = React.DOM;
-
-/**
- * Renders an array. The array is enclosed by left and right bracket
- * and the max number of rendered items depends on the current mode.
- */
-var ArrayRep = React.createClass({
-  displayName: "ArrayRep",
-
-  render: function() {
-    var mode = this.props.mode || "short";
-    var object = this.props.object;
-    var hasTwisty = this.hasSpecialProperties(object);
-
-    var items;
-
-    if (mode == "tiny") {
-      items = DOM.span({className: "length"}, object.length);
-    } else {
-      var max = (mode == "short") ? 3 : 300;
-      items = this.arrayIterator(object, max);
-    }
+  // Dependencies
+  const React = require("devtools/client/shared/vendor/react");
+  const { createFactories } = require("./rep-utils");
+  const { Rep } = createFactories(require("./rep"));
+  const { ObjectBox } = createFactories(require("./object-box"));
+  const { Caption } = createFactories(require("./caption"));
 
-    return (
-      ObjectBox({className: "array", onClick: this.onToggleProperties},
-        DOM.a({className: "objectLink", onclick: this.onClickBracket},
-          DOM.span({className: "arrayLeftBracket", role: "presentation"}, "[")
-        ),
-        items,
-        DOM.a({className: "objectLink", onclick: this.onClickBracket},
-          DOM.span({className: "arrayRightBracket", role: "presentation"}, "]")
-        ),
-        DOM.span({className: "arrayProperties", role: "group"})
-      )
-    )
-  },
-
-  getTitle: function(object, context) {
-    return "[" + object.length + "]";
-  },
-
-  arrayIterator: function(array, max) {
-    var items = [];
-
-    for (var i=0; i<array.length && i<=max; i++) {
-      try {
-        var delim = (i == array.length-1 ? "" : ", ");
-        var value = array[i];
-
-        if (value === array) {
-          items.push(Reference({
-            key: i,
-            object: value,
-            delim: delim
-          }));
-        } else {
-          items.push(ItemRep({
-            key: i,
-            object: value,
-            delim: delim
-          }));
-        }
-      } catch (exc) {
-        items.push(ItemRep({object: exc, delim: delim, key: i}));
-      }
-    }
-
-    if (array.length > max + 1) {
-      items.pop();
-      items.push(Caption({
-        key: "more",
-        object: Locale.$STR("jsonViewer.reps.more"),
-      }));
-    }
-
-    return items;
-  },
+  // Shortcuts
+  const DOM = React.DOM;
 
   /**
-   * Returns true if the passed object is an array with additional (custom)
-   * properties, otherwise returns false. Custom properties should be
-   * displayed in extra expandable section.
-   *
-   * Example array with a custom property.
-   * let arr = [0, 1];
-   * arr.myProp = "Hello";
-   *
-   * @param {Array} array The array object.
+   * Renders an array. The array is enclosed by left and right bracket
+   * and the max number of rendered items depends on the current mode.
    */
-  hasSpecialProperties: function(array) {
-    function isInteger(x) {
-      var y = parseInt(x, 10);
-      if (isNaN(y)) {
-        return false;
+  let ArrayRep = React.createClass({
+    displayName: "ArrayRep",
+
+    render: function() {
+      let mode = this.props.mode || "short";
+      let object = this.props.object;
+      let items;
+
+      if (mode == "tiny") {
+        items = DOM.span({className: "length"}, object.length);
+      } else {
+        let max = (mode == "short") ? 3 : 300;
+        items = this.arrayIterator(object, max);
       }
-      return x === y.toString();
-    }
 
-    var n = 0;
-    var props = Object.getOwnPropertyNames(array);
-    for (var i=0; i<props.length; i++) {
-      var p = props[i];
+      return (
+        ObjectBox({
+          className: "array",
+          onClick: this.onToggleProperties},
+          DOM.a({
+            className: "objectLink",
+            onclick: this.onClickBracket},
+            DOM.span({
+              className: "arrayLeftBracket",
+              role: "presentation"},
+              "["
+            )
+          ),
+          items,
+          DOM.a({
+            className: "objectLink",
+            onclick: this.onClickBracket},
+            DOM.span({
+              className: "arrayRightBracket",
+              role: "presentation"},
+              "]"
+            )
+          ),
+          DOM.span({
+            className: "arrayProperties",
+            role: "group"}
+          )
+        )
+      );
+    },
+
+    getTitle: function(object, context) {
+      return "[" + object.length + "]";
+    },
 
-      // Valid indexes are skipped
-      if (isInteger(p)) {
-        continue;
+    arrayIterator: function(array, max) {
+      let items = [];
+      let delim;
+
+      for (let i = 0; i < array.length && i <= max; i++) {
+        try {
+          let value = array[i];
+
+          delim = (i == array.length - 1 ? "" : ", ");
+
+          if (value === array) {
+            items.push(Reference({
+              key: i,
+              object: value,
+              delim: delim
+            }));
+          } else {
+            items.push(ItemRep({
+              key: i,
+              object: value,
+              delim: delim
+            }));
+          }
+        } catch (exc) {
+          items.push(ItemRep({
+            object: exc,
+            delim: delim,
+            key: i
+          }));
+        }
+      }
+
+      if (array.length > max + 1) {
+        items.pop();
+        items.push(Caption({
+          key: "more",
+          object: "more...",
+        }));
       }
 
-      // Ignore standard 'length' property, anything else is custom.
-      if (p != "length") {
-        return true;
+      return items;
+    },
+
+    /**
+     * Returns true if the passed object is an array with additional (custom)
+     * properties, otherwise returns false. Custom properties should be
+     * displayed in extra expandable section.
+     *
+     * Example array with a custom property.
+     * let arr = [0, 1];
+     * arr.myProp = "Hello";
+     *
+     * @param {Array} array The array object.
+     */
+    hasSpecialProperties: function(array) {
+      function isInteger(x) {
+        let y = parseInt(x, 10);
+        if (isNaN(y)) {
+          return false;
+        }
+        return x === y.toString();
       }
-    }
-
-    return false;
-  },
-
-  // Event Handlers
 
-  onToggleProperties: function(event) {
-  },
+      let props = Object.getOwnPropertyNames(array);
+      for (let i = 0; i < props.length; i++) {
+        let p = props[i];
+
+        // Valid indexes are skipped
+        if (isInteger(p)) {
+          continue;
+        }
 
-  onClickBracket: function(event) {
-  }
-});
+        // Ignore standard 'length' property, anything else is custom.
+        if (p != "length") {
+          return true;
+        }
+      }
 
-/**
- * Renders array item. Individual values are separated by a comma.
- */
-var ItemRep = React.createFactory(React.createClass({
-  displayName: "ItemRep",
+      return false;
+    },
+
+    // Event Handlers
+
+    onToggleProperties: function(event) {
+    },
 
-  render: function(){
-    var object = this.props.object;
-    var delim = this.props.delim;
-    return (
-      DOM.span({},
-        Rep({object: object}),
-        delim
-      )
-    )
-  }
-}));
+    onClickBracket: function(event) {
+    }
+  });
+
+  /**
+   * Renders array item. Individual values are separated by a comma.
+   */
+  let ItemRep = React.createFactory(React.createClass({
+    displayName: "ItemRep",
 
-/**
- * Renders cycle references in an array.
- */
-var Reference = React.createFactory(React.createClass({
-  displayName: "Reference",
+    render: function() {
+      let object = this.props.object;
+      let delim = this.props.delim;
+      return (
+        DOM.span({},
+          Rep({object: object}),
+          delim
+        )
+      );
+    }
+  }));
 
-  render: function(){
-    var tooltip = Locale.$STR("jsonView.reps.reference");
-    return (
-      span({title: tooltip},
-        "[...]")
-    )
-  }
-}));
+  /**
+   * Renders cycle references in an array.
+   */
+  let Reference = React.createFactory(React.createClass({
+    displayName: "Reference",
 
-function supportsObject(object, type) {
-  return Array.isArray(object) ||
-    Object.prototype.toString.call(object) === "[object Arguments]";
-}
+    render: function() {
+      let tooltip = "Circular reference";
+      return (
+        DOM.span({title: tooltip},
+          "[...]")
+      );
+    }
+  }));
 
-// Exports from this module
-exports.ArrayRep = {
-  rep: ArrayRep,
-  supportsObject: supportsObject
-};
+  function supportsObject(object, type) {
+    return Array.isArray(object) ||
+      Object.prototype.toString.call(object) === "[object Arguments]";
+  }
 
+  // Exports from this module
+  exports.ArrayRep = {
+    rep: ArrayRep,
+    supportsObject: supportsObject
+  };
 });
rename from devtools/client/jsonview/components/reps/caption.js
rename to devtools/client/shared/components/reps/caption.js
--- a/devtools/client/jsonview/components/reps/caption.js
+++ b/devtools/client/shared/components/reps/caption.js
@@ -1,31 +1,31 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Make this available to both AMD and CJS environments
 define(function(require, exports, module) {
-
-// Dependencies
-const React = require("devtools/client/shared/vendor/react");
-const DOM = React.DOM;
+  // Dependencies
+  const React = require("devtools/client/shared/vendor/react");
+  const DOM = React.DOM;
 
-/**
- * Renders a caption. This template is used by other components
- * that needs to distinguish between a simple text/value and a label.
- */
-const Caption = React.createClass({
-  displayName: "Caption",
+  /**
+   * Renders a caption. This template is used by other components
+   * that needs to distinguish between a simple text/value and a label.
+   */
+  const Caption = React.createClass({
+    displayName: "Caption",
 
-  render: function() {
-    return (
-      DOM.span({"className": "caption"}, this.props.object)
-    );
-  },
+    render: function() {
+      return (
+        DOM.span({"className": "caption"}, this.props.object)
+      );
+    },
+  });
+
+  // Exports from this module
+  exports.Caption = Caption;
 });
-
-// Exports from this module
-exports.Caption = Caption;
-});
copy from devtools/client/jsonview/components/reps/moz.build
copy to devtools/client/shared/components/reps/moz.build
--- a/devtools/client/jsonview/components/reps/moz.build
+++ b/devtools/client/shared/components/reps/moz.build
@@ -9,14 +9,12 @@ DevToolsModules(
     'caption.js',
     'null.js',
     'number.js',
     'object-box.js',
     'object-link.js',
     'object.js',
     'rep-utils.js',
     'rep.js',
+    'reps.css',
     'string.js',
-    'tabs.js',
-    'toolbar.js',
-    'tree-view.js',
     'undefined.js',
 )
rename from devtools/client/jsonview/components/reps/null.js
rename to devtools/client/shared/components/reps/null.js
--- a/devtools/client/jsonview/components/reps/null.js
+++ b/devtools/client/shared/components/reps/null.js
@@ -1,46 +1,45 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Make this available to both AMD and CJS environments
 define(function(require, exports, module) {
+  // Dependencies
+  const React = require("devtools/client/shared/vendor/react");
+  const { createFactories } = require("./rep-utils");
+  const { ObjectBox } = createFactories(require("./object-box"));
 
-// Dependencies
-const React = require("devtools/client/shared/vendor/react");
-const { createFactories } = require("./rep-utils");
-const { ObjectBox } = createFactories(require("./object-box"));
-
-/**
- * Renders null value
- */
-const Null = React.createClass({
-  displayName: "NullRep",
+  /**
+   * Renders null value
+   */
+  const Null = React.createClass({
+    displayName: "NullRep",
 
-  render: function() {
-    return (
-      ObjectBox({className: "null"},
-        "null"
-      )
-    )
-  },
-});
+    render: function() {
+      return (
+        ObjectBox({className: "null"},
+          "null"
+        )
+      );
+    },
+  });
 
-function supportsObject(object, type) {
-  if (object && object.type && object.type == "null") {
-    return true;
+  function supportsObject(object, type) {
+    if (object && object.type && object.type == "null") {
+      return true;
+    }
+
+    return (object == null);
   }
 
-  return (object == null);
-}
-
-// Exports from this module
+  // Exports from this module
 
-exports.Null = {
-  rep: Null,
-  supportsObject: supportsObject
-};
-
+  exports.Null = {
+    rep: Null,
+    supportsObject: supportsObject
+  };
 });
rename from devtools/client/jsonview/components/reps/number.js
rename to devtools/client/shared/components/reps/number.js
--- a/devtools/client/jsonview/components/reps/number.js
+++ b/devtools/client/shared/components/reps/number.js
@@ -1,47 +1,46 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Make this available to both AMD and CJS environments
 define(function(require, exports, module) {
+  // Dependencies
+  const React = require("devtools/client/shared/vendor/react");
+  const { createFactories } = require("./rep-utils");
+  const { ObjectBox } = createFactories(require("./object-box"));
 
-// Dependencies
-const React = require("devtools/client/shared/vendor/react");
-const { createFactories } = require("./rep-utils");
-const { ObjectBox } = createFactories(require("./object-box"));
-
-/**
- * Renders a number
- */
-const Number = React.createClass({
-  displayName: "Number",
+  /**
+   * Renders a number
+   */
+  const Number = React.createClass({
+    displayName: "Number",
 
-  render: function() {
-    var value = this.props.object;
-    return (
-      ObjectBox({className: "number"},
-        this.stringify(value)
-      )
-    )
-  },
+    render: function() {
+      let value = this.props.object;
+      return (
+        ObjectBox({className: "number"},
+          this.stringify(value)
+        )
+      );
+    },
 
-  stringify: function(object) {
-    return (Object.is(object, -0) ? "-0" : String(object));
-  },
+    stringify: function(object) {
+      return (Object.is(object, -0) ? "-0" : String(object));
+    },
+  });
+
+  function supportsObject(object, type) {
+    return type == "boolean" || type == "number";
+  }
+
+  // Exports from this module
+
+  exports.Number = {
+    rep: Number,
+    supportsObject: supportsObject
+  };
 });
-
-function supportsObject(object, type) {
-  return type == "boolean" || type == "number";
-}
-
-// Exports from this module
-
-exports.Number = {
-  rep: Number,
-  supportsObject: supportsObject
-};
-
-});
rename from devtools/client/jsonview/components/reps/object-box.js
rename to devtools/client/shared/components/reps/object-box.js
--- a/devtools/client/jsonview/components/reps/object-box.js
+++ b/devtools/client/shared/components/reps/object-box.js
@@ -1,35 +1,35 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Make this available to both AMD and CJS environments
 define(function(require, exports, module) {
-
-// Dependencies
-const React = require("devtools/client/shared/vendor/react");
-const DOM = React.DOM;
+  // Dependencies
+  const React = require("devtools/client/shared/vendor/react");
+  const DOM = React.DOM;
 
-/**
- * Renders a box for given object.
- */
-const ObjectBox = React.createClass({
-  displayName: "ObjectBox",
+  /**
+   * Renders a box for given object.
+   */
+  const ObjectBox = React.createClass({
+    displayName: "ObjectBox",
 
-  render: function() {
-    var className = this.props.className;
-    var boxClassName = className ? " objectBox-" + className : "";
+    render: function() {
+      let className = this.props.className;
+      let boxClassName = className ? " objectBox-" + className : "";
 
-    return (
-      DOM.span({className: "objectBox" + boxClassName, role: "presentation"},
-        this.props.children
-      )
-    )
-  }
+      return (
+        DOM.span({className: "objectBox" + boxClassName, role: "presentation"},
+          this.props.children
+        )
+      );
+    }
+  });
+
+  // Exports from this module
+  exports.ObjectBox = ObjectBox;
 });
-
-// Exports from this module
-exports.ObjectBox = ObjectBox;
-});
rename from devtools/client/jsonview/components/reps/object-link.js
rename to devtools/client/shared/components/reps/object-link.js
--- a/devtools/client/jsonview/components/reps/object-link.js
+++ b/devtools/client/shared/components/reps/object-link.js
@@ -1,36 +1,36 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Make this available to both AMD and CJS environments
 define(function(require, exports, module) {
-
-// Dependencies
-const React = require("devtools/client/shared/vendor/react");
-const DOM = React.DOM;
+  // Dependencies
+  const React = require("devtools/client/shared/vendor/react");
+  const DOM = React.DOM;
 
-/**
- * Renders a link for given object.
- */
-const ObjectLink = React.createClass({
-  displayName: "ObjectLink",
+  /**
+   * Renders a link for given object.
+   */
+  const ObjectLink = React.createClass({
+    displayName: "ObjectLink",
 
-  render: function() {
-    var className = this.props.className;
-    var objectClassName = className ? " objectLink-" + className : "";
-    var linkClassName = "objectLink" + objectClassName + " a11yFocus";
+    render: function() {
+      let className = this.props.className;
+      let objectClassName = className ? " objectLink-" + className : "";
+      let linkClassName = "objectLink" + objectClassName + " a11yFocus";
 
-    return (
-      DOM.a({className: linkClassName, _repObject: this.props.object},
-        this.props.children
-      )
-    )
-  }
+      return (
+        DOM.a({className: linkClassName, _repObject: this.props.object},
+          this.props.children
+        )
+      );
+    }
+  });
+
+  // Exports from this module
+  exports.ObjectLink = ObjectLink;
 });
-
-// Exports from this module
-exports.ObjectLink = ObjectLink;
-});
rename from devtools/client/jsonview/components/reps/object.js
rename to devtools/client/shared/components/reps/object.js
--- a/devtools/client/jsonview/components/reps/object.js
+++ b/devtools/client/shared/components/reps/object.js
@@ -1,178 +1,190 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Make this available to both AMD and CJS environments
 define(function(require, exports, module) {
+  // Dependencies
+  const React = require("devtools/client/shared/vendor/react");
+  const { createFactories } = require("./rep-utils");
+  const { ObjectBox } = createFactories(require("./object-box"));
+  const { Caption } = createFactories(require("./caption"));
 
-// Dependencies
-const React = require("devtools/client/shared/vendor/react");
-const { createFactories } = require("./rep-utils");
-const { ObjectBox } = createFactories(require("./object-box"));
-const { Caption } = createFactories(require("./caption"));
+  // Shortcuts
+  const DOM = React.DOM;
 
-// Shortcuts
-const DOM = React.DOM;
+  /**
+   * Renders an object. An object is represented by a list of its
+   * properties enclosed in curly brackets.
+   */
+  const Obj = React.createClass({
+    displayName: "Obj",
 
-/**
- * Renders an object. An object is represented by a list of its
- * properties enclosed in curly brackets.
- */
-const Obj = React.createClass({
-  displayName: "Obj",
+    render: function() {
+      let object = this.props.object;
+      let props = this.shortPropIterator(object);
 
-  render: function() {
-    var object = this.props.object;
-    var props = this.shortPropIterator(object);
+      return (
+        ObjectBox({className: "object"},
+          DOM.span({className: "objectTitle"}, this.getTitle(object)),
+          DOM.span({className: "objectLeftBrace", role: "presentation"}, "{"),
+          props,
+          DOM.span({className: "objectRightBrace"}, "}")
+        )
+      );
+    },
 
-    return (
-      ObjectBox({className: "object"},
-        DOM.span({className: "objectTitle"}, this.getTitle(object)),
-        DOM.span({className: "objectLeftBrace", role: "presentation"}, "{"),
-        props,
-        DOM.span({className: "objectRightBrace"}, "}")
-      )
-    )
-  },
+    getTitle: function() {
+      return "";
+    },
 
-  getTitle: function() {
-    return ""; // Could also be "Object";
-  },
+    longPropIterator: function(object) {
+      try {
+        return this.propIterator(object, 100);
+      } catch (err) {
+        console.error(err);
+      }
+      return [];
+    },
 
-  longPropIterator: function (object) {
-    try {
-      return this.propIterator(object, 100);
-    }
-    catch (err) {
-      console.error(err);
-    }
-  },
+    shortPropIterator: function(object) {
+      try {
+        return this.propIterator(object, 3);
+      } catch (err) {
+        console.error(err);
+      }
+      return [];
+    },
 
-  shortPropIterator: function (object) {
-    try {
-      return this.propIterator(object, /*could be a pref*/ 3);
-    }
-    catch (err) {
-      console.error(err);
-    }
-  },
+    propIterator: function(object, max) {
+      function isInterestingProp(t, value) {
+        return (t == "boolean" || t == "number" || (t == "string" && value) ||
+          (t == "object" && value && value.toString));
+      }
+
+      // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
+      if (Object.prototype.toString.call(object) === "[object Generator]") {
+        object = Object.getPrototypeOf(object);
+      }
 
-  propIterator: function(object, max) {
-    function isInterestingProp(t, value) {
-      return (t == "boolean" || t == "number" || (t == "string" && value) ||
-        (t == "object" && value && value.toString));
-    }
-
-    // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
-    if (Object.prototype.toString.call(object) === "[object Generator]") {
-      object = Object.getPrototypeOf(object);
-    }
+      // Object members with non-empty values are preferred since it gives the
+      // user a better overview of the object.
+      let props = [];
+      this.getProps(props, object, max, isInterestingProp);
 
-    // Object members with non-empty values are preferred since it gives the
-    // user a better overview of the object.
-    var props = [];
-    this.getProps(props, object, max, isInterestingProp);
+      if (props.length <= max) {
+        // There are not enough props yet (or at least, not enough props to
+        // be able to know whether we should print "more..." or not).
+        // Let's display also empty members and functions.
+        this.getProps(props, object, max, function(t, value) {
+          return !isInterestingProp(t, value);
+        });
+      }
 
-    if (props.length <= max) {
-      // There are not enough props yet (or at least, not enough props to
-      // be able to know whether we should print "more..." or not).
-      // Let's display also empty members and functions.
-      this.getProps(props, object, max, function(t, value) {
-        return !isInterestingProp(t, value);
-      });
-    }
+      if (props.length > max) {
+        props.pop();
+        props.push(Caption({
+          key: "more",
+          object: "more...",
+        }));
+      } else if (props.length > 0) {
+        // Remove the last comma.
+        props[props.length - 1] = React.cloneElement(
+          props[props.length - 1], { delim: "" });
+      }
 
-    if (props.length > max) {
-      props.pop();
-      props.push(Caption({
-        key: "more",
-        object: Locale.$STR("jsonViewer.reps.more"),
-      }));
-    }
-    else if (props.length > 0) {
-      // Remove the last comma.
-      props[props.length-1] = React.cloneElement(
-        props[props.length-1], { delim: "" });
-    }
+      return props;
+    },
 
-    return props;
-  },
+    getProps: function(props, object, max, filter) {
+      max = max || 3;
+      if (!object) {
+        return [];
+      }
+
+      let mode = this.props.mode;
+
+      try {
+        for (let name in object) {
+          if (props.length > max) {
+            return [];
+          }
 
-  getProps: function (props, object, max, filter) {
-    max = max || 3;
-    if (!object) {
-      return [];
-    }
-
-    var len = 0;
-    var mode = this.props.mode;
+          let value;
+          try {
+            value = object[name];
+          } catch (exc) {
+            continue;
+          }
 
-    try {
-      for (var name in object) {
-        if (props.length > max) {
-          return;
+          let t = typeof value;
+          if (filter(t, value)) {
+            props.push(PropRep({
+              key: name,
+              mode: mode,
+              name: name,
+              object: value,
+              equal: ": ",
+              delim: ", ",
+            }));
+          }
         }
+      } catch (err) {
+        console.error(err);
+      }
 
-        var value;
-        try {
-          value = object[name];
-        }
-        catch (exc) {
-          continue;
-        }
+      return [];
+    },
+  });
 
-        var t = typeof(value);
-        if (filter(t, value)) {
-          props.push(PropRep({
-            key: name,
-            mode: "short",
-            name: name,
-            object: value,
-            equal: ": ",
-            delim: ", ",
-            mode: mode,
-          }));
-        }
-      }
-    }
-    catch (exc) {
-    }
-  },
-});
+  /**
+   * Renders object property, name-value pair.
+   */
+  let PropRep = React.createFactory(React.createClass({
+    displayName: "PropRep",
 
-/**
- * Renders object property, name-value pair.
- */
-var PropRep = React.createFactory(React.createClass({
-  displayName: "PropRep",
+    render: function() {
+      let { Rep } = createFactories(require("./rep"));
+      let object = this.props.object;
+      let mode = this.props.mode;
 
-  render: function(){
-    var { Rep } = createFactories(require("./rep"));
-    var object = this.props.object;
-    var mode = this.props.mode;
-    return (
-      DOM.span({},
-        DOM.span({"className": "nodeName"}, this.props.name),
-        DOM.span({"className": "objectEqual", role: "presentation"}, this.props.equal),
-        Rep({object: object, mode: mode}),
-        DOM.span({"className": "objectComma", role: "presentation"}, this.props.delim)
-      )
-    );
+      return (
+        DOM.span({},
+          DOM.span({
+            "className": "nodeName"},
+            this.props.name
+          ),
+          DOM.span({
+            "className": "objectEqual",
+            role: "presentation"},
+            this.props.equal
+          ),
+          Rep({
+            object: object,
+            mode: mode
+          }),
+          DOM.span({
+            "className": "objectComma",
+            role: "presentation"},
+            this.props.delim
+          )
+        )
+      );
+    }
+  }));
+
+  function supportsObject(object, type) {
+    return true;
   }
-}));
 
-function supportsObject(object, type) {
-  return true;
-}
+  // Exports from this module
 
-// Exports from this module
-
-exports.Obj = {
-  rep: Obj,
-  supportsObject: supportsObject
-};
-
+  exports.Obj = {
+    rep: Obj,
+    supportsObject: supportsObject
+  };
 });
rename from devtools/client/jsonview/components/reps/rep-utils.js
rename to devtools/client/shared/components/reps/rep-utils.js
--- a/devtools/client/jsonview/components/reps/rep-utils.js
+++ b/devtools/client/shared/components/reps/rep-utils.js
@@ -1,29 +1,29 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Make this available to both AMD and CJS environments
 define(function(require, exports, module) {
-
-// Dependencies
-const React = require("devtools/client/shared/vendor/react");
+  // Dependencies
+  const React = require("devtools/client/shared/vendor/react");
 
-/**
- * Create React factories for given arguments.
- * Example:
- *   const { Rep } = createFactories(require("./rep"));
- */
-function createFactories(args) {
-  var result = {};
-  for (var p in args) {
-    result[p] = React.createFactory(args[p]);
+  /**
+   * Create React factories for given arguments.
+   * Example:
+   *   const { Rep } = createFactories(require("./rep"));
+   */
+  function createFactories(args) {
+    let result = {};
+    for (let p in args) {
+      result[p] = React.createFactory(args[p]);
+    }
+    return result;
   }
-  return result;
-}
 
-// Exports from this module
-exports.createFactories = createFactories;
+  // Exports from this module
+  exports.createFactories = createFactories;
 });
rename from devtools/client/jsonview/components/reps/rep.js
rename to devtools/client/shared/components/reps/rep.js
--- a/devtools/client/jsonview/components/reps/rep.js
+++ b/devtools/client/shared/components/reps/rep.js
@@ -1,87 +1,86 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Make this available to both AMD and CJS environments
 define(function(require, exports, module) {
+  // Dependencies
+  const React = require("devtools/client/shared/vendor/react");
 
-// Dependencies
-const React = require("devtools/client/shared/vendor/react");
+  // Load all existing rep templates
+  const { Undefined } = require("./undefined");
+  const { Null } = require("./null");
+  const { StringRep } = require("./string");
+  const { Number } = require("./number");
+  const { ArrayRep } = require("./array");
+  const { Obj } = require("./object");
 
-// Load all existing rep templates
-const { Undefined } = require("./undefined");
-const { Null } = require("./null");
-const { StringRep } = require("./string");
-const { Number } = require("./number");
-const { ArrayRep } = require("./array");
-const { Obj } = require("./object");
+  // List of all registered template.
+  // XXX there should be a way for extensions to register a new
+  // or modify an existing rep.
+  let reps = [Undefined, Null, StringRep, Number, ArrayRep, Obj];
+  let defaultRep;
 
-// List of all registered template.
-// XXX there should be a way for extensions to register a new
-// or modify an existing rep.
-var reps = [Undefined, Null, StringRep, Number, ArrayRep, Obj];
-var defaultRep;
+  /**
+   * Generic rep that is using for rendering native JS types or an object.
+   * The right template used for rendering is picked automatically according
+   * to the current value type. The value must be passed is as 'object'
+   * property.
+   */
+  const Rep = React.createClass({
+    displayName: "Rep",
+
+    render: function() {
+      let rep = getRep(this.props.object);
+      return rep(this.props);
+    },
+  });
 
-/**
- * Generic rep that is using for rendering native JS types or an object.
- * The right template used for rendering is picked automatically according
- * to the current value type. The value must be passed is as 'object'
- * property.
- */
-const Rep = React.createClass({
-  displayName: "Rep",
+  // Helpers
 
-  render: function() {
-    var rep = getRep(this.props.object);
-    return rep(this.props);
-  },
-});
+  /**
+   * Return a rep object that is responsible for rendering given
+   * object.
+   *
+   * @param object {Object} Object to be rendered in the UI. This
+   * can be generic JS object as well as a grip (handle to a remote
+   * debuggee object).
+   */
+  function getRep(object) {
+    let type = typeof object;
+    if (type == "object" && object instanceof String) {
+      type = "string";
+    }
 
-// Helpers
+    if (isGrip(object)) {
+      type = object.class;
+    }
 
-/**
- * Return a rep object that is responsible for rendering given
- * object.
- *
- * @param object {Object} Object to be rendered in the UI. This
- * can be generic JS object as well as a grip (handle to a remote
- * debuggee object).
- */
-function getRep(object) {
-  var type = typeof(object);
-  if (type == "object" && object instanceof String) {
-    type = "string";
+    for (let i = 0; i < reps.length; i++) {
+      let rep = reps[i];
+      try {
+        // supportsObject could return weight (not only true/false
+        // but a number), which would allow to priorities templates and
+        // support better extensibility.
+        if (rep.supportsObject(object, type)) {
+          return React.createFactory(rep.rep);
+        }
+      } catch (err) {
+        console.error("reps.getRep; EXCEPTION ", err, err);
+      }
+    }
+
+    return React.createFactory(defaultRep.rep);
   }
 
-  if (isGrip(object)) {
-    type = object.class;
+  function isGrip(object) {
+    return object && object.actor;
   }
 
-  for (var i=0; i<reps.length; i++) {
-    var rep = reps[i];
-    try {
-      // supportsObject could return weight (not only true/false
-      // but a number), which would allow to priorities templates and
-      // support better extensibility.
-      if (rep.supportsObject(object, type)) {
-        return React.createFactory(rep.rep);
-      }
-    }
-    catch (err) {
-      console.error("reps.getRep; EXCEPTION ", err, err);
-    }
-  }
-
-  return React.createFactory(defaultRep.rep);
-}
-
-function isGrip(object) {
-  return object && object.actor;
-}
-
-// Exports from this module
-exports.Rep = Rep;
+  // Exports from this module
+  exports.Rep = Rep;
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/reps/reps.css
@@ -0,0 +1,219 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+.objectLink:hover {
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+/******************************************************************************/
+
+.inline {
+  display: inline;
+  white-space: normal;
+}
+
+.objectBox-object {
+  font-family: Lucida Grande, sans-serif;
+  font-weight: bold;
+  color: DarkGreen;
+  white-space: pre-wrap;
+}
+
+.objectBox-string,
+.objectBox-text,
+.objectBox-number,
+.objectLink-element,
+.objectLink-textNode,
+.objectLink-function,
+.objectBox-stackTrace,
+.objectLink-profile,
+.objectBox-table {
+  font-family: monospace;
+}
+
+.objectBox-string,
+.objectBox-text,
+.objectLink-textNode,
+.objectBox-table {
+  white-space: pre-wrap;
+}
+
+.objectBox-number,
+.objectLink-styleRule,
+.objectLink-element,
+.objectLink-textNode,
+.objectBox-array > .length {
+  color: #000088;
+}
+
+.objectBox-string {
+  color: #FF0000;
+}
+
+.objectLink-function,
+.objectBox-stackTrace,
+.objectLink-profile {
+  color: DarkGreen;
+}
+
+.objectBox-null,
+.objectBox-undefined,
+.objectBox-hint,
+.logRowHint {
+  font-style: italic;
+  color: #787878;
+}
+
+.objectBox-scope {
+  color: #707070;
+}
+.objectBox-optimizedAway {
+  color: #909090;
+}
+
+.objectLink-sourceLink {
+  position: absolute;
+  right: 4px;
+  top: 2px;
+  padding-left: 8px;
+  font-family: Lucida Grande, sans-serif;
+  font-weight: bold;
+  color: #0000FF;
+}
+
+.objectLink-sourceLink > .systemLink {
+  float: right;
+  color: #FF0000;
+}
+
+/******************************************************************************/
+
+.objectLink-event,
+.objectLink-eventLog,
+.objectLink-regexp,
+.objectLink-object,
+.objectLink-Date {
+  font-family: Lucida Grande, sans-serif;
+  font-weight: bold;
+  color: DarkGreen;
+  white-space: pre-wrap;
+}
+
+/******************************************************************************/
+
+.objectLink-object .nodeName,
+.objectLink-NamedNodeMap .nodeName,
+.objectLink-NamedNodeMap .objectEqual,
+.objectLink-NamedNodeMap .arrayLeftBracket,
+.objectLink-NamedNodeMap .arrayRightBracket,
+.objectLink-Attr .attrEqual,
+.objectLink-Attr .attrTitle {
+  color: rgb(0, 0, 136)
+}
+
+.objectLink-object .nodeName {
+  font-weight: normal;
+}
+
+/******************************************************************************/
+
+.objectLeftBrace,
+.objectRightBrace,
+.objectEqual,
+.objectComma,
+.arrayLeftBracket,
+.arrayRightBracket,
+.arrayComma {
+  font-family: monospace;
+}
+
+.objectLeftBrace,
+.objectRightBrace,
+.arrayLeftBracket,
+.arrayRightBracket {
+  cursor: pointer;
+  font-weight: bold;
+}
+
+.objectLeftBrace,
+.arrayLeftBracket {
+  margin-right: 4px;
+}
+
+.objectRightBrace,
+.arrayRightBracket {
+  margin-left: 4px;
+}
+
+/******************************************************************************/
+/* Cycle reference*/
+
+.objectLink-Reference {
+  font-family: monospace;
+  font-weight: bold;
+  color: rgb(102, 102, 255);
+}
+
+.objectBox-array > .objectTitle {
+  font-weight: bold;
+  color: DarkGreen;
+}
+
+/******************************************************************************/
+
+.caption {
+  font-family: Lucida Grande, Tahoma, sans-serif;
+  font-weight: bold;
+  color:  #444444;
+}
+
+/******************************************************************************/
+/* Light Theme & Dark Theme */
+
+.theme-dark .domLabel,
+.theme-light .domLabel {
+  color: var(--theme-highlight-blue);
+}
+
+.theme-dark .objectBox-array .length,
+.theme-light .objectBox-array .length,
+.theme-dark .objectBox-number,
+.theme-light .objectBox-number {
+  color: var(--theme-highlight-green);
+}
+
+.theme-dark .objectBox-string,
+.theme-light .objectBox-string {
+  color: var(--theme-highlight-orange);
+}
+
+.theme-dark .objectBox-null,
+.theme-dark .objectBox-undefined,
+.theme-light .objectBox-null,
+.theme-light .objectBox-undefined {
+  font-style: normal;
+  color: var(--theme-comment);
+}
+
+.theme-dark .objectBox-array,
+.theme-light .objectBox-array {
+  color: var(--theme-body-color);
+}
+
+.theme-dark .objectBox-object,
+.theme-light .objectBox-object {
+  font-family: Lucida Grande, sans-serif;
+  font-weight: normal;
+  color: var(--theme-highlight-blue);
+  white-space: pre-wrap;
+}
+
+.theme-dark .caption,
+.theme-light .caption {
+  font-family: Lucida Grande, Tahoma, sans-serif;
+  font-weight: normal;
+  color: var(--theme-highlight-blue);
+}
rename from devtools/client/jsonview/components/reps/string.js
rename to devtools/client/shared/components/reps/string.js
--- a/devtools/client/jsonview/components/reps/string.js
+++ b/devtools/client/shared/components/reps/string.js
@@ -1,102 +1,101 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Make this available to both AMD and CJS environments
 define(function(require, exports, module) {
-
-// Dependencies
-const React = require("devtools/client/shared/vendor/react");
-const { createFactories } = require("./rep-utils");
-const { ObjectBox } = createFactories(require("./object-box"));
+  // Dependencies
+  const React = require("devtools/client/shared/vendor/react");
+  const { createFactories } = require("./rep-utils");
+  const { ObjectBox } = createFactories(require("./object-box"));
 
-/**
- * Renders a string. String value is enclosed within quotes.
- */
-const StringRep = React.createClass({
-  displayName: "StringRep",
+  /**
+   * Renders a string. String value is enclosed within quotes.
+   */
+  const StringRep = React.createClass({
+    displayName: "StringRep",
 
-  render: function() {
-    var text = this.props.object;
-    var member = this.props.member;
-    if (member && member.open) {
-      return (
-        ObjectBox({className: "string"},
-          "\"" + text + "\""
-        )
-      )
-    } else {
+    render: function() {
+      let text = this.props.object;
+      let member = this.props.member;
+      if (member && member.open) {
+        return (
+          ObjectBox({className: "string"},
+            "\"" + text + "\""
+          )
+        );
+      }
+
       return (
         ObjectBox({className: "string"},
           "\"" + cropMultipleLines(text) + "\""
         )
-      )
-    }
-  },
-});
+      );
+    },
+  });
 
-// Helpers
+  // Helpers
 
-function escapeNewLines(value) {
-  return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
-};
+  function escapeNewLines(value) {
+    return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
+  }
 
-function cropMultipleLines(text, limit) {
-  return escapeNewLines(cropString(text, limit));
-};
-
-function cropString(text, limit, alternativeText) {
-  if (!alternativeText) {
-    alternativeText = "...";
+  function cropMultipleLines(text, limit) {
+    return escapeNewLines(cropString(text, limit));
   }
 
-  // Make sure it's a string.
-  text = text + "";
+  function cropString(text, limit, alternativeText) {
+    if (!alternativeText) {
+      alternativeText = "...";
+    }
+
+    // Make sure it's a string.
+    text = text + "";
+
+    // Use default limit if necessary.
+    if (!limit) {
+      limit = 50;
+    }
 
-  // Use default limit if necessary.
-  if (!limit) {
-    limit = 50;
-  }
+    // Crop the string only if a limit is actually specified.
+    if (limit <= 0) {
+      return text;
+    }
 
-  // Crop the string only if a limit is actually specified.
-  if (limit <= 0) {
+    // Set the limit at least to the length of the alternative text
+    // plus one character of the original text.
+    if (limit <= alternativeText.length) {
+      limit = alternativeText.length + 1;
+    }
+
+    let halfLimit = (limit - alternativeText.length) / 2;
+
+    if (text.length > limit) {
+      return text.substr(0, Math.ceil(halfLimit)) + alternativeText +
+        text.substr(text.length - Math.floor(halfLimit));
+    }
+
     return text;
   }
 
-  // Set the limit at least to the length of the alternative text
-  // plus one character of the original text.
-  if (limit <= alternativeText.length) {
-    limit = alternativeText.length + 1;
+  function isCropped(value) {
+    let cropLength = 50;
+    return typeof value == "string" && value.length > cropLength;
   }
 
-  var halfLimit = (limit - alternativeText.length) / 2;
-
-  if (text.length > limit) {
-    return text.substr(0, Math.ceil(halfLimit)) + alternativeText +
-      text.substr(text.length - Math.floor(halfLimit));
+  function supportsObject(object, type) {
+    return (type == "string");
   }
 
-  return text;
-};
-
-function isCropped(value) {
-  var cropLength = 50;
-  return typeof(value) == "string" && value.length > cropLength;
-}
+  // Exports from this module
 
-function supportsObject(object, type) {
-  return (type == "string");
-}
-
-// Exports from this module
-
-exports.StringRep = {
-  rep: StringRep,
-  supportsObject: supportsObject,
-  isCropped: isCropped
-};
-
+  exports.StringRep = {
+    rep: StringRep,
+    supportsObject: supportsObject,
+    isCropped: isCropped
+  };
 });
rename from devtools/client/jsonview/components/reps/undefined.js
rename to devtools/client/shared/components/reps/undefined.js
--- a/devtools/client/jsonview/components/reps/undefined.js
+++ b/devtools/client/shared/components/reps/undefined.js
@@ -1,46 +1,45 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+// Make this available to both AMD and CJS environments
 define(function(require, exports, module) {
+  // Dependencies
+  const React = require("devtools/client/shared/vendor/react");
+  const { createFactories } = require("./rep-utils");
+  const { ObjectBox } = createFactories(require("./object-box"));
 
-// Dependencies
-const React = require("devtools/client/shared/vendor/react");
-const { createFactories } = require("./rep-utils");
-const { ObjectBox } = createFactories(require("./object-box"));
-
-/**
- * Renders undefined value
- */
-const Undefined = React.createClass({
-  displayName: "UndefinedRep",
+  /**
+   * Renders undefined value
+   */
+  const Undefined = React.createClass({
+    displayName: "UndefinedRep",
 
-  render: function() {
-    return (
-      ObjectBox({className: "undefined"},
-        "undefined"
-      )
-    )
-  },
-});
+    render: function() {
+      return (
+        ObjectBox({className: "undefined"},
+          "undefined"
+        )
+      );
+    },
+  });
 
-function supportsObject(object, type) {
-  if (object && object.type && object.type == "undefined") {
-    return true;
+  function supportsObject(object, type) {
+    if (object && object.type && object.type == "undefined") {
+      return true;
+    }
+
+    return (type == "undefined");
   }
 
-  return (type == "undefined");
-}
-
-// Exports from this module
+  // Exports from this module
 
-exports.Undefined = {
-  rep: Undefined,
-  supportsObject: supportsObject
-};
-
+  exports.Undefined = {
+    rep: Undefined,
+    supportsObject: supportsObject
+  };
 });
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -1,12 +1,13 @@
 [DEFAULT]
 support-files =
   head.js
 
+[test_HSplitBox_01.html]
 [test_frame_01.html]
 [test_frame_02.html]
 [test_tree_01.html]
 [test_tree_02.html]
 [test_tree_03.html]
 [test_tree_04.html]
 [test_tree_05.html]
 [test_tree_06.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_HSplitBox_01.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Basic tests for the HSplitBox component.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Tree component test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript "src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
+  <style>
+    html {
+      --theme-splitter-color: black;
+    }
+  </style>
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+const FUDGE_FACTOR = .1;
+function aboutEq(a, b) {
+  dumpn(`Checking ${a} ~= ${b}`);
+  return Math.abs(a - b) < FUDGE_FACTOR;
+}
+
+window.onload = Task.async(function* () {
+  try {
+    const React = browserRequire("devtools/client/shared/vendor/react");
+    const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+
+    let HSplitBox = React.createFactory(browserRequire("devtools/client/shared/components/h-split-box"));
+    ok(HSplitBox, "Should get HSplitBox");
+
+    const newSizes = [];
+    const box = ReactDOM.render(HSplitBox({
+      start: "hello!",
+      end: "world!",
+      startWidth: .5,
+      onResize(newSize) {
+        newSizes.push(newSize);
+      },
+    }), window.document.body);
+
+    // Test that we properly rendered our two panes.
+
+    let panes = document.querySelectorAll(".h-split-box-pane");
+    is(panes.length, 2, "Should get two panes");
+    is(panes[0].style.flexGrow, "0.5", "Each pane should have .5 width");
+    is(panes[1].style.flexGrow, "0.5", "Each pane should have .5 width");
+    is(panes[0].textContent.trim(), "hello!", "First pane should be hello");
+    is(panes[1].textContent.trim(), "world!", "Second pane should be world");
+
+    // Now change the left width and assert that the changes are reflected.
+
+    yield setProps(box, { startWidth: .25 });
+    panes = document.querySelectorAll(".h-split-box-pane");
+    is(panes.length, 2, "Should still have two panes");
+    is(panes[0].style.flexGrow, "0.25", "First pane's width should be .25");
+    is(panes[1].style.flexGrow, "0.75", "Second pane's width should be .75");
+
+    // Mouse moves without having grabbed the splitter should have no effect.
+
+    let container = document.querySelector(".h-split-box");
+    ok(container, "Should get our container .h-split-box");
+
+    const { left, top, width } = container.getBoundingClientRect();
+    const middle = left + width / 2;
+    const oneQuarter = left + width / 4;
+    const threeQuarters = left + 3 * width / 4;
+
+    synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
+    is(newSizes.length, 0, "Mouse moves without dragging the splitter should have no effect");
+
+    // Send a mouse down on the splitter, and then move the mouse a couple
+    // times. Now we should get resizes.
+
+    const splitter = document.querySelector(".h-split-box-splitter");
+    ok(splitter, "Should get our splitter");
+
+    synthesizeMouseAtCenter(splitter, { button: 1, type: "mousedown" }, window);
+
+    function mouseMove(clientX) {
+      const event = new MouseEvent("mousemove", { clientX });
+      document.defaultView.top.dispatchEvent(event);
+    }
+
+    mouseMove(middle);
+    is(newSizes.length, 1, "Should get 1 resize");
+    ok(aboutEq(newSizes[0], .5), "New size should be ~.5");
+
+    mouseMove(left);
+    is(newSizes.length, 2, "Should get 2 resizes");
+    ok(aboutEq(newSizes[1], 0), "New size should be ~0");
+
+    mouseMove(oneQuarter);
+    is(newSizes.length, 3, "Sould get 3 resizes");
+    ok(aboutEq(newSizes[2], .25), "New size should be ~.25");
+
+    mouseMove(threeQuarters);
+    is(newSizes.length, 4, "Should get 4 resizes");
+    ok(aboutEq(newSizes[3], .75), "New size should be ~.75");
+
+    synthesizeMouseAtCenter(splitter, { button: 1, type: "mouseup" }, window);
+
+    // Now that we have let go of the splitter, mouse moves should not result in resizes.
+
+    synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
+    is(newSizes.length, 4, "Should still have 4 resizes");
+
+  } catch(e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+});
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/components-h-split-box.css
@@ -0,0 +1,31 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * HSplitBox Component
+ * Styles for React component at `devtools/client/shared/components/h-split-box.js`
+ */
+
+.h-split-box,
+.h-split-box-pane {
+  overflow: auto;
+  margin: 0;
+  padding: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.h-split-box {
+  display: flex;
+  flex-direction: row;
+  flex: 1;
+}
+
+.h-split-box-splitter {
+  -moz-border-end: 1px solid var(--theme-splitter-color);
+  cursor: ew-resize;
+  width: 3px;
+  -moz-margin-start: -3px;
+}
--- a/devtools/client/themes/memory.css
+++ b/devtools/client/themes/memory.css
@@ -219,16 +219,36 @@ html, body, #app, #memory-tool {
   /* Text inside a selected item should not be custom colored. */
   color: inherit !important;
 }
 
 /**
  * Main panel
  */
 
+.vbox {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+  padding: 0;
+  margin: 0;
+}
+
+.vbox > * {
+  flex: 1;
+
+  /**
+   * By default, flex items have min-width: auto;
+   * (https://drafts.csswg.org/css-flexbox/#min-size-auto)
+   */
+  min-width: 0;
+}
+
 #heap-view {
   /**
    * Flex: contains a .heap-view-panel which needs to fill out all the
    * available space, horizontally and vertically.
    */;
   display: flex;
   /**
    * Flexing to fill out remaining horizontal space. The preceeding sibling
@@ -262,17 +282,18 @@ html, body, #app, #memory-tool {
   /**
    * By default, flex items have min-width: auto;
    * (https://drafts.csswg.org/css-flexbox/#min-size-auto)
    */
   min-width: 0;
 }
 
 #heap-view > .heap-view-panel > .snapshot-status,
-#heap-view > .heap-view-panel > .take-snapshot {
+#heap-view > .heap-view-panel > .take-snapshot,
+#shortest-paths-select-node-msg {
   margin: auto;
   margin-top: 65px;
   font-size: 120%;
 }
 
 #heap-view > .heap-view-panel > .take-snapshot {
   padding: 5px;
 }
@@ -292,30 +313,47 @@ html, body, #app, #memory-tool {
    * Flex: contains several span columns, all of which need to be laid out
    * horizontally. All columns except the last one have percentage widths, and
    * the last one needs to flex to fill out all remaining horizontal space.
    */
   display: flex;
   color: var(--theme-body-color);
   background-color: var(--theme-tab-toolbar-background);
   border-bottom: 1px solid var(--cell-border-color);
+  flex: 0;
+}
+
+.header > span,
+#shortest-paths-header {
+  text-overflow: ellipsis;
+  line-height: var(--heap-tree-header-height);
+  justify-content: center;
+  justify-self: center;
+  white-space: nowrap;
 }
 
 .header > span {
   overflow: hidden;
-  text-overflow: ellipsis;
-  line-height: var(--heap-tree-header-height);
-  justify-content: center;
-  white-space: nowrap;
 }
 
 .header > .heap-tree-item-name {
   justify-content: flex-start;
 }
 
+#shortest-paths {
+  background-color: var(--theme-body-background);
+  overflow: hidden;
+  height: 100%;
+  width: 100%;
+}
+
+#shortest-paths-select-node-msg {
+  justify-self: center;
+}
+
 /**
  * Heap tree view body
  */
 
 .tree {
   /**
    * Flexing to fill out remaining vertical space. @see .heap-view-panel
    */
@@ -324,16 +362,20 @@ html, body, #app, #memory-tool {
   background-color: var(--theme-body-background);
 }
 
 .tree-node {
   height: var(--heap-tree-row-height);
   line-height: var(--heap-tree-row-height);
 }
 
+.children-pointer {
+  padding-inline-end: 5px;
+}
+
 /**
  * Heap tree view columns
  */
 
 .heap-tree-item {
   /**
    * Flex: contains several span columns, all of which need to be laid out
    * horizontally. All columns except the last one have percentage widths, and
@@ -479,8 +521,55 @@ html, body, #app, #memory-tool {
 
 .no-allocation-stacks {
   border-color: var(--theme-splitter-color);
   border-style: solid;
   border-width: 0px 0px 1px 0px;
   text-align: center;
   padding: 5px;
 }
+
+/**
+ * Dagre-D3 graphs
+ */
+
+.edgePath path {
+  stroke-width: 1px;
+  fill: none;
+}
+
+.theme-dark .edgePath path {
+  stroke: var(--theme-body-color-alt);
+}
+.theme-light .edgePath path {
+  stroke: var(--theme-splitter-color);
+}
+
+g.edgeLabel rect {
+  fill: var(--theme-body-background);
+}
+g.edgeLabel tspan {
+  fill: var(--theme-body-color-alt);
+}
+
+.nodes rect {
+  stroke-width: 1px;
+}
+
+.nodes rect {
+  stroke: var(--theme-tab-toolbar-background);
+}
+.theme-light rect {
+  fill: var(--theme-tab-toolbar-background);
+}
+.theme-dark rect {
+  fill: var(--theme-toolbar-background);
+}
+
+text {
+  font-weight: 300;
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serf;
+  font-size: 14px;
+}
+
+text {
+  fill: var(--theme-body-color-alt);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_takeCensus_12.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that when we take a census and get a bucket list of ids that matched the
+// given category, that the returned ids are all in the snapshot and their
+// reported category.
+
+function run_test() {
+  const g = newGlobal();
+  const dbg = new Debugger(g);
+
+  const path = saveNewHeapSnapshot({ debugger: dbg });
+  const snapshot = readHeapSnapshot(path);
+
+  const bucket = { by: "bucket" };
+  const count = { by: "count", count: true, bytes: false };
+  const objectClassCount = { by: "objectClass", then: count, other: count };
+
+  const byClassName = snapshot.takeCensus({
+    breakdown: {
+      by: "objectClass",
+      then: bucket,
+      other: bucket,
+    }
+  });
+
+  const byClassNameCount = snapshot.takeCensus({
+    breakdown: objectClassCount
+  });
+
+  const keys = new Set(Object.keys(byClassName));
+  equal(keys.size, Object.keys(byClassNameCount).length,
+        "Should have the same number of keys.");
+  for (let k of Object.keys(byClassNameCount)) {
+    ok(keys.has(k), "Should not have any unexpected class names");
+  }
+
+  for (let key of Object.keys(byClassName)) {
+    equal(byClassNameCount[key].count, byClassName[key].length,
+          "Length of the bucket and count should be equal");
+
+    for (let id of byClassName[key]) {
+      const desc = snapshot.describeNode(objectClassCount, id);
+      equal(desc[key].count, 1,
+            "Describing the bucketed node confirms that it belongs to the category");
+    }
+  }
+
+  do_test_finished();
+}
--- a/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
+++ b/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini
@@ -76,15 +76,16 @@ support-files =
 [test_HeapSnapshot_takeCensus_04.js]
 [test_HeapSnapshot_takeCensus_05.js]
 [test_HeapSnapshot_takeCensus_06.js]
 [test_HeapSnapshot_takeCensus_07.js]
 [test_HeapSnapshot_takeCensus_08.js]
 [test_HeapSnapshot_takeCensus_09.js]
 [test_HeapSnapshot_takeCensus_10.js]
 [test_HeapSnapshot_takeCensus_11.js]
+[test_HeapSnapshot_takeCensus_12.js]
 [test_ReadHeapSnapshot.js]
 [test_ReadHeapSnapshot_with_allocations.js]
 skip-if = os == 'linux' # Bug 1176173
 [test_ReadHeapSnapshot_worker.js]
 skip-if = os == 'linux' # Bug 1176173
 [test_SaveHeapSnapshot.js]
 [test_saveHeapSnapshot_e10s_01.js]
--- a/js/src/doc/Debugger/Debugger.Memory.md
+++ b/js/src/doc/Debugger/Debugger.Memory.md
@@ -319,16 +319,32 @@ Function Properties of the `Debugger.Mem
 
         where the `count` and `bytes` properties are present as directed by the
         <i>count</i> and <i>bytes</i> properties on the breakdown.
 
         Note that the census can produce byte sizes only for the most common
         types. When the census cannot find the byte size for a given type, it
         returns zero.
 
+    <code>{ by: "bucket" }</code>
+    :   Do not do any filtering or categorizing. Instead, accumulate a bucket of
+        each node's ID for every node that matches. The resulting report is an
+        array of the IDs.
+
+        For example, to find the ID of all nodes whose internal object
+        `[[class]]` property is named "RegExp", you could use the following code:
+
+            const report = dbg.memory.takeCensus({
+              breakdown: {
+                by: "objectClass",
+                then: { by: "bucket" }
+              }
+            });
+            doStuffWithRegExpIDs(report.RegExp);
+
     <code>{ by: "allocationStack", then:<i>breakdown</i>, noStack:<i>noStackBreakdown</i> }</code>
     :   Group items by the full JavaScript stack trace at which they were
         allocated.
 
         Further categorize all the items allocated at each distinct stack using
         <i>breakdown</i>.
 
         In the result of the census, this breakdown produces a JavaScript `Map`
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Memory-takeCensus-12.js
@@ -0,0 +1,61 @@
+// Sanity test that we can accumulate matching individuals in a bucket.
+
+var g = newGlobal();
+var dbg = new Debugger(g);
+
+var bucket = { by: "bucket" };
+var count = { by: "count", count: true, bytes: false };
+
+var all = dbg.memory.takeCensus({ breakdown: bucket });
+var allCount = dbg.memory.takeCensus({ breakdown: count }).count;
+
+var coarse = dbg.memory.takeCensus({
+  breakdown: {
+    by: "coarseType",
+    objects: bucket,
+    strings: bucket,
+    scripts: bucket,
+    other: bucket
+  }
+});
+var coarseCount = dbg.memory.takeCensus({
+  breakdown: {
+    by: "coarseType",
+    objects: count,
+    strings: count,
+    scripts: count,
+    other: count
+  }
+});
+
+assertEq(all.length > 0, true);
+assertEq(all.length, allCount);
+
+assertEq(coarse.objects.length > 0, true);
+assertEq(coarseCount.objects.count, coarse.objects.length);
+
+assertEq(coarse.strings.length > 0, true);
+assertEq(coarseCount.strings.count, coarse.strings.length);
+
+assertEq(coarse.scripts.length > 0, true);
+assertEq(coarseCount.scripts.count, coarse.scripts.length);
+
+assertEq(coarse.other.length > 0, true);
+assertEq(coarseCount.other.count, coarse.other.length);
+
+assertEq(all.length >= coarse.objects.length, true);
+assertEq(all.length >= coarse.strings.length, true);
+assertEq(all.length >= coarse.scripts.length, true);
+assertEq(all.length >= coarse.other.length, true);
+
+function assertIsIdentifier(id) {
+  assertEq(id, Math.floor(id));
+  assertEq(id > 0, true);
+  assertEq(id <= Math.pow(2, 48), true);
+}
+
+all.forEach(assertIsIdentifier);
+coarse.objects.forEach(assertIsIdentifier);
+coarse.strings.forEach(assertIsIdentifier);
+coarse.scripts.forEach(assertIsIdentifier);
+coarse.other.forEach(assertIsIdentifier);
--- a/js/src/vm/UbiNodeCensus.cpp
+++ b/js/src/vm/UbiNodeCensus.cpp
@@ -117,16 +117,70 @@ SimpleCount::report(JSContext* cx, Count
             return false;
     }
 
     report.setObject(*obj);
     return true;
 }
 
 
+// A count type that collects all matching nodes in a bucket.
+class BucketCount : public CountType {
+
+    struct Count : CountBase {
+        mozilla::Vector<JS::ubi::Node::Id> ids_;
+
+        explicit Count(BucketCount& count)
+          : CountBase(count),
+            ids_()
+        { }
+    };
+
+  public:
+    explicit BucketCount()
+      : CountType()
+    { }
+
+    void destructCount(CountBase& countBase) override {
+        Count& count = static_cast<Count&>(countBase);
+        count.~Count();
+    }
+
+    CountBasePtr makeCount() override { return CountBasePtr(js_new<Count>(*this)); }
+    void traceCount(CountBase& countBase, JSTracer* trc) final { }
+    bool count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node) override;
+    bool report(JSContext* cx, CountBase& countBase, MutableHandleValue report) override;
+};
+
+bool
+BucketCount::count(CountBase& countBase, mozilla::MallocSizeOf mallocSizeOf, const Node& node)
+{
+    Count& count = static_cast<Count&>(countBase);
+    return count.ids_.append(node.identifier());
+}
+
+bool
+BucketCount::report(JSContext* cx, CountBase& countBase, MutableHandleValue report)
+{
+    Count& count = static_cast<Count&>(countBase);
+
+    size_t length = count.ids_.length();
+    RootedArrayObject arr(cx, NewDenseFullyAllocatedArray(cx, length));
+    if (!arr)
+        return false;
+    arr->ensureDenseInitializedLength(cx, 0, length);
+
+    for (size_t i = 0; i < length; i++)
+        arr->setDenseElement(i, NumberValue(count.ids_[i]));
+
+    report.setObject(*arr);
+    return true;
+}
+
+
 // A type that categorizes nodes by their JavaScript type -- 'objects',
 // 'strings', 'scripts', and 'other' -- and then passes the nodes to child
 // types.
 //
 // Implementation details of scripts like jitted code are counted under
 // 'scripts'.
 class ByCoarseType : public CountType {
     CountTypePtr objects;
@@ -972,16 +1026,19 @@ ParseBreakdown(JSContext* cx, HandleValu
         }
 
         CountTypePtr simple(js_new<SimpleCount>(labelUnique,
                                                 ToBoolean(countValue),
                                                 ToBoolean(bytesValue)));
         return simple;
     }
 
+    if (StringEqualsAscii(by, "bucket"))
+        return CountTypePtr(js_new<BucketCount>());
+
     if (StringEqualsAscii(by, "objectClass")) {
         CountTypePtr thenType(ParseChildBreakdown(cx, breakdown, cx->names().then));
         if (!thenType)
             return nullptr;
 
         CountTypePtr otherType(ParseChildBreakdown(cx, breakdown, cx->names().other));
         if (!otherType)
             return nullptr;
--- a/mobile/android/base/java/org/mozilla/gecko/PrefsHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/PrefsHelper.java
@@ -21,23 +21,25 @@ import java.util.List;
  * Helper class to get/set gecko prefs.
  */
 public final class PrefsHelper {
     private static final String LOGTAG = "GeckoPrefsHelper";
 
     // Map pref name to ArrayList for multiple observers or PrefHandler for single observer.
     private static final SimpleArrayMap<String, Object> OBSERVERS = new SimpleArrayMap<>();
     private static final HashSet<String> INT_TO_STRING_PREFS = new HashSet<>(8);
+    private static final HashSet<String> INT_TO_BOOL_PREFS = new HashSet<>(2);
 
     static {
         INT_TO_STRING_PREFS.add("browser.chrome.titlebarMode");
         INT_TO_STRING_PREFS.add("network.cookie.cookieBehavior");
         INT_TO_STRING_PREFS.add("font.size.inflation.minTwips");
         INT_TO_STRING_PREFS.add("home.sync.updateMode");
         INT_TO_STRING_PREFS.add("browser.image_blocking");
+        INT_TO_BOOL_PREFS.add("browser.display.use_document_fonts");
     }
 
     @WrapForJNI
     private static final int PREF_INVALID = -1;
     @WrapForJNI
     private static final int PREF_FINISH = 0;
     @WrapForJNI
     private static final int PREF_BOOL = 1;
@@ -84,16 +86,19 @@ public final class PrefsHelper {
         String strVal = null;
 
         if (INT_TO_STRING_PREFS.contains(pref)) {
             // When sending to Java, we normalized special preferences that use integers
             // and strings to represent booleans. Here, we convert them back to their
             // actual types so we can store them.
             type = PREF_INT;
             intVal = Integer.parseInt(String.valueOf(value));
+        } else if (INT_TO_BOOL_PREFS.contains(pref)) {
+            type = PREF_INT;
+            intVal = (Boolean) value ? 1 : 0;
         } else if (value instanceof Boolean) {
             type = PREF_BOOL;
             boolVal = (Boolean) value;
         } else if (value instanceof Integer) {
             type = PREF_INT;
             intVal = (Integer) value;
         } else {
             type = PREF_STRING;
@@ -209,16 +214,19 @@ public final class PrefsHelper {
 
         // Some Gecko preferences use integers or strings to reference state instead of
         // directly representing the value.  Since the Java UI uses the type to determine
         // which ui elements to show and how to handle them, we need to normalize these
         // preferences to the correct type.
         if (INT_TO_STRING_PREFS.contains(pref)) {
             type = PREF_STRING;
             strVal = String.valueOf(intVal);
+        } else if (INT_TO_BOOL_PREFS.contains(pref)) {
+            type = PREF_BOOL;
+            boolVal = intVal == 1;
         }
 
         switch (type) {
             case PREF_FINISH:
                 handler.finish();
                 return;
             case PREF_BOOL:
                 handler.prefValue(pref, boolVal);
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
@@ -3,18 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.firstrun;
 
 import android.content.Context;
 import android.os.Bundle;
 import android.util.Log;
-import com.keepsafe.switchboard.SwitchBoard;
-import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.util.Experiments;
 
 import java.util.LinkedList;
 import java.util.List;
 
@@ -23,56 +22,43 @@ public class FirstrunPagerConfig {
 
     public static final String KEY_IMAGE = "imageRes";
     public static final String KEY_TEXT = "textRes";
     public static final String KEY_SUBTEXT = "subtextRes";
 
    public static List<FirstrunPanelConfig> getDefault(Context context) {
         final List<FirstrunPanelConfig> panels = new LinkedList<>();
 
-        if (isInExperimentLocal(context, Experiments.ONBOARDING2_A)) {
+        if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING2_A)) {
             panels.add(new FirstrunPanelConfig(WelcomePanel.class.getName(), WelcomePanel.TITLE_RES));
             Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING2_A);
-        } else if (isInExperimentLocal(context, Experiments.ONBOARDING2_B)) {
+            GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING2_A).apply();
+        } else if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING2_B)) {
             panels.add(SimplePanelConfigs.urlbarPanelConfig);
             panels.add(SimplePanelConfigs.bookmarksPanelConfig);
             panels.add(SimplePanelConfigs.syncPanelConfig);
             panels.add(new FirstrunPanelConfig(SyncPanel.class.getName(), SyncPanel.TITLE_RES));
             Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING2_B);
-        } else if (isInExperimentLocal(context, Experiments.ONBOARDING2_C)) {
+            GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING2_B).apply();
+        } else if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING2_C)) {
             panels.add(SimplePanelConfigs.urlbarPanelConfig);
             panels.add(SimplePanelConfigs.bookmarksPanelConfig);
             panels.add(SimplePanelConfigs.dataPanelConfig);
             panels.add(SimplePanelConfigs.syncPanelConfig);
             panels.add(new FirstrunPanelConfig(SyncPanel.class.getName(), SyncPanel.TITLE_RES));
             Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING2_C);
+            GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING2_C).apply();
         } else {
             Log.d(LOGTAG, "Not in an experiment!");
             panels.add(new FirstrunPanelConfig(WelcomePanel.class.getName(), WelcomePanel.TITLE_RES));
         }
 
         return panels;
     }
 
-    /*
-     * Wrapper method for using local bucketing rather than server-side.
-     * This needs to match the server-side bucketing used on mozilla-switchboard.herokuapp.com.
-     */
-    private static boolean isInExperimentLocal(Context context, String name) {
-        if (AppConstants.MOZ_SWITCHBOARD) {
-            if (SwitchBoard.isInBucket(context, 0, 33)) {
-                return Experiments.ONBOARDING2_A.equals(name);
-            } else if (SwitchBoard.isInBucket(context, 33, 66)) {
-                return Experiments.ONBOARDING2_B.equals(name);
-            } else if (SwitchBoard.isInBucket(context, 66, 100)) {
-                return Experiments.ONBOARDING2_C.equals(name);
-            }
-        }
-        return false;
-    }
 
     public static List<FirstrunPanelConfig> getRestricted() {
         final List<FirstrunPanelConfig> panels = new LinkedList<>();
         panels.add(new FirstrunPanelConfig(RestrictedWelcomePanel.class.getName(), RestrictedWelcomePanel.TITLE_RES));
         return panels;
     }
 
     public static class FirstrunPanelConfig {
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java
@@ -3,22 +3,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/. */
 
 package org.mozilla.gecko.telemetry;
 
 import android.content.Context;
 import android.os.Build;
 
-import com.keepsafe.switchboard.SwitchBoard;
-
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.telemetry.TelemetryConstants.CorePing;
+import org.mozilla.gecko.util.Experiments;
 import org.mozilla.gecko.util.StringUtils;
 
 import java.io.IOException;
 import java.util.Locale;
 
 /**
  * A class with static methods to generate the various Java-created Telemetry pings to upload to the telemetry server.
  */
@@ -82,19 +81,17 @@ public class TelemetryPingGenerator {
                 StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19);
 
         ping.put(CorePing.ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH);
         ping.put(CorePing.CLIENT_ID, clientId);
         ping.put(CorePing.DEVICE, deviceDescriptor);
         ping.put(CorePing.LOCALE, Locales.getLanguageTag(Locale.getDefault()));
         ping.put(CorePing.OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
         ping.put(CorePing.SEQ, seq);
-        if (AppConstants.MOZ_SWITCHBOARD) {
-            ping.putArray(CorePing.EXPERIMENTS, SwitchBoard.getActiveExperiments(context));
-        }
+        ping.putArray(CorePing.EXPERIMENTS, Experiments.getActiveExperiments(context));
         // TODO (bug 1246816): Remove this "optional" parameter work-around when
         // GeckoProfile.getAndPersistProfileCreationDateFromFilesystem is implemented. That method returns -1
         // while it's not implemented so we don't include the parameter in the ping if that's the case.
         if (profileCreationDate >= 0) {
             ping.put(CorePing.PROFILE_CREATION_DATE, profileCreationDate);
         }
         return ping;
     }
--- a/mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
@@ -1,32 +1,42 @@
 /* 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/. */
 
 package org.mozilla.gecko.util;
 
+import android.content.Context;
+
 import android.util.Log;
 import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
+import android.text.TextUtils;
+import com.keepsafe.switchboard.SwitchBoard;
+import org.mozilla.gecko.GeckoSharedPrefs;
+
+import java.util.LinkedList;
+import java.util.List;
 
 /**
  * This class should reflect the experiment names found in the Switchboard experiments config here:
  * https://github.com/mozilla-services/switchboard-experiments
  */
 public class Experiments {
     private static final String LOGTAG = "GeckoExperiments";
 
     // Display History and Bookmarks in 3-dot menu.
     public static final String BOOKMARKS_HISTORY_MENU = "bookmark-history-menu";
 
     // Onboarding: "Features and Story"
     public static final String ONBOARDING2_A = "onboarding2-a"; // Control: Single (blue) welcome screen
     public static final String ONBOARDING2_B = "onboarding2-b"; // 4 static Feature slides
     public static final String ONBOARDING2_C = "onboarding2-c"; // 4 static + 1 clickable (Data saving) Feature slides
 
+    public static final String PREF_ONBOARDING_VERSION = "onboarding_version";
+
     // Show search mode (instead of home panels) when tapping on urlbar if there is a search term in the urlbar.
     public static final String SEARCH_TERM = "search-term";
 
     private static volatile Boolean disabled = null;
 
     /**
      * Determines whether Switchboard is disabled by the MOZ_DISABLE_SWITCHBOARD
      * environment variable. We need to read this value from the intent string
@@ -50,9 +60,43 @@ public class Experiments {
                     return disabled;
                 }
             }
             env = intent.getStringExtra("env" + i);
         }
         disabled = false;
         return disabled;
     }
+
+    /**
+     * Returns if a user is in certain local experiment.
+     * @param experiment Name of experiment to look up
+     * @return returns value for experiment or false if experiment does not exist.
+     */
+    public static boolean isInExperimentLocal(Context context, String experiment) {
+        if (SwitchBoard.isInBucket(context, 0, 33)) {
+            return Experiments.ONBOARDING2_A.equals(experiment);
+        } else if (SwitchBoard.isInBucket(context, 33, 66)) {
+            return Experiments.ONBOARDING2_B.equals(experiment);
+        } else if (SwitchBoard.isInBucket(context, 66, 100)) {
+            return Experiments.ONBOARDING2_C.equals(experiment);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns list of all active experiments, remote and local.
+     * @return List of experiment names Strings
+     */
+    public static List<String> getActiveExperiments(Context c) {
+        final List<String> experiments = new LinkedList<>();
+        experiments.addAll(SwitchBoard.getActiveExperiments(c));
+
+        // Add onboarding version.
+        final String onboardingExperiment = GeckoSharedPrefs.forProfile(c).getString(Experiments.PREF_ONBOARDING_VERSION, null);
+        if (!TextUtils.isEmpty(onboardingExperiment)) {
+            experiments.add(onboardingExperiment);
+        }
+
+        return experiments;
+    }
 }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -225,16 +225,18 @@
 <!ENTITY pref_cookies_not_accept_foreign "Enabled, excluding 3rd party">
 <!ENTITY pref_cookies_disabled "Disabled">
 
 <!ENTITY pref_tap_to_load_images_title2 "Show images">
 <!ENTITY pref_tap_to_load_images_enabled "Always">
 <!ENTITY pref_tap_to_load_images_data "Only over Wi-Fi">
 <!ENTITY pref_tap_to_load_images_disabled2 "Blocked">
 
+<!ENTITY pref_show_web_fonts "Show web fonts">
+
 <!ENTITY pref_tracking_protection_title "Tracking protection">
 <!ENTITY pref_tracking_protection_summary3 "Enabled in Private Browsing">
 <!ENTITY pref_donottrack_title "Do not track">
 <!ENTITY pref_donottrack_summary "&brandShortName; will tell sites that you do not want to be tracked">
 
 <!ENTITY pref_tracking_protection_enabled "Enabled">
 <!ENTITY pref_tracking_protection_enabled_pb "Enabled in Private Browsing">
 <!ENTITY pref_tracking_protection_disabled "Disabled">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -94,17 +94,16 @@ gujar.sources += ['java/org/mozilla/geck
     'util/ActivityResultHandler.java',
     'util/ActivityResultHandlerMap.java',
     'util/ActivityUtils.java',
     'util/BundleEventListener.java',
     'util/Clipboard.java',
     'util/ColorUtils.java',
     'util/DrawableUtil.java',
     'util/EventCallback.java',
-    'util/Experiments.java',
     'util/FileUtils.java',
     'util/FloatUtils.java',
     'util/GamepadUtils.java',
     'util/GeckoBackgroundThread.java',
     'util/GeckoEventListener.java',
     'util/GeckoJarReader.java',
     'util/GeckoRequest.java',
     'util/HardwareCodecCapabilityUtils.java',
@@ -573,16 +572,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'toolbar/ToolbarEditLayout.java',
     'toolbar/ToolbarEditText.java',
     'toolbar/ToolbarPrefs.java',
     'toolbar/ToolbarProgressView.java',
     'TouchEventInterceptor.java',
     'trackingprotection/TrackingProtectionPrompt.java',
     'updater/UpdateService.java',
     'updater/UpdateServiceHelper.java',
+    'util/Experiments.java',
     'Webapp.java',
     'webapp/Allocator.java',
     'webapp/ApkResources.java',
     'webapp/Dispatcher.java',
     'webapp/EventListener.java',
     'webapp/InstallHelper.java',
     'webapp/InstallListener.java',
     'webapp/TaskKiller.java',
--- a/mobile/android/base/resources/xml/preferences_advanced.xml
+++ b/mobile/android/base/resources/xml/preferences_advanced.xml
@@ -33,16 +33,19 @@
                     android:persistent="true" />
 
     <ListPreference android:key="browser.image_blocking"
                     android:title="@string/pref_tap_to_load_images_title2"
                     android:entries="@array/pref_browser_image_blocking_entries"
                     android:entryValues="@array/pref_browser_image_blocking_values"
                     android:persistent="false" />
 
+    <CheckBoxPreference android:key="browser.display.use_document_fonts"
+                        android:title="@string/pref_show_web_fonts" />
+
     <ListPreference android:key="plugin.enable"
                     android:title="@string/pref_plugins"
                     android:entries="@array/pref_plugins_entries"
                     android:entryValues="@array/pref_plugins_values"
                     android:persistent="false" />
 
     <CheckBoxPreference android:key="media.autoplay.enabled"
                         android:title="@string/pref_media_autoplay_enabled"
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -209,16 +209,18 @@
   <string name="pref_cookies_not_accept_foreign">&pref_cookies_not_accept_foreign;</string>
   <string name="pref_cookies_disabled">&pref_cookies_disabled;</string>
 
   <string name="pref_tap_to_load_images_title2">&pref_tap_to_load_images_title2;</string>
   <string name="pref_tap_to_load_images_enabled">&pref_tap_to_load_images_enabled;</string>
   <string name="pref_tap_to_load_images_data">&pref_tap_to_load_images_data;</string>
   <string name="pref_tap_to_load_images_disabled2">&pref_tap_to_load_images_disabled2;</string>
 
+  <string name="pref_show_web_fonts">&pref_show_web_fonts;</string>
+
   <string name="pref_tracking_protection_title">&pref_tracking_protection_title;</string>
   <string name="pref_tracking_protection_summary">&pref_tracking_protection_summary3;</string>
   <string name="pref_donottrack_title">&pref_donottrack_title;</string>
   <string name="pref_donottrack_summary">&pref_donottrack_summary;</string>
 
   <string name="pref_tracking_protection_enabled">&pref_tracking_protection_enabled;</string>
   <string name="pref_tracking_protection_enabled_pb">&pref_tracking_protection_enabled_pb;</string>
   <string name="pref_tracking_protection_disabled">&pref_tracking_protection_disabled;</string>
--- a/toolkit/components/alerts/nsIAlertsService.idl
+++ b/toolkit/components/alerts/nsIAlertsService.idl
@@ -25,19 +25,19 @@ interface nsIAlertNotification : nsISupp
             [optional] in AString cookie,
             [optional] in AString dir,
             [optional] in AString lang,
             [optional] in AString data,
             [optional] in nsIPrincipal principal,
             [optional] in boolean inPrivateBrowsing);
 
   /**
-   * The name of the notification. This is currently only used on Android and
-   * OS X. On Android, the name is hashed and used as a notification ID.
-   * Notifications will replace previous notifications with the same name.
+   * The name of the notification. On Android, the name is hashed and used as
+   * a notification ID. Notifications will replace previous notifications with
+   * the same name.
    */
   readonly attribute AString name;
 
   /**
    * A URL identifying the image to put in the alert. The OS X backend limits
    * the amount of time it will wait for the image to load to six seconds. After
    * that time, the alert will show without an image.
    */
@@ -113,42 +113,21 @@ interface nsIAlertNotification : nsISupp
 };
 
 [scriptable, uuid(f7a36392-d98b-4141-a7d7-4e46642684e3)]
 interface nsIAlertsService : nsISupports
 {
   void showAlert(in nsIAlertNotification alert,
                  [optional] in nsIObserver alertListener);
   /**
-   * Displays a sliding notification window.
+   * Initializes and shows an |nsIAlertNotification| with the given parameters.
    *
-   * @param imageUrl       A URL identifying the image to put in the alert.
-   *                       The OS X implemenation limits the amount of time it
-   *                       will wait for an icon to load to six seconds. After
-   *                       that time the alert will show with no icon.
-   * @param title          The title for the alert.
-   * @param text           The contents of the alert.
-   * @param textClickable  If true, causes the alert text to look like a link
-   *                       and notifies the listener when user attempts to
-   *                       click the alert text.
-   * @param cookie         A blind cookie the alert will pass back to the
-   *                       consumer during the alert listener callbacks.
    * @param alertListener  Used for callbacks. May be null if the caller
    *                       doesn't care about callbacks.
-   * @param name           The name of the notification. This is currently only
-   *                       used on Android and OS X. On Android the name is
-   *                       hashed and used as a notification ID. Notifications
-   *                       will replace previous notifications with the same name.
-   * @param dir            Bidi override for the title. Valid values are
-   *                       "auto", "ltr" or "rtl". Only available on supported
-   *                       platforms.
-   * @param lang           Language of title and text of the alert. Only available
-   *                       on supported platforms.
-   * @param inPrivateBrowsing If set to true, imageUrl will be loaded in private
-   *                          browsing mode.
+   * @see nsIAlertNotification for descriptions of all other parameters.
    * @throws NS_ERROR_NOT_AVAILABLE If the notification cannot be displayed.
    *
    * The following arguments will be passed to the alertListener's observe()
    * method:
    *   subject - null
    *   topic   - "alertfinished" when the alert goes away
    *             "alertdisablecallback" when alerts should be disabled for the principal
    *             "alertsettingscallback" when alert settings should be opened
--- a/toolkit/components/extensions/ext-alarms.js
+++ b/toolkit/components/extensions/ext-alarms.js
@@ -100,17 +100,17 @@ extensions.registerPrivilegedAPI("alarms
         } else {
           [name, alarmInfo] = args;
         }
 
         let alarm = new Alarm(extension, name, alarmInfo);
         alarmsMap.get(extension).add(alarm);
       },
 
-      get: function(args) {
+      get: function(...args) {
         let name = "", callback;
         if (args.length == 1) {
           callback = args[0];
         } else {
           [name, callback] = args;
         }
 
         let promise = new Promise((resolve, reject) => {
@@ -122,17 +122,17 @@ extensions.registerPrivilegedAPI("alarms
           reject("No matching alarm");
         });
 
         return context.wrapPromise(promise, callback);
       },
 
       getAll: function(callback) {
         let alarms = alarmsMap.get(extension);
-        let result = alarms.map(alarm => alarm.data);
+        let result = Array.from(alarms, alarm => alarm.data);
         return context.wrapPromise(Promise.resolve(result), callback);
       },
 
       clear: function(...args) {
         let name = "", callback;
         if (args.length == 1) {
           callback = args[0];
         } else {
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -272,17 +272,17 @@ extensions.registerSchemaAPI("cookies", 
           path = uri.directory;
         }
 
         let name = details.name !== null ? details.name : "";
         let value = details.value !== null ? details.value : "";
         let secure = details.secure !== null ? details.secure : false;
         let httpOnly = details.httpOnly !== null ? details.httpOnly : false;
         let isSession = details.expirationDate === null;
-        let expiry = isSession ? 0 : details.expirationDate;
+        let expiry = isSession ? Number.MAX_SAFE_INTEGER : details.expirationDate;
         // Ignore storeID.
 
         let cookieAttrs = {host: details.domain, path: path, isSecure: secure};
         if (!checkSetCookiePermissions(extension, uri, cookieAttrs)) {
           return Promise.reject({message: `Permission denied to set cookie ${JSON.stringify(details)}`});
         }
 
         // The permission check may have modified the domain, so use
--- a/toolkit/components/extensions/test/mochitest/test_ext_alarms.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_alarms.html
@@ -17,24 +17,23 @@ add_task(function* test_alarm_without_pe
   function backgroundScript() {
     browser.test.log("running alarm script");
 
     browser.test.assertTrue(!browser.alarms,
                             "alarm API should not be available if the alarm permission is not required");
     browser.test.notifyPass("alarms_permission");
   }
 
-  let extensionData = {
-    background: "(" + backgroundScript.toString() + ")()",
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
     manifest: {
       permissions: [],
     },
-  };
+  });
 
-  let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
   info("extension loaded");
   yield extension.awaitFinish("alarms_permission");
   yield extension.unload();
   info("extension unloaded");
 });
 
 
@@ -48,24 +47,23 @@ add_task(function* test_alarm_fires() {
       browser.test.notifyPass("alarms");
     });
     chrome.alarms.create(ALARM_NAME, {delayInMinutes: 0.02});
     setTimeout(() => {
       browser.test.notifyFail("alarms test failed, took too long");
     }, 10000);
   }
 
-  let extensionData = {
-    background: "(" + backgroundScript.toString() + ")()",
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
     manifest: {
       permissions: ["alarms"],
     },
-  };
+  });
 
-  let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
   info("extension loaded");
   yield extension.awaitFinish("alarms");
   yield extension.unload();
   info("extension unloaded");
 });
 
 
@@ -88,27 +86,107 @@ add_task(function* test_periodic_alarm_f
     setTimeout(() => {
       browser.test.notifyFail("alarms test failed, took too long");
       chrome.alarms.clear(ALARM_NAME, (wasCleared) => {
         browser.test.assertTrue(wasCleared, "alarm should be cleared");
       });
     }, 30000);
   }
 
-  let extensionData = {
-    background: "(" + backgroundScript.toString() + ")()",
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
     manifest: {
       permissions: ["alarms"],
     },
-  };
+  });
 
-  let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
   info("extension loaded");
   yield extension.awaitFinish("alarms");
   yield extension.unload();
   info("extension unloaded");
 });
 
+
+add_task(function* test_get_get_all_clear_all_alarms() {
+  function backgroundScript() {
+    const ALARM_NAME = "test_alarm";
+
+    let suffixes = [0, 1, 2];
+
+    // Wrap API methods in promise-based variants.
+    let promiseAlarms = {};
+    Object.keys(browser.alarms).forEach(method => {
+      promiseAlarms[method] = (...args) => {
+        return new Promise(resolve => {
+          browser.alarms[method](...args, resolve);
+        });
+      };
+    });
+
+    suffixes.forEach(suffix => {
+      browser.alarms.create(ALARM_NAME + suffix, {when: Date.now() + (suffix + 1) * 10000});
+    });
+
+    promiseAlarms.getAll().then(alarms => {
+      browser.test.assertEq(suffixes.length, alarms.length);
+      alarms.forEach((alarm, index) => {
+        browser.test.assertEq(ALARM_NAME + index, alarm.name, "expected alarm returned");
+      });
+
+      return Promise.all(
+        suffixes.map(suffix => {
+          return promiseAlarms.get(ALARM_NAME + suffix).then(alarm => {
+            browser.test.assertEq(ALARM_NAME + suffix, alarm.name, "expected alarm returned");
+            browser.test.sendMessage(`get-${suffix}`);
+          });
+        }));
+    }).then(() => {
+      return promiseAlarms.clear(ALARM_NAME + suffixes[0]);
+    }).then(wasCleared => {
+      browser.test.assertTrue(wasCleared, "alarm was cleared");
+
+      return promiseAlarms.getAll();
+    }).then(alarms => {
+      browser.test.assertEq(2, alarms.length, "alarm was removed");
+
+      return promiseAlarms.get(ALARM_NAME + suffixes[0]);
+    }).then(alarm => {
+      browser.test.assertEq(undefined, alarm, "non-existent alarm should be undefined");
+      browser.test.sendMessage(`get-invalid`);
+
+      return promiseAlarms.clearAll();
+    }).then(wasCleared => {
+      browser.test.assertTrue(wasCleared, "alarms were cleared");
+
+      return promiseAlarms.getAll();
+    }).then(alarms => {
+      browser.test.assertEq(0, alarms.length, "no alarms exist");
+      browser.test.sendMessage("clearAll");
+      browser.test.sendMessage("clear");
+      browser.test.sendMessage("getAll");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: ["alarms"],
+    },
+  });
+
+  yield Promise.all([
+    extension.startup(),
+    extension.awaitMessage("getAll"),
+    extension.awaitMessage("get-0"),
+    extension.awaitMessage("get-1"),
+    extension.awaitMessage("get-2"),
+    extension.awaitMessage("clear"),
+    extension.awaitMessage("get-invalid"),
+    extension.awaitMessage("clearAll"),
+  ]);
+  yield extension.unload();
+});
+
 </script>
 
 </body>
 </html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_cookies.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html
@@ -8,81 +8,92 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript() {
-  function assertExpected(cookie, expected) {
-    for (let key of Object.keys(cookie)) {
-      browser.test.assertTrue(key in expected, "found property " + key);
-      browser.test.assertEq(cookie[key], expected[key], "property value for " + key + " is wrong");
+add_task(function* test_cookies() {
+  function backgroundScript() {
+    function assertExpected(expected, cookie) {
+      for (let key of Object.keys(cookie)) {
+        browser.test.assertTrue(key in expected, `found property ${key}`);
+        browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`);
+      }
+      browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found");
     }
-    browser.test.assertEq(Object.keys(cookie).length, Object.keys(expected).length, "all expected properties found");
+
+    let TEST_URL = "http://example.org/";
+    let THE_FUTURE = Date.now() + 5 * 60;
+
+    let expected = {
+      name: "name1",
+      value: "value1",
+      domain: "example.org",
+      hostOnly: true,
+      path: "/",
+      secure: false,
+      httpOnly: false,
+      session: false,
+      expirationDate: THE_FUTURE,
+      storeId: "firefox-default",
+    };
+
+    browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", expirationDate: THE_FUTURE}).then(cookie => {
+      assertExpected(expected, cookie);
+      return browser.cookies.get({url: TEST_URL, name: "name1"});
+    }).then(cookie => {
+      assertExpected(expected, cookie);
+      return browser.cookies.getAll({domain: "example.org"});
+    }).then(cookies => {
+      browser.test.assertEq(cookies.length, 1, "only found one cookie for example.org");
+      assertExpected(expected, cookies[0]);
+      return browser.cookies.remove({url: TEST_URL, name: "name1"});
+    }).then(details => {
+      assertExpected({url: TEST_URL, name: "name1", storeId: "firefox-default"}, details);
+      return browser.cookies.get({url: TEST_URL, name: "name1"});
+    }).then(cookie => {
+      browser.test.assertEq(null, cookie, "removed cookie not found");
+      return browser.cookies.getAllCookieStores();
+    }).then(stores => {
+      browser.test.assertEq(1, stores.length, "expected number of stores returned");
+      browser.test.assertEq("firefox-default", stores[0].id, "expected store id returned");
+      browser.test.assertEq(0, stores[0].tabIds.length, "no tabs returned for store"); // Todo: Implement this.
+      return browser.cookies.set({url: TEST_URL, name: "name2", domain: ".example.org", expirationDate: THE_FUTURE});
+    }).then(cookie => {
+      browser.test.assertEq(false, cookie.hostOnly, "cookie is not a hostOnly cookie");
+      return browser.cookies.remove({url: TEST_URL, name: "name2"});
+    }).then(details => {
+      assertExpected({url: TEST_URL, name: "name2", storeId: "firefox-default"}, details);
+      // Create a session cookie.
+      return browser.cookies.set({url: TEST_URL, name: "name1", value: "value1"});
+    }).then(cookie => {
+      browser.test.assertEq(true, cookie.session, "session cookie set");
+      return browser.cookies.get({url: TEST_URL, name: "name1"});
+    }).then(cookie => {
+      browser.test.assertEq(true, cookie.session, "got session cookie");
+      return browser.cookies.remove({url: TEST_URL, name: "name1"});
+    }).then(details => {
+      assertExpected({url: TEST_URL, name: "name1", storeId: "firefox-default"}, details);
+      return browser.cookies.get({url: TEST_URL, name: "name1"});
+    }).then(cookie => {
+      browser.test.assertEq(null, cookie, "removed cookie not found");
+      browser.test.notifyPass("cookies");
+    });
   }
 
-  let TEST_URL = "http://example.org/";
-  let THE_FUTURE = Date.now() + 5 * 60;
-
-  let expected = {
-    name: "name1",
-    value: "value1",
-    domain: "example.org",
-    hostOnly: true,
-    path: "/",
-    secure: false,
-    httpOnly: false,
-    session: false,
-    expirationDate: THE_FUTURE,
-    storeId: "firefox-default",
-  };
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: ["cookies", "*://example.org/"],
+    },
+  });
 
-  browser.cookies.set({url: TEST_URL, name: "name1", value: "value1", expirationDate: THE_FUTURE}).then(cookie => {
-    assertExpected(cookie, expected);
-    return browser.cookies.get({url: TEST_URL, name: "name1"});
-  }).then(cookie => {
-    assertExpected(cookie, expected);
-    return browser.cookies.getAll({domain: "example.org"});
-  }).then(cookies => {
-    browser.test.assertEq(cookies.length, 1, "only found one cookie for example.org");
-    assertExpected(cookies[0], expected);
-    return browser.cookies.remove({url: TEST_URL, name: "name1"});
-  }).then(details => {
-    assertExpected(details, {url: TEST_URL, name: "name1", storeId: "firefox-default"});
-    return browser.cookies.get({url: TEST_URL, name: "name1"});
-  }).then(cookie => {
-    browser.test.assertEq(cookie, null);
-    return browser.cookies.getAllCookieStores();
-  }).then(stores => {
-    browser.test.assertEq(stores.length, 1);
-    browser.test.assertEq(stores[0].id, "firefox-default");
-    browser.test.assertEq(stores[0].tabIds.length, 0); // Todo: Implement this.
-    return browser.cookies.set({url: TEST_URL, name: "name2", domain: ".example.org", expirationDate: THE_FUTURE});
-  }).then(cookie => {
-    browser.test.assertEq(cookie.hostOnly, false, "not a hostOnly cookie");
-    return browser.cookies.remove({url: TEST_URL, name: "name2"});
-  }).then(details => {
-    assertExpected(details, {url: TEST_URL, name: "name2", storeId: "firefox-default"});
-  }).then(() => {
-    browser.test.notifyPass("cookies");
-  });
-}
-
-let extensionData = {
-  background: "(" + backgroundScript.toString() + ")()",
-  manifest: {
-    permissions: ["cookies", "*://example.org/"],
-  },
-};
-
-add_task(function* test_cookies() {
-  let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
   info("extension loaded");
   yield extension.awaitFinish("cookies");
   yield extension.unload();
   info("extension unloaded");
 });
 
 </script>
--- a/toolkit/system/gnome/nsAlertsIconListener.cpp
+++ b/toolkit/system/gnome/nsAlertsIconListener.cpp
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsAlertsIconListener.h"
 #include "imgIContainer.h"
 #include "imgILoader.h"
 #include "imgIRequest.h"
 #include "nsNetUtil.h"
 #include "nsServiceManagerUtils.h"
+#include "nsSystemAlertsService.h"
 #include "nsIAlertsService.h"
 #include "nsIImageToPixbuf.h"
 #include "nsIStringBundle.h"
 #include "nsIObserverService.h"
 #include "nsIURI.h"
 #include "nsCRT.h"
 
 #include <dlfcn.h>
@@ -26,16 +27,17 @@ void* nsAlertsIconListener::libNotifyHan
 bool nsAlertsIconListener::libNotifyNotAvail = false;
 nsAlertsIconListener::notify_is_initted_t nsAlertsIconListener::notify_is_initted = nullptr;
 nsAlertsIconListener::notify_init_t nsAlertsIconListener::notify_init = nullptr;
 nsAlertsIconListener::notify_get_server_caps_t nsAlertsIconListener::notify_get_server_caps = nullptr;
 nsAlertsIconListener::notify_notification_new_t nsAlertsIconListener::notify_notification_new = nullptr;
 nsAlertsIconListener::notify_notification_show_t nsAlertsIconListener::notify_notification_show = nullptr;
 nsAlertsIconListener::notify_notification_set_icon_from_pixbuf_t nsAlertsIconListener::notify_notification_set_icon_from_pixbuf = nullptr;
 nsAlertsIconListener::notify_notification_add_action_t nsAlertsIconListener::notify_notification_add_action = nullptr;
+nsAlertsIconListener::notify_notification_close_t nsAlertsIconListener::notify_notification_close = nullptr;
 
 static void notify_action_cb(NotifyNotification *notification,
                              gchar *action, gpointer user_data)
 {
   nsAlertsIconListener* alert = static_cast<nsAlertsIconListener*> (user_data);
   alert->SendCallback();
 }
 
@@ -67,18 +69,21 @@ GetPixbufFromImgRequest(imgIRequest* aRe
     do_GetService("@mozilla.org/widget/image-to-gdk-pixbuf;1");
 
   return imgToPixbuf->ConvertImageToPixbuf(image);
 }
 
 NS_IMPL_ISUPPORTS(nsAlertsIconListener, imgINotificationObserver,
                   nsIObserver, nsISupportsWeakReference)
 
-nsAlertsIconListener::nsAlertsIconListener()
-: mLoadedFrame(false),
+nsAlertsIconListener::nsAlertsIconListener(nsSystemAlertsService* aBackend,
+                                           const nsAString& aAlertName)
+: mAlertName(aAlertName),
+  mBackend(aBackend),
+  mLoadedFrame(false),
   mNotification(nullptr)
 {
   if (!libNotifyHandle && !libNotifyNotAvail) {
     libNotifyHandle = dlopen("libnotify.so.4", RTLD_LAZY);
     if (!libNotifyHandle) {
       libNotifyHandle = dlopen("libnotify.so.1", RTLD_LAZY);
       if (!libNotifyHandle) {
         libNotifyNotAvail = true;
@@ -88,25 +93,27 @@ nsAlertsIconListener::nsAlertsIconListen
 
     notify_is_initted = (notify_is_initted_t)dlsym(libNotifyHandle, "notify_is_initted");
     notify_init = (notify_init_t)dlsym(libNotifyHandle, "notify_init");
     notify_get_server_caps = (notify_get_server_caps_t)dlsym(libNotifyHandle, "notify_get_server_caps");
     notify_notification_new = (notify_notification_new_t)dlsym(libNotifyHandle, "notify_notification_new");
     notify_notification_show = (notify_notification_show_t)dlsym(libNotifyHandle, "notify_notification_show");
     notify_notification_set_icon_from_pixbuf = (notify_notification_set_icon_from_pixbuf_t)dlsym(libNotifyHandle, "notify_notification_set_icon_from_pixbuf");
     notify_notification_add_action = (notify_notification_add_action_t)dlsym(libNotifyHandle, "notify_notification_add_action");
-    if (!notify_is_initted || !notify_init || !notify_get_server_caps || !notify_notification_new || !notify_notification_show || !notify_notification_set_icon_from_pixbuf || !notify_notification_add_action) {
+    notify_notification_close = (notify_notification_close_t)dlsym(libNotifyHandle, "notify_notification_close");
+    if (!notify_is_initted || !notify_init || !notify_get_server_caps || !notify_notification_new || !notify_notification_show || !notify_notification_set_icon_from_pixbuf || !notify_notification_add_action || !notify_notification_close) {
       dlclose(libNotifyHandle);
       libNotifyHandle = nullptr;
     }
   }
 }
 
 nsAlertsIconListener::~nsAlertsIconListener()
 {
+  mBackend->RemoveListener(mAlertName, this);
   if (mIconRequest)
     mIconRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
   // Don't dlclose libnotify as it uses atexit().
 }
 
 NS_IMETHODIMP
 nsAlertsIconListener::Notify(imgIRequest *aRequest, int32_t aType, const nsIntRect* aData)
 {
@@ -176,16 +183,19 @@ nsAlertsIconListener::OnFrameComplete(im
   mIconRequest = nullptr;
 
   return NS_OK;
 }
 
 nsresult
 nsAlertsIconListener::ShowAlert(GdkPixbuf* aPixbuf)
 {
+  if (!mBackend->IsActiveListener(mAlertName, this))
+    return NS_OK;
+
   mNotification = notify_notification_new(mAlertTitle.get(), mAlertText.get(),
                                           nullptr, nullptr);
 
   if (!mNotification)
     return NS_ERROR_OUT_OF_MEMORY;
 
   nsCOMPtr<nsIObserverService> obsServ =
       do_GetService("@mozilla.org/observer-service;1");
@@ -206,22 +216,27 @@ nsAlertsIconListener::ShowAlert(GdkPixbu
 
   // Fedora 10 calls NotifyNotification "closed" signal handlers with a
   // different signature, so a marshaller is used instead of a C callback to
   // get the user_data (this) in a parseable format.  |closure| is created
   // with a floating reference, which gets sunk by g_signal_connect_closure().
   GClosure* closure = g_closure_new_simple(sizeof(GClosure), this);
   g_closure_set_marshal(closure, notify_closed_marshal);
   mClosureHandler = g_signal_connect_closure(mNotification, "closed", closure, FALSE);
-  gboolean result = notify_notification_show(mNotification, nullptr);
+  GError* error = nullptr;
+  if (!notify_notification_show(mNotification, &error)) {
+    NS_WARNING(error->message);
+    g_error_free(error);
+    return NS_ERROR_FAILURE;
+  }
 
-  if (result && mAlertListener)
+  if (mAlertListener)
     mAlertListener->Observe(nullptr, "alertshow", mAlertCookie.get());
 
-  return result ? NS_OK : NS_ERROR_FAILURE;
+  return NS_OK;
 }
 
 nsresult
 nsAlertsIconListener::StartRequest(const nsAString & aImageUrl, bool aInPrivateBrowsing)
 {
   if (mIconRequest) {
     // Another icon request is already in flight.  Kill it.
     mIconRequest->Cancel(NS_BINDING_ABORTED);
@@ -259,18 +274,17 @@ nsAlertsIconListener::SendCallback()
 
 void
 nsAlertsIconListener::SendClosed()
 {
   if (mNotification) {
     g_object_unref(mNotification);
     mNotification = nullptr;
   }
-  if (mAlertListener)
-    mAlertListener->Observe(nullptr, "alertfinished", mAlertCookie.get());
+  NotifyFinished();
 }
 
 NS_IMETHODIMP
 nsAlertsIconListener::Observe(nsISupports *aSubject, const char *aTopic,
                               const char16_t *aData) {
   // We need to close any open notifications upon application exit, otherwise
   // we will leak since libnotify holds a ref for us.
   if (!nsCRT::strcmp(aTopic, "quit-application") && mNotification) {
@@ -278,16 +292,39 @@ nsAlertsIconListener::Observe(nsISupport
     g_object_unref(mNotification);
     mNotification = nullptr;
     Release(); // equivalent to NS_RELEASE(this)
   }
   return NS_OK;
 }
 
 nsresult
+nsAlertsIconListener::Close()
+{
+  if (mIconRequest) {
+    mIconRequest->Cancel(NS_BINDING_ABORTED);
+    mIconRequest = nullptr;
+  }
+
+  if (!mNotification) {
+    NotifyFinished();
+    return NS_OK;
+  }
+
+  GError* error = nullptr;
+  if (!notify_notification_close(mNotification, &error)) {
+    NS_WARNING(error->message);
+    g_error_free(error);
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+nsresult
 nsAlertsIconListener::InitAlertAsync(nsIAlertNotification* aAlert,
                                      nsIObserver* aAlertListener)
 {
   if (!libNotifyHandle)
     return NS_ERROR_FAILURE;
 
   if (!notify_is_initted()) {
     // Give the name of this application to libnotify
@@ -366,8 +403,14 @@ nsAlertsIconListener::InitAlertAsync(nsI
   rv = aAlert->GetImageURL(imageUrl);
   NS_ENSURE_SUCCESS(rv, rv);
   bool inPrivateBrowsing;
   rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return StartRequest(imageUrl, inPrivateBrowsing);
 }
+
+void nsAlertsIconListener::NotifyFinished()
+{
+  if (mAlertListener)
+    mAlertListener->Observe(nullptr, "alertfinished", mAlertCookie.get());
+}
--- a/toolkit/system/gnome/nsAlertsIconListener.h
+++ b/toolkit/system/gnome/nsAlertsIconListener.h
@@ -11,32 +11,35 @@
 #include "nsString.h"
 #include "nsIObserver.h"
 #include "nsWeakReference.h"
 
 #include <gdk-pixbuf/gdk-pixbuf.h>
 
 class imgIRequest;
 class nsIAlertNotification;
+class nsSystemAlertsService;
 
 struct NotifyNotification;
 
 class nsAlertsIconListener : public imgINotificationObserver,
                              public nsIObserver,
                              public nsSupportsWeakReference
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_IMGINOTIFICATIONOBSERVER
   NS_DECL_NSIOBSERVER
 
-  nsAlertsIconListener();
+  nsAlertsIconListener(nsSystemAlertsService* aBackend,
+                       const nsAString& aAlertName);
 
   nsresult InitAlertAsync(nsIAlertNotification* aAlert,
                           nsIObserver* aAlertListener);
+  nsresult Close();
 
   void SendCallback();
   void SendClosed();
 
 protected:
   virtual ~nsAlertsIconListener();
 
   nsresult OnLoadComplete(imgIRequest* aRequest);
@@ -48,39 +51,46 @@ protected:
    * four in libnotify.so.1.
    * Passing the fourth argument as NULL is binary compatible.
    */
   typedef void (*NotifyActionCallback)(NotifyNotification*, char*, gpointer);
   typedef bool (*notify_is_initted_t)(void);
   typedef bool (*notify_init_t)(const char*);
   typedef GList* (*notify_get_server_caps_t)(void);
   typedef NotifyNotification* (*notify_notification_new_t)(const char*, const char*, const char*, const char*);
-  typedef bool (*notify_notification_show_t)(void*, char*);
+  typedef bool (*notify_notification_show_t)(void*, GError**);
   typedef void (*notify_notification_set_icon_from_pixbuf_t)(void*, GdkPixbuf*);
   typedef void (*notify_notification_add_action_t)(void*, const char*, const char*, NotifyActionCallback, gpointer, GFreeFunc);
+  typedef bool (*notify_notification_close_t)(void*, GError**);
 
   nsCOMPtr<imgIRequest> mIconRequest;
   nsCString mAlertTitle;
   nsCString mAlertText;
 
   nsCOMPtr<nsIObserver> mAlertListener;
   nsString mAlertCookie;
+  nsString mAlertName;
+
+  RefPtr<nsSystemAlertsService> mBackend;
 
   bool mLoadedFrame;
   bool mAlertHasAction;
 
   static void* libNotifyHandle;
   static bool libNotifyNotAvail;
   static notify_is_initted_t notify_is_initted;
   static notify_init_t notify_init;
   static notify_get_server_caps_t notify_get_server_caps;
   static notify_notification_new_t notify_notification_new;
   static notify_notification_show_t notify_notification_show;
   static notify_notification_set_icon_from_pixbuf_t notify_notification_set_icon_from_pixbuf;
   static notify_notification_add_action_t notify_notification_add_action;
+  static notify_notification_close_t notify_notification_close;
   NotifyNotification* mNotification;
   gulong mClosureHandler;
 
   nsresult StartRequest(const nsAString & aImageUrl, bool aInPrivateBrowsing);
   nsresult ShowAlert(GdkPixbuf* aPixbuf);
+
+  void NotifyFinished();
 };
 
 #endif
--- a/toolkit/system/gnome/nsSystemAlertsService.cpp
+++ b/toolkit/system/gnome/nsSystemAlertsService.cpp
@@ -52,20 +52,58 @@ NS_IMETHODIMP nsSystemAlertsService::Sho
   return ShowAlert(alert, aAlertListener);
 }
 
 NS_IMETHODIMP nsSystemAlertsService::ShowAlert(nsIAlertNotification* aAlert,
                                                nsIObserver* aAlertListener)
 {
   NS_ENSURE_ARG(aAlert);
 
-  RefPtr<nsAlertsIconListener> alertListener = new nsAlertsIconListener();
+  nsAutoString alertName;
+  nsresult rv = aAlert->GetName(alertName);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  RefPtr<nsAlertsIconListener> alertListener =
+    new nsAlertsIconListener(this, alertName);
   if (!alertListener)
     return NS_ERROR_OUT_OF_MEMORY;
 
+  AddListener(alertName, alertListener);
   return alertListener->InitAlertAsync(aAlert, aAlertListener);
 }
 
 NS_IMETHODIMP nsSystemAlertsService::CloseAlert(const nsAString& aAlertName,
                                                 nsIPrincipal* aPrincipal)
 {
-  return NS_ERROR_NOT_IMPLEMENTED;
+  RefPtr<nsAlertsIconListener> listener = mActiveListeners.Get(aAlertName);
+  if (!listener) {
+    return NS_OK;
+  }
+  mActiveListeners.Remove(aAlertName);
+  return listener->Close();
+}
+
+bool nsSystemAlertsService::IsActiveListener(const nsAString& aAlertName,
+                                             nsAlertsIconListener* aListener)
+{
+  return mActiveListeners.Get(aAlertName) == aListener;
 }
+
+void nsSystemAlertsService::AddListener(const nsAString& aAlertName,
+                                        nsAlertsIconListener* aListener)
+{
+  RefPtr<nsAlertsIconListener> oldListener = mActiveListeners.Get(aAlertName);
+  mActiveListeners.Put(aAlertName, aListener);
+  if (oldListener) {
+    // If an alert with this name already exists, close it.
+    oldListener->Close();
+  }
+}
+
+void nsSystemAlertsService::RemoveListener(const nsAString& aAlertName,
+                                           nsAlertsIconListener* aListener)
+{
+  if (IsActiveListener(aAlertName, aListener)) {
+    // The alert may have been replaced; only remove it from the active
+    // listeners map if it's the same.
+    mActiveListeners.Remove(aAlertName);
+  }
+}
--- a/toolkit/system/gnome/nsSystemAlertsService.h
+++ b/toolkit/system/gnome/nsSystemAlertsService.h
@@ -2,26 +2,38 @@
 /* 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/. */
 
 #ifndef nsSystemAlertsService_h__
 #define nsSystemAlertsService_h__
 
 #include "nsIAlertsService.h"
+#include "nsDataHashtable.h"
 #include "nsCOMPtr.h"
 
+class nsAlertsIconListener;
+
 class nsSystemAlertsService : public nsIAlertsService
 {
 public:
   NS_DECL_NSIALERTSSERVICE
   NS_DECL_ISUPPORTS
 
   nsSystemAlertsService();
 
   nsresult Init();
 
+  bool IsActiveListener(const nsAString& aAlertName,
+                        nsAlertsIconListener* aListener);
+  void RemoveListener(const nsAString& aAlertName,
+                      nsAlertsIconListener* aListener);
+
 protected:
   virtual ~nsSystemAlertsService();
 
+  void AddListener(const nsAString& aAlertName,
+                   nsAlertsIconListener* aListener);
+
+  nsDataHashtable<nsStringHashKey, nsAlertsIconListener*> mActiveListeners;
 };
 
 #endif /* nsSystemAlertsService_h__ */