Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 28 Sep 2016 16:25:01 +0200
changeset 345688 2a3ee1d58982e0c6a2f9587d9eb95cfc758bc089
parent 345687 2e36f973cedf30349bf8de1f4b9a24e21e440365 (current diff)
parent 345563 b1d60f2f68c7cccc96fcf9a2075bb430a500a0f2 (diff)
child 345689 10243fd520cacbf3008d6679694e1d54de69b0cf
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-esr52@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone52.0a1
Merge mozilla-central to mozilla-inbound
devtools/client/inspector/inspector-panel.js
netwerk/srtp/src/crypto/include/gf2_8.h
netwerk/srtp/src/crypto/math/gf2_8.c
old-configure.in
taskcluster/taskgraph/task/legacy.py
--- a/addon-sdk/source/python-lib/cuddlefish/prefs.py
+++ b/addon-sdk/source/python-lib/cuddlefish/prefs.py
@@ -45,16 +45,17 @@ DEFAULT_NO_CONNECTIONS_PREFS = {
     'app.update.auto' : False,
     'app.update.url': 'http://localhost/app-dummy/update',
     # Make sure GMPInstallManager won't hit the network.
     'media.gmp-gmpopenh264.autoupdate' : False,
     'media.gmp-manager.cert.checkAttributes' : False,
     'media.gmp-manager.cert.requireBuiltIn' : False,
     'media.gmp-manager.url' : 'http://localhost/media-dummy/gmpmanager',
     'media.gmp-manager.url.override': 'http://localhost/dummy-gmp-manager.xml',
+    'media.gmp-manager.updateEnabled': False,
     'browser.aboutHomeSnippets.updateUrl': 'https://localhost/snippet-dummy',
     'browser.newtab.url' : 'about:blank',
     'browser.search.update': False,
     'browser.search.suggest.enabled' : False,
     'browser.safebrowsing.phishing.enabled' : False,
     'browser.safebrowsing.provider.google.updateURL': 'http://localhost/safebrowsing-dummy/update',
     'browser.safebrowsing.provider.google.gethashURL': 'http://localhost/safebrowsing-dummy/gethash',
     'browser.safebrowsing.malware.reportURL': 'http://localhost/safebrowsing-dummy/malwarereport',
--- a/addon-sdk/source/test/preferences/no-connections.json
+++ b/addon-sdk/source/test/preferences/no-connections.json
@@ -5,16 +5,17 @@
   "app.update.url": "http://localhost/app-dummy/update",
   "app.update.enabled": false,
   "app.update.staging.enabled": false,
   "media.gmp-gmpopenh264.autoupdate": false,
   "media.gmp-manager.cert.checkAttributes": false,
   "media.gmp-manager.cert.requireBuiltIn": false,
   "media.gmp-manager.url": "http://localhost/media-dummy/gmpmanager",
   "media.gmp-manager.url.override": "http://localhost/dummy-gmp-manager.xml",
+  "media.gmp-manager.updateEnabled": false,
   "browser.aboutHomeSnippets.updateUrl": "https://localhost/snippet-dummy",
   "browser.newtab.url": "about:blank",
   "browser.search.update": false,
   "browser.search.suggest.enabled": false,
   "browser.safebrowsing.phishing.enabled": false,
   "browser.safebrowsing.provider.google.updateURL": "http://localhost/safebrowsing-dummy/update",
   "browser.safebrowsing.provider.google.gethashURL": "http://localhost/safebrowsing-dummy/gethash",
   "browser.safebrowsing.provider.google.reportURL": "http://localhost/safebrowsing-dummy/malwarereport",
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1282,16 +1282,20 @@ pref("identity.fxaccounts.remote.signup.
 pref("identity.fxaccounts.remote.force_auth.uri", "https://accounts.firefox.com/force_auth?service=sync&context=fx_desktop_v3");
 
 // The remote content URL shown for signin in. Must use HTTPS.
 pref("identity.fxaccounts.remote.signin.uri", "https://accounts.firefox.com/signin?service=sync&context=fx_desktop_v3");
 
 // The remote content URL where FxAccountsWebChannel messages originate.
 pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com/");
 
+// The value of the context query parameter passed in some fxa requests when config
+// discovery is enabled.
+pref("identity.fxaccounts.contextParam", "fx_desktop_v3");
+
 // The URL we take the user to when they opt to "manage" their Firefox Account.
 // Note that this will always need to be in the same TLD as the
 // "identity.fxaccounts.remote.signup.uri" pref.
 pref("identity.fxaccounts.settings.uri", "https://accounts.firefox.com/settings?service=sync&context=fx_desktop_v3");
 
 // The remote URL of the FxA Profile Server
 pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
 
--- a/browser/base/content/aboutaccounts/aboutaccounts.css
+++ b/browser/base/content/aboutaccounts/aboutaccounts.css
@@ -4,17 +4,17 @@ html, body {
 
 #remote {
   width: 100%;
   height: 100%;
   border: 0;
   display: none;
 }
 
-#networkError, #manage, #intro, #stage {
+#networkError, #manage, #intro, #stage, #configError {
   display: none;
 }
 
 #oldsync {
   background: none;
   border: 0;
   color: #0095dd;
 }
--- a/browser/base/content/aboutaccounts/aboutaccounts.js
+++ b/browser/base/content/aboutaccounts/aboutaccounts.js
@@ -159,24 +159,24 @@ var wrapper = {
           }
         }
       }
 
       // Calling cancel() will raise some OnStateChange notifications by itself,
       // so avoid doing that more than once
       if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
         aRequest.cancel(Components.results.NS_BINDING_ABORTED);
-        setErrorPage();
+        setErrorPage("networkError");
       }
     },
 
     onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
       if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
         aRequest.cancel(Components.results.NS_BINDING_ABORTED);
-        setErrorPage();
+        setErrorPage("networkError");
       }
     },
 
     onProgressChange: function() {},
     onStatusChange: function() {},
     onSecurityChange: function() {},
   },
 
@@ -289,28 +289,27 @@ var wrapper = {
         break;
       default:
         log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
         break;
     }
   },
 
   injectData: function (type, content) {
-    let authUrl;
-    try {
-      authUrl = fxAccounts.getAccountsSignUpURI();
-    } catch (e) {
-      error("Couldn't inject data: " + e.message);
-      return;
-    }
-    let data = {
-      type: type,
-      content: content
-    };
-    this.iframe.contentWindow.postMessage(data, authUrl);
+    return fxAccounts.promiseAccountsSignUpURI().then(authUrl => {
+      let data = {
+        type: type,
+        content: content
+      };
+      this.iframe.contentWindow.postMessage(data, authUrl);
+    })
+    .catch(e => {
+      console.log("Failed to inject data", e);
+      setErrorPage("configError");
+    });
   },
 };
 
 
 // Button onclick handlers
 function handleOldSync() {
   let chromeWin = window
     .QueryInterface(Ci.nsIInterfaceRequestor)
@@ -339,80 +338,87 @@ function openPrefs() {
   window.location = "about:preferences#sync";
 }
 
 function init() {
   fxAccounts.getSignedInUser().then(user => {
     // tests in particular might cause the window to start closing before
     // getSignedInUser has returned.
     if (window.closed) {
-      return;
+      return Promise.resolve();
     }
 
     updateDisplayedEmail(user);
 
     // Ideally we'd use new URL(document.URL).searchParams, but for about: URIs,
     // searchParams is empty.
     let urlParams = new URLSearchParams(document.URL.split("?")[1] || "");
     let action = urlParams.get(ACTION_URL_PARAM);
     urlParams.delete(ACTION_URL_PARAM);
 
     switch (action) {
     case "signin":
       if (user) {
         // asking to sign-in when already signed in just shows manage.
         show("stage", "manage");
       } else {
-        show("remote");
-        wrapper.init(fxAccounts.getAccountsSignInURI(), urlParams);
+        return fxAccounts.promiseAccountsSignInURI().then(url => {
+          show("remote");
+          wrapper.init(url, urlParams);
+        });
       }
       break;
     case "signup":
       if (user) {
         // asking to sign-up when already signed in just shows manage.
         show("stage", "manage");
       } else {
-        show("remote");
-        wrapper.init(fxAccounts.getAccountsSignUpURI(), urlParams);
+        return fxAccounts.promiseAccountsSignUpURI().then(url => {
+          show("remote");
+          wrapper.init(url, urlParams);
+        });
       }
       break;
     case "reauth":
       // ideally we would only show this when we know the user is in a
       // "must reauthenticate" state - but we don't.
       // As the email address will be included in the URL returned from
       // promiseAccountsForceSigninURI, just always show it.
-      fxAccounts.promiseAccountsForceSigninURI().then(url => {
+      return fxAccounts.promiseAccountsForceSigninURI().then(url => {
         show("remote");
         wrapper.init(url, urlParams);
       });
-      break;
     default:
       // No action specified.
       if (user) {
         show("stage", "manage");
       } else {
         // Attempt a migration if enabled or show the introductory page
         // otherwise.
-        migrateToDevEdition(urlParams).then(migrated => {
+        return migrateToDevEdition(urlParams).then(migrated => {
           if (!migrated) {
             show("stage", "intro");
             // load the remote frame in the background
-            wrapper.init(fxAccounts.getAccountsSignUpURI(), urlParams);
+            return fxAccounts.promiseAccountsSignUpURI().then(uri =>
+              wrapper.init(uri, urlParams));
           }
+          return Promise.resolve();
         });
       }
       break;
     }
+    return Promise.resolve();
   }).catch(err => {
-    error("Failed to get the signed in user: " + err);
+    console.log("Configuration or sign in error", err);
+    setErrorPage("configError");
   });
 }
 
-function setErrorPage() {
-  show("stage", "networkError");
+function setErrorPage(errorType) {
+  show("stage", errorType);
 }
 
 // Causes the "top-level" element with |id| to be shown - all other top-level
 // elements are hidden.  Optionally, ensures that only 1 "second-level" element
 // inside the top-level one is shown.
 function show(id, childId) {
   // top-level items are either <div> or <iframe>
   let allTop = document.querySelectorAll("body > div, iframe");
@@ -465,17 +471,22 @@ function migrateToDevEdition(urlParams) 
     return fxAccounts.promiseAccountsForceSigninURI().then(url => {
       show("remote");
       wrapper.init(url, urlParams);
     });
   }).then(null, error => {
     log("Failed to migrate FX Account: " + error);
     show("stage", "intro");
     // load the remote frame in the background
-    wrapper.init(fxAccounts.getAccountsSignUpURI(), urlParams);
+    fxAccounts.promiseAccountsSignUpURI().then(uri => {
+      wrapper.init(uri, urlParams)
+    }).catch(e => {
+      console.log("Failed to load signup page", e);
+      setErrorPage("configError");
+    });
   }).then(() => {
     // Reset the pref after migration.
     Services.prefs.setBoolPref("identity.fxaccounts.migrateToDevEdition", false);
     return true;
   }).then(null, err => {
     Cu.reportError("Failed to reset the migrateToDevEdition pref: " + err);
     return false;
   });
--- a/browser/base/content/aboutaccounts/aboutaccounts.xhtml
+++ b/browser/base/content/aboutaccounts/aboutaccounts.xhtml
@@ -82,16 +82,29 @@
             <div class="description">&aboutAccounts.noConnection.description;</div>
 
             <div class="button-row">
               <button id="buttonRetry" class="button" tabindex="3">&aboutAccounts.noConnection.retry;</button>
             </div>
         </section>
       </div>
 
+      <div id="configError">
+        <header>
+          <h1>&aboutAccounts.badConfig.title;</h1>
+        </header>
+
+        <section>
+            <div class="graphic graphic-sync-intro"> </div>
+
+            <div class="description">&aboutAccounts.badConfig.description;</div>
+
+        </section>
+      </div>
+
     </div>
 
     <iframe mozframetype="content" id="remote" />
 
     <script type="application/javascript;version=1.8"
       src="chrome://browser/content/utilityOverlay.js"/>
     <script type="text/javascript;version=1.8"
       src="chrome://browser/content/aboutaccounts/aboutaccounts.js" />
--- a/browser/base/content/browser-media.js
+++ b/browser/base/content/browser-media.js
@@ -196,20 +196,16 @@ XPCOMUtils.defineLazyGetter(gEMEHandler,
 
 const TELEMETRY_DDSTAT_SHOWN = 0;
 const TELEMETRY_DDSTAT_SHOWN_FIRST = 1;
 const TELEMETRY_DDSTAT_CLICKED = 2;
 const TELEMETRY_DDSTAT_CLICKED_FIRST = 3;
 const TELEMETRY_DDSTAT_SOLVED = 4;
 
 let gDecoderDoctorHandler = {
-  shouldShowLearnMoreButton() {
-    return AppConstants.platform == "win";
-  },
-
   getLabelForNotificationBox(type) {
     if (type == "adobe-cdm-not-found" &&
         AppConstants.platform == "win") {
       if (AppConstants.isPlatformAndVersionAtMost("win", "5.9")) {
         // We supply our own Learn More button so we don't need to populate the message here.
         return gNavigatorBundle.getFormattedString("emeNotifications.drmContentDisabled.message", [""]);
       }
       return gNavigatorBundle.getString("decoder.noCodecs.message");
@@ -230,16 +226,29 @@ let gDecoderDoctorHandler = {
       }
       if (AppConstants.isPlatformAndVersionAtLeast("win", "6")) {
         return gNavigatorBundle.getString("decoder.noHWAccelerationVista.message");
       }
       if (AppConstants.platform == "linux") {
         return gNavigatorBundle.getString("decoder.noCodecsLinux.message");
       }
     }
+    if (type == "cannot-initialize-pulseaudio") {
+      return gNavigatorBundle.getString("decoder.noPulseAudio.message");
+    }
+    return "";
+  },
+
+  getSumoForLearnHowButton(type) {
+    if (AppConstants.platform == "win") {
+      return "fix-video-audio-problems-firefox-windows";
+    }
+    if (type == "cannot-initialize-pulseaudio") {
+      return "fix-common-audio-and-video-issues";
+    }
     return "";
   },
 
   receiveMessage({target: browser, data: data}) {
     let box = gBrowser.getNotificationBox(browser);
     let notificationId = "decoder-doctor-notification";
     if (box.getNotificationWithValue(notificationId)) {
       return;
@@ -301,31 +310,32 @@ let gDecoderDoctorHandler = {
         if (newbies.length) {
           Services.prefs.setCharPref(formatsPref,
                                      existing.concat(newbies).join(", "));
         }
       }
       histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SHOWN);
 
       let buttons = [];
-      if (gDecoderDoctorHandler.shouldShowLearnMoreButton()) {
+      let sumo = gDecoderDoctorHandler.getSumoForLearnHowButton(type);
+      if (sumo) {
         buttons.push({
           label: gNavigatorBundle.getString("decoder.noCodecs.button"),
           accessKey: gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
           callback() {
             let clickedInPref = Services.prefs.getPrefType(buttonClickedPref) &&
                                 Services.prefs.getBoolPref(buttonClickedPref);
             if (!clickedInPref) {
               Services.prefs.setBoolPref(buttonClickedPref, true);
               histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED_FIRST);
             }
             histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED);
 
             let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
-            openUILinkIn(baseURL + "fix-video-audio-problems-firefox-windows", "tab");
+            openUILinkIn(baseURL + sumo, "tab");
           }
         });
       }
 
       box.appendNotification(
           title,
           notificationId,
           "", // This uses the info icon as specified below.
--- a/browser/base/content/test/general/browser_aboutAccounts.js
+++ b/browser/base/content/test/general/browser_aboutAccounts.js
@@ -313,17 +313,17 @@ var gTests = [
     let mm = tab.linkedBrowser.messageManager;
     mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
       url: "about:accounts",
       profilePath: mockDir.path,
     });
 
     let response = yield readyPromise;
     // We are expecting the iframe to be on the "signup" URL
-    let expected = fxAccounts.getAccountsSignUpURI();
+    let expected = yield fxAccounts.promiseAccountsSignUpURI();
     is(response.data.url, expected);
 
     // and expect no signed in user.
     let userData = yield fxAccounts.getSignedInUser();
     is(userData, null);
     // The migration pref should have still been switched off.
     is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
     yield OS.File.removeEmptyDir(mockDir.path);
--- a/browser/base/content/test/general/browser_decoderDoctor.js
+++ b/browser/base/content/test/general/browser_decoderDoctor.js
@@ -32,17 +32,18 @@ function* test_decoder_doctor_notificati
     }
 
     is(button.getAttribute("label"), gNavigatorBundle.getString("decoder.noCodecs.button"),
       "notification button should be 'Learn more'");
     is(button.getAttribute("accesskey"), gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
       "notification button should have accesskey");
 
     let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
-    let url = baseURL + "fix-video-audio-problems-firefox-windows";
+    let url = baseURL + ((options && options.sumo) ||
+                         "fix-video-audio-problems-firefox-windows");
     let awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser, url);
     button.click();
     let sumoTab = yield awaitNewTab;
     yield BrowserTestUtils.removeTab(sumoTab);
   });
 }
 
 add_task(function* test_adobe_cdm_not_found() {
@@ -90,8 +91,20 @@ add_task(function* test_platform_decoder
   } else {
     message = gNavigatorBundle.getString("decoder.noHWAcceleration.message");
   }
 
   yield test_decoder_doctor_notification("platform-decoder-not-found",
                                          message,
                                          {noLearnMoreButton: isLinux});
 });
+
+add_task(function* test_cannot_initialize_pulseaudio() {
+  // This is only sent on Linux.
+  if (AppConstants.platform != "linux") {
+    return;
+  }
+
+  let message = gNavigatorBundle.getString("decoder.noPulseAudio.message");
+  yield test_decoder_doctor_notification("cannot-initialize-pulseaudio",
+                                         message,
+                                         {sumo: "fix-common-audio-and-video-issues"});
+});
--- a/browser/locales/en-US/chrome/browser/aboutAccounts.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutAccounts.dtd
@@ -7,8 +7,10 @@
 
 <!ENTITY aboutAccountsConfig.description "Sign in to sync your tabs, bookmarks, passwords &amp; more.">
 <!ENTITY aboutAccountsConfig.startButton.label "Get started">
 <!ENTITY aboutAccountsConfig.useOldSync.label "Using an older version of Sync?">
 <!ENTITY aboutAccountsConfig.syncPreferences.label "Sync preferences">
 <!ENTITY aboutAccounts.noConnection.title "No connection">
 <!ENTITY aboutAccounts.noConnection.description "You must be connected to the Internet to sign in.">
 <!ENTITY aboutAccounts.noConnection.retry "Try again">
+<!ENTITY aboutAccounts.badConfig.title "Bad configuration">
+<!ENTITY aboutAccounts.badConfig.description "Unable to determine your Firefox Account server configuration. Please try again later.">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -729,16 +729,17 @@ pendingCrashReports.alwaysSend = Always 
 decoder.noCodecs.button = Learn how
 decoder.noCodecs.accesskey = L
 decoder.noCodecs.message = To play video, you may need to install Microsoft’s Media Feature Pack.
 decoder.noCodecsVista.message = To play video, you may need to install Microsoft’s Platform Update Supplement for Windows Vista.
 decoder.noCodecsXP.message = To play video, you may need to enable Adobe’s Primetime Content Decryption Module.
 decoder.noCodecsLinux.message = To play video, you may need to install the required video codecs.
 decoder.noHWAcceleration.message = To improve video quality, you may need to install Microsoft’s Media Feature Pack.
 decoder.noHWAccelerationVista.message = To improve video quality, you may need to install Microsoft’s Platform Update Supplement for Windows Vista.
+decoder.noPulseAudio.message = To play audio, you may need to install the required PulseAudio software.
 
 permissions.remove.tooltip = Clear this permission and ask again
 
 # LOCALIZATION NOTE (aboutDialog.architecture.*):
 # The sixtyFourBit and thirtyTwoBit strings describe the architecture of the
 # current Firefox build: 32-bit or 64-bit. These strings are used in parentheses
 # between the Firefox version and the "What's new" link in the About dialog,
 # e.g.: "48.0.2 (32-bit) <What's new>" or "51.0a1 (2016-09-05) (64-bit)".
--- a/build/moz.configure/compile-checks.configure
+++ b/build/moz.configure/compile-checks.configure
@@ -13,23 +13,24 @@
 # - `body` is the code that will appear in the main function of the generated
 #   test program. `return 0;` is appended to the function body automatically.
 # - `language` is the language selection, so that the appropriate compiler is
 #   used.
 # - `flags` are the flags to be passed to the compiler, in addition to `-c`.
 # - `check_msg` is the message to be printed to accompany compiling the test
 #   program.
 @template
-def try_compile(includes=None, body='', language='C++', flags=None, check_msg=None):
+def try_compile(includes=None, body='', language='C++', flags=None, check_msg=None,
+                when=None):
     compiler = {
         'C': c_compiler,
         'C++': cxx_compiler,
     }[language]
 
-    return compiler.try_compile(includes, body, flags, check_msg)
+    return compiler.try_compile(includes, body, flags, check_msg, when=when)
 
 
 # Checks for the presence of the given header on the target system by compiling
 # a test program including that header. The return value of the template is a
 # check function returning True if the header is present, and None if it is not.
 # The value of this check function is also used to set a variable (with set_define)
 # corresponding to the checked header. For instance, HAVE_MALLOC_H will be set in
 # defines if check_header if called with 'malloc.h' as input and malloc.h is
@@ -47,20 +48,18 @@ def check_header(header, language='C++',
     when = when or always
 
     if includes:
         includes = includes[:]
     else:
         includes = []
     includes.append(header)
 
-    @depends_when(try_compile(includes=includes, language=language, flags=flags,
-                              check_msg='for %s' % header), when=when)
-    def have_header(value):
-        return value
+    have_header = try_compile(includes=includes, language=language, flags=flags,
+                              check_msg='for %s' % header, when=when)
     header_var = 'HAVE_%s' % (header.upper()
                                     .replace('-', '_')
                                     .replace('/', '_')
                                     .replace('.', '_'))
     set_define(header_var, have_header)
     return have_header
 
 # A convenience wrapper for check_header for checking multiple headers.
--- a/config/external/nss/nss.symbols
+++ b/config/external/nss/nss.symbols
@@ -674,16 +674,17 @@ SSL_HandshakeNegotiatedExtension
 SSL_ImplementedCiphers @DATA@
 SSL_ImportFD
 SSL_NamedGroupConfig
 SSL_NumImplementedCiphers @DATA@
 SSL_OptionSet
 SSL_OptionSetDefault
 SSL_PeerCertificate
 SSL_PeerCertificateChain
+SSL_PeerSignedCertTimestamps
 SSL_PeerStapledOCSPResponses
 SSL_ResetHandshake
 SSL_SendAdditionalKeyShares
 SSL_SetCanFalseStartCallback
 SSL_SetDowngradeCheckVersion
 SSL_SetNextProtoNego
 SSL_SetPKCS11PinArg
 SSL_SetSockPeerID
--- a/devtools/client/dom/content/components/dom-tree.js
+++ b/devtools/client/dom/content/components/dom-tree.js
@@ -56,16 +56,17 @@ var DomTree = React.createClass({
     }];
 
     // This is the integration point with Reps. The DomTree is using
     // Reps to render all values. The code also specifies default rep
     // used for data types that don't have its own specific template.
     let renderValue = props => {
       return Rep(Object.assign({}, props, {
         defaultRep: Grip,
+        cropLimit: 50,
       }));
     };
 
     return (
       TreeView({
         object: this.props.object,
         provider: new GripProvider(this.props.grips, this.props.dispatch),
         decorator: new DomDecorator(),
--- a/devtools/client/inspector/inspector.xhtml
+++ b/devtools/client/inspector/inspector.xhtml
@@ -21,17 +21,17 @@
 <!DOCTYPE html>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/shared/theme-switching.js"></script>
 </head>
-<body class="theme-body devtools-monospace" role="application">
+<body class="theme-body" role="application">
   <div class="inspector-responsive-container theme-body inspector">
 
     <!-- Main Panel Content -->
     <div id="inspector-main-content" class="devtools-main-content">
       <div id="inspector-toolbar" class="devtools-toolbar" nowindowdrag="true"
                 data-localization-bundle="devtools/locale/inspector.properties">
         <button id="inspector-element-add-button" class="devtools-button"
                      data-localization="title=inspectorAddNode.label"/>
--- a/devtools/client/inspector/markup/markup.xhtml
+++ b/devtools/client/inspector/markup/markup.xhtml
@@ -2,17 +2,16 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 <!DOCTYPE html>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <link rel="stylesheet" href="chrome://devtools/content/inspector/markup/markup.css" type="text/css"/>
   <link rel="stylesheet" href="chrome://devtools/skin/markup.css" type="text/css"/>
 
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/shared/theme-switching.js"/>
 
 </head>
 <body class="theme-body devtools-monospace" role="application">
 
--- a/devtools/client/jsonview/components/json-panel.js
+++ b/devtools/client/jsonview/components/json-panel.js
@@ -88,17 +88,19 @@ define(function (require, exports, modul
       let member = props.member;
 
       // Hide object summary when object is expanded (bug 1244912).
       if (typeof member.value == "object" && member.open) {
         return null;
       }
 
       // Render the value (summary) using Reps library.
-      return Rep(props);
+      return Rep(Object.assign({}, props, {
+        cropLimit: 50,
+      }));
     },
 
     renderTree: function () {
       // Append custom column for displaying values. This column
       // Take all available horizontal space.
       let columns = [{
         id: "value",
         width: "100%"
--- a/devtools/client/performance/components/moz.build
+++ b/devtools/client/performance/components/moz.build
@@ -3,15 +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/.
 
 DevToolsModules(
     'jit-optimizations-item.js',
     'jit-optimizations.js',
     'recording-button.js',
     'recording-controls.js',
+    'recording-list-item.js',
+    'recording-list.js',
     'waterfall-header.js',
     'waterfall-tree-row.js',
     'waterfall-tree.js',
     'waterfall.js',
 )
 
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/recording-list-item.js
@@ -0,0 +1,49 @@
+/* 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, createClass} = require("devtools/client/shared/vendor/react");
+const {div, li, span, button} = DOM;
+const {L10N} = require("devtools/client/performance/modules/global");
+
+module.exports = createClass({
+  displayName: "Recording List Item",
+
+  render() {
+    const {
+      label,
+      duration,
+      onSelect,
+      onSave,
+      isLoading,
+      isSelected,
+      isRecording
+    } = this.props;
+
+    const className = `recording-list-item ${isSelected ? "selected" : ""}`;
+
+    let durationText;
+    if (isLoading) {
+      durationText = L10N.getStr("recordingsList.loadingLabel");
+    } else if (isRecording) {
+      durationText = L10N.getStr("recordingsList.recordingLabel");
+    } else {
+      durationText = L10N.getFormatStr("recordingsList.durationLabel", duration);
+    }
+
+    return (
+      li({ className, onClick: onSelect },
+        div({ className: "recording-list-item-label" },
+          label
+        ),
+        div({ className: "recording-list-item-footer" },
+          span({ className: "recording-list-item-duration" }, durationText),
+          button({ className: "recording-list-item-save", onClick: onSave },
+            L10N.getStr("recordingsList.saveLabel")
+          )
+        )
+      )
+    );
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/components/recording-list.js
@@ -0,0 +1,23 @@
+/* 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, createClass} = require("devtools/client/shared/vendor/react");
+const {L10N} = require("devtools/client/performance/modules/global");
+const {ul, div} = DOM;
+
+module.exports = createClass({
+  displayName: "Recording List",
+
+  render() {
+    const {
+      items,
+      itemComponent: Item,
+    } = this.props;
+
+    return items.length > 0
+      ? ul({ className: "recording-list" }, ...items.map(Item))
+      : div({ className: "recording-list-empty" }, L10N.getStr("noRecordingsText"));
+  }
+});
--- a/devtools/client/performance/performance-controller.js
+++ b/devtools/client/performance/performance-controller.js
@@ -22,23 +22,26 @@ var { gDevTools } = require("devtools/cl
 var EVENTS = require("devtools/client/performance/events");
 Object.defineProperty(this, "EVENTS", {
   value: EVENTS,
   enumerable: true,
   writable: false
 });
 
 /* exported React, ReactDOM, JITOptimizationsView, RecordingControls, RecordingButton,
-   Waterfall, Services, promise, EventEmitter, DevToolsUtils, system */
+   RecordingList, RecordingListItem, Services, Waterfall, promise, EventEmitter,
+   DevToolsUtils, system */
 var React = require("devtools/client/shared/vendor/react");
 var ReactDOM = require("devtools/client/shared/vendor/react-dom");
 var Waterfall = React.createFactory(require("devtools/client/performance/components/waterfall"));
 var JITOptimizationsView = React.createFactory(require("devtools/client/performance/components/jit-optimizations"));
 var RecordingControls = React.createFactory(require("devtools/client/performance/components/recording-controls"));
 var RecordingButton = React.createFactory(require("devtools/client/performance/components/recording-button"));
+var RecordingList = React.createFactory(require("devtools/client/performance/components/recording-list"));
+var RecordingListItem = React.createFactory(require("devtools/client/performance/components/recording-list-item"));
 
 var Services = require("Services");
 var promise = require("promise");
 var EventEmitter = require("devtools/shared/event-emitter");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 var flags = require("devtools/shared/flags");
 var system = require("devtools/shared/system");
 
--- a/devtools/client/performance/performance.xul
+++ b/devtools/client/performance/performance.xul
@@ -79,17 +79,19 @@
 
   <hbox id="body" class="theme-body performance-tool" flex="1">
 
     <!-- Sidebar: controls and recording list -->
     <vbox id="recordings-pane">
       <hbox id="recordings-controls">
         <html:div id='recording-controls-mount'/>
       </hbox>
-      <vbox id="recordings-list" class="theme-sidebar" flex="1"/>
+      <vbox id="recordings-list" class="theme-sidebar" flex="1">
+        <html:div id="recording-list-mount"/>
+      </vbox>
     </vbox>
 
     <!-- Main panel content -->
     <vbox id="performance-pane" flex="1">
 
       <!-- Top toolbar controls -->
       <toolbar id="performance-toolbar"
                class="devtools-toolbar">
--- a/devtools/client/performance/test/browser_perf-calltree-js-events.js
+++ b/devtools/client/performance/test/browser_perf-calltree-js-events.js
@@ -29,21 +29,28 @@ add_task(function* () {
   yield rendered;
 
   // Mock the profile used so we can get a deterministic tree created.
   let profile = synthesizeProfile();
   let threadNode = new ThreadNode(profile.threads[0], OverviewView.getTimeInterval());
   JsCallTreeView._populateCallTree(threadNode);
   JsCallTreeView.emit(EVENTS.UI_JS_CALL_TREE_RENDERED);
 
+  let firstTreeItem = $("#js-calltree-view .call-tree-item");
+
+  // DE-XUL: There are focus issues with XUL. Focus first, then synthesize the clicks
+  // so that keyboard events work correctly.
+  firstTreeItem.focus();
+
   let count = 0;
   let onFocus = () => count++;
   JsCallTreeView.on("focus", onFocus);
 
-  click($("#js-calltree-view .call-tree-item"));
+  click(firstTreeItem);
+
   key("VK_DOWN");
   key("VK_DOWN");
   key("VK_DOWN");
   key("VK_DOWN");
 
   JsCallTreeView.off("focus", onFocus);
   is(count, 4, "Several focus events are fired for the calltree.");
 
--- a/devtools/client/performance/test/browser_perf-console-record-01.js
+++ b/devtools/client/performance/test/browser_perf-console-record-01.js
@@ -5,36 +5,39 @@
 /**
  * Tests if the profiler is populated by console recordings that have finished
  * before it was opened.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   yield console.profile("rust");
   yield console.profileEnd("rust");
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { PerformanceController, RecordingsView, WaterfallView } = panel.panelWin;
+  let { PerformanceController, WaterfallView } = panel.panelWin;
 
   yield waitUntil(() => PerformanceController.getRecordings().length == 1);
   yield waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 1, "One recording found in the performance panel.");
   is(recordings[0].isConsole(), true, "Recording came from console.profile.");
   is(recordings[0].getLabel(), "rust", "Correct label in the recording model.");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  const selected = getSelectedRecording(panel);
+
+  is(selected, recordings[0],
     "The profile from console should be selected as it's the only one.");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-console-record-02.js
+++ b/devtools/client/performance/test/browser_perf-console-record-02.js
@@ -8,43 +8,45 @@
  */
 
 const { Constants } = require("devtools/client/performance/modules/constants");
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
 const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   yield console.profile("rust");
   yield console.profile("rust2");
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { EVENTS, PerformanceController, OverviewView, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
   yield waitUntil(() => PerformanceController.getRecordings().length == 2);
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
   is(recordings[0].isConsole(), true, "Recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "Correct label in the recording model (1).");
   is(recordings[0].isRecording(), true, "Recording is still recording (1).");
   is(recordings[1].isConsole(), true, "Recording came from console.profile (2).");
   is(recordings[1].getLabel(), "rust2", "Correct label in the recording model (2).");
   is(recordings[1].isRecording(), true, "Recording is still recording (2).");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  const selected = getSelectedRecording(panel);
+  is(selected, recordings[0],
     "The first console recording should be selected.");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
   let stopped = waitForRecordingStoppedEvents(panel, {
--- a/devtools/client/performance/test/browser_perf-console-record-03.js
+++ b/devtools/client/performance/test/browser_perf-console-record-03.js
@@ -6,45 +6,47 @@
  * Tests if the profiler is populated by in-progress console recordings, and
  * also console recordings that have finished before it was opened.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   yield console.profile("rust");
   yield console.profileEnd("rust");
   yield console.profile("rust2");
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { PerformanceController, RecordingsView, WaterfallView } = panel.panelWin;
+  let { PerformanceController, WaterfallView } = panel.panelWin;
 
   yield waitUntil(() => PerformanceController.getRecordings().length == 2);
   yield waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
   is(recordings[0].isConsole(), true, "Recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "Correct label in the recording model (1).");
   is(recordings[0].isRecording(), false, "Recording is still recording (1).");
   is(recordings[1].isConsole(), true, "Recording came from console.profile (2).");
   is(recordings[1].getLabel(), "rust2", "Correct label in the recording model (2).");
   is(recordings[1].isRecording(), true, "Recording is still recording (2).");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  const selected = getSelectedRecording(panel);
+  is(selected, recordings[0],
     "The first console recording should be selected.");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   let stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when a finished recording is selected
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
--- a/devtools/client/performance/test/browser_perf-console-record-04.js
+++ b/devtools/client/performance/test/browser_perf-console-record-04.js
@@ -7,42 +7,44 @@
  * after being opened.
  */
 
 const { Constants } = require("devtools/client/performance/modules/constants");
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { EVENTS, PerformanceController, OverviewView, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
   let started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profile("rust");
   yield started;
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 1, "One recording found in the performance panel.");
   is(recordings[0].isConsole(), true, "Recording came from console.profile.");
   is(recordings[0].getLabel(), "rust", "Correct label in the recording model.");
   is(recordings[0].isRecording(), true, "Recording is still recording.");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  const selected = getSelectedRecording(panel);
+  is(selected, recordings[0],
     "The profile from console should be selected as it's the only one.");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
   let stopped = waitForRecordingStoppedEvents(panel, {
--- a/devtools/client/performance/test/browser_perf-console-record-05.js
+++ b/devtools/client/performance/test/browser_perf-console-record-05.js
@@ -7,42 +7,44 @@
  * in the recording list.
  */
 
 const { Constants } = require("devtools/client/performance/modules/constants");
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { EVENTS, PerformanceController, OverviewView, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
   let started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profile("rust");
   yield started;
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 1, "One recording found in the performance panel.");
   is(recordings[0].isConsole(), true, "Recording came from console.profile (1).");
   is(recordings[0].getLabel(), "rust", "Correct label in the recording model (1).");
   is(recordings[0].isRecording(), true, "Recording is still recording (1).");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  let selected = getSelectedRecording(panel);
+  is(selected, recordings[0],
     "The profile from console should be selected as it's the only one.");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
   let stopped = waitForRecordingStoppedEvents(panel, {
@@ -65,19 +67,20 @@ add_task(function* () {
   yield started;
 
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
   is(recordings[1].isConsole(), true, "Recording came from console.profile (2).");
   is(recordings[1].getLabel(), "rust", "Correct label in the recording model (2).");
   is(recordings[1].isRecording(), true, "Recording is still recording (2).");
 
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  selected = getSelectedRecording(panel);
+  is(selected, recordings[0],
     "The profile from console should still be selected");
-  is(RecordingsView.selectedItem.attachment.getLabel(), "rust",
+  is(selected.getLabel(), "rust",
     "The profile label for the first recording is correct.");
 
   stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when a finished recording is selected
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
--- a/devtools/client/performance/test/browser_perf-console-record-06.js
+++ b/devtools/client/performance/test/browser_perf-console-record-06.js
@@ -6,36 +6,37 @@
  * Tests that console recordings can overlap (not completely nested).
  */
 
 const { Constants } = require("devtools/client/performance/modules/constants");
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { times } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { EVENTS, PerformanceController, OverviewView, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
   let started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profile("rust");
   yield started;
 
   let recordings = PerformanceController.getRecordings();
   is(recordings.length, 1, "A recording found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(getSelectedRecording(panel), recordings[0],
     "The first console recording should be selected.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
   started = waitForRecordingStartedEvents(panel, {
@@ -47,49 +48,49 @@ add_task(function* () {
     // in-progress recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profile("golang");
   yield started;
 
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(getSelectedRecording(panel), recordings[0],
     "The first console recording should still be selected.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
   let stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profileEnd("rust");
   yield stopped;
 
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(getSelectedRecording(panel), recordings[0],
     "The first console recording should still be selected.");
   is(recordings[0].isRecording(), false,
     "The first console recording should no longer be recording.");
 
   stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when a finished recording is selected
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
   });
   yield console.profileEnd("golang");
   yield stopped;
 
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 2, "Two recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(getSelectedRecording(panel), recordings[0],
     "The first console recording should still be selected.");
   is(recordings[1].isRecording(), false,
     "The second console recording should no longer be recording.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-console-record-07.js
+++ b/devtools/client/performance/test/browser_perf-console-record-07.js
@@ -7,25 +7,26 @@
  * most recent console recording, and console.profileEnd() with a label that
  * does not match any pending recordings does nothing.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { idleWait } = require("devtools/client/performance/test/helpers/wait-utils");
+const { getSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { PerformanceController, RecordingsView } = panel.panelWin;
+  let { PerformanceController } = panel.panelWin;
 
   let started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profile();
   yield started;
 
@@ -49,21 +50,22 @@ add_task(function* () {
     // the view state won't switch to "console-recording" unless the new
     // in-progress recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profile("2");
   yield started;
 
   let recordings = PerformanceController.getRecordings();
+  let selected = getSelectedRecording(panel);
   is(recordings.length, 3, "Three recordings found in the performance panel.");
   is(recordings[0].getLabel(), "", "Checking label of recording 1");
   is(recordings[1].getLabel(), "1", "Checking label of recording 2");
   is(recordings[2].getLabel(), "2", "Checking label of recording 3");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(selected, recordings[0],
     "The first console recording should be selected.");
 
   is(recordings[0].isRecording(), true,
     "All recordings should now be started. (1)");
   is(recordings[1].isRecording(), true,
     "All recordings should now be started. (2)");
   is(recordings[2].isRecording(), true,
     "All recordings should now be started. (3)");
@@ -76,35 +78,37 @@ add_task(function* () {
     skipWaitingForSubview: true,
     // the view state won't switch to "recorded" unless the new
     // finished recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profileEnd();
   yield stopped;
 
+  selected = getSelectedRecording(panel);
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 3, "Three recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(selected, recordings[0],
     "The first console recording should still be selected.");
 
   is(recordings[0].isRecording(), true, "The not most recent recording should not stop " +
     "when calling console.profileEnd with no args.");
   is(recordings[1].isRecording(), true, "The not most recent recording should not stop " +
     "when calling console.profileEnd with no args.");
   is(recordings[2].isRecording(), false, "Only the most recent recording should stop " +
     "when calling console.profileEnd with no args.");
 
   info("Trying to `profileEnd` a non-existent console recording.");
   console.profileEnd("fxos");
   yield idleWait(1000);
 
+  selected = getSelectedRecording(panel);
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 3, "Three recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(selected, recordings[0],
     "The first console recording should still be selected.");
 
   is(recordings[0].isRecording(), true,
     "The first recording should not be ended yet.");
   is(recordings[1].isRecording(), true,
     "The second recording should not be ended yet.");
   is(recordings[2].isRecording(), false,
     "The third recording should still be ended.");
@@ -117,38 +121,40 @@ add_task(function* () {
     skipWaitingForSubview: true,
     // the view state won't switch to "recorded" unless the new
     // finished recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profileEnd();
   yield stopped;
 
+  selected = getSelectedRecording(panel);
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 3, "Three recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(selected, recordings[0],
     "The first console recording should still be selected.");
 
   is(recordings[0].isRecording(), true,
     "The first recording should not be ended yet.");
   is(recordings[1].isRecording(), false,
     "The second recording should not be ended yet.");
   is(recordings[2].isRecording(), false,
     "The third recording should still be ended.");
 
   stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profileEnd();
   yield stopped;
 
+  selected = getSelectedRecording(panel);
   recordings = PerformanceController.getRecordings();
   is(recordings.length, 3, "Three recordings found in the performance panel.");
-  is(RecordingsView.selectedItem.attachment, recordings[0],
+  is(selected, recordings[0],
     "The first console recording should be selected.");
 
   is(recordings[0].isRecording(), false,
     "All recordings should now be ended. (1)");
   is(recordings[1].isRecording(), false,
     "All recordings should now be ended. (2)");
   is(recordings[2].isRecording(), false,
     "All recordings should now be ended. (3)");
--- a/devtools/client/performance/test/browser_perf-console-record-08.js
+++ b/devtools/client/performance/test/browser_perf-console-record-08.js
@@ -8,104 +8,164 @@
  */
 
 const { Constants } = require("devtools/client/performance/modules/constants");
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { waitForRecordingStartedEvents, waitForRecordingStoppedEvents } = require("devtools/client/performance/test/helpers/actions");
 const { once, times } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
+
+/**
+ * The following are bit flag constants that are used to represent the state of a
+ * recording.
+ */
+
+// Represents a manually recorded profile, if a user hit the record button.
+const MANUAL = 0;
+// Represents a recorded profile from console.profile().
+const CONSOLE = 1;
+// Represents a profile that is currently recording.
+const RECORDING = 2;
+// Represents a profile that is currently selected.
+const SELECTED = 4;
+
+/**
+ * Utility function to provide a meaningful inteface for testing that the bits
+ * match for the recording state.
+ * @param {integer} expected - The expected bit values packed in an integer.
+ * @param {integer} actual - The actual bit values packed in an integer.
+ */
+function hasBitFlag(expected, actual) {
+  return !!(expected & actual);
+}
 
 add_task(function* () {
   // This test seems to take a very long time to finish on Linux VMs.
   requestLongerTimeout(4);
 
   let { target, console } = yield initConsoleInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
-  let { EVENTS, PerformanceController, RecordingsView, OverviewView } = panel.panelWin;
+  let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
-  info("Starting console.profile()...");
+  info("Recording 1 - Starting console.profile()...");
   let started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true
   });
   yield console.profile("rust");
   yield started;
-  testRecordings(PerformanceController, [C + S + R]);
+  testRecordings(PerformanceController, [
+    CONSOLE + SELECTED + RECORDING
+  ]);
 
-  info("Starting manual recording...");
+  info("Recording 2 - Starting manual recording...");
   yield startRecording(panel);
-  testRecordings(PerformanceController, [C + R, R + S]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + RECORDING + SELECTED
+  ]);
 
-  info("Starting console.profile(\"3\")...");
+  info("Recording 3 - Starting console.profile(\"3\")...");
   started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when an in-progress recording is selected
     skipWaitingForOverview: true,
     // the view state won't switch to "console-recording" unless the new
     // in-progress recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profile("3");
   yield started;
-  testRecordings(PerformanceController, [C + R, R + S, C + R]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + RECORDING + SELECTED,
+    CONSOLE + RECORDING
+  ]);
 
-  info("Starting console.profile(\"4\")...");
+  info("Recording 4 - Starting console.profile(\"4\")...");
   started = waitForRecordingStartedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when an in-progress  recording is selected
     skipWaitingForOverview: true,
     // the view state won't switch to "console-recording" unless the new
     // in-progress recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profile("4");
   yield started;
-  testRecordings(PerformanceController, [C + R, R + S, C + R, C + R]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + RECORDING + SELECTED,
+    CONSOLE + RECORDING,
+    CONSOLE + RECORDING
+  ]);
 
-  info("Ending console.profileEnd()...");
+  info("Recording 4 - Ending console.profileEnd()...");
   let stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when a finished recording is selected
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
     // the view state won't switch to "recorded" unless the new
     // finished recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profileEnd();
   yield stopped;
-  testRecordings(PerformanceController, [C + R, R + S, C + R, C]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + RECORDING + SELECTED,
+    CONSOLE + RECORDING,
+    CONSOLE
+  ]);
 
-  info("Select last recording...");
+  info("Recording 4 - Select last recording...");
   let recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 3;
+  setSelectedRecording(panel, 3);
   yield recordingSelected;
-  testRecordings(PerformanceController, [C + R, R, C + R, C + S]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + RECORDING,
+    CONSOLE + RECORDING,
+    CONSOLE + SELECTED
+  ]);
   ok(!OverviewView.isRendering(),
     "Stop rendering overview when a completed recording is selected.");
 
-  info("Stop manual recording...");
+  info("Recording 2 - Stop manual recording.");
+
   yield stopRecording(panel);
-  testRecordings(PerformanceController, [C + R, S, C + R, C]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL + SELECTED,
+    CONSOLE + RECORDING,
+    CONSOLE
+  ]);
   ok(!OverviewView.isRendering(),
     "Stop rendering overview when a completed recording is selected.");
 
-  info("Select first recording...");
+  info("Recording 1 - Select first recording.");
   recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield recordingSelected;
-  testRecordings(PerformanceController, [C + R + S, 0, C + R, C]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING + SELECTED,
+    MANUAL,
+    CONSOLE + RECORDING,
+    CONSOLE
+  ]);
   ok(OverviewView.isRendering(),
     "Should be rendering overview a recording in progress is selected.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
@@ -117,75 +177,92 @@ add_task(function* () {
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
     // the view state won't switch to "recorded" unless the new
     // finished recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profileEnd();
   yield stopped;
-  testRecordings(PerformanceController, [C + R + S, 0, C, C]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING + SELECTED,
+    MANUAL,
+    CONSOLE,
+    CONSOLE
+  ]);
   ok(OverviewView.isRendering(),
     "Should be rendering overview a recording in progress is selected.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
-  info("Start one more manual recording...");
+  info("Recording 5 - Start one more manual recording.");
   yield startRecording(panel);
-  testRecordings(PerformanceController, [C + R, 0, C, C, R + S]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL,
+    CONSOLE,
+    CONSOLE,
+    MANUAL + RECORDING + SELECTED
+  ]);
   ok(OverviewView.isRendering(),
     "Should be rendering overview a recording in progress is selected.");
 
   // Ensure overview is still rendering.
   yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
     expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
   });
 
-  info("Stop manual recording...");
+  info("Recording 5 - Stop manual recording.");
   yield stopRecording(panel);
-  testRecordings(PerformanceController, [C + R, 0, C, C, S]);
+  testRecordings(PerformanceController, [
+    CONSOLE + RECORDING,
+    MANUAL,
+    CONSOLE,
+    CONSOLE,
+    MANUAL + SELECTED
+  ]);
   ok(!OverviewView.isRendering(),
   "Stop rendering overview when a completed recording is selected.");
 
-  info("Ending console.profileEnd()...");
+  info("Recording 1 - Ending console.profileEnd()...");
   stopped = waitForRecordingStoppedEvents(panel, {
     // only emitted for manual recordings
     skipWaitingForBackendReady: true,
     // only emitted when a finished recording is selected
     skipWaitingForOverview: true,
     skipWaitingForSubview: true,
     // the view state won't switch to "recorded" unless the new
     // in-progress recording is selected, which won't happen
     skipWaitingForViewState: true,
   });
   yield console.profileEnd();
   yield stopped;
-  testRecordings(PerformanceController, [C, 0, C, C, S]);
+  testRecordings(PerformanceController, [
+    CONSOLE,
+    MANUAL,
+    CONSOLE,
+    CONSOLE,
+    MANUAL + SELECTED
+  ]);
   ok(!OverviewView.isRendering(),
     "Stop rendering overview when a completed recording is selected.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
 
-// is console
-const C = 1;
-// is recording
-const R = 2;
-// is selected
-const S = 4;
-
-function testRecordings(controller, expected) {
+function testRecordings(controller, expectedBitFlags) {
   let recordings = controller.getRecordings();
   let current = controller.getCurrentRecording();
-  is(recordings.length, expected.length, "Expected number of recordings.");
+  is(recordings.length, expectedBitFlags.length, "Expected number of recordings.");
 
   recordings.forEach((recording, i) => {
-    ok(recording.isConsole() == !!(expected[i] & C),
+    const expected = expectedBitFlags[i];
+    is(recording.isConsole(), hasBitFlag(expected, CONSOLE),
       `Recording ${i + 1} has expected console state.`);
-    ok(recording.isRecording() == !!(expected[i] & R),
+    is(recording.isRecording(), hasBitFlag(expected, RECORDING),
       `Recording ${i + 1} has expected console state.`);
-    ok((recording == current) == !!(expected[i] & S),
+    is((recording == current), hasBitFlag(expected, SELECTED),
       `Recording ${i + 1} has expected selected state.`);
   });
 }
--- a/devtools/client/performance/test/browser_perf-details-03-without-allocations.js
+++ b/devtools/client/performance/test/browser_perf-details-03-without-allocations.js
@@ -9,27 +9,27 @@
  * to a default panel instead.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let {
     EVENTS,
     $,
-    RecordingsView,
     DetailsView,
     WaterfallView,
     MemoryCallTreeView,
     MemoryFlameGraphView
   } = panel.panelWin;
 
   let flameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
   let callBtn = $("toolbarbutton[data-view='memory-calltree']");
@@ -75,31 +75,31 @@ add_task(function* () {
   yield rendered;
 
   ok(DetailsView.isViewSelected(MemoryFlameGraphView),
     "The memory flamegraph view can now be selected.");
 
   // Select the first recording with no memory data.
   selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
   rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
   yield rendered;
 
   ok(DetailsView.isViewSelected(WaterfallView), "The waterfall view is now selected " +
     "when switching back to a recording that does not have memory data.");
 
   is(callBtn.hidden, true,
     "The `memory-calltree` button is hidden when recording has no memory data.");
   is(flameBtn.hidden, true,
     "The `memory-flamegraph` button is hidden when recording has no memory data.");
 
   // Go back to the recording with memory data.
   rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield rendered;
 
   ok(DetailsView.isViewSelected(WaterfallView),
     "The waterfall view is still selected in the details view.");
 
   is(callBtn.hidden, false,
     "The `memory-calltree` button is shown when recording has memory data.");
   is(flameBtn.hidden, false,
--- a/devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js
+++ b/devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js
@@ -6,28 +6,28 @@
  * Tests that the details view hides the toolbar buttons when a recording
  * doesn't exist or is in progress.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording, getSelectedRecordingIndex } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let {
     EVENTS,
     $,
     PerformanceController,
-    RecordingsView,
     WaterfallView
   } = panel.panelWin;
 
   let waterfallBtn = $("toolbarbutton[data-view='waterfall']");
   let jsFlameBtn = $("toolbarbutton[data-view='js-flamegraph']");
   let jsCallBtn = $("toolbarbutton[data-view='js-calltree']");
   let memFlameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
   let memCallBtn = $("toolbarbutton[data-view='memory-calltree']");
@@ -79,39 +79,41 @@ add_task(function* () {
     "The `js-calltree` button is hidden when another recording starts.");
   is(memFlameBtn.hidden, true,
     "The `memory-flamegraph` button is hidden when another recording starts.");
   is(memCallBtn.hidden, true,
     "The `memory-calltree` button is hidden when another recording starts.");
 
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
   let rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
   yield rendered;
 
-  is(RecordingsView.selectedIndex, 0,
+  let selectedIndex = getSelectedRecordingIndex(panel);
+  is(selectedIndex, 0,
     "The first recording was selected again.");
 
   is(waterfallBtn.hidden, false,
     "The `waterfall` button is visible when first recording selected.");
   is(jsFlameBtn.hidden, false,
     "The `js-flamegraph` button is visible when first recording selected.");
   is(jsCallBtn.hidden, false,
     "The `js-calltree` button is visible when first recording selected.");
   is(memFlameBtn.hidden, true,
     "The `memory-flamegraph` button is hidden when first recording selected.");
   is(memCallBtn.hidden, true,
     "The `memory-calltree` button is hidden when first recording selected.");
 
   selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield selected;
 
-  is(RecordingsView.selectedIndex, 1,
+  selectedIndex = getSelectedRecordingIndex(panel);
+  is(selectedIndex, 1,
     "The second recording was selected again.");
 
   is(waterfallBtn.hidden, true,
     "The `waterfall button` still is hidden when second recording selected.");
   is(jsFlameBtn.hidden, true,
     "The `js-flamegraph button` still is hidden when second recording selected.");
   is(jsCallBtn.hidden, true,
     "The `js-calltree button` still is hidden when second recording selected.");
@@ -119,17 +121,18 @@ add_task(function* () {
     "The `memory-flamegraph button` still is hidden when second recording selected.");
   is(memCallBtn.hidden, true,
     "The `memory-calltree button` still is hidden when second recording selected.");
 
   rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
   yield stopRecording(panel);
   yield rendered;
 
-  is(RecordingsView.selectedIndex, 1,
+  selectedIndex = getSelectedRecordingIndex(panel);
+  is(selectedIndex, 1,
     "The second recording is still selected.");
 
   is(waterfallBtn.hidden, false,
     "The `waterfall` button is visible when second recording finished.");
   is(jsFlameBtn.hidden, false,
     "The `js-flamegraph` button is visible when second recording finished.");
   is(jsCallBtn.hidden, false,
     "The `js-calltree` button is visible when second recording finished.");
--- a/devtools/client/performance/test/browser_perf-loading-01.js
+++ b/devtools/client/performance/test/browser_perf-loading-01.js
@@ -6,46 +6,47 @@
  * Tests that the recordings view shows the right label while recording, after
  * recording, and once the record has loaded.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecording, getDurationLabelText } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { EVENTS, L10N, $, PerformanceController, RecordingsView } = panel.panelWin;
+  let { EVENTS, L10N, PerformanceController } = panel.panelWin;
 
   yield startRecording(panel);
 
-  let durationLabel = $(".recording-item-duration", RecordingsView.selectedItem.target);
-  is(durationLabel.getAttribute("value"),
+  is(getDurationLabelText(panel, 0),
     L10N.getStr("recordingsList.recordingLabel"),
     "The duration node should show the 'recording' message while recording");
 
   let recordingStopping = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
     expectedArgs: { "1": "recording-stopping" }
   });
   let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
     expectedArgs: { "1": "recording-stopped" }
   });
   let everythingStopped = stopRecording(panel);
 
   yield recordingStopping;
-  is(durationLabel.getAttribute("value"),
+  is(getDurationLabelText(panel, 0),
     L10N.getStr("recordingsList.loadingLabel"),
     "The duration node should show the 'loading' message while stopping");
 
   yield recordingStopped;
-  is(durationLabel.getAttribute("value"),
+  const selected = getSelectedRecording(panel);
+  is(getDurationLabelText(panel, 0),
     L10N.getFormatStr("recordingsList.durationLabel",
-    RecordingsView.selectedItem.attachment.getDuration().toFixed(0)),
+    selected.getDuration().toFixed(0)),
     "The duration node should show the duration after the record has stopped");
 
   yield everythingStopped;
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-loading-02.js
+++ b/devtools/client/performance/test/browser_perf-loading-02.js
@@ -8,24 +8,25 @@
  * Also test that the details view isn't locked if the recording that is being
  * stopped isn't the active one.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecordingIndex, setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { EVENTS, $, PerformanceController, RecordingsView } = panel.panelWin;
+  let { EVENTS, $, PerformanceController } = panel.panelWin;
   let detailsContainer = $("#details-pane-container");
   let recordingNotice = $("#recording-notice");
   let loadingNotice = $("#loading-notice");
   let detailsPane = $("#details-pane");
 
   yield startRecording(panel);
 
   is(detailsContainer.selectedPanel, recordingNotice,
@@ -47,35 +48,35 @@ add_task(function* () {
   is(detailsContainer.selectedPanel, detailsPane,
     "The details panel is shown after the record has stopped.");
 
   yield everythingStopped;
   yield startRecording(panel);
 
   info("While the 2nd record is still going, switch to the first one.");
   let recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield recordingSelected;
 
   recordingStopping = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
     expectedArgs: { "1": "recording-stopping" }
   });
   recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
     expectedArgs: { "1": "recording-stopped" }
   });
   everythingStopped = stopRecording(panel);
 
   yield recordingStopping;
   is(detailsContainer.selectedPanel, detailsPane,
     "The details panel is still shown while the 2nd record is being stopped.");
-  is(RecordingsView.selectedIndex, 0,
+  is(getSelectedRecordingIndex(panel), 0,
     "The first record is still selected.");
 
   yield recordingStopped;
 
   is(detailsContainer.selectedPanel, detailsPane,
     "The details panel is still shown after the 2nd record has stopped.");
-  is(RecordingsView.selectedIndex, 1,
+  is(getSelectedRecordingIndex(panel), 1,
     "The second record is now selected.");
 
   yield everythingStopped;
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js
+++ b/devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js
@@ -4,23 +4,23 @@
 /* eslint-disable */
 // Bug 1235788, increase time out of this test
 requestLongerTimeout(2);
 
 /**
  * Tests that the JIT Optimizations view renders optimization data
  * if on, and displays selected frames on focus.
  */
-
+ const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 Services.prefs.setBoolPref(INVERT_PREF, false);
 
 function* spawnTest() {
   let { panel } = yield initPerformance(SIMPLE_URL);
   let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
-  let { OverviewView, DetailsView, OptimizationsListView, JsCallTreeView, RecordingsView } = panel.panelWin;
+  let { OverviewView, DetailsView, OptimizationsListView, JsCallTreeView } = panel.panelWin;
 
   let profilerData = { threads: [gThread] };
 
   is(Services.prefs.getBoolPref(JIT_PREF), false, "record JIT Optimizations pref off by default");
   Services.prefs.setBoolPref(JIT_PREF, true);
   is(Services.prefs.getBoolPref(JIT_PREF), true, "toggle on record JIT Optimizations");
 
   // Make two recordings, so we have one to switch to later, as the
@@ -53,24 +53,24 @@ function* spawnTest() {
   let rendered = once(JsCallTreeView, "focus");
   mousedown(window, $$(".call-tree-item")[2]);
   yield rendered;
   let isHidden = $("#jit-optimizations-view").classList.contains("hidden");
   ok(!isHidden, "opts view should be visible when selecting a frame with opts");
 
   let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
   rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield Promise.all([select, rendered]);
 
   isHidden = $("#jit-optimizations-view").classList.contains("hidden");
   ok(isHidden, "opts view is hidden when switching recordings");
 
   rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield rendered;
 
   rendered = once(JsCallTreeView, "focus");
   mousedown(window, $$(".call-tree-item")[2]);
   yield rendered;
   isHidden = $("#jit-optimizations-view").classList.contains("hidden");
   ok(!isHidden, "opts view should be visible when selecting a frame with opts");
 
--- a/devtools/client/performance/test/browser_perf-overview-render-04.js
+++ b/devtools/client/performance/test/browser_perf-overview-render-04.js
@@ -8,24 +8,25 @@
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { UI_ENABLE_MEMORY_PREF } = require("devtools/client/performance/test/helpers/prefs");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
 const { isVisible } = require("devtools/client/performance/test/helpers/dom-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { $, EVENTS, PerformanceController, RecordingsView, OverviewView } = panel.panelWin;
+  let { $, EVENTS, PerformanceController, OverviewView } = panel.panelWin;
 
   // Enable memory to test.
   Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
 
   // Set realtime rendering off.
   OverviewView.isRealtimeRenderingEnabled = () => false;
 
   let updated = 0;
@@ -48,22 +49,22 @@ add_task(function* () {
   is(updated, 1, "Overview graphs rendered upon completion.");
 
   yield startRecording(panel, { skipWaitingForOverview: true });
 
   is(isVisible($("#overview-pane")), false,
      "Overview graphs hidden again when starting new recording.");
   is(updated, 1, "Overview graphs have not been updated again.");
 
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   is(isVisible($("#overview-pane")), true,
      "Overview graphs no longer hidden when switching back to complete recording.");
   is(updated, 1, "Overview graphs have not been updated again.");
 
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   is(isVisible($("#overview-pane")), false,
      "Overview graphs hidden again when going back to inprogress recording.");
   is(updated, 1, "Overview graphs have not been updated again.");
 
   yield stopRecording(panel);
 
   is(isVisible($("#overview-pane")), true,
      "overview graphs no longer hidden when recording finishes");
--- a/devtools/client/performance/test/browser_perf-recording-notices-02.js
+++ b/devtools/client/performance/test/browser_perf-recording-notices-02.js
@@ -6,29 +6,29 @@
  * Tests that the recording notice panes are toggled when going between
  * a completed recording and an in-progress recording.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
   let {
     EVENTS,
     $,
     PerformanceController,
     PerformanceView,
-    RecordingsView
   } = panel.panelWin;
 
   let MAIN_CONTAINER = $("#performance-view");
   let CONTENT = $("#performance-view-content");
   let DETAILS_CONTAINER = $("#details-pane-container");
   let RECORDING = $("#recording-notice");
   let DETAILS = $("#details-pane");
 
@@ -37,26 +37,26 @@ add_task(function* () {
 
   yield startRecording(panel);
 
   is(PerformanceView.getState(), "recording", "Correct state during recording.");
   is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
   is(DETAILS_CONTAINER.selectedPanel, RECORDING, "Showing recording panel.");
 
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
 
   is(PerformanceView.getState(), "recorded",
      "Correct state during recording but selecting a completed recording.");
   is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
   is(DETAILS_CONTAINER.selectedPanel, DETAILS, "Showing recorded panel.");
 
   selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield selected;
 
   is(PerformanceView.getState(), "recording",
      "Correct state when switching back to recording in progress.");
   is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
   is(DETAILS_CONTAINER.selectedPanel, RECORDING, "Showing recording panel.");
 
   yield stopRecording(panel);
--- a/devtools/client/performance/test/browser_perf-recording-notices-03.js
+++ b/devtools/client/performance/test/browser_perf-recording-notices-03.js
@@ -10,16 +10,17 @@
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { PROFILER_BUFFER_SIZE_PREF } = require("devtools/client/performance/test/helpers/prefs");
 const { pmmLoadFrameScripts, pmmStopProfiler, pmmClearFrameScripts } = require("devtools/client/performance/test/helpers/profiler-mm-utils");
 const { initPerformanceInTab, initConsoleInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { waitUntil } = require("devtools/client/performance/test/helpers/wait-utils");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   // Make sure the profiler module is stopped so we can set a new buffer limit.
   pmmLoadFrameScripts(gBrowser);
   yield pmmStopProfiler();
 
   // Keep the profiler's buffer large, but still get to 1% relatively quick.
   Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 1000000);
@@ -31,17 +32,16 @@ add_task(function* () {
 
   let { panel } = yield initPerformanceInTab({ tab: target.tab });
   let {
     gFront,
     EVENTS,
     $,
     PerformanceController,
     PerformanceView,
-    RecordingsView
   } = panel.panelWin;
 
   // Set a fast profiler-status update interval.
   yield gFront.setProfilerStatusInterval(10);
 
   let DETAILS_CONTAINER = $("#details-pane-container");
   let NORMAL_BUFFER_STATUS_MESSAGE = $("#recording-notice .buffer-status-message");
   let CONSOLE_BUFFER_STATUS_MESSAGE =
@@ -83,17 +83,17 @@ add_task(function* () {
     PerformanceController.getCurrentRecording());
   either(DETAILS_CONTAINER.getAttribute("buffer-status"), "in-progress", "full",
     "Container has [buffer-status=in-progress] or [buffer-status=full].");
   ok(NORMAL_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
     "Buffer status text has correct percentage.");
 
   // Select the console recording.
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield selected;
 
   yield waitUntil(function* () {
     [, gPercent] = yield once(PerformanceView,
                               EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
                               { spreadArgs: true });
     return gPercent > 0;
   });
@@ -104,17 +104,17 @@ add_task(function* () {
     "Container has [buffer-status=in-progress] or [buffer-status=full].");
   ok(CONSOLE_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
     "Buffer status text has correct percentage for console recording.");
 
   // Stop the console profile, then select the original manual recording.
   yield console.profileEnd("rust");
 
   selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
 
   yield waitUntil(function* () {
     [, gPercent] = yield once(PerformanceView,
                               EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
                               { spreadArgs: true });
     return gPercent > Math.floor(bufferUsage * 100);
   });
--- a/devtools/client/performance/test/browser_perf-recording-selected-01.js
+++ b/devtools/client/performance/test/browser_perf-recording-selected-01.js
@@ -6,39 +6,40 @@
  * Tests if the profiler correctly handles multiple recordings and can
  * successfully switch between them.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording, getRecordingsCount, getSelectedRecordingIndex } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController } = panel.panelWin;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
-  is(RecordingsView.itemCount, 2,
+  is(getRecordingsCount(panel), 2,
     "There should be two recordings visible.");
-  is(RecordingsView.selectedIndex, 1,
+  is(getSelectedRecordingIndex(panel), 1,
     "The second recording item should be selected.");
 
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
 
-  is(RecordingsView.itemCount, 2,
+  is(getRecordingsCount(panel), 2,
     "There should still be two recordings visible.");
-  is(RecordingsView.selectedIndex, 0,
+  is(getSelectedRecordingIndex(panel), 0,
     "The first recording item should be selected.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-recording-selected-02.js
+++ b/devtools/client/performance/test/browser_perf-recording-selected-02.js
@@ -6,53 +6,53 @@
  * Tests if the profiler correctly handles multiple recordings and can
  * successfully switch between them, even when one of them is in progress.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getSelectedRecordingIndex, setSelectedRecording, getRecordingsCount } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   // This test seems to take a very long time to finish on Linux VMs.
   requestLongerTimeout(4);
 
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController } = panel.panelWin;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield startRecording(panel);
 
-  is(RecordingsView.itemCount, 2,
+  is(getRecordingsCount(panel), 2,
     "There should be two recordings visible.");
-  is(RecordingsView.selectedIndex, 1,
+  is(getSelectedRecordingIndex(panel), 1,
     "The new recording item should be selected.");
 
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
 
-  is(RecordingsView.itemCount, 2,
+  is(getRecordingsCount(panel), 2,
     "There should still be two recordings visible.");
-  is(RecordingsView.selectedIndex, 0,
+  is(getSelectedRecordingIndex(panel), 0,
     "The first recording item should be selected now.");
 
   selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield selected;
 
-  is(RecordingsView.itemCount, 2,
+  is(getRecordingsCount(panel), 2,
     "There should still be two recordings visible.");
-  is(RecordingsView.selectedIndex, 1,
+  is(getSelectedRecordingIndex(panel), 1,
     "The second recording item should be selected again.");
 
   yield stopRecording(panel);
 
   yield teardownToolboxAndRemoveTab(panel);
 });
-
--- a/devtools/client/performance/test/browser_perf-recording-selected-03.js
+++ b/devtools/client/performance/test/browser_perf-recording-selected-03.js
@@ -7,34 +7,35 @@
  * Tests if the profiler UI does not forget that recording is active when
  * selected recording changes.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { once } = require("devtools/client/performance/test/helpers/event-utils");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { $, EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
+  let { $, EVENTS, PerformanceController } = panel.panelWin;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   yield startRecording(panel);
 
   info("Selecting recording #0 and waiting for it to be displayed.");
 
   let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield selected;
 
   ok($("#main-record-button").classList.contains("checked"),
     "Button is still checked after selecting another item.");
   ok(!$("#main-record-button").hasAttribute("disabled"),
     "Button is not locked after selecting another item.");
 
   yield stopRecording(panel);
--- a/devtools/client/performance/test/browser_perf-recording-selected-04.js
+++ b/devtools/client/performance/test/browser_perf-recording-selected-04.js
@@ -5,24 +5,25 @@
 /**
  * Tests that all components can get rerendered for a profile when switching.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { UI_ENABLE_MEMORY_PREF, UI_ENABLE_ALLOCATIONS_PREF } = require("devtools/client/performance/test/helpers/prefs");
 const { initPerformanceInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording, waitForAllWidgetsRendered } = require("devtools/client/performance/test/helpers/actions");
+const { setSelectedRecording } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPerformanceInNewTab({
     url: SIMPLE_URL,
     win: window
   });
 
-  let { DetailsView, DetailsSubview, RecordingsView } = panel.panelWin;
+  let { DetailsView, DetailsSubview } = panel.panelWin;
 
   // Enable memory to test the memory overview.
   Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
 
   // Enable allocations to test the memory-calltree and memory-flamegraph.
   Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
 
   yield startRecording(panel);
@@ -38,21 +39,21 @@ add_task(function* () {
   yield DetailsView.selectView("js-flamegraph");
   yield DetailsView.selectView("memory-calltree");
   yield DetailsView.selectView("memory-flamegraph");
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
   let rerender = waitForAllWidgetsRendered(panel);
-  RecordingsView.selectedIndex = 0;
+  setSelectedRecording(panel, 0);
   yield rerender;
 
   ok(true, "All widgets were rendered when selecting the first recording.");
 
   rerender = waitForAllWidgetsRendered(panel);
-  RecordingsView.selectedIndex = 1;
+  setSelectedRecording(panel, 1);
   yield rerender;
 
   ok(true, "All widgets were rendered when selecting the second recording.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-recordings-clear-01.js
+++ b/devtools/client/performance/test/browser_perf-recordings-clear-01.js
@@ -5,49 +5,50 @@
 /**
  * Tests that clearing recordings empties out the recordings list and toggles
  * the empty notice state.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPanelInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
+const { getRecordingsCount } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPanelInNewTab({
     tool: "performance",
     url: SIMPLE_URL,
     win: window
   });
 
-  let { PerformanceController, PerformanceView, RecordingsView } = panel.panelWin;
+  let { PerformanceController, PerformanceView } = panel.panelWin;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
-  is(RecordingsView.itemCount, 1,
-    "RecordingsView should have one recording.");
+  is(getRecordingsCount(panel), 1,
+    "The recordings list should have one recording.");
   isnot(PerformanceView.getState(), "empty",
     "PerformanceView should not be in an empty state.");
   isnot(PerformanceController.getCurrentRecording(), null,
     "There should be a current recording.");
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
-  is(RecordingsView.itemCount, 2,
-    "RecordingsView should have two recordings.");
+  is(getRecordingsCount(panel), 2,
+    "The recordings list should have two recordings.");
   isnot(PerformanceView.getState(), "empty",
     "PerformanceView should not be in an empty state.");
   isnot(PerformanceController.getCurrentRecording(), null,
     "There should be a current recording.");
 
   yield PerformanceController.clearRecordings();
 
-  is(RecordingsView.itemCount, 0,
-    "RecordingsView should be empty.");
+  is(getRecordingsCount(panel), 0,
+    "The recordings list should be empty.");
   is(PerformanceView.getState(), "empty",
     "PerformanceView should be in an empty state.");
   is(PerformanceController.getCurrentRecording(), null,
     "There should be no current recording.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-recordings-clear-02.js
+++ b/devtools/client/performance/test/browser_perf-recordings-clear-02.js
@@ -6,63 +6,64 @@
  * Tests that clearing recordings empties out the recordings list and stops
  * a current recording if recording and can continue recording after.
  */
 
 const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
 const { initPanelInNewTab, teardownToolboxAndRemoveTab } = require("devtools/client/performance/test/helpers/panel-utils");
 const { startRecording, stopRecording } = require("devtools/client/performance/test/helpers/actions");
 const { times, once } = require("devtools/client/performance/test/helpers/event-utils");
+const { getRecordingsCount } = require("devtools/client/performance/test/helpers/recording-utils");
 
 add_task(function* () {
   let { panel } = yield initPanelInNewTab({
     tool: "performance",
     url: SIMPLE_URL,
     win: window
   });
 
-  let { EVENTS, PerformanceController, PerformanceView, RecordingsView } = panel.panelWin;
+  let { EVENTS, PerformanceController, PerformanceView } = panel.panelWin;
 
   yield startRecording(panel);
   yield stopRecording(panel);
 
-  is(RecordingsView.itemCount, 1,
-    "RecordingsView should have one recording.");
+  is(getRecordingsCount(panel), 1,
+    "The recordings list should have one recording.");
   isnot(PerformanceView.getState(), "empty",
     "PerformanceView should not be in an empty state.");
   isnot(PerformanceController.getCurrentRecording(), null,
     "There should be a current recording.");
 
   yield startRecording(panel);
 
-  is(RecordingsView.itemCount, 2,
-    "RecordingsView should have two recordings.");
+  is(getRecordingsCount(panel), 2,
+    "The recordings list should have two recordings.");
   isnot(PerformanceView.getState(), "empty",
     "PerformanceView should not be in an empty state.");
   isnot(PerformanceController.getCurrentRecording(), null,
     "There should be a current recording.");
 
   let recordingDeleted = times(PerformanceController, EVENTS.RECORDING_DELETED, 2);
   let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
     expectedArgs: { "1": "recording-stopped" }
   });
 
   PerformanceController.clearRecordings();
 
   yield recordingDeleted;
   yield recordingStopped;
 
-  is(RecordingsView.itemCount, 0,
-    "RecordingsView should be empty.");
+  is(getRecordingsCount(panel), 0,
+    "The recordings list should be empty.");
   is(PerformanceView.getState(), "empty",
     "PerformanceView should be in an empty state.");
   is(PerformanceController.getCurrentRecording(), null,
     "There should be no current recording.");
 
   // Bug 1169146: Try another recording after clearing mid-recording.
   yield startRecording(panel);
   yield stopRecording(panel);
 
-  is(RecordingsView.itemCount, 1,
-    "RecordingsView should have one recording.");
+  is(getRecordingsCount(panel), 1,
+    "The recordings list should have one recording.");
 
   yield teardownToolboxAndRemoveTab(panel);
 });
--- a/devtools/client/performance/test/browser_perf-tree-view-11.js
+++ b/devtools/client/performance/test/browser_perf-tree-view-11.js
@@ -7,17 +7,17 @@
  * icon is next to the frame with optimizations
  */
 
 var { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
 
 function* spawnTest() {
   let { panel } = yield initPerformance(SIMPLE_URL);
   let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
-  let { OverviewView, DetailsView, JsCallTreeView, RecordingsView } = panel.panelWin;
+  let { OverviewView, DetailsView, JsCallTreeView } = panel.panelWin;
 
   let profilerData = { threads: [gThread] };
 
   Services.prefs.setBoolPref(JIT_PREF, true);
   Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);
   Services.prefs.setBoolPref(INVERT_PREF, false);
 
   // Make two recordings, so we have one to switch to later, as the
--- a/devtools/client/performance/test/helpers/moz.build
+++ b/devtools/client/performance/test/helpers/moz.build
@@ -7,13 +7,14 @@
 DevToolsModules(
     'actions.js',
     'dom-utils.js',
     'event-utils.js',
     'input-utils.js',
     'panel-utils.js',
     'prefs.js',
     'profiler-mm-utils.js',
+    'recording-utils.js',
     'synth-utils.js',
     'tab-utils.js',
     'urls.js',
     'wait-utils.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/test/helpers/recording-utils.js
@@ -0,0 +1,54 @@
+/* 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";
+
+/**
+ * These utilities provide a functional interface for accessing the particulars
+ * about the recording's details.
+ */
+
+/**
+ * Access the selected view from the panel's recording list.
+ *
+ * @param {object} panel - The current panel.
+ * @return {object} The recording model.
+ */
+exports.getSelectedRecording = function (panel) {
+  const view = panel.panelWin.RecordingsView;
+  return view.selected;
+};
+
+/**
+ * Set the selected index of the recording via the panel.
+ *
+ * @param {object} panel - The current panel.
+ * @return {number} index
+ */
+exports.setSelectedRecording = function (panel, index) {
+  const view = panel.panelWin.RecordingsView;
+  view.setSelectedByIndex(index);
+  return index;
+};
+
+/**
+ * Access the selected view from the panel's recording list.
+ *
+ * @param {object} panel - The current panel.
+ * @return {number} index
+ */
+exports.getSelectedRecordingIndex = function (panel) {
+  const view = panel.panelWin.RecordingsView;
+  return view.getSelectedIndex();
+};
+
+exports.getDurationLabelText = function (panel, elementIndex) {
+  const { $$ } = panel.panelWin;
+  const elements = $$(".recording-list-item-duration", panel.panelWin.document);
+  return elements[elementIndex].innerHTML;
+};
+
+exports.getRecordingsCount = function (panel) {
+  const { $$ } = panel.panelWin;
+  return $$(".recording-list-item", panel.panelWin.document).length;
+};
--- a/devtools/client/performance/views/recordings.js
+++ b/devtools/client/performance/views/recordings.js
@@ -4,98 +4,108 @@
 /* import-globals-from ../performance-controller.js */
 /* import-globals-from ../performance-view.js */
 /* globals document, window */
 "use strict";
 
 /**
  * Functions handling the recordings UI.
  */
-var RecordingsView = Heritage.extend(WidgetMethods, {
+var RecordingsView = {
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: function () {
-    this.widget = new SideMenuWidget($("#recordings-list"));
-
     this._onSelect = this._onSelect.bind(this);
     this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
     this._onNewRecording = this._onNewRecording.bind(this);
     this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
     this._onRecordingDeleted = this._onRecordingDeleted.bind(this);
     this._onRecordingExported = this._onRecordingExported.bind(this);
 
-    this.emptyText = L10N.getStr("noRecordingsText");
-
     PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange);
     PerformanceController.on(EVENTS.RECORDING_ADDED, this._onNewRecording);
     PerformanceController.on(EVENTS.RECORDING_DELETED, this._onRecordingDeleted);
     PerformanceController.on(EVENTS.RECORDING_EXPORTED, this._onRecordingExported);
-    this.widget.addEventListener("select", this._onSelect, false);
+
+    // DE-XUL: Begin migrating the recording sidebar to React. Temporarily hold state
+    // here.
+    this._listState = {
+      recordings: [],
+      labels: new WeakMap(),
+      selected: null,
+    };
+    this._listMount = PerformanceUtils.createHtmlMount($("#recording-list-mount"));
+    this._renderList();
+  },
+
+  /**
+   * Get the index of the currently selected recording. Only used by tests.
+   * @return {integer} index
+   */
+  getSelectedIndex() {
+    const { recordings, selected } = this._listState;
+    return recordings.indexOf(selected);
+  },
+
+  /**
+   * Set the currently selected recording via its index. Only used by tests.
+   * @param {integer} index
+   */
+  setSelectedByIndex(index) {
+    this._onSelect(this._listState.recordings[index]);
+    this._renderList();
+  },
+
+  /**
+   * DE-XUL: During the migration, this getter will access the selected recording from
+   * the private _listState object so that tests will continue to pass.
+   */
+  get selected() {
+    return this._listState.selected;
+  },
+
+  /**
+   * DE-XUL: During the migration, this getter will access the number of recordings.
+   */
+  get itemCount() {
+    return this._listState.recordings.length;
+  },
+
+  /**
+   * DE-XUL: Render the recording list using React.
+   */
+  _renderList: function () {
+    const {recordings, labels, selected} = this._listState;
+
+    const recordingList = RecordingList({
+      itemComponent: RecordingListItem,
+      items: recordings.map(recording => ({
+        onSelect: () => this._onSelect(recording),
+        onSave: () => this._onSaveButtonClick(recording),
+        isLoading: !recording.isRecording() && !recording.isCompleted(),
+        isRecording: recording.isRecording(),
+        isSelected: recording === selected,
+        duration: recording.getDuration().toFixed(0),
+        label: labels.get(recording),
+      }))
+    });
+
+    ReactDOM.render(recordingList, this._listMount);
   },
 
   /**
    * Destruction function, called when the tool is closed.
    */
   destroy: function () {
     PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE,
                               this._onRecordingStateChange);
     PerformanceController.off(EVENTS.RECORDING_ADDED, this._onNewRecording);
     PerformanceController.off(EVENTS.RECORDING_DELETED, this._onRecordingDeleted);
     PerformanceController.off(EVENTS.RECORDING_EXPORTED, this._onRecordingExported);
-    this.widget.removeEventListener("select", this._onSelect, false);
-  },
-
-  /**
-   * Adds an empty recording to this container.
-   *
-   * @param RecordingModel recording
-   *        A model for the new recording item created.
-   */
-  addEmptyRecording: function (recording) {
-    let titleNode = document.createElement("label");
-    titleNode.className = "plain recording-item-title";
-    titleNode.setAttribute("crop", "end");
-    titleNode.setAttribute("value", recording.getLabel() ||
-      L10N.getFormatStr("recordingsList.itemLabel", this.itemCount + 1));
-
-    let durationNode = document.createElement("label");
-    durationNode.className = "plain recording-item-duration";
-    durationNode.setAttribute("value",
-      L10N.getStr("recordingsList.recordingLabel"));
-
-    let saveNode = document.createElement("label");
-    saveNode.className = "plain recording-item-save";
-    saveNode.addEventListener("click", this._onSaveButtonClick);
-
-    let hspacer = document.createElement("spacer");
-    hspacer.setAttribute("flex", "1");
-
-    let footerNode = document.createElement("hbox");
-    footerNode.className = "recording-item-footer";
-    footerNode.appendChild(durationNode);
-    footerNode.appendChild(hspacer);
-    footerNode.appendChild(saveNode);
-
-    let vspacer = document.createElement("spacer");
-    vspacer.setAttribute("flex", "1");
-
-    let contentsNode = document.createElement("vbox");
-    contentsNode.className = "recording-item";
-    contentsNode.setAttribute("flex", "1");
-    contentsNode.appendChild(titleNode);
-    contentsNode.appendChild(vspacer);
-    contentsNode.appendChild(footerNode);
-
-    // Append a recording item to this container.
-    return this.push([contentsNode], {
-      // Store the recording model that contains all the data to be
-      // rendered in the item.
-      attachment: recording
-    });
   },
 
   /**
    * Called when a new recording is stored in the UI. This handles
    * when recordings are lazily loaded (like a console.profile occurring
    * before the tool is loaded) or imported. In normal manual recording cases,
    * this will also be fired.
    */
@@ -107,119 +117,86 @@ var RecordingsView = Heritage.extend(Wid
    * Signals that a recording has changed state.
    *
    * @param string state
    *        Can be "recording-started", "recording-stopped", "recording-stopping"
    * @param RecordingModel recording
    *        Model of the recording that was started.
    */
   _onRecordingStateChange: function (_, state, recording) {
-    let recordingItem = this.getItemForPredicate(e => e.attachment === recording);
-    if (!recordingItem) {
-      recordingItem = this.addEmptyRecording(recording);
+    const { recordings, labels } = this._listState;
+
+    if (!recordings.includes(recording)) {
+      recordings.push(recording);
+      labels.set(recording, recording.getLabel() ||
+        L10N.getFormatStr("recordingsList.itemLabel", recordings.length));
 
       // If this is a manual recording, immediately select it, or
       // select a console profile if its the only one
-      if (!recording.isConsole() || this.selectedIndex === -1) {
-        this.selectedItem = recordingItem;
+      if (!recording.isConsole() || !this._listState.selected) {
+        this._onSelect(recording);
       }
     }
 
-    recordingItem.isRecording = recording.isRecording();
-
-    // This recording is in the process of stopping.
-    if (!recording.isRecording() && !recording.isCompleted()) {
-      // Mark the corresponding item as loading.
-      let durationNode = $(".recording-item-duration", recordingItem.target);
-      durationNode.setAttribute("value", L10N.getStr("recordingsList.loadingLabel"));
+    // Determine if the recording needs to be selected.
+    const isCompletedManualRecording = !recording.isConsole() && recording.isCompleted();
+    if (recording.isImported() || isCompletedManualRecording) {
+      this._onSelect(recording);
     }
 
-    // Render the recording item with finalized information (timing, etc)
-    if (recording.isCompleted() && !recordingItem.finalized) {
-      this.finalizeRecording(recordingItem);
-      // Select the recording if it was a manual recording only
-      if (!recording.isConsole()) {
-        this.forceSelect(recordingItem);
-      }
-    }
-
-    // Auto select imported items.
-    if (recording.isImported()) {
-      this.selectedItem = recordingItem;
-    }
+    this._renderList();
   },
 
   /**
    * Clears out all non-console recordings.
    */
   _onRecordingDeleted: function (_, recording) {
-    let recordingItem = this.getItemForPredicate(e => e.attachment === recording);
-    this.remove(recordingItem);
-  },
-
-  /**
-   * Adds recording data to a recording item in this container.
-   *
-   * @param Item recordingItem
-   *        An item inserted via `RecordingsView.addEmptyRecording`.
-   */
-  finalizeRecording: function (recordingItem) {
-    let model = recordingItem.attachment;
-    recordingItem.finalized = true;
-
-    let saveNode = $(".recording-item-save", recordingItem.target);
-    saveNode.setAttribute("value",
-      L10N.getStr("recordingsList.saveLabel"));
-
-    let durationMillis = model.getDuration().toFixed(0);
-    let durationNode = $(".recording-item-duration", recordingItem.target);
-    durationNode.setAttribute("value",
-      L10N.getFormatStr("recordingsList.durationLabel", durationMillis));
+    const { recordings } = this._listState;
+    const index = recordings.indexOf(recording);
+    if (index === -1) {
+      throw new Error("Attempting to remove a recording that doesn't exist.");
+    }
+    recordings.splice(index, 1);
+    this._renderList();
   },
 
   /**
    * The select listener for this container.
    */
-  _onSelect: Task.async(function* ({ detail: recordingItem }) {
-    if (!recordingItem) {
-      return;
-    }
-
-    let model = recordingItem.attachment;
-    this.emit(EVENTS.UI_RECORDING_SELECTED, model);
+  _onSelect: Task.async(function* (recording) {
+    this._listState.selected = recording;
+    this.emit(EVENTS.UI_RECORDING_SELECTED, recording);
+    this._renderList();
   }),
 
   /**
    * The click listener for the "save" button of each item in this container.
    */
-  _onSaveButtonClick: function (e) {
+  _onSaveButtonClick: function (recording) {
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"),
             Ci.nsIFilePicker.modeSave);
     fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
     fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
     fp.defaultString = "profile.json";
 
     fp.open({ done: result => {
       if (result == Ci.nsIFilePicker.returnCancel) {
         return;
       }
-      let recordingItem = this.getItemForElement(e.target);
-      this.emit(EVENTS.UI_EXPORT_RECORDING, recordingItem.attachment, fp.file);
+      this.emit(EVENTS.UI_EXPORT_RECORDING, recording, fp.file);
     }});
   },
 
   _onRecordingExported: function (_, recording, file) {
     if (recording.isConsole()) {
       return;
     }
-    let recordingItem = this.getItemForPredicate(e => e.attachment === recording);
-    let titleNode = $(".recording-item-title", recordingItem.target);
-    titleNode.setAttribute("value", file.leafName.replace(/\..+$/, ""));
-  },
-
-  toString: () => "[object RecordingsView]"
-});
+    const name = file.leafName.replace(/\..+$/, "");
+    this._listState.labels.set(recording, name);
+    this._renderList();
+  }
+};
 
 /**
  * Convenient way of emitting events from the RecordingsView.
  */
 EventEmitter.decorate(RecordingsView);
--- a/devtools/client/shared/components/reps/rep-utils.js
+++ b/devtools/client/shared/components/reps/rep-utils.js
@@ -43,23 +43,18 @@ define(function (require, exports, modul
   function cropString(text, limit, alternativeText) {
     if (!alternativeText) {
       alternativeText = "\u2026";
     }
 
     // Make sure it's a string.
     text = text + "";
 
-    // Use default limit if necessary.
-    if (!limit) {
-      limit = 50;
-    }
-
     // Crop the string only if a limit is actually specified.
-    if (limit <= 0) {
+    if (!limit || limit <= 0) {
       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;
     }
--- a/devtools/client/shared/components/test/mochitest/test_reps_string.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_string.html
@@ -31,17 +31,17 @@ window.onload = Task.async(function* () 
   } catch(e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
     SimpleTest.finish();
   }
 
   function testMultiline() {
     const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline") });
-    is(renderedComponent.textContent, "\"aaaaaaaaaaaaaaaaaaaaa\\nbbb…bbbbbb\\ncccccccccccccccc\\n\"", "String rep has expected text content for multiline string");
+    is(renderedComponent.textContent, "\"aaaaaaaaaaaaaaaaaaaaa\\nbbbbbbbbbbbbbbbbbbb\\ncccccccccccccccc\\n\"", "String rep has expected text content for multiline string");
   }
 
   function testMultilineLimit() {
     const renderedComponent = renderComponent(StringRep.rep, { object: getGripStub("testMultiline"), cropLimit: 20 });
     is(renderedComponent.textContent, "\"aaaaaaaaaa…cccccccc\\n\"", "String rep has expected text content for multiline string with specified number of characters");
   }
 
   function testMultilineOpen() {
--- a/devtools/client/shared/developer-toolbar.js
+++ b/devtools/client/shared/developer-toolbar.js
@@ -327,16 +327,18 @@ DeveloperToolbar.prototype.createToolbar
   hbox.setAttribute("class", "gclitoolbar-complete-node");
   stack.appendChild(hbox);
 
   let toolboxBtn = this._doc.createElement("toolbarbutton");
   toolboxBtn.setAttribute("id", "developer-toolbar-toolbox-button");
   toolboxBtn.setAttribute("class", "developer-toolbar-button");
   let toolboxTooltip = L10N.getStr("toolbar.toolsButton.tooltip");
   toolboxBtn.setAttribute("tooltiptext", toolboxTooltip);
+  let toolboxOpen = gDevToolsBrowser.hasToolboxOpened(this._chromeWindow);
+  toolboxBtn.setAttribute("checked", toolboxOpen);
   toolboxBtn.addEventListener("command", function (event) {
     let window = event.target.ownerDocument.defaultView;
     gDevToolsBrowser.toggleToolboxCommand(window.gBrowser);
   });
   this._errorCounterButton = toolboxBtn;
   this._errorCounterButton._defaultTooltipText = toolboxTooltip;
 
   // On Mac, the close button is on the left,
@@ -709,16 +711,18 @@ DeveloperToolbar.prototype.handleEvent =
         }
 
         // Propagate other errors as they're more likely to cause real issues
         // and thus should cause tests to fail.
         throw error;
       });
 
       if (ev.type == "TabSelect") {
+        let toolboxOpen = gDevToolsBrowser.hasToolboxOpened(this._chromeWindow);
+        this._errorCounterButton.setAttribute("checked", toolboxOpen);
         this._initErrorsCount(ev.target);
       }
     }
   }
   else if (ev.type == "TabClose") {
     this._stopErrorsCount(ev.target);
   }
   else if (ev.type == "beforeunload") {
--- a/devtools/client/storage/test/browser_storage_values.js
+++ b/devtools/client/storage/test/browser_storage_values.js
@@ -37,16 +37,24 @@ const testCases = [
   ]],
   [null, [
     {name: "c1", value: "Array"},
     {name: "c1.0", value: "foo"},
     {name: "c1.1", value: "Bar"},
     {name: "c1.2", value: "Object"},
     {name: "c1.2.foo", value: "Bar"},
   ], true],
+  ["c_encoded", [
+    {name: "c_encoded", value: encodeURIComponent(JSON.stringify({foo: {foo1: "bar"}}))}
+  ]],
+  [null, [
+    {name: "c_encoded", value: "Object"},
+    {name: "c_encoded.foo", value: "Object"},
+    {name: "c_encoded.foo.foo1", value: "bar"}
+  ], true],
   [["localStorage", "http://test1.example.org"]],
   ["ls2", [
     {name: "ls2", value: "foobar-2"}
   ]],
   ["ls1", [
     {name: "ls1", value: JSON.stringify({
       es6: "for", the: "win", baz: [0, 2, 3, {
         deep: "down",
--- a/devtools/client/storage/test/storage-complex-values.html
+++ b/devtools/client/storage/test/storage-complex-values.html
@@ -14,16 +14,19 @@ let partialHostname = location.hostname.
 let cookieExpiresTime = 2000000000000;
 // Setting up some cookies to eat.
 document.cookie = "c1=" + JSON.stringify([
   "foo", "Bar", {
     foo: "Bar"
   }]) + "; expires=" + new Date(cookieExpiresTime).toGMTString() +
   "; path=/browser";
 document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname;
+// URLEncoded cookie
+document.cookie = "c_encoded=" + encodeURIComponent(JSON.stringify({foo: {foo1: "bar"}}));
+
 // ... and some local storage items ..
 const es6 = "for";
 localStorage.setItem("ls1", JSON.stringify({
   es6, the: "win", baz: [0, 2, 3, {
     deep: "down",
     nobody: "cares"
   }]}));
 localStorage.setItem("ls2", "foobar-2");
--- a/devtools/client/storage/ui.js
+++ b/devtools/client/storage/ui.js
@@ -640,17 +640,27 @@ StorageUI.prototype = {
    * object and populates the sidebar with the parsed value. The value can also
    * be a key separated array.
    *
    * @param {string} name
    *        The key corresponding to the `value` string in the object
    * @param {string} value
    *        The string to be parsed into an object
    */
-  parseItemValue: function (name, value) {
+  parseItemValue: function (name, originalValue) {
+    // Find if value is URLEncoded ie
+    let decodedValue = "";
+    try {
+      decodedValue = decodeURIComponent(originalValue);
+    } catch (e) {
+      // Unable to decode, nothing to do
+    }
+    let value = (decodedValue && decodedValue !== originalValue)
+      ? decodedValue : originalValue;
+
     let json = null;
     try {
       json = JSOL.parse(value);
     } catch (ex) {
       json = null;
     }
 
     if (!json && value) {
--- a/devtools/client/themes/performance.css
+++ b/devtools/client/themes/performance.css
@@ -139,43 +139,91 @@
 
 /*
  * DE-XUL: The height of the toolbar is not correct without tweaking the line-height.
  */
 #recordings-pane .devtools-toolbar {
   line-height: 0;
 }
 
-#recordings-list {
-  max-width: 300px;
+.theme-sidebar {
+  position: relative;
+}
+
+/**
+ * DE-XUL: This is probably only needed for the html:div inside of a vbox.
+ */
+#recordings-list > div {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  overflow-y: auto;
+  overflow-x: hidden;
 }
 
-.recording-item {
-  padding: 4px;
+.recording-list {
+  width: var(--sidebar-width);
+  min-width: var(--sidebar-width);
+  margin: 0;
+  padding: 0;
+  background-color: var(--theme-sidebar-background);
+  border-inline-end: 1px solid var(--theme-splitter-color);
 }
 
-.recording-item-title {
+.recording-list-item {
+  display: flex;
+  flex-direction: column;
+  color: var(--theme-body-color);
+  border-bottom: 1px solid rgba(128,128,128,0.15);
+  padding: 8px;
+  cursor: default;
+}
+
+.recording-list-item.selected {
+  background-color: var(--theme-selection-background);
+  color: var(--theme-selection-color);
+}
+
+.recording-list-empty {
+  padding: 8px;
+}
+
+.recording-list-item-label {
   font-size: 110%;
 }
 
-.recording-item-footer {
+.recording-list-item-footer {
   padding-top: 4px;
   font-size: 90%;
+  display: flex;
+  justify-content: space-between;
 }
 
-.recording-item-save {
+.recording-list-item-save {
+  background: none;
+  border: none;
   text-decoration: underline;
   cursor: pointer;
+  font-size: 90%;
+  padding:0;
 }
 
-.recording-item-duration,
-.recording-item-save {
+.recording-list-item-duration,
+.recording-list-item-save {
   color: var(--theme-body-color-alt);
 }
 
+.recording-list-item.selected .recording-list-item-duration,
+.recording-list-item.selected .recording-list-item-save {
+  color: var(--theme-body-color-alt);
+  color: var(--theme-selection-color);
+}
+
 #recordings-list .selected label {
   /* Text inside a selected item should not be custom colored. */
   color: inherit !important;
 }
 
 /* Recording notices */
 
 .notice-container {
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -718,8 +718,44 @@ a.learn-more-link.webconsole-learn-more-
 
 .message.info > .icon::before {
   background-position: -36px -36px;
 }
 
 .message.network .method {
   margin-inline-end: 5px;
 }
+
+/* console.table() */
+.new-consoletable {
+  width: 100%;
+  border-collapse: collapse;
+  --consoletable-border: 1px solid var(--table-splitter-color);
+}
+
+.new-consoletable thead,
+.new-consoletable tbody {
+  background-color: var(--theme-body-background);
+}
+
+.new-consoletable th {
+  background-color: var(--theme-selection-background);
+  color: var(--theme-selection-color);
+  margin: 0;
+  padding: 5px 0 0;
+  font-weight: inherit;
+  border-inline-end: var(--consoletable-border);
+  border-bottom: var(--consoletable-border);
+}
+
+.new-consoletable tr:nth-of-type(even) {
+  background-color: var(--table-zebra-background);
+}
+
+.new-consoletable td {
+  padding: 3px 4px;
+  min-width: 100px;
+  -moz-user-focus: normal;
+  color: var(--theme-body-color);
+  border-inline-end: var(--consoletable-border);
+  height: 1.25em;
+  line-height: 1.25em;
+}
--- a/devtools/client/webconsole/net/components/post-tab.js
+++ b/devtools/client/webconsole/net/components/post-tab.js
@@ -72,17 +72,19 @@ var PostTab = React.createClass({
     }
 
     return {
       key: "json",
       content: TreeView({
         columns: [{id: "value"}],
         object: json,
         mode: "tiny",
-        renderValue: props => Rep(props)
+        renderValue: props => Rep(Object.assign({}, props, {
+          cropLimit: 50,
+        })),
       }),
       name: Locale.$STR("jsonScopeName")
     };
   },
 
   parseXml(file) {
     let text = file.request.postData.text;
     if (isLongString(text)) {
--- a/devtools/client/webconsole/net/components/response-tab.js
+++ b/devtools/client/webconsole/net/components/response-tab.js
@@ -100,17 +100,19 @@ var ResponseTab = React.createClass({
     }
 
     return {
       key: "json",
       content: TreeView({
         columns: [{id: "value"}],
         object: json,
         mode: "tiny",
-        renderValue: props => Rep(props)
+        renderValue: props => Rep(Object.assign({}, props, {
+          cropLimit: 50,
+        })),
       }),
       name: Locale.$STR("jsonScopeName")
     };
   },
 
   renderImage(file) {
     let content = file.response.content;
     if (!this.isImage(content)) {
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/enhancers.js
@@ -0,0 +1,20 @@
+/* -*- 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";
+
+const { BATCH_ACTIONS } = require("../constants");
+
+function batchActions(batchedActions) {
+  return {
+    type: BATCH_ACTIONS,
+    actions: batchedActions,
+  };
+}
+
+module.exports = {
+  batchActions
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/actions/index.js
@@ -0,0 +1,18 @@
+/* -*- 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";
+
+const actionModules = [
+  "enhancers",
+  "filters",
+  "messages",
+  "ui",
+].map(filename => require(`./${filename}`));
+
+const actions = Object.assign({}, ...actionModules);
+
+module.exports = actions;
--- a/devtools/client/webconsole/new-console-output/actions/messages.js
+++ b/devtools/client/webconsole/new-console-output/actions/messages.js
@@ -5,43 +5,45 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
   prepareMessage
 } = require("devtools/client/webconsole/new-console-output/utils/messages");
 const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator");
-
+const { batchActions } = require("devtools/client/webconsole/new-console-output/actions/enhancers");
 const {
   MESSAGE_ADD,
   MESSAGES_CLEAR,
   MESSAGE_OPEN,
   MESSAGE_CLOSE,
   MESSAGE_TYPE,
+  MESSAGE_TABLE_RECEIVE,
 } = require("../constants");
 
 const defaultIdGenerator = new IdGenerator();
 
 function messageAdd(packet, idGenerator = null) {
-  return (dispatch) => {
-    if (idGenerator == null) {
-      idGenerator = defaultIdGenerator;
-    }
-    let message = prepareMessage(packet, idGenerator);
+  if (idGenerator == null) {
+    idGenerator = defaultIdGenerator;
+  }
+  let message = prepareMessage(packet, idGenerator);
+  const addMessageAction = {
+    type: MESSAGE_ADD,
+    message
+  };
 
-    if (message.type === MESSAGE_TYPE.CLEAR) {
-      dispatch(messagesClear());
-    }
-
-    dispatch({
-      type: MESSAGE_ADD,
-      message
-    });
-  };
+  if (message.type === MESSAGE_TYPE.CLEAR) {
+    return batchActions([
+      messagesClear(),
+      addMessageAction,
+    ]);
+  }
+  return addMessageAction;
 }
 
 function messagesClear() {
   return {
     type: MESSAGES_CLEAR
   };
 }
 
@@ -54,12 +56,44 @@ function messageOpen(id) {
 
 function messageClose(id) {
   return {
     type: MESSAGE_CLOSE,
     id
   };
 }
 
-exports.messageAdd = messageAdd;
-exports.messagesClear = messagesClear;
-exports.messageOpen = messageOpen;
-exports.messageClose = messageClose;
+function messageTableDataGet(id, client, dataType) {
+  return (dispatch) => {
+    let fetchObjectActorData;
+    if (["Map", "WeakMap", "Set", "WeakSet"].includes(dataType)) {
+      fetchObjectActorData = (cb) => client.enumEntries(cb);
+    } else {
+      fetchObjectActorData = (cb) => client.enumProperties({
+        ignoreNonIndexedProperties: dataType === "Array"
+      }, cb);
+    }
+
+    fetchObjectActorData(enumResponse => {
+      const {iterator} = enumResponse;
+      iterator.slice(0, iterator.count, sliceResponse => {
+        let {ownProperties} = sliceResponse;
+        dispatch(messageTableDataReceive(id, ownProperties));
+      });
+    });
+  };
+}
+
+function messageTableDataReceive(id, data) {
+  return {
+    type: MESSAGE_TABLE_RECEIVE,
+    id,
+    data
+  };
+}
+
+module.exports = {
+  messageAdd,
+  messagesClear,
+  messageOpen,
+  messageClose,
+  messageTableDataGet,
+};
--- a/devtools/client/webconsole/new-console-output/actions/moz.build
+++ b/devtools/client/webconsole/new-console-output/actions/moz.build
@@ -1,10 +1,12 @@
 # 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(
+    'enhancers.js',
     'filters.js',
+    'index.js',
     'messages.js',
     'ui.js',
 )
--- a/devtools/client/webconsole/new-console-output/components/console-output.js
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -7,23 +7,23 @@ const {
   createClass,
   createFactory,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
-const { getAllMessages, getAllMessagesUiById } = require("devtools/client/webconsole/new-console-output/selectors/messages");
+const { getAllMessages, getAllMessagesUiById, getAllMessagesTableDataById } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer);
 
 const ConsoleOutput = createClass({
 
   propTypes: {
-    jsterm: PropTypes.object.isRequired,
+    hudProxyClient: PropTypes.object.isRequired,
     messages: PropTypes.object.isRequired,
     messagesUi: PropTypes.object.isRequired,
     sourceMapService: PropTypes.object,
     onViewSourceInDebugger: PropTypes.func.isRequired,
     openNetworkPanel: PropTypes.func.isRequired,
     openLink: PropTypes.func.isRequired,
   },
 
@@ -41,35 +41,39 @@ const ConsoleOutput = createClass({
       let node = ReactDOM.findDOMNode(this);
       node.scrollTop = node.scrollHeight;
     }
   },
 
   render() {
     let {
       dispatch,
+      hudProxyClient,
       messages,
       messagesUi,
+      messagesTableData,
       sourceMapService,
       onViewSourceInDebugger,
       openNetworkPanel,
       openLink,
     } = this.props;
 
     let messageNodes = messages.map((message) => {
       return (
         MessageContainer({
           dispatch,
+          hudProxyClient,
           message,
           key: message.id,
           sourceMapService,
           onViewSourceInDebugger,
           openNetworkPanel,
           openLink,
           open: messagesUi.includes(message.id),
+          tableData: messagesTableData.get(message.id),
         })
       );
     });
     return (
       dom.div({className: "webconsole-output"}, messageNodes)
     );
   }
 });
@@ -80,12 +84,13 @@ function isScrolledToBottom(outputNode, 
   return scrollNode.scrollTop + scrollNode.clientHeight >=
          scrollNode.scrollHeight - lastNodeHeight / 2;
 }
 
 function mapStateToProps(state) {
   return {
     messages: getAllMessages(state),
     messagesUi: getAllMessagesUiById(state),
+    messagesTableData: getAllMessagesTableDataById(state),
   };
 }
 
 module.exports = connect(mapStateToProps)(ConsoleOutput);
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/console-table.js
@@ -0,0 +1,200 @@
+/* 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 {
+  createClass,
+  createFactory,
+  DOM: dom,
+  PropTypes
+} = require("devtools/client/shared/vendor/react");
+const { ObjectClient } = require("devtools/shared/client/main");
+const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
+const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages");
+const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body").GripMessageBody);
+
+const TABLE_ROW_MAX_ITEMS = 1000;
+const TABLE_COLUMN_MAX_ITEMS = 10;
+
+const ConsoleTable = createClass({
+
+  displayName: "ConsoleTable",
+
+  propTypes: {
+    dispatch: PropTypes.func.isRequired,
+    parameters: PropTypes.array.isRequired,
+    hudProxyClient: PropTypes.object.isRequired,
+    id: PropTypes.string.isRequired,
+  },
+
+  componentWillMount: function () {
+    const {id, dispatch, hudProxyClient, parameters} = this.props;
+
+    if (!Array.isArray(parameters) || parameters.length === 0) {
+      return;
+    }
+
+    const client = new ObjectClient(hudProxyClient, parameters[0]);
+    let dataType = getParametersDataType(parameters);
+
+    // Get all the object properties.
+    dispatch(actions.messageTableDataGet(id, client, dataType));
+  },
+
+  getHeaders: function (columns) {
+    let headerItems = [];
+    columns.forEach((value, key) => headerItems.push(dom.th({}, value)));
+    return headerItems;
+  },
+
+  getRows: function (columns, items) {
+    return items.map(item => {
+      let cells = [];
+      columns.forEach((value, key) => {
+        cells.push(
+          dom.td(
+            {},
+            GripMessageBody({
+              grip: item[key]
+            })
+          )
+        );
+      });
+      return dom.tr({}, cells);
+    });
+  },
+
+  render: function () {
+    const {parameters, tableData} = this.props;
+    const headersGrip = parameters[1];
+    const headers = headersGrip && headersGrip.preview ? headersGrip.preview.items : null;
+
+    // if tableData is nullable, we don't show anything.
+    if (!tableData) {
+      return null;
+    }
+
+    const {columns, items} = getTableItems(
+      tableData,
+      getParametersDataType(parameters),
+      headers
+    );
+
+    return (
+      dom.table({className: "new-consoletable devtools-monospace"},
+        dom.thead({}, this.getHeaders(columns)),
+        dom.tbody({}, this.getRows(columns, items))
+      )
+    );
+  }
+});
+
+function getParametersDataType(parameters = null) {
+  if (!Array.isArray(parameters) || parameters.length === 0) {
+    return null;
+  }
+  return parameters[0].class;
+}
+
+function getTableItems(data = {}, type, headers = null) {
+  const INDEX_NAME = "_index";
+  const VALUE_NAME = "_value";
+  const namedIndexes = {
+    [INDEX_NAME]: (
+      ["Object", "Array"].includes(type) ?
+        l10n.getStr("table.index") : l10n.getStr("table.iterationIndex")
+    ),
+    [VALUE_NAME]: l10n.getStr("table.value"),
+    key: l10n.getStr("table.key")
+  };
+
+  let columns = new Map();
+  let items = [];
+
+  let addItem = function (item) {
+    items.push(item);
+    Object.keys(item).forEach(key => addColumn(key));
+  };
+
+  let addColumn = function (columnIndex) {
+    let columnExists = columns.has(columnIndex);
+    let hasMaxColumns = columns.size == TABLE_COLUMN_MAX_ITEMS;
+    let hasCustomHeaders = Array.isArray(headers);
+
+    if (
+      !columnExists &&
+      !hasMaxColumns && (
+        !hasCustomHeaders ||
+        headers.includes(columnIndex) ||
+        columnIndex === INDEX_NAME
+      )
+    ) {
+      columns.set(columnIndex, namedIndexes[columnIndex] || columnIndex);
+    }
+  };
+
+  for (let index of Object.keys(data)) {
+    if (type !== "Object" && index == parseInt(index, 10)) {
+      index = parseInt(index, 10);
+    }
+
+    let item = {
+      [INDEX_NAME]: index
+    };
+
+    let property = data[index].value;
+
+    if (property.preview) {
+      let {preview} = property;
+      let entries = preview.ownProperties || preview.items;
+      if (entries) {
+        for (let key of Object.keys(entries)) {
+          let entry = entries[key];
+          item[key] = entry.value || entry;
+        }
+      } else {
+        if (preview.key) {
+          item.key = preview.key;
+        }
+
+        item[VALUE_NAME] = preview.value || property;
+      }
+    } else {
+      item[VALUE_NAME] = property;
+    }
+
+    addItem(item);
+
+    if (items.length === TABLE_ROW_MAX_ITEMS) {
+      break;
+    }
+  }
+
+  // Some headers might not be present in the items, so we make sure to
+  // return all the headers set by the user.
+  if (Array.isArray(headers)) {
+    headers.forEach(header => addColumn(header));
+  }
+
+  // We want to always have the index column first
+  if (columns.has(INDEX_NAME)) {
+    let index = columns.get(INDEX_NAME);
+    columns.delete(INDEX_NAME);
+    columns = new Map([[INDEX_NAME, index], ...columns.entries()]);
+  }
+
+  // We want to always have the values column last
+  if (columns.has(VALUE_NAME)) {
+    let index = columns.get(VALUE_NAME);
+    columns.delete(VALUE_NAME);
+    columns.set(VALUE_NAME, index);
+  }
+
+  return {
+    columns,
+    items
+  };
+}
+
+exports.ConsoleTable = ConsoleTable;
--- a/devtools/client/webconsole/new-console-output/components/filter-bar.js
+++ b/devtools/client/webconsole/new-console-output/components/filter-bar.js
@@ -7,19 +7,19 @@ const {
   createFactory,
   createClass,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
 const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
-const { filterTextSet, filtersClear } = require("devtools/client/webconsole/new-console-output/actions/filters");
-const { messagesClear } = require("devtools/client/webconsole/new-console-output/actions/messages");
-const uiActions = require("devtools/client/webconsole/new-console-output/actions/ui");
+const { filterTextSet, filtersClear } = require("devtools/client/webconsole/new-console-output/actions/index");
+const { messagesClear } = require("devtools/client/webconsole/new-console-output/actions/index");
+const uiActions = require("devtools/client/webconsole/new-console-output/actions/index");
 const {
   MESSAGE_LEVEL
 } = require("../constants");
 const FilterButton = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-button").FilterButton);
 
 const FilterBar = createClass({
 
   displayName: "FilterBar",
--- a/devtools/client/webconsole/new-console-output/components/filter-button.js
+++ b/devtools/client/webconsole/new-console-output/components/filter-button.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {
   createClass,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
-const actions = require("devtools/client/webconsole/new-console-output/actions/filters");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 
 const FilterButton = createClass({
 
   displayName: "FilterButton",
 
   propTypes: {
     label: PropTypes.string.isRequired,
     filterKey: PropTypes.string.isRequired,
--- a/devtools/client/webconsole/new-console-output/components/grip-message-body.js
+++ b/devtools/client/webconsole/new-console-output/components/grip-message-body.js
@@ -36,19 +36,21 @@ GripMessageBody.propTypes = {
 function GripMessageBody(props) {
   const { grip } = props;
 
   return (
     // @TODO once there is a longString rep, also turn off quotes for those.
     typeof grip === "string"
       ? StringRep({
         object: grip,
-        useQuotes: false
+        useQuotes: false,
+        mode: props.mode,
       })
       : Rep({
         object: grip,
         objectLink: VariablesViewLink,
-        defaultRep: Grip
+        defaultRep: Grip,
+        mode: props.mode,
       })
   );
 }
 
 module.exports.GripMessageBody = GripMessageBody;
--- a/devtools/client/webconsole/new-console-output/components/message-container.js
+++ b/devtools/client/webconsole/new-console-output/components/message-container.js
@@ -32,49 +32,56 @@ const MessageContainer = createClass({
 
   propTypes: {
     message: PropTypes.object.isRequired,
     sourceMapService: PropTypes.object,
     onViewSourceInDebugger: PropTypes.func.isRequired,
     openNetworkPanel: PropTypes.func.isRequired,
     openLink: PropTypes.func.isRequired,
     open: PropTypes.bool.isRequired,
+    hudProxyClient: PropTypes.object.isRequired,
   },
 
   getDefaultProps: function () {
     return {
       open: false
     };
   },
 
   shouldComponentUpdate(nextProps, nextState) {
-    return this.props.message.repeat !== nextProps.message.repeat
-      || this.props.open !== nextProps.open;
+    const repeatChanged = this.props.message.repeat !== nextProps.message.repeat;
+    const openChanged = this.props.open !== nextProps.open;
+    const tableDataChanged = this.props.tableData !== nextProps.tableData;
+    return repeatChanged || openChanged || tableDataChanged;
   },
 
   render() {
     const {
       dispatch,
       message,
       sourceMapService,
       onViewSourceInDebugger,
       openNetworkPanel,
       openLink,
       open,
+      tableData,
+      hudProxyClient,
     } = this.props;
 
     let MessageComponent = createFactory(getMessageComponent(message));
     return MessageComponent({
       dispatch,
       message,
       sourceMapService,
       onViewSourceInDebugger,
       openNetworkPanel,
       openLink,
       open,
+      tableData,
+      hudProxyClient,
     });
   }
 });
 
 function getMessageComponent(message) {
   switch (message.source) {
     case MESSAGE_SOURCE.CONSOLE_API:
       return componentMap.get("ConsoleApiCall");
--- a/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-api-call.js
@@ -13,41 +13,54 @@ const {
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const FrameView = createFactory(require("devtools/client/shared/components/frame"));
 const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
 const GripMessageBody = createFactory(require("devtools/client/webconsole/new-console-output/components/grip-message-body").GripMessageBody);
 const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat").MessageRepeat);
 const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
 const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button").CollapseButton);
-const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
+const ConsoleTable = createFactory(require("devtools/client/webconsole/new-console-output/components/console-table").ConsoleTable);
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 
 ConsoleApiCall.displayName = "ConsoleApiCall";
 
 ConsoleApiCall.propTypes = {
   message: PropTypes.object.isRequired,
   sourceMapService: PropTypes.object,
   onViewSourceInDebugger: PropTypes.func.isRequired,
   open: PropTypes.bool,
+  hudProxyClient: PropTypes.object.isRequired,
 };
 
 ConsoleApiCall.defaultProps = {
   open: false
 };
 
 function ConsoleApiCall(props) {
-  const { dispatch, message, sourceMapService, onViewSourceInDebugger, open } = props;
-  const { source, level, stacktrace, type, frame, parameters } = message;
+  const {
+    dispatch,
+    message,
+    sourceMapService,
+    onViewSourceInDebugger,
+    open,
+    hudProxyClient,
+    tableData
+  } = props;
+  const {source, level, stacktrace, type, frame, parameters } = message;
 
   let messageBody;
   if (type === "trace") {
-    messageBody = dom.span({ className: "cm-variable" }, "console.trace()");
+    messageBody = dom.span({className: "cm-variable"}, "console.trace()");
   } else if (type === "assert") {
     let reps = formatReps(parameters);
     messageBody = dom.span({ className: "cm-variable" }, "Assertion failed: ", reps);
+  } else if (type === "table") {
+    // TODO: Chrome does not output anything, see if we want to keep this
+    messageBody = dom.span({className: "cm-variable"}, "console.table()");
   } else if (parameters) {
     messageBody = formatReps(parameters);
   } else {
     messageBody = message.messageText;
   }
 
   const icon = MessageIcon({ level });
   const repeat = MessageRepeat({ repeat: message.repeat });
@@ -78,16 +91,24 @@ function ConsoleApiCall(props) {
       onClick: function () {
         if (open) {
           dispatch(actions.messageClose(message.id));
         } else {
           dispatch(actions.messageOpen(message.id));
         }
       },
     });
+  } else if (type === "table") {
+    attachment = ConsoleTable({
+      dispatch,
+      id: message.id,
+      hudProxyClient,
+      parameters: message.parameters,
+      tableData
+    });
   }
 
   const classes = ["message", "cm-s-mozilla"];
 
   classes.push(source);
   classes.push(type);
   classes.push(level);
 
--- a/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/network-event-message.js
@@ -10,17 +10,17 @@
 const {
   createFactory,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
 const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button").CollapseButton);
 const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
-const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 
 NetworkEventMessage.displayName = "NetworkEventMessage";
 
 NetworkEventMessage.propTypes = {
   message: PropTypes.object.isRequired,
   openNetworkPanel: PropTypes.func.isRequired,
   // @TODO: openLink will be used for mixed-content handling
   openLink: PropTypes.func.isRequired,
@@ -55,33 +55,21 @@ function NetworkEventMessage(props) {
   function onUrlClick() {
     openNetworkPanel(actor);
   }
 
   return dom.div({ className: classes.join(" ") },
     // @TODO add timestamp
     // @TODO add indent if necessary
     MessageIcon({ level }),
-    CollapseButton({
-      open,
-      title: l10n.getStr("messageToggleDetails"),
-      onClick: () => {
-        if (open) {
-          dispatch(actions.messageClose(message.id));
-        } else {
-          dispatch(actions.messageOpen(message.id));
-        }
-      },
-    }),
     dom.span({
       className: "message-body-wrapper message-body devtools-monospace",
       "aria-haspopup": "true"
     },
       dom.span({ className: "method" }, method),
       isXHR ? dom.span({ className: "xhr" }, xhr) : null,
       dom.a({ className: "url", title: url, onClick: onUrlClick },
-        url.replace(/\?.+/, "")),
-      dom.a({ className: "status" }, statusInfo)
+        url.replace(/\?.+/, ""))
     )
   );
 }
 
 module.exports.NetworkEventMessage = NetworkEventMessage;
--- a/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
@@ -13,17 +13,17 @@ const {
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const FrameView = createFactory(require("devtools/client/shared/components/frame"));
 const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
 const CollapseButton = createFactory(require("devtools/client/webconsole/new-console-output/components/collapse-button").CollapseButton);
 const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat").MessageRepeat);
 const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
 
-const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 
 PageError.displayName = "PageError";
 
 PageError.propTypes = {
   message: PropTypes.object.isRequired,
   open: PropTypes.bool,
 };
 
--- a/devtools/client/webconsole/new-console-output/components/moz.build
+++ b/devtools/client/webconsole/new-console-output/components/moz.build
@@ -5,16 +5,17 @@
 
 DIRS += [
     'message-types'
 ]
 
 DevToolsModules(
     'collapse-button.js',
     'console-output.js',
+    'console-table.js',
     'filter-bar.js',
     'filter-button.js',
     'grip-message-body.js',
     'message-container.js',
     'message-icon.js',
     'message-repeat.js',
     'variables-view-link.js'
 )
--- a/devtools/client/webconsole/new-console-output/constants.js
+++ b/devtools/client/webconsole/new-console-output/constants.js
@@ -1,20 +1,22 @@
 /* -*- 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";
 
 const actionTypes = {
+  BATCH_ACTIONS: "BATCH_ACTIONS",
   MESSAGE_ADD: "MESSAGE_ADD",
   MESSAGES_CLEAR: "MESSAGES_CLEAR",
   MESSAGE_OPEN: "MESSAGE_OPEN",
   MESSAGE_CLOSE: "MESSAGE_CLOSE",
+  MESSAGE_TABLE_RECEIVE: "MESSAGE_TABLE_RECEIVE",
   FILTER_TOGGLE: "FILTER_TOGGLE",
   FILTER_TEXT_SET: "FILTER_TEXT_SET",
   FILTERS_CLEAR: "FILTERS_CLEAR",
   FILTER_BAR_TOGGLE: "FILTER_BAR_TOGGLE",
 };
 
 const chromeRDPEnums = {
   MESSAGE_SOURCE: {
--- a/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
+++ b/devtools/client/webconsole/new-console-output/new-console-output-wrapper.js
@@ -3,58 +3,72 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 // React & Redux
 const React = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
-const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
 
 const ConsoleOutput = React.createFactory(require("devtools/client/webconsole/new-console-output/components/console-output"));
 const FilterBar = React.createFactory(require("devtools/client/webconsole/new-console-output/components/filter-bar"));
 
 const store = configureStore();
 
 function NewConsoleOutputWrapper(parentNode, jsterm, toolbox, owner) {
-  const sourceMapService = toolbox ? toolbox._sourceMapService : null;
-  let childComponent = ConsoleOutput({
-    jsterm,
-    sourceMapService,
-    onViewSourceInDebugger: frame => toolbox.viewSourceInDebugger.call(
-      toolbox,
-      frame.url,
-      frame.line
-    ),
-    openNetworkPanel: (requestId) => {
-      return toolbox.selectTool("netmonitor").then(panel => {
-        return panel.panelWin.NetMonitorController.inspectRequest(requestId);
-      });
-    },
-    openLink: (url) => {
-      owner.openLink(url);
-    },
-  });
-  let filterBar = FilterBar({});
-  let provider = React.createElement(
-    Provider,
-    { store },
-    React.DOM.div(
-      {className: "webconsole-output-wrapper"},
-      filterBar,
-      childComponent
-  ));
-  this.body = ReactDOM.render(provider, parentNode);
+  this.parentNode = parentNode;
+  this.jsterm = jsterm;
+  this.toolbox = toolbox;
+  this.owner = owner;
+
+  this.init = this.init.bind(this);
 }
 
 NewConsoleOutputWrapper.prototype = {
+  init: function () {
+    const sourceMapService = this.toolbox ? this.toolbox._sourceMapService : null;
+
+    let childComponent = ConsoleOutput({
+      hudProxyClient: this.jsterm.hud.proxy.client,
+      sourceMapService,
+      onViewSourceInDebugger: frame => this.toolbox.viewSourceInDebugger.call(
+        this.toolbox,
+        frame.url,
+        frame.line
+      ),
+      openNetworkPanel: (requestId) => {
+        return this.toolbox.selectTool("netmonitor").then(panel => {
+          return panel.panelWin.NetMonitorController.inspectRequest(requestId);
+        });
+      },
+      openLink: (url) => {
+        this.owner.openLink(url);
+      },
+    });
+    let filterBar = FilterBar({});
+    let provider = React.createElement(
+      Provider,
+      { store },
+      React.DOM.div(
+        {className: "webconsole-output-wrapper"},
+        filterBar,
+        childComponent
+    ));
+
+    this.body = ReactDOM.render(provider, this.parentNode);
+  },
   dispatchMessageAdd: (message) => {
     store.dispatch(actions.messageAdd(message));
   },
+  dispatchMessagesAdd: (messages) => {
+    const batchedActions = messages.map(message => actions.messageAdd(message));
+    store.dispatch(actions.batchActions(batchedActions));
+  },
   dispatchMessagesClear: () => {
     store.dispatch(actions.messagesClear());
   },
 };
 
 // Exports from this module
 module.exports = NewConsoleOutputWrapper;
--- a/devtools/client/webconsole/new-console-output/reducers/messages.js
+++ b/devtools/client/webconsole/new-console-output/reducers/messages.js
@@ -6,21 +6,23 @@
 "use strict";
 
 const Immutable = require("devtools/client/shared/vendor/immutable");
 const constants = require("devtools/client/webconsole/new-console-output/constants");
 
 const MessageState = Immutable.Record({
   messagesById: Immutable.List(),
   messagesUiById: Immutable.List(),
+  messagesTableDataById: Immutable.Map(),
 });
 
 function messages(state = new MessageState(), action) {
   const messagesById = state.messagesById;
   const messagesUiById = state.messagesUiById;
+  const messagesTableDataById = state.messagesTableDataById;
 
   switch (action.type) {
     case constants.MESSAGE_ADD:
       let newMessage = action.message;
 
       if (newMessage.type === constants.MESSAGE_TYPE.NULL_MESSAGE) {
         return state;
       }
@@ -47,14 +49,17 @@ function messages(state = new MessageSta
         record.set("messagesById", Immutable.List());
         record.set("messagesUiById", Immutable.List());
       });
     case constants.MESSAGE_OPEN:
       return state.set("messagesUiById", messagesUiById.push(action.id));
     case constants.MESSAGE_CLOSE:
       let index = state.messagesUiById.indexOf(action.id);
       return state.deleteIn(["messagesUiById", index]);
+    case constants.MESSAGE_TABLE_RECEIVE:
+      const {id, data} = action;
+      return state.set("messagesTableDataById", messagesTableDataById.set(id, data));
   }
 
   return state;
 }
 
 exports.messages = messages;
--- a/devtools/client/webconsole/new-console-output/selectors/messages.js
+++ b/devtools/client/webconsole/new-console-output/selectors/messages.js
@@ -29,16 +29,20 @@ function getAllMessages(state) {
     logLimit
   );
 }
 
 function getAllMessagesUiById(state) {
   return state.messages.messagesUiById;
 }
 
+function getAllMessagesTableDataById(state) {
+  return state.messages.messagesTableDataById;
+}
+
 function filterLevel(messages, filters) {
   return messages.filter((message) => {
     return filters.get(message.level) === true
       || [MESSAGE_TYPE.COMMAND, MESSAGE_TYPE.RESULT].includes(message.type);
   });
 }
 
 function filterNetwork(messages, filters) {
@@ -109,8 +113,9 @@ function prune(messages, logLimit) {
     return messages.splice(0, messageCount - logLimit);
   }
 
   return messages;
 }
 
 exports.getAllMessages = getAllMessages;
 exports.getAllMessagesUiById = getAllMessagesUiById;
+exports.getAllMessagesTableDataById = getAllMessagesTableDataById;
--- a/devtools/client/webconsole/new-console-output/store.js
+++ b/devtools/client/webconsole/new-console-output/store.js
@@ -1,17 +1,23 @@
 /* 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 {FilterState} = require("devtools/client/webconsole/new-console-output/reducers/filters");
 const {PrefState} = require("devtools/client/webconsole/new-console-output/reducers/prefs");
-const { applyMiddleware, combineReducers, createStore } = require("devtools/client/shared/vendor/redux");
+const {
+  applyMiddleware,
+  combineReducers,
+  compose,
+  createStore
+} = require("devtools/client/shared/vendor/redux");
 const { thunk } = require("devtools/client/shared/redux/middleware/thunk");
+const constants = require("devtools/client/webconsole/new-console-output/constants");
 const { reducers } = require("./reducers/index");
 const Services = require("Services");
 
 function configureStore() {
   const initialState = {
     prefs: new PrefState({
       logLimit: Math.max(Services.prefs.getIntPref("devtools.hud.loglimit"), 1),
     }),
@@ -23,16 +29,39 @@ function configureStore() {
       network: Services.prefs.getBoolPref("devtools.webconsole.filter.network"),
       netxhr: Services.prefs.getBoolPref("devtools.webconsole.filter.netxhr"),
     })
   };
 
   return createStore(
     combineReducers(reducers),
     initialState,
-    applyMiddleware(thunk)
+    compose(applyMiddleware(thunk), enableBatching())
   );
 }
 
+/**
+ * A enhancer for the store to handle batched actions.
+ */
+function enableBatching() {
+  return next => (reducer, initialState, enhancer) => {
+    function batchingReducer(state, action) {
+      switch (action.type) {
+        case constants.BATCH_ACTIONS:
+          return action.actions.reduce(batchingReducer, state);
+        default:
+          return reducer(state, action);
+      }
+    }
+
+    if (typeof initialState === "function" && typeof enhancer === "undefined") {
+      enhancer = initialState;
+      initialState = undefined;
+    }
+
+    return next(batchingReducer, initialState, enhancer);
+  };
+}
+
 // Provide the store factory for test code so that each test is working with
 // its own instance.
 module.exports.configureStore = configureStore;
 
--- a/devtools/client/webconsole/new-console-output/test/actions/filters.test.js
+++ b/devtools/client/webconsole/new-console-output/test/actions/filters.test.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
-const actions = require("devtools/client/webconsole/new-console-output/actions/filters");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const {
   FILTER_TEXT_SET,
   FILTER_TOGGLE,
   FILTERS_CLEAR,
   MESSAGE_LEVEL
 } = require("devtools/client/webconsole/new-console-output/constants");
 
 const expect = require("expect");
--- a/devtools/client/webconsole/new-console-output/test/actions/messages.test.js
+++ b/devtools/client/webconsole/new-console-output/test/actions/messages.test.js
@@ -21,47 +21,64 @@ describe("Message actions:", () => {
   });
 
   describe("messageAdd", () => {
     it("dispatches expected action given a packet", () => {
       const packet = stubPackets.get("console.log('foobar', 'test')");
       const store = mockStore({});
       store.dispatch(actions.messageAdd(packet));
 
+      const actualActions = store.getActions();
+      expect(actualActions.length).toEqual(1);
+
+      const addAction = actualActions[0];
+      const {message} = addAction;
+      const expectedAction = {
+        type: constants.MESSAGE_ADD,
+        message: stubPreparedMessages.get("console.log('foobar', 'test')")
+      };
+      expect(message.toJS()).toEqual(expectedAction.message.toJS());
+    });
+
+    it("dispatches expected actions given a console.clear packet", () => {
+      const packet = stubPackets.get("console.clear()");
+      const store = mockStore({});
+      store.dispatch(actions.messageAdd(packet));
+
+      const actualActions = store.getActions();
+      expect(actualActions.length).toEqual(1);
+
+      const [clearAction, addAction] = actualActions[0].actions;
+      expect(clearAction.type).toEqual(constants.MESSAGES_CLEAR);
+
+      const {message} = addAction;
+      const expectedAction = {
+        type: constants.MESSAGE_ADD,
+        message: stubPreparedMessages.get("console.clear()")
+      };
+      expect(addAction.type).toEqual(constants.MESSAGE_ADD);
+      expect(message.toJS()).toEqual(expectedAction.message.toJS());
+    });
+
+    it("dispatches expected action given a console.table packet", () => {
+      const packet = stubPackets.get("console.table(['a', 'b', 'c'])");
+      const store = mockStore({});
+      store.dispatch(actions.messageAdd(packet));
+
       const expectedActions = store.getActions();
       expect(expectedActions.length).toEqual(1);
 
       const addAction = expectedActions[0];
       const {message} = addAction;
       const expected = {
         type: constants.MESSAGE_ADD,
-        message: stubPreparedMessages.get("console.log('foobar', 'test')")
+        message: stubPreparedMessages.get("console.table(['a', 'b', 'c'])")
       };
       expect(message.toJS()).toEqual(expected.message.toJS());
     });
-
-    it("dispatches expected actions given a console.clear packet", () => {
-      const packet = stubPackets.get("console.clear()");
-      const store = mockStore({});
-      store.dispatch(actions.messageAdd(packet));
-
-      const expectedActions = store.getActions();
-      expect(expectedActions.length).toEqual(2);
-
-      const [clearAction, addAction] = expectedActions;
-      expect(clearAction.type).toEqual(constants.MESSAGES_CLEAR);
-
-      const {message} = addAction;
-      const expected = {
-        type: constants.MESSAGE_ADD,
-        message: stubPreparedMessages.get("console.clear()")
-      };
-      expect(addAction.type).toEqual(constants.MESSAGE_ADD);
-      expect(message.toJS()).toEqual(expected.message.toJS());
-    });
   });
 
   describe("messagesClear", () => {
     it("creates expected action", () => {
       const action = actions.messagesClear();
       const expected = {
         type: constants.MESSAGES_CLEAR,
       };
--- a/devtools/client/webconsole/new-console-output/test/actions/ui.test.js
+++ b/devtools/client/webconsole/new-console-output/test/actions/ui.test.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
-const actions = require("devtools/client/webconsole/new-console-output/actions/ui");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const {
   FILTER_BAR_TOGGLE
 } = require("devtools/client/webconsole/new-console-output/constants");
 
 const expect = require("expect");
 
 describe("UI actions:", () => {
   describe("filterBarToggle", () => {
--- a/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html
+++ b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html
@@ -53,17 +53,17 @@ function timeit(cb) {
     cb();
     let elapsed = performance.now() - start;
     resolve(elapsed / 1000);
   });
 }
 
 window.onload = Task.async(function* () {
   const { configureStore } = browserRequire("devtools/client/webconsole/new-console-output/store");
-  const { filterTextSet, filtersClear } = browserRequire("devtools/client/webconsole/new-console-output/actions/filters");
+  const { filterTextSet, filtersClear } = browserRequire("devtools/client/webconsole/new-console-output/actions/index");
   const NewConsoleOutputWrapper = browserRequire("devtools/client/webconsole/new-console-output/new-console-output-wrapper");
   const wrapper = new NewConsoleOutputWrapper(document.querySelector("#output"), {});
 
   const store = configureStore();
 
   let time = yield timeit(() => {
     testPackets.forEach((message) => {
       wrapper.dispatchMessageAdd(message);
--- a/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/network-event-message.test.js
@@ -24,42 +24,39 @@ describe("NetworkEventMessage component:
     it("renders as expected", () => {
       const message = stubPreparedMessages.get("GET request");
       const wrapper = render(NetworkEventMessage({ message, onViewSourceInDebugger, openNetworkPanel, openLink }));
 
       expect(wrapper.find(".message-body .method").text()).toBe("GET");
       expect(wrapper.find(".message-body .xhr").length).toBe(0);
       expect(wrapper.find(".message-body .url").length).toBe(1);
       expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
-      expect(wrapper.find(".message-body .status").length).toBe(1);
       expect(wrapper.find("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1);
     });
   });
 
   describe("XHR GET request", () => {
     it("renders as expected", () => {
       const message = stubPreparedMessages.get("XHR GET request");
       const wrapper = render(NetworkEventMessage({ message, onViewSourceInDebugger, openNetworkPanel, openLink }));
 
       expect(wrapper.find(".message-body .method").text()).toBe("GET");
       expect(wrapper.find(".message-body .xhr").length).toBe(1);
       expect(wrapper.find(".message-body .xhr").text()).toBe("XHR");
       expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
-      expect(wrapper.find(".message-body .status").length).toBe(1);
       expect(wrapper.find("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1);
     });
   });
 
   describe("XHR POST request", () => {
     it("renders as expected", () => {
       const message = stubPreparedMessages.get("XHR POST request");
       const wrapper = render(NetworkEventMessage({ message, onViewSourceInDebugger, openNetworkPanel, openLink }));
 
       expect(wrapper.find(".message-body .method").text()).toBe("POST");
       expect(wrapper.find(".message-body .xhr").length).toBe(1);
       expect(wrapper.find(".message-body .xhr").text()).toBe("XHR");
       expect(wrapper.find(".message-body .url").length).toBe(1);
       expect(wrapper.find(".message-body .url").text()).toBe(EXPECTED_URL);
-      expect(wrapper.find(".message-body .status").length).toBe(1);
       expect(wrapper.find("div.message.cm-s-mozilla span.message-body.devtools-monospace").length).toBe(1);
     });
   });
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+class ObjectClient {
+}
+
+module.exports = ObjectClient;
--- a/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/stub-snippets.js
@@ -41,16 +41,27 @@ foo()
 
 consoleApi.set("console.time('bar')", {
   keys: ["console.time('bar')", "console.timeEnd('bar')"],
   code: `
 console.time("bar");
 console.timeEnd("bar");
 `});
 
+consoleApi.set("console.table('bar')", {
+  keys: ["console.table('bar')"],
+  code: `
+console.table('bar');
+`});
+
+consoleApi.set("console.table(['a', 'b', 'c'])", {
+  keys: ["console.table(['a', 'b', 'c'])"],
+  code: `
+console.table(['a', 'b', 'c']);
+`});
 // Evaluation Result
 
 const evaluationResultCommands = [
   "new Date(0)",
   "asdf()"
 ];
 
 let evaluationResult = new Map(evaluationResultCommands.map(cmd => [cmd, cmd]));
--- a/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/stubs/consoleApi.js
@@ -326,28 +326,85 @@ stubPreparedMessages.set("console.time('
 }));
 
 stubPreparedMessages.set("console.timeEnd('bar')", new ConsoleMessage({
 	"id": "1",
 	"allowRepeating": true,
 	"source": "console-api",
 	"type": "timeEnd",
 	"level": "log",
-	"messageText": "bar: 1.63ms",
+	"messageText": "bar: 1.81ms",
 	"parameters": null,
 	"repeat": 1,
-	"repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"timeEnd\",\"level\":\"log\",\"messageText\":\"bar: 1.63ms\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)\",\"line\":3,\"column\":1}}",
+	"repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"timeEnd\",\"level\":\"log\",\"messageText\":\"bar: 1.81ms\",\"parameters\":null,\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)\",\"line\":3,\"column\":1}}",
 	"stacktrace": null,
 	"frame": {
 		"source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.time(%27bar%27)",
 		"line": 3,
 		"column": 1
 	}
 }));
 
+stubPreparedMessages.set("console.table('bar')", new ConsoleMessage({
+	"id": "1",
+	"allowRepeating": true,
+	"source": "console-api",
+	"type": "log",
+	"level": "log",
+	"messageText": null,
+	"parameters": [
+		"bar"
+	],
+	"repeat": 1,
+	"repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"log\",\"level\":\"log\",\"messageText\":null,\"parameters\":[\"bar\"],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)\",\"line\":2,\"column\":1}}",
+	"stacktrace": null,
+	"frame": {
+		"source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)",
+		"line": 2,
+		"column": 1
+	}
+}));
+
+stubPreparedMessages.set("console.table(['a', 'b', 'c'])", new ConsoleMessage({
+	"id": "1",
+	"allowRepeating": true,
+	"source": "console-api",
+	"type": "table",
+	"level": "log",
+	"messageText": null,
+	"parameters": [
+		{
+			"type": "object",
+			"actor": "server1.conn14.child1/obj31",
+			"class": "Array",
+			"extensible": true,
+			"frozen": false,
+			"sealed": false,
+			"ownPropertyLength": 4,
+			"preview": {
+				"kind": "ArrayLike",
+				"length": 3,
+				"items": [
+					"a",
+					"b",
+					"c"
+				]
+			}
+		}
+	],
+	"repeat": 1,
+	"repeatId": "{\"id\":null,\"allowRepeating\":true,\"source\":\"console-api\",\"type\":\"table\",\"level\":\"log\",\"messageText\":null,\"parameters\":[{\"type\":\"object\",\"actor\":\"server1.conn14.child1/obj31\",\"class\":\"Array\",\"extensible\":true,\"frozen\":false,\"sealed\":false,\"ownPropertyLength\":4,\"preview\":{\"kind\":\"ArrayLike\",\"length\":3,\"items\":[\"a\",\"b\",\"c\"]}}],\"repeatId\":null,\"stacktrace\":null,\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)\",\"line\":2,\"column\":1}}",
+	"stacktrace": null,
+	"frame": {
+		"source": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)",
+		"line": 2,
+		"column": 1
+	}
+}));
+
 
 stubPackets.set("console.log('foobar', 'test')", {
 	"from": "server1.conn0.child1/consoleActor2",
 	"type": "consoleAPICall",
 	"message": {
 		"arguments": [
 			"foobar",
 			"test"
@@ -365,17 +422,17 @@ stubPackets.set("console.log('foobar', '
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
 		"styles": [],
-		"timeStamp": 1474329261562,
+		"timeStamp": 1474757913492,
 		"timer": null,
 		"workerType": "none",
 		"category": "webdev"
 	}
 });
 
 stubPackets.set("console.log(undefined)", {
 	"from": "server1.conn1.child1/consoleActor2",
@@ -399,17 +456,17 @@ stubPackets.set("console.log(undefined)"
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
 		"styles": [],
-		"timeStamp": 1474329262588,
+		"timeStamp": 1474757916196,
 		"timer": null,
 		"workerType": "none",
 		"category": "webdev"
 	}
 });
 
 stubPackets.set("console.warn('danger, will robinson!')", {
 	"from": "server1.conn2.child1/consoleActor2",
@@ -431,17 +488,17 @@ stubPackets.set("console.warn('danger, w
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
 		"styles": [],
-		"timeStamp": 1474329263650,
+		"timeStamp": 1474757918499,
 		"timer": null,
 		"workerType": "none",
 		"category": "webdev"
 	}
 });
 
 stubPackets.set("console.log(NaN)", {
 	"from": "server1.conn3.child1/consoleActor2",
@@ -465,17 +522,17 @@ stubPackets.set("console.log(NaN)", {
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
 		"styles": [],
-		"timeStamp": 1474329264822,
+		"timeStamp": 1474757920577,
 		"timer": null,
 		"workerType": "none",
 		"category": "webdev"
 	}
 });
 
 stubPackets.set("console.log(null)", {
 	"from": "server1.conn4.child1/consoleActor2",
@@ -499,17 +556,17 @@ stubPackets.set("console.log(null)", {
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
 		"styles": [],
-		"timeStamp": 1474329265855,
+		"timeStamp": 1474757922439,
 		"timer": null,
 		"workerType": "none",
 		"category": "webdev"
 	}
 });
 
 stubPackets.set("console.log('鼬')", {
 	"from": "server1.conn5.child1/consoleActor2",
@@ -531,17 +588,17 @@ stubPackets.set("console.log('鼬')", {
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
 		"styles": [],
-		"timeStamp": 1474329266922,
+		"timeStamp": 1474757924400,
 		"timer": null,
 		"workerType": "none",
 		"category": "webdev"
 	}
 });
 
 stubPackets.set("console.clear()", {
 	"from": "server1.conn6.child1/consoleActor2",
@@ -560,17 +617,17 @@ stubPackets.set("console.clear()", {
 			"appId": 0,
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
-		"timeStamp": 1474329267971,
+		"timeStamp": 1474757926626,
 		"timer": null,
 		"workerType": "none",
 		"styles": [],
 		"category": "webdev"
 	}
 });
 
 stubPackets.set("console.count('bar')", {
@@ -595,17 +652,17 @@ stubPackets.set("console.count('bar')", 
 			"appId": 0,
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
-		"timeStamp": 1474329269084,
+		"timeStamp": 1474757929281,
 		"timer": null,
 		"workerType": "none",
 		"styles": [],
 		"category": "webdev"
 	}
 });
 
 stubPackets.set("console.assert(false, {message: 'foobar'})", {
@@ -649,17 +706,17 @@ stubPackets.set("console.assert(false, {
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
 		"styles": [],
-		"timeStamp": 1474329270125,
+		"timeStamp": 1474757931800,
 		"timer": null,
 		"stacktrace": [
 			{
 				"columnNumber": 27,
 				"filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.assert(false%2C%20%7Bmessage%3A%20%27foobar%27%7D)",
 				"functionName": "triggerPacket",
 				"language": 2,
 				"lineNumber": 1
@@ -690,17 +747,17 @@ stubPackets.set("console.log('hello \nfr
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
 		"styles": [],
-		"timeStamp": 1474329271256,
+		"timeStamp": 1474757936217,
 		"timer": null,
 		"workerType": "none",
 		"category": "webdev"
 	}
 });
 
 stubPackets.set("console.log('úṇĩçödê țĕșť')", {
 	"from": "server1.conn10.child1/consoleActor2",
@@ -722,17 +779,17 @@ stubPackets.set("console.log('úṇĩçödê țĕșť')", {
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
 		"styles": [],
-		"timeStamp": 1474329272298,
+		"timeStamp": 1474757938480,
 		"timer": null,
 		"workerType": "none",
 		"category": "webdev"
 	}
 });
 
 stubPackets.set("console.trace()", {
 	"from": "server1.conn11.child1/consoleActor2",
@@ -751,17 +808,17 @@ stubPackets.set("console.trace()", {
 			"appId": 0,
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
-		"timeStamp": 1474329273375,
+		"timeStamp": 1474757940569,
 		"timer": null,
 		"stacktrace": [
 			{
 				"columnNumber": 3,
 				"filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.trace()",
 				"functionName": "testStacktraceFiltering",
 				"language": 2,
 				"lineNumber": 3
@@ -806,20 +863,20 @@ stubPackets.set("console.time('bar')", {
 			"appId": 0,
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
-		"timeStamp": 1474329274410,
+		"timeStamp": 1474757942740,
 		"timer": {
 			"name": "bar",
-			"started": 618.57
+			"started": 1220.705
 		},
 		"workerType": "none",
 		"styles": [],
 		"category": "webdev"
 	}
 });
 
 stubPackets.set("console.timeEnd('bar')", {
@@ -841,24 +898,105 @@ stubPackets.set("console.timeEnd('bar')"
 			"appId": 0,
 			"firstPartyDomain": "",
 			"inIsolatedMozBrowser": false,
 			"privateBrowsingId": 0,
 			"signedPkg": "",
 			"userContextId": 0
 		},
 		"private": false,
-		"timeStamp": 1474329274411,
+		"timeStamp": 1474757942742,
 		"timer": {
-			"duration": 1.3249999999999318,
+			"duration": 1.8100000000001728,
 			"name": "bar"
 		},
 		"workerType": "none",
 		"styles": [],
 		"category": "webdev"
 	}
 });
 
+stubPackets.set("console.table('bar')", {
+	"from": "server1.conn13.child1/consoleActor2",
+	"type": "consoleAPICall",
+	"message": {
+		"arguments": [
+			"bar"
+		],
+		"columnNumber": 1,
+		"counter": null,
+		"filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%27bar%27)",
+		"functionName": "triggerPacket",
+		"groupName": "",
+		"level": "table",
+		"lineNumber": 2,
+		"originAttributes": {
+			"addonId": "",
+			"appId": 0,
+			"firstPartyDomain": "",
+			"inIsolatedMozBrowser": false,
+			"privateBrowsingId": 0,
+			"signedPkg": "",
+			"userContextId": 0
+		},
+		"private": false,
+		"timeStamp": 1474757944789,
+		"timer": null,
+		"workerType": "none",
+		"styles": [],
+		"category": "webdev"
+	}
+});
+
+stubPackets.set("console.table(['a', 'b', 'c'])", {
+	"from": "server1.conn14.child1/consoleActor2",
+	"type": "consoleAPICall",
+	"message": {
+		"arguments": [
+			{
+				"type": "object",
+				"actor": "server1.conn14.child1/obj31",
+				"class": "Array",
+				"extensible": true,
+				"frozen": false,
+				"sealed": false,
+				"ownPropertyLength": 4,
+				"preview": {
+					"kind": "ArrayLike",
+					"length": 3,
+					"items": [
+						"a",
+						"b",
+						"c"
+					]
+				}
+			}
+		],
+		"columnNumber": 1,
+		"counter": null,
+		"filename": "http://example.com/browser/devtools/client/webconsole/new-console-output/test/fixtures/stub-generators/test-tempfile.js?key=console.table(%5B%27a%27%2C%20%27b%27%2C%20%27c%27%5D)",
+		"functionName": "triggerPacket",
+		"groupName": "",
+		"level": "table",
+		"lineNumber": 2,
+		"originAttributes": {
+			"addonId": "",
+			"appId": 0,
+			"firstPartyDomain": "",
+			"inIsolatedMozBrowser": false,
+			"privateBrowsingId": 0,
+			"signedPkg": "",
+			"userContextId": 0
+		},
+		"private": false,
+		"timeStamp": 1474757946731,
+		"timer": null,
+		"workerType": "none",
+		"styles": [],
+		"category": "webdev"
+	}
+});
+
 
 module.exports = {
   stubPreparedMessages,
   stubPackets,
 }
\ No newline at end of file
--- a/devtools/client/webconsole/new-console-output/test/helpers.js
+++ b/devtools/client/webconsole/new-console-output/test/helpers.js
@@ -2,17 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 let ReactDOM = require("devtools/client/shared/vendor/react-dom");
 let React = require("devtools/client/shared/vendor/react");
 var TestUtils = React.addons.TestUtils;
 
-const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const { configureStore } = require("devtools/client/webconsole/new-console-output/store");
 const { IdGenerator } = require("devtools/client/webconsole/new-console-output/utils/id-generator");
 const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 
 /**
  * Prepare actions for use in testing.
  */
 function setupActions() {
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -1,13 +1,15 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
   !/devtools/client/framework/test/shared-head.js
+  test-console-table.html
   test-console.html
 
+[browser_webconsole_console_table.js]
 [browser_webconsole_init.js]
 [browser_webconsole_input_focus.js]
 [browser_webconsole_observer_notifications.js]
 [browser_webconsole_vview_close_on_esc_key.js]
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_console_table.js
@@ -0,0 +1,173 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check console.table calls with all the test cases shown
+// in the MDN doc (https://developer.mozilla.org/en-US/docs/Web/API/Console/table)
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html";
+
+add_task(function* () {
+  let toolbox = yield openNewTabAndToolbox(TEST_URI, "webconsole");
+  let hud = toolbox.getCurrentPanel().hud;
+
+  function Person(firstName, lastName) {
+    this.firstName = firstName;
+    this.lastName = lastName;
+  }
+
+  const testCases = [{
+    info: "Testing when data argument is an array",
+    input: ["apples", "oranges", "bananas"],
+    expected: {
+      columns: ["(index)", "Values"],
+      rows: [
+        ["0", "apples"],
+        ["1", "oranges"],
+        ["2", "bananas"],
+      ]
+    }
+  }, {
+    info: "Testing when data argument is an object",
+    input: new Person("John", "Smith"),
+    expected: {
+      columns: ["(index)", "Values"],
+      rows: [
+        ["firstName", "John"],
+        ["lastName", "Smith"],
+      ]
+    }
+  }, {
+    info: "Testing when data argument is an array of arrays",
+    input: [["Jane", "Doe"], ["Emily", "Jones"]],
+    expected: {
+      columns: ["(index)", "0", "1"],
+      rows: [
+        ["0", "Jane", "Doe"],
+        ["1", "Emily", "Jones"],
+      ]
+    }
+  }, {
+    info: "Testing when data argument is an array of objects",
+    input: [
+      new Person("Jack", "Foo"),
+      new Person("Emma", "Bar"),
+      new Person("Michelle", "Rax"),
+    ],
+    expected: {
+      columns: ["(index)", "firstName", "lastName"],
+      rows: [
+        ["0", "Jack", "Foo"],
+        ["1", "Emma", "Bar"],
+        ["2", "Michelle", "Rax"],
+      ]
+    }
+  }, {
+    info: "Testing when data argument is an object whose properties are objects",
+    input: {
+      father: new Person("Darth", "Vader"),
+      daughter: new Person("Leia", "Organa"),
+      son: new Person("Luke", "Skywalker"),
+    },
+    expected: {
+      columns: ["(index)", "firstName", "lastName"],
+      rows: [
+        ["father", "Darth", "Vader"],
+        ["daughter", "Leia", "Organa"],
+        ["son", "Luke", "Skywalker"],
+      ]
+    }
+  }, {
+    info: "Testing when data argument is a Set",
+    input: new Set(["a", "b", "c"]),
+    expected: {
+      columns: ["(iteration index)", "Values"],
+      rows: [
+        ["0", "a"],
+        ["1", "b"],
+        ["2", "c"],
+      ]
+    }
+  }, {
+    info: "Testing when data argument is a Map",
+    input: new Map([["key-a", "value-a"], ["key-b", "value-b"]]),
+    expected: {
+      columns: ["(iteration index)", "Key", "Values"],
+      rows: [
+        ["0", "key-a", "value-a"],
+        ["1", "key-b", "value-b"],
+      ]
+    }
+  }, {
+    info: "Testing restricting the columns displayed",
+    input: [
+      new Person("Sam", "Wright"),
+      new Person("Elena", "Bartz"),
+    ],
+    headers: ["firstName"],
+    expected: {
+      columns: ["(index)", "firstName"],
+      rows: [
+        ["0", "Sam"],
+        ["1", "Elena"],
+      ]
+    }
+  }];
+
+  yield ContentTask.spawn(gBrowser.selectedBrowser, testCases, function (tests) {
+    tests.forEach((test) => {
+      content.wrappedJSObject.doConsoleTable(test.input, test.headers);
+    });
+  });
+
+  let nodes = [];
+  for (let testCase of testCases) {
+    let node = yield waitFor(
+      () => findConsoleTable(hud.ui.experimentalOutputNode, testCases.indexOf(testCase))
+    );
+    nodes.push(node);
+  }
+
+  let consoleTableNodes = hud.ui.experimentalOutputNode.querySelectorAll(
+    ".message .new-consoletable");
+
+  is(consoleTableNodes.length, testCases.length,
+    "console has the expected number of consoleTable items");
+
+  testCases.forEach((testCase, index) => {
+    info(testCase.info);
+
+    let node = nodes[index];
+    let columns = Array.from(node.querySelectorAll("thead th"));
+    let rows = Array.from(node.querySelectorAll("tbody tr"));
+
+    is(
+      JSON.stringify(testCase.expected.columns),
+      JSON.stringify(columns.map(column => column.textContent)),
+      "table has the expected columns"
+    );
+
+    is(testCase.expected.rows.length, rows.length,
+      "table has the expected number of rows");
+
+    testCase.expected.rows.forEach((expectedRow, rowIndex) => {
+      let row = rows[rowIndex];
+      let cells = row.querySelectorAll("td");
+      is(expectedRow.length, cells.length, "row has the expected number of cells");
+
+      expectedRow.forEach((expectedCell, cellIndex) => {
+        let cell = cells[cellIndex];
+        is(expectedCell, cell.textContent, "cell has the expected content");
+      });
+    });
+  });
+});
+
+function findConsoleTable(node, index) {
+  let condition = node.querySelector(
+    `.message:nth-of-type(${index + 1}) .new-consoletable`);
+  return condition;
+}
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_input_focus.js
@@ -2,17 +2,56 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that the input field is focused when the console is opened.
 
 "use strict";
 
-const TEST_URI = "data:text/html;charset=utf-8,Test input focus";
+const TEST_URI =
+  `data:text/html;charset=utf-8,Test input focused
+  <script>
+    console.log("console message 1");
+  </script>`;
 
 add_task(function* () {
   let hud = yield openNewTabAndConsole(TEST_URI);
   hud.jsterm.clearOutput();
 
   let inputNode = hud.jsterm.inputNode;
-  ok(inputNode.getAttribute("focused"), "input node is focused");
+  ok(inputNode.getAttribute("focused"), "input node is focused after output is cleared");
+
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+    content.wrappedJSObject.console.log("console message 2");
+  });
+  let msg = yield waitFor(() => findMessage(hud, "console message 2"));
+  let outputItem = msg.querySelector(".message-body");
+
+  inputNode = hud.jsterm.inputNode;
+  ok(inputNode.getAttribute("focused"), "input node is focused, first");
+
+  yield waitForBlurredInput(inputNode);
+
+  EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+  ok(inputNode.getAttribute("focused"), "input node is focused, second time");
+
+  yield waitForBlurredInput(inputNode);
+
+  info("Setting a text selection and making sure a click does not re-focus");
+  let selection = hud.iframeWindow.getSelection();
+  selection.selectAllChildren(outputItem);
+
+  EventUtils.sendMouseEvent({type: "click"}, hud.outputNode);
+  ok(!inputNode.getAttribute("focused"),
+    "input node focused after text is selected");
 });
+
+function waitForBlurredInput(inputNode) {
+  return new Promise(resolve => {
+    let lostFocus = () => {
+      ok(!inputNode.getAttribute("focused"), "input node is not focused");
+      resolve();
+    };
+    inputNode.addEventListener("blur", lostFocus, { once: true });
+    document.getElementById("urlbar").click();
+  });
+}
--- a/devtools/client/webconsole/new-console-output/test/mochitest/head.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/head.js
@@ -86,17 +86,17 @@ function waitForMessages({ hud, messages
  * @param number interval [optional]
  *        How often the predicate is invoked, in milliseconds.
  * @return object
  *         A promise that is resolved with the result of the condition.
  */
 function* waitFor(condition, message = "waitFor", interval = 100, maxTries = 50) {
   return new Promise(resolve => {
     BrowserTestUtils.waitForCondition(condition, message, interval, maxTries)
-      .then(resolve(condition()));
+      .then(() => resolve(condition()));
   });
 }
 
 /**
  * Find a message in the output.
  *
  * @param object hud
  *        The web console.
@@ -105,10 +105,10 @@ function* waitFor(condition, message = "
  * @param selector [optional]
  *        The selector to use in finding the message.
  */
 function findMessage(hud, text, selector = ".message") {
   const elements = Array.prototype.filter.call(
     hud.ui.experimentalOutputNode.querySelectorAll(selector),
     (el) => el.textContent.includes(text)
   );
-  return elements.pop();
+  return elements.length > 0 ? elements.pop() : false;
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/test-console-table.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Simple webconsole test page</title>
+  </head>
+  <body>
+    <p>console.table() test page</p>
+    <script>
+      function doConsoleTable(data, constrainedHeaders = null) {
+        if (constrainedHeaders) {
+          console.table(data, constrainedHeaders);
+        } else {
+          console.table(data);
+        }
+      }
+    </script>
+  </body>
+</html>
--- a/devtools/client/webconsole/new-console-output/test/requireHelper.js
+++ b/devtools/client/webconsole/new-console-output/test/requireHelper.js
@@ -25,10 +25,12 @@ requireHacker.global_hook("default", pat
   switch (path) {
     case "devtools/client/webconsole/utils":
       return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils")`;
     case "devtools/shared/l10n":
       return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/LocalizationHelper")`;
     case "Services":
     case "Services.default":
       return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/Services")`;
+    case "devtools/shared/client/main":
+      return `module.exports = require("devtools/client/webconsole/new-console-output/test/fixtures/ObjectClient")`;
   }
 });
--- a/devtools/client/webconsole/new-console-output/test/store/filters.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/filters.test.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const expect = require("expect");
 
-const actions = require("devtools/client/webconsole/new-console-output/actions/filters");
-const { messageAdd } = require("devtools/client/webconsole/new-console-output/actions/messages");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
+const { messageAdd } = require("devtools/client/webconsole/new-console-output/actions/index");
 const { ConsoleCommand } = require("devtools/client/webconsole/new-console-output/types");
 const { getAllMessages } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
 const { setupStore } = require("devtools/client/webconsole/new-console-output/test/helpers");
 const { MESSAGE_LEVEL } = require("devtools/client/webconsole/new-console-output/constants");
 const { stubPackets } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 const { stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
 
--- a/devtools/client/webconsole/new-console-output/test/store/messages.test.js
+++ b/devtools/client/webconsole/new-console-output/test/store/messages.test.js
@@ -6,16 +6,19 @@ const {
   getAllMessages,
   getAllMessagesUiById,
 } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const {
   setupActions,
   setupStore
 } = require("devtools/client/webconsole/new-console-output/test/helpers");
 const { stubPackets, stubPreparedMessages } = require("devtools/client/webconsole/new-console-output/test/fixtures/stubs/index");
+const {
+  MESSAGE_TYPE,
+} = require("devtools/client/webconsole/new-console-output/constants");
 
 const expect = require("expect");
 
 describe("Message reducer:", () => {
   let actions;
 
   before(() => {
     actions = setupActions();
@@ -112,16 +115,27 @@ describe("Message reducer:", () => {
       const { dispatch, getState } = setupStore([]);
 
       const message = stubPackets.get("console.time('bar')");
       dispatch(actions.messageAdd(message));
 
       const messages = getAllMessages(getState());
       expect(messages.size).toBe(0);
     });
+
+    it("adds console.table call with unsupported type as console.log", () => {
+      const { dispatch, getState } = setupStore([]);
+
+      const packet = stubPackets.get("console.table('bar')");
+      dispatch(actions.messageAdd(packet));
+
+      const messages = getAllMessages(getState());
+      const tableMessage = messages.last();
+      expect(tableMessage.level).toEqual(MESSAGE_TYPE.LOG);
+    });
   });
 
   describe("messagesUiById", () => {
     it("opens console.trace messages when they are added", () => {
       const { dispatch, getState } = setupStore([]);
 
       const message = stubPackets.get("console.trace()");
       dispatch(actions.messageAdd(message));
--- a/devtools/client/webconsole/new-console-output/utils/messages.js
+++ b/devtools/client/webconsole/new-console-output/utils/messages.js
@@ -76,16 +76,29 @@ function transformPacket(packet) {
             // if corresponding console.time() was called before.
             let duration = Math.round(timer.duration * 100) / 100;
             messageText = l10n.getFormatStr("timeEnd", [timer.name, duration]);
           } else {
             // If the `timer` property does not exists, we don't output anything.
             type = MESSAGE_TYPE.NULL_MESSAGE;
           }
           break;
+        case "table":
+          const supportedClasses = [
+            "Array", "Object", "Map", "Set", "WeakMap", "WeakSet"];
+          if (
+            !Array.isArray(parameters) ||
+            parameters.length === 0 ||
+            !supportedClasses.includes(parameters[0].class)
+          ) {
+            // If the class of the first parameter is not supported,
+            // we handle the call as a simple console.log
+            type = "log";
+          }
+          break;
       }
 
       const frame = message.filename ? {
         source: message.filename,
         line: message.lineNumber,
         column: message.columnNumber,
       } : null;
 
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -489,16 +489,20 @@ WebConsoleFrame.prototype = {
     // the returned promise because the console panel needs to be attached
     // to the toolbox before the web-console-created event is receieved.
     let notifyObservers = () => {
       let id = WebConsoleUtils.supportsString(this.hudId);
       Services.obs.notifyObservers(id, "web-console-created", null);
     };
     allReady.then(notifyObservers, notifyObservers);
 
+    if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
+      allReady.then(this.newConsoleOutput.init);
+    }
+
     return allReady;
   },
 
   /**
    * Connect to the server using the remote debugging protocol.
    *
    * @private
    * @return object
@@ -3268,16 +3272,23 @@ WebConsoleConnectionProxy.prototype = {
     let messageNodes = this.webConsoleFrame.experimentalOutputNode.querySelectorAll(".message");
     this.webConsoleFrame.emit("new-messages", {
       response: packet,
       node: messageNodes[messageNodes.length - 1],
     });
   },
 
   /**
+   * Batched dispatch of messages.
+   */
+  dispatchMessagesAdd: function(packets) {
+    this.webConsoleFrame.newConsoleOutput.dispatchMessagesAdd(packets);
+  },
+
+  /**
    * The "cachedMessages" response handler.
    *
    * @private
    * @param object response
    *        The JSON response object received from the server.
    */
   _onCachedMessages: function (response) {
     if (response.error) {
@@ -3296,19 +3307,17 @@ WebConsoleConnectionProxy.prototype = {
     let messages =
       response.messages.concat(...this.webConsoleClient.getNetworkEvents());
     messages.sort((a, b) => a.timeStamp - b.timeStamp);
 
     if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
       // Filter out CSS page errors.
       messages = messages.filter(message => !(message._type == "PageError"
           && Utils.categoryForScriptError(message) === CATEGORY_CSS));
-      for (let packet of messages) {
-        this.dispatchMessageAdd(packet);
-      }
+      this.dispatchMessagesAdd(messages);
     } else {
       this.webConsoleFrame.displayCachedMessages(messages);
       if (!this._hasNativeConsoleAPI) {
         this.webConsoleFrame.logWarningAboutReplacedAPI();
       }
     }
 
     this.connected = true;
--- a/devtools/client/webide/test/test_manifestUpdate.html
+++ b/devtools/client/webide/test/test_manifestUpdate.html
@@ -27,18 +27,21 @@
 
             function isProjectMarkedAsValid() {
               let details = win.frames[1];
               return !details.document.body.classList.contains("error");
             }
 
             let packagedAppLocation = getTestFilePath("app");
 
+            let onValidated = waitForUpdate(win, "project-validated");
+            let onDetails = waitForUpdate(win, "details");
             yield winProject.projectList.importPackagedApp(packagedAppLocation);
-            yield waitForUpdate(win, "details");
+            yield onValidated;
+            yield onDetails;
 
             let project = win.AppManager.selectedProject;
 
             ok("name" in project.manifest, "manifest includes name");
             is(project.name, project.manifest.name, "Display name uses manifest name");
             ok(isProjectMarkedAsValid(), "project is marked as valid");
 
             // Change the name
--- a/dom/events/TextComposition.cpp
+++ b/dom/events/TextComposition.cpp
@@ -434,20 +434,26 @@ TextComposition::HandleSelectionEvent(ns
   handler.OnSelectionEvent(aSelectionEvent);
 }
 
 uint32_t
 TextComposition::GetSelectionStartOffset()
 {
   nsCOMPtr<nsIWidget> widget = mPresContext->GetRootWidget();
   WidgetQueryContentEvent selectedTextEvent(true, eQuerySelectedText, widget);
-  if (mRanges && mRanges->HasClauses()) {
+  // Due to a bug of widget, mRanges may not be nullptr even though composition
+  // string is empty.  So, we need to check it here for avoiding to return
+  // odd start offset.
+  if (!mLastData.IsEmpty() && mRanges && mRanges->HasClauses()) {
     selectedTextEvent.InitForQuerySelectedText(
                         ToSelectionType(mRanges->GetFirstClause()->mRangeType));
   } else {
+    NS_WARNING_ASSERTION(
+      !mLastData.IsEmpty() || !mRanges || !mRanges->HasClauses(),
+      "Shouldn't have empty clause info when composition string is empty");
     selectedTextEvent.InitForQuerySelectedText(SelectionType::eNormal);
   }
 
   // The editor which has this composition is observed by active
   // IMEContentObserver, we can use the cache of it.
   RefPtr<IMEContentObserver> contentObserver =
     IMEStateManager::GetActiveContentObserver();
   bool doQuerySelection = true;
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -5616,32 +5616,32 @@ struct ConnectionPool::IdleResource
 {
   TimeStamp mIdleTime;
 
 protected:
   explicit
   IdleResource(const TimeStamp& aIdleTime);
 
   explicit
-  IdleResource(const IdleResource& aOther);
+  IdleResource(const IdleResource& aOther) = delete;
 
   ~IdleResource();
 };
 
 struct ConnectionPool::IdleDatabaseInfo final
   : public IdleResource
 {
   DatabaseInfo* mDatabaseInfo;
 
 public:
   MOZ_IMPLICIT
   IdleDatabaseInfo(DatabaseInfo* aDatabaseInfo);
 
   explicit
-  IdleDatabaseInfo(const IdleDatabaseInfo& aOther);
+  IdleDatabaseInfo(const IdleDatabaseInfo& aOther) = delete;
 
   ~IdleDatabaseInfo();
 
   bool
   operator==(const IdleDatabaseInfo& aOther) const
   {
     return mDatabaseInfo == aOther.mDatabaseInfo;
   }
@@ -5660,17 +5660,17 @@ struct ConnectionPool::IdleThreadInfo fi
 
 public:
   // Boo, this is needed because nsTArray::InsertElementSorted() doesn't yet
   // work with rvalue references.
   MOZ_IMPLICIT
   IdleThreadInfo(const ThreadInfo& aThreadInfo);
 
   explicit
-  IdleThreadInfo(const IdleThreadInfo& aOther);
+  IdleThreadInfo(const IdleThreadInfo& aOther) = delete;
 
   ~IdleThreadInfo();
 
   bool
   operator==(const IdleThreadInfo& aOther) const
   {
     return mThreadInfo.mRunnable == aOther.mThreadInfo.mRunnable &&
            mThreadInfo.mThread == aOther.mThreadInfo.mThread;
@@ -12963,26 +12963,16 @@ IdleResource::IdleResource(const TimeSta
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(!aIdleTime.IsNull());
 
   MOZ_COUNT_CTOR(ConnectionPool::IdleResource);
 }
 
 ConnectionPool::
-IdleResource::IdleResource(const IdleResource& aOther)
-  : mIdleTime(aOther.mIdleTime)
-{
-  AssertIsOnBackgroundThread();
-  MOZ_ASSERT(!aOther.mIdleTime.IsNull());
-
-  MOZ_COUNT_CTOR(ConnectionPool::IdleResource);
-}
-
-ConnectionPool::
 IdleResource::~IdleResource()
 {
   AssertIsOnBackgroundThread();
 
   MOZ_COUNT_DTOR(ConnectionPool::IdleResource);
 }
 
 ConnectionPool::
@@ -12995,27 +12985,16 @@ IdleDatabaseInfo::IdleDatabaseInfo(Datab
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aDatabaseInfo);
 
   MOZ_COUNT_CTOR(ConnectionPool::IdleDatabaseInfo);
 }
 
 ConnectionPool::
-IdleDatabaseInfo::IdleDatabaseInfo(const IdleDatabaseInfo& aOther)
-  : IdleResource(aOther)
-  , mDatabaseInfo(aOther.mDatabaseInfo)
-{
-  AssertIsOnBackgroundThread();
-  MOZ_ASSERT(mDatabaseInfo);
-
-  MOZ_COUNT_CTOR(ConnectionPool::IdleDatabaseInfo);
-}
-
-ConnectionPool::
 IdleDatabaseInfo::~IdleDatabaseInfo()
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(mDatabaseInfo);
 
   MOZ_COUNT_DTOR(ConnectionPool::IdleDatabaseInfo);
 }
 
@@ -13028,28 +13007,16 @@ IdleThreadInfo::IdleThreadInfo(const Thr
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(aThreadInfo.mRunnable);
   MOZ_ASSERT(aThreadInfo.mThread);
 
   MOZ_COUNT_CTOR(ConnectionPool::IdleThreadInfo);
 }
 
 ConnectionPool::
-IdleThreadInfo::IdleThreadInfo(const IdleThreadInfo& aOther)
-  : IdleResource(aOther)
-  , mThreadInfo(aOther.mThreadInfo)
-{
-  AssertIsOnBackgroundThread();
-  MOZ_ASSERT(mThreadInfo.mRunnable);
-  MOZ_ASSERT(mThreadInfo.mThread);
-
-  MOZ_COUNT_CTOR(ConnectionPool::IdleThreadInfo);
-}
-
-ConnectionPool::
 IdleThreadInfo::~IdleThreadInfo()
 {
   AssertIsOnBackgroundThread();
 
   MOZ_COUNT_DTOR(ConnectionPool::IdleThreadInfo);
 }
 
 ConnectionPool::
--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -129,16 +129,17 @@ MediaWMFNeeded=To play video formats %S,
 # LOCALIZATION NOTE: %S is a comma-separated list of codecs (e.g. 'video/mp4, video/webm')
 MediaUnsupportedBeforeWindowsVista=Video formats %S unsupported by Microsoft before Windows Vista
 # LOCALIZATION NOTE: %S is a comma-separated list of codecs (e.g. 'video/mp4, video/webm')
 MediaPlatformDecoderNotFound=The video on this page can’t be played. Your system may not have the required video codecs for: %S
 # LOCALIZATION NOTE: %S is a comma-separated list of codecs (e.g. 'video/mp4, video/webm')
 MediaCannotPlayNoDecoders=Cannot play media. No decoders for requested formats: %S
 # LOCALIZATION NOTE: %S is a comma-separated list of codecs (e.g. 'video/mp4, video/webm')
 MediaNoDecoders=No decoders for some of the requested formats: %S
+MediaCannotInitializePulseAudio=Unable to use PulseAudio
 # LOCALIZATION NOTE: Do not translate "MediaRecorder".
 MediaRecorderMultiTracksNotSupported=MediaRecorder does not support recording multiple tracks of the same type at this time.
 # LOCALIZATION NOTE: %S is the ID of the MediaStreamTrack passed to MediaStream.addTrack(). Do not translate "MediaStreamTrack" and "AudioChannel".
 MediaStreamAddTrackDifferentAudioChannel=MediaStreamTrack %S could not be added since it belongs to a different AudioChannel.
 # LOCALIZATION NOTE: Do not translate "MediaStream", "stop()" and "MediaStreamTrack"
 MediaStreamStopDeprecatedWarning=MediaStream.stop() is deprecated and will soon be removed. Use MediaStreamTrack.stop() instead.
 # LOCALIZATION NOTE: Do not translate "DOMException", "code" and "name"
 DOMExceptionCodeWarning=Use of DOMException’s code attribute is deprecated. Use name instead.
--- a/dom/media/AudioStream.cpp
+++ b/dom/media/AudioStream.cpp
@@ -346,17 +346,17 @@ AudioStream::Init(uint32_t aNumChannels,
 
   params.format = ToCubebFormat<AUDIO_OUTPUT_FORMAT>::value;
   mAudioClock.Init(aRate);
 
   cubeb* cubebContext = CubebUtils::GetCubebContext();
   if (!cubebContext) {
     NS_WARNING("Can't get cubeb context!");
     CubebUtils::ReportCubebStreamInitFailure(true);
-    return NS_ERROR_FAILURE;
+    return NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR;
   }
 
   return OpenCubeb(cubebContext, params, startTime, CubebUtils::GetFirstStream());
 }
 
 nsresult
 AudioStream::OpenCubeb(cubeb* aContext, cubeb_stream_params& aParams,
                        TimeStamp aStartTime, bool aIsFirst)
--- a/dom/media/DecoderDoctorDiagnostics.cpp
+++ b/dom/media/DecoderDoctorDiagnostics.cpp
@@ -12,16 +12,17 @@
 #include "nsContentUtils.h"
 #include "nsGkAtoms.h"
 #include "nsIDocument.h"
 #include "nsIObserverService.h"
 #include "nsIScriptError.h"
 #include "nsITimer.h"
 #include "nsIWeakReference.h"
 #include "nsPluginHost.h"
+#include "nsPrintfCString.h"
 #include "VideoUtils.h"
 
 #if defined(XP_WIN)
 #include "mozilla/WindowsVersion.h"
 #endif
 
 static mozilla::LazyLogModule sDecoderDoctorLog("DecoderDoctor");
 #define DD_LOG(level, arg, ...) MOZ_LOG(sDecoderDoctorLog, level, (arg, ##__VA_ARGS__))
@@ -75,20 +76,16 @@ private:
   static void DestroyPropertyCallback(void* aObject,
                                       nsIAtom* aPropertyName,
                                       void* aPropertyValue,
                                       void* aData);
 
   static const uint32_t sAnalysisPeriod_ms = 1000;
   void EnsureTimerIsStarted();
 
-  void ReportAnalysis(const NotificationAndReportStringId& aNotification,
-                      bool aIsSolved,
-                      const nsAString& aFormats);
-
   void SynthesizeAnalysis();
 
   // Raw pointer to an nsIDocument.
   // Must be non-null during construction.
   // Nulled when we want to stop watching, because either:
   // 1. The document has been destroyed (notified through
   //    DestroyPropertyCallback).
   // 2. We have not received new diagnostic information within a short time
@@ -262,26 +259,30 @@ static const NotificationAndReportString
   { dom::DecoderDoctorNotificationType::Platform_decoder_not_found,
     "MediaPlatformDecoderNotFound" };
 static const NotificationAndReportStringId sMediaCannotPlayNoDecoders =
   { dom::DecoderDoctorNotificationType::Cannot_play,
     "MediaCannotPlayNoDecoders" };
 static const NotificationAndReportStringId sMediaNoDecoders =
   { dom::DecoderDoctorNotificationType::Can_play_but_some_missing_decoders,
     "MediaNoDecoders" };
+static const NotificationAndReportStringId sCannotInitializePulseAudio =
+  { dom::DecoderDoctorNotificationType::Cannot_initialize_pulseaudio,
+    "MediaCannotInitializePulseAudio" };
 
 static const NotificationAndReportStringId*
 sAllNotificationsAndReportStringIds[] =
 {
   &sMediaWidevineNoWMFNoSilverlight,
   &sMediaWMFNeeded,
   &sMediaUnsupportedBeforeWindowsVista,
   &sMediaPlatformDecoderNotFound,
   &sMediaCannotPlayNoDecoders,
-  &sMediaNoDecoders
+  &sMediaNoDecoders,
+  &sCannotInitializePulseAudio
 };
 
 static void
 DispatchNotification(nsISupports* aSubject,
                      const NotificationAndReportStringId& aNotification,
                      bool aIsSolved,
                      const nsAString& aFormats)
 {
@@ -306,57 +307,68 @@ DispatchNotification(nsISupports* aSubje
   }
   DD_DEBUG("DecoderDoctorDiagnostics/DispatchEvent() %s", NS_ConvertUTF16toUTF8(json).get());
   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
   if (obs) {
     obs->NotifyObservers(aSubject, "decoder-doctor-notification", json.get());
   }
 }
 
-void
-DecoderDoctorDocumentWatcher::ReportAnalysis(
-  const NotificationAndReportStringId& aNotification,
-  bool aIsSolved,
-  const nsAString& aParams)
+static void
+ReportToConsole(nsIDocument* aDocument,
+                const char* aConsoleStringId,
+                const nsAString& aParams)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aDocument);
+
+  // 'params' will only be forwarded for non-empty strings.
+  const char16_t* params[1] = { aParams.Data() };
+  DD_DEBUG("DecoderDoctorDiagnostics.cpp:ReportToConsole(doc=%p) ReportToConsole - aMsg='%s' params[0]='%s'",
+           aDocument, aConsoleStringId,
+           aParams.IsEmpty() ? "<no params>" : NS_ConvertUTF16toUTF8(params[0]).get());
+  nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+                                  NS_LITERAL_CSTRING("Media"),
+                                  aDocument,
+                                  nsContentUtils::eDOM_PROPERTIES,
+                                  aConsoleStringId,
+                                  aParams.IsEmpty() ? nullptr : params,
+                                  aParams.IsEmpty() ? 0 : 1);
+}
+
+static void
+ReportAnalysis(nsIDocument* aDocument,
+               const NotificationAndReportStringId& aNotification,
+               bool aIsSolved,
+               const nsAString& aParams)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  if (!mDocument) {
+  if (!aDocument) {
     return;
   }
 
   // Report non-solved issues to console.
   if (!aIsSolved) {
-    // 'params' will only be forwarded for non-empty strings.
-    const char16_t* params[1] = { aParams.Data() };
-    DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::ReportAnalysis() ReportToConsole - aMsg='%s' params[0]='%s'",
-             this, mDocument, aNotification.mReportStringId,
-             aParams.IsEmpty() ? "<no params>" : NS_ConvertUTF16toUTF8(params[0]).get());
-    nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
-                                    NS_LITERAL_CSTRING("Media"),
-                                    mDocument,
-                                    nsContentUtils::eDOM_PROPERTIES,
-                                    aNotification.mReportStringId,
-                                    aParams.IsEmpty() ? nullptr : params,
-                                    aParams.IsEmpty() ? 0 : 1);
+    ReportToConsole(aDocument, aNotification.mReportStringId, aParams);
   }
 
   // "media.decoder-doctor.notifications-allowed" controls which notifications
   // may be dispatched to the front-end. It either contains:
   // - '*' -> Allow everything.
   // - Comma-separater list of ids -> Allow if aReportStringId (from
   //                                  dom.properties) is one of them.
   // - Nothing (missing or empty) -> Disable everything.
   nsAdoptingCString filter =
     Preferences::GetCString("media.decoder-doctor.notifications-allowed");
   filter.StripWhitespace();
   if (filter.EqualsLiteral("*")
       || StringListContains(filter, aNotification.mReportStringId)) {
     DispatchNotification(
-      mDocument->GetInnerWindow(), aNotification, aIsSolved, aParams);
+      aDocument->GetInnerWindow(), aNotification, aIsSolved, aParams);
   }
 }
 
 enum SilverlightPresence {
   eNoSilverlight,
   eSilverlightDisabled,
   eSilverlightEnabled
 };
@@ -460,16 +472,20 @@ DecoderDoctorDocumentWatcher::Synthesize
                               diag.mDecoderDoctorDiagnostics.KeySystem());
           DecoderDoctorDiagnostics::KeySystemIssue issue =
             diag.mDecoderDoctorDiagnostics.GetKeySystemIssue();
           if (issue != DecoderDoctorDiagnostics::eUnset) {
             lastKeySystemIssue = issue;
           }
         }
         break;
+      case DecoderDoctorDiagnostics::eEvent:
+        // Events shouldn't be stored for processing.
+        MOZ_ASSERT(false);
+        break;
       default:
         MOZ_ASSERT(diag.mDecoderDoctorDiagnostics.Type()
                      == DecoderDoctorDiagnostics::eFormatSupportCheck
                    || diag.mDecoderDoctorDiagnostics.Type()
                         == DecoderDoctorDiagnostics::eMediaKeySystemAccessRequest);
         break;
     }
   }
@@ -502,17 +518,17 @@ DecoderDoctorDocumentWatcher::Synthesize
         for (const auto& workingFormat : MakeStringListRange(*workingFormats)) {
           if (FormatsListContains(formatsWithIssues, workingFormat)) {
             // This now-working format used not to work -> Report solved issue.
             DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - %s solved ('%s' now works, it was in pref(%s)='%s')",
                     this, mDocument, id->mReportStringId,
                     NS_ConvertUTF16toUTF8(workingFormat).get(),
                     formatsPref.Data(),
                     NS_ConvertUTF16toUTF8(formatsWithIssues).get());
-            ReportAnalysis(*id, true, workingFormat);
+            ReportAnalysis(mDocument, *id, true, workingFormat);
             // This particular Notification&ReportId has been solved, no need
             // to keep looking at other keysys/formats that might solve it too.
             solved = true;
             break;
           }
         }
         if (solved) {
           break;
@@ -529,18 +545,18 @@ DecoderDoctorDocumentWatcher::Synthesize
   // Look at Key System issues first, as they take precedence over format checks.
   if (!unsupportedKeySystems.IsEmpty() && supportedKeySystems.IsEmpty()) {
     // No supported key systems!
     switch (lastKeySystemIssue) {
       case DecoderDoctorDiagnostics::eWidevineWithNoWMF:
         if (CheckSilverlight() != eSilverlightEnabled) {
           DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unsupported key systems: %s, widevine without WMF nor Silverlight",
                   this, mDocument, NS_ConvertUTF16toUTF8(unsupportedKeySystems).get());
-          ReportAnalysis(
-            sMediaWidevineNoWMFNoSilverlight, false, unsupportedKeySystems);
+          ReportAnalysis(mDocument, sMediaWidevineNoWMFNoSilverlight,
+                         false, unsupportedKeySystems);
           return;
         }
         break;
       default:
         break;
     }
   }
 
@@ -550,65 +566,67 @@ DecoderDoctorDocumentWatcher::Synthesize
     if (playableFormats.IsEmpty()) {
       // No requested formats can be played. See if we can help the user, by
       // going through expected decoders from most to least desirable.
 #if defined(XP_WIN)
       if (!formatsRequiringWMF.IsEmpty()) {
         if (IsVistaOrLater()) {
           DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media because WMF was not found",
                   this, mDocument, NS_ConvertUTF16toUTF8(formatsRequiringWMF).get());
-          ReportAnalysis(sMediaWMFNeeded, false, formatsRequiringWMF);
+          ReportAnalysis(mDocument, sMediaWMFNeeded, false, formatsRequiringWMF);
         } else {
           DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media before Windows Vista",
                   this, mDocument, NS_ConvertUTF16toUTF8(formatsRequiringWMF).get());
-          ReportAnalysis(sMediaUnsupportedBeforeWindowsVista, false, formatsRequiringWMF);
+          ReportAnalysis(mDocument, sMediaUnsupportedBeforeWindowsVista,
+                         false, formatsRequiringWMF);
         }
         return;
       }
 #endif
 #if defined(MOZ_FFMPEG)
       if (!formatsRequiringFFMpeg.IsEmpty()) {
         DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media because platform decoder was not found",
                 this, mDocument, NS_ConvertUTF16toUTF8(formatsRequiringFFMpeg).get());
-        ReportAnalysis(sMediaPlatformDecoderNotFound,
+        ReportAnalysis(mDocument, sMediaPlatformDecoderNotFound,
                        false, formatsRequiringFFMpeg);
         return;
       }
 #endif
       DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Cannot play media, unplayable formats: %s",
               this, mDocument, NS_ConvertUTF16toUTF8(unplayableFormats).get());
-      ReportAnalysis(sMediaCannotPlayNoDecoders, false, unplayableFormats);
+      ReportAnalysis(mDocument, sMediaCannotPlayNoDecoders,
+                     false, unplayableFormats);
       return;
     }
 
     DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can play media, but no decoders for some requested formats: %s",
             this, mDocument, NS_ConvertUTF16toUTF8(unplayableFormats).get());
     if (Preferences::GetBool("media.decoder-doctor.verbose", false)) {
-      ReportAnalysis(sMediaNoDecoders, false, unplayableFormats);
+      ReportAnalysis(mDocument, sMediaNoDecoders, false, unplayableFormats);
     }
     return;
   }
   DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can play media, decoders available for all requested formats",
            this, mDocument);
 }
 
 void
 DecoderDoctorDocumentWatcher::AddDiagnostics(DecoderDoctorDiagnostics&& aDiagnostics,
                                              const char* aCallSite)
 {
   MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aDiagnostics.Type() != DecoderDoctorDiagnostics::eEvent);
 
   if (!mDocument) {
     return;
   }
 
   DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics(DecoderDoctorDiagnostics{%s}, call site '%s')",
            this, mDocument, aDiagnostics.GetDescription().Data(), aCallSite);
-  mDiagnosticsSequence.AppendElement(
-    Diagnostics(Move(aDiagnostics), aCallSite));
+  mDiagnosticsSequence.AppendElement(Diagnostics(Move(aDiagnostics), aCallSite));
   EnsureTimerIsStarted();
 }
 
 NS_IMETHODIMP
 DecoderDoctorDocumentWatcher::Notify(nsITimer* timer)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(timer == mTimer);
@@ -706,36 +724,81 @@ DecoderDoctorDiagnostics::StoreMediaKeyS
     return;
   }
 
   RefPtr<DecoderDoctorDocumentWatcher> watcher =
     DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument);
 
   if (NS_WARN_IF(!watcher)) {
     DD_WARN("DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(nsIDocument* aDocument=%p, keysystem='%s', supported=%d, call site '%s') - Could not create document watcher",
-            this, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported, aCallSite);
+            this, aDocument, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported, aCallSite);
     return;
   }
 
   mKeySystem = aKeySystem;
   mIsKeySystemSupported = aIsSupported;
 
-  // StoreDiagnostics should only be called once, after all data is available,
-  // so it is safe to Move() from this object.
+  // StoreMediaKeySystemAccess should only be called once, after all data is
+  // available, so it is safe to Move() from this object.
   watcher->AddDiagnostics(Move(*this), aCallSite);
   // Even though it's moved-from, the type should stay set
   // (Only used to ensure that we do store only once.)
   MOZ_ASSERT(mDiagnosticsType == eMediaKeySystemAccessRequest);
 }
 
+void
+DecoderDoctorDiagnostics::StoreEvent(nsIDocument* aDocument,
+                                     const DecoderDoctorEvent& aEvent,
+                                     const char* aCallSite)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  // Make sure we only store once.
+  MOZ_ASSERT(mDiagnosticsType == eUnsaved);
+  mDiagnosticsType = eEvent;
+  mEvent = aEvent;
+
+  if (NS_WARN_IF(!aDocument)) {
+    DD_WARN("DecoderDoctorDiagnostics[%p]::StoreEvent(nsIDocument* aDocument=nullptr, aEvent=%s, call site '%s')",
+            this, GetDescription().get(), aCallSite);
+    return;
+  }
+
+  // Don't keep events for later processing, just handle them now.
+#ifdef MOZ_PULSEAUDIO
+  switch (aEvent.mDomain) {
+    case DecoderDoctorEvent::eAudioSinkStartup:
+      if (aEvent.mResult == NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR) {
+        DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics() - unable to initialize PulseAudio",
+                this, aDocument);
+        ReportAnalysis(aDocument, sCannotInitializePulseAudio,
+                       false, NS_LITERAL_STRING("*"));
+      } else if (aEvent.mResult == NS_OK) {
+        DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics() - now able to initialize PulseAudio",
+                this, aDocument);
+        ReportAnalysis(aDocument, sCannotInitializePulseAudio,
+                       true, NS_LITERAL_STRING("*"));
+      }
+      break;
+  }
+#endif // MOZ_PULSEAUDIO
+}
+
+static const char*
+EventDomainString(DecoderDoctorEvent::Domain aDomain)
+{
+  switch (aDomain) {
+    case DecoderDoctorEvent::eAudioSinkStartup:
+      return "audio-sink-startup";
+  }
+  return "?";
+}
+
 nsCString
 DecoderDoctorDiagnostics::GetDescription() const
 {
-  MOZ_ASSERT(mDiagnosticsType == eFormatSupportCheck
-             || mDiagnosticsType == eMediaKeySystemAccessRequest);
   nsCString s;
   switch (mDiagnosticsType) {
     case eUnsaved:
       s = "Unsaved diagnostics, cannot get accurate description";
       break;
     case eFormatSupportCheck:
       s = "format='";
       s += NS_ConvertUTF16toUTF8(mFormat).get();
@@ -767,16 +830,21 @@ DecoderDoctorDiagnostics::GetDescription
       switch (mKeySystemIssue) {
         case eUnset:
           break;
         case eWidevineWithNoWMF:
           s += ", Widevine with no WMF";
           break;
       }
       break;
+    case eEvent:
+      s = nsPrintfCString("event domain %s result=%u",
+                          EventDomainString(mEvent.mDomain), mEvent.mResult);
+      break;
     default:
+      MOZ_ASSERT_UNREACHABLE("Unexpected DiagnosticsType");
       s = "?";
       break;
   }
   return s;
 }
 
 } // namespace mozilla
--- a/dom/media/DecoderDoctorDiagnostics.h
+++ b/dom/media/DecoderDoctorDiagnostics.h
@@ -8,16 +8,23 @@
 #define DecoderDoctorDiagnostics_h_
 
 #include "nsString.h"
 
 class nsIDocument;
 
 namespace mozilla {
 
+struct DecoderDoctorEvent {
+  enum Domain {
+    eAudioSinkStartup,
+  } mDomain;
+  nsresult mResult;
+};
+
 // DecoderDoctorDiagnostics class, used to gather data from PDMs/DecoderTraits,
 // and then notify the user about issues preventing (or worsening) playback.
 //
 // The expected usage is:
 // 1. Instantiate a DecoderDoctorDiagnostics in a function (close to the point
 //    where a webpage is trying to know whether some MIME types can be played,
 //    or trying to play a media file).
 // 2. Pass a pointer to the DecoderDoctorDiagnostics structure to one of the
@@ -41,20 +48,25 @@ public:
                               bool aCanPlay,
                               const char* aCallSite);
 
   void StoreMediaKeySystemAccess(nsIDocument* aDocument,
                                  const nsAString& aKeySystem,
                                  bool aIsSupported,
                                  const char* aCallSite);
 
+  void StoreEvent(nsIDocument* aDocument,
+                  const DecoderDoctorEvent& aEvent,
+                  const char* aCallSite);
+
   enum DiagnosticsType {
     eUnsaved,
     eFormatSupportCheck,
-    eMediaKeySystemAccessRequest
+    eMediaKeySystemAccessRequest,
+    eEvent
   };
   DiagnosticsType Type() const { return mDiagnosticsType; }
 
   // Description string, for logging purposes; only call on stored diags.
   nsCString GetDescription() const;
 
   // Methods to record diagnostic information:
 
@@ -65,18 +77,18 @@ public:
   bool DidWMFFailToLoad() const { return mWMFFailedToLoad; }
 
   void SetFFmpegFailedToLoad() { mFFmpegFailedToLoad = true; }
   bool DidFFmpegFailToLoad() const { return mFFmpegFailedToLoad; }
 
   void SetGMPPDMFailedToStartup() { mGMPPDMFailedToStartup = true; }
   bool DidGMPPDMFailToStartup() const { return mGMPPDMFailedToStartup; }
 
-  void SetVideoFormatNotSupport() { mVideoNotSupported = true; }
-  void SetAudioFormatNotSupport() { mAudioNotSupported = true; }
+  void SetVideoNotSupported() { mVideoNotSupported = true; }
+  void SetAudioNotSupported() { mAudioNotSupported = true; }
 
   void SetGMP(const nsACString& aGMP) { mGMP = aGMP; }
   const nsACString& GMP() const { return mGMP; }
 
   const nsAString& KeySystem() const { return mKeySystem; }
   bool IsKeySystemSupported() const { return mIsKeySystemSupported; }
   enum KeySystemIssue {
     eUnset,
@@ -86,16 +98,21 @@ public:
   {
     mKeySystemIssue = aKeySystemIssue;
   }
   KeySystemIssue GetKeySystemIssue() const
   {
     return mKeySystemIssue;
   }
 
+  DecoderDoctorEvent event() const
+  {
+    return mEvent;
+  }
+
 private:
   // Currently-known type of diagnostics. Set from one of the 'Store...' methods.
   // This helps ensure diagnostics are only stored once,
   // and makes it easy to know what information they contain.
   DiagnosticsType mDiagnosticsType = eUnsaved;
 
   nsString mFormat;
   // True if there is at least one decoder that can play that format.
@@ -106,13 +123,15 @@ private:
   bool mGMPPDMFailedToStartup = false;
   bool mVideoNotSupported = false;
   bool mAudioNotSupported = false;
   nsCString mGMP;
 
   nsString mKeySystem;
   bool mIsKeySystemSupported = false;
   KeySystemIssue mKeySystemIssue = eUnset;
+
+  DecoderDoctorEvent mEvent;
 };
 
 } // namespace mozilla
 
 #endif
--- a/dom/media/MediaDecoder.cpp
+++ b/dom/media/MediaDecoder.cpp
@@ -602,16 +602,17 @@ MediaDecoder::Shutdown()
   // necessary to unblock the state machine thread if it's blocked, so
   // the asynchronous shutdown in nsDestroyStateMachine won't deadlock.
   if (mDecoderStateMachine) {
     mTimedMetadataListener.Disconnect();
     mMetadataLoadedListener.Disconnect();
     mFirstFrameLoadedListener.Disconnect();
     mOnPlaybackEvent.Disconnect();
     mOnPlaybackErrorEvent.Disconnect();
+    mOnDecoderDoctorEvent.Disconnect();
     mOnMediaNotSeekable.Disconnect();
 
     mDecoderStateMachine->BeginShutdown()
       ->Then(AbstractThread::MainThread(), __func__, this,
              &MediaDecoder::FinishShutdown,
              &MediaDecoder::FinishShutdown);
   } else {
     // Ensure we always unregister asynchronously in order not to disrupt
@@ -675,16 +676,34 @@ MediaDecoder::OnPlaybackEvent(MediaEvent
 
 void
 MediaDecoder::OnPlaybackErrorEvent(const MediaResult& aError)
 {
   DecodeError(aError);
 }
 
 void
+MediaDecoder::OnDecoderDoctorEvent(DecoderDoctorEvent aEvent)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  // OnDecoderDoctorEvent is disconnected at shutdown time.
+  MOZ_ASSERT(!IsShutdown());
+  HTMLMediaElement* element = mOwner->GetMediaElement();
+  if (!element) {
+    return;
+  }
+  nsIDocument* doc = element->OwnerDoc();
+  if (!doc) {
+    return;
+  }
+  DecoderDoctorDiagnostics diags;
+  diags.StoreEvent(doc, aEvent, __func__);
+}
+
+void
 MediaDecoder::FinishShutdown()
 {
   MOZ_ASSERT(NS_IsMainThread());
   mDecoderStateMachine->BreakCycles();
   SetStateMachine(nullptr);
   mVideoFrameContainer = nullptr;
   MediaShutdownManager::Instance().Unregister(this);
 }
@@ -752,16 +771,18 @@ MediaDecoder::SetStateMachineParameters(
     AbstractThread::MainThread(), this, &MediaDecoder::MetadataLoaded);
   mFirstFrameLoadedListener = mDecoderStateMachine->FirstFrameLoadedEvent().Connect(
     AbstractThread::MainThread(), this, &MediaDecoder::FirstFrameLoaded);
 
   mOnPlaybackEvent = mDecoderStateMachine->OnPlaybackEvent().Connect(
     AbstractThread::MainThread(), this, &MediaDecoder::OnPlaybackEvent);
   mOnPlaybackErrorEvent = mDecoderStateMachine->OnPlaybackErrorEvent().Connect(
     AbstractThread::MainThread(), this, &MediaDecoder::OnPlaybackErrorEvent);
+  mOnDecoderDoctorEvent = mDecoderStateMachine->OnDecoderDoctorEvent().Connect(
+    AbstractThread::MainThread(), this, &MediaDecoder::OnDecoderDoctorEvent);
   mOnMediaNotSeekable = mDecoderStateMachine->OnMediaNotSeekable().Connect(
     AbstractThread::MainThread(), this, &MediaDecoder::OnMediaNotSeekable);
 }
 
 void
 MediaDecoder::SetMinimizePrerollUntilPlaybackStarts()
 {
   MOZ_ASSERT(NS_IsMainThread());
--- a/dom/media/MediaDecoder.h
+++ b/dom/media/MediaDecoder.h
@@ -22,16 +22,17 @@
 #include "necko-config.h"
 #include "nsAutoPtr.h"
 #include "nsCOMPtr.h"
 #include "nsIObserver.h"
 #include "nsISupports.h"
 #include "nsITimer.h"
 
 #include "AbstractMediaDecoder.h"
+#include "DecoderDoctorDiagnostics.h"
 #include "MediaDecoderOwner.h"
 #include "MediaEventSource.h"
 #include "MediaMetadataManager.h"
 #include "MediaResource.h"
 #include "MediaResourceCallback.h"
 #include "MediaStatistics.h"
 #include "MediaStreamGraph.h"
 #include "TimeUnits.h"
@@ -589,16 +590,18 @@ private:
                       MediaDecoderEventVisibility aEventVisibility);
 
   MediaEventSource<void>*
   DataArrivedEvent() override { return &mDataArrivedEvent; }
 
   void OnPlaybackEvent(MediaEventType aEvent);
   void OnPlaybackErrorEvent(const MediaResult& aError);
 
+  void OnDecoderDoctorEvent(DecoderDoctorEvent aEvent);
+
   void OnMediaNotSeekable()
   {
     SetMediaSeekable(false);
   }
 
   void FinishShutdown();
 
   void ConnectMirrors(MediaDecoderStateMachine* aObject);
@@ -728,16 +731,17 @@ protected:
   // A listener to receive metadata updates from MDSM.
   MediaEventListener mTimedMetadataListener;
 
   MediaEventListener mMetadataLoadedListener;
   MediaEventListener mFirstFrameLoadedListener;
 
   MediaEventListener mOnPlaybackEvent;
   MediaEventListener mOnPlaybackErrorEvent;
+  MediaEventListener mOnDecoderDoctorEvent;
   MediaEventListener mOnMediaNotSeekable;
 
 protected:
   // Whether the state machine is shut down.
   Mirror<bool> mStateMachineIsShutdown;
 
   // Buffered range, mirrored from the reader.
   Mirror<media::TimeIntervals> mBuffered;
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -2872,27 +2872,36 @@ void MediaDecoderStateMachine::OnMediaSi
   MOZ_ASSERT(OnTaskQueue());
   MOZ_ASSERT(mInfo.HasAudio());
   VERBOSE_LOG("[%s]", __func__);
 
   mMediaSinkAudioPromise.Complete();
   mAudioCompleted = true;
   // To notify PlaybackEnded as soon as possible.
   ScheduleStateMachine();
+
+  // Report OK to Decoder Doctor (to know if issue may have been resolved).
+  mOnDecoderDoctorEvent.Notify(
+    DecoderDoctorEvent{DecoderDoctorEvent::eAudioSinkStartup, NS_OK});
 }
 
-void MediaDecoderStateMachine::OnMediaSinkAudioError()
+void MediaDecoderStateMachine::OnMediaSinkAudioError(nsresult aResult)
 {
   MOZ_ASSERT(OnTaskQueue());
   MOZ_ASSERT(mInfo.HasAudio());
   VERBOSE_LOG("[%s]", __func__);
 
   mMediaSinkAudioPromise.Complete();
   mAudioCompleted = true;
 
+  // Result should never be NS_OK in this *error* handler. Report to Dec-Doc.
+  MOZ_ASSERT(NS_FAILED(aResult));
+  mOnDecoderDoctorEvent.Notify(
+    DecoderDoctorEvent{DecoderDoctorEvent::eAudioSinkStartup, aResult});
+
   // Make the best effort to continue playback when there is video.
   if (HasVideo()) {
     return;
   }
 
   // Otherwise notify media decoder/element about this error for it makes
   // no sense to play an audio-only file without sound output.
   DecodeError(MediaResult(NS_ERROR_DOM_MEDIA_MEDIASINK_ERR, __func__));
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -248,16 +248,19 @@ public:
                       MediaDecoderEventVisibility>&
   FirstFrameLoadedEvent() { return mFirstFrameLoadedEvent; }
 
   MediaEventSource<MediaEventType>&
   OnPlaybackEvent() { return mOnPlaybackEvent; }
   MediaEventSource<MediaResult>&
   OnPlaybackErrorEvent() { return mOnPlaybackErrorEvent; }
 
+  MediaEventSource<DecoderDoctorEvent>&
+  OnDecoderDoctorEvent() { return mOnDecoderDoctorEvent; }
+
   size_t SizeOfVideoQueue() const;
 
   size_t SizeOfAudioQueue() const;
 
 private:
   class StateObject;
   class DecodeMetadataState;
   class WaitForCDMState;
@@ -556,17 +559,17 @@ protected:
 
 private:
   // Resolved by the MediaSink to signal that all audio/video outstanding
   // work is complete and identify which part(a/v) of the sink is shutting down.
   void OnMediaSinkAudioComplete();
   void OnMediaSinkVideoComplete();
 
   // Rejected by the MediaSink to signal errors for audio/video.
-  void OnMediaSinkAudioError();
+  void OnMediaSinkAudioError(nsresult aResult);
   void OnMediaSinkVideoError();
 
   // Return true if the video decoder's decode speed can not catch up the
   // play time.
   bool NeedToSkipToNextKeyframe();
 
   void* const mDecoderID;
   const RefPtr<FrameStatistics> mFrameStats;
@@ -859,16 +862,18 @@ private:
                         nsAutoPtr<MetadataTags>,
                         MediaDecoderEventVisibility> mMetadataLoadedEvent;
   MediaEventProducerExc<nsAutoPtr<MediaInfo>,
                         MediaDecoderEventVisibility> mFirstFrameLoadedEvent;
 
   MediaEventProducer<MediaEventType> mOnPlaybackEvent;
   MediaEventProducer<MediaResult> mOnPlaybackErrorEvent;
 
+  MediaEventProducer<DecoderDoctorEvent> mOnDecoderDoctorEvent;
+
   // True if audio is offloading.
   // Playback will not start when audio is offloading.
   bool mAudioOffloading;
 
 #ifdef MOZ_EME
   void OnCDMProxyReady(RefPtr<CDMProxy> aProxy);
   void OnCDMProxyNotReady();
   RefPtr<CDMProxy> mCDMProxy;
--- a/dom/media/ipc/VideoDecoderChild.cpp
+++ b/dom/media/ipc/VideoDecoderChild.cpp
@@ -16,16 +16,17 @@ namespace dom {
 
 using base::Thread;
 using namespace ipc;
 using namespace layers;
 using namespace gfx;
 
 VideoDecoderChild::VideoDecoderChild()
   : mThread(VideoDecoderManagerChild::GetManagerThread())
+  , mLayersBackend(layers::LayersBackend::LAYERS_NONE)
   , mCanSend(true)
 {
 }
 
 VideoDecoderChild::~VideoDecoderChild()
 {
   AssertOnManagerThread();
   mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
--- a/dom/media/platforms/PDMFactory.cpp
+++ b/dom/media/platforms/PDMFactory.cpp
@@ -233,19 +233,19 @@ PDMFactory::CreateDecoderWithPDM(Platfor
   const TrackInfo& config = aParams.mConfig;
   supportChecker.AddMediaFormatChecker(config);
 
   auto reason = supportChecker.Check();
   if (reason != SupportChecker::Result::kSupported) {
     DecoderDoctorDiagnostics* diagnostics = aParams.mDiagnostics;
     if (diagnostics) {
       if (reason == SupportChecker::Result::kVideoFormatNotSupported) {
-        diagnostics->SetVideoFormatNotSupport();
+        diagnostics->SetVideoNotSupported();
       } else if (reason == SupportChecker::Result::kAudioFormatNotSupported) {
-        diagnostics->SetAudioFormatNotSupport();
+        diagnostics->SetAudioNotSupported();
       }
     }
     return nullptr;
   }
 
   if (config.IsAudio()) {
     m = aPDM->CreateAudioDecoder(aParams);
     return m.forget();
--- a/dom/media/platforms/wrappers/H264Converter.cpp
+++ b/dom/media/platforms/wrappers/H264Converter.cpp
@@ -173,17 +173,17 @@ H264Converter::CreateDecoder(DecoderDoct
 
   mp4_demuxer::SPSData spsdata;
   if (mp4_demuxer::H264::DecodeSPSFromExtraData(mCurrentConfig.mExtraData, spsdata)) {
     // Do some format check here.
     // WMF H.264 Video Decoder and Apple ATDecoder do not support YUV444 format.
     if (spsdata.chroma_format_idc == 3 /*YUV444*/) {
       mLastError = NS_ERROR_FAILURE;
       if (aDiagnostics) {
-        aDiagnostics->SetVideoFormatNotSupport();
+        aDiagnostics->SetVideoNotSupported();
       }
       return NS_ERROR_FAILURE;
     }
   } else {
     // SPS was invalid.
     mLastError = NS_ERROR_FAILURE;
     return NS_ERROR_FAILURE;
   }
--- a/dom/media/test/external/external_media_tests/media_utils/video_puppeteer.py
+++ b/dom/media/test/external/external_media_tests/media_utils/video_puppeteer.py
@@ -54,16 +54,17 @@ class VideoPuppeteer(object):
      of the video.
     :param stall_wait_time: The amount of time to wait to see if a stall has
      cleared. If 0, do not check for stalls.
     :param timeout: The amount of time to wait until the video starts.
     """
 
     _video_var_script = (
         'var video = arguments[0];'
+        'var baseURI = arguments[0].baseURI;'
         'var currentTime = video.wrappedJSObject.currentTime;'
         'var duration = video.wrappedJSObject.duration;'
         'var buffered = video.wrappedJSObject.buffered;'
         'var bufferedRanges = [];'
         'for (var i = 0; i < buffered.length; i++) {'
         'bufferedRanges.push([buffered.start(i), buffered.end(i)]);'
         '}'
         'var played = video.wrappedJSObject.played;'
@@ -198,17 +199,17 @@ class VideoPuppeteer(object):
         if self._last_seen_video_state.remaining_time < self.interval:
             return True
 
         # Check to see if the video has stalled. Accumulate the amount of lag
         # since the video started, and if it is too high, then raise.
         if (self.stall_wait_time and
                 self._last_seen_video_state.lag > self.stall_wait_time):
             raise VideoException('Video {} stalled.\n{}'
-                                 .format(self._last_seen_video_state.video_url,
+                                 .format(self._last_seen_video_state.video_uri,
                                          self))
 
         # We are cruising, so we are not done.
         return False
 
     def _update_expected_duration(self):
         """
         Update the duration of the target video at self.test_url (in seconds).
@@ -246,17 +247,18 @@ class VideoPuppeteer(object):
 
     @staticmethod
     def _video_state_named_tuple():
         """
         Create a named tuple class that can be used to store state snapshots
         of the wrapped element. The fields in the tuple should be used as
         follows:
 
-        current_time: The current time of the wrapped element.
+        base_uri: the baseURI attribute of the wrapped element.
+        current_time: the current time of the wrapped element.
         duration: the duration of the wrapped element.
         buffered: the buffered ranges of the wrapped element. In its raw form
         this is as a list where the first element is the length and the second
         element is a list of 2 item lists, where each two items are a buffered
         range. Once assigned to the tuple this data should be wrapped in the
         TimeRanges class.
         played: the played ranges of the wrapped element. In its raw form this
         is as a list where the first element is the length and the second
@@ -264,32 +266,31 @@ class VideoPuppeteer(object):
         range. Once assigned to the tuple this data should be wrapped in the
         TimeRanges class.
         lag: the difference in real world time and wrapped element time.
         Calculated as real world time passed - element time passed.
         totalFrames: number of total frames for the wrapped element
         droppedFrames: number of dropped frames for the wrapped element.
         corruptedFrames: number of corrupted frames for the wrapped.
         video_src: the src attribute of the wrapped element.
-        video_url: the url attribute of the wrapped element.
 
         :return: A 'video_state_info' named tuple class.
         """
         return namedtuple('video_state_info',
-                          ['current_time',
+                          ['base_uri',
+                           'current_time',
                            'duration',
                            'remaining_time',
                            'buffered',
                            'played',
                            'lag',
                            'total_frames',
                            'dropped_frames',
                            'corrupted_frames',
-                           'video_src',
-                           'video_url'])
+                           'video_src'])
 
     def _create_video_state_info(self, **video_state_info_kwargs):
         """
         Create an instance of the video_state_info named tuple. This function
         expects a dictionary populated with the following keys: current_time,
         duration, raw_played_ranges, total_frames, dropped_frames, and
         corrupted_frames.
 
@@ -325,27 +326,27 @@ class VideoPuppeteer(object):
                                video_state_info_kwargs['played'].start(0))
             video_state_info_kwargs['remaining_time'] = (
                 self.expected_duration - played_duration)
         else:
             # No playback has happened yet, remaining time is duration
             video_state_info_kwargs['remaining_time'] = self.expected_duration
         # Fetch non time critical source information
         video_state_info_kwargs['video_src'] = self.video.get_attribute('src')
-        video_state_info_kwargs['video_url'] = self.video.get_attribute('url')
         # Create video state snapshot
         state_info = self._video_state_named_tuple()
         return state_info(**video_state_info_kwargs)
 
     @property
     def _fetch_state_script(self):
         if not self._fetch_state_script_string:
             self._fetch_state_script_string = (
                 self._video_var_script +
                 'return ['
+                'baseURI,'
                 'currentTime,'
                 'duration,'
                 '[buffered.length, bufferedRanges],'
                 '[played.length, playedRanges],'
                 'totalFrames,'
                 'droppedFrames,'
                 'corruptedFrames];')
         return self._fetch_state_script_string
@@ -354,17 +355,17 @@ class VideoPuppeteer(object):
         """
         Refresh the snapshot of the underlying video state. We do this all
         in one so that the state doesn't change in between queries.
 
         We also store information that can be derived from the snapshotted
         information, such as lag. This is stored in the last seen state to
         stress that it's based on the snapshot.
         """
-        keys = ['current_time', 'duration', 'raw_buffered_ranges',
+        keys = ['base_uri', 'current_time', 'duration', 'raw_buffered_ranges',
                 'raw_played_ranges', 'total_frames', 'dropped_frames',
                 'corrupted_frames']
         values = self._execute_video_script(self._fetch_state_script)
         self._last_seen_video_state = (
             self._create_video_state_info(**dict(zip(keys, values))))
 
     def _measure_progress(self):
         self._refresh_state()
--- a/dom/media/test/external/external_media_tests/media_utils/youtube_puppeteer.py
+++ b/dom/media/test/external/external_media_tests/media_utils/youtube_puppeteer.py
@@ -339,16 +339,17 @@ class YouTubePuppeteer(VideoPuppeteer):
 
     @property
     def _fetch_state_script(self):
         if not self._fetch_state_script_string:
             self._fetch_state_script_string = (
                 self._video_var_script +
                 self._player_var_script +
                 'return ['
+                'baseURI,'
                 'currentTime,'
                 'duration,'
                 '[buffered.length, bufferedRanges],'
                 '[played.length, playedRanges],'
                 'totalFrames,'
                 'droppedFrames,'
                 'corruptedFrames,'
                 'player_duration,'
@@ -367,19 +368,19 @@ class YouTubePuppeteer(VideoPuppeteer):
         Refresh the snapshot of the underlying video and player state. We do
         this allin one so that the state doesn't change in between queries.
 
         We also store information that can be derived from the snapshotted
         information, such as lag. This is stored in the last seen state to
         stress that it's based on the snapshot.
         """
         values = self._execute_yt_script(self._fetch_state_script)
-        video_keys = ['current_time', 'duration', 'raw_buffered_ranges',
-                      'raw_played_ranges', 'total_frames', 'dropped_frames',
-                      'corrupted_frames']
+        video_keys = ['base_uri', 'current_time', 'duration',
+                      'raw_buffered_ranges', 'raw_played_ranges',
+                      'total_frames', 'dropped_frames', 'corrupted_frames']
         player_keys = ['player_duration', 'player_current_time',
                        'player_playback_quality', 'player_movie_id',
                        'player_movie_title', 'player_url', 'player_state',
                        'player_ad_state', 'player_breaks_count']
         # Get video state
         self._last_seen_video_state = (
             self._create_video_state_info(**dict(
                 zip(video_keys, values[:len(video_keys)]))))
--- a/dom/webidl/DecoderDoctorNotification.webidl
+++ b/dom/webidl/DecoderDoctorNotification.webidl
@@ -2,17 +2,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/.
  */
 
 enum DecoderDoctorNotificationType {
   "cannot-play",
   "platform-decoder-not-found",
-  "can-play-but-some-missing-decoders"
+  "can-play-but-some-missing-decoders",
+  "cannot-initialize-pulseaudio",
 };
 
 dictionary DecoderDoctorNotification {
   required DecoderDoctorNotificationType type;
   // True when the issue has been solved.
   required boolean isSolved;
   // Key from dom.properties, used for telemetry and prefs.
   required DOMString decoderDoctorReportId;
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/svg-integration/mask-clipPath-opacity-01-ref.xhtml
@@ -0,0 +1,14 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<head>
+<style>
+  div {
+    width: 50px;
+    height: 100px;
+    background-color: rgba(0,0,255,0.5);
+  }
+</style>
+</head>
+<body>
+  <div/>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/svg-integration/mask-clipPath-opacity-01a.xhtml
@@ -0,0 +1,20 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<head>
+<style>
+  div {
+    width: 100px;
+    height: 100px;
+    mask: url(#m1);
+    background-color: blue;
+  }
+</style>
+</head>
+<body>
+  <div/>
+  <svg:svg height="0">
+    <svg:mask id="m1" style="mask-type:alpha">
+      <svg:rect x="0" y="0" width="50" height="100" style="stroke:none; fill: #ffffff" fill-opacity="0.5"/>
+    </svg:mask>
+  </svg:svg>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/svg-integration/mask-clipPath-opacity-01b.xhtml
@@ -0,0 +1,21 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<head>
+<style>
+  div {
+    width: 100px;
+    height: 100px;
+    opacity: 0.5;
+    mask:url(#m1);
+    background-color: blue;
+  }
+</style>
+</head>
+<body>
+  <div/>
+  <svg:svg height="0">
+    <svg:mask id="m1" style="mask-type:alpha">
+      <svg:rect x="0" y="0" width="50" height="100" style="stroke:none; fill: #ffffff"/>
+    </svg:mask>
+  </svg:svg>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/svg-integration/mask-clipPath-opacity-01c.xhtml
@@ -0,0 +1,21 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<head>
+<style>
+  div {
+    width: 100px;
+    height: 100px;
+    clip-path: url(#c1);
+    background-color: blue;
+    opacity: 0.5;
+  }
+</style>
+</head>
+<body>
+  <div/>
+  <svg:svg height="0">
+    <svg:clipPath id="c1">
+      <svg:rect x="0" y="0" width="50" height="100"/>
+    </svg:clipPath>
+  </svg:svg>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/svg-integration/mask-clipPath-opacity-01d.xhtml
@@ -0,0 +1,21 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<head>
+<style>
+  div {
+    width: 100px;
+    height: 100px;
+    clip-path: url(#c1);
+    background-color: blue;
+    opacity: 0.5;
+  }
+</style>
+</head>
+<body>
+  <div/>
+  <svg:svg height="0">
+    <svg:clipPath id="c1">
+      <svg:rect x="0" y="0" width="50" height="100"/>
+    </svg:clipPath>
+  </svg:svg>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/svg-integration/mask-clipPath-opacity-01e.xhtml
@@ -0,0 +1,25 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<head>
+<style>
+  div {
+    width: 100px;
+    height: 100px;
+    clip-path: url(#c1);
+    mask:url(#m1);
+    background-color: blue;
+    opacity: 0.5;
+  }
+</style>
+</head>
+<body>
+  <div/>
+  <svg:svg height="0">
+    <svg:mask id="m1" style="mask-type:alpha">
+      <svg:rect x="0" y="0" width="100" height="100" style="stroke:none; fill: #ffffff"/>
+    </svg:mask>
+    <svg:clipPath id="c1">
+      <svg:rect x="0" y="0" width="50" height="100"/>
+    </svg:clipPath>
+  </svg:svg>
+</body>
+</html>
--- a/layout/reftests/svg/svg-integration/reftest.list
+++ b/layout/reftests/svg/svg-integration/reftest.list
@@ -33,8 +33,13 @@ random == mask-html-01-extref-02.xhtml m
 fuzzy-if(B2G&&browserIsRemote,1,2300) == mask-html-zoomed-01.xhtml mask-html-01-ref.svg
 # Skil XBL test case on B2G
 skip-if(B2G||Mulet) == mask-html-xbl-bound-01.html mask-html-01-ref.svg # Initial mulet triage: parity with B2G/B2G Desktop
 == mask-transformed-html-01.xhtml ../pass.svg
 == mask-transformed-html-02.xhtml ../pass.svg
 fuzzy-if(skiaContent,1,5) == patterned-svg-under-transformed-html-01.xhtml ../pass.svg
 == patterned-svg-under-transformed-html-02.xhtml ../pass.svg
 
+fuzzy(1,5000) == mask-clipPath-opacity-01a.xhtml mask-clipPath-opacity-01-ref.xhtml
+fuzzy(1,5000) == mask-clipPath-opacity-01b.xhtml mask-clipPath-opacity-01-ref.xhtml
+fuzzy(1,5000) == mask-clipPath-opacity-01c.xhtml mask-clipPath-opacity-01-ref.xhtml
+fuzzy(1,5000) == mask-clipPath-opacity-01d.xhtml mask-clipPath-opacity-01-ref.xhtml
+fuzzy(1,5000) == mask-clipPath-opacity-01e.xhtml mask-clipPath-opacity-01-ref.xhtml
--- a/layout/svg/nsSVGIntegrationUtils.cpp
+++ b/layout/svg/nsSVGIntegrationUtils.cpp
@@ -487,17 +487,17 @@ ComputeMaskGeometry(const PaintFramesPar
   IntRect result = ComputeClipExtsInDeviceSpace(ctx);
   ctx.Restore();
 
   return result;
 }
 
 static DrawResult
 GenerateMaskSurface(const PaintFramesParams& aParams,
-                    float aOpacity, nsStyleContext* aSC,
+                    nsStyleContext* aSC,
                     const nsTArray<nsSVGMaskFrame *>& aMaskFrames,
                     const nsPoint& aOffsetToUserSpace,
                     Matrix& aOutMaskTransform,
                     RefPtr<SourceSurface>& aOutMaskSurface)
 {
   const nsStyleSVGReset *svgReset = aSC->StyleSVGReset();
   MOZ_ASSERT(aMaskFrames.Length() > 0);
 
@@ -505,17 +505,17 @@ GenerateMaskSurface(const PaintFramesPar
     nsSVGIntegrationUtils::GetCSSPxToDevPxMatrix(aParams.frame);
 
   gfxContext& ctx = aParams.ctx;
 
   // There is only one SVG mask.
   if (((aMaskFrames.Length() == 1) && aMaskFrames[0])) {
     aOutMaskSurface =
       aMaskFrames[0]->GetMaskForMaskedFrame(&ctx, aParams.frame,
-                                            cssPxToDevPxMatrix, aOpacity,
+                                            cssPxToDevPxMatrix, 1.0,
                                             &aOutMaskTransform,
                                             svgReset->mMask.mLayers[0].mMaskMode);
     return DrawResult::SUCCESS;
   }
 
   IntRect maskSurfaceRect = ComputeMaskGeometry(aParams, svgReset,
                                                 aOffsetToUserSpace,
                                                 aMaskFrames);
@@ -560,17 +560,17 @@ GenerateMaskSurface(const PaintFramesPar
       : nsCSSRendering::GetGFXCompositeMode(svgReset->mMask.mLayers[i].mComposite);
 
     // maskFrame != nullptr means we get a SVG mask.
     // maskFrame == nullptr means we get an image mask.
     if (maskFrame) {
       Matrix svgMaskMatrix;
       RefPtr<SourceSurface> svgMask =
         maskFrame->GetMaskForMaskedFrame(maskContext, aParams.frame,
-                                         cssPxToDevPxMatrix, aOpacity,
+                                         cssPxToDevPxMatrix, 1.0,
                                          &svgMaskMatrix,
                                          svgReset->mMask.mLayers[i].mMaskMode);
       if (svgMask) {
         gfxContextMatrixAutoSaveRestore matRestore(maskContext);
 
         maskContext->Multiply(ThebesMatrix(svgMaskMatrix));
         Rect drawRect = IntRectToRect(IntRect(IntPoint(0, 0), svgMask->GetSize()));
         maskDT->MaskSurface(ColorPattern(Color(0.0, 0.0, 0.0, 1.0)), svgMask, drawRect.TopLeft(),
@@ -874,17 +874,17 @@ nsSVGIntegrationUtils::PaintMaskAndClipP
     if (shouldGenerateMaskLayer) {
       matSR.SetContext(&context);
 
       // For css-mask, we want to generate a mask for each continuation frame,
       // so we setup context matrix by the position of the current frame,
       // instead of the first continuation frame.
       SetupContextMatrix(frame, aParams, offsetToBoundingBox,
                          offsetToUserSpace, true);
-      result = GenerateMaskSurface(aParams, opacity,
+      result = GenerateMaskSurface(aParams,
                                   firstFrame->StyleContext(),
                                   maskFrames, offsetToUserSpace,
                                   maskTransform, maskSurface);
       context.PopClip();
       if (!maskSurface) {
         // Entire surface is clipped out.
         return result;
       }
--- a/layout/tools/reftest/reftest-preferences.js
+++ b/layout/tools/reftest/reftest-preferences.js
@@ -105,16 +105,17 @@ user_pref("browser.tabs.remote.autostart
 user_pref("browser.tabs.remote.autostart.2", false);
 
 user_pref("startup.homepage_welcome_url", "");
 user_pref("startup.homepage_welcome_url.additional", "");
 user_pref("startup.homepage_override_url", "");
 user_pref("browser.usedOnWindows10.introURL", "");
 
 user_pref("media.gmp-manager.url.override", "http://localhost/dummy-gmp-manager.xml");
+user_pref("media.gmp-manager.updateEnabled", false);
 
 // A fake bool pref for "@supports -moz-bool-pref" sanify test.
 user_pref("testing.supports.moz-bool-pref", true);
 
 // Reftests load a lot of URLs very quickly. This puts avoidable and
 // unnecessary I/O pressure on the Places DB (measured to be in the
 // gigabytes).
 user_pref("places.history.enabled", false);
--- a/media/mtransport/nr_socket_prsock.cpp
+++ b/media/mtransport/nr_socket_prsock.cpp
@@ -2244,43 +2244,43 @@ static int nr_socket_local_close(void *o
 
   sock->close();
 
   return 0;
 }
 
 static int nr_socket_local_write(void *obj, const void *msg, size_t len,
                                  size_t *written) {
-  NrSocket *sock = static_cast<NrSocket *>(obj);
+  NrSocketBase *sock = static_cast<NrSocketBase *>(obj);
 
   return sock->write(msg, len, written);
 }
 
 static int nr_socket_local_read(void *obj, void * restrict buf, size_t maxlen,
                                 size_t *len) {
-  NrSocket *sock = static_cast<NrSocket *>(obj);
+  NrSocketBase *sock = static_cast<NrSocketBase *>(obj);
 
   return sock->read(buf, maxlen, len);
 }
 
 static int nr_socket_local_connect(void *obj, nr_transport_addr *addr) {
-  NrSocket *sock = static_cast<NrSocket *>(obj);
+  NrSocketBase *sock = static_cast<NrSocketBase *>(obj);
 
   return sock->connect(addr);
 }
 
 static int nr_socket_local_listen(void *obj, int backlog) {
-  NrSocket *sock = static_cast<NrSocket *>(obj);
+  NrSocketBase *sock = static_cast<NrSocketBase *>(obj);
 
   return sock->listen(backlog);
 }
 
 static int nr_socket_local_accept(void *obj, nr_transport_addr *addrp,
                                   nr_socket **sockp) {
-  NrSocket *sock = static_cast<NrSocket *>(obj);
+  NrSocketBase *sock = static_cast<NrSocketBase *>(obj);
 
   return sock->accept(addrp, sockp);
 }
 
 // Implement async api
 int NR_async_wait(NR_SOCKET sock, int how, NR_async_cb cb,void *cb_arg,
                   char *function,int line) {
   NrSocketBase *s = static_cast<NrSocketBase *>(sock);
--- a/media/mtransport/test_nr_socket.cpp
+++ b/media/mtransport/test_nr_socket.cpp
@@ -686,17 +686,17 @@ void TestNrSocket::destroy_stale_port_ma
     }
   }
 }
 
 void TestNrSocket::socket_readable_callback(void *real_sock_v,
                                              int how,
                                              void *test_sock_v) {
   TestNrSocket *test_socket = static_cast<TestNrSocket*>(test_sock_v);
-  NrSocket *real_socket = static_cast<NrSocket*>(real_sock_v);
+  NrSocketBase *real_socket = static_cast<NrSocketBase*>(real_sock_v);
 
   test_socket->on_socket_readable(real_socket);
 }
 
 void TestNrSocket::on_socket_readable(NrSocketBase *real_socket) {
   if (!readable_socket_ && (real_socket != internal_socket_)) {
     readable_socket_ = real_socket;
   }
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -41,17 +41,20 @@
 
 #ifdef MOZ_NATIVE_DEVICES
         <!-- This resources comes from Google Play Services. Required for casting support. -->
         <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
 #endif
 
         <!-- This activity handles all incoming Intents and dispatches them to other activities. -->
         <activity android:name="org.mozilla.gecko.LauncherActivity"
-            android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+            android:theme="@android:style/Theme.Translucent.NoTitleBar"
+            android:relinquishTaskIdentity="true"
+            android:taskAffinity=""
+            android:excludeFromRecents="true" />
 
         <!-- Fennec is shipped as the Android package named
              org.mozilla.{fennec,firefox,firefox_beta}.  The internal Java
              package hierarchy inside the Android package used to have an
              org.mozilla.{fennec,firefox,firefox_beta} subtree *and* an
              org.mozilla.gecko subtree; it now only has org.mozilla.gecko. -->
         <activity android:name="@MOZ_ANDROID_BROWSER_INTENT_CLASS@"
                   android:label="@string/moz_app_displayname"
--- a/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java
@@ -64,30 +64,41 @@ public class LauncherActivity extends Ac
 
     /**
      * Launch the browser activity.
      */
     private void dispatchNormalIntent() {
         Intent intent = new Intent(getIntent());
         intent.setClassName(getApplicationContext(), AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
 
-        // Explicitly remove the new task and clear task flags (Our browser activity is a single
-        // task activity and we never want to start a second task here). See bug 1280112.
-        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        filterFlags(intent);
 
         startActivity(intent);
     }
 
     private void dispatchCustomTabsIntent() {
         Intent intent = new Intent(getIntent());
         intent.setClassName(getApplicationContext(), CustomTabsActivity.class.getName());
+
+        filterFlags(intent);
+
         startActivity(intent);
     }
 
+    private static void filterFlags(Intent intent) {
+        // Explicitly remove the new task and clear task flags (Our browser activity is a single
+        // task activity and we never want to start a second task here). See bug 1280112.
+        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+        // LauncherActivity is started with the "exclude from recents" flag (set in manifest). We do
+        // not want to propagate this flag from the launcher activity to the browser.
+        intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+    }
+
     private static boolean isViewIntentWithURL(@NonNull final SafeIntent safeIntent) {
         return Intent.ACTION_VIEW.equals(safeIntent.getAction())
                 && safeIntent.getDataString() != null;
     }
 
     private static boolean isCustomTabsIntent(@NonNull final SafeIntent safeIntent) {
         return isViewIntentWithURL(safeIntent)
                 && safeIntent.hasExtra(CustomTabsIntent.EXTRA_SESSION);
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
@@ -30,17 +30,17 @@ import android.content.SharedPreferences
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
 
 public class HomeConfigPrefsBackend implements HomeConfigBackend {
     private static final String LOGTAG = "GeckoHomeConfigBackend";
 
     // Increment this to trigger a migration.
-    private static final int VERSION = 7;
+    private static final int VERSION = 8;
 
     // This key was originally used to store only an array of panel configs.
     public static final String PREFS_CONFIG_KEY_OLD = "home_panels";
 
     // This key is now used to store a version number with the array of panel configs.
     public static final String PREFS_CONFIG_KEY = "home_panels_with_version";
 
     // Keys used with JSON object stored in prefs.
@@ -206,17 +206,22 @@ public class HomeConfigPrefsBackend impl
             } else {
                 newArray.put(jsonPanels.get(i));
             }
         }
 
         return newArray;
     }
 
-    private static void ensureDefaultPanelForV5(Context context, JSONArray jsonPanels) throws JSONException {
+    /**
+     * Iterate over all homepanels to verify that there is at least one default panel. If there is
+     * no default panel, set History as the default panel. (This is only relevant for two botched
+     * migrations where the history panel should have been made the default panel, but wasn't.)
+     */
+    private static void ensureDefaultPanelForV5orV8(Context context, JSONArray jsonPanels) throws JSONException {
         int historyIndex = -1;
 
         for (int i = 0; i < jsonPanels.length(); i++) {
             final PanelConfig panelConfig = new PanelConfig(jsonPanels.getJSONObject(i));
             if (panelConfig.isDefault()) {
                 return;
             }
 
@@ -243,46 +248,63 @@ public class HomeConfigPrefsBackend impl
      *                     otherwise only if we turn it into the new default panel.
      * @return new array of updated JSON panels
      * @throws JSONException
      */
     private static JSONArray removePanel(Context context, JSONArray jsonPanels,
                                          PanelType panelToRemove, PanelType replacementPanel, boolean alwaysUnhide) throws JSONException {
         boolean wasDefault = false;
         int replacementPanelIndex = -1;
+        boolean replacementWasDefault = false;
 
         // JSONArrary doesn't provide remove() for API < 19, therefore we need to manually copy all
         // the items we don't want deleted into a new array.
         final JSONArray newJSONPanels = new JSONArray();
 
         for (int i = 0; i < jsonPanels.length(); i++) {
             final JSONObject panelJSON = jsonPanels.getJSONObject(i);
             final PanelConfig panelConfig = new PanelConfig(panelJSON);
 
             if (panelConfig.getType() == panelToRemove) {
                 // If this panel was the default we'll need to assign a new default:
                 wasDefault = panelConfig.isDefault();
             } else {
                 if (panelConfig.getType() == replacementPanel) {
                     replacementPanelIndex = newJSONPanels.length();
+                    if (panelConfig.isDefault()) {
+                        replacementWasDefault = true;
+                    }
                 }
 
                 newJSONPanels.put(panelJSON);
             }
         }
 
         // Unless alwaysUnhide is true, we make the replacement panel visible only if it is going
         // to be the new default panel, since a hidden default panel doesn't make sense.
         // This is to allow preserving the behaviour of the original reading list migration function.
         if (wasDefault || alwaysUnhide) {
             final JSONObject replacementPanelConfig;
             if (wasDefault) {
+                // If the removed panel was the default, the replacement has to be made the new default
                 replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
             } else {
-                replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel).toJSON();
+                final EnumSet<HomeConfig.PanelConfig.Flags> flags;
+                if (replacementWasDefault) {
+                    // However if the replacement panel was already default, we need to preserve it's default status
+                    // (By rewriting the PanelConfig, we lose all existing flags, so we need to make sure desired
+                    // flags are retained - in this case there's only DEFAULT_PANEL, which is mutually
+                    // exclusive with the DISABLE_PANEL case).
+                    flags = EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL);
+                } else {
+                    flags = EnumSet.noneOf(PanelConfig.Flags.class);
+                }
+
+                // The panel is visible since we don't set Flags.DISABLED_PANEL.
+                replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, flags).toJSON();
             }
 
             if (replacementPanelIndex != -1) {
                 newJSONPanels.put(replacementPanelIndex, replacementPanelConfig);
             } else {
                 newJSONPanels.put(replacementPanelConfig);
             }
         }
@@ -391,28 +413,34 @@ public class HomeConfigPrefsBackend impl
                     // Combine the History and Sync panels. In order to minimize an unexpected reordering
                     // of panels, we try to replace the History panel if it's visible, and fall back to
                     // the Sync panel if that's visible.
                     jsonPanels = combineHistoryAndSyncPanels(context, jsonPanels);
                     break;
 
                 case 5:
                     // This is the fix for bug 1264136 where we lost track of the default panel during some migrations.
-                    ensureDefaultPanelForV5(context, jsonPanels);
+                    ensureDefaultPanelForV5orV8(context, jsonPanels);
                     break;
 
                 case 6:
                     jsonPanels = removePanel(context, jsonPanels,
                             PanelType.DEPRECATED_READING_LIST, PanelType.BOOKMARKS, false);
                     break;
 
                 case 7:
                     jsonPanels = removePanel(context, jsonPanels,
                             PanelType.DEPRECATED_RECENT_TABS, PanelType.COMBINED_HISTORY, true);
                     break;
+
+                case 8:
+                    // Similar to "case 5" above, this time 1304777 - once again we lost track
+                    // of the history panel
+                    ensureDefaultPanelForV5orV8(context, jsonPanels);
+                    break;
             }
         }
 
         // Save the new panel config and the new version number.
         final JSONObject newJson = new JSONObject();
         newJson.put(JSON_KEY_PANELS, jsonPanels);
         newJson.put(JSON_KEY_VERSION, VERSION);
 
--- a/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaControlService.java
@@ -51,16 +51,17 @@ public class MediaControlService extends
     private PrefsHelper.PrefHandler mPrefsObserver;
     private final String[] mPrefs = { MEDIA_CONTROL_PREF };
 
     private boolean mInitialize = false;
     private boolean mIsMediaControlPrefOn = true;
 
     private static WeakReference<Tab> mTabReference = new WeakReference<>(null);
 
+    private int minCoverSize;
     private int coverSize;
 
     @Override
     public void onCreate() {
         initialize();
     }
 
     @Override
@@ -122,16 +123,17 @@ public class MediaControlService extends
             return;
         }
 
         Log.d(LOGTAG, "initialize");
         getGeckoPreference();
         initMediaSession();
 
         coverSize = (int) getResources().getDimension(R.dimen.notification_media_cover);
+        minCoverSize = getResources().getDimensionPixelSize(R.dimen.favicon_bg);
 
         Tabs.registerOnTabsChangedListener(this);
         mInitialize = true;
     }
 
     private void shutdown() {
         if (!mInitialize) {
             return;
@@ -374,17 +376,17 @@ public class MediaControlService extends
         intent.setAction(ACTION_STOP);
         return  PendingIntent.getService(getApplicationContext(), 1, intent, 0);
     }
 
     private Bitmap generateCoverArt(Tab tab) {
         final Bitmap favicon = tab.getFavicon();
 
         // If we do not have a favicon or if it's smaller than 72 pixels then just use the default icon.
-        if (favicon == null || favicon.getWidth() < 72 || favicon.getHeight() < 72) {
+        if (favicon == null || favicon.getWidth() < minCoverSize || favicon.getHeight() < minCoverSize) {
             // Use the launcher icon as fallback
             return BitmapFactory.decodeResource(getResources(), R.drawable.notification_media);
         }
 
         // Favicon should at least have half of the size of the cover
         int width = Math.max(favicon.getWidth(), coverSize / 2);
         int height = Math.max(favicon.getHeight(), coverSize / 2);
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/db/BrowserProviderHistoryVisitsTestBase.java
@@ -5,16 +5,17 @@ package org.mozilla.gecko.db;
 
 import android.content.ContentProviderClient;
 import android.content.ContentValues;
 import android.net.Uri;
 import android.os.RemoteException;
 
 import org.junit.After;
 import org.junit.Before;
+import org.mozilla.gecko.background.db.DelegatingTestContentProvider;
 import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
 import org.robolectric.shadows.ShadowContentResolver;
 
 import java.util.UUID;
 
 public class BrowserProviderHistoryVisitsTestBase {
     /* package-private */ ShadowContentResolver contentResolver;
     /* package-private */ ContentProviderClient historyClient;
@@ -23,17 +24,17 @@ public class BrowserProviderHistoryVisit
     /* package-private */ Uri visitsTestUri;
 
     private BrowserProvider provider;
 
     @Before
     public void setUp() throws Exception {
         provider = new BrowserProvider();
         provider.onCreate();
-        ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY_URI.toString(), provider);
+        ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider));
 
         contentResolver = new ShadowContentResolver();
         historyClient = contentResolver.acquireContentProviderClient(BrowserContractHelpers.HISTORY_CONTENT_URI);
         visitsClient = contentResolver.acquireContentProviderClient(BrowserContractHelpers.VISITS_CONTENT_URI);
 
         historyTestUri = testUri(BrowserContract.History.CONTENT_URI);
         visitsTestUri = testUri(BrowserContract.Visits.CONTENT_URI);
     }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestLegacyLoader.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/icons/loader/TestLegacyLoader.java
@@ -1,54 +1,75 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.icons.loader;
 
 import android.graphics.Bitmap;
 
 import org.junit.Assert;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.background.db.DelegatingTestContentProvider;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.BrowserProvider;
 import org.mozilla.gecko.icons.IconDescriptor;
 import org.mozilla.gecko.icons.IconRequest;
 import org.mozilla.gecko.icons.IconResponse;
 import org.mozilla.gecko.icons.Icons;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowContentResolver;
 
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
 import java.util.Iterator;
+import java.util.concurrent.ConcurrentHashMap;
 
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
 @RunWith(TestRunner.class)
 public class TestLegacyLoader {
     private static final String TEST_PAGE_URL = "http://www.mozilla.org";
     private static final String TEST_ICON_URL = "https://example.org/favicon.ico";
     private static final String TEST_ICON_URL_2 = "https://example.com/page/favicon.ico";
     private static final String TEST_ICON_URL_3 = "https://example.net/icon/favicon.ico";
 
     @Test
     public void testDatabaseIsQueriesForNormalRequestsWithNetworkSkipped() {
-        final IconRequest request = Icons.with(RuntimeEnvironment.application)
-                .pageUrl(TEST_PAGE_URL)
-                .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
-                .skipNetwork()
-                .build();
+        // We're going to query BrowserProvider via LegacyLoader, and will access a database.
+        // We need to ensure we close our db connection properly.
+        // This is the only test in this class that actually accesses a database. If that changes,
+        // move BrowserProvider registration into a @Before method, and provider.shutdown into @After.
+        final BrowserProvider provider = new BrowserProvider();
+        provider.onCreate();
+        ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider));
+        try {
+            final IconRequest request = Icons.with(RuntimeEnvironment.application)
+                    .pageUrl(TEST_PAGE_URL)
+                    .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
+                    .skipNetwork()
+                    .build();
 
-        final LegacyLoader loader = spy(new LegacyLoader());
-        final IconResponse response = loader.load(request);
+            final LegacyLoader loader = spy(new LegacyLoader());
+            final IconResponse response = loader.load(request);
 
-        verify(loader).loadBitmapFromDatabase(request);
-
-        Assert.assertNull(response);
+            verify(loader).loadBitmapFromDatabase(request);
+            Assert.assertNull(response);
+        // Close any open db connections.
+        } finally {
+            provider.shutdown();
+        }
     }
 
     @Test
     public void testNothingIsLoadedIfNetworkIsNotSkipped() {
         final IconRequest request = Icons.with(RuntimeEnvironment.application)
                 .pageUrl(TEST_PAGE_URL)
                 .icon(IconDescriptor.createGenericIcon(TEST_ICON_URL))
                 .build();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/VisitsHelperTest.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/repositories/android/VisitsHelperTest.java
@@ -8,16 +8,17 @@ import android.content.ContentValues;
 import android.net.Uri;
 
 import junit.framework.Assert;
 
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.db.DelegatingTestContentProvider;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserProvider;
 import org.robolectric.shadows.ShadowContentResolver;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -53,17 +54,17 @@ public class VisitsHelperTest {
     @Test
     public void testGetRecentHistoryVisitsForGUID() throws Exception {
         Uri historyTestUri = testUri(BrowserContract.History.CONTENT_URI);
         Uri visitsTestUri = testUri(BrowserContract.Visits.CONTENT_URI);
 
         BrowserProvider provider = new BrowserProvider();
         try {
             provider.onCreate();
-            ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY_URI.toString(), provider);
+            ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider));
 
             final ShadowContentResolver cr = new ShadowContentResolver();
             ContentProviderClient historyClient = cr.acquireContentProviderClient(BrowserContractHelpers.HISTORY_CONTENT_URI);
             ContentProviderClient visitsClient = cr.acquireContentProviderClient(BrowserContractHelpers.VISITS_CONTENT_URI);
 
             ContentValues historyItem = new ContentValues();
             historyItem.put(BrowserContract.History.URL, "https://www.mozilla.org");
             historyItem.put(BrowserContract.History.GUID, "testGUID");
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -386,17 +386,17 @@ pref("media.apple.mp4.enabled", true);
 // media.gmp.storage.version.observed, and if the versions don't match,
 // we clear storage and set media.gmp.storage.version.observed=expected.
 // This provides a mechanism to clear GMP storage when non-compatible
 // changes are made.
 pref("media.gmp.storage.version.expected", 1);
 
 // Filter what triggers user notifications.
 // See DecoderDoctorDocumentWatcher::ReportAnalysis for details.
-pref("media.decoder-doctor.notifications-allowed", "MediaWMFNeeded,MediaWidevineNoWMFNoSilverlight");
+pref("media.decoder-doctor.notifications-allowed", "MediaWMFNeeded,MediaWidevineNoWMFNoSilverlight,MediaCannotInitializePulseAudio");
 // Whether we report partial failures.
 pref("media.decoder-doctor.verbose", false);
 // Whether DD should consider WMF-disabled a WMF failure, useful for testing.
 pref("media.decoder-doctor.wmf-disabled-is-failure", false);
 
 // Whether to suspend decoding of videos in background tabs.
 #ifdef NIGHTLY_BUILD
 pref("media.suspend-bkgnd-video.enabled", true);
--- a/netwerk/base/security-prefs.js
+++ b/netwerk/base/security-prefs.js
@@ -81,16 +81,21 @@ pref("security.pki.name_matching_mode", 
 // 2: similarly, but for 23 August 2015
 // 3: it is never considered equivalent
 #ifdef RELEASE_BUILD
 pref("security.pki.netscape_step_up_policy", 1);
 #else
 pref("security.pki.netscape_step_up_policy", 2);
 #endif
 
+// Configures Certificate Transparency support mode:
+// 0: Fully disabled.
+// 1: Only collect telemetry. CT qualification checks are not performed.
+pref("security.pki.certificate_transparency.mode", 1);
+
 pref("security.webauth.u2f", false);
 pref("security.webauth.u2f_enable_softtoken", false);
 pref("security.webauth.u2f_enable_usbtoken", false);
 
 pref("security.ssl.errorReporting.enabled", true);
 pref("security.ssl.errorReporting.url", "https://incoming.telemetry.mozilla.org/submit/sslreports/");
 pref("security.ssl.errorReporting.automatic", false);
 
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -1524,16 +1524,19 @@ HttpBaseChannel::SetReferrerWithPolicy(n
   // cross-origin.
   // "Strict" request from https->http case was bailed out, so here:
   // "strict-origin" behaves the same as "origin".
   // "strict-origin-when-cross-origin" behaves the same as "origin-when-cross-origin"
   if (referrerPolicy == REFERRER_POLICY_ORIGIN ||
       referrerPolicy == REFERRER_POLICY_STRICT_ORIGIN ||
       (isCrossOrigin && (referrerPolicy == REFERRER_POLICY_ORIGIN_WHEN_XORIGIN ||
                          referrerPolicy == REFERRER_POLICY_STRICT_ORIGIN_WHEN_XORIGIN))) {
+    // We can override the user trimming preference because "origin"
+    // (network.http.referer.trimmingPolicy = 2) is the strictest
+    // trimming policy that users can specify.
     userReferrerTrimmingPolicy = 2;
   }
 
   // check how much referer to send
   if (userReferrerTrimmingPolicy) {
     // All output strings start with: scheme+host+port
     // We want the IDN-normalized PrePath.  That's not something currently
     // available and there doesn't yet seem to be justification for adding it to
--- a/netwerk/srtp/src/crypto/cipher/aes.c
+++ b/netwerk/srtp/src/crypto/cipher/aes.c
@@ -41,16 +41,30 @@
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
  * OF THE POSSIBILITY OF SUCH DAMAGE.
  *
  */
 
 
 #include "aes.h"
 #include "err.h"
+#include "datatypes.h"
+
+typedef uint8_t gf2_8;
+
+#define gf2_8_field_polynomial 0x1B
+
+/*
+ * gf2_8_shift(z) returns the result of the GF(2^8) 'multiply by x'
+ * operation, using the field representation from AES; that is, the
+ * next gf2_8 value in the cyclic representation of that field.  The
+ * value z should be an uint8_t.
+ */
+#define gf2_8_shift(z) (((z) & 128) ?                       \
+       (((z) << 1) ^ gf2_8_field_polynomial) : ((z) << 1))
 
 /* 
  * we use the tables T0, T1, T2, T3, and T4 to compute AES, and 
  * the tables U0, U1, U2, and U4 to compute its inverse
  *
  * different tables are used on little-endian (Intel, VMS) and
  * big-endian processors (everything else)
  *
--- a/netwerk/srtp/src/crypto/include/aes.h
+++ b/netwerk/srtp/src/crypto/include/aes.h
@@ -44,17 +44,16 @@
  */
 
 #ifndef _AES_H
 #define _AES_H
 
 #include "config.h"
 
 #include "datatypes.h"
-#include "gf2_8.h"
 #include "err.h"
 
 /* aes internals */
 
 typedef struct {
   v128_t round[15];
   int num_rounds;
 } aes_expanded_key_t;
deleted file mode 100644
--- a/netwerk/srtp/src/crypto/include/gf2_8.h
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * gf2_8.h
- *
- * GF(256) implementation
- *
- * David A. McGrew
- * Cisco Systems, Inc.
- */
-
-/*
- *	
- * Copyright (c) 2001-2006, Cisco Systems, Inc.
- * All rights reserved.
- * 
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 
- *   Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- * 
- *   Redistributions in binary form must reproduce the above
- *   copyright notice, this list of conditions and the following
- *   disclaimer in the documentation and/or other materials provided
- *   with the distribution.
- * 
- *   Neither the name of the Cisco Systems, Inc. nor the names of its
- *   contributors may be used to endorse or promote products derived
- *   from this software without specific prior written permission.
- * 
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
- * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
- * OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- */
-
-
-#ifndef GF2_8_H
-#define GF2_8_H
-
-#include "datatypes.h"  /* for uint8_t definition */
-
-typedef uint8_t gf2_8;
-
-#define gf2_8_field_polynomial 0x1B
-
-/*
- * gf2_8_shift(x) returns 
- */
-
-/*
- * gf2_8_shift(z) returns the result of the GF(2^8) 'multiply by x' 
- * operation, using the field representation from AES; that is, the 
- * next gf2_8 value in the cyclic representation of that field.  The 
- * value z should be an uint8_t.
- */
-
-#define gf2_8_shift(z) (((z) & 128) ? \
-       (((z) << 1) ^ gf2_8_field_polynomial) : ((z) << 1))
-
-gf2_8
-gf2_8_compute_inverse(gf2_8 x);
-
-void
-test_gf2_8(void);
-
-gf2_8
-gf2_8_multiply(gf2_8 x, gf2_8 y);
-
-#endif /* GF2_8_H */
deleted file mode 100644
--- a/netwerk/srtp/src/crypto/math/gf2_8.c
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * gf2_8.c
- *
- * GF(256) finite field implementation, with the representation used
- * in the AES cipher.
- * 
- * David A. McGrew
- * Cisco Systems, Inc.
- */
-
-/*
- *	
- * Copyright (c) 2001-2006, Cisco Systems, Inc.
- * All rights reserved.
- * 
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 
- *   Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- * 
- *   Redistributions in binary form must reproduce the above
- *   copyright notice, this list of conditions and the following
- *   disclaimer in the documentation and/or other materials provided
- *   with the distribution.
- * 
- *   Neither the name of the Cisco Systems, Inc. nor the names of its
- *   contributors may be used to endorse or promote products derived
- *   from this software without specific prior written permission.
- * 
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
- * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
- * OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- */
-
-
-#include "datatypes.h"
-#include "gf2_8.h"
-
-/* gf2_8_shift() moved to gf2_8.h as an inline function */
-
-gf2_8
-gf2_8_multiply(gf2_8 x, gf2_8 y) {
-  gf2_8 z = 0;
-
-  if (y &   1) z ^= x; x = gf2_8_shift(x);
-  if (y &   2) z ^= x; x = gf2_8_shift(x);
-  if (y &   4) z ^= x; x = gf2_8_shift(x);
-  if (y &   8) z ^= x; x = gf2_8_shift(x);
-  if (y &  16) z ^= x; x = gf2_8_shift(x);
-  if (y &  32) z ^= x; x = gf2_8_shift(x);
-  if (y &  64) z ^= x; x = gf2_8_shift(x);
-  if (y & 128) z ^= x; 
-  
-  return z;
-}
-
-
-/* this should use the euclidean algorithm */
-
-gf2_8
-gf2_8_compute_inverse(gf2_8 x) {
-  unsigned int i;
-
-  if (x == 0) return 0;    /* zero is a special case */
-  for (i=0; i < 256; i++)
-    if (gf2_8_multiply((gf2_8) i, x) == 1)
-      return (gf2_8) i;
-
-    return 0;
-}
-
--- a/netwerk/srtp/src/moz.build
+++ b/netwerk/srtp/src/moz.build
@@ -14,17 +14,16 @@ UNIFIED_SOURCES += [
     'crypto/hash/hmac.c',
     'crypto/hash/null_auth.c',
     'crypto/hash/sha1.c',
     'crypto/kernel/alloc.c',
     'crypto/kernel/crypto_kernel.c',
     'crypto/kernel/err.c',
     'crypto/kernel/key.c',
     'crypto/math/datatypes.c',
-    'crypto/math/gf2_8.c',
     'crypto/math/stat.c',
     'crypto/replay/rdb.c',
     'crypto/replay/rdbx.c',
     'crypto/replay/ut_sim.c',
     'crypto/rng/ctr_prng.c',
     'crypto/rng/prng.c',
     'crypto/rng/rand_source.c',
     'srtp/ekt.c',
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -331,17 +331,19 @@ skip-if = os == "android"
 skip-if = os != "win"
 [test_synthesized_response.js]
 [test_udp_multicast.js]
 [test_redirect_history.js]
 [test_reply_without_content_type.js]
 [test_websocket_offline.js]
 [test_tls_server.js]
 # The local cert service used by this test is not currently shipped on Android
-skip-if = os == "android"
+# Disabled on XP in bug 1190674 for intermittent failures
+skip-if = os == "android" || (os == "win" && (os_version == "5.1" || os_version == "5.2"))
+reason = bug 1190674
 firefox-appdir = browser
 [test_tls_server_multiple_clients.js]
 # The local cert service used by this test is not currently shipped on Android
 skip-if = os == "android"
 [test_1073747.js]
 [test_multipart_streamconv_application_package.js]
 [test_safeoutputstream_append.js]
 [test_packaged_app_service.js]
--- a/old-configure.in
+++ b/old-configure.in
@@ -3256,23 +3256,18 @@ fi
 MOZ_WEBM_ENCODER=1
 AC_DEFINE(MOZ_WEBM_ENCODER)
 AC_SUBST(MOZ_WEBM_ENCODER)
 
 dnl ==================================
 dnl = Check alsa availability on Linux
 dnl ==================================
 
-dnl If using Linux, ensure that the alsa library is available
-if test "$OS_TARGET" = "Linux"; then
-    MOZ_ALSA=1
-fi
-
 MOZ_ARG_ENABLE_BOOL(alsa,
-[  --enable-alsa          Enable Alsa support (default on Linux)],
+[  --enable-alsa          Enable Alsa support],
    MOZ_ALSA=1,
    MOZ_ALSA=)
 
 if test -n "$MOZ_ALSA"; then
     PKG_CHECK_MODULES(MOZ_ALSA, alsa, ,
          [echo "$MOZ_ALSA_PKG_ERRORS"
           AC_MSG_ERROR([Need alsa for audio output on Linux. (On Ubuntu, you might try installing the package libasound2-dev.)])])
 fi
@@ -3293,16 +3288,17 @@ WINNT|Darwin|Android|OpenBSD)
 esac
 
 MOZ_ARG_DISABLE_BOOL(pulseaudio,
 [  --disable-pulseaudio          Disable PulseAudio support],
    MOZ_PULSEAUDIO=,
    MOZ_PULSEAUDIO=1)
 
 if test -n "$MOZ_PULSEAUDIO"; then
+    AC_DEFINE(MOZ_PULSEAUDIO)
     if test -z "$gonkdir"; then
         PKG_CHECK_MODULES(MOZ_PULSEAUDIO, libpulse, ,
              [echo "$MOZ_PULSEAUDIO_PKG_ERRORS"
               AC_MSG_ERROR([pulseaudio audio backend requires libpulse development package])])
     else
         MOZ_PULSEAUDIO_CFLAGS="-I$gonkdir/external/pulseaudio/pulseaudio/src"
     fi
 fi
--- a/python/mozbuild/mozbuild/test/configure/test_compile_checks.py
+++ b/python/mozbuild/mozbuild/test/configure/test_compile_checks.py
@@ -175,16 +175,26 @@ class TestHeaderChecks(BaseCompileChecks
         config, out, status = self.do_compile_test(cmd,
                                                    expected_test_content=expected_test_content)
         self.assertEqual(status, 0)
         self.assertEqual(config, {'DEFINES': {'HAVE_FOO_H': True}})
         self.assertEqual(out, textwrap.dedent('''\
             checking for foo.h... yes
         '''))
 
+    def test_check_header_conditional(self):
+        cmd = textwrap.dedent('''\
+            check_headers('foo.h', 'bar.h', when=never)
+        ''')
+
+        config, out, status = self.do_compile_test(cmd)
+        self.assertEqual(status, 0)
+        self.assertEqual(out, '')
+        self.assertEqual(config, {'DEFINES':{}})
+
     def test_check_header_include(self):
         expected_test_content = textwrap.dedent('''\
           #include <std.h>
           #include <bar.h>
           #include <foo.h>
           int
           main(void)
           {
new file mode 100644
--- /dev/null
+++ b/security/certverifier/CTKnownLogs.h
@@ -0,0 +1,155 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=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/. */
+
+/* This file is generated by print_log_list.py from
+ * https://github.com/google/certificate-transparency/ */
+
+#ifndef CTKnownLogs_h
+#define CTKnownLogs_h
+
+#include <stddef.h>
+
+struct CTLogInfo {
+  const char* const logName;
+  const char* const logUrl;
+  const char* const logKey;
+  const size_t logKeyLength;
+};
+
+const CTLogInfo kCTLogList[] = {
+  { "Google 'Pilot' log",
+    "https://ct.googleapis.com/pilot/",
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
+    "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x7d\xa8\x4b\x12\x29\x80\xa3\x3d\xad"
+    "\xd3\x5a\x77\xb8\xcc\xe2\x88\xb3\xa5\xfd\xf1\xd3\x0c\xcd\x18\x0c\xe8\x41"
+    "\x46\xe8\x81\x01\x1b\x15\xe1\x4b\xf1\x1b\x62\xdd\x36\x0a\x08\x18\xba\xed"
+    "\x0b\x35\x84\xd0\x9e\x40\x3c\x2d\x9e\x9b\x82\x65\xbd\x1f\x04\x10\x41\x4c"
+    "\xa0",
+    91 },
+  { "Google 'Aviator' log",
+    "https://ct.googleapis.com/aviator/",
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
+    "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\xd7\xf4\xcc\x69\xb2\xe4\x0e\x90\xa3"
+    "\x8a\xea\x5a\x70\x09\x4f\xef\x13\x62\xd0\x8d\x49\x60\xff\x1b\x40\x50\x07"
+    "\x0c\x6d\x71\x86\xda\x25\x49\x8d\x65\xe1\x08\x0d\x47\x34\x6b\xbd\x27\xbc"
+    "\x96\x21\x3e\x34\xf5\x87\x76\x31\xb1\x7f\x1d\xc9\x85\x3b\x0d\xf7\x1f\x3f"
+    "\xe9",
+    91 },
+  { "DigiCert Log Server",
+    "https://ct1.digicert-ct.com/log/",
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
+    "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x02\x46\xc5\xbe\x1b\xbb\x82\x40\x16"
+    "\xe8\xc1\xd2\xac\x19\x69\x13\x59\xf8\xf8\x70\x85\x46\x40\xb9\x38\xb0\x23"
+    "\x82\xa8\x64\x4c\x7f\xbf\xbb\x34\x9f\x4a\x5f\x28\x8a\xcf\x19\xc4\x00\xf6"
+    "\x36\x06\x93\x65\xed\x4c\xf5\xa9\x21\x62\x5a\xd8\x91\xeb\x38\x24\x40\xac"
+    "\xe8",
+    91 },
+  { "Google 'Rocketeer' log",
+    "https://ct.googleapis.com/rocketeer/",
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
+    "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x20\x5b\x18\xc8\x3c\xc1\x8b\xb3\x31"
+    "\x08\x00\xbf\xa0\x90\x57\x2b\xb7\x47\x8c\x6f\xb5\x68\xb0\x8e\x90\x78\xe9"
+    "\xa0\x73\xea\x4f\x28\x21\x2e\x9c\xc0\xf4\x16\x1b\xaa\xf9\xd5\xd7\xa9\x80"
+    "\xc3\x4e\x2f\x52\x3c\x98\x01\x25\x46\x24\x25\x28\x23\x77\x2d\x05\xc2\x40"
+    "\x7a",
+    91 },
+  { "Certly.IO log",
+    "https://log.certly.io/",
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
+    "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x0b\x23\xcb\x85\x62\x98\x61\x48\x04"
+    "\x73\xeb\x54\x5d\xf3\xd0\x07\x8c\x2d\x19\x2d\x8c\x36\xf5\xeb\x8f\x01\x42"
+    "\x0a\x7c\x98\x26\x27\xc1\xb5\xdd\x92\x93\xb0\xae\xf8\x9b\x3d\x0c\xd8\x4c"
+    "\x4e\x1d\xf9\x15\xfb\x47\x68\x7b\xba\x66\xb7\x25\x9c\xd0\x4a\xc2\x66\xdb"
+    "\x48",
+    91 },
+  { "Izenpe log",
+    "https://ct.izenpe.com/",
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
+    "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x27\x64\x39\x0c\x2d\xdc\x50\x18\xf8"
+    "\x21\x00\xa2\x0e\xed\x2c\xea\x3e\x75\xba\x9f\x93\x64\x09\x00\x11\xc4\x11"
+    "\x17\xab\x5c\xcf\x0f\x74\xac\xb5\x97\x90\x93\x00\x5b\xb8\xeb\xf7\x27\x3d"
+    "\xd9\xb2\x0a\x81\x5f\x2f\x0d\x75\x38\x94\x37\x99\x1e\xf6\x07\x76\xe0\xee"
+    "\xbe",
+    91 },
+  { "Symantec log",
+    "https://ct.ws.symantec.com/",
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
+    "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x96\xea\xac\x1c\x46\x0c\x1b\x55\xdc"
+    "\x0d\xfc\xb5\x94\x27\x46\x57\x42\x70\x3a\x69\x18\xe2\xbf\x3b\xc4\xdb\xab"
+    "\xa0\xf4\xb6\x6c\xc0\x53\x3f\x4d\x42\x10\x33\xf0\x58\x97\x8f\x6b\xbe\x72"
+    "\xf4\x2a\xec\x1c\x42\xaa\x03\x2f\x1a\x7e\x28\x35\x76\x99\x08\x3d\x21\x14"
+    "\x86",
+    91 },
+  { "Venafi log",
+    "https://ctlog.api.venafi.com/",
+    "\x30\x82\x01\x22\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05"
+    "\x00\x03\x82\x01\x0f\x00\x30\x82\x01\x0a\x02\x82\x01\x01\x00\xa2\x5a\x48"
+    "\x1f\x17\x52\x95\x35\xcb\xa3\x5b\x3a\x1f\x53\x82\x76\x94\xa3\xff\x80\xf2"
+    "\x1c\x37\x3c\xc0\xb1\xbd\xc1\x59\x8b\xab\x2d\x65\x93\xd7\xf3\xe0\x04\xd5"
+    "\x9a\x6f\xbf\xd6\x23\x76\x36\x4f\x23\x99\xcb\x54\x28\xad\x8c\x15\x4b\x65"
+    "\x59\x76\x41\x4a\x9c\xa6\xf7\xb3\x3b\x7e\xb1\xa5\x49\xa4\x17\x51\x6c\x80"
+    "\xdc\x2a\x90\x50\x4b\x88\x24\xe9\xa5\x12\x32\x93\x04\x48\x90\x02\xfa\x5f"
+    "\x0e\x30\x87\x8e\x55\x76\x05\xee\x2a\x4c\xce\xa3\x6a\x69\x09\x6e\x25\xad"
+    "\x82\x76\x0f\x84\x92\xfa\x38\xd6\x86\x4e\x24\x8f\x9b\xb0\x72\xcb\x9e\xe2"
+    "\x6b\x3f\xe1\x6d\xc9\x25\x75\x23\x88\xa1\x18\x58\x06\x23\x33\x78\xda\x00"
+    "\xd0\x38\x91\x67\xd2\xa6\x7d\x27\x97\x67\x5a\xc1\xf3\x2f\x17\xe6\xea\xd2"
+    "\x5b\xe8\x81\xcd\xfd\x92\x68\xe7\xf3\x06\xf0\xe9\x72\x84\xee\x01\xa5\xb1"
+    "\xd8\x33\xda\xce\x83\xa5\xdb\xc7\xcf\xd6\x16\x7e\x90\x75\x18\xbf\x16\xdc"
+    "\x32\x3b\x6d\x8d\xab\x82\x17\x1f\x89\x20\x8d\x1d\x9a\xe6\x4d\x23\x08\xdf"
+    "\x78\x6f\xc6\x05\xbf\x5f\xae\x94\x97\xdb\x5f\x64\xd4\xee\x16\x8b\xa3\x84"
+    "\x6c\x71\x2b\xf1\xab\x7f\x5d\x0d\x32\xee\x04\xe2\x90\xec\x41\x9f\xfb\x39"
+    "\xc1\x02\x03\x01\x00\x01",
+    294 },
+  { "Symantec 'Vega' log",
+    "https://vega.ws.symantec.com/",
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
+    "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\xea\x95\x9e\x02\xff\xee\xf1\x33\x6d"
+    "\x4b\x87\xbc\xcd\xfd\x19\x17\x62\xff\x94\xd3\xd0\x59\x07\x3f\x02\x2d\x1c"
+    "\x90\xfe\xc8\x47\x30\x3b\xf1\xdd\x0d\xb8\x11\x0c\x5d\x1d\x86\xdd\xab\xd3"
+    "\x2b\x46\x66\xfb\x6e\x65\xb7\x3b\xfd\x59\x68\xac\xdf\xa6\xf8\xce\xd2\x18"
+    "\x4d",
+    91 },
+  { "CNNIC CT log",
+    "https://ctserver.cnnic.cn/",
+    "\x30\x82\x01\x22\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05"
+    "\x00\x03\x82\x01\x0f\x00\x30\x82\x01\x0a\x02\x82\x01\x01\x00\xbf\xb5\x08"
+    "\x61\x9a\x29\x32\x04\xd3\x25\x63\xe9\xd8\x85\xe1\x86\xe0\x1f\xd6\x5e\x9a"
+    "\xf7\x33\x3b\x80\x1b\xe7\xb6\x3e\x5f\x2d\xa1\x66\xf6\x95\x4a\x84\xa6\x21"
+    "\x56\x79\xe8\xf7\x85\xee\x5d\xe3\x7c\x12\xc0\xe0\x89\x22\x09\x22\x3e\xba"
+    "\x16\x95\x06\xbd\xa8\xb9\xb1\xa9\xb2\x7a\xd6\x61\x2e\x87\x11\xb9\x78\x40"
+    "\x89\x75\xdb\x0c\xdc\x90\xe0\xa4\x79\xd6\xd5\x5e\x6e\xd1\x2a\xdb\x34\xf4"
+    "\x99\x3f\x65\x89\x3b\x46\xc2\x29\x2c\x15\x07\x1c\xc9\x4b\x1a\x54\xf8\x6c"
+    "\x1e\xaf\x60\x27\x62\x0a\x65\xd5\x9a\xb9\x50\x36\x16\x6e\x71\xf6\x1f\x01"
+    "\xf7\x12\xa7\xfc\xbf\xf6\x21\xa3\x29\x90\x86\x2d\x77\xde\xbb\x4c\xd4\xcf"
+    "\xfd\xd2\xcf\x82\x2c\x4d\xd4\xf2\xc2\x2d\xac\xa9\xbe\xea\xc3\x19\x25\x43"
+    "\xb2\xe5\x9a\x6c\x0d\xc5\x1c\xa5\x8b\xf7\x3f\x30\xaf\xb9\x01\x91\xb7\x69"
+    "\x12\x12\xe5\x83\x61\xfe\x34\x00\xbe\xf6\x71\x8a\xc7\xeb\x50\x92\xe8\x59"
+    "\xfe\x15\x91\xeb\x96\x97\xf8\x23\x54\x3f\x2d\x8e\x07\xdf\xee\xda\xb3\x4f"
+    "\xc8\x3c\x9d\x6f\xdf\x3c\x2c\x43\x57\xa1\x47\x0c\x91\x04\xf4\x75\x4d\xda"
+    "\x89\x81\xa4\x14\x06\x34\xb9\x98\xc3\xda\xf1\xfd\xed\x33\x36\xd3\x16\x2d"
+    "\x35\x02\x03\x01\x00\x01",
+    294 },
+  { "WoSign log",
+    "https://ctlog.wosign.com/",
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
+    "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\xcc\x11\x88\x7b\x2d\x66\xcb\xae\x8f"
+    "\x4d\x30\x66\x27\x19\x25\x22\x93\x21\x46\xb4\x2f\x01\xd3\xc6\xf9\x2b\xd5"
+    "\xc8\xba\x73\x9b\x06\xa2\xf0\x8a\x02\x9c\xd0\x6b\x46\x18\x30\x85\xba\xe9"
+    "\x24\x8b\x0e\xd1\x5b\x70\x28\x0c\x7e\xf1\x3a\x45\x7f\x5a\xf3\x82\x42\x60"
+    "\x31",
+    91 },
+  { "StartCom log",
+    "https://ct.startssl.com/",
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
+    "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x48\xf3\x59\xf3\xf6\x05\x18\xd3\xdb"
+    "\xb2\xed\x46\x7e\xcf\xc8\x11\xb5\x57\xb1\xa8\xd6\x4c\xe6\x9f\xb7\x4a\x1a"
+    "\x14\x86\x43\xa9\x48\xb0\xcb\x5a\x3f\x3c\x4a\xca\xdf\xc4\x82\x14\x55\x9a"
+    "\xf8\xf7\x8e\x40\x55\xdc\xf4\xd2\xaf\xea\x75\x74\xfb\x4e\x7f\x60\x86\x2e"
+    "\x51",
+    91 }
+};
+
+#endif // CTKnownLogs_h
--- a/security/certverifier/CTSerialization.cpp
+++ b/security/certverifier/CTSerialization.cpp
@@ -504,16 +504,20 @@ DecodeSignedCertificateTimestamp(Reader&
     return rv;
   }
   rv = InputToBuffer(extensions, result.extensions);
   if (rv != Success) {
     return rv;
   }
   result.timestamp = timestamp;
 
+  result.origin = SignedCertificateTimestamp::Origin::Unknown;
+  result.verificationStatus =
+    SignedCertificateTimestamp::VerificationStatus::None;
+
   output = Move(result);
   return Success;
 }
 
 Result
 EncodeSCTList(const Vector<pkix::Input>& scts, Buffer& output)
 {
   // Find out the total size of the SCT list to be written so we can
new file mode 100644
--- /dev/null
+++ b/security/certverifier/CTVerifyResult.cpp
@@ -0,0 +1,18 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=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/. */
+
+#include "CTVerifyResult.h"
+
+namespace mozilla { namespace ct {
+
+void
+CTVerifyResult::Reset()
+{
+  scts.clear();
+  decodingErrors = 0;
+}
+
+} } // namespace mozilla::ct
new file mode 100644
--- /dev/null
+++ b/security/certverifier/CTVerifyResult.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=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/. */
+
+#ifndef CTVerifyResult_h
+#define CTVerifyResult_h
+
+#include "mozilla/Vector.h"
+#include "SignedCertificateTimestamp.h"
+
+namespace mozilla { namespace ct {
+
+typedef Vector<SignedCertificateTimestamp> SCTList;
+
+// Holds Signed Certificate Timestamps verification results.
+class CTVerifyResult
+{
+public:
+  // SCTs that were processed during the verification. For each SCT,
+  // the verification result is stored in its |verificationStatus| field.
+  SCTList scts;
+
+  // The verifier makes the best effort to extract the available SCTs
+  // from the binary sources provided to it.
+  // If some SCT cannot be extracted due to encoding errors, the verifier
+  // proceeds to the next available one. In other words, decoding errors are
+  // effectively ignored.
+  // Note that a serialized SCT may fail to decode for a "legitimate" reason,
+  // e.g. if the SCT is from a future version of the Certificate Transparency
+  // standard.
+  // |decodingErrors| field counts the errors of the above kind.
+  size_t decodingErrors;
+
+  void Reset();
+};
+
+} } // namespace mozilla::ct
+
+#endif  // CTVerifyResult_h
--- a/security/certverifier/CertVerifier.cpp
+++ b/security/certverifier/CertVerifier.cpp
@@ -4,31 +4,35 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "CertVerifier.h"
 
 #include <stdint.h>
 
 #include "BRNameMatchingPolicy.h"
+#include "CTKnownLogs.h"
 #include "ExtendedValidation.h"
+#include "MultiLogCTVerifier.h"
 #include "NSSCertDBTrustDomain.h"
 #include "NSSErrorsService.h"
 #include "cert.h"
+#include "mozilla/Assertions.h"
 #include "mozilla/Casting.h"
 #include "nsNSSComponent.h"
 #include "nsServiceManagerUtils.h"
 #include "pk11pub.h"
 #include "pkix/pkix.h"
 #include "pkix/pkixnss.h"
 #include "prerror.h"
 #include "secerr.h"
 #include "secmod.h"
 #include "sslerr.h"
 
+using namespace mozilla::ct;
 using namespace mozilla::pkix;
 using namespace mozilla::psm;
 
 mozilla::LazyLogModule gCertVerifierLog("certverifier");
 
 namespace mozilla { namespace psm {
 
 const CertVerifier::Flags CertVerifier::FLAG_LOCAL_ONLY = 1;
@@ -37,26 +41,29 @@ const CertVerifier::Flags CertVerifier::
 
 CertVerifier::CertVerifier(OcspDownloadConfig odc,
                            OcspStrictConfig osc,
                            OcspGetConfig ogc,
                            uint32_t certShortLifetimeInDays,
                            PinningMode pinningMode,
                            SHA1Mode sha1Mode,
                            BRNameMatchingPolicy::Mode nameMatchingMode,
-                           NetscapeStepUpPolicy netscapeStepUpPolicy)
+                           NetscapeStepUpPolicy netscapeStepUpPolicy,
+                           CertificateTransparencyMode ctMode)
   : mOCSPDownloadConfig(odc)
   , mOCSPStrict(osc == ocspStrict)
   , mOCSPGETEnabled(ogc == ocspGetEnabled)
   , mCertShortLifetimeInDays(certShortLifetimeInDays)
   , mPinningMode(pinningMode)
   , mSHA1Mode(sha1Mode)
   , mNameMatchingMode(nameMatchingMode)
   , mNetscapeStepUpPolicy(netscapeStepUpPolicy)
+  , mCTMode(ctMode)
 {
+  LoadKnownCTLogs();
 }
 
 CertVerifier::~CertVerifier()
 {
 }
 
 Result
 IsCertChainRootBuiltInRoot(const UniqueCERTCertList& chain, bool& result)
@@ -116,41 +123,187 @@ static Result
 BuildCertChainForOneKeyUsage(NSSCertDBTrustDomain& trustDomain, Input certDER,
                              Time time, KeyUsage ku1, KeyUsage ku2,
                              KeyUsage ku3, KeyPurposeId eku,
                              const CertPolicyId& requiredPolicy,
                              const Input* stapledOCSPResponse,
                              /*optional out*/ CertVerifier::OCSPStaplingStatus*
                                                 ocspStaplingStatus)
 {
-  trustDomain.ResetOCSPStaplingStatus();
+  trustDomain.ResetAccumulatedState();
   Result rv = BuildCertChain(trustDomain, certDER, time,
                              EndEntityOrCA::MustBeEndEntity, ku1,
                              eku, requiredPolicy, stapledOCSPResponse);
   if (rv == Result::ERROR_INADEQUATE_KEY_USAGE) {
-    trustDomain.ResetOCSPStaplingStatus();
+    trustDomain.ResetAccumulatedState();
     rv = BuildCertChain(trustDomain, certDER, time,
                         EndEntityOrCA::MustBeEndEntity, ku2,
                         eku, requiredPolicy, stapledOCSPResponse);
     if (rv == Result::ERROR_INADEQUATE_KEY_USAGE) {
-      trustDomain.ResetOCSPStaplingStatus();
+      trustDomain.ResetAccumulatedState();
       rv = BuildCertChain(trustDomain, certDER, time,
                           EndEntityOrCA::MustBeEndEntity, ku3,
                           eku, requiredPolicy, stapledOCSPResponse);
       if (rv != Success) {
         rv = Result::ERROR_INADEQUATE_KEY_USAGE;
       }
     }
   }
   if (ocspStaplingStatus) {
     *ocspStaplingStatus = trustDomain.GetOCSPStaplingStatus();
   }
   return rv;
 }
 
+void
+CertVerifier::LoadKnownCTLogs()
+{
+  mCTVerifier = MakeUnique<MultiLogCTVerifier>();
+  for (const CTLogInfo& log : kCTLogList) {
+    Input publicKey;
+    Result rv = publicKey.Init(
+      BitwiseCast<const uint8_t*, const char*>(log.logKey), log.logKeyLength);
+    if (rv != Success) {
+      MOZ_ASSERT_UNREACHABLE("Failed reading a log key for a known CT Log");
+      continue;
+    }
+    rv = mCTVerifier->AddLog(publicKey);
+    if (rv != Success) {
+      MOZ_ASSERT_UNREACHABLE("Failed initializing a known CT Log");
+      continue;
+    }
+  }
+}
+
+Result
+CertVerifier::VerifySignedCertificateTimestamps(
+  NSSCertDBTrustDomain& trustDomain, const UniqueCERTCertList& builtChain,
+  Input sctsFromTLS, Time time,
+  /*optional out*/ CertificateTransparencyInfo* ctInfo)
+{
+  if (ctInfo) {
+    ctInfo->Reset();
+  }
+  if (mCTMode == CertificateTransparencyMode::Disabled) {
+    return Success;
+  }
+  if (ctInfo) {
+    ctInfo->enabled = true;
+  }
+
+  if (!builtChain || CERT_LIST_EMPTY(builtChain)) {
+    return Result::FATAL_ERROR_INVALID_ARGS;
+  }
+
+  bool gotScts = false;
+  Input embeddedSCTs = trustDomain.GetSCTListFromCertificate();
+  if (embeddedSCTs.GetLength() > 0) {
+    gotScts = true;
+    MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+            ("Got embedded SCT data of length %zu\n",
+              static_cast<size_t>(embeddedSCTs.GetLength())));
+  }
+  Input sctsFromOCSP = trustDomain.GetSCTListFromOCSPStapling();
+  if (sctsFromOCSP.GetLength() > 0) {
+    gotScts = true;
+    MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+            ("Got OCSP SCT data of length %zu\n",
+              static_cast<size_t>(sctsFromOCSP.GetLength())));
+  }
+  if (sctsFromTLS.GetLength() > 0) {
+    gotScts = true;
+    MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+            ("Got TLS SCT data of length %zu\n",
+              static_cast<size_t>(sctsFromTLS.GetLength())));
+  }
+  if (!gotScts) {
+    return Success;
+  }
+
+  CERTCertListNode* endEntityNode = CERT_LIST_HEAD(builtChain);
+  if (!endEntityNode) {
+    return Result::FATAL_ERROR_INVALID_ARGS;
+  }
+  CERTCertListNode* issuerNode = CERT_LIST_NEXT(endEntityNode);
+  if (!issuerNode) {
+    // Issuer certificate is required for SCT verification.
+    return Success;
+  }
+
+  CERTCertificate* endEntity = endEntityNode->cert;
+  CERTCertificate* issuer = issuerNode->cert;
+  if (!endEntity || !issuer) {
+    return Result::FATAL_ERROR_INVALID_ARGS;
+  }
+
+  Input endEntityDER;
+  Result rv = endEntityDER.Init(endEntity->derCert.data,
+                                endEntity->derCert.len);
+  if (rv != Success) {
+    return rv;
+  }
+
+  Input issuerPublicKeyDER;
+  rv = issuerPublicKeyDER.Init(issuer->derPublicKey.data,
+                               issuer->derPublicKey.len);
+  if (rv != Success) {
+    return rv;
+  }
+
+  CTVerifyResult result;
+  rv = mCTVerifier->Verify(endEntityDER, issuerPublicKeyDER,
+                           embeddedSCTs, sctsFromOCSP, sctsFromTLS, time,
+                           result);
+  if (rv != Success) {
+    MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+            ("SCT verification failed with fatal error %i\n", rv));
+    return rv;
+  }
+
+  if (MOZ_LOG_TEST(gCertVerifierLog, LogLevel::Debug)) {
+    size_t verifiedCount = 0;
+    size_t unknownLogCount = 0;
+    size_t invalidSignatureCount = 0;
+    size_t invalidTimestampCount = 0;
+    for (const SignedCertificateTimestamp& sct : result.scts) {
+      switch (sct.verificationStatus) {
+        case SignedCertificateTimestamp::VerificationStatus::OK:
+          verifiedCount++;
+          break;
+        case SignedCertificateTimestamp::VerificationStatus::UnknownLog:
+          unknownLogCount++;
+          break;
+        case SignedCertificateTimestamp::VerificationStatus::InvalidSignature:
+          invalidSignatureCount++;
+          break;
+        case SignedCertificateTimestamp::VerificationStatus::InvalidTimestamp:
+          invalidTimestampCount++;
+          break;
+        case SignedCertificateTimestamp::VerificationStatus::None:
+        default:
+          MOZ_ASSERT_UNREACHABLE("Unexpected SCT verificationStatus");
+      }
+    }
+    MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
+            ("SCT verification result: "
+             "verified=%zu unknownLog=%zu "
+             "invalidSignature=%zu invalidTimestamp=%zu "
+             "decodingErrors=%zu\n",
+             verifiedCount, unknownLogCount,
+             invalidSignatureCount, invalidTimestampCount,
+             result.decodingErrors));
+  }
+
+  if (ctInfo) {
+    ctInfo->processedSCTs = true;
+    ctInfo->verifyResult = Move(result);
+  }
+  return Success;
+}
+
 bool
 CertVerifier::SHA1ModeMoreRestrictiveThanGivenMode(SHA1Mode mode)
 {
   switch (mSHA1Mode) {
     case SHA1Mode::Forbidden:
       return mode != SHA1Mode::Forbidden;
     case SHA1Mode::ImportedRoot:
       return mode != SHA1Mode::Forbidden && mode != SHA1Mode::ImportedRoot;
@@ -170,21 +323,23 @@ static const unsigned int MIN_RSA_BITS =
 static const unsigned int MIN_RSA_BITS_WEAK = 1024;
 
 SECStatus
 CertVerifier::VerifyCert(CERTCertificate* cert, SECCertificateUsage usage,
                          Time time, void* pinArg, const char* hostname,
                  /*out*/ UniqueCERTCertList& builtChain,
             /*optional*/ const Flags flags,
             /*optional*/ const SECItem* stapledOCSPResponseSECItem,
+            /*optional*/ const SECItem* sctsFromTLSSECItem,
         /*optional out*/ SECOidTag* evOidPolicy,
         /*optional out*/ OCSPStaplingStatus* ocspStaplingStatus,
         /*optional out*/ KeySizeStatus* keySizeStatus,
         /*optional out*/ SHA1ModeResult* sha1ModeResult,
-        /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo)
+        /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo,
+        /*optional out*/ CertificateTransparencyInfo* ctInfo)
 {
   MOZ_LOG(gCertVerifierLog, LogLevel::Debug, ("Top of VerifyCert\n"));
 
   PR_ASSERT(cert);
   PR_ASSERT(usage == certificateUsageSSLServer || !(flags & FLAG_MUST_BE_EV));
   PR_ASSERT(usage == certificateUsageSSLServer || !keySizeStatus);
   PR_ASSERT(usage == certificateUsageSSLServer || !sha1ModeResult);
 
@@ -250,16 +405,25 @@ CertVerifier::VerifyCert(CERTCertificate
     if (rv != Success) {
       // The stapled OCSP response was too big.
       PR_SetError(SEC_ERROR_OCSP_MALFORMED_RESPONSE, 0);
       return SECFailure;
     }
     stapledOCSPResponse = &stapledOCSPResponseInput;
   }
 
+  Input sctsFromTLSInput;
+  if (sctsFromTLSSECItem) {
+    rv = sctsFromTLSInput.Init(sctsFromTLSSECItem->data,
+                               sctsFromTLSSECItem->len);
+    // Silently discard the error of the extension being too big,
+    // do not fail the verification.
+    MOZ_ASSERT(rv == Success);
+  }
+
   switch (usage) {
     case certificateUsageSSLClient: {
       // XXX: We don't really have a trust bit for SSL client authentication so
       // just use trustEmail as it is the closest alternative.
       NSSCertDBTrustDomain trustDomain(trustEmail, defaultOCSPFetching,
                                        mOCSPCache, pinArg, ocspGETConfig,
                                        mCertShortLifetimeInDays,
                                        pinningDisabled, MIN_RSA_BITS_WEAK,
@@ -363,16 +527,22 @@ CertVerifier::VerifyCert(CERTCertificate
           MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
                   ("cert is EV with status %i\n", sha1ModeResults[i]));
           if (evOidPolicy) {
             *evOidPolicy = evPolicyOidTag;
           }
           if (sha1ModeResult) {
             *sha1ModeResult = sha1ModeResults[i];
           }
+          rv = VerifySignedCertificateTimestamps(trustDomain, builtChain,
+                                                 sctsFromTLSInput, time,
+                                                 ctInfo);
+          if (rv != Success) {
+            break;
+          }
         }
       }
       if (rv == Success) {
         break;
       }
 #endif
 
       if (flags & FLAG_MUST_BE_EV) {
@@ -444,16 +614,22 @@ CertVerifier::VerifyCert(CERTCertificate
           }
           if (rv == Success) {
             if (keySizeStatus) {
               *keySizeStatus = keySizeStatuses[i];
             }
             if (sha1ModeResult) {
               *sha1ModeResult = sha1ModeResults[j];
             }
+            rv = VerifySignedCertificateTimestamps(trustDomain, builtChain,
+                                                   sctsFromTLSInput, time,
+                                                   ctInfo);
+            if (rv != Success) {
+              break;
+            }
           }
         }
       }
 
       if (rv == Success) {
         break;
       }
 
@@ -626,27 +802,29 @@ CertVerifier::VerifyCert(CERTCertificate
   }
 
   return SECSuccess;
 }
 
 SECStatus
 CertVerifier::VerifySSLServerCert(const UniqueCERTCertificate& peerCert,
                      /*optional*/ const SECItem* stapledOCSPResponse,
+                     /*optional*/ const SECItem* sctsFromTLS,
                                   Time time,
                      /*optional*/ void* pinarg,
                                   const char* hostname,
                           /*out*/ UniqueCERTCertList& builtChain,
                      /*optional*/ bool saveIntermediatesInPermanentDatabase,
                      /*optional*/ Flags flags,
                  /*optional out*/ SECOidTag* evOidPolicy,
                  /*optional out*/ OCSPStaplingStatus* ocspStaplingStatus,
                  /*optional out*/ KeySizeStatus* keySizeStatus,
                  /*optional out*/ SHA1ModeResult* sha1ModeResult,
-                 /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo)
+                 /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo,
+                 /*optional out*/ CertificateTransparencyInfo* ctInfo)
 {
   PR_ASSERT(peerCert);
   // XXX: PR_ASSERT(pinarg)
   PR_ASSERT(hostname);
   PR_ASSERT(hostname[0]);
 
   if (evOidPolicy) {
     *evOidPolicy = SEC_OID_UNKNOWN;
@@ -656,19 +834,20 @@ CertVerifier::VerifySSLServerCert(const 
     PR_SetError(SSL_ERROR_BAD_CERT_DOMAIN, 0);
     return SECFailure;
   }
 
   // CreateCertErrorRunnable assumes that CheckCertHostname is only called
   // if VerifyCert succeeded.
   SECStatus rv = VerifyCert(peerCert.get(), certificateUsageSSLServer, time,
                             pinarg, hostname, builtChain, flags,
-                            stapledOCSPResponse, evOidPolicy,
-                            ocspStaplingStatus, keySizeStatus,
-                            sha1ModeResult, pinningTelemetryInfo);
+                            stapledOCSPResponse, sctsFromTLS,
+                            evOidPolicy, ocspStaplingStatus, keySizeStatus,
+                            sha1ModeResult, pinningTelemetryInfo,
+                            ctInfo);
   if (rv != SECSuccess) {
     return rv;
   }
 
   Input peerCertInput;
   Result result = peerCertInput.Init(peerCert->derCert.data,
                                      peerCert->derCert.len);
   if (result != Success) {
--- a/security/certverifier/CertVerifier.h
+++ b/security/certverifier/CertVerifier.h
@@ -3,21 +3,32 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef CertVerifier_h
 #define CertVerifier_h
 
 #include "BRNameMatchingPolicy.h"
+#include "CTVerifyResult.h"
 #include "OCSPCache.h"
 #include "ScopedNSSTypes.h"
 #include "mozilla/Telemetry.h"
+#include "mozilla/UniquePtr.h"
 #include "pkix/pkixtypes.h"
 
+namespace mozilla { namespace ct {
+
+// Including MultiLogCTVerifier.h would bring along all of its dependent
+// headers and force us to export them in moz.build. Just forward-declare
+// the class here instead.
+class MultiLogCTVerifier;
+
+} } // namespace mozilla::ct
+
 namespace mozilla { namespace psm {
 
 // These values correspond to the CERT_CHAIN_KEY_SIZE_STATUS telemetry.
 enum class KeySizeStatus {
   NeverChecked = 0,
   LargeMinimumSucceeded = 1,
   CompatibilityRisk = 2,
   AlreadyBad = 3,
@@ -44,16 +55,31 @@ public:
   int32_t certPinningResultBucket;
   // Should we accumulate telemetry for the root?
   bool accumulateForRoot;
   int32_t rootBucket;
 
   void Reset() { accumulateForRoot = false; accumulateResult = false; }
 };
 
+class CertificateTransparencyInfo
+{
+public:
+  // Was CT enabled?
+  bool enabled;
+  // Did we receive and process any binary SCT data from the supported sources?
+  bool processedSCTs;
+  // Verification result of the processed SCTs.
+  mozilla::ct::CTVerifyResult verifyResult;
+
+  void Reset() { enabled = false; processedSCTs = false; verifyResult.Reset(); }
+};
+
+class NSSCertDBTrustDomain;
+
 class CertVerifier
 {
 public:
   typedef unsigned int Flags;
   // XXX: FLAG_LOCAL_ONLY is ignored in the classic verification case
   static const Flags FLAG_LOCAL_ONLY;
   // Don't perform fallback DV validation on EV validation failure.
   static const Flags FLAG_MUST_BE_EV;
@@ -74,36 +100,40 @@ public:
   SECStatus VerifyCert(CERTCertificate* cert,
                        SECCertificateUsage usage,
                        mozilla::pkix::Time time,
                        void* pinArg,
                        const char* hostname,
                /*out*/ UniqueCERTCertList& builtChain,
                        Flags flags = 0,
        /*optional in*/ const SECItem* stapledOCSPResponse = nullptr,
+       /*optional in*/ const SECItem* sctsFromTLS = nullptr,
       /*optional out*/ SECOidTag* evOidPolicy = nullptr,
       /*optional out*/ OCSPStaplingStatus* ocspStaplingStatus = nullptr,
       /*optional out*/ KeySizeStatus* keySizeStatus = nullptr,
       /*optional out*/ SHA1ModeResult* sha1ModeResult = nullptr,
-      /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo = nullptr);
+      /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo = nullptr,
+      /*optional out*/ CertificateTransparencyInfo* ctInfo = nullptr);
 
   SECStatus VerifySSLServerCert(
                     const UniqueCERTCertificate& peerCert,
        /*optional*/ const SECItem* stapledOCSPResponse,
+       /*optional*/ const SECItem* sctsFromTLS,
                     mozilla::pkix::Time time,
        /*optional*/ void* pinarg,
                     const char* hostname,
             /*out*/ UniqueCERTCertList& builtChain,
        /*optional*/ bool saveIntermediatesInPermanentDatabase = false,
        /*optional*/ Flags flags = 0,
    /*optional out*/ SECOidTag* evOidPolicy = nullptr,
    /*optional out*/ OCSPStaplingStatus* ocspStaplingStatus = nullptr,
    /*optional out*/ KeySizeStatus* keySizeStatus = nullptr,
    /*optional out*/ SHA1ModeResult* sha1ModeResult = nullptr,
-   /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo = nullptr);
+   /*optional out*/ PinningTelemetryInfo* pinningTelemetryInfo = nullptr,
+   /*optional out*/ CertificateTransparencyInfo* ctInfo = nullptr);
 
   enum PinningMode {
     pinningDisabled = 0,
     pinningAllowUserCAMITM = 1,
     pinningStrict = 2,
     pinningEnforceTestMode = 3
   };
 
@@ -121,37 +151,56 @@ public:
   enum OcspDownloadConfig {
     ocspOff = 0,
     ocspOn = 1,
     ocspEVOnly = 2
   };
   enum OcspStrictConfig { ocspRelaxed = 0, ocspStrict };
   enum OcspGetConfig { ocspGetDisabled = 0, ocspGetEnabled = 1 };
 
+  enum class CertificateTransparencyMode {
+    Disabled = 0,
+    TelemetryOnly = 1,
+  };
+
   CertVerifier(OcspDownloadConfig odc, OcspStrictConfig osc,
                OcspGetConfig ogc, uint32_t certShortLifetimeInDays,
                PinningMode pinningMode, SHA1Mode sha1Mode,
                BRNameMatchingPolicy::Mode nameMatchingMode,
-               NetscapeStepUpPolicy netscapeStepUpPolicy);
+               NetscapeStepUpPolicy netscapeStepUpPolicy,
+               CertificateTransparencyMode ctMode);
   ~CertVerifier();
 
   void ClearOCSPCache() { mOCSPCache.Clear(); }
 
   const OcspDownloadConfig mOCSPDownloadConfig;
   const bool mOCSPStrict;
   const bool mOCSPGETEnabled;
   const uint32_t mCertShortLifetimeInDays;
   const PinningMode mPinningMode;
   const SHA1Mode mSHA1Mode;
   const BRNameMatchingPolicy::Mode mNameMatchingMode;
   const NetscapeStepUpPolicy mNetscapeStepUpPolicy;
+  const CertificateTransparencyMode mCTMode;
 
 private:
   OCSPCache mOCSPCache;
 
+  // We only have a forward declaration of MultiLogCTVerifier (see above),
+  // so we keep a pointer to it and allocate dynamically.
+  UniquePtr<mozilla::ct::MultiLogCTVerifier> mCTVerifier;
+
+  void LoadKnownCTLogs();
+  mozilla::pkix::Result VerifySignedCertificateTimestamps(
+                     NSSCertDBTrustDomain& trustDomain,
+                     const UniqueCERTCertList& builtChain,
+                     mozilla::pkix::Input sctsFromTLS,
+                     mozilla::pkix::Time time,
+    /*optional out*/ CertificateTransparencyInfo* ctInfo);
+
   // Returns true if the configured SHA1 mode is more restrictive than the given
   // mode. SHA1Mode::Forbidden is more restrictive than any other mode except
   // Forbidden. Next is ImportedRoot, then ImportedRootOrBefore2016, then
   // Allowed. (A mode is never more restrictive than itself.)
   bool SHA1ModeMoreRestrictiveThanGivenMode(SHA1Mode mode);
 };
 
 mozilla::pkix::Result IsCertBuiltInRoot(CERTCertificate* cert, bool& result);
--- a/security/certverifier/MultiLogCTVerifier.cpp
+++ b/security/certverifier/MultiLogCTVerifier.cpp
@@ -3,65 +3,36 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "MultiLogCTVerifier.h"
 
 #include "CTObjectsExtractor.h"
 #include "CTSerialization.h"
+#include "mozilla/Assertions.h"
 #include "mozilla/Move.h"
 
 namespace mozilla { namespace ct {
 
 using namespace mozilla::pkix;
 
-// The possible verification statuses for a Signed Certificate Timestamp.
-enum class SCTVerifyStatus {
-  UnknownLog, // The SCT is from an unknown log and can not be verified.
-  Invalid, // The SCT is from a known log, but the signature is invalid.
-  OK // The SCT is from a known log, and the signature is valid.
-};
-
 // Note: this moves |sct| to the target list in |result|, invalidating |sct|.
 static Result
 StoreVerifiedSct(CTVerifyResult& result,
                  SignedCertificateTimestamp&& sct,
-                 SCTVerifyStatus status)
+                 SignedCertificateTimestamp::VerificationStatus status)
 {
-  SCTList* target;
-  switch (status) {
-    case SCTVerifyStatus::UnknownLog:
-      target = &result.unknownLogsScts;
-      break;
-    case SCTVerifyStatus::Invalid:
-      target = &result.invalidScts;
-      break;
-    case SCTVerifyStatus::OK:
-      target = &result.verifiedScts;
-      break;
-    default:
-      MOZ_ASSERT_UNREACHABLE("Unexpected SCTVerifyStatus type");
-      return Result::FATAL_ERROR_LIBRARY_FAILURE;
-  }
-  if (!target->append(Move(sct))) {
+  sct.verificationStatus = status;
+  if (!result.scts.append(Move(sct))) {
     return Result::FATAL_ERROR_NO_MEMORY;
   }
   return Success;
 }
 
-void
-CTVerifyResult::Reset()
-{
-  verifiedScts.clear();
-  invalidScts.clear();
-  unknownLogsScts.clear();
-  decodingErrors = 0;
-}
-
 Result
 MultiLogCTVerifier::AddLog(Input publicKey)
 {
   CTLogVerifier log;
   Result rv = log.Init(publicKey);
   if (rv != Success) {
     return rv;
   }
@@ -72,17 +43,17 @@ MultiLogCTVerifier::AddLog(Input publicK
 }
 
 Result
 MultiLogCTVerifier::Verify(Input cert,
                            Input issuerSubjectPublicKeyInfo,
                            Input sctListFromCert,
                            Input sctListFromOCSPResponse,
                            Input sctListFromTLSExtension,
-                           uint64_t time,
+                           Time time,
                            CTVerifyResult& result)
 {
   MOZ_ASSERT(cert.GetLength() > 0);
   result.Reset();
 
   Result rv;
 
   // Verify embedded SCTs
@@ -128,17 +99,17 @@ MultiLogCTVerifier::Verify(Input cert,
   }
   return Success;
 }
 
 Result
 MultiLogCTVerifier::VerifySCTs(Input encodedSctList,
                                const LogEntry& expectedEntry,
                                SignedCertificateTimestamp::Origin origin,
-                               uint64_t time,
+                               Time time,
                                CTVerifyResult& result)
 {
   Reader listReader;
   Result rv = DecodeSCTList(encodedSctList, listReader);
   if (rv != Success) {
     result.decodingErrors++;
     return Success;
   }
@@ -166,46 +137,57 @@ MultiLogCTVerifier::VerifySCTs(Input enc
     }
   }
   return Success;
 }
 
 Result
 MultiLogCTVerifier::VerifySingleSCT(SignedCertificateTimestamp&& sct,
                                     const LogEntry& expectedEntry,
-                                    uint64_t time,
+                                    Time time,
                                     CTVerifyResult& result)
 {
   CTLogVerifier* matchingLog = nullptr;
   for (auto& log : mLogs) {
     if (log.keyId() == sct.logId) {
       matchingLog = &log;
       break;
     }
   }
 
   if (!matchingLog) {
     // SCT does not match any known log.
-    return StoreVerifiedSct(result, Move(sct), SCTVerifyStatus::UnknownLog);
+    return StoreVerifiedSct(result, Move(sct),
+      SignedCertificateTimestamp::VerificationStatus::UnknownLog);
   }
 
   if (!matchingLog->SignatureParametersMatch(sct.signature)) {
     // SCT signature parameters do not match the log's.
-    return StoreVerifiedSct(result, Move(sct), SCTVerifyStatus::Invalid);
+    return StoreVerifiedSct(result, Move(sct),
+      SignedCertificateTimestamp::VerificationStatus::InvalidSignature);
   }
 
   Result rv = matchingLog->Verify(expectedEntry, sct);
   if (rv != Success) {
     if (rv == Result::ERROR_BAD_SIGNATURE) {
-      return StoreVerifiedSct(result, Move(sct), SCTVerifyStatus::Invalid);
+      return StoreVerifiedSct(result, Move(sct),
+        SignedCertificateTimestamp::VerificationStatus::InvalidSignature);
     }
     return rv;
   }
 
+  // |sct.timestamp| is measured in milliseconds since the epoch,
+  // ignoring leap seconds. When converting it to a second-level precision
+  // pkix::Time, we need to round it either up or down. In our case, rounding up
+  // is more "secure", although practically it does not matter.
+  Time sctTime = TimeFromEpochInSeconds((sct.timestamp + 999u) / 1000u);
+
   // SCT verified ok, just make sure the timestamp is legitimate.
-  if (sct.timestamp > time) {
-    return StoreVerifiedSct(result, Move(sct), SCTVerifyStatus::Invalid);
+  if (sctTime > time) {
+    return StoreVerifiedSct(result, Move(sct),
+      SignedCertificateTimestamp::VerificationStatus::InvalidTimestamp);
   }
 
-  return StoreVerifiedSct(result, Move(sct), SCTVerifyStatus::OK);
+  return StoreVerifiedSct(result, Move(sct),
+    SignedCertificateTimestamp::VerificationStatus::OK);
 }
 
 } } // namespace mozilla::ct
--- a/security/certverifier/MultiLogCTVerifier.h
+++ b/security/certverifier/MultiLogCTVerifier.h
@@ -3,55 +3,25 @@
 /* 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 MultiLogCTVerifier_h
 #define MultiLogCTVerifier_h
 
 #include "CTLogVerifier.h"
+#include "CTVerifyResult.h"
 #include "mozilla/Vector.h"
 #include "pkix/Input.h"
 #include "pkix/Result.h"
+#include "pkix/Time.h"
 #include "SignedCertificateTimestamp.h"
 
 namespace mozilla { namespace ct {
 
-typedef Vector<SignedCertificateTimestamp> SCTList;
-
-// Holds Signed Certificate Timestamps, arranged by their verification results.
-class CTVerifyResult
-{
-public:
-  // SCTs from known logs where the signature verified correctly.
-  SCTList verifiedScts;
-
-  // SCTs from known logs where the signature failed to verify.
-  SCTList invalidScts;
-
-  // SCTs from unknown logs and as such are unverifiable.
-  SCTList unknownLogsScts;
-
-  // For a certificate to pass Certificate Transparency verification, at least
-  // one of the provided SCTs must validate. The verifier makes the best effort
-  // to extract the available SCTs from the binary sources provided to it.
-  // If some SCT cannot be extracted due to encoding errors, the verifier
-  // proceeds to the next available one. In other words, decoding errors are
-  // effectively ignored.
-  // Note that a serialized SCT may fail to decode for a "legitimate" reason,
-  // e.g. if the SCT is from a future version of the Certificate Transparency
-  // standard.
-  // |decodingErrors| field counts the errors of the above kind.
-  // This field is purely informational; there is probably nothing to do with it
-  // in release builds, but it is useful in unit tests.
-  size_t decodingErrors;
-
-  void Reset();
-};
-
 // A Certificate Transparency verifier that can verify Signed Certificate
 // Timestamps from multiple logs.
 class MultiLogCTVerifier
 {
 public:
   // Adds a new log to the list of known logs to verify against.
   pkix::Result AddLog(pkix::Input publicKey);
 
@@ -75,44 +45,41 @@ public:
   // |issuerSubjectPublicKeyInfo|  SPKI of |cert|'s issuer. Can be empty,
   //                               in which case the embedded SCT list
   //                               won't be verified.
   // |sctListFromOCSPResponse|  SCT list included in a stapled OCSP response
   //                            for |cert|. Empty if not available.
   // |sctListFromTLSExtension|  is the SCT list from the TLS extension. Empty
   //                            if no extension was present.
   // |time|  the current time. Used to make sure SCTs are not in the future.
-  //         Measured in milliseconds since the epoch, ignoring leap seconds
-  //         (same format as used by the "timestamp" field of
-  //         SignedCertificateTimestamp).
   // |result|  will be filled with the SCTs present, divided into categories
   //           based on the verification result.
   pkix::Result Verify(pkix::Input cert,
                       pkix::Input issuerSubjectPublicKeyInfo,
                       pkix::Input sctListFromCert,
                       pkix::Input sctListFromOCSPResponse,
                       pkix::Input sctListFromTLSExtension,
-                      uint64_t time,
+                      pkix::Time time,
                       CTVerifyResult& result);
 
 private:
   // Verifies a list of SCTs from |encodedSctList| over |expectedEntry|,
   // placing the verification results in |result|. The SCTs in the list
   // come from |origin| (as will be reflected in the origin field of each SCT).
   pkix::Result VerifySCTs(pkix::Input encodedSctList,
                           const LogEntry& expectedEntry,
                           SignedCertificateTimestamp::Origin origin,
-                          uint64_t time,
+                          pkix::Time time,
                           CTVerifyResult& result);
 
   // Verifies a single, parsed SCT against all known logs.
   // Note: moves |sct| to the target list in |result|, invalidating |sct|.
   pkix::Result VerifySingleSCT(SignedCertificateTimestamp&& sct,
                                const ct::LogEntry& expectedEntry,
-                               uint64_t time,
+                               pkix::Time time,
                                CTVerifyResult& result);
 
   // The list of known logs.
   Vector<CTLogVerifier> mLogs;
 };
 
 } } // namespace mozilla::ct
 
--- a/security/certverifier/NSSCertDBTrustDomain.cpp
+++ b/security/certverifier/NSSCertDBTrustDomain.cpp
@@ -10,16 +10,17 @@
 
 #include "ExtendedValidation.h"
 #include "NSSErrorsService.h"
 #include "OCSPRequestor.h"
 #include "OCSPVerificationTrustDomain.h"
 #include "PublicKeyPinningService.h"
 #include "cert.h"
 #include "certdb.h"
+#include "mozilla/Assertions.h"
 #include "mozilla/Casting.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/Unused.h"
 #include "nsNSSCertificate.h"
 #include "nsServiceManagerUtils.h"
 #include "nss.h"
 #include "pk11pub.h"
 #include "pkix/Result.h"
@@ -68,16 +69,18 @@ NSSCertDBTrustDomain::NSSCertDBTrustDoma
   , mValidityCheckingMode(validityCheckingMode)
   , mSHA1Mode(sha1Mode)
   , mNetscapeStepUpPolicy(netscapeStepUpPolicy)
   , mBuiltChain(builtChain)
   , mPinningTelemetryInfo(pinningTelemetryInfo)
   , mHostname(hostname)
   , mCertBlocklist(do_GetService(NS_CERTBLOCKLIST_CONTRACTID))
   , mOCSPStaplingStatus(CertVerifier::OCSP_STAPLING_NEVER_CHECKED)
+  , mSCTListFromCertificate()
+  , mSCTListFromOCSPStapling()
 {
 }
 
 // If useRoots is true, we only use root certificates in the candidate list.
 // If useRoots is false, we only use non-root certificates in the list.
 static Result
 FindIssuerInner(const UniqueCERTCertList& candidates, bool useRoots,
                 Input encodedIssuerName, TrustDomain::IssuerChecker& checker,
@@ -958,19 +961,69 @@ NSSCertDBTrustDomain::NetscapeStepUpMatc
       return Success;
     default:
       MOZ_ASSERT_UNREACHABLE("unhandled NetscapeStepUpPolicy type");
   }
   return Result::FATAL_ERROR_LIBRARY_FAILURE;
 }
 
 void
-NSSCertDBTrustDomain::NoteAuxiliaryExtension(AuxiliaryExtension /*extension*/,
-                                             Input /*extensionData*/)
+NSSCertDBTrustDomain::ResetAccumulatedState()
+{
+  mOCSPStaplingStatus = CertVerifier::OCSP_STAPLING_NEVER_CHECKED;
+  mSCTListFromOCSPStapling = nullptr;
+  mSCTListFromCertificate = nullptr;
+}
+
+static Input
+SECItemToInput(const UniqueSECItem& item)
+{
+  Input result;
+  if (item) {
+    MOZ_ASSERT(item->type == siBuffer);
+    Result rv = result.Init(item->data, item->len);
+    // As used here, |item| originally comes from an Input,
+    // so there should be no issues converting it back.
+    MOZ_ASSERT(rv == Success);
+    Unused << rv; // suppresses warnings in release builds
+  }
+  return result;
+}
+
+Input
+NSSCertDBTrustDomain::GetSCTListFromCertificate() const
 {
+  return SECItemToInput(mSCTListFromCertificate);
+}
+
+Input
+NSSCertDBTrustDomain::GetSCTListFromOCSPStapling() const
+{
+  return SECItemToInput(mSCTListFromOCSPStapling);
+}
+
+void
+NSSCertDBTrustDomain::NoteAuxiliaryExtension(AuxiliaryExtension extension,
+                                             Input extensionData)
+{
+  UniqueSECItem* out = nullptr;
+  switch (extension) {
+    case AuxiliaryExtension::EmbeddedSCTList:
+      out = &mSCTListFromCertificate;
+      break;
+    case AuxiliaryExtension::SCTListFromOCSPResponse:
+      out = &mSCTListFromOCSPStapling;
+      break;
+    default:
+      MOZ_ASSERT_UNREACHABLE("unhandled AuxiliaryExtension");
+  }
+  if (out) {
+    SECItem extensionDataItem = UnsafeMapInputToSECItem(extensionData);
+    out->reset(SECITEM_DupItem(&extensionDataItem));
+  }
 }
 
 SECStatus
 InitializeNSS(const char* dir, bool readOnly, bool loadPKCS11Modules)
 {
   // The NSS_INIT_NOROOTINIT flag turns off the loading of the root certs
   // module by NSS_Initialize because we will load it in InstallLoadableRoots
   // later.  It also allows us to work around a bug in the system NSS in
--- a/security/certverifier/NSSCertDBTrustDomain.h
+++ b/security/certverifier/NSSCertDBTrustDomain.h
@@ -140,24 +140,32 @@ public:
 
   virtual Result IsChainValid(const mozilla::pkix::DERArray& certChain,
                               mozilla::pkix::Time time) override;
 
   virtual void NoteAuxiliaryExtension(
                    mozilla::pkix::AuxiliaryExtension extension,
                    mozilla::pkix::Input extensionData) override;
 
+  // Resets the OCSP stapling status and SCT lists accumulated during
+  // the chain building.
+  void ResetAccumulatedState();
+
   CertVerifier::OCSPStaplingStatus GetOCSPStaplingStatus() const
   {
     return mOCSPStaplingStatus;
   }
-  void ResetOCSPStaplingStatus()
-  {
-    mOCSPStaplingStatus = CertVerifier::OCSP_STAPLING_NEVER_CHECKED;
-  }
+
+  // SCT lists (see Certificate Transparency) extracted during
+  // certificate verification. Note that the returned Inputs are invalidated
+  // the next time a chain is built and by ResetAccumulatedState method
+  // (and when the TrustDomain object is destroyed).
+
+  mozilla::pkix::Input GetSCTListFromCertificate() const;
+  mozilla::pkix::Input GetSCTListFromOCSPStapling() const;
 
 private:
   enum EncodedResponseSource {
     ResponseIsFromNetwork = 1,
     ResponseWasStapled = 2
   };
   Result VerifyAndMaybeCacheEncodedOCSPResponse(
     const mozilla::pkix::CertID& certID, mozilla::pkix::Time time,
@@ -175,13 +183,16 @@ private:
   ValidityCheckingMode mValidityCheckingMode;
   CertVerifier::SHA1Mode mSHA1Mode;
   NetscapeStepUpPolicy mNetscapeStepUpPolicy;
   UniqueCERTCertList& mBuiltChain; // non-owning
   PinningTelemetryInfo* mPinningTelemetryInfo;
   const char* mHostname; // non-owning - only used for pinning checks
   nsCOMPtr<nsICertBlocklist> mCertBlocklist;
   CertVerifier::OCSPStaplingStatus mOCSPStaplingStatus;
+  // Certificate Transparency data extracted during certificate verification
+  UniqueSECItem mSCTListFromCertificate;
+  UniqueSECItem mSCTListFromOCSPStapling;
 };
 
 } } // namespace mozilla::psm
 
 #endif // NSSCertDBTrustDomain_h
--- a/security/certverifier/SignedCertificateTimestamp.h
+++ b/security/certverifier/SignedCertificateTimestamp.h
@@ -73,33 +73,49 @@ struct DigitallySigned
 // SignedCertificateTimestamp struct in RFC 6962, Section 3.2.
 struct SignedCertificateTimestamp
 {
   // Version enum in RFC 6962, Section 3.2.
   enum class Version {
     V1 = 0,
   };
 
-  // Source of the SCT - supplementary, not defined in CT RFC.
-  // Note: The numeric values are used within histograms and should not change
-  // or be re-assigned.
-  enum class Origin {
-    Embedded = 0,
-    TLSExtension = 1,
-    OCSPResponse = 2,
-  };
-
   Version version;
   Buffer logId;
   // "timestamp" is the current time in milliseconds, measured since the epoch,
   // ignoring leap seconds. See RFC 6962, Section 3.2.
   uint64_t timestamp;
   Buffer extensions;
   DigitallySigned signature;
+
+  // Supplementary fields, not defined in CT RFC. Set during the various
+  // stages of processing the received SCTs.
+
+  enum class Origin {
+    Unknown,
+    Embedded,
+    TLSExtension,
+    OCSPResponse
+  };
+
+  enum class VerificationStatus {
+    None,
+    // The SCT is from a known log, and the signature is valid.
+    OK,
+    // The SCT is from an unknown log and can not be verified.
+    UnknownLog,
+    // The SCT is from a known log, but the signature is invalid.
+    InvalidSignature,
+    // The SCT signature is valid, but the timestamp is in the future.
+    // Such SCT are considered invalid (see RFC 6962, Section 5.2).
+    InvalidTimestamp
+  };
+
   Origin origin;
+  VerificationStatus verificationStatus;
 };
 
 
 inline pkix::Result BufferToInput(const Buffer& buffer, pkix::Input& input)
 {
   return input.Init(buffer.begin(), buffer.length());
 }
 
--- a/security/certverifier/moz.build
+++ b/security/certverifier/moz.build
@@ -2,25 +2,29 @@
 # 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/.
 
 EXPORTS += [
     'BRNameMatchingPolicy.h',
     'CertVerifier.h',
+    'CTVerifyResult.h',
     'OCSPCache.h',
+    'SignedCertificateTimestamp.h',
+    'SignedTreeHead.h',
 ]
 
 UNIFIED_SOURCES += [
     'BRNameMatchingPolicy.cpp',
     'CertVerifier.cpp',
     'CTLogVerifier.cpp',
     'CTObjectsExtractor.cpp',
     'CTSerialization.cpp',
+    'CTVerifyResult.cpp',
     'MultiLogCTVerifier.cpp',
     'NSSCertDBTrustDomain.cpp',
     'OCSPCache.cpp',
     'OCSPRequestor.cpp',
     'OCSPVerificationTrustDomain.cpp',
     'SignedCertificateTimestamp.cpp',
 ]
 
--- a/security/certverifier/tests/gtest/MultiLogCTVerifierTest.cpp
+++ b/security/certverifier/tests/gtest/MultiLogCTVerifierTest.cpp
@@ -19,42 +19,46 @@
 
 namespace mozilla { namespace ct {
 
 using namespace mozilla::pkix;
 
 class MultiLogCTVerifierTest : public ::testing::Test
 {
 public:
+  MultiLogCTVerifierTest()
+    : mNow(Time::uninitialized)
+  {}
+
   void SetUp() override
   {
     // Does nothing if NSS is already initialized.
     MOZ_RELEASE_ASSERT(NSS_NoDB_Init(nullptr) == SECSuccess);
 
     ASSERT_EQ(Success, mVerifier.AddLog(InputForBuffer(GetTestPublicKey())));
 
     mTestCert = GetDEREncodedX509Cert();
     mEmbeddedCert = GetDEREncodedTestEmbeddedCert();
     mCaCert = GetDEREncodedCACert();
     mCaCertSPKI = ExtractCertSPKI(mCaCert);
     mIntermediateCert = GetDEREncodedIntermediateCert();
     mIntermediateCertSPKI = ExtractCertSPKI(mIntermediateCert);
 
     // Set the current time making sure all test timestamps are in the past.
-    mNow = UINT64_MAX;
+    mNow = TimeFromEpochInSeconds(1451606400u); // Date.parse("2016-01-01")/1000
   }
 
   void CheckForSingleVerifiedSCTInResult(const CTVerifyResult& result,
     SignedCertificateTimestamp::Origin origin)
   {
     EXPECT_EQ(0U, result.decodingErrors);
-    EXPECT_TRUE(result.invalidScts.empty());
-    EXPECT_TRUE(result.unknownLogsScts.empty());
-    ASSERT_EQ(1U, result.verifiedScts.length());
-    EXPECT_EQ(origin, result.verifiedScts[0].origin);
+    ASSERT_EQ(1U, result.scts.length());
+    EXPECT_EQ(SignedCertificateTimestamp::VerificationStatus::OK,
+              result.scts[0].verificationStatus);
+    EXPECT_EQ(origin, result.scts[0].origin);
   }
 
   // Writes an SCTList containing a single |sct| into |output|.
   void EncodeSCTListForTesting(Input sct, Buffer& output)
   {
     Vector<Input> list;
     ASSERT_TRUE(list.append(Move(sct)));
     ASSERT_EQ(Success, EncodeSCTList(list, output));
@@ -88,17 +92,17 @@ public:
 protected:
   MultiLogCTVerifier mVerifier;
   Buffer mTestCert;
   Buffer mEmbeddedCert;
   Buffer mCaCert;
   Buffer mCaCertSPKI;
   Buffer mIntermediateCert;
   Buffer mIntermediateCertSPKI;
-  uint64_t mNow;
+  Time mNow;
 };
 
 // Test that an embedded SCT can be extracted and the extracted SCT contains
 // the expected data. This tests the ExtractEmbeddedSCTList function from
 // CTTestUtils.h that other tests here rely upon.
 TEST_F(MultiLogCTVerifierTest, ExtractEmbeddedSCT)
 {
   SignedCertificateTimestamp sct;
@@ -191,17 +195,19 @@ TEST_F(MultiLogCTVerifierTest, VerifiesS
   CTVerifyResult result;
   ASSERT_EQ(Success,
             mVerifier.Verify(InputForBuffer(mTestCert), Input(), Input(),
                              InputForBuffer(sctList), InputForBuffer(sctList),
                              mNow, result));
 
   // The result should contain verified SCTs from TLS and OCSP origins.
   EnumSet<SignedCertificateTimestamp::Origin> origins;
-  for (auto& sct : result.verifiedScts) {
+  for (const SignedCertificateTimestamp& sct : result.scts) {
+    EXPECT_EQ(SignedCertificateTimestamp::VerificationStatus::OK,
+              sct.verificationStatus);
     origins += sct.origin;
   }
   EXPECT_FALSE(
     origins.contains(SignedCertificateTimestamp::Origin::Embedded));
   EXPECT_TRUE(
     origins.contains(SignedCertificateTimestamp::Origin::OCSPResponse));
   EXPECT_TRUE(
     origins.contains(SignedCertificateTimestamp::Origin::TLSExtension));
@@ -213,13 +219,15 @@ TEST_F(MultiLogCTVerifierTest, Identifie
   GetSCTListWithInvalidLogID(sctList);
 
   CTVerifyResult result;
   ASSERT_EQ(Success,
             mVerifier.Verify(InputForBuffer(mTestCert), Input(),
                              Input(), Input(), InputForBuffer(sctList),
                              mNow, result));
 
-  EXPECT_EQ(1U, result.unknownLogsScts.length());
   EXPECT_EQ(0U, result.decodingErrors);
+  ASSERT_EQ(1U, result.scts.length());
+  EXPECT_EQ(SignedCertificateTimestamp::VerificationStatus::UnknownLog,
+            result.scts[0].verificationStatus);
 }
 
 } } // namespace mozilla::ct
--- a/security/manager/ssl/SSLServerCertVerification.cpp
+++ b/security/manager/ssl/SSLServerCertVerification.cpp
@@ -736,60 +736,65 @@ class SSLServerCertVerificationJob : pub
 {
 public:
   // Must be called only on the socket transport thread
   static SECStatus Dispatch(const RefPtr<SharedCertVerifier>& certVerifier,
                             const void* fdForLogging,
                             nsNSSSocketInfo* infoObject,
                             const UniqueCERTCertificate& serverCert,
                             const UniqueCERTCertList& peerCertChain,
-                            SECItem* stapledOCSPResponse,
+                            const SECItem* stapledOCSPResponse,
+                            const SECItem* sctsFromTLSExtension,
                             uint32_t providerFlags,
                             Time time,
                             PRTime prtime);
 private:
   NS_DECL_NSIRUNNABLE
 
   // Must be called only on the socket transport thread
   SSLServerCertVerificationJob(const RefPtr<SharedCertVerifier>& certVerifier,
                                const void* fdForLogging,
                                nsNSSSocketInfo* infoObject,
                                const UniqueCERTCertificate& cert,
                                UniqueCERTCertList peerCertChain,
-                               SECItem* stapledOCSPResponse,
+                               const SECItem* stapledOCSPResponse,
+                               const SECItem* sctsFromTLSExtension,
                                uint32_t providerFlags,
                                Time time,
                                PRTime prtime);
   const RefPtr<SharedCertVerifier> mCertVerifier;
   const void* const mFdForLogging;
   const RefPtr<nsNSSSocketInfo> mInfoObject;
   const UniqueCERTCertificate mCert;
   UniqueCERTCertList mPeerCertChain;
   const uint32_t mProviderFlags;
   const Time mTime;
   const PRTime mPRTime;
   const TimeStamp mJobStartTime;
   const UniqueSECItem mStapledOCSPResponse;
+  const UniqueSECItem mSCTsFromTLSExtension;
 };
 
 SSLServerCertVerificationJob::SSLServerCertVerificationJob(
     const RefPtr<SharedCertVerifier>& certVerifier, const void* fdForLogging,
     nsNSSSocketInfo* infoObject, const UniqueCERTCertificate& cert,
-    UniqueCERTCertList peerCertChain, SECItem* stapledOCSPResponse,
+    UniqueCERTCertList peerCertChain, const SECItem* stapledOCSPResponse,
+    const SECItem* sctsFromTLSExtension,
     uint32_t providerFlags, Time time, PRTime prtime)
   : mCertVerifier(certVerifier)
   , mFdForLogging(fdForLogging)
   , mInfoObject(infoObject)
   , mCert(CERT_DupCertificate(cert.get()))
   , mPeerCertChain(Move(peerCertChain))
   , mProviderFlags(providerFlags)
   , mTime(time)
   , mPRTime(prtime)
   , mJobStartTime(TimeStamp::Now())
   , mStapledOCSPResponse(SECITEM_DupItem(stapledOCSPResponse))
+  , mSCTsFromTLSExtension(SECITEM_DupItem(sctsFromTLSExtension))
 {
 }
 
 // This function assumes that we will only use the SPDY connection coalescing
 // feature on connections where we have negotiated SPDY using NPN. If we ever
 // talk SPDY without having negotiated it with SPDY, this code will give wrong
 // and perhaps unsafe results.
 //
@@ -1211,23 +1216,98 @@ void
 GatherSuccessfulValidationTelemetry(const UniqueCERTCertList& certList)
 {
   GatherBaselineRequirementsTelemetry(certList);
   GatherEKUTelemetry(certList);
   GatherRootCATelemetry(certList);
   GatherEndEntityTelemetry(certList);
 }
 
+void
+GatherTelemetryForSingleSCT(const ct::SignedCertificateTimestamp& sct)
+{
+  // See SSL_SCTS_ORIGIN in Histograms.json.
+  uint32_t origin = 0;
+  switch (sct.origin) {
+    case ct::SignedCertificateTimestamp::Origin::Embedded:
+      origin = 1;
+      break;
+    case ct::SignedCertificateTimestamp::Origin::TLSExtension:
+      origin = 2;
+      break;
+    case ct::SignedCertificateTimestamp::Origin::OCSPResponse:
+      origin = 3;
+      break;
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unexpected SCT::Origin type");
+  }
+  Telemetry::Accumulate(Telemetry::SSL_SCTS_ORIGIN, origin);
+
+  // See SSL_SCTS_VERIFICATION_STATUS in Histograms.json.
+  uint32_t verificationStatus = 0;
+  switch (sct.verificationStatus) {
+    case ct::SignedCertificateTimestamp::VerificationStatus::OK:
+      verificationStatus = 1;
+      break;
+    case ct::SignedCertificateTimestamp::VerificationStatus::UnknownLog:
+      verificationStatus = 2;
+      break;
+    case ct::SignedCertificateTimestamp::VerificationStatus::InvalidSignature:
+      verificationStatus = 3;
+      break;
+    case ct::SignedCertificateTimestamp::VerificationStatus::InvalidTimestamp:
+      verificationStatus = 4;
+      break;
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unexpected SCT::VerificationStatus type");
+  }
+  Telemetry::Accumulate(Telemetry::SSL_SCTS_VERIFICATION_STATUS,
+                        verificationStatus);
+}
+
+void
+GatherCertificateTransparencyTelemetry(const UniqueCERTCertList& certList,
+                                       const CertificateTransparencyInfo& info)
+{
+  if (!info.enabled) {
+    // No telemetry is gathered when CT is disabled.
+    return;
+  }
+
+  if (!info.processedSCTs) {
+    // We didn't receive any SCT data for this connection.
+    Telemetry::Accumulate(Telemetry::SSL_SCTS_PER_CONNECTION, 0);
+    return;
+  }
+
+  for (const ct::SignedCertificateTimestamp& sct : info.verifyResult.scts) {
+    GatherTelemetryForSingleSCT(sct);
+  }
+
+  // Decoding errors are reported to the 0th bucket
+  // of the SSL_SCTS_VERIFICATION_STATUS enumerated probe.
+  for (size_t i = 0; i < info.verifyResult.decodingErrors; ++i) {
+    Telemetry::Accumulate(Telemetry::SSL_SCTS_VERIFICATION_STATUS, 0);
+  }
+
+  // Handle the histogram of SCTs counts.
+  uint32_t sctsCount = static_cast<uint32_t>(info.verifyResult.scts.length());
+  // Note that sctsCount can be 0 in case we've received SCT binary data,
+  // but it failed to parse (e.g. due to unsupported CT protocol version).
+  Telemetry::Accumulate(Telemetry::SSL_SCTS_PER_CONNECTION, sctsCount);
+}
+
 // Note: Takes ownership of |peerCertChain| if SECSuccess is not returned.
 SECStatus
 AuthCertificate(CertVerifier& certVerifier,
                 nsNSSSocketInfo* infoObject,
                 const UniqueCERTCertificate& cert,
                 UniqueCERTCertList& peerCertChain,
-                SECItem* stapledOCSPResponse,
+                const SECItem* stapledOCSPResponse,
+                const SECItem* sctsFromTLSExtension,
                 uint32_t providerFlags,
                 Time time)
 {
   MOZ_ASSERT(infoObject);
   MOZ_ASSERT(cert);
 
   SECStatus rv;
 
@@ -1238,30 +1318,32 @@ AuthCertificate(CertVerifier& certVerifi
 
   SECOidTag evOidPolicy;
   UniqueCERTCertList certList;
   CertVerifier::OCSPStaplingStatus ocspStaplingStatus =
     CertVerifier::OCSP_STAPLING_NEVER_CHECKED;
   KeySizeStatus keySizeStatus = KeySizeStatus::NeverChecked;
   SHA1ModeResult sha1ModeResult = SHA1ModeResult::NeverChecked;
   PinningTelemetryInfo pinningTelemetryInfo;
+  CertificateTransparencyInfo certificateTransparencyInfo;
 
   int flags = 0;
   if (!infoObject->SharedState().IsOCSPStaplingEnabled() ||
       !infoObject->SharedState().IsOCSPMustStapleEnabled()) {
     flags |= CertVerifier::FLAG_TLS_IGNORE_STATUS_REQUEST;
   }
 
   rv = certVerifier.VerifySSLServerCert(cert, stapledOCSPResponse,
-                                        time, infoObject,
+                                        sctsFromTLSExtension, time, infoObject,
                                         infoObject->GetHostNameRaw(),
                                         certList, saveIntermediates, flags,
                                         &evOidPolicy, &ocspStaplingStatus,
                                         &keySizeStatus, &sha1ModeResult,
-                                        &pinningTelemetryInfo);
+                                        &pinningTelemetryInfo,
+                                        &certificateTransparencyInfo);
   PRErrorCode savedErrorCode;
   if (rv != SECSuccess) {
     savedErrorCode = PR_GetError();
   }
 
   uint32_t evStatus = (rv != SECSuccess) ? 0                // 0 = Failure
                     : (evOidPolicy == SEC_OID_UNKNOWN) ? 1  // 1 = DV
                     : 2;                                    // 2 = EV
@@ -1302,16 +1384,18 @@ AuthCertificate(CertVerifier& certVerifi
     }
     else {
       nsc = nsNSSCertificate::Create(cert.get());
     }
   }
 
   if (rv == SECSuccess) {
     GatherSuccessfulValidationTelemetry(certList);
+    GatherCertificateTransparencyTelemetry(certList,
+                                           certificateTransparencyInfo);
 
     // The connection may get terminated, for example, if the server requires
     // a client cert. Let's provide a minimal SSLStatus
     // to the caller that contains at least the cert and its status.
     if (!status) {
       status = new nsSSLStatus();
       infoObject->SetSSLStatus(status);
     }
@@ -1354,17 +1438,18 @@ AuthCertificate(CertVerifier& certVerifi
 
 /*static*/ SECStatus
 SSLServerCertVerificationJob::Dispatch(
   const RefPtr<SharedCertVerifier>& certVerifier,
   const void* fdForLogging,
   nsNSSSocketInfo* infoObject,
   const UniqueCERTCertificate& serverCert,
   const UniqueCERTCertList& peerCertChain,
-  SECItem* stapledOCSPResponse,
+  const SECItem* stapledOCSPResponse,
+  const SECItem* sctsFromTLSExtension,
   uint32_t providerFlags,
   Time time,
   PRTime prtime)
 {
   // Runs on the socket transport thread
   if (!certVerifier || !infoObject || !serverCert) {
     NS_ERROR("Invalid parameters for SSL server cert validation");
     PR_SetError(PR_INVALID_ARGUMENT_ERROR, 0);
@@ -1381,18 +1466,18 @@ SSLServerCertVerificationJob::Dispatch(
   if (!peerCertChainCopy) {
     PR_SetError(SEC_ERROR_NO_MEMORY, 0);
     return SECFailure;
   }
 
   RefPtr<SSLServerCertVerificationJob> job(
     new SSLServerCertVerificationJob(certVerifier, fdForLogging, infoObject,
                                      serverCert, Move(peerCertChainCopy),
-                                     stapledOCSPResponse, providerFlags,
-                                     time, prtime));
+                                     stapledOCSPResponse, sctsFromTLSExtension,
+                                     providerFlags, time, prtime));
 
   nsresult nrv;
   if (!gCertVerificationThreadPool) {
     nrv = NS_ERROR_NOT_INITIALIZED;
   } else {
     nrv = gCertVerificationThreadPool->Dispatch(job, NS_DISPATCH_NORMAL);
   }
   if (NS_FAILED(nrv)) {
@@ -1433,16 +1518,17 @@ SSLServerCertVerificationJob::Run()
     Telemetry::ID failureTelemetry
       = Telemetry::SSL_INITIAL_FAILED_CERT_VALIDATION_TIME_MOZILLAPKIX;
 
     // Reset the error code here so we can detect if AuthCertificate fails to
     // set the error code if/when it fails.
     PR_SetError(0, 0);
     SECStatus rv = AuthCertificate(*mCertVerifier, mInfoObject, mCert,
                                    mPeerCertChain, mStapledOCSPResponse.get(),
+                                   mSCTsFromTLSExtension.get(),
                                    mProviderFlags, mTime);
     MOZ_ASSERT(mPeerCertChain || rv != SECSuccess,
                "AuthCertificate() should take ownership of chain on failure");
     if (rv == SECSuccess) {
       uint32_t interval = (uint32_t) ((TimeStamp::Now() - mJobStartTime).ToMilliseconds());
       RefPtr<SSLServerCertVerificationResult> restart(
         new SSLServerCertVerificationResult(mInfoObject, 0,
                                             successTelemetry, interval));
@@ -1583,41 +1669,49 @@ AuthCertificateHook(void* arg, PRFileDes
   // We don't own these pointers.
   const SECItemArray* csa = SSL_PeerStapledOCSPResponses(fd);
   SECItem* stapledOCSPResponse = nullptr;
   // we currently only support single stapled responses
   if (csa && csa->len == 1) {
     stapledOCSPResponse = &csa->items[0];
   }
 
+  const SECItem* sctsFromTLSExtension = SSL_PeerSignedCertTimestamps(fd);
+  if (sctsFromTLSExtension && sctsFromTLSExtension->len == 0) {
+    // SSL_PeerSignedCertTimestamps returns null on error and empty item
+    // when no extension was returned by the server. We always use null when
+    // no extension was received (for whatever reason), ignoring errors.
+    sctsFromTLSExtension = nullptr;
+  }
+
   uint32_t providerFlags = 0;
   socketInfo->GetProviderFlags(&providerFlags);
 
   if (onSTSThread) {
 
     // We *must* do certificate verification on a background thread because
     // we need the socket transport thread to be free for our OCSP requests,
     // and we *want* to do certificate verification on a background thread
     // because of the performance benefits of doing so.
     socketInfo->SetCertVerificationWaiting();
     SECStatus rv = SSLServerCertVerificationJob::Dispatch(
                      certVerifier, static_cast<const void*>(fd), socketInfo,
                      serverCert, peerCertChain, stapledOCSPResponse,
-                     providerFlags, now, prnow);
+                     sctsFromTLSExtension, providerFlags, now, prnow);
     return rv;
   }
 
   // We can't do certificate verification on a background thread, because the
   // thread doing the network I/O may not interrupt its network I/O on receipt
   // of our SSLServerCertVerificationResult event, and/or it might not even be
   // a non-blocking socket.
 
   SECStatus rv = AuthCertificate(*certVerifier, socketInfo, serverCert,
                                  peerCertChain, stapledOCSPResponse,
-                                 providerFlags, now);
+                                 sctsFromTLSExtension, providerFlags, now);
   MOZ_ASSERT(peerCertChain || rv != SECSuccess,
              "AuthCertificate() should take ownership of chain on failure");
   if (rv == SECSuccess) {
     Telemetry::Accumulate(Telemetry::SSL_CERT_ERROR_OVERRIDES, 1);
     return SECSuccess;
   }
 
   PRErrorCode error = PR_GetError();
--- a/security/manager/ssl/SharedCertVerifier.h
+++ b/security/manager/ssl/SharedCertVerifier.h
@@ -17,19 +17,20 @@ protected:
 
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SharedCertVerifier)
 
   SharedCertVerifier(OcspDownloadConfig odc, OcspStrictConfig osc,
                      OcspGetConfig ogc, uint32_t certShortLifetimeInDays,
                      PinningMode pinningMode, SHA1Mode sha1Mode,
                      BRNameMatchingPolicy::Mode nameMatchingMode,
-                     NetscapeStepUpPolicy netscapeStepUpPolicy)
+                     NetscapeStepUpPolicy netscapeStepUpPolicy,
+                     CertificateTransparencyMode ctMode)
     : mozilla::psm::CertVerifier(odc, osc, ogc, certShortLifetimeInDays,
                                  pinningMode, sha1Mode, nameMatchingMode,
-                                 netscapeStepUpPolicy)
+                                 netscapeStepUpPolicy, ctMode)
   {
   }
 };
 
 } } // namespace mozilla::psm
 
 #endif // SharedCertVerifier_h
--- a/security/manager/ssl/SharedSSLState.h
+++ b/security/manager/ssl/SharedSSLState.h
@@ -38,23 +38,31 @@ public:
   void SetOCSPStaplingEnabled(bool staplingEnabled)
   {
     mOCSPStaplingEnabled = staplingEnabled;
   }
   void SetOCSPMustStapleEnabled(bool mustStapleEnabled)
   {
     mOCSPMustStapleEnabled = mustStapleEnabled;
   }
+  void SetSignedCertTimestampsEnabled(bool signedCertTimestampsEnabled)
+  {
+    mSignedCertTimestampsEnabled = signedCertTimestampsEnabled;
+  }
 
   // The following methods may be called from any thread
   bool SocketCreated();
   void NoteSocketCreated();
   static void NoteCertOverrideServiceInstantiated();
   bool IsOCSPStaplingEnabled() const { return mOCSPStaplingEnabled; }
   bool IsOCSPMustStapleEnabled() const { return mOCSPMustStapleEnabled; }
+  bool IsSignedCertTimestampsEnabled() const
+  {
+    return mSignedCertTimestampsEnabled;
+  }
 
 private:
   ~SharedSSLState();
 
   void Cleanup();
 
   nsCOMPtr<nsIObserver> mObserver;
   RefPtr<nsClientAuthRememberService> mClientAuthRemember;
@@ -62,16 +70,17 @@ private:
 
   // True if any sockets have been created that use this shared data.
   // Requires synchronization between the socket and main threads for
   // reading/writing.
   Mutex mMutex;
   bool mSocketCreated;
   bool mOCSPStaplingEnabled;
   bool mOCSPMustStapleEnabled;
+  bool mSignedCertTimestampsEnabled;
 };
 
 SharedSSLState* PublicSSLState();
 SharedSSLState* PrivateSSLState();
 
 } // namespace psm
 } // namespace mozilla
 
--- a/security/manager/ssl/nsNSSCertificate.cpp
+++ b/security/manager/ssl/nsNSSCertificate.cpp
@@ -1312,17 +1312,20 @@ nsNSSCertificate::hasValidEVOidTag(SECOi
   uint32_t flags = mozilla::psm::CertVerifier::FLAG_LOCAL_ONLY |
     mozilla::psm::CertVerifier::FLAG_MUST_BE_EV;
   UniqueCERTCertList unusedBuiltChain;
   SECStatus rv = certVerifier->VerifyCert(mCert.get(),
     certificateUsageSSLServer, mozilla::pkix::Now(),
     nullptr /* XXX pinarg */,
     nullptr /* hostname */,
     unusedBuiltChain,
-    flags, nullptr /* stapledOCSPResponse */, &resultOidTag);
+    flags,
+    nullptr /* stapledOCSPResponse */,
+    nullptr /* sctsFromTLSExtension */,
+    &resultOidTag);
 
   if (rv != SECSuccess) {
     resultOidTag = SEC_OID_UNKNOWN;
   }
   if (resultOidTag != SEC_OID_UNKNOWN) {
     validEV = true;
   }
   return NS_OK;
--- a/security/manager/ssl/nsNSSCertificateDB.cpp
+++ b/security/manager/ssl/nsNSSCertificateDB.cpp
@@ -1488,30 +1488,32 @@ VerifyCertAtTime(nsIX509Cert* aCert,
 
   UniqueCERTCertList resultChain;
   SECOidTag evOidPolicy;
   SECStatus srv;
 
   if (aHostname && aUsage == certificateUsageSSLServer) {
     srv = certVerifier->VerifySSLServerCert(nssCert,
                                             nullptr, // stapledOCSPResponse
+                                            nullptr, // sctsFromTLSExtension
                                             aTime,
                                             nullptr, // Assume no context
                                             aHostname,
                                             resultChain,
                                             false, // don't save intermediates
                                             aFlags,
                                             &evOidPolicy);
   } else {
     srv = certVerifier->VerifyCert(nssCert.get(), aUsage, aTime,
                                    nullptr, // Assume no context
                                    aHostname,
                                    resultChain,
                                    aFlags,
                                    nullptr, // stapledOCSPResponse
+                                   nullptr, // sctsFromTLSExtension
                                    &evOidPolicy);
   }
 
   PRErrorCode error = PR_GetError();
 
   nsCOMPtr<nsIX509CertList> nssCertList;
   // This adopts the list
   nssCertList = new nsNSSCertList(Move(resultChain), locker);
--- a/security/manager/ssl/nsNSSComponent.cpp
+++ b/security/manager/ssl/nsNSSComponent.cpp
@@ -1497,16 +1497,35 @@ void nsNSSComponent::setValidationOption
   PublicSSLState()->SetOCSPStaplingEnabled(ocspStaplingEnabled);
   PrivateSSLState()->SetOCSPStaplingEnabled(ocspStaplingEnabled);
 
   bool ocspMustStapleEnabled = Preferences::GetBool("security.ssl.enable_ocsp_must_staple",
                                                     true);
   PublicSSLState()->SetOCSPMustStapleEnabled(ocspMustStapleEnabled);
   PrivateSSLState()->SetOCSPMustStapleEnabled(ocspMustStapleEnabled);
 
+  const CertVerifier::CertificateTransparencyMode defaultCTMode =
+    CertVerifier::CertificateTransparencyMode::TelemetryOnly;
+  CertVerifier::CertificateTransparencyMode ctMode =
+    static_cast<CertVerifier::CertificateTransparencyMode>
+      (Preferences::GetInt("security.pki.certificate_transparency.mode",
+                           static_cast<int32_t>(defaultCTMode)));
+  switch (ctMode) {
+    case CertVerifier::CertificateTransparencyMode::Disabled:
+    case CertVerifier::CertificateTransparencyMode::TelemetryOnly:
+      break;
+    default:
+      ctMode = defaultCTMode;
+      break;
+  }
+  bool sctsEnabled =
+    ctMode != CertVerifier::CertificateTransparencyMode::Disabled;
+  PublicSSLState()->SetSignedCertTimestampsEnabled(sctsEnabled);
+  PrivateSSLState()->SetSignedCertTimestampsEnabled(sctsEnabled);
+
   CertVerifier::PinningMode pinningMode =
     static_cast<CertVerifier::PinningMode>
       (Preferences::GetInt("security.cert_pinning.enforcement_level",
                            CertVerifier::pinningDisabled));
   if (pinningMode > CertVerifier::pinningEnforceTestMode) {
     pinningMode = CertVerifier::pinningDisabled;
   }
 
@@ -1566,17 +1585,18 @@ void nsNSSComponent::setValidationOption
   uint32_t certShortLifetimeInDays;
 
   GetRevocationBehaviorFromPrefs(&odc, &osc, &ogc, &certShortLifetimeInDays,
                                  lock);
   mDefaultCertVerifier = new SharedCertVerifier(odc, osc, ogc,
                                                 certShortLifetimeInDays,
                                                 pinningMode, sha1Mode,
                                                 nameMatchingMode,
-                                                netscapeStepUpPolicy);
+                                                netscapeStepUpPolicy,
+                                                ctMode);
 }
 
 // Enable the TLS versions given in the prefs, defaulting to TLS 1.0 (min) and
 // TLS 1.2 (max) when the prefs aren't set or set to invalid values.
 nsresult
 nsNSSComponent::setEnabledTLSVersions()
 {
   // keep these values in sync with security-prefs.js
@@ -1977,16 +1997,17 @@ nsNSSComponent::Observe(nsISupports* aSu
     } else if (prefName.Equals("security.ssl.disable_session_identifiers")) {
       ConfigureTLSSessionIdentifiers();
     } else if (prefName.EqualsLiteral("security.OCSP.enabled") ||
                prefName.EqualsLiteral("security.OCSP.require") ||
                prefName.EqualsLiteral("security.OCSP.GET.enabled") ||
                prefName.EqualsLiteral("security.pki.cert_short_lifetime_in_days") ||
                prefName.EqualsLiteral("security.ssl.enable_ocsp_stapling") ||
                prefName.EqualsLiteral("security.ssl.enable_ocsp_must_staple") ||
+               prefName.EqualsLiteral("security.pki.certificate_transparency.mode") ||
                prefName.EqualsLiteral("security.cert_pinning.enforcement_level") ||
                prefName.EqualsLiteral("security.pki.sha1_enforcement_level") ||
                prefName.EqualsLiteral("security.pki.name_matching_mode") ||
                prefName.EqualsLiteral("security.pki.netscape_step_up_policy")) {
       MutexAutoLock lock(mutex);
       setValidationOptions(false, lock);
 #ifdef DEBUG
     } else if (prefName.EqualsLiteral("security.test.built_in_root_hash")) {
--- a/security/manager/ssl/nsNSSIOLayer.cpp
+++ b/security/manager/ssl/nsNSSIOLayer.cpp
@@ -438,20 +438,25 @@ nsNSSSocketInfo::IsAcceptableForHost(con
   // is trying to join onto this connection.
   RefPtr<SharedCertVerifier> certVerifier(GetDefaultCertVerifier());
   if (!certVerifier) {
     return NS_OK;
   }
   nsAutoCString hostnameFlat(PromiseFlatCString(hostname));
   CertVerifier::Flags flags = CertVerifier::FLAG_LOCAL_ONLY;
   UniqueCERTCertList unusedBuiltChain;
-  SECStatus rv = certVerifier->VerifySSLServerCert(nssCert, nullptr,
+  SECStatus rv = certVerifier->VerifySSLServerCert(nssCert,
+                                                   nullptr, // stapledOCSPResponse
+                                                   nullptr, // sctsFromTLSExtension
                                                    mozilla::pkix::Now(),
-                                                   nullptr, hostnameFlat.get(),
-                                                   unusedBuiltChain, false, flags);
+                                                   nullptr, // pinarg
+                                                   hostnameFlat.get(),
+                                                   unusedBuiltChain,
+                                                   false, // save intermediates
+                                                   flags);
   if (rv != SECSuccess) {
     return NS_OK;
   }
 
   // All tests pass
   *_retval = true;
   return NS_OK;
 }
@@ -2507,16 +2512,22 @@ nsSSLIOLayerSetOptions(PRFileDesc* fd, b
     return NS_ERROR_FAILURE;
   }
 
   bool enabled = infoObject->SharedState().IsOCSPStaplingEnabled();
   if (SECSuccess != SSL_OptionSet(fd, SSL_ENABLE_OCSP_STAPLING, enabled)) {
     return NS_ERROR_FAILURE;
   }
 
+  bool sctsEnabled = infoObject->SharedState().IsSignedCertTimestampsEnabled();
+  if (SECSuccess != SSL_OptionSet(fd, SSL_ENABLE_SIGNED_CERT_TIMESTAMPS,
+      sctsEnabled)) {
+    return NS_ERROR_FAILURE;
+  }
+
   if (SECSuccess != SSL_OptionSet(fd, SSL_HANDSHAKE_AS_CLIENT, true)) {
     return NS_ERROR_FAILURE;
   }
 
   // Set the Peer ID so that SSL proxy connections work properly and to
   // separate anonymous and/or private browsing connections.
   uint32_t flags = infoObject->GetProviderFlags();
   nsAutoCString peerId;
--- a/security/manager/ssl/nsSiteSecurityService.cpp
+++ b/security/manager/ssl/nsSiteSecurityService.cpp
@@ -725,17 +725,19 @@ nsSiteSecurityService::ProcessPKPHeader(
   // We don't want this verification to cause any network traffic that would
   // block execution. Also, since we don't have access to the original stapled
   // OCSP response, we can't enforce this aspect of the TLS Feature extension.
   // This is ok, because it will have been enforced when we originally connected
   // to the site (or it's disabled, in which case we wouldn't want to enforce it
   // anyway).
   CertVerifier::Flags flags = CertVerifier::FLAG_LOCAL_ONLY |
                               CertVerifier::FLAG_TLS_IGNORE_STATUS_REQUEST;
-  if (certVerifier->VerifySSLServerCert(nssCert, nullptr, // stapled ocsp
+  if (certVerifier->VerifySSLServerCert(nssCert,
+                                        nullptr, // stapledOCSPResponse
+                                        nullptr, // sctsFromTLSExtension
                                         now, nullptr, // pinarg
                                         host.get(), // hostname
                                         certList,
                                         false, // don't store intermediates
                                         flags)
       != SECSuccess) {
     return NS_ERROR_FAILURE;
   }
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -5,27 +5,31 @@
 
 this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/FxAccountsStorage.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient",
   "resource://gre/modules/FxAccountsClient.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsConfig",
+  "resource://gre/modules/FxAccountsConfig.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
   "resource://gre/modules/identity/jwcrypto.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsOAuthGrantClient",
   "resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsProfile",
   "resource://gre/modules/FxAccountsProfile.jsm");
@@ -33,34 +37,35 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
   "resource://services-sync/util.js");
 
 // All properties exposed by the public FxAccounts API.
 var publicProperties = [
   "accountStatus",
   "checkVerificationStatus",
   "getAccountsClient",
-  "getAccountsSignInURI",
-  "getAccountsSignUpURI",
   "getAssertion",
   "getDeviceId",
   "getKeys",
   "getOAuthToken",
   "getSignedInUser",
   "getSignedInUserProfile",
   "handleDeviceDisconnection",
   "invalidateCertificate",
   "loadAndPoll",
   "localtimeOffsetMsec",
   "notifyDevices",
   "now",
   "promiseAccountsChangeProfileURI",
   "promiseAccountsForceSigninURI",
   "promiseAccountsManageURI",
+  "promiseAccountsSignUpURI",
+  "promiseAccountsSignInURI",
   "removeCachedOAuthToken",
+  "requiresHttps",
   "resendVerificationEmail",
   "resetCredentials",
   "sessionStatus",
   "setSignedInUser",
   "signOut",
   "updateDeviceRegistration",
   "updateUserAccountData",
   "whenVerified",
@@ -773,19 +778,24 @@ FxAccountsInternal.prototype = {
           log.warn("Missing session token; skipping remote sign out");
         }).catch(err => {
           log.error("Error during remote sign out of Firefox Accounts", err);
         }).then(() => {
           return this._destroyAllOAuthTokens(tokensToRevoke);
         }).catch(err => {
           log.error("Error during destruction of oauth tokens during signout", err);
         }).then(() => {
+          FxAccountsConfig.resetConfigURLs();
           // just for testing - notifications are cheap when no observers.
           this.notifyObservers("testhelper-fxa-signout-complete");
-        });
+        })
+      } else {
+        // We want to do this either way -- but if we're signing out remotely we
+        // need to wait until we destroy the oauth tokens if we want that to succeed.
+        FxAccountsConfig.resetConfigURLs();
       }
     }).then(() => {
       this.notifyObservers(ONLOGOUT_NOTIFICATION);
     });
   },
 
   /**
    * This function should be called in conjunction with a server-side
@@ -1223,76 +1233,67 @@ FxAccountsInternal.prototype = {
                                      : this.VERIFICATION_POLL_TIMEOUT_SUBSEQUENT;
     }
     log.debug("polling with timeout = " + timeoutMs);
     this.currentTimer = setTimeout(() => {
       this.pollEmailStatus(currentState, sessionToken, "timer");
     }, timeoutMs);
   },
 
-  _requireHttps: function() {
+  requiresHttps: function() {
     let allowHttp = false;
     try {
       allowHttp = Services.prefs.getBoolPref("identity.fxaccounts.allowHttp");
     } catch(e) {
       // Pref doesn't exist
     }
     return allowHttp !== true;
   },
 
-  // Return the URI of the remote UI flows.
-  getAccountsSignUpURI: function() {
-    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri");
-    if (this._requireHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
-      throw new Error("Firefox Accounts server must use HTTPS");
-    }
-    return url;
+  promiseAccountsSignUpURI() {
+    return FxAccountsConfig.promiseAccountsSignUpURI();
   },
 
-  // Return the URI of the remote UI flows.
-  getAccountsSignInURI: function() {
-    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri");
-    if (this._requireHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
-      throw new Error("Firefox Accounts server must use HTTPS");
-    }
-    return url;
+  promiseAccountsSignInURI() {
+    return FxAccountsConfig.promiseAccountsSignInURI();
   },
 
   // Returns a promise that resolves with the URL to use to force a re-signin
   // of the current account.
-  promiseAccountsForceSigninURI: function() {
+  promiseAccountsForceSigninURI: Task.async(function *() {
+    yield FxAccountsConfig.ensureConfigured();
     let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.force_auth.uri");
-    if (this._requireHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+    if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
       throw new Error("Firefox Accounts server must use HTTPS");
     }
     let currentState = this.currentAccountState;
     // but we need to append the email address onto a query string.
     return this.getSignedInUser().then(accountData => {
       if (!accountData) {
         return null;
       }
       let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
       newQueryPortion += "email=" + encodeURIComponent(accountData.email);
       return url + newQueryPortion;
     }).then(result => currentState.resolve(result));
-  },
+  }),
 
   // Returns a promise that resolves with the URL to use to change
   // the current account's profile image.
   // if settingToEdit is set, the profile page should hightlight that setting
   // for the user to edit.
   promiseAccountsChangeProfileURI: function(entrypoint, settingToEdit = null) {
     let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri");
 
     if (settingToEdit) {
       url += (url.indexOf("?") == -1 ? "?" : "&") +
              "setting=" + encodeURIComponent(settingToEdit);
     }
 
-    if (this._requireHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+    if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
       throw new Error("Firefox Accounts server must use HTTPS");
     }
     let currentState = this.currentAccountState;
     // but we need to append the email address onto a query string.
     return this.getSignedInUser().then(accountData => {
       if (!accountData) {
         return null;
       }
@@ -1305,17 +1306,17 @@ FxAccountsInternal.prototype = {
       return url + newQueryPortion;
     }).then(result => currentState.resolve(result));
   },
 
   // Returns a promise that resolves with the URL to use to manage the current
   // user's FxA acct.
   promiseAccountsManageURI: function(entrypoint) {
     let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.settings.uri");
-    if (this._requireHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+    if (this.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
       throw new Error("Firefox Accounts server must use HTTPS");
     }
     let currentState = this.currentAccountState;
     // but we need to append the uid and email address onto a query string
     // (if the server has no matching uid it will offer to sign in with the
     // email address)
     return this.getSignedInUser().then(accountData => {
       if (!accountData) {
--- a/services/fxaccounts/FxAccountsClient.jsm
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -11,22 +11,22 @@ Cu.import("resource://gre/modules/Promis
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-common/hawkclient.js");
 Cu.import("resource://services-common/hawkrequest.js");
 Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 Cu.import("resource://gre/modules/Credentials.jsm");
 
-const HOST = Services.prefs.getCharPref("identity.fxaccounts.auth.uri");
+const HOST_PREF = "identity.fxaccounts.auth.uri";
 
 const SIGNIN = "/account/login";
 const SIGNUP = "/account/create";
 
-this.FxAccountsClient = function(host = HOST) {
+this.FxAccountsClient = function(host = Services.prefs.getCharPref(HOST_PREF)) {
   this.host = host;
 
   // The FxA auth server expects requests to certain endpoints to be authorized
   // using Hawk.
   this.hawk = new HawkClient(host);
   this.hawk.observerPrefix = "FxA:hawk";
 
   // Manage server backoff state. C.f.
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccountsConfig.jsm
@@ -0,0 +1,175 @@
+/* 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";
+this.EXPORTED_SYMBOLS = ["FxAccountsConfig"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://services-common/rest.js");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+                                  "resource://gre/modules/FxAccounts.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
+                                  "resource://gre/modules/FxAccountsWebChannel.jsm");
+
+const CONFIG_PREFS = [
+  "identity.fxaccounts.auth.uri",
+  "identity.fxaccounts.remote.oauth.uri",
+  "identity.fxaccounts.remote.profile.uri",
+  "identity.sync.tokenserver.uri",
+  "identity.fxaccounts.remote.webchannel.uri",
+  "identity.fxaccounts.settings.uri",
+  "identity.fxaccounts.remote.signup.uri",
+  "identity.fxaccounts.remote.signin.uri",
+  "identity.fxaccounts.remote.force_auth.uri",
+];
+
+this.FxAccountsConfig = {
+
+  // Returns a promise that resolves with the URI of the remote UI flows.
+  promiseAccountsSignUpURI: Task.async(function*() {
+    yield this.ensureConfigured();
+    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signup.uri");
+    if (fxAccounts.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+      throw new Error("Firefox Accounts server must use HTTPS");
+    }
+    return url;
+  }),
+
+  // Returns a promise that resolves with the URI of the remote UI flows.
+  promiseAccountsSignInURI: Task.async(function*() {
+    yield this.ensureConfigured();
+    let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.signin.uri");
+    if (fxAccounts.requiresHttps() && !/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
+      throw new Error("Firefox Accounts server must use HTTPS");
+    }
+    return url;
+  }),
+
+  resetConfigURLs() {
+    let autoconfigURL = this.getAutoConfigURL();
+    if (!autoconfigURL) {
+      return;
+    }
+    // They have the autoconfig uri pref set, so we clear all the prefs that we
+    // will have initialized, which will leave them pointing at production.
+    for (let pref of CONFIG_PREFS) {
+      Services.prefs.clearUserPref(pref);
+    }
+    // Reset the webchannel.
+    EnsureFxAccountsWebChannel();
+    if (!Services.prefs.prefHasUserValue("webchannel.allowObject.urlWhitelist")) {
+      return;
+    }
+    let whitelistValue = Services.prefs.getCharPref("webchannel.allowObject.urlWhitelist");
+    if (whitelistValue.startsWith(autoconfigURL + " ")) {
+      whitelistValue = whitelistValue.slice(autoconfigURL.length + 1);
+      // Check and see if the value will be the default, and just clear the pref if it would
+      // to avoid it showing up as changed in about:config.
+      let defaultWhitelist;
+      try {
+        defaultWhitelist = Services.prefs.getDefaultBranch("webchannel.allowObject.").getCharPref("urlWhitelist");
+      } catch (e) {
+        // No default value ...
+      }
+
+      if (defaultWhitelist === whitelistValue) {
+        Services.prefs.clearUserPref("webchannel.allowObject.urlWhitelist");
+      } else {
+        Services.prefs.setCharPref("webchannel.allowObject.urlWhitelist", whitelistValue);
+      }
+    }
+  },
+
+  getAutoConfigURL() {
+    let pref;
+    try {
+      pref = Services.prefs.getCharPref("identity.fxaccounts.autoconfig.uri");
+    } catch (e) { /* no pref */ }
+    if (!pref) {
+      // no pref / empty pref means we don't bother here.
+      return "";
+    }
+    let rootURL = Services.urlFormatter.formatURL(pref);
+    if (rootURL.endsWith("/")) {
+      rootURL.slice(0, -1);
+    }
+    return rootURL;
+  },
+
+  ensureConfigured: Task.async(function*() {
+    let isSignedIn = !!(yield fxAccounts.getSignedInUser());
+    if (!isSignedIn) {
+      yield this.fetchConfigURLs();
+    }
+  }),
+
+  // Read expected client configuration from the fxa auth server
+  // (from `identity.fxaccounts.autoconfig.uri`/.well-known/fxa-client-configuration)
+  // and replace all the relevant our prefs with the information found there.
+  // This is only done before sign-in and sign-up, and even then only if the
+  // `identity.fxaccounts.autoconfig.uri` preference is set.
+  fetchConfigURLs: Task.async(function*() {
+    let rootURL = this.getAutoConfigURL();
+    if (!rootURL) {
+      return;
+    }
+    let configURL = rootURL + "/.well-known/fxa-client-configuration";
+    let jsonStr = yield new Promise((resolve, reject) => {
+      let request = new RESTRequest(configURL);
+      request.setHeader("Accept", "application/json");
+      request.get(error => {
+        if (error) {
+          log.error(`Failed to get configuration object from "${configURL}"`, error);
+          return reject(error);
+        }
+        if (!request.response.success) {
+          log.error(`Received HTTP response code ${request.response.status} from configuration object request`);
+          if (request.response && request.response.body) {
+            log.debug("Got error response", request.response.body);
+          }
+          return reject(request.response.status);
+        }
+        resolve(request.response.body);
+      });