Merge m-c to a CLOSED TREE m-i
authorPhil Ringnalda <philringnalda@gmail.com>
Thu, 27 Oct 2016 20:36:38 -0700
changeset 319964 53679ebdd1a82107b9679839e2302b0f98e332b7
parent 319906 de41e684fb55d7e64a3eecf8ee0e4279f37e0724 (current diff)
parent 319839 944cb0fd05526894fcd90fbe7d1e625ee53cd73d (diff)
child 319965 4f928f174d21555a623a2ba24dd6a8e42824fcb9
push id20749
push userryanvm@gmail.com
push dateSat, 29 Oct 2016 13:21:21 +0000
treeherderfx-team@1b170b39ed6b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone52.0a1
Merge m-c to a CLOSED TREE m-i MozReview-Commit-ID: 2JxLeQ8GYIX
browser/locales/searchjson.py
browser/locales/searchplugins.py
devtools/client/locales/en-US/promisedebugger.dtd
devtools/client/promisedebugger/moz.build
devtools/client/promisedebugger/promise-controller.js
devtools/client/promisedebugger/promise-debugger.xhtml
devtools/client/promisedebugger/promise-panel.js
devtools/client/promisedebugger/test/.eslintrc.js
devtools/client/promisedebugger/test/head.js
devtools/client/responsive.html/browser/tunnel.js
dom/base/moz.build
dom/base/nsFrameLoader.cpp
dom/base/nsFrameLoader.h
dom/base/nsIFrameLoader.idl
dom/ipc/PBrowser.ipdl
dom/ipc/TabChild.cpp
dom/ipc/TabChild.h
dom/ipc/TabParent.cpp
dom/ipc/TabParent.h
dom/ipc/nsIBrowser.idl
dom/media/eme/MediaKeySystemAccess.cpp
layout/style/nsCSSParser.cpp
--- a/.eslintignore
+++ b/.eslintignore
@@ -60,21 +60,19 @@ browser/app/**
 browser/branding/**/firefox-branding.js
 browser/base/content/browser-social.js
 browser/base/content/nsContextMenu.js
 browser/base/content/sanitizeDialog.js
 browser/base/content/test/general/file_csp_block_all_mixedcontent.html
 browser/base/content/test/urlbar/file_blank_but_not_blank.html
 browser/base/content/newtab/**
 browser/components/downloads/**
-browser/components/feeds/**
 browser/components/privatebrowsing/**
 browser/components/sessionstore/**
 browser/components/tabview/**
-browser/components/translation/**
 # generated files in cld2
 browser/components/translation/cld2/cld-worker.js
 browser/extensions/pdfjs/**
 # generated or library files in pocket
 browser/extensions/pocket/content/panels/js/tmpl.js
 browser/extensions/pocket/content/panels/js/vendor/**
 browser/locales/**
 
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -394,44 +394,46 @@ SocialShare = {
     // define at least url. If it is undefined, we're sharing the current url in
     // the browser tab.
     let pageData = graphData ? graphData : this.currentShare;
     let sharedURI = pageData ? Services.io.newURI(pageData.url, null, null) :
                                 gBrowser.currentURI;
     if (!SocialUI.canSharePage(sharedURI))
       return;
 
+    let browserMM = gBrowser.selectedBrowser.messageManager;
+
     // the point of this action type is that we can use existing share
     // endpoints (e.g. oexchange) that do not support additional
     // socialapi functionality.  One tweak is that we shoot an event
     // containing the open graph data.
     let _dataFn;
     if (!pageData || sharedURI == gBrowser.currentURI) {
-      messageManager.addMessageListener("PageMetadata:PageDataResult", _dataFn = (msg) => {
-        messageManager.removeMessageListener("PageMetadata:PageDataResult", _dataFn);
+      browserMM.addMessageListener("PageMetadata:PageDataResult", _dataFn = (msg) => {
+        browserMM.removeMessageListener("PageMetadata:PageDataResult", _dataFn);
         let pageData = msg.json;
         if (graphData) {
           // overwrite data retreived from page with data given to us as a param
           for (let p in graphData) {
             pageData[p] = graphData[p];
           }
         }
         this.sharePage(providerOrigin, pageData, target, anchor);
       });
-      gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData", null, { target });
+      browserMM.sendAsyncMessage("PageMetadata:GetPageData", null, { target });
       return;
     }
     // if this is a share of a selected item, get any microformats
     if (!pageData.microformats && target) {
-      messageManager.addMessageListener("PageMetadata:MicroformatsResult", _dataFn = (msg) => {
-        messageManager.removeMessageListener("PageMetadata:MicroformatsResult", _dataFn);
+      browserMM.addMessageListener("PageMetadata:MicroformatsResult", _dataFn = (msg) => {
+        browserMM.removeMessageListener("PageMetadata:MicroformatsResult", _dataFn);
         pageData.microformats = msg.data;
         this.sharePage(providerOrigin, pageData, target, anchor);
       });
-      gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetMicroformats", null, { target });
+      browserMM.sendAsyncMessage("PageMetadata:GetMicroformats", null, { target });
       return;
     }
     this.currentShare = pageData;
 
     let provider;
     if (providerOrigin)
       provider = Social._getProviderFromOrigin(providerOrigin);
     else
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js
@@ -44,20 +44,22 @@ var gTests = [
     ok(gIdentityHandler._identityPopup.hidden,
        "control center should be hidden in the first window");
     win.gIdentityHandler._identityPopup.hidden = true;
 
     // Closing the new window should remove all sharing indicators.
     // We need to load the content script in the first window so that we can
     // catch the notifications fired globally when closing the second window.
     gBrowser.selectedBrowser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true);
-    yield BrowserTestUtils.closeWindow(win);
 
-    yield expectObserverCalled("recording-window-ended");
-    yield expectObserverCalled("recording-device-events");
+    let promises = [promiseObserverCalled("recording-device-events"),
+                    promiseObserverCalled("recording-window-ended")];
+    yield BrowserTestUtils.closeWindow(win);
+    yield Promise.all(promises);
+
     yield expectNoObserverCalled();
     yield checkNotSharing();
   }
 }
 
 ];
 
 function test() {
--- a/browser/components/feeds/FeedConverter.js
+++ b/browser/components/feeds/FeedConverter.js
@@ -1,9 +1,9 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* 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/. */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/debug.js");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
@@ -222,17 +222,17 @@ FeedConverter.prototype = {
             case "client":
             case "default":
               try {
                 let title = feed.title ? feed.title.plainText() : "";
                 let desc = feed.subtitle ? feed.subtitle.plainText() : "";
                 let feedReader = safeGetCharPref(getPrefActionForType(feedType), "bookmarks");
                 feedService.addToClientReader(result.uri.spec, title, desc, feed.type, feedReader);
                 return;
-              } catch(ex) { /* fallback to preview mode */ }
+              } catch (ex) { /* fallback to preview mode */ }
           }
         }
       }
 
       let ios =
           Cc["@mozilla.org/network/io-service;1"].
           getService(Ci.nsIIOService);
       let chromeChannel;
--- a/browser/components/feeds/FeedWriter.js
+++ b/browser/components/feeds/FeedWriter.js
@@ -606,17 +606,17 @@ FeedWriter.prototype = {
   _setAlwaysUseCheckedState(feedType) {
     let checkbox = this._document.getElementById("alwaysUse");
     if (checkbox) {
       let alwaysUse = false;
       try {
         if (Services.prefs.getCharPref(getPrefActionForType(feedType)) != "ask")
           alwaysUse = true;
       }
-      catch(ex) { }
+      catch (ex) { }
       this._setCheckboxCheckedState(checkbox, alwaysUse);
     }
   },
 
   _setSubscribeUsingLabel() {
     let stringLabel = "subscribeFeedUsing";
     switch (this._getFeedType()) {
       case Ci.nsIFeed.TYPE_VIDEO:
@@ -944,17 +944,17 @@ FeedWriter.prototype = {
     prefs.addObserver(PREF_AUDIO_SELECTED_READER, this, false);
     prefs.addObserver(PREF_AUDIO_SELECTED_WEB, this, false);
     prefs.addObserver(PREF_AUDIO_SELECTED_APP, this, false);
 
     this._mm.addMessageListener("FeedWriter:SetApplicationLauncherMenuItem", this);
   },
 
   receiveMessage(msg) {
-    switch(msg.name) {
+    switch (msg.name) {
       case "FeedWriter:SetApplicationLauncherMenuItem":
         let menuItem = null;
 
         if (msg.data.type == "DefaultAppMenuItem") {
           menuItem = this._defaultHandlerMenuItem;
         } else {
           // Most likely SelectedAppMenuItem
           menuItem = this._selectedAppMenuItem;
--- a/browser/components/feeds/WebContentConverter.js
+++ b/browser/components/feeds/WebContentConverter.js
@@ -915,17 +915,17 @@ WebContentConverterRegistrarContent.prot
     }).filter(child => !!child)
       .sort();
 
     // now register them
     for (num of nums) {
       let branch = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + num + ".");
       try {
         this._registerContentHandlerHavingBranch(branch);
-      } catch(ex) {
+      } catch (ex) {
         // do nothing, the next branch might have values
       }
     }
   },
 
   _typeIsRegistered(contentType, uri) {
     return this._contentTypes[contentType]
                .some(e => e.uri == uri);
--- a/browser/components/feeds/content/subscribe.js
+++ b/browser/components/feeds/content/subscribe.js
@@ -3,17 +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/. */
 
 var SubscribeHandler = {
   /**
    * The nsIFeedWriter object that produces the UI
    */
   _feedWriter: null,
-  
+
   init: function SH_init() {
     this._feedWriter = new BrowserFeedWriter();
   },
 
   writeContent: function SH_writeContent() {
     this._feedWriter.writeContent();
   },
 
--- a/browser/components/feeds/test/test_bug436801.html
+++ b/browser/components/feeds/test/test_bug436801.html
@@ -81,29 +81,29 @@ function checkNode(node, schema) {
     var text = schema.shift();
     is(node.data, text, "Text should match");
     return;
   }
   // type == Node.ELEMENT_NODE
   var tag = schema.shift();
   is(node.localName, tag, "Element should have expected tag");
   while (schema.length) {
-    var val = schema.shift();
+    let val = schema.shift();
     if (Array.isArray(val))
       var childSchema = val;
     else
       var attrSchema = val;
   }
   if (attrSchema) {
     var nsTable = {
       xml: "http://www.w3.org/XML/1998/namespace",
     };
     for (var name in attrSchema) {
       var [ns, nsName] = name.split(":");
-      var val = nsName ? node.getAttributeNS(nsTable[ns], nsName) :
+      let val = nsName ? node.getAttributeNS(nsTable[ns], nsName) :
                 node.getAttribute(name);
       is(val, attrSchema[name], "Attribute " + name + " should match");
     }
   }
   if (childSchema) {
     var numChildren = node.childNodes.length;
     is(childSchema.length, numChildren,
        "Element should have expected number of children");
--- a/browser/components/feeds/test/test_registerHandler.html
+++ b/browser/components/feeds/test/test_registerHandler.html
@@ -23,17 +23,17 @@ https://bugzilla.mozilla.org/show_bug.cg
   function testRegisterHandler(aIsProtocol, aTxt, aUri, aTitle)
   {
     try {
       if (aIsProtocol)
         navigator.registerProtocolHandler(aTxt, aUri, aTitle);
       else
         navigator.registerContentHandler(aTxt, aUri, aTitle);
     }
-    catch(e) {
+    catch (e) {
       return false;
     }
 
     return true;
   }
 
   ok(navigator.registerProtocolHandler, "navigator.registerProtocolHandler should be defined");
   ok(navigator.registerContentHandler, "navigator.registerContentHandler should be defined");
@@ -56,20 +56,20 @@ https://bugzilla.mozilla.org/show_bug.cg
 
   // restriction to http(s) for the uri of the handler (bug 401343)
   // https should work (http already tested in the generic case)
   is(testRegisterHandler(true, "foo", "https://mochi.test:8888/%s", "Foo handler"), true, "registering a foo protocol handler with https scheme should work");
   is(testRegisterHandler(false, "application/rss+xml", "https://mochi.test:8888/%s", "Foo handler"), true, "registering a foo content handler with https scheme should work");
   // ftp should not work
   is(testRegisterHandler(true, "foo", "ftp://mochi.test:8888/%s", "Foo handler"), false, "registering a foo protocol handler with ftp scheme should not work");
   is(testRegisterHandler(false, "application/rss+xml", "ftp://mochi.test:8888/%s", "Foo handler"), false, "registering a foo content handler with ftp scheme should not work");
-  // chrome should not work 
+  // chrome should not work
   is(testRegisterHandler(true, "foo", "chrome://mochi.test:8888/%s", "Foo handler"), false, "registering a foo protocol handler with chrome scheme should not work");
   is(testRegisterHandler(false, "application/rss+xml", "chrome://mochi.test:8888/%s", "Foo handler"), false, "registering a foo content handler with chrome scheme should not work");
-  // foo should not work 
+  // foo should not work
   is(testRegisterHandler(true, "foo", "foo://mochi.test:8888/%s", "Foo handler"), false, "registering a foo protocol handler with foo scheme should not work");
   is(testRegisterHandler(false, "application/rss+xml", "foo://mochi.test:8888/%s", "Foo handler"), false, "registering a foo content handler with foo scheme should not work");
 
   // for security reasons, protocol handlers should never be registered for some schemes (chrome, vbscript, ...) (bug 402788)
   is(testRegisterHandler(true, "chrome", "http://mochi.test:8888/%s", "chrome handler"), false, "registering a chrome protocol handler should not work");
   is(testRegisterHandler(true, "vbscript", "http://mochi.test:8888/%s", "vbscript handler"), false, "registering a vbscript protocol handler should not work");
   is(testRegisterHandler(true, "javascript", "http://mochi.test:8888/%s", "javascript handler"), false, "registering a javascript protocol handler should not work");
   is(testRegisterHandler(true, "moz-icon", "http://mochi.test:8888/%s", "moz-icon handler"), false, "registering a moz-icon protocol handler should not work");
--- a/browser/components/translation/BingTranslator.jsm
+++ b/browser/components/translation/BingTranslator.jsm
@@ -281,17 +281,17 @@ function BingRequest(translationData, so
   this.characterCount = 0;
 }
 
 BingRequest.prototype = {
   /**
    * Initiates the request
    */
   fireRequest: function() {
-    return Task.spawn(function *(){
+    return Task.spawn(function *() {
       // Prepare authentication.
       let token = yield BingTokenManager.getToken();
       let auth = "Bearer " + token;
 
       // Prepare URL.
       let url = getUrlParam("https://api.microsofttranslator.com/v2/Http.svc/TranslateArray",
                             "browser.translation.bing.translateArrayURL");
 
--- a/browser/components/translation/TranslationDocument.jsm
+++ b/browser/components/translation/TranslationDocument.jsm
@@ -161,29 +161,27 @@ this.TranslationDocument.prototype = {
         // it's a translation node: it has useful content for translation.
         // In this case, we need to stringify this node.
         // However, if this item is a root, we should skip it here in this
         // object's child list (and just add a placeholder for it), because
         // it will be stringfied separately for being a root.
         item.original.push(objInMap);
         str += this.generateTextForItem(objInMap);
         wasLastItemPlaceholder = false;
-      } else {
+      } else if (!wasLastItemPlaceholder) {
         // Otherwise, if this node doesn't contain any useful content,
         // or if it is a root itself, we can replace it with a placeholder node.
         // We can't simply eliminate this node from our string representation
         // because that could change the HTML structure (e.g., it would
         // probably merge two separate text nodes).
         // It's not necessary to add more than one placeholder in sequence;
         // we can optimize them away.
-        if (!wasLastItemPlaceholder) {
-          item.original.push(TranslationItem_NodePlaceholder);
-          str += '<br>';
-          wasLastItemPlaceholder = true;
-        }
+        item.original.push(TranslationItem_NodePlaceholder);
+        str += '<br>';
+        wasLastItemPlaceholder = true;
       }
     }
 
     return generateTranslationHtmlForItem(item, str);
   },
 
   /**
    * Changes the document to display its translated
@@ -272,19 +270,25 @@ function TranslationItem(node, id, isRoo
   this.children = [];
 }
 
 TranslationItem.prototype = {
   isRoot: false,
   isSimpleRoot: false,
 
   toString: function() {
-    let rootType = this.isRoot
-                   ? (this.isSimpleRoot ? ' (simple root)' : ' (non simple root)')
-                   : '';
+    let rootType = "";
+    if (this.isRoot) {
+      if (this.isSimpleRoot) {
+        rootType = " (simple root)";
+      }
+      else {
+        rootType = " (non simple root)";
+      }
+    }
     return "[object TranslationItem: <" + this.nodeRef.localName + ">"
            + rootType + "]";
   },
 
   /**
    * This function will parse the result of the translation of one translation
    * item. If this item was a simple root, all we sent was a plain-text version
    * of it, so the result is also straightforward text.
--- a/browser/components/translation/YandexTranslator.jsm
+++ b/browser/components/translation/YandexTranslator.jsm
@@ -288,17 +288,17 @@ function YandexRequest(translationData, 
   this.characterCount = 0;
 }
 
 YandexRequest.prototype = {
   /**
    * Initiates the request
    */
   fireRequest: function() {
-    return Task.spawn(function *(){
+    return Task.spawn(function *() {
       // Prepare URL.
       let url = getUrlParam("https://translate.yandex.net/api/v1.5/tr.json/translate",
                             "browser.translation.yandex.translateURLOverride");
 
       // Prepare the request body.
       let apiKey = getUrlParam("%YANDEX_API_KEY%", "browser.translation.yandex.apiKeyOverride");
       let params = [
         ["key", apiKey],
--- a/browser/components/translation/cld2/post.js
+++ b/browser/components/translation/cld2/post.js
@@ -120,17 +120,17 @@ var Encodings = {
 // Accept forms both with and without underscores/hypens.
 for (let code of Object.keys(Encodings)) {
   if (code['includes']("_"))
     Encodings[code.replace(/_/g, "")] = Encodings[code];
 }
 
 addOnPreMain(function() {
 
-  onmessage = function(aMsg){
+  onmessage = function(aMsg) {
     let data = aMsg['data'];
 
     let langInfo;
     if (data['tld'] == undefined && data['encoding'] == undefined && data['language'] == undefined) {
       langInfo = LanguageInfo.detectLanguage(data['text'], !data['isHTML']);
     } else {
       // Do our best to find the given encoding in the encodings table.
       // Otherwise, just fall back to unknown.
--- a/browser/components/translation/test/browser_translation_bing.js
+++ b/browser/components/translation/test/browser_translation_bing.js
@@ -98,17 +98,17 @@ add_task(function* test_handling_out_of_
 });
 
 /**
  * A helper function for constructing a URL to a page stored in the
  * local fixture folder.
  *
  * @param filename  Name of a fixture file.
  */
-function constructFixtureURL(filename){
+function constructFixtureURL(filename) {
   // Deduce the Mochitest server address in use from a pref that was pre-processed.
   let server = Services.prefs.getCharPref("browser.translation.bing.authURL")
                              .replace("http://", "");
   server = server.substr(0, server.indexOf("/"));
   let url = "http://" + server +
     "/browser/browser/components/translation/test/fixtures/" + filename;
   return url;
 }
--- a/browser/components/translation/test/browser_translation_telemetry.js
+++ b/browser/components/translation/test/browser_translation_telemetry.js
@@ -19,17 +19,17 @@ var MetricsChecker = {
     AUTO_REJECTED         : Services.telemetry.getHistogramById("AUTO_REJECTED_TRANSLATION_OFFERS"),
     SHOW_ORIGINAL         : Services.telemetry.getHistogramById("REQUESTS_OF_ORIGINAL_CONTENT"),
     TARGET_CHANGES        : Services.telemetry.getHistogramById("CHANGES_OF_TARGET_LANGUAGE"),
     DETECTION_CHANGES     : Services.telemetry.getHistogramById("CHANGES_OF_DETECTED_LANGUAGE"),
     SHOW_UI               : Services.telemetry.getHistogramById("SHOULD_TRANSLATION_UI_APPEAR"),
     DETECT_LANG           : Services.telemetry.getHistogramById("SHOULD_AUTO_DETECT_LANGUAGE"),
   },
 
-  reset: function(){
+  reset: function() {
     for (let i of Object.keys(this.HISTOGRAMS)) {
       this.HISTOGRAMS[i].clear();
     }
     this.updateMetrics();
   },
 
   updateMetrics: function () {
     this._metrics = {
@@ -60,17 +60,17 @@ var MetricsChecker = {
         this._metrics.pageCountByLang[key] = pages[key] ? pages[key].sum : 0;
       }
     }
   },
 
   /**
    * A recurrent loop for making assertions about collected metrics.
    */
-  _assertionLoop: function (prevMetrics, metrics, additions){
+  _assertionLoop: function (prevMetrics, metrics, additions) {
     for (let metric of Object.keys(additions)) {
       let addition = additions[metric];
       // Allows nesting metrics. Useful for keyed histograms.
       if (typeof addition === 'object') {
         this._assertionLoop(prevMetrics[metric], metrics[metric], addition);
         continue;
       }
       Assert.equal(prevMetrics[metric] + addition, metrics[metric]);
@@ -112,19 +112,19 @@ var acceptTranslationOffer = Task.async(
   yield waitForMessage(browser, "Translation:Finished");
 });
 
 var translate = Task.async(function*(text, from, closeTab = true) {
   let tab = yield offerTranslationFor(text, from);
   yield acceptTranslationOffer(tab);
   if (closeTab) {
     gBrowser.removeTab(tab);
-  } else {
-    return tab;
+    return null;
   }
+  return tab;
 });
 
 function waitForMessage({messageManager}, name) {
   return new Promise(resolve => {
     messageManager.addMessageListener(name, function onMessage() {
       messageManager.removeMessageListener(name, onMessage);
       resolve();
     });
--- a/browser/components/translation/test/browser_translation_yandex.js
+++ b/browser/components/translation/test/browser_translation_yandex.js
@@ -86,17 +86,17 @@ add_task(function* test_preference_attri
 });
 
 /**
  * A helper function for constructing a URL to a page stored in the
  * local fixture folder.
  *
  * @param filename  Name of a fixture file.
  */
-function constructFixtureURL(filename){
+function constructFixtureURL(filename) {
   // Deduce the Mochitest server address in use from a pref that was pre-processed.
   let server = Services.prefs.getCharPref("browser.translation.yandex.translateURLOverride")
                              .replace("http://", "");
   server = server.substr(0, server.indexOf("/"));
   let url = "http://" + server +
     "/browser/browser/components/translation/test/fixtures/" + filename;
   return url;
 }
--- a/browser/locales/Makefile.in
+++ b/browser/locales/Makefile.in
@@ -56,17 +56,17 @@ UNINSTALLER_PACKAGE_HOOK = $(RM) -r $(ST
 
 STUB_HOOK = $(NSINSTALL) -D '$(ABS_DIST)/$(PKG_INST_PATH)'; \
     $(RM) '$(ABS_DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe'; \
     cp ../installer/windows/l10ngen/stub.exe '$(ABS_DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe'; \
     chmod 0755 '$(ABS_DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe'; \
     $(NULL)
 endif
 
-SEARCHPLUGINS_FILENAMES := $(shell $(PYTHON) $(srcdir)/searchplugins.py $(srcdir)/search/list.json $(AB_CD))
+SEARCHPLUGINS_FILENAMES := $(shell $(call py_action,output_searchplugins_list,$(srcdir)/search/list.json $(AB_CD)))
 SEARCHPLUGINS_PATH := .deps/generated_$(AB_CD)
 SEARCHPLUGINS_TARGET := libs searchplugins
 SEARCHPLUGINS := $(foreach plugin,$(addsuffix .xml,$(SEARCHPLUGINS_FILENAMES)),$(or $(wildcard $(call EN_US_OR_L10N_FILE,searchplugins/$(plugin))),$(warning Missing searchplugin: $(plugin))))
 # Some locale-specific search plugins may have preprocessor directives, but the
 # default en-US ones do not.
 SEARCHPLUGINS_FLAGS := --silence-missing-directive-warnings
 PP_TARGETS += SEARCHPLUGINS
 
@@ -79,17 +79,17 @@ libs:: searchplugins
 # be included in langpack xpis.
 DIST_SUBDIRS = $(DIST_SUBDIR)
 
 include $(topsrcdir)/config/rules.mk
 
 include $(topsrcdir)/toolkit/locales/l10n.mk
 
 $(list-json): $(call mkdir_deps,$(SEARCHPLUGINS_PATH)) $(if $(IS_LANGUAGE_REPACK),FORCE)
-	$(shell $(PYTHON) $(srcdir)/searchjson.py $(srcdir)/search/list.json $(AB_CD) $(list-json))
+	$(call py_action,generate_searchjson,$(srcdir)/search/list.json $(AB_CD) $(list-json))
 searchplugins:: $(list-json)
 
 $(STAGEDIST): $(DIST)/branding
 
 $(DIST)/branding:
 	$(NSINSTALL) -D $@
 
 DEFINES += -DBOOKMARKS_INCLUDE_DIR=$(dir $(call MERGE_FILE,profile/bookmarks.inc))
--- a/devtools/client/animationinspector/components/animation-time-block.js
+++ b/devtools/client/animationinspector/components/animation-time-block.js
@@ -7,16 +7,33 @@
 "use strict";
 
 const EventEmitter = require("devtools/shared/event-emitter");
 const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/locale/animationinspector.properties");
 
+// In the createPathSegments function, an animation duration is divided by
+// DURATION_RESOLUTION in order to draw the way the animation progresses.
+// But depending on the timing-function, we may be not able to make the graph
+// smoothly progress if this resolution is not high enough.
+// So, if the difference of animation progress between 2 divisions is more than
+// MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
+// by DURATION_RESOLUTION.
+// DURATION_RESOLUTION shoud be integer and more than 2.
+const DURATION_RESOLUTION = 4;
+// MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
+const MIN_PROGRESS_THRESHOLD = 0.1;
+// Show max 10 iterations for infinite animations
+// to give users a clue that the animation does repeat.
+const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
+// SVG namespace
+const SVG_NS = "http://www.w3.org/2000/svg";
+
 /**
  * UI component responsible for displaying a single animation timeline, which
  * basically looks like a rectangle that shows the delay and iterations.
  */
 function AnimationTimeBlock() {
   EventEmitter.decorate(this);
   this.onClick = this.onClick.bind(this);
 }
@@ -46,85 +63,200 @@ AnimationTimeBlock.prototype = {
     this.unrender();
 
     this.animation = animation;
     let {state} = this.animation;
 
     // Create a container element to hold the delay and iterations.
     // It is positioned according to its delay (divided by the playbackrate),
     // and its width is according to its duration (divided by the playbackrate).
-    let {x, iterationW, delayX, delayW, negativeDelayW, endDelayX, endDelayW} =
+    const {x, delayX, delayW, endDelayX, endDelayW} =
       TimeScale.getAnimationDimensions(animation);
 
-    // background properties for .iterations element
-    let backgroundIterations = TimeScale.getIterationsBackgroundData(animation);
-
-    createNode({
+    // Animation summary graph element.
+    const summaryEl = createNode({
       parent: this.containerEl,
+      namespace: "http://www.w3.org/2000/svg",
+      nodeType: "svg",
       attributes: {
-        "class": "iterations" + (state.iterationCount ? "" : " infinite"),
-        // Individual iterations are represented by setting the size of the
-        // repeating linear-gradient.
-        // The background-size, background-position, background-repeat represent
-        // iterationCount and iterationStart.
-        "style": `left:${x}%;
-                  width:${iterationW}%;
-                  background-size:${backgroundIterations.size}% 100%;
-                  background-position:${backgroundIterations.position}% 0;
-                  background-repeat:${backgroundIterations.repeat};`
+        "class": "summary",
+        "preserveAspectRatio": "none",
+        "style": `left: ${ x - (state.delay > 0 ? delayW : 0) }%`
       }
     });
 
-    // The animation name is displayed over the iterations.
-    // Note that in case of negative delay, it is pushed towards the right so
-    // the delay element does not overlap.
+    // Total displayed duration
+    const totalDisplayedDuration = state.playbackRate * TimeScale.getDuration();
+
+    // Calculate stroke height in viewBox to display stroke of path.
+    const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
+
+    // Set viewBox
+    summaryEl.setAttribute("viewBox",
+                           `${ state.delay < 0 ? state.delay : 0 }
+                            -${ 1 + strokeHeightForViewBox }
+                            ${ totalDisplayedDuration }
+                            ${ 1 + strokeHeightForViewBox * 2 }`);
+
+    // Get a helper function that returns the path segment of timing-function.
+    const segmentHelper = getSegmentHelper(state, this.win);
+
+    // Minimum segment duration is the duration of one pixel.
+    const minSegmentDuration =
+      totalDisplayedDuration / this.containerEl.clientWidth;
+    // Minimum progress threshold.
+    let minProgressThreshold = MIN_PROGRESS_THRESHOLD;
+    // If the easing is step function,
+    // minProgressThreshold should be changed by the steps.
+    const stepFunction = state.easing.match(/steps\((\d+)/);
+    if (stepFunction) {
+      minProgressThreshold = 1 / (parseInt(stepFunction[1], 10) + 1);
+    }
+
+    // Starting time of main iteration.
+    let mainIterationStartTime = 0;
+    let iterationStart = state.iterationStart;
+    let iterationCount = state.iterationCount ? state.iterationCount : Infinity;
+
+    // Append delay.
+    if (state.delay > 0) {
+      renderDelay(summaryEl, state, segmentHelper);
+      mainIterationStartTime = state.delay;
+    } else {
+      const negativeDelayCount = -state.delay / state.duration;
+      // Move to forward the starting point for negative delay.
+      iterationStart += negativeDelayCount;
+      // Consume iteration count by negative delay.
+      if (iterationCount !== Infinity) {
+        iterationCount -= negativeDelayCount;
+      }
+    }
+
+    // Append 1st section of iterations,
+    // This section is only useful in cases where iterationStart has decimals.
+    // e.g.
+    // if { iterationStart: 0.25, iterations: 3 }, firstSectionCount is 0.75.
+    const firstSectionCount =
+      iterationStart % 1 === 0
+      ? 0 : Math.min(iterationCount, 1) - iterationStart % 1;
+    if (firstSectionCount) {
+      renderFirstIteration(summaryEl, state, mainIterationStartTime,
+                           firstSectionCount, minSegmentDuration,
+                           minProgressThreshold, segmentHelper);
+    }
+
+    if (iterationCount === Infinity) {
+      // If the animation repeats infinitely,
+      // we fill the remaining area with iteration paths.
+      renderInfinity(summaryEl, state, mainIterationStartTime,
+                     firstSectionCount, totalDisplayedDuration,
+                     minSegmentDuration, minProgressThreshold, segmentHelper);
+    } else {
+      // Otherwise, we show remaining iterations, endDelay and fill.
+
+      // Append forwards fill-mode.
+      if (state.fill === "both" || state.fill === "forwards") {
+        renderForwardsFill(summaryEl, state, mainIterationStartTime,
+                           iterationCount, totalDisplayedDuration,
+                           segmentHelper);
+      }
+
+      // Append middle section of iterations.
+      // e.g.
+      // if { iterationStart: 0.25, iterations: 3 }, middleSectionCount is 2.
+      const middleSectionCount =
+        Math.floor(iterationCount - firstSectionCount);
+      renderMiddleIterations(summaryEl, state, mainIterationStartTime,
+                             firstSectionCount, middleSectionCount,
+                             minSegmentDuration, minProgressThreshold,
+                             segmentHelper);
+
+      // Append last section of iterations, if there is remaining iteration.
+      // e.g.
+      // if { iterationStart: 0.25, iterations: 3 }, lastSectionCount is 0.25.
+      const lastSectionCount =
+        iterationCount - middleSectionCount - firstSectionCount;
+      if (lastSectionCount) {
+        renderLastIteration(summaryEl, state, mainIterationStartTime,
+                            firstSectionCount, middleSectionCount,
+                            lastSectionCount, minSegmentDuration,
+                            minProgressThreshold, segmentHelper);
+      }
+
+      // Append endDelay.
+      if (state.endDelay > 0) {
+        renderEndDelay(summaryEl, state,
+                       mainIterationStartTime, iterationCount, segmentHelper);
+      }
+    }
+
+    // Append negative delay (which overlap the animation).
+    if (state.delay < 0) {
+      segmentHelper.animation.effect.timing.fill = "both";
+      segmentHelper.asOriginalBehavior = false;
+      renderNegativeDelayHiddenProgress(summaryEl, state, minSegmentDuration,
+                                        minProgressThreshold, segmentHelper);
+    }
+    // Append negative endDelay (which overlap the animation).
+    if (state.iterationCount && state.endDelay < 0) {
+      if (segmentHelper.asOriginalBehavior) {
+        segmentHelper.animation.effect.timing.fill = "both";
+        segmentHelper.asOriginalBehavior = false;
+      }
+      renderNegativeEndDelayHiddenProgress(summaryEl, state,
+                                           minSegmentDuration,
+                                           minProgressThreshold,
+                                           segmentHelper);
+    }
+
+    // The animation name is displayed over the animation.
     createNode({
       parent: createNode({
         parent: this.containerEl,
         attributes: {
           "class": "name",
-          "title": this.getTooltipText(state),
-          // Place the name at the same position as the iterations, but make
-          // space for the negative delay if any.
-          "style": `left:${x + negativeDelayW}%;
-                    width:${iterationW - negativeDelayW}%;`
+          "title": this.getTooltipText(state)
         },
       }),
       textContent: state.name
     });
 
     // Delay.
     if (state.delay) {
       // Negative delays need to start at 0.
       createNode({
         parent: this.containerEl,
         attributes: {
-          "class": "delay" + (state.delay < 0 ? " negative" : ""),
-          "style": `left:${delayX}%;
-                    width:${delayW}%;`
+          "class": "delay"
+                   + (state.delay < 0 ? " negative" : " positive")
+                   + (state.fill === "both" ||
+                      state.fill === "backwards" ? " fill" : ""),
+          "style": `left:${ delayX }%; width:${ delayW }%;`
         }
       });
     }
 
     // endDelay
-    if (state.endDelay) {
+    if (state.iterationCount && state.endDelay) {
       createNode({
         parent: this.containerEl,
         attributes: {
-          "class": "end-delay" + (state.endDelay < 0 ? " negative" : ""),
-          "style": `left:${endDelayX}%;
-                    width:${endDelayW}%;`
+          "class": "end-delay"
+                   + (state.endDelay < 0 ? " negative" : " positive")
+                   + (state.fill === "both" ||
+                      state.fill === "forwards" ? " fill" : ""),
+          "style": `left:${ endDelayX }%; width:${ endDelayW }%;`
         }
       });
     }
   },
 
   getTooltipText: function (state) {
     let getTime = time => L10N.getFormatStr("player.timeLabel",
-                            L10N.numberWithDecimals(time / 1000, 2));
+                                            L10N.numberWithDecimals(time / 1000, 2));
 
     let text = "";
 
     // Adding the name.
     text += getFormattedAnimationTitle({state});
     text += "\n";
 
     // Adding the delay.
@@ -158,43 +290,68 @@ AnimationTimeBlock.prototype = {
     if (state.iterationStart !== 0) {
       let iterationStartTime = state.iterationStart * state.duration / 1000;
       text += L10N.getFormatStr("player.animationIterationStartLabel",
                                 state.iterationStart,
                                 L10N.numberWithDecimals(iterationStartTime, 2));
       text += "\n";
     }
 
+    // Adding the easing.
+    if (state.easing) {
+      text += L10N.getStr("player.animationEasingLabel") + " ";
+      text += state.easing;
+      text += "\n";
+    }
+
+    // Adding the fill mode.
+    if (state.fill) {
+      text += L10N.getStr("player.animationFillLabel") + " ";
+      text += state.fill;
+      text += "\n";
+    }
+
+    // Adding the direction mode.
+    if (state.direction) {
+      text += L10N.getStr("player.animationDirectionLabel") + " ";
+      text += state.direction;
+      text += "\n";
+    }
+
     // Adding the playback rate if it's different than 1.
     if (state.playbackRate !== 1) {
       text += L10N.getStr("player.animationRateLabel") + " ";
       text += state.playbackRate;
       text += "\n";
     }
 
     // Adding a note that the animation is running on the compositor thread if
     // needed.
     if (state.propertyState) {
       if (state.propertyState
-          .every(propState => propState.runningOnCompositor)) {
+               .every(propState => propState.runningOnCompositor)) {
         text += L10N.getStr("player.allPropertiesOnCompositorTooltip");
       } else if (state.propertyState
-                 .some(propState => propState.runningOnCompositor)) {
+                      .some(propState => propState.runningOnCompositor)) {
         text += L10N.getStr("player.somePropertiesOnCompositorTooltip");
       }
     } else if (state.isRunningOnCompositor) {
       text += L10N.getStr("player.runningOnCompositorTooltip");
     }
 
     return text;
   },
 
   onClick: function (e) {
     e.stopPropagation();
     this.emit("selected", this.animation);
+  },
+
+  get win() {
+    return this.containerEl.ownerDocument.defaultView;
   }
 };
 
 /**
  * Get a formatted title for this animation. This will be either:
  * "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
  * "some-name : Script Animation", or "Script Animation", depending
  * if the server provides the type, what type it is and if the animation
@@ -211,8 +368,351 @@ function getFormattedAnimationTitle({sta
 
   // Script-generated animations may not have a name.
   if (state.type === "scriptanimation" && !state.name) {
     return L10N.getStr("timeline.scriptanimation.unnamedLabel");
   }
 
   return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
 }
+
+/**
+ * Render delay section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderDelay(parentEl, state, segmentHelper) {
+  const startSegment = segmentHelper.getSegment(0);
+  const endSegment = { x: state.delay, y: startSegment.y };
+  appendPathElement(parentEl, [startSegment, endSegment], "delay-path");
+}
+
+/**
+ * Render first iteration section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderFirstIteration(parentEl, state, mainIterationStartTime,
+                              firstSectionCount, minSegmentDuration,
+                              minProgressThreshold, segmentHelper) {
+  const startTime = mainIterationStartTime;
+  const endTime = startTime + firstSectionCount * state.duration;
+  const segments =
+    createPathSegments(startTime, endTime, minSegmentDuration,
+                       minProgressThreshold, segmentHelper);
+  appendPathElement(parentEl, segments, "iteration-path");
+}
+
+/**
+ * Render middle iterations section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} middleSectionCount - Iteration count of middle section.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderMiddleIterations(parentEl, state, mainIterationStartTime,
+                                firstSectionCount, middleSectionCount,
+                                minSegmentDuration, minProgressThreshold,
+                                segmentHelper) {
+  const offset = mainIterationStartTime + firstSectionCount * state.duration;
+  for (let i = 0; i < middleSectionCount; i++) {
+    // Get the path segments of each iteration.
+    const startTime = offset + i * state.duration;
+    const endTime = startTime + state.duration;
+    const segments =
+      createPathSegments(startTime, endTime, minSegmentDuration,
+                         minProgressThreshold, segmentHelper);
+    appendPathElement(parentEl, segments, "iteration-path");
+  }
+}
+
+/**
+ * Render last iteration section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} middleSectionCount - Iteration count of middle section.
+ * @param {Number} lastSectionCount - Iteration count of last section.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderLastIteration(parentEl, state, mainIterationStartTime,
+                             firstSectionCount, middleSectionCount,
+                             lastSectionCount, minSegmentDuration,
+                             minProgressThreshold, segmentHelper) {
+  const startTime = mainIterationStartTime +
+                      (firstSectionCount + middleSectionCount) * state.duration;
+  const endTime = startTime + lastSectionCount * state.duration;
+  const segments =
+    createPathSegments(startTime, endTime, minSegmentDuration,
+                       minProgressThreshold, segmentHelper);
+  appendPathElement(parentEl, segments, "iteration-path");
+}
+
+/**
+ * Render Infinity iterations.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} firstSectionCount - Iteration count of first section.
+ * @param {Number} totalDuration - Displayed max duration.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderInfinity(parentEl, state, mainIterationStartTime,
+                        firstSectionCount, totalDuration, minSegmentDuration,
+                        minProgressThreshold, segmentHelper) {
+  // Calculate the number of iterations to display,
+  // with a maximum of MAX_INFINITE_ANIMATIONS_ITERATIONS
+  let uncappedInfinityIterationCount =
+    (totalDuration - firstSectionCount * state.duration) / state.duration;
+  // If there is a small floating point error resulting in, e.g. 1.0000001
+  // ceil will give us 2 so round first.
+  uncappedInfinityIterationCount =
+    parseFloat(uncappedInfinityIterationCount.toPrecision(6));
+  const infinityIterationCount =
+    Math.min(MAX_INFINITE_ANIMATIONS_ITERATIONS,
+             Math.ceil(uncappedInfinityIterationCount));
+
+  // Append first full iteration path.
+  const firstStartTime =
+    mainIterationStartTime + firstSectionCount * state.duration;
+  const firstEndTime = firstStartTime + state.duration;
+  const firstSegments =
+    createPathSegments(firstStartTime, firstEndTime, minSegmentDuration,
+                       minProgressThreshold, segmentHelper);
+  appendPathElement(parentEl, firstSegments, "iteration-path infinity");
+
+  // Append other iterations. We can copy first segments.
+  const isAlternate = state.direction.match(/alternate/);
+  for (let i = 1; i < infinityIterationCount; i++) {
+    const startTime = firstStartTime + i * state.duration;
+    let segments;
+    if (isAlternate && i % 2) {
+      // Copy as reverse.
+      segments = firstSegments.map(segment => {
+        return { x: firstEndTime - segment.x + startTime, y: segment.y };
+      });
+    } else {
+      // Copy as is.
+      segments = firstSegments.map(segment => {
+        return { x: segment.x - firstStartTime + startTime, y: segment.y };
+      });
+    }
+    appendPathElement(parentEl, segments, "iteration-path infinity copied");
+  }
+}
+
+/**
+ * Render endDelay section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} iterationCount - Whole iteration count.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderEndDelay(parentEl, state,
+                        mainIterationStartTime, iterationCount, segmentHelper) {
+  const startTime = mainIterationStartTime + iterationCount * state.duration;
+  const startSegment = segmentHelper.getSegment(startTime);
+  const endSegment = { x: startTime + state.endDelay, y: startSegment.y };
+  appendPathElement(parentEl, [startSegment, endSegment], "enddelay-path");
+}
+
+/**
+ * Render forwards fill section.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} mainIterationStartTime - Starting time of main iteration.
+ * @param {Number} iterationCount - Whole iteration count.
+ * @param {Number} totalDuration - Displayed max duration.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderForwardsFill(parentEl, state, mainIterationStartTime,
+                            iterationCount, totalDuration, segmentHelper) {
+  const startTime = mainIterationStartTime + iterationCount * state.duration +
+                      (state.endDelay > 0 ? state.endDelay : 0);
+  const startSegment = segmentHelper.getSegment(startTime);
+  const endSegment = { x: totalDuration, y: startSegment.y };
+  appendPathElement(parentEl, [startSegment, endSegment], "fill-forwards-path");
+}
+
+/**
+ * Render hidden progress of negative delay.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderNegativeDelayHiddenProgress(parentEl, state, minSegmentDuration,
+                                           minProgressThreshold,
+                                           segmentHelper) {
+  const startTime = state.delay;
+  const endTime = 0;
+  const segments =
+    createPathSegments(startTime, endTime, minSegmentDuration,
+                       minProgressThreshold, segmentHelper);
+  appendPathElement(parentEl, segments, "delay-path negative");
+}
+
+/**
+ * Render hidden progress of negative endDelay.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Object} state - State of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object returned by getSegmentHelper.
+ */
+function renderNegativeEndDelayHiddenProgress(parentEl, state,
+                                              minSegmentDuration,
+                                              minProgressThreshold,
+                                              segmentHelper) {
+  const endTime = state.delay + state.iterationCount * state.duration;
+  const startTime = endTime + state.endDelay;
+  const segments =
+    createPathSegments(startTime, endTime, minSegmentDuration,
+                       minProgressThreshold, segmentHelper);
+  appendPathElement(parentEl, segments, "enddelay-path negative");
+}
+
+/**
+ * Get a helper function which returns the segment coord from given time.
+ * @param {Object} state - animation state
+ * @param {Object} win - window object
+ * @return {Object} A segmentHelper object that has the following properties:
+ * - animation: The script animation used to get the progress
+ * - endTime: The end time of the animation
+ * - asOriginalBehavior: The spec is that the progress of animation is changed
+ *                       if the time of setCurrentTime is during the endDelay.
+ *                       Likewise, in case the time is less than 0.
+ *                       If this flag is true, we prevent the time
+ *                       to make the same animation behavior as the original.
+ * - getSegment: Helper function that, given a time,
+ *               will calculate the progress through the dummy animation.
+ */
+function getSegmentHelper(state, win) {
+  // Create a dummy Animation timing data as the
+  // state object we're being passed in.
+  const timing = Object.assign({}, state, {
+    iterations: state.iterationCount ? state.iterationCount : Infinity
+  });
+
+  // Create a dummy Animation with the given timing.
+  const dummyAnimation =
+    new win.Animation(new win.KeyframeEffect(null, null, timing), null);
+
+  // Returns segment helper object.
+  return {
+    animation: dummyAnimation,
+    endTime: dummyAnimation.effect.getComputedTiming().endTime,
+    asOriginalBehavior: true,
+    getSegment: function (time) {
+      if (this.asOriginalBehavior) {
+        // If the given time is less than 0, returned progress is 0.
+        if (time < 0) {
+          return { x: time, y: 0 };
+        }
+        // Avoid to apply over endTime.
+        this.animation.currentTime = time < this.endTime ? time : this.endTime;
+      } else {
+        this.animation.currentTime = time;
+      }
+      const progress = this.animation.effect.getComputedTiming().progress;
+      return { x: time, y: Math.max(progress, 0) };
+    }
+  };
+}
+
+/**
+ * Create the path segments from given parameters.
+ * @param {Number} startTime - Starting time of animation.
+ * @param {Number} endTime - Ending time of animation.
+ * @param {Number} minSegmentDuration - Minimum segment duration.
+ * @param {Number} minProgressThreshold - Minimum progress threshold.
+ * @param {Object} segmentHelper - The object of getSegmentHelper.
+ * @return {Array} path segments -
+ *                 [{x: {Number} time, y: {Number} progress}, ...]
+ */
+function createPathSegments(startTime, endTime, minSegmentDuration,
+                            minProgressThreshold, segmentHelper) {
+  // If the duration is too short, early return.
+  if (endTime - startTime < minSegmentDuration) {
+    return [segmentHelper.getSegment(startTime),
+            segmentHelper.getSegment(endTime)];
+  }
+
+  // Otherwise, start creating segments.
+  let pathSegments = [];
+
+  // Append the segment for the startTime position.
+  const startTimeSegment = segmentHelper.getSegment(startTime);
+  pathSegments.push(startTimeSegment);
+  let previousSegment = startTimeSegment;
+
+  // Split the duration in equal intervals, and iterate over them.
+  // See the definition of DURATION_RESOLUTION for more information about this.
+  const interval = (endTime - startTime) / DURATION_RESOLUTION;
+  for (let index = 1; index <= DURATION_RESOLUTION; index++) {
+    // Create a segment for this interval.
+    const currentSegment =
+      segmentHelper.getSegment(startTime + index * interval);
+
+    // If the distance between the Y coordinate (the animation's progress) of
+    // the previous segment and the Y coordinate of the current segment is too
+    // large, then recurse with a smaller duration to get more details
+    // in the graph.
+    if (Math.abs(currentSegment.y - previousSegment.y) > minProgressThreshold) {
+      // Divide the current interval (excluding start and end bounds
+      // by adding/subtracting 1ms).
+      pathSegments = pathSegments.concat(
+        createPathSegments(previousSegment.x + 1, currentSegment.x - 1,
+                           minSegmentDuration, minProgressThreshold,
+                           segmentHelper));
+    }
+
+    pathSegments.push(currentSegment);
+    previousSegment = currentSegment;
+  }
+
+  return pathSegments;
+}
+
+/**
+ * Append path element.
+ * @param {Element} parentEl - Parent element of this appended path element.
+ * @param {Array} pathSegments - Path segments. Please see createPathSegments.
+ * @param {String} cls - Class name.
+ * @return {Element} path element.
+ */
+function appendPathElement(parentEl, pathSegments, cls) {
+  // Create path string.
+  let path = `M${ pathSegments[0].x },0`;
+  pathSegments.forEach(pathSegment => {
+    path += ` L${ pathSegment.x },${ pathSegment.y }`;
+  });
+  path += ` L${ pathSegments[pathSegments.length - 1].x },0 Z`;
+  // Append and return the path element.
+  return createNode({
+    parent: parentEl,
+    namespace: SVG_NS,
+    nodeType: "path",
+    attributes: {
+      "d": path,
+      "class": cls,
+      "vector-effect": "non-scaling-stroke",
+      "transform": "scale(1, -1)"
+    }
+  });
+}
--- a/devtools/client/animationinspector/test/browser.ini
+++ b/devtools/client/animationinspector/test/browser.ini
@@ -7,16 +7,17 @@ support-files =
   doc_frame_script.js
   doc_keyframes.html
   doc_modify_playbackRate.html
   doc_negative_animation.html
   doc_pseudo_elements.html
   doc_script_animation.html
   doc_simple_animation.html
   doc_multiple_animation_types.html
+  doc_timing_combination_animation.html
   head.js
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/inspector/test/head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/test-actor-registry.js
   !/devtools/client/shared/test/test-actor.js
 
@@ -51,16 +52,17 @@ skip-if = os == "linux" && !debug # Bug 
 [browser_animation_timeline_pause_button_01.js]
 [browser_animation_timeline_pause_button_02.js]
 [browser_animation_timeline_pause_button_03.js]
 [browser_animation_timeline_rate_selector.js]
 [browser_animation_timeline_rewind_button.js]
 [browser_animation_timeline_scrubber_exists.js]
 [browser_animation_timeline_scrubber_movable.js]
 [browser_animation_timeline_scrubber_moves.js]
+[browser_animation_timeline_setCurrentTime.js]
 [browser_animation_timeline_shows_delay.js]
 [browser_animation_timeline_shows_endDelay.js]
 [browser_animation_timeline_shows_iterations.js]
 [browser_animation_timeline_shows_time_info.js]
 [browser_animation_timeline_takes_rate_into_account.js]
 [browser_animation_timeline_ui.js]
 [browser_animation_toggle_button_resets_on_navigate.js]
 [browser_animation_toggle_button_toggles_animations.js]
--- a/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_iterationStart.js
@@ -16,17 +16,17 @@ add_task(function* () {
   for (let i = 0; i < timeBlockComponents.length; i++) {
     info(`Expand time block ${i} so its keyframes are visible`);
     yield clickOnAnimation(panel, i);
 
     info(`Check the state of time block ${i}`);
     let {containerEl, animation: {state}} = timeBlockComponents[i];
 
     checkAnimationTooltip(containerEl, state);
-    checkIterationBackground(containerEl, state);
+    checkProgressAtStartingTime(containerEl, state);
 
     // Get the first set of keyframes (there's only one animated property
     // anyway), and the first frame element from there, we're only interested in
     // its offset.
     let keyframeComponent = detailsComponents[i].keyframeComponents[0];
     let frameEl = keyframeComponent.keyframesEl.querySelector(".frame");
     checkKeyframeOffset(containerEl, frameEl, state);
   }
@@ -43,37 +43,24 @@ function checkAnimationTooltip(el, {iter
   }).replace(".", "\\.");
   let iterationStartString = iterationStart.toString().replace(".", "\\.");
 
   let regex = new RegExp("Iteration start: " + iterationStartString +
                          " \\(" + iterationStartTimeString + "s\\)");
   ok(title.match(regex), "The tooltip shows the expected iteration start");
 }
 
-function checkIterationBackground(el, {iterationCount, iterationStart}) {
-  info("Check the background-image used to display iterations is offset " +
-       "correctly to represent the iterationStart");
-
-  let iterationsEl = el.querySelector(".iterations");
-  let start = getIterationStartFromBackground(iterationsEl, iterationCount);
-  is(start, iterationStart % 1,
-     "The right background-position for iteration start");
-}
-
-function getIterationStartFromBackground(el, iterationCount) {
-  if (iterationCount == 1) {
-    let size = parseFloat(/([.\d]+)%/.exec(el.style.backgroundSize)[1]);
-    return 1 - size / 100;
-  }
-
-  let size = parseFloat(/([.\d]+)%/.exec(el.style.backgroundSize)[1]);
-  let position = parseFloat(/([-\d]+)%/.exec(el.style.backgroundPosition)[1]);
-  let iterationStartW = -position / size * (100 - size);
-  let rounded = Math.round(iterationStartW * 100);
-  return rounded / 10000;
+function checkProgressAtStartingTime(el, { iterationStart }) {
+  info("Check the progress of starting time");
+  const pathEl = el.querySelector(".iteration-path");
+  const pathSegList = pathEl.pathSegList;
+  const pathSeg = pathSegList.getItem(1);
+  const progress = pathSeg.y;
+  is(progress, iterationStart % 1,
+     `The progress at starting point should be ${ iterationStart % 1 }`);
 }
 
 function checkKeyframeOffset(timeBlockEl, frameEl, {iterationStart}) {
   info("Check that the first keyframe is offset correctly");
 
   let start = getIterationStartFromLeft(frameEl);
   is(start, iterationStart % 1, "The frame offset for iteration start");
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_setCurrentTime.js
@@ -0,0 +1,88 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+// Animation.currentTime ignores neagtive delay and positive/negative endDelay
+// during fill-mode, even if they are set.
+// For example, when the animation timing is
+// { duration: 1000, iterations: 1, endDelay: -500, easing: linear },
+// the animation progress is 0.5 at 700ms because the progress stops as 0.5 at
+// 500ms in original animation. However, if you set as
+// animation.currentTime = 700 manually, the progress will be 0.7.
+// So we modify setCurrentTime method since
+// AnimationInspector should re-produce same as original animation.
+// In these tests,
+// we confirm the behavior of setCurrentTime by delay and endDelay.
+
+add_task(function* () {
+  yield addTab(URL_ROOT + "doc_timing_combination_animation.html");
+  const { panel, controller } = yield openAnimationInspector();
+
+  yield clickTimelinePlayPauseButton(panel);
+
+  const timelineComponent = panel.animationsTimelineComponent;
+  const timeBlockComponents = timelineComponent.timeBlocks;
+
+  // Test -5000ms.
+  let time = -5000;
+  yield controller.setCurrentTimeAll(time, true);
+  for (let i = 0; i < timeBlockComponents.length; i++) {
+    yield timeBlockComponents[i].animation.refreshState();
+    const state = yield timeBlockComponents[i].animation.state;
+    info(`Check the state at ${ time }ms with `
+         + `delay:${ state.delay } and endDelay:${ state.endDelay }`);
+    is(state.currentTime, 0,
+       `The currentTime should be 0 at setCurrentTime(${ time })`);
+  }
+
+  // Test 10000ms.
+  time = 10000;
+  yield controller.setCurrentTimeAll(time, true);
+  for (let i = 0; i < timeBlockComponents.length; i++) {
+    yield timeBlockComponents[i].animation.refreshState();
+    const state = yield timeBlockComponents[i].animation.state;
+    info(`Check the state at ${ time }ms with `
+         + `delay:${ state.delay } and endDelay:${ state.endDelay }`);
+    const expected = state.delay < 0 ? 0 : time;
+    is(state.currentTime, expected,
+       `The currentTime should be ${ expected } at setCurrentTime(${ time }).`
+       + ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
+  }
+
+  // Test 60000ms.
+  time = 60000;
+  yield controller.setCurrentTimeAll(time, true);
+  for (let i = 0; i < timeBlockComponents.length; i++) {
+    yield timeBlockComponents[i].animation.refreshState();
+    const state = yield timeBlockComponents[i].animation.state;
+    info(`Check the state at ${ time }ms with `
+         + `delay:${ state.delay } and endDelay:${ state.endDelay }`);
+    const expected = state.delay < 0 ? time + state.delay : time;
+    is(state.currentTime, expected,
+       `The currentTime should be ${ expected } at setCurrentTime(${ time }).`
+       + ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
+  }
+
+  // Test 150000ms.
+  time = 150000;
+  yield controller.setCurrentTimeAll(time, true);
+  for (let i = 0; i < timeBlockComponents.length; i++) {
+    yield timeBlockComponents[i].animation.refreshState();
+    const state = yield timeBlockComponents[i].animation.state;
+    info(`Check the state at ${ time }ms with `
+         + `delay:${ state.delay } and endDelay:${ state.endDelay }`);
+    const currentTime = state.delay < 0 ? time + state.delay : time;
+    const endTime =
+      state.delay + state.iterationCount * state.duration + state.endDelay;
+    const expected =
+      state.endDelay < 0 && state.fill === "both" && currentTime > endTime
+      ? endTime : currentTime;
+    is(state.currentTime, expected,
+       `The currentTime should be ${ expected } at setCurrentTime(${ time }).`
+       + ` delay: ${ state.delay } and endDelay: ${ state.endDelay }`);
+  }
+});
--- a/devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_delay.js
@@ -14,42 +14,83 @@ requestLongerTimeout(2);
 add_task(function* () {
   yield addTab(URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel} = yield openAnimationInspector();
 
   info("Selecting a delayed animated node");
   yield selectNodeAndWaitForAnimations(".delayed", inspector);
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
   checkDelayAndName(timelineEl, true);
+  let animationEl = timelineEl.querySelector(".animation");
+  let state = panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+  checkPath(animationEl, state);
 
   info("Selecting a no-delay animated node");
   yield selectNodeAndWaitForAnimations(".animated", inspector);
   checkDelayAndName(timelineEl, false);
+  animationEl = timelineEl.querySelector(".animation");
+  state = panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+  checkPath(animationEl, state);
 
   info("Selecting a negative-delay animated node");
   yield selectNodeAndWaitForAnimations(".negative-delay", inspector);
   checkDelayAndName(timelineEl, true);
+  animationEl = timelineEl.querySelector(".animation");
+  state = panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+  checkPath(animationEl, state);
 });
 
 function checkDelayAndName(timelineEl, hasDelay) {
   let delay = timelineEl.querySelector(".delay");
 
   is(!!delay, hasDelay, "The timeline " +
                         (hasDelay ? "contains" : "does not contain") +
                         " a delay element, as expected");
 
   if (hasDelay) {
-    let name = timelineEl.querySelector(".name");
     let targetNode = timelineEl.querySelector(".target");
 
     // Check that the delay element does not cause the timeline to overflow.
     let delayLeft = Math.round(delay.getBoundingClientRect().x);
     let sidebarWidth = Math.round(targetNode.getBoundingClientRect().width);
     ok(delayLeft >= sidebarWidth,
        "The delay element isn't displayed over the sidebar");
-
-    // Check that the delay is not displayed on top of the name.
-    let delayRight = Math.round(delay.getBoundingClientRect().right);
-    let nameLeft = Math.round(name.getBoundingClientRect().left);
-    ok(delayRight <= nameLeft,
-       "The delay element does not span over the name element");
   }
 }
+
+function checkPath(animationEl, state) {
+  // Check existance of delay path.
+  const delayPathEl = animationEl.querySelector(".delay-path");
+  if (!state.iterationCount && state.delay < 0) {
+    // Infinity
+    ok(!delayPathEl, "The delay path for Infinity should not exist");
+    return;
+  }
+  if (state.delay === 0) {
+    ok(!delayPathEl, "The delay path for zero delay should not exist");
+    return;
+  }
+  ok(delayPathEl, "The delay path should exist");
+
+  // Check delay path coordinates.
+  const pathSegList = delayPathEl.pathSegList;
+  const startingPathSeg = pathSegList.getItem(0);
+  const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
+  if (state.delay < 0) {
+    ok(delayPathEl.classList.contains("negative"),
+       "The delay path should have 'negative' class");
+    const startingX = state.delay;
+    const endingX = 0;
+    is(startingPathSeg.x, startingX,
+       `The x of starting point should be ${ startingX }`);
+    is(endingPathSeg.x, endingX,
+       `The x of ending point should be ${ endingX }`);
+  } else {
+    ok(!delayPathEl.classList.contains("negative"),
+       "The delay path should not have 'negative' class");
+    const startingX = 0;
+    const endingX = state.delay;
+    is(startingPathSeg.x, startingX,
+       `The x of starting point should be ${ startingX }`);
+    is(endingPathSeg.x, endingX,
+       `The x of ending point should be ${ endingX }`);
+  }
+}
--- a/devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_endDelay.js
@@ -15,18 +15,21 @@ add_task(function* () {
   yield addTab(URL_ROOT + "doc_end_delay.html");
   let {inspector, panel} = yield openAnimationInspector();
 
   let selectors = ["#target1", "#target2", "#target3", "#target4"];
   for (let i = 0; i < selectors.length; i++) {
     let selector = selectors[i];
     yield selectNode(selector, inspector);
     let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
-    let animationEl = timelineEl.querySelectorAll(".animation")[0];
+    let animationEl = timelineEl.querySelector(".animation");
     checkEndDelayAndName(animationEl);
+    const state =
+      panel.animationsTimelineComponent.timeBlocks[0].animation.state;
+    checkPath(animationEl, state);
   }
 });
 
 function checkEndDelayAndName(animationEl) {
   let endDelay = animationEl.querySelector(".end-delay");
   let name = animationEl.querySelector(".name");
   let targetNode = animationEl.querySelector(".target");
 
@@ -37,8 +40,39 @@ function checkEndDelayAndName(animationE
      "The endDelay element isn't displayed over the sidebar");
 
   // Check that the endDelay is not displayed on top of the name.
   let endDelayRight = Math.round(endDelay.getBoundingClientRect().right);
   let nameLeft = Math.round(name.getBoundingClientRect().left);
   ok(endDelayRight >= nameLeft,
      "The endDelay element does not span over the name element");
 }
+
+function checkPath(animationEl, state) {
+  // Check existance of enddelay path.
+  const endDelayPathEl = animationEl.querySelector(".enddelay-path");
+  ok(endDelayPathEl, "The endDelay path should exist");
+
+  // Check enddelay path coordinates.
+  const pathSegList = endDelayPathEl.pathSegList;
+  const startingPathSeg = pathSegList.getItem(0);
+  const endingPathSeg = pathSegList.getItem(pathSegList.numberOfItems - 2);
+  if (state.endDelay < 0) {
+    ok(endDelayPathEl.classList.contains("negative"),
+       "The endDelay path should have 'negative' class");
+    const endingX = state.delay + state.iterationCount * state.duration;
+    const startingX = endingX + state.endDelay;
+    is(startingPathSeg.x, startingX,
+       `The x of starting point should be ${ startingX }`);
+    is(endingPathSeg.x, endingX,
+       `The x of ending point should be ${ endingX }`);
+  } else {
+    ok(!endDelayPathEl.classList.contains("negative"),
+       "The endDelay path should not have 'negative' class");
+    const startingX =
+      state.delay + state.iterationCount * state.duration;
+    const endingX = startingX + state.endDelay;
+    is(startingPathSeg.x, startingX,
+       `The x of starting point should be ${ startingX }`);
+    is(endingPathSeg.x, endingX,
+       `The x of ending point should be ${ endingX }`);
+  }
+}
--- a/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_iterations.js
@@ -12,41 +12,36 @@ requestLongerTimeout(2);
 add_task(function* () {
   yield addTab(URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel} = yield openAnimationInspector();
 
   info("Selecting the test node");
   yield selectNodeAndWaitForAnimations(".delayed", inspector);
 
   info("Getting the animation element from the panel");
-  let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
+  const timelineComponent = panel.animationsTimelineComponent;
+  const timelineEl = timelineComponent.rootWrapperEl;
   let animation = timelineEl.querySelector(".time-block");
-  let iterations = animation.querySelector(".iterations");
-
-  // Iterations are rendered with a repeating linear-gradient, so we need to
-  // calculate how many iterations are represented by looking at the background
-  // size.
-  let iterationCount = getIterationCountFromBackground(iterations);
+  // Get iteration count from summary graph path.
+  let iterationCount = getIterationCount(animation);
 
   is(iterationCount, 10,
      "The animation timeline contains the right number of iterations");
-  ok(!iterations.classList.contains("infinite"),
-     "The iteration element doesn't have the infinite class");
+  ok(!animation.querySelector(".infinity"),
+     "The summary graph does not have any elements "
+     + " that have infinity class");
 
   info("Selecting another test node with an infinite animation");
   yield selectNodeAndWaitForAnimations(".animated", inspector);
 
   info("Getting the animation element from the panel again");
   animation = timelineEl.querySelector(".time-block");
-  iterations = animation.querySelector(".iterations");
-
-  iterationCount = getIterationCountFromBackground(iterations);
+  iterationCount = getIterationCount(animation);
 
   is(iterationCount, 1,
-     "The animation timeline contains just one iteration");
-  ok(iterations.classList.contains("infinite"),
-     "The iteration element has the infinite class");
+     "The animation timeline contains one iteration");
+  ok(animation.querySelector(".infinity"),
+     "The summary graph has an element that has infinity class");
 });
 
-function getIterationCountFromBackground(el) {
-  let backgroundSize = parseFloat(el.style.backgroundSize.split(" ")[0]);
-  return Math.round(100 / backgroundSize);
+function getIterationCount(timeblockEl) {
+  return timeblockEl.querySelectorAll(".iteration-path").length;
 }
--- a/devtools/client/animationinspector/test/browser_animation_timeline_shows_time_info.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_shows_time_info.js
@@ -30,12 +30,21 @@ add_task(function* () {
     if (controller.animationPlayers[i].state.endDelay) {
       ok(title.match(/End delay: [\d.-]+s/), "The tooltip shows the endDelay");
     }
     if (controller.animationPlayers[i].state.iterationCount !== 1) {
       ok(title.match(/Repeats: /), "The tooltip shows the iterations");
     } else {
       ok(!title.match(/Repeats: /), "The tooltip doesn't show the iterations");
     }
+    if (controller.animationPlayers[i].state.easing) {
+      ok(title.match(/Easing: /), "The tooltip shows the easing");
+    }
+    if (controller.animationPlayers[i].state.fill) {
+      ok(title.match(/Fill: /), "The tooltip shows the fill");
+    }
+    if (controller.animationPlayers[i].state.direction) {
+      ok(title.match(/Direction: /), "The tooltip shows the direction");
+    }
     ok(!title.match(/Iteration start:/),
       "The tooltip doesn't show the iteration start");
   });
 });
--- a/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
@@ -19,25 +19,63 @@ add_task(function* () {
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
 
   let timeBlocks = timelineEl.querySelectorAll(".time-block");
   is(timeBlocks.length, 2, "2 animations are displayed");
 
   info("The first animation has its rate set to 1, let's measure it");
 
   let el = timeBlocks[0];
-  let duration = parseInt(el.querySelector(".iterations").style.width, 10);
+  let duration = getDuration(el.querySelector("path"));
   let delay = parseInt(el.querySelector(".delay").style.width, 10);
 
   info("The second animation has its rate set to 2, so should be shorter");
 
   let el2 = timeBlocks[1];
-  let duration2 = parseInt(el2.querySelector(".iterations").style.width, 10);
+  let duration2 = getDuration(el2.querySelector("path"));
   let delay2 = parseInt(el2.querySelector(".delay").style.width, 10);
 
   // The width are calculated by the animation-inspector dynamically depending
   // on the size of the panel, and therefore depends on the test machine/OS.
   // Let's not try to be too precise here and compare numbers.
   let durationDelta = (2 * duration2) - duration;
   ok(durationDelta <= 1, "The duration width is correct");
   let delayDelta = (2 * delay2) - delay;
   ok(delayDelta <= 1, "The delay width is correct");
 });
+
+function getDuration(pathEl) {
+  const pathSegList = pathEl.pathSegList;
+  // Find the index of starting iterations.
+  let startingIterationIndex = 0;
+  const firstPathSeg = pathSegList.getItem(1);
+  for (let i = 2, n = pathSegList.numberOfItems - 2; i < n; i++) {
+    // Changing point of the progress acceleration is the time.
+    const pathSeg = pathSegList.getItem(i);
+    if (firstPathSeg.y != pathSeg.y) {
+      startingIterationIndex = i;
+      break;
+    }
+  }
+  // Find the index of ending iterations.
+  let endingIterationIndex = 0;
+  let previousPathSegment = pathSegList.getItem(startingIterationIndex);
+  for (let i = startingIterationIndex + 1, n = pathSegList.numberOfItems - 2;
+       i < n; i++) {
+    // Find forwards fill-mode.
+    const pathSeg = pathSegList.getItem(i);
+    if (previousPathSegment.y == pathSeg.y) {
+      endingIterationIndex = i;
+      break;
+    }
+    previousPathSegment = pathSeg;
+  }
+  if (endingIterationIndex) {
+    // Not forwards fill-mode
+    endingIterationIndex = pathSegList.numberOfItems - 2;
+  }
+  // Return the distance of starting and ending
+  const startingIterationPathSegment =
+    pathSegList.getItem(startingIterationIndex);
+  const endingIterationPathSegment =
+    pathSegList.getItem(startingIterationIndex);
+  return endingIterationPathSegment.x - startingIterationPathSegment.x;
+}
--- a/devtools/client/animationinspector/test/browser_animation_timeline_ui.js
+++ b/devtools/client/animationinspector/test/browser_animation_timeline_ui.js
@@ -32,12 +32,12 @@ add_task(function* () {
 
     ok(animationEl.querySelector(".target"),
        "The animated node target element is in the DOM");
     ok(animationEl.querySelector(".time-block"),
        "The timeline element is in the DOM");
     is(animationEl.querySelector(".name").textContent,
        animation.state.name,
        "The name on the timeline is correct");
-    ok(animationEl.querySelector(".iterations"),
-       "The timeline has iterations displayed");
+    ok(animationEl.querySelector("svg path"),
+       "The timeline has svg and path element as summary graph");
   }
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/animationinspector/test/doc_timing_combination_animation.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <style>
+    div {
+      display: inline-block;
+      width: 100px;
+      height: 100px;
+      background-color: lime;
+    }
+    </style>
+  </head>
+  <body>
+    <script>
+    "use strict";
+
+    const delayList = [0, 50000, -50000];
+    const endDelayList = [0, 50000, -50000];
+
+    delayList.forEach(delay => {
+      endDelayList.forEach(endDelay => {
+        const el = document.createElement("div");
+        document.body.appendChild(el);
+        el.animate({ opacity: [0, 1] },
+                   { duration: 200000,
+                     iterations: 1,
+                     fill: "both",
+                     delay: delay,
+                     endDelay: endDelay });
+      });
+    });
+    </script>
+  </body>
+</html>
--- a/devtools/client/animationinspector/utils.js
+++ b/devtools/client/animationinspector/utils.js
@@ -22,25 +22,29 @@ const MILLIS_TIME_FORMAT_MAX_DURATION = 
 /**
  * DOM node creation helper function.
  * @param {Object} Options to customize the node to be created.
  * - nodeType {String} Optional, defaults to "div",
  * - attributes {Object} Optional attributes object like
  *   {attrName1:value1, attrName2: value2, ...}
  * - parent {DOMNode} Mandatory node to append the newly created node to.
  * - textContent {String} Optional text for the node.
+ * - namespace {String} Optional namespace
  * @return {DOMNode} The newly created node.
  */
 function createNode(options) {
   if (!options.parent) {
     throw new Error("Missing parent DOMNode to create new node");
   }
 
   let type = options.nodeType || "div";
-  let node = options.parent.ownerDocument.createElement(type);
+  let node =
+    options.namespace
+    ? options.parent.ownerDocument.createElementNS(options.namespace, type)
+    : options.parent.ownerDocument.createElement(type);
 
   for (let name in options.attributes || {}) {
     let value = options.attributes[name];
     node.setAttribute(name, value);
   }
 
   if (options.textContent) {
     node.textContent = options.textContent;
@@ -259,37 +263,12 @@ var TimeScale = {
     // The width of the endDelay.
     let endDelayW = this.durationToDistance(Math.abs(endDelay) / rate);
     // The start position of the endDelay.
     let endDelayX = endDelay < 0 ? x + iterationW - endDelayW
                                  : x + iterationW;
 
     return {x, w, iterationW, delayX, delayW, negativeDelayW,
             endDelayX, endDelayW};
-  },
-
-  /**
-   * Given an animation, get the background data for .iterations element.
-   * This background represents iterationCount and iterationStart.
-   * Returns three properties.
-   * 1. size: x of background-size (%)
-   * 2. position: x of background-position (%)
-   * 3. repeat: background-repeat (string)
-   */
-  getIterationsBackgroundData: function ({state}) {
-    let iterationCount = state.iterationCount || 1;
-    let iterationStartW = state.iterationStart % 1 * 100;
-    let background = {};
-    if (iterationCount == 1) {
-      background.size = 100 - iterationStartW;
-      background.position = 0;
-      background.repeat = "no-repeat";
-    } else {
-      background.size = 1 / iterationCount * 100;
-      background.position = -iterationStartW * background.size /
-                              (100 - background.size);
-      background.repeat = "repeat-x";
-    }
-    return background;
   }
 };
 
 exports.TimeScale = TimeScale;
--- a/devtools/client/debugger/content/views/sources-view.js
+++ b/devtools/client/debugger/content/views/sources-view.js
@@ -186,17 +186,16 @@ SourcesView.prototype = Heritage.extend(
   _addCommands: function () {
     XULUtils.addCommands(this._commandset, {
       addBreakpointCommand: e => this._onCmdAddBreakpoint(e),
       addConditionalBreakpointCommand: e => this._onCmdAddConditionalBreakpoint(e),
       blackBoxCommand: () => this.toggleBlackBoxing(),
       unBlackBoxButton: () => this._onStopBlackBoxing(),
       prettyPrintCommand: () => this.togglePrettyPrint(),
       toggleBreakpointsCommand: () =>this.toggleBreakpoints(),
-      togglePromiseDebuggerCommand: () => this.togglePromiseDebugger(),
       nextSourceCommand: () => this.selectNextItem(),
       prevSourceCommand: () => this.selectPrevItem()
     });
   },
 
   /**
    * Sets the preferred location to be selected in this sources container.
    * @param string aUrl
@@ -609,27 +608,16 @@ SourcesView.prototype = Heritage.extend(
       this._toggleBreakpointsButton.setAttribute("checked", true);
       this._onDisableAll();
     } else {
       this._toggleBreakpointsButton.removeAttribute("checked");
       this._onEnableAll();
     }
   },
 
-  togglePromiseDebugger: function () {
-    if (Prefs.promiseDebuggerEnabled) {
-      let promisePane = this.DebuggerView._promisePane;
-      promisePane.hidden = !promisePane.hidden;
-
-      if (!this.DebuggerView._promiseDebuggerIframe) {
-        this.DebuggerView._initializePromiseDebugger();
-      }
-    }
-  },
-
   hidePrettyPrinting: function () {
     this._prettyPrintButton.style.display = "none";
 
     if (this._blackBoxButton.style.display === "none") {
       let sep = document.querySelector("#sources-toolbar .devtools-separator");
       sep.style.display = "none";
     }
   },
--- a/devtools/client/debugger/debugger-controller.js
+++ b/devtools/client/debugger/debugger-controller.js
@@ -1206,17 +1206,16 @@ var Prefs = new PrefsHelper("devtools", 
   pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"],
   ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"],
   sourceMapsEnabled: ["Bool", "debugger.source-maps-enabled"],
   prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"],
   autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"],
   workersEnabled: ["Bool", "debugger.workers"],
   editorTabSize: ["Int", "editor.tabsize"],
   autoBlackBox: ["Bool", "debugger.auto-black-box"],
-  promiseDebuggerEnabled: ["Bool", "debugger.promise"]
 });
 
 /**
  * Convenient way of emitting events from the panel window.
  */
 EventEmitter.decorate(this);
 
 /**
--- a/devtools/client/debugger/debugger-view.js
+++ b/devtools/client/debugger/debugger-view.js
@@ -18,18 +18,16 @@ const FUNCTION_SEARCH_ACTION_MAX_DELAY =
 const SEARCH_GLOBAL_FLAG = "!";
 const SEARCH_FUNCTION_FLAG = "@";
 const SEARCH_TOKEN_FLAG = "#";
 const SEARCH_LINE_FLAG = ":";
 const SEARCH_VARIABLE_FLAG = "*";
 const SEARCH_AUTOFILL = [SEARCH_GLOBAL_FLAG, SEARCH_FUNCTION_FLAG, SEARCH_TOKEN_FLAG];
 const TOOLBAR_ORDER_POPUP_POSITION = "topcenter bottomleft";
 const RESIZE_REFRESH_RATE = 50; // ms
-const PROMISE_DEBUGGER_URL =
-  "chrome://devtools/content/promisedebugger/promise-debugger.xhtml";
 
 const EventListenersView = require("./content/views/event-listeners-view");
 const SourcesView = require("./content/views/sources-view");
 var actions = Object.assign(
   {},
   require("./content/globalActions"),
   require("./content/actions/breakpoints"),
   require("./content/actions/sources"),
@@ -130,17 +128,16 @@ var DebuggerView = {
     this.Filtering.destroy();
     this.StackFrames.destroy();
     this.StackFramesClassicList.destroy();
     this.Sources.destroy();
     this.VariableBubble.destroy();
     this.WatchExpressions.destroy();
     this.EventListeners.destroy();
     this.GlobalSearch.destroy();
-    this._destroyPromiseDebugger();
     this._destroyPanes();
 
     this.editor.destroy();
     this.editor = null;
 
     this.controller.dispatch(actions.removeAllBreakpoints());
   },
 
@@ -150,17 +147,16 @@ var DebuggerView = {
   _initializePanes: function () {
     dumpn("Initializing the DebuggerView panes");
 
     this._body = document.getElementById("body");
     this._editorDeck = document.getElementById("editor-deck");
     this._workersAndSourcesPane = document.getElementById("workers-and-sources-pane");
     this._instrumentsPane = document.getElementById("instruments-pane");
     this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle");
-    this._promisePane = document.getElementById("promise-debugger-pane");
 
     this.showEditor = this.showEditor.bind(this);
     this.showBlackBoxMessage = this.showBlackBoxMessage.bind(this);
     this.showProgressBar = this.showProgressBar.bind(this);
 
     this._onTabSelect = this._onInstrumentsPaneTabSelect.bind(this);
     this._instrumentsPane.tabpanels.addEventListener("select", this._onTabSelect);
 
@@ -186,17 +182,16 @@ var DebuggerView = {
     if (gHostType != "side") {
       Prefs.workersAndSourcesWidth = this._workersAndSourcesPane.getAttribute("width");
       Prefs.instrumentsWidth = this._instrumentsPane.getAttribute("width");
     }
 
     this._workersAndSourcesPane = null;
     this._instrumentsPane = null;
     this._instrumentsPaneToggleButton = null;
-    this._promisePane = null;
   },
 
   /**
    * Initializes the VariablesView instance and attaches a controller.
    */
   _initializeVariablesView: function () {
     this.Variables = new VariablesView(document.getElementById("variables"), {
       searchPlaceholder: L10N.getStr("emptyVariablesFilterText"),
@@ -234,51 +229,16 @@ var DebuggerView = {
         case "properties":
           window.emit(EVENTS.FETCHED_PROPERTIES);
           break;
       }
     });
   },
 
   /**
-   * Initialie the Promise Debugger instance.
-   */
-  _initializePromiseDebugger: function () {
-    let iframe = this._promiseDebuggerIframe = document.createElement("iframe");
-    iframe.setAttribute("flex", 1);
-
-    let onLoad = (event) => {
-      iframe.removeEventListener("load", onLoad, true);
-
-      let doc = event.target;
-      let win = doc.defaultView;
-
-      win.setPanel(DebuggerController._toolbox);
-    };
-
-    iframe.addEventListener("load", onLoad, true);
-    iframe.setAttribute("src", PROMISE_DEBUGGER_URL);
-    this._promisePane.appendChild(iframe);
-  },
-
-  /**
-   * Destroy the Promise Debugger instance.
-   */
-  _destroyPromiseDebugger: function () {
-    if (this._promiseDebuggerIframe) {
-      this._promiseDebuggerIframe.contentWindow.destroy();
-
-      this._promiseDebuggerIframe.parentNode.removeChild(
-        this._promiseDebuggerIframe);
-
-      this._promiseDebuggerIframe = null;
-    }
-  },
-
-  /**
    * Initializes the Editor instance.
    *
    * @param function aCallback
    *        Called after the editor finishes initializing.
    */
   _initializeEditor: function (callback) {
     dumpn("Initializing the DebuggerView editor");
 
--- a/devtools/client/debugger/debugger.xul
+++ b/devtools/client/debugger/debugger.xul
@@ -336,21 +336,16 @@
                                  command="prettyPrintCommand"
                                  hidden="true"/>
                 </hbox>
                 <vbox class="devtools-separator"/>
                 <toolbarbutton id="toggle-breakpoints"
                                class="devtools-toolbarbutton"
                                tooltiptext="&debuggerUI.sources.toggleBreakpoints;"
                                command="toggleBreakpointsCommand"/>
-                <toolbarbutton id="toggle-promise-debugger"
-                               class="devtools-toolbarbutton"
-                               tooltiptext="&debuggerUI.sources.togglePromiseDebugger;"
-                               command="togglePromiseDebuggerCommand"
-                               hidden="true"/>
               </toolbar>
             </tabpanel>
             <tabpanel id="callstack-tabpanel">
               <vbox id="callstack-list" flex="1"/>
             </tabpanel>
           </tabpanels>
         </tabbox>
       </vbox>
@@ -398,22 +393,16 @@
                 <vbox id="variables" flex="1"/>
               </tabpanel>
               <tabpanel id="events-tabpanel">
                 <vbox id="event-listeners" flex="1"/>
               </tabpanel>
             </tabpanels>
           </tabbox>
         </hbox>
-        <splitter id="editor-and-promise-splitter"
-                class="devtools-horizontal-splitter"/>
-        <vbox id="promise-debugger-pane"
-              flex="1"
-              hidden="true">
-        </vbox>
       </vbox>
       <splitter id="vertical-layout-splitter"
                 class="devtools-horizontal-splitter"/>
       <hbox id="vertical-layout-panes-container">
         <splitter id="sources-and-instruments-splitter"
                   class="devtools-side-splitter"/>
         <!-- The sources-pane and instruments-pane will be moved in this
              container if the toolbox's host requires it. -->
--- a/devtools/client/debugger/test/mochitest/browser_dbg_host-layout.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_host-layout.js
@@ -120,49 +120,43 @@ function testHost(aPanel, aHostType, aLa
   is(gView._body.getAttribute("layout"), aLayoutType,
     "The default host type is present as an attribute on the panel's body.");
 
   if (aLayoutType == "horizontal") {
     is(gView._workersAndSourcesPane.parentNode.id, "debugger-widgets",
       "The workers and sources pane's parent is correct for the horizontal layout.");
     is(gView._instrumentsPane.parentNode.id, "editor-and-instruments-pane",
       "The instruments pane's parent is correct for the horizontal layout.");
-    is(gDebugger.document.getElementById("promise-debugger-pane").parentNode.id,
-      "debugger-content",
-      "The promise pane's parent is correct for the horizontal layout.");
   } else {
     is(gView._workersAndSourcesPane.parentNode.id, "vertical-layout-panes-container",
       "The workers and sources pane's parent is correct for the vertical layout.");
     is(gView._instrumentsPane.parentNode.id, "vertical-layout-panes-container",
       "The instruments pane's parent is correct for the vertical layout.");
-    is(gDebugger.document.getElementById("promise-debugger-pane").parentNode.id,
-      "debugger-content",
-      "The promise pane's parent is correct for the horizontal layout.");
   }
 
   let widgets = gDebugger.document.getElementById("debugger-widgets").childNodes;
   let content = gDebugger.document.getElementById("debugger-content").childNodes;
   let editorPane =
     gDebugger.document.getElementById("editor-and-instruments-pane").childNodes;
   let verticalPane =
     gDebugger.document.getElementById("vertical-layout-panes-container").childNodes;
 
   if (aLayoutType == "horizontal") {
     is(widgets.length, 5, // 1 pane, 1 content box, 2 splitters and a phantom box.
       "Found the correct number of debugger widgets.");
-    is(content.length, 3, // 2 panes, 1 splitter.
+    is(content.length, 1, // 1 pane
       "Found the correct number of debugger content.");
     is(editorPane.length, 3, // 2 panes, 1 splitter
       "Found the correct number of debugger panes.");
     is(verticalPane.length, 1, // 1 lonely splitter in the phantom box.
       "Found the correct number of debugger panes.");
   } else {
     is(widgets.length, 4, // 1 content box, 2 splitters and a phantom box.
       "Found the correct number of debugger widgets.");
-    is(content.length, 3, // 2 panes, 1 splitter.
+    is(content.length, 1, // 1 pane
       "Found the correct number of debugger content.");
     is(editorPane.length, 2, // 1 pane, 1 splitter
       "Found the correct number of debugger panes.");
     is(verticalPane.length, 3, // 2 panes and 1 splitter in the phantom box.
       "Found the correct number of debugger panes.");
   }
 }
 
--- a/devtools/client/framework/test/browser_toolbox_select_event.js
+++ b/devtools/client/framework/test/browser_toolbox_select_event.js
@@ -38,39 +38,64 @@ add_task(function* () {
   yield testSelectEvent("inspector");
   yield testSelectEvent("webconsole");
   yield testSelectEvent("styleeditor");
   yield testSelectEvent("inspector");
   yield testSelectEvent("webconsole");
   yield testSelectEvent("styleeditor");
   yield toolbox.destroy();
 
+  yield testSelectToolRace();
+
   /**
    * Assert that selecting the given toolId raises a select event
    * @param {toolId} Id of the tool to test
    */
-  function testSelectEvent(toolId) {
-    return new Promise(resolve => {
-      toolbox.once("select", (event, id) => {
-        is(id, toolId, toolId + " selected");
-        resolve();
-      });
-      toolbox.selectTool(toolId);
-    });
+  function* testSelectEvent(toolId) {
+    let onSelect = toolbox.once("select");
+    toolbox.selectTool(toolId);
+    let id = yield onSelect;
+    is(id, toolId, toolId + " selected");
   }
 
   /**
    * Assert that selecting the given toolId raises its corresponding
    * selected event
    * @param {toolId} Id of the tool to test
    */
-  function testToolSelectEvent(toolId) {
-    return new Promise(resolve => {
-      toolbox.once(toolId + "-selected", () => {
-        let msg = toolId + " tool selected";
-        is(toolbox.currentToolId, toolId, msg);
-        resolve();
-      });
-      toolbox.selectTool(toolId);
-    });
+  function* testToolSelectEvent(toolId) {
+    let onSelected = toolbox.once(toolId + "-selected");
+    toolbox.selectTool(toolId);
+    yield onSelected;
+    is(toolbox.currentToolId, toolId, toolId + " tool selected");
+  }
+
+  /**
+   * Assert that two calls to selectTool won't race
+   */
+  function* testSelectToolRace() {
+    let toolbox = yield openToolboxForTab(tab, "webconsole");
+    let selected = false;
+    let onSelect = (event, id) => {
+      if (selected) {
+        ok(false, "Got more than one 'select' event");
+      } else {
+        selected = true;
+      }
+    };
+    toolbox.once("select", onSelect);
+    let p1 = toolbox.selectTool("inspector")
+    let p2 = toolbox.selectTool("inspector");
+    // Check that both promises don't resolve too early
+    let checkSelectToolResolution = panel => {
+      ok(selected, "selectTool resolves only after 'select' event is fired");
+      let inspector = toolbox.getPanel("inspector");
+      is(panel, inspector, "selecTool resolves to the panel instance");
+    };
+    p1.then(checkSelectToolResolution);
+    p2.then(checkSelectToolResolution);
+    yield p1;
+    yield p2;
+
+    yield toolbox.destroy();
   }
 });
 
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -1446,21 +1446,29 @@ Toolbox.prototype = {
     let sep = this.doc.getElementById("toolbox-controls-separator");
     if (id === "options") {
       sep.setAttribute("invisible", "true");
     } else {
       sep.removeAttribute("invisible");
     }
 
     if (this.currentToolId == id) {
-      // re-focus tool to get key events again
-      this.focusTool(id);
+      let panel = this._toolPanels.get(id);
+      if (panel) {
+        // We have a panel instance, so the tool is already fully loaded.
+
+        // re-focus tool to get key events again
+        this.focusTool(id);
 
-      // Return the existing panel in order to have a consistent return value.
-      return promise.resolve(this._toolPanels.get(id));
+        // Return the existing panel in order to have a consistent return value.
+        return promise.resolve(panel);
+      }
+      // Otherwise, if there is no panel instance, it is still loading,
+      // so we are racing another call to selectTool with the same id.
+      return this.once("select").then(() => promise.resolve(this._toolPanels.get(id)));
     }
 
     if (!this.isReady) {
       throw new Error("Can't select tool, wait for toolbox 'ready' event");
     }
 
     let tab = this.doc.getElementById("toolbox-tab-" + id);
 
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -128,16 +128,18 @@ subsuite = clipboard
 [browser_inspector_menu-03-paste-items.js]
 subsuite = clipboard
 [browser_inspector_menu-03-paste-items-svg.js]
 subsuite = clipboard
 [browser_inspector_menu-04-use-in-console.js]
 [browser_inspector_menu-05-attribute-items.js]
 [browser_inspector_menu-06-other.js]
 [browser_inspector_navigation.js]
+[browser_inspector_navigate_to_errors.js]
+[browser_inspector_open_on_neterror.js]
 [browser_inspector_pane-toggle-01.js]
 [browser_inspector_pane-toggle-02.js]
 [browser_inspector_pane-toggle-03.js]
 [browser_inspector_pane-toggle-05.js]
 skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keyboard Access setting is set to All Control in System Keyboard
 [browser_inspector_picker-stop-on-destroy.js]
 [browser_inspector_picker-stop-on-tool-change.js]
 [browser_inspector_portrait_mode.js]
--- a/devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js
+++ b/devtools/client/inspector/test/browser_inspector_destroy-after-navigation.js
@@ -4,24 +4,19 @@
 "use strict";
 
 // Testing that closing the inspector after navigating to a page doesn't fail.
 
 const URL_1 = "data:text/plain;charset=UTF-8,abcde";
 const URL_2 = "data:text/plain;charset=UTF-8,12345";
 
 add_task(function* () {
-  let { toolbox } = yield openInspectorForURL(URL_1);
+  let { inspector, toolbox } = yield openInspectorForURL(URL_1);
 
-  info("Navigating to different URL.");
-  let navigated = toolbox.target.once("navigate");
-  navigateTo(toolbox, URL_2);
-
-  info("Waiting for 'navigate' event from toolbox target.");
-  yield navigated;
+  yield navigateTo(inspector, URL_2);
 
   info("Destroying toolbox");
   try {
     yield toolbox.destroy();
     ok(true, "Toolbox destroyed");
   } catch (e) {
     ok(false, "An exception occured while destroying toolbox");
     console.error(e);
--- a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-xul.js
@@ -6,17 +6,17 @@
 // Test that the eyedropper icons in the toolbar and in the color picker aren't displayed
 // when the page isn't an HTML one.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_xbl.xul";
 const TEST_URL_2 =
   "data:text/html;charset=utf-8,<h1 style='color:red'>HTML test page</h1>";
 
 add_task(function* () {
-  let {inspector, toolbox} = yield openInspectorForURL(TEST_URL);
+  let {inspector} = yield openInspectorForURL(TEST_URL);
 
   info("Check the inspector toolbar");
   let button = inspector.panelDoc.querySelector("#inspector-eyedropper-toggle");
   ok(isDisabled(button), "The button is hidden in the toolbar");
 
   info("Check the color picker");
   yield selectNode("#scale", inspector);
 
@@ -30,21 +30,17 @@ add_task(function* () {
   let onColorPickerReady = cPicker.once("ready");
   swatchEl.click();
   yield onColorPickerReady;
 
   button = cPicker.tooltip.doc.querySelector("#eyedropper-button");
   ok(isDisabled(button), "The button is disabled in the color picker");
 
   info("Navigate to a HTML document");
-  let navigated = toolbox.target.once("navigate");
-  let markuploaded = inspector.once("markuploaded");
-  navigateTo(toolbox, TEST_URL_2);
-  yield navigated;
-  yield markuploaded;
+  yield navigateTo(inspector, TEST_URL_2);
 
   info("Check the inspector toolbar in HTML document");
   button = inspector.panelDoc.querySelector("#inspector-eyedropper-toggle");
   ok(!isDisabled(button), "The button is enabled in the toolbar");
 
   info("Check the color picker in HTML document");
   // Find the color swatch in the rule-view.
   yield selectNode("h1", inspector);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_navigate_to_errors.js
@@ -0,0 +1,50 @@
+/* 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";
+
+// Test that inspector works when navigating to error pages.
+
+const TEST_URL_1 = "data:text/html,<html><body id=\"test-doc-1\">page</body></html>";
+const TEST_URL_2 = "http://127.0.0.1:36325/";
+const TEST_URL_3 = "http://www.wronguri.wronguri/";
+const TEST_URL_4 = "data:text/html,<html><body>test-doc-4</body></html>";
+
+add_task(function* () {
+  // Open the inspector on a valid URL
+  let { inspector, testActor } = yield openInspectorForURL(TEST_URL_1);
+
+  info("Navigate to closed port");
+  yield navigateTo(inspector, TEST_URL_2);
+
+  let documentURI = yield testActor.eval("document.documentURI;");
+  ok(documentURI.startsWith("about:neterror"), "content is correct.");
+
+  let hasPage = yield getNodeFront("#test-doc-1", inspector);
+  ok(!hasPage, "Inspector actor is no longer able to reach previous page DOM node");
+
+  let hasNetErrorNode = yield getNodeFront("#errorShortDesc", inspector);
+  ok(hasNetErrorNode, "Inspector actor is able to reach error page DOM node");
+
+  let bundle = Services.strings.createBundle("chrome://global/locale/appstrings.properties");
+  let domain = TEST_URL_2.match(/^http:\/\/(.*)\/$/)[1];
+  let errorMsg = bundle.formatStringFromName("connectionFailure",
+                                             [domain], 1);
+  is(yield getDisplayedNodeTextContent("#errorShortDescText", inspector), errorMsg,
+     "Inpector really inspects the error page");
+
+  info("Navigate to unknown domain");
+  yield navigateTo(inspector, TEST_URL_3);
+
+  domain = TEST_URL_3.match(/^http:\/\/(.*)\/$/)[1];
+  errorMsg = bundle.formatStringFromName("dnsNotFound",
+                                         [domain], 1);
+  is(yield getDisplayedNodeTextContent("#errorShortDescText", inspector), errorMsg,
+     "Inspector really inspects the new error page");
+
+  info("Navigate to a valid url");
+  yield navigateTo(inspector, TEST_URL_4);
+
+  is(yield getDisplayedNodeTextContent("body", inspector), "test-doc-4",
+     "Inspector really inspects the valid url");
+});
--- a/devtools/client/inspector/test/browser_inspector_navigation.js
+++ b/devtools/client/inspector/test/browser_inspector_navigation.js
@@ -9,35 +9,35 @@
 
 const TEST_URL_FILE = "browser/devtools/client/inspector/test/" +
   "doc_inspector_breadcrumbs.html";
 
 const TEST_URL_1 = "http://test1.example.org/" + TEST_URL_FILE;
 const TEST_URL_2 = "http://test2.example.org/" + TEST_URL_FILE;
 
 add_task(function* () {
-  let { inspector, toolbox, testActor } = yield openInspectorForURL(TEST_URL_1);
-  let markuploaded = inspector.once("markuploaded");
+  let { inspector, testActor } = yield openInspectorForURL(TEST_URL_1);
 
   yield selectNode("#i1", inspector);
 
   info("Navigating to a different page.");
-  yield navigateTo(toolbox, TEST_URL_2);
-
-  info("Waiting for markup view to load after navigation.");
-  yield markuploaded;
+  yield navigateTo(inspector, TEST_URL_2);
 
   ok(true, "New page loaded");
   yield selectNode("#i1", inspector);
 
-  markuploaded = inspector.once("markuploaded");
+  let markuploaded = inspector.once("markuploaded");
+  let onUpdated = inspector.once("inspector-updated");
 
   info("Going back in history");
   yield testActor.eval("history.go(-1)");
 
   info("Waiting for markup view to load after going back in history.");
   yield markuploaded;
 
+  info("Check that the inspector updates");
+  yield onUpdated;
+
   ok(true, "Old page loaded");
   is((yield testActor.eval("location.href;")), TEST_URL_1, "URL is correct.");
 
   yield selectNode("#i1", inspector);
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_open_on_neterror.js
@@ -0,0 +1,37 @@
+/* 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";
+
+// Test that inspector works correctly when opened against a net error page
+
+const TEST_URL_1 = "http://127.0.0.1:36325/";
+const TEST_URL_2 = "data:text/html,<html><body>test-doc-2</body></html>";
+
+add_task(function* () {
+  // Unfortunately, net error page are not firing load event, so that we can't
+  // use addTab helper and have to do that:
+  let tab = gBrowser.selectedTab = gBrowser.addTab("data:text/html,empty");
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield ContentTask.spawn(tab.linkedBrowser, { url: TEST_URL_1 }, function* ({ url }) {
+    // Also, the neterror being privileged, the DOMContentLoaded only fires on
+    // the chromeEventHandler.
+    let { chromeEventHandler } = docShell; // eslint-disable-line no-undef
+    let onDOMContentLoaded = ContentTaskUtils.waitForEvent(chromeEventHandler,
+      "DOMContentLoaded", true);
+    content.location = url;
+    yield onDOMContentLoaded;
+  });
+
+  let { inspector, testActor } = yield openInspector();
+  ok(true, "Inspector loaded on the already opened net error");
+
+  let documentURI = yield testActor.eval("document.documentURI;");
+  ok(documentURI.startsWith("about:neterror"), "content is really a net error page.");
+
+  info("Navigate to a valid url");
+  yield navigateTo(inspector, TEST_URL_2);
+
+  is(yield getDisplayedNodeTextContent("body", inspector), "test-doc-2",
+     "Inspector really inspects the valid url");
+});
--- a/devtools/client/inspector/test/browser_inspector_select-last-selected.js
+++ b/devtools/client/inspector/test/browser_inspector_select-last-selected.js
@@ -59,43 +59,37 @@ add_task(function* () {
   let { inspector, toolbox, testActor } = yield openInspectorForURL(PAGE_1);
 
   for (let { url, nodeToSelect, selectedNode } of TEST_DATA) {
     if (nodeToSelect) {
       info("Selecting node " + nodeToSelect + " before navigation.");
       yield selectNode(nodeToSelect, inspector);
     }
 
-    let onNewRoot = inspector.once("new-root");
     yield navigateToAndWaitForNewRoot(url);
 
-    info("Waiting for new root.");
-    yield onNewRoot;
-
-    info("Waiting for inspector to update after new-root event.");
-    yield inspector.once("inspector-updated");
-
     let nodeFront = yield getNodeFront(selectedNode, inspector);
     ok(nodeFront, "Got expected node front");
     is(inspector.selection.nodeFront, nodeFront,
        selectedNode + " is selected after navigation.");
   }
 
-  function navigateToAndWaitForNewRoot(url) {
+  function* navigateToAndWaitForNewRoot(url) {
     info("Navigating and waiting for new-root event after navigation.");
 
-    let newRoot = inspector.once("new-root");
+    let current = yield testActor.eval("location.href");
+    if (url == current) {
+      info("Reloading page.");
+      let markuploaded = inspector.once("markuploaded");
+      let onNewRoot = inspector.once("new-root");
+      let onUpdated = inspector.once("inspector-updated");
 
-    return testActor.eval("location.href")
-      .then(current => {
-        if (url == current) {
-          info("Reloading page.");
-          let activeTab = toolbox.target.activeTab;
-          return activeTab.reload();
-        }
-
-        info("Navigating to " + url);
-        navigateTo(toolbox, url);
-
-        return newRoot;
-      });
+      let activeTab = toolbox.target.activeTab;
+      yield activeTab.reload();
+      info("Waiting for inspector to be ready.");
+      yield markuploaded;
+      yield onNewRoot;
+      yield onUpdated;
+    } else {
+      yield navigateTo(inspector, url);
+    }
   }
 });
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -49,20 +49,34 @@ registerCleanupFunction(() => {
 registerCleanupFunction(function* () {
   // Move the mouse outside inspector. If the test happened fake a mouse event
   // somewhere over inspector the pointer is considered to be there when the
   // next test begins. This might cause unexpected events to be emitted when
   // another test moves the mouse.
   EventUtils.synthesizeMouseAtPoint(1, 1, {type: "mousemove"}, window);
 });
 
-var navigateTo = function (toolbox, url) {
-  let activeTab = toolbox.target.activeTab;
-  return activeTab.navigateTo(url);
-};
+var navigateTo = Task.async(function* (inspector, url) {
+  let markuploaded = inspector.once("markuploaded");
+  let onNewRoot = inspector.once("new-root");
+  let onUpdated = inspector.once("inspector-updated");
+
+  info("Navigating to: " + url);
+  let activeTab = inspector.toolbox.target.activeTab;
+  yield activeTab.navigateTo(url);
+
+  info("Waiting for markup view to load after navigation.");
+  yield markuploaded;
+
+  info("Waiting for new root.");
+  yield onNewRoot;
+
+  info("Waiting for inspector to update after new-root event.");
+  yield onUpdated;
+});
 
 /**
  * Start the element picker and focus the content window.
  * @param {Toolbox} toolbox
  * @param {Boolean} skipFocus - Allow tests to bypass the focus event.
  */
 var startPicker = Task.async(function* (toolbox, skipFocus) {
   info("Start the element picker");
@@ -687,8 +701,31 @@ function openContextMenuAndGetAllItems(i
  *        The child node index of the element to get
  * @return {DOMNode} The rule editor if any at this index
  */
 function getRuleViewRuleEditor(view, childrenIndex, nodeIndex) {
   return nodeIndex !== undefined ?
     view.element.children[childrenIndex].childNodes[nodeIndex]._ruleEditor :
     view.element.children[childrenIndex]._ruleEditor;
 }
+
+/**
+ * Get the text displayed for a given DOM Element's textContent within the
+ * markup view.
+ *
+ * @param {String} selector
+ * @param {InspectorPanel} inspector
+ * @return {String} The text displayed in the markup view
+ */
+function* getDisplayedNodeTextContent(selector, inspector) {
+  // We have to ensure that the textContent is displayed, for that the DOM
+  // Element has to be selected in the markup view and to be expanded.
+  yield selectNode(selector, inspector);
+
+  let container = yield getContainerForSelector(selector, inspector);
+  yield inspector.markup.expandNode(container.node);
+  yield waitForMultipleChildrenUpdates(inspector);
+  if (container) {
+    let textContainer = container.elt.querySelector("pre");
+    return textContainer.textContent;
+  }
+  return null;
+}
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -96,19 +96,16 @@ devtools.jar:
     content/performance/views/details-waterfall.js (performance/views/details-waterfall.js)
     content/performance/views/details-js-call-tree.js (performance/views/details-js-call-tree.js)
     content/performance/views/details-js-flamegraph.js (performance/views/details-js-flamegraph.js)
     content/performance/views/details-memory-call-tree.js (performance/views/details-memory-call-tree.js)
     content/performance/views/details-memory-flamegraph.js (performance/views/details-memory-flamegraph.js)
     content/performance/views/recordings.js (performance/views/recordings.js)
     content/memory/memory.xhtml (memory/memory.xhtml)
     content/memory/initializer.js (memory/initializer.js)
-    content/promisedebugger/promise-controller.js (promisedebugger/promise-controller.js)
-    content/promisedebugger/promise-panel.js (promisedebugger/promise-panel.js)
-    content/promisedebugger/promise-debugger.xhtml (promisedebugger/promise-debugger.xhtml)
     content/commandline/commandline.css (commandline/commandline.css)
     content/commandline/commandlineoutput.xhtml (commandline/commandlineoutput.xhtml)
     content/commandline/commandlinetooltip.xhtml (commandline/commandlinetooltip.xhtml)
     content/framework/toolbox-window.xul (framework/toolbox-window.xul)
     content/framework/toolbox-options.xhtml (framework/toolbox-options.xhtml)
     content/framework/toolbox.xul (framework/toolbox.xul)
     content/framework/toolbox-init.js (framework/toolbox-init.js)
     content/framework/options-panel.css (framework/options-panel.css)
--- a/devtools/client/locales/en-US/animationinspector.properties
+++ b/devtools/client/locales/en-US/animationinspector.properties
@@ -65,16 +65,34 @@ player.infiniteIterationCountText=∞
 # LOCALIZATION NOTE (player.animationIterationStartLabel):
 # This string is displayed in a tooltip that appears when hovering over
 # animations in the timeline. It is the label displayed before the animation
 # iterationStart value.
 # %1$S will be replaced by the original iteration start value
 # %2$S will be replaced by the actual time of iteration start
 player.animationIterationStartLabel=Iteration start: %1$S (%2$Ss)
 
+# LOCALIZATION NOTE (player.animationEasingLabel):
+# This string is displayed in a tooltip that appears when hovering over
+# animations in the timeline. It is the label displayed before the animation
+# easing value.
+player.animationEasingLabel=Easing:
+
+# LOCALIZATION NOTE (player.animationFillLabel):
+# This string is displayed in a tooltip that appears when hovering over
+# animations in the timeline. It is the label displayed before the animation
+# fill mode value.
+player.animationFillLabel=Fill:
+
+# LOCALIZATION NOTE (player.animationDirectionLabel):
+# This string is displayed in a tooltip that appears when hovering over
+# animations in the timeline. It is the label displayed before the animation
+# direction value.
+player.animationDirectionLabel=Direction:
+
 # LOCALIZATION NOTE (player.timeLabel):
 # This string is displayed in each animation player widget, to indicate either
 # how long (in seconds) the animation lasts, or what is the animation's current
 # time (in seconds too);
 player.timeLabel=%Ss
 
 # LOCALIZATION NOTE (player.playbackRateLabel):
 # This string is displayed in each animation player widget, as the label of
--- a/devtools/client/locales/en-US/debugger.dtd
+++ b/devtools/client/locales/en-US/debugger.dtd
@@ -45,20 +45,16 @@
   -  checkbox that toggles auto pretty print. -->
 <!ENTITY debuggerUI.autoPrettyPrint     "Auto Prettify Minified Sources">
 <!ENTITY debuggerUI.autoPrettyPrint.accesskey "P">
 
 <!-- LOCALIZATION NOTE (debuggerUI.sources.toggleBreakpoints): This is the tooltip for the
   -  button that toggles all breakpoints for all sources. -->
 <!ENTITY debuggerUI.sources.toggleBreakpoints "Enable/disable all breakpoints">
 
-<!-- LOCALIZATION NOTE (debuggerUI.sources.togglePromiseDebugger): This is the
-  -  tooltip for the button that toggles the promise debugger. -->
-<!ENTITY debuggerUI.sources.togglePromiseDebugger "Toggle Promise Debugger">
-
 <!-- LOCALIZATION NOTE (debuggerUI.clearButton): This is the label for
   -  the button that clears the collected tracing data in the tracing tab. -->
 <!ENTITY debuggerUI.clearButton "Clear">
 
 <!-- LOCALIZATION NOTE (debuggerUI.clearButton.tooltip): This is the tooltip for
   -  the button that clears the collected tracing data in the tracing tab. -->
 <!ENTITY debuggerUI.clearButton.tooltip "Clear the collected traces">
 
deleted file mode 100644
--- a/devtools/client/locales/en-US/promisedebugger.dtd
+++ /dev/null
@@ -1,15 +0,0 @@
-<!-- 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/. -->
-
-<!-- LOCALIZATION NOTE : FILE This file contains the Promise debugger panel
-     strings. The Promise debugger panel is part of the debugger -->
-<!-- LOCALIZATION NOTE : FILE Do not translate commandkey -->
-
-<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
-  - keep it in English, or another language commonly spoken among web developers.
-  - You want to make that choice consistent across the developer tools.
-  - A good criteria is the language in which you'd find the best
-  - documentation on web development on the web. -->
-
-<!ENTITY title "Promise Debugger">
--- a/devtools/client/moz.build
+++ b/devtools/client/moz.build
@@ -17,17 +17,16 @@ DIRS += [
     'inspector',
     'jsonview',
     'locales',
     'memory',
     'netmonitor',
     'performance',
     'preferences',
     'projecteditor',
-    'promisedebugger',
     'responsive.html',
     'responsivedesign',
     'scratchpad',
     'shadereditor',
     'shared',
     'shims',
     'sourceeditor',
     'storage',
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -95,17 +95,16 @@ pref("devtools.debugger.remote-timeout",
 pref("devtools.debugger.pause-on-exceptions", false);
 pref("devtools.debugger.ignore-caught-exceptions", true);
 pref("devtools.debugger.source-maps-enabled", true);
 pref("devtools.debugger.client-source-maps-enabled", true);
 pref("devtools.debugger.pretty-print-enabled", true);
 pref("devtools.debugger.auto-pretty-print", false);
 pref("devtools.debugger.auto-black-box", true);
 pref("devtools.debugger.workers", false);
-pref("devtools.debugger.promise", false);
 
 #if defined(NIGHTLY_BUILD)
 pref("devtools.debugger.new-debugger-frontend", true);
 #else
 pref("devtools.debugger.new-debugger-frontend", false);
 #endif
 
 // The default Debugger UI settings
deleted file mode 100644
--- a/devtools/client/promisedebugger/moz.build
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
-# vim: set filetype=python:
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-DevToolsModules(
-)
deleted file mode 100644
--- a/devtools/client/promisedebugger/promise-controller.js
+++ /dev/null
@@ -1,102 +0,0 @@
-/* -*- 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/. */
-
-/* global PromisesPanel */
-
-"use strict";
-
-var { utils: Cu } = Components;
-const { loader, require } =
-  Cu.import("resource://devtools/shared/Loader.jsm", {});
-
-const { Task } = require("devtools/shared/task");
-
-loader.lazyRequireGetter(this, "promise");
-loader.lazyRequireGetter(this, "EventEmitter",
-  "devtools/shared/event-emitter");
-loader.lazyRequireGetter(this, "DevToolsUtils",
-  "devtools/shared/DevToolsUtils");
-loader.lazyRequireGetter(this, "PromisesFront",
-  "devtools/server/actors/promises", true);
-
-// Global toolbox, set when startup is called.
-var gToolbox;
-
-/**
- * Initialize the promise debugger controller and view upon loading the iframe.
- */
-var startup = Task.async(function* (toolbox) {
-  gToolbox = toolbox;
-
-  yield PromisesController.initialize(toolbox);
-  yield PromisesPanel.initialize();
-});
-
-/**
- * Destroy the promise debugger controller and view when unloading the iframe.
- */
-var shutdown = Task.async(function* () {
-  yield PromisesController.destroy();
-  yield PromisesPanel.destroy();
-
-  gToolbox = null;
-});
-
-function setPanel(toolbox) {
-  return startup(toolbox).catch(e =>
-    DevToolsUtils.reportException("setPanel", e));
-}
-
-function destroy() {
-  return shutdown().catch(e => DevToolsUtils.reportException("destroy", e));
-}
-
-/**
- * The promisedebugger controller's job is to retrieve PromisesFronts from the
- * server.
- */
-var PromisesController = {
-  initialize: Task.async(function* () {
-    if (this.initialized) {
-      return this.initialized.promise;
-    }
-
-    this.initialized = promise.defer();
-
-    let target = gToolbox.target;
-    this.promisesFront = new PromisesFront(target.client, target.form);
-    yield this.promisesFront.attach();
-
-    if (this.destroyed) {
-      console.warn("Could not fully initialize the PromisesController");
-      return null;
-    }
-
-    this.initialized.resolve();
-  }),
-
-  destroy: Task.async(function* () {
-    if (!this.initialized) {
-      return null;
-    }
-
-    if (this.destroyed) {
-      return this.destroyed.promise;
-    }
-
-    this.destroyed = promise.defer();
-
-    if (this.promisesFront) {
-      yield this.promisesFront.detach();
-      this.promisesFront.destroy();
-      this.promisesFront = null;
-    }
-
-    this.destroyed.resolve();
-  }),
-};
-
-EventEmitter.decorate(PromisesController);
deleted file mode 100644
--- a/devtools/client/promisedebugger/promise-debugger.xhtml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- This Source Code Form is subject to the terms of the Mkozilla 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 [
-  <!ENTITY % promisedebuggerDTD SYSTEM "chrome://devtools/locale/promisedebugger.dtd">
-  %promisedebuggerDTD;
-]>
-
-<html xmlns="http://www.w3.org/1999/xhtml">
-  <head>
-    <title>&title;</title>
-    <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"/>
-  </head>
-  <body class="devtools-monospace" role="application">
-    <script type="application/javascript;version=1.8" src="promise-controller.js"></script>
-    <script type="application/javascript;version=1.8" src="promise-panel.js"></script>
-  </body>
-</html>
deleted file mode 100644
--- a/devtools/client/promisedebugger/promise-panel.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/* -*- 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/. */
-
-/* global PromisesController, promise */
-/* import-globals-from promise-controller.js */
-
-"use strict";
-
-/**
- * The main promise debugger UI.
- */
-var PromisesPanel = {
-  PANEL_INITIALIZED: "panel-initialized",
-
-  initialize: Task.async(function* () {
-    if (PromisesController.destroyed) {
-      return null;
-    }
-    if (this.initialized) {
-      return this.initialized.promise;
-    }
-    this.initialized = promise.defer();
-
-    this.initialized.resolve();
-
-    this.emit(this.PANEL_INITIALIZED);
-  }),
-
-  destroy: Task.async(function* () {
-    if (!this.initialized) {
-      return null;
-    }
-    if (this.destroyed) {
-      return this.destroyed.promise;
-    }
-    this.destroyed = promise.defer();
-
-    this.destroyed.resolve();
-  }),
-};
-
-EventEmitter.decorate(PromisesPanel);
deleted file mode 100644
--- a/devtools/client/promisedebugger/test/.eslintrc.js
+++ /dev/null
@@ -1,6 +0,0 @@
-"use strict";
-
-module.exports = {
-  // Extend from the shared list of defined globals for mochitests.
-  "extends": "../../../.eslintrc.mochitests.js"
-};
deleted file mode 100644
--- a/devtools/client/promisedebugger/test/head.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
--- a/devtools/client/responsive.html/browser/tunnel.js
+++ b/devtools/client/responsive.html/browser/tunnel.js
@@ -104,16 +104,27 @@ function tunnelToInnerBrowser(outer, inn
             return outer[FRAME_LOADER];
           }
           return inner.frameLoader;
         },
         configurable: true,
         enumerable: true,
       });
 
+      // The `outerWindowID` of the content is used by browser actions like view source
+      // and print.  They send the ID down to the client to find the right content frame
+      // to act on.
+      Object.defineProperty(outer, "outerWindowID", {
+        get() {
+          return inner.outerWindowID;
+        },
+        configurable: true,
+        enumerable: true,
+      });
+
       // The `permanentKey` property on a <xul:browser> is used to index into various maps
       // held by the session store.  When you swap content around with
       // `_swapBrowserDocShells`, these keys are also swapped so they follow the content.
       // This means the key that matches the content is on the inner browser.  Since we
       // want the browser UI to believe the page content is part of the outer browser, we
       // copy the content's `permanentKey` up to the outer browser.
       copyPermanentKey(outer, inner);
 
@@ -269,16 +280,17 @@ function tunnelToInnerBrowser(outer, inn
       mmTunnel.destroy();
       mmTunnel = null;
 
       // Reset overridden XBL properties and methods.  Deleting the override
       // means it will fallback to the original XBL binding definitions which
       // are on the prototype.
       delete outer.frameLoader;
       delete outer[FRAME_LOADER];
+      delete outer.outerWindowID;
 
       // Invalidate outer's permanentKey so that SessionStore stops associating
       // things that happen to the outer browser with the content inside in the
       // inner browser.
       outer.permanentKey = { id: "zombie" };
 
       browserWindow = null;
       gBrowser = null;
@@ -401,31 +413,55 @@ MessageManagerTunnel.prototype = {
     "PageVisibility:Show",
     // Messages sent to SessionStore.jsm
     "SessionStore:update",
     // Messages sent to BrowserTestUtils.jsm
     "browser-test-utils:loadEvent",
   ],
 
   OUTER_TO_INNER_MESSAGE_PREFIXES: [
+    // Messages sent from nsContextMenu.js
+    "ContextMenu:",
     // Messages sent from DevTools
     "debug:",
     // Messages sent from findbar.xml
     "Findbar:",
     // Messages sent from RemoteFinder.jsm
     "Finder:",
+    // Messages sent from InlineSpellChecker.jsm
+    "InlineSpellChecker:",
+    // Messages sent from pageinfo.js
+    "PageInfo:",
+    // Messsage sent from printUtils.js
+    "Printing:",
+    // Messages sent from browser-social.js
+    "Social:",
+    "PageMetadata:",
+    // Messages sent from viewSourceUtils.js
+    "ViewSource:",
   ],
 
   INNER_TO_OUTER_MESSAGE_PREFIXES: [
+    // Messages sent to nsContextMenu.js
+    "ContextMenu:",
     // Messages sent to DevTools
     "debug:",
     // Messages sent to findbar.xml
     "Findbar:",
     // Messages sent to RemoteFinder.jsm
     "Finder:",
+    // Messages sent to pageinfo.js
+    "PageInfo:",
+    // Messsage sent from printUtils.js
+    "Printing:",
+    // Messages sent to browser-social.js
+    "Social:",
+    "PageMetadata:",
+    // Messages sent to viewSourceUtils.js
+    "ViewSource:",
   ],
 
   OUTER_TO_INNER_FRAME_SCRIPTS: [
     // DevTools server for OOP frames
     "resource://devtools/server/child.js"
   ],
 
   get outerParentMM() {
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -40,16 +40,22 @@
   --keyframes-marker-size: 10px;
   /* The color of the time graduation borders */
   --time-graduation-border-color: rgba(128, 136, 144, .5);
 }
 
 .animation {
   --timeline-border-color: var(--theme-body-color);
   --timeline-background-color: var(--theme-splitter-color);
+  /* The color of the endDelay hidden progress */
+  --enddelay-hidden-progress-color: var(--theme-graphs-grey);
+  /* The color of none fill mode */
+  --fill-none-color: var(--theme-highlight-gray);
+  /* The color of enable fill mode */
+  --fill-enable-color: var(--timeline-border-color);
 }
 
 .animation.cssanimation {
   --timeline-border-color: var(--theme-highlight-lightorange);
   --timeline-background-color: var(--theme-contrast-background);
 }
 
 .animation.csstransition {
@@ -339,76 +345,61 @@ body {
 .animation-timeline .animation-target {
   background-color: transparent;
 }
 
 .animation-timeline .animation .time-block {
   cursor: pointer;
 }
 
-/* Animation iterations */
-
-.animation-timeline .animation .iterations {
+/* Animation summary graph */
+.animation-timeline .animation .summary {
   position: absolute;
+  width: 100%;
   height: 100%;
-  box-sizing: border-box;
-
-  /* Iterations of the animation are displayed with a repeating linear-gradient
-     which size is dynamically changed from JS. The gradient only draws 1px
-     borders between each iteration. These borders must have the same color as
-     the border of this element */
-  background-image:
-    linear-gradient(to left,
-                    var(--timeline-border-color) 0,
-                    var(--timeline-border-color) 1px,
-                    transparent 1px,
-                    transparent 2px);
-  border: 1px solid var(--timeline-border-color);
-  /* Border-right is already handled by the gradient */
-  border-width: 1px 0 1px 1px;
-
-  /* The background color is set independently */
-  background-color: var(--timeline-background-color);
 }
 
-.animation-timeline .animation .iterations.infinite::before,
-.animation-timeline .animation .iterations.infinite::after {
-  content: "";
-  position: absolute;
-  top: 0;
-  right: 0;
-  width: 0;
-  height: 0;
-  border-right: 4px solid var(--theme-body-background);
-  border-top: 4px solid transparent;
-  border-bottom: 4px solid transparent;
+.animation-timeline .animation .summary path {
+  fill: var(--timeline-background-color);
+  stroke: var(--timeline-border-color);
 }
 
-.animation-timeline .animation .iterations.infinite::after {
-  bottom: 0;
-  top: unset;
+.animation-timeline .animation .summary .infinity.copied {
+  opacity: .3;
+}
+
+.animation-timeline .animation .summary path.delay-path.negative,
+.animation-timeline .animation .summary path.enddelay-path.negative {
+  fill: none;
+  stroke: var(--enddelay-hidden-progress-color);
+  stroke-dasharray: 2, 2;
 }
 
 .animation-timeline .animation .name {
   position: absolute;
-  color: var(--theme-selection-color);
+  color: var(--theme-selection-color3);
+  top: 0px;
+  left: 0px;
   height: 100%;
+  width: 100%;
   display: flex;
   align-items: center;
   padding: 0 2px;
   box-sizing: border-box;
   --fast-track-icon-width: 15px;
   z-index: 1;
 }
 
 .animation-timeline .animation .name div {
   /* Flex items don't support text-overflow, so a child div is used */
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
+  background-color: rgba(255, 255, 255, 0.7);
+  max-width: 50%;
 }
 
 .animation-timeline .fast-track .name div {
   width: calc(100% - var(--fast-track-icon-width));
 }
 
 .animation-timeline .fast-track .name::after {
   /* Animations running on the compositor have the fast-track background image*/
@@ -418,64 +409,67 @@ body {
   top: 1px;
   right: 0;
   height: 100%;
   width: var(--fast-track-icon-width);
   z-index: 1;
 }
 
 .animation-timeline .all-properties .name::after {
-  background-color: white;
+  background-color: var(--theme-content-color3);
   clip-path: url(images/animation-fast-track.svg#thunderbolt);
   background-repeat: no-repeat;
   background-position: center;
 }
 
 .animation-timeline .some-properties .name::after {
   background-color: var(--theme-content-color3);
   clip-path: url(images/animation-fast-track.svg#thunderbolt);
   background-repeat: no-repeat;
   background-position: center;
 }
 
 .animation-timeline .animation .delay,
 .animation-timeline .animation .end-delay {
   position: absolute;
-  height: 100%;
-  border: 1px solid var(--timeline-border-color);
-  box-sizing: border-box;
+  border-bottom: 3px solid var(--fill-none-color);
+  bottom: -0.5px;
 }
 
-.animation-timeline .animation .delay {
-  border-width: 1px 0 1px 1px;
-  background-image: repeating-linear-gradient(45deg,
-                                              transparent,
-                                              transparent 1px,
-                                              var(--theme-selection-color) 1px,
-                                              var(--theme-selection-color) 4px);
-  background-color: var(--timeline-border-color);
+.animation-timeline .animation .delay::after,
+.animation-timeline .animation .end-delay::after {
+  content: "";
+  position: absolute;
+  top: -2px;
+  width: 3px;
+  height: 3px;
+  border: 2px solid var(--fill-none-color);
+  background-color: var(--fill-none-color);
+  border-radius: 50%;
 }
 
-.animation-timeline .animation .end-delay {
-  border-width: 1px 1px 1px 0;
-  background-image: repeating-linear-gradient(
-                      -45deg,
-                      transparent,
-                      transparent 3px,
-                      var(--timeline-border-color) 3px,
-                      var(--timeline-border-color) 4px);
+.animation-timeline .animation .negative.delay::after,
+.animation-timeline .animation .positive.end-delay::after {
+  right: -3px;
+}
+
+.animation-timeline .animation .positive.delay::after,
+.animation-timeline .animation .negative.end-delay::after {
+  left: -3px;
 }
 
-.animation-timeline .animation .delay.negative,
-.animation-timeline .animation .end-delay.negative {
-  /* Negative delays are displayed on top of the animation, so they need a
-     right border. Whereas normal delays are displayed just before the
-     animation, so there's already the animation's left border that serves as
-     a separation. */
-  border-width: 1px;
+.animation-timeline .animation .fill.delay,
+.animation-timeline .animation .fill.end-delay {
+  border-color: var(--fill-enable-color);
+}
+
+.animation-timeline .animation .fill.delay::after,
+.animation-timeline .animation .fill.end-delay::after {
+  border-color: var(--fill-enable-color);
+  background-color: var(--fill-enable-color);
 }
 
 /* Animation target node gutter, contains a preview of the dom node */
 
 .animation-target {
   background-color: var(--theme-toolbar-background);
   padding: 0 4px;
   box-sizing: border-box;
--- a/devtools/client/themes/debugger.css
+++ b/devtools/client/themes/debugger.css
@@ -124,20 +124,16 @@
   -moz-image-region: rect(0,16px,16px,0);
 }
 
 #toggle-breakpoints[checked] > image {
   /* This button has a special checked image, don't make it blue */
   filter: none;
 }
 
-#toggle-promise-debugger {
-  /* TODO Bug 1186119: Add a toggle promise debugger image */
-}
-
 #sources .black-boxed {
   color: rgba(128,128,128,0.4);
 }
 
 #sources .selected .black-boxed {
   color: rgba(255,255,255,0.4);
 }
 
--- a/devtools/server/actors/animation.js
+++ b/devtools/server/actors/animation.js
@@ -233,16 +233,40 @@ var AnimationPlayerActor = protocol.Acto
    * Get the animation iterationStart from this player, in ratio.
    * That is offset of starting position of the animation.
    * @return {Number}
    */
   getIterationStart: function () {
     return this.player.effect.getComputedTiming().iterationStart;
   },
 
+  /**
+   * Get the animation easing from this player.
+   * @return {String}
+   */
+  getEasing: function () {
+    return this.player.effect.timing.easing;
+  },
+
+  /**
+   * Get the animation fill mode from this player.
+   * @return {String}
+   */
+  getFill: function () {
+    return this.player.effect.getComputedTiming().fill;
+  },
+
+  /**
+   * Get the animation direction from this player.
+   * @return {String}
+   */
+  getDirection: function () {
+    return this.player.effect.getComputedTiming().direction;
+  },
+
   getPropertiesCompositorStatus: function () {
     let properties = this.player.effect.getProperties();
     return properties.map(prop => {
       return {
         property: prop.property,
         runningOnCompositor: prop.runningOnCompositor,
         warning: prop.warning
       };
@@ -275,16 +299,19 @@ var AnimationPlayerActor = protocol.Acto
       playState: this.player.playState,
       playbackRate: this.player.playbackRate,
       name: this.getName(),
       duration: this.getDuration(),
       delay: this.getDelay(),
       endDelay: this.getEndDelay(),
       iterationCount: this.getIterationCount(),
       iterationStart: this.getIterationStart(),
+      fill: this.getFill(),
+      easing: this.getEasing(),
+      direction: this.getDirection(),
       // animation is hitting the fast path or not. Returns false whenever the
       // animation is paused as it is taken off the compositor then.
       isRunningOnCompositor:
         this.getPropertiesCompositorStatus()
             .some(propState => propState.runningOnCompositor),
       propertyState: this.getPropertiesCompositorStatus(),
       // The document timeline's currentTime is being sent along too. This is
       // not strictly related to the node's animationPlayer, but is useful to
@@ -392,16 +419,30 @@ var AnimationPlayerActor = protocol.Acto
   ready: function () {
     return this.player.ready;
   },
 
   /**
    * Set the current time of the animation player.
    */
   setCurrentTime: function (currentTime) {
+    // The spec is that the progress of animation is changed
+    // if the time of setCurrentTime is during the endDelay.
+    // We should prevent the time
+    // to make the same animation behavior as the original.
+    // Likewise, in case the time is less than 0.
+    const timing = this.player.effect.getComputedTiming();
+    if (timing.delay < 0) {
+      currentTime += timing.delay;
+    }
+    if (currentTime < 0) {
+      currentTime = 0;
+    } else if (currentTime * this.player.playbackRate > timing.endTime) {
+      currentTime = timing.endTime;
+    }
     this.player.currentTime = currentTime * this.player.playbackRate;
   },
 
   /**
    * Set the playback rate of the animation player.
    */
   setPlaybackRate: function (playbackRate) {
     this.player.playbackRate = playbackRate;
--- a/devtools/server/actors/highlighters.js
+++ b/devtools/server/actors/highlighters.js
@@ -191,17 +191,19 @@ var HighlighterActor = exports.Highlight
       this._highlighter.hide();
     }
   },
 
   /**
    * Hide the box model highlighting if it was shown before
    */
   hideBoxModel: function () {
-    this._highlighter.hide();
+    if (this._highlighter) {
+      this._highlighter.hide();
+    }
   },
 
   /**
    * Returns `true` if the event was dispatched from a window included in
    * the current highlighter environment; or if the highlighter environment has
    * chrome privileges
    *
    * The method is specifically useful on B2G, where we do not want that events
--- a/devtools/server/actors/highlighters/utils/markup.js
+++ b/devtools/server/actors/highlighters/utils/markup.js
@@ -237,17 +237,22 @@ exports.createNode = createNode;
 function CanvasFrameAnonymousContentHelper(highlighterEnv, nodeBuilder) {
   this.highlighterEnv = highlighterEnv;
   this.nodeBuilder = nodeBuilder;
   this.anonymousContentDocument = this.highlighterEnv.document;
   // XXX the next line is a wallpaper for bug 1123362.
   this.anonymousContentGlobal = Cu.getGlobalForObject(
                                 this.anonymousContentDocument);
 
-  this._insert();
+  // Only try to create the highlighter when the document is loaded,
+  // otherwise, wait for the navigate event to fire.
+  let doc = this.highlighterEnv.document;
+  if (doc.documentElement && doc.readyState != "uninitialized") {
+    this._insert();
+  }
 
   this._onNavigate = this._onNavigate.bind(this);
   this.highlighterEnv.on("navigate", this._onNavigate);
 
   this.listeners = new Map();
 }
 
 CanvasFrameAnonymousContentHelper.prototype = {
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -3,17 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /* global XPCNativeWrapper */
 
-var { Ci, Cu } = require("chrome");
+var { Ci, Cu, Cr } = require("chrome");
 var Services = require("Services");
 var { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 var promise = require("promise");
 var {
   ActorPool, createExtraActors, appendExtraActors, GeneratedLocation
 } = require("devtools/server/actors/common");
 var { DebuggerServer } = require("devtools/server/main");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
@@ -2490,19 +2490,37 @@ DebuggerProgressListener.prototype = {
     let window = progress.DOMWindow;
     if (isDocument && isStart) {
       // One of the earliest events that tells us a new URI
       // is being loaded in this window.
       let newURI = request instanceof Ci.nsIChannel ? request.URI.spec : null;
       this._tabActor._willNavigate(window, newURI, request);
     }
     if (isWindow && isStop) {
-      // Somewhat equivalent of load event.
-      // (window.document.readyState == complete)
-      this._tabActor._navigate(window);
+      // Don't dispatch "navigate" event just yet when there is a redirect to
+      // about:neterror page.
+      if (request.status != Cr.NS_OK) {
+        // Instead, listen for DOMContentLoaded as about:neterror is loaded
+        // with LOAD_BACKGROUND flags and never dispatches load event.
+        // That may be the same reason why there is no onStateChange event
+        // for about:neterror loads.
+        let handler = getDocShellChromeEventHandler(progress);
+        let onLoad = evt => {
+          // Ignore events from iframes
+          if (evt.target == window.document) {
+            handler.removeEventListener("DOMContentLoaded", onLoad, true);
+            this._tabActor._navigate(window);
+          }
+        };
+        handler.addEventListener("DOMContentLoaded", onLoad, true);
+      } else {
+        // Somewhat equivalent of load event.
+        // (window.document.readyState == complete)
+        this._tabActor._navigate(window);
+      }
     }
   }, "DebuggerProgressListener.prototype.onStateChange")
 };
 
 exports.register = function (handle) {
   handle.setRootActor(createRootActor);
 };
 
--- a/devtools/server/tests/browser/animation.html
+++ b/devtools/server/tests/browser/animation.html
@@ -23,28 +23,31 @@
   .multiple-animations {
     display: inline-block;
 
     width: 50px;
     height: 50px;
     border-radius: 50%;
     background: #eee;
 
-    animation: move 200s infinite, glow 100s 5;
+    animation: move 200s infinite , glow 100s 5;
+    animation-timing-function: ease-out;
+    animation-direction: reverse;
+    animation-fill-mode: both;
   }
 
   .transition {
     display: inline-block;
 
     width: 50px;
     height: 50px;
     border-radius: 50%;
     background: #f06;
 
-    transition: width 500s;
+    transition: width 500s ease-out;
   }
   .transition.get-round {
     width: 200px;
   }
 
   .long-animation {
     display: inline-block;
 
--- a/devtools/server/tests/browser/browser_animation_playerState.js
+++ b/devtools/server/tests/browser/browser_animation_playerState.js
@@ -26,71 +26,98 @@ function* playerHasAnInitialState(walker
   ok("currentTime" in player.initialState, "Player's state has currentTime");
   ok("playState" in player.initialState, "Player's state has playState");
   ok("playbackRate" in player.initialState, "Player's state has playbackRate");
   ok("name" in player.initialState, "Player's state has name");
   ok("duration" in player.initialState, "Player's state has duration");
   ok("delay" in player.initialState, "Player's state has delay");
   ok("iterationCount" in player.initialState,
      "Player's state has iterationCount");
+  ok("fill" in player.initialState, "Player's state has fill");
+  ok("easing" in player.initialState, "Player's state has easing");
+  ok("direction" in player.initialState, "Player's state has direction");
   ok("isRunningOnCompositor" in player.initialState,
      "Player's state has isRunningOnCompositor");
   ok("type" in player.initialState, "Player's state has type");
   ok("documentCurrentTime" in player.initialState,
      "Player's state has documentCurrentTime");
 }
 
 function* playerStateIsCorrect(walker, animations) {
   info("Checking the state of the simple animation");
 
-  let state = yield getAnimationStateForNode(walker, animations,
-                                             ".simple-animation", 0);
+  let player = yield getAnimationPlayerForNode(walker, animations,
+                                               ".simple-animation", 0);
+  let state = yield player.getCurrentState();
   is(state.name, "move", "Name is correct");
   is(state.duration, 200000, "Duration is correct");
   // null = infinite count
   is(state.iterationCount, null, "Iteration count is correct");
+  is(state.fill, "none", "Fill is correct");
+  is(state.easing, "linear", "Easing is correct");
+  is(state.direction, "normal", "Direction is correct");
   is(state.playState, "running", "PlayState is correct");
   is(state.playbackRate, 1, "PlaybackRate is correct");
   is(state.type, "cssanimation", "Type is correct");
 
   info("Checking the state of the transition");
 
-  state = yield getAnimationStateForNode(walker, animations, ".transition", 0);
+  player =
+    yield getAnimationPlayerForNode(walker, animations, ".transition", 0);
+  state = yield player.getCurrentState();
   is(state.name, "width", "Transition name matches transition property");
   is(state.duration, 500000, "Transition duration is correct");
   // transitions run only once
   is(state.iterationCount, 1, "Transition iteration count is correct");
+  is(state.fill, "backwards", "Transition fill is correct");
+  is(state.easing, "linear", "Transition easing is correct");
+  is(state.direction, "normal", "Transition direction is correct");
   is(state.playState, "running", "Transition playState is correct");
   is(state.playbackRate, 1, "Transition playbackRate is correct");
   is(state.type, "csstransition", "Transition type is correct");
+  // chech easing in keyframe
+  let keyframes = yield player.getFrames();
+  is(keyframes.length, 2, "Transition length of keyframe is correct");
+  is(keyframes[0].easing,
+     "ease-out", "Transition kerframes's easing is correct");
 
   info("Checking the state of one of multiple animations on a node");
 
   // Checking the 2nd player
-  state = yield getAnimationStateForNode(walker, animations,
-                                         ".multiple-animations", 1);
+  player = yield getAnimationPlayerForNode(walker, animations,
+                                           ".multiple-animations", 1);
+  state = yield player.getCurrentState();
   is(state.name, "glow", "The 2nd animation's name is correct");
   is(state.duration, 100000, "The 2nd animation's duration is correct");
   is(state.iterationCount, 5, "The 2nd animation's iteration count is correct");
+  is(state.fill, "both", "The 2nd animation's fill is correct");
+  is(state.easing, "linear", "The 2nd animation's easing is correct");
+  is(state.direction, "reverse", "The 2nd animation's direction is correct");
   is(state.playState, "running", "The 2nd animation's playState is correct");
   is(state.playbackRate, 1, "The 2nd animation's playbackRate is correct");
+  // chech easing in keyframe
+  keyframes = yield player.getFrames();
+  is(keyframes.length, 2, "The 2nd animation's length of keyframe is correct");
+  is(keyframes[0].easing,
+     "ease-out", "The 2nd animation's easing of kerframes is correct");
 
   info("Checking the state of an animation with delay");
 
-  state = yield getAnimationStateForNode(walker, animations,
-                                         ".delayed-animation", 0);
+  player = yield getAnimationPlayerForNode(walker, animations,
+                                           ".delayed-animation", 0);
+  state = yield player.getCurrentState();
   is(state.delay, 5000, "The animation delay is correct");
 
   info("Checking the state of an transition with delay");
 
-  state = yield getAnimationStateForNode(walker, animations,
-                                         ".delayed-transition", 0);
+  player = yield getAnimationPlayerForNode(walker, animations,
+                                           ".delayed-transition", 0);
+  state = yield player.getCurrentState();
   is(state.delay, 3000, "The transition delay is correct");
 }
 
-function* getAnimationStateForNode(walker, animations, nodeSelector, index) {
+function* getAnimationPlayerForNode(walker, animations, nodeSelector, index) {
   let node = yield walker.querySelector(walker.rootNode, nodeSelector);
   let players = yield animations.getAnimationPlayersForNode(node);
   let player = players[index];
   yield player.ready();
-  let state = yield player.getCurrentState();
-  return state;
+  return player;
 }
--- a/devtools/shared/fronts/animation.js
+++ b/devtools/shared/fronts/animation.js
@@ -60,16 +60,19 @@ const AnimationPlayerFront = FrontClassW
       playState: this._form.playState,
       playbackRate: this._form.playbackRate,
       name: this._form.name,
       duration: this._form.duration,
       delay: this._form.delay,
       endDelay: this._form.endDelay,
       iterationCount: this._form.iterationCount,
       iterationStart: this._form.iterationStart,
+      easing: this._form.easing,
+      fill: this._form.fill,
+      direction: this._form.direction,
       isRunningOnCompositor: this._form.isRunningOnCompositor,
       propertyState: this._form.propertyState,
       documentCurrentTime: this._form.documentCurrentTime
     };
   },
 
   /**
    * Executed when the AnimationPlayerActor emits a "changed" event. Used to
--- a/docshell/shistory/moz.build
+++ b/docshell/shistory/moz.build
@@ -1,16 +1,19 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPIDL_SOURCES += [
     'nsIBFCacheEntry.idl',
+    'nsIGroupedSHistory.idl',
+    'nsIPartialSHistory.idl',
+    'nsIPartialSHistoryListener.idl',
     'nsISHContainer.idl',
     'nsISHEntry.idl',
     'nsISHistory.idl',
     'nsISHistoryInternal.idl',
     'nsISHistoryListener.idl',
     'nsISHTransaction.idl',
 ]
 
new file mode 100644
--- /dev/null
+++ b/docshell/shistory/nsIGroupedSHistory.idl
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "nsISupports.idl"
+
+interface nsIFrameLoader;
+interface nsIPartialSHistory;
+
+/**
+ * nsIGroupedSHistory represent a combined session history across multiple
+ * root docshells (usually browser tabs). The participating nsISHistory can
+ * either be in chrome process or in content process, but nsIGroupedSHistory
+ * itself lives in chrome process. The communication is proxyed through
+ * nsIPartialSHistory.
+ */
+[scriptable, builtinclass, uuid(813e498d-73a8-449a-be09-6187e62c5352)]
+interface nsIGroupedSHistory : nsISupports
+{
+  // The total number of entries of all its partial session histories.
+  [infallible] readonly attribute unsigned long count;
+
+  /**
+   * Remove all partial histories after currently active one (if any) and then
+   * append the given partial session history to the end of the list.
+   */
+  void appendPartialSessionHistory(in nsIPartialSHistory aPartialHistory);
+
+  /**
+   * Notify the grouped session history that the active partial session history
+   * has been modified. All partial session histories after the active one
+   * will be removed and destroy.
+   */
+  void onPartialSessionHistoryChange(in nsIPartialSHistory aPartialHistory);
+
+  /**
+   * Find the proper partial session history and navigate to the entry
+   * corresponding to the given global index. Note it doesn't swap frameloaders,
+   * but rather return the target loader for the caller to swap.
+   *
+   * @param aGlobalIndex        The global index to navigate to.
+   * @param aTargetLoaderToSwap The owner frameloader of the to-be-navigate
+   *                            partial session history.
+   */
+  void gotoIndex(in unsigned long aGlobalIndex, out nsIFrameLoader aTargetLoaderToSwap);
+};
new file mode 100644
--- /dev/null
+++ b/docshell/shistory/nsIPartialSHistory.idl
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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 "nsISupports.idl"
+
+interface nsIFrameLoader;
+
+/**
+ * nsIPartialSHistory represents a part of nsIGroupedSHistory. It associates to
+ * a "partial" nsISHistory in either local or remote process.
+ */
+[scriptable, builtinclass, uuid(5cd75e28-838c-4a0a-972e-6005f736ef7a)]
+interface nsIPartialSHistory : nsISupports
+{
+  // The number of entries of its corresponding nsISHistory.
+  [infallible] readonly attribute unsigned long count;
+
+  // If it's part of a grouped session history, globalIndexOffset denotes the
+  // number of entries ahead.
+  [infallible] readonly attribute unsigned long globalIndexOffset;
+
+  // The frameloader which owns this partial session history.
+  readonly attribute nsIFrameLoader ownerFrameLoader;
+
+  /**
+   * Notify that it's been added to a grouped session history. It also implies
+   * it's becoming the active partial history of the group.
+   *
+   * @param aOffset                The number of entries in preceding partial
+   *                               session histories.
+   */
+  void onAttachGroupedSessionHistory(in unsigned long aOffset);
+
+  /**
+   * Notify that one or more entries in its associated nsISHistory object
+   * have been changed (i.e. add / remove / replace). It's mainly used for
+   * cross-process case, since in the in-process case we can just register an
+   * nsISHistoryListener instead.
+   *
+   * @param aCount The number of entries in the associated session history.
+   *               It can be the same as the old value if entries were replaced.
+   */
+  void onSessionHistoryChange(in unsigned long aCount);
+
+  /**
+   * Notify that the partial session history has been swapped in as the active
+   * session history. Only an active session history can possibly add / remove /
+   * replace its history entries.
+   *
+   * @param aGlobalLength      The up-to-date global length.
+   * @param aTargetLocalIndex  The local index to navigate to.
+   */
+  void onActive(in unsigned long aGlobalLength, in unsigned long aTargetLocalIndex);
+
+  /**
+   * Notify that the partial session history has been swapped out and is no
+   * longer active.
+   */
+  void onDeactive();
+};
new file mode 100644
--- /dev/null
+++ b/docshell/shistory/nsIPartialSHistoryListener.idl
@@ -0,0 +1,24 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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 "nsISupports.idl"
+
+/**
+ * Listener to handle cross partial nsISHistory navigation requests.
+ */
+[scriptable, uuid(be0cd2b6-6f03-4366-9fe2-184c914ff3df)]
+interface nsIPartialSHistoryListener : nsISupports
+{
+  /**
+   * Called when the navigation target belongs to another nsISHistory within
+   * the same nsIGroupedSHistory, and it needs to initiate cross nsISHistory
+   * navigation.
+   *
+   * @param aIndex The index of complete history to navigate to.
+   */
+   void onRequestCrossBrowserNavigation(in unsigned long aIndex);
+};
--- a/docshell/shistory/nsISHistory.idl
+++ b/docshell/shistory/nsISHistory.idl
@@ -3,16 +3,18 @@
  * 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 "nsISupports.idl"
 
 interface nsISHEntry;
 interface nsISHistoryListener;
 interface nsISimpleEnumerator;
+interface nsIPartialSHistoryListener;
+
 /**
  * An interface to the primary properties of the Session History
  * component. In an embedded browser environment, the nsIWebBrowser
  * object creates an instance of session history for each open window.
  * A handle to the session history object can be obtained from
  * nsIWebNavigation. In a non-embedded situation, the  owner of the
  * session history component must create a instance of it and set
  * it in the nsIWebNavigation object.
@@ -26,24 +28,51 @@ interface nsISimpleEnumerator;
 
 #define NS_SHISTORY_CONTRACTID "@mozilla.org/browser/shistory;1"
 %}
 
 [scriptable, uuid(7b807041-e60a-4384-935f-af3061d8b815)]
 interface nsISHistory: nsISupports
 {
   /**
+   * An attribute denoting whether the nsISHistory is associated to a grouped
+   * session history.
+   *
+   * The abstraction of grouped session history is implemented at
+   * nsIWebNavigation level, so those canGoBack / canGoForward / gotoIndex
+   * functions work transparently;
+   *
+   * On the other hand, nsISHistory works on partial session history directly.
+   * Unless otherwise specified, count / index attributes and parameters all
+   * indicate local count / index, so we won't mess up docshell.
+   */
+   readonly attribute bool isPartial;
+
+  /**
    * A readonly property of the interface that returns 
    * the number of toplevel documents currently available
    * in session history.
    */
    readonly attribute long count;
 
   /**
-   * A readonly property of the interface that returns 
+   * If isPartial, globalCount denotes the total number of entries in the
+   * grouped session history; Otherwise it has the same value as count.
+   */
+   readonly attribute long globalCount;
+
+  /**
+   * A readonly property which represents the difference between global indices
+   * of grouped session history and local indices of this particular session
+   * history object.
+   */
+   readonly attribute long globalIndexOffset;
+
+  /**
+   * A readonly property of the interface that returns
    * the index of the current document in session history.
    */
    readonly attribute long index;
 
   /**
    * A readonly property of the interface that returns 
    * the index of the last document that started to load and
    * didn't finished yet. When document finishes the loading
@@ -119,17 +148,23 @@ interface nsISHistory: nsISupports
    * @note                    A listener object must implement 
    *                          nsISHistoryListener and nsSupportsWeakReference
    * @see nsISHistoryListener
    * @see nsSupportsWeakReference
    */ 
    void removeSHistoryListener(in nsISHistoryListener aListener);
 
   /**
-   * Called to obtain a enumerator for all the  documents stored in 
+   * Set the listener to handle cross nsISHistory navigation when it works
+   * in "partial" mode.
+   */
+   void setPartialSHistoryListener(in nsIPartialSHistoryListener aListener);
+
+  /**
+   * Called to obtain a enumerator for all the  documents stored in
    * session history. The enumerator object thus returned by this method
    * can be traversed using nsISimpleEnumerator. 
    *
    * @note  To access individual history entries of the enumerator, perform the
    *        following steps:
    *        1) Call nsISHistory->GetSHistoryEnumerator() to obtain handle 
    *           the nsISimpleEnumerator object.
    *        2) Use nsISimpleEnumerator->GetNext() on the object returned
@@ -154,9 +189,33 @@ interface nsISHistory: nsISupports
    * @param aEntry            The entry to obtain the index of.
    *
    * @return                  <code>NS_OK</code> index for the history entry
    *                          is obtained successfully.
    *                          <code>NS_ERROR_FAILURE</code> Error in obtaining
    *                          index for the given history entry.
    */
    long getIndexOfEntry(in nsISHEntry aEntry);
+
+  /**
+   * Called when this nsISHistory has became the active history of a grouped
+   * session history.
+   *
+   * @param globalLength      The up to date number of entries in the grouped
+   *                          session history.
+   * @param targetIndex       The local index to navigate to.
+   */
+   void onPartialSessionHistoryActive(in long globalLength, in long targetIndex);
+
+  /**
+   * Called when this nsISHistory has became inactive history of a grouped
+   * session history.
+   */
+   void onPartialSessionHistoryDeactive();
+
+  /**
+   * Called when it's attached to a nsIGroupedSHistory instance.
+   *
+   * @param offset            The number of entries in the grouped session
+   *                          history before this session history object.
+   */
+   void onAttachGroupedSessionHistory(in long offset);
 };
--- a/docshell/shistory/nsISHistoryListener.idl
+++ b/docshell/shistory/nsISHistoryListener.idl
@@ -93,9 +93,16 @@ interface nsISHistoryListener : nsISuppo
    * Called when an entry is replaced in the session history. Entries are
    * replaced when navigating away from non-persistent history entries (such as
    * about pages) and when history.replaceState is called.
    *
    * @param aIndex        The index in session history of the entry being
   *                       replaced
    */
    void OnHistoryReplaceEntry(in long aIndex);
+
+   /**
+    * Called when nsISHistory::count has been updated. Unlike OnHistoryNewEntry
+    * and OnHistoryPurge which happen before the modifications are actually done
+    * and maybe cancellable, this function is called after these modifications.
+    */
+   void OnLengthChange(in long aCount);
 };
--- a/docshell/shistory/nsSHistory.cpp
+++ b/docshell/shistory/nsSHistory.cpp
@@ -221,16 +221,19 @@ EvictContentViewerForTransaction(nsISHTr
 }
 
 } // namespace
 
 nsSHistory::nsSHistory()
   : mIndex(-1)
   , mLength(0)
   , mRequestedIndex(-1)
+  , mIsPartial(false)
+  , mGlobalIndexOffset(0)
+  , mEntriesInFollowingPartialHistories(0)
   , mRootDocShell(nullptr)
 {
   // Add this new SHistory object to the list
   PR_APPEND_LINK(this, &gSHistoryList);
 }
 
 nsSHistory::~nsSHistory()
 {
@@ -414,40 +417,105 @@ nsSHistory::AddEntry(nsISHEntry* aSHEntr
   // parent will properly set the parent child relationship
   txn->SetPersist(aPersist);
   NS_ENSURE_SUCCESS(txn->Create(aSHEntry, currentTxn), NS_ERROR_FAILURE);
 
   // A little tricky math here...  Basically when adding an object regardless of
   // what the length was before, it should always be set back to the current and
   // lop off the forward.
   mLength = (++mIndex + 1);
+  NOTIFY_LISTENERS(OnLengthChange, (mLength));
+
+  // Much like how mLength works above, when changing our entries, all following
+  // partial histories should be purged, so we just reset the number to zero.
+  mEntriesInFollowingPartialHistories = 0;
 
   // If this is the very first transaction, initialize the list
   if (!mListRoot) {
     mListRoot = txn;
   }
 
   // Purge History list if it is too long
   if (gHistoryMaxSize >= 0 && mLength > gHistoryMaxSize) {
     PurgeHistory(mLength - gHistoryMaxSize);
   }
 
   RemoveDynEntries(mIndex - 1, mIndex);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsSHistory::GetIsPartial(bool* aResult)
+{
+  NS_ENSURE_ARG_POINTER(aResult);
+  *aResult = mIsPartial;
+  return NS_OK;
+}
+
 /* Get size of the history list */
 NS_IMETHODIMP
 nsSHistory::GetCount(int32_t* aResult)
 {
   NS_ENSURE_ARG_POINTER(aResult);
   *aResult = mLength;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsSHistory::GetGlobalCount(int32_t* aResult)
+{
+  NS_ENSURE_ARG_POINTER(aResult);
+  *aResult = mGlobalIndexOffset + mLength + mEntriesInFollowingPartialHistories;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::GetGlobalIndexOffset(int32_t* aResult)
+{
+  NS_ENSURE_ARG_POINTER(aResult);
+  *aResult = mGlobalIndexOffset;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHistory::OnPartialSessionHistoryActive(int32_t aGlobalLength, int32_t aTargetIndex)
+{
+  NS_ENSURE_TRUE(mIsPartial, NS_ERROR_UNEXPECTED);
+
+  int32_t extraLength = aGlobalLength - mLength - mGlobalIndexOffset;
+  NS_ENSURE_TRUE(extraLength >= 0, NS_ERROR_UNEXPECTED);
+
+  if (extraLength != mEntriesInFollowingPartialHistories) {
+    mEntriesInFollowingPartialHistories = extraLength;
+  }
+
+  if (mIndex == aTargetIndex) {
+    // TODO When we finish OnPartialSessionHistoryDeactive, we'll need to active
+    // the suspended document here.
+
+    // Fire location change to update canGoBack / canGoForward.
+    NS_DispatchToCurrentThread(NewRunnableMethod(static_cast<nsDocShell*>(mRootDocShell),
+                                                 &nsDocShell::FireDummyOnLocationChange));
+    return NS_OK;
+  }
+
+  return LoadEntry(aTargetIndex, nsIDocShellLoadInfo::loadHistory,
+                   HIST_CMD_GOTOINDEX);
+}
+
+NS_IMETHODIMP
+nsSHistory::OnPartialSessionHistoryDeactive()
+{
+  NS_ENSURE_TRUE(mIsPartial, NS_ERROR_UNEXPECTED);
+
+  // TODO We need to suspend current document first. Much like what happens when
+  // loading a new page. Move the ownership of the document to nsISHEntry or so.
+  return NS_OK;
+}
+
 /* Get index of the history list */
 NS_IMETHODIMP
 nsSHistory::GetIndex(int32_t* aResult)
 {
   NS_PRECONDITION(aResult, "null out param?");
   *aResult = mIndex;
   return NS_OK;
 }
@@ -696,16 +764,20 @@ nsSHistory::PurgeHistory(int32_t aEntrie
     mListRoot = nextTxn;
     if (mListRoot) {
       mListRoot->SetPrev(nullptr);
     }
     cnt++;
   }
   mLength -= cnt;
   mIndex -= cnt;
+  NOTIFY_LISTENERS(OnLengthChange, (mLength));
+
+  // All following partial histories will be deleted in this case.
+  mEntriesInFollowingPartialHistories = 0;
 
   // Now if we were not at the end of the history, mIndex could have
   // become far too negative.  If so, just set it to -1.
   if (mIndex < -1) {
     mIndex = -1;
   }
 
   if (mRootDocShell) {
@@ -737,16 +809,23 @@ nsSHistory::RemoveSHistoryListener(nsISH
 {
   // Make sure the listener that wants to be removed is the
   // one we have in store.
   nsWeakPtr listener = do_GetWeakReference(aListener);
   mListeners.RemoveElement(listener);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsSHistory::SetPartialSHistoryListener(nsIPartialSHistoryListener* aListener)
+{
+  mPartialHistoryListener = do_GetWeakReference(aListener);
+  return NS_OK;
+}
+
 /* Replace an entry in the History list at a particular index.
  * Do not update index or count.
  */
 NS_IMETHODIMP
 nsSHistory::ReplaceEntry(int32_t aIndex, nsISHEntry* aReplaceEntry)
 {
   NS_ENSURE_ARG(aReplaceEntry);
   nsresult rv;
@@ -803,43 +882,53 @@ nsSHistory::EvictAllContentViewers()
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::GetCanGoBack(bool* aCanGoBack)
 {
   NS_ENSURE_ARG_POINTER(aCanGoBack);
-  *aCanGoBack = false;
+
+  if (mGlobalIndexOffset) {
+    *aCanGoBack = true;
+    return NS_OK;
+  }
 
   int32_t index = -1;
   NS_ENSURE_SUCCESS(GetIndex(&index), NS_ERROR_FAILURE);
   if (index > 0) {
     *aCanGoBack = true;
+    return NS_OK;
   }
 
+  *aCanGoBack = false;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::GetCanGoForward(bool* aCanGoForward)
 {
   NS_ENSURE_ARG_POINTER(aCanGoForward);
-  *aCanGoForward = false;
+
+  if (mEntriesInFollowingPartialHistories) {
+    *aCanGoForward = true;
+    return NS_OK;
+  }
 
   int32_t index = -1;
   int32_t count = -1;
-
   NS_ENSURE_SUCCESS(GetIndex(&index), NS_ERROR_FAILURE);
   NS_ENSURE_SUCCESS(GetCount(&count), NS_ERROR_FAILURE);
-
   if (index >= 0 && index < (count - 1)) {
     *aCanGoForward = true;
+    return NS_OK;
   }
 
+  *aCanGoForward = false;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::GoBack()
 {
   bool canGoBack = false;
 
@@ -1353,16 +1442,18 @@ nsSHistory::RemoveDuplicate(int32_t aInd
     // NB: We don't need to guard on mRequestedIndex being nonzero here,
     // because either they're strictly greater than aIndex which is at least
     // zero, or they are equal to aIndex in which case aKeepNext must be true
     // if aIndex is zero.
     if (mRequestedIndex > aIndex || (mRequestedIndex == aIndex && !aKeepNext)) {
       mRequestedIndex = mRequestedIndex - 1;
     }
     --mLength;
+    mEntriesInFollowingPartialHistories = 0;
+    NOTIFY_LISTENERS(OnLengthChange, (mLength));
     return true;
   }
   return false;
 }
 
 NS_IMETHODIMP_(void)
 nsSHistory::RemoveEntries(nsTArray<uint64_t>& aIDs, int32_t aStartIndex)
 {
@@ -1508,19 +1599,21 @@ nsSHistory::LoadURI(const char16_t* aURI
                     nsIURI* aReferringURI,
                     nsIInputStream* aPostStream,
                     nsIInputStream* aExtraHeaderStream)
 {
   return NS_OK;
 }
 
 NS_IMETHODIMP
-nsSHistory::GotoIndex(int32_t aIndex)
+nsSHistory::GotoIndex(int32_t aGlobalIndex)
 {
-  return LoadEntry(aIndex, nsIDocShellLoadInfo::loadHistory,
+  // We provide abstraction of grouped session history for nsIWebNavigation
+  // functions, so the index passed in here is global index.
+  return LoadEntry(aGlobalIndex - mGlobalIndexOffset, nsIDocShellLoadInfo::loadHistory,
                    HIST_CMD_GOTOINDEX);
 }
 
 nsresult
 nsSHistory::LoadNextPossibleEntry(int32_t aNewIndex, long aLoadType,
                                   uint32_t aHistCmd)
 {
   mRequestedIndex = -1;
@@ -1535,16 +1628,36 @@ nsSHistory::LoadNextPossibleEntry(int32_
 
 NS_IMETHODIMP
 nsSHistory::LoadEntry(int32_t aIndex, long aLoadType, uint32_t aHistCmd)
 {
   if (!mRootDocShell) {
     return NS_ERROR_FAILURE;
   }
 
+  if (aIndex < 0 || aIndex >= mLength) {
+    if (aIndex + mGlobalIndexOffset < 0) {
+      // The global index is negative.
+      return NS_ERROR_FAILURE;
+    }
+
+    if (aIndex - mLength >= mEntriesInFollowingPartialHistories) {
+      // The global index exceeds max possible value.
+      return NS_ERROR_FAILURE;
+    }
+
+    // The global index is valid. trigger cross browser navigation.
+    nsCOMPtr<nsIPartialSHistoryListener> listener =
+      do_QueryReferent(mPartialHistoryListener);
+    if (!listener) {
+      return NS_ERROR_FAILURE;
+    }
+    return listener->OnRequestCrossBrowserNavigation(aIndex + mGlobalIndexOffset);
+  }
+
   // Keep note of requested history index in mRequestedIndex.
   mRequestedIndex = aIndex;
 
   nsCOMPtr<nsISHEntry> prevEntry;
   GetEntryAtIndex(mIndex, false, getter_AddRefs(prevEntry));
 
   nsCOMPtr<nsISHEntry> nextEntry;
   GetEntryAtIndex(mRequestedIndex, false, getter_AddRefs(nextEntry));
@@ -1750,16 +1863,36 @@ NS_IMETHODIMP
 nsSHistory::GetSHistoryEnumerator(nsISimpleEnumerator** aEnumerator)
 {
   NS_ENSURE_ARG_POINTER(aEnumerator);
   RefPtr<nsSHEnumerator> iterator = new nsSHEnumerator(this);
   iterator.forget(aEnumerator);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsSHistory::OnAttachGroupedSessionHistory(int32_t aOffset)
+{
+  NS_ENSURE_TRUE(!mIsPartial, NS_ERROR_UNEXPECTED);
+  NS_ENSURE_TRUE(aOffset >= 0, NS_ERROR_ILLEGAL_VALUE);
+
+  mIsPartial = true;
+  mGlobalIndexOffset = aOffset;
+
+  // The last attached history is always at the end of the group.
+  mEntriesInFollowingPartialHistories = 0;
+
+  // Setting grouped history info may change canGoBack / canGoForward.
+  // Send a location change to update these values.
+  NS_DispatchToCurrentThread(NewRunnableMethod(static_cast<nsDocShell*>(mRootDocShell),
+                                               &nsDocShell::FireDummyOnLocationChange));
+  return NS_OK;
+
+}
+
 nsSHEnumerator::nsSHEnumerator(nsSHistory* aSHistory) : mIndex(-1)
 {
   mSHistory = aSHistory;
 }
 
 nsSHEnumerator::~nsSHEnumerator()
 {
   mSHistory = nullptr;
--- a/docshell/shistory/nsSHistory.h
+++ b/docshell/shistory/nsSHistory.h
@@ -9,16 +9,17 @@
 
 #include "nsCOMPtr.h"
 #include "nsISHistory.h"
 #include "nsISHistoryInternal.h"
 #include "nsIWebNavigation.h"
 #include "nsISimpleEnumerator.h"
 #include "nsTObserverArray.h"
 #include "nsWeakPtr.h"
+#include "nsIPartialSHistoryListener.h"
 
 #include "prclist.h"
 
 class nsIDocShell;
 class nsSHEnumerator;
 class nsSHistoryObserver;
 class nsISHEntry;
 class nsISHTransaction;
@@ -41,17 +42,17 @@ public:
   static void UpdatePrefs();
 
   // Max number of total cached content viewers.  If the pref
   // browser.sessionhistory.max_total_viewers is negative, then
   // this value is calculated based on the total amount of memory.
   // Otherwise, it comes straight from the pref.
   static uint32_t GetMaxTotalViewers() { return sHistoryMaxTotalViewers; }
 
-protected:
+private:
   virtual ~nsSHistory();
   friend class nsSHEnumerator;
   friend class nsSHistoryObserver;
 
   // Could become part of nsIWebNavigation
   NS_IMETHOD GetTransactionAtIndex(int32_t aIndex, nsISHTransaction** aResult);
   nsresult LoadDifferingEntries(nsISHEntry* aPrevEntry, nsISHEntry* aNextEntry,
                                 nsIDocShell* aRootDocShell, long aLoadType,
@@ -75,28 +76,41 @@ protected:
   // content viewers to cache, based on amount of total memory
   static uint32_t CalcMaxTotalViewers();
 
   void RemoveDynEntries(int32_t aOldIndex, int32_t aNewIndex);
 
   nsresult LoadNextPossibleEntry(int32_t aNewIndex, long aLoadType,
                                  uint32_t aHistCmd);
 
-protected:
   // aIndex is the index of the transaction which may be removed.
   // If aKeepNext is true, aIndex is compared to aIndex + 1,
   // otherwise comparison is done to aIndex - 1.
   bool RemoveDuplicate(int32_t aIndex, bool aKeepNext);
 
   nsCOMPtr<nsISHTransaction> mListRoot;
   int32_t mIndex;
   int32_t mLength;
   int32_t mRequestedIndex;
+
+  // Set to true if attached to a grouped session history.
+  bool mIsPartial;
+
+  // The number of entries before this session history object.
+  int32_t mGlobalIndexOffset;
+
+  // The number of entries after this session history object.
+  int32_t mEntriesInFollowingPartialHistories;
+
   // Session History listeners
   nsAutoTObserverArray<nsWeakPtr, 2> mListeners;
+
+  // Partial session history listener
+  nsWeakPtr mPartialHistoryListener;
+
   // Weak reference. Do not refcount this.
   nsIDocShell* mRootDocShell;
 
   // Max viewers allowed total, across all SHistory objects
   static int32_t sHistoryMaxTotalViewers;
 };
 
 class nsSHEnumerator : public nsISimpleEnumerator
new file mode 100644
--- /dev/null
+++ b/dom/base/GroupedSHistory.cpp
@@ -0,0 +1,172 @@
+/* -*- 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 "GroupedSHistory.h"
+#include "TabParent.h"
+#include "PartialSHistory.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION(GroupedSHistory, mPartialHistories)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(GroupedSHistory)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(GroupedSHistory)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GroupedSHistory)
+  NS_INTERFACE_MAP_ENTRY(nsIGroupedSHistory)
+  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIGroupedSHistory)
+NS_INTERFACE_MAP_END
+
+GroupedSHistory::GroupedSHistory()
+  : mCount(0),
+    mIndexOfActivePartialHistory(-1)
+{
+}
+
+NS_IMETHODIMP
+GroupedSHistory::GetCount(uint32_t* aResult)
+{
+  MOZ_ASSERT(aResult);
+  *aResult = mCount;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+GroupedSHistory::AppendPartialSessionHistory(nsIPartialSHistory* aPartialHistory)
+{
+  if (!aPartialHistory) {
+    return NS_ERROR_INVALID_POINTER;
+  }
+
+  nsCOMPtr<nsIPartialSHistory> partialHistory(aPartialHistory);
+  if (!partialHistory || mPartialHistories.Contains(partialHistory)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Remove all items after active one and deactive it, unless it's the first
+  // call and no active partial history has been set yet.
+  if (mIndexOfActivePartialHistory >= 0) {
+    PurgePartialHistories(mIndexOfActivePartialHistory);
+    nsCOMPtr<nsIPartialSHistory> prevPartialHistory =
+      mPartialHistories[mIndexOfActivePartialHistory];
+    if (NS_WARN_IF(!prevPartialHistory)) {
+      // Cycle collected?
+      return NS_ERROR_UNEXPECTED;
+    }
+    prevPartialHistory->OnDeactive();
+  }
+
+  // Attach the partial history.
+  uint32_t offset = mCount;
+  mCount += partialHistory->GetCount();
+  mPartialHistories.AppendElement(partialHistory);
+  partialHistory->OnAttachGroupedSessionHistory(offset);
+  mIndexOfActivePartialHistory = mPartialHistories.Count() - 1;
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+GroupedSHistory::OnPartialSessionHistoryChange(
+  nsIPartialSHistory* aPartialSessionHistory)
+{
+  if (!aPartialSessionHistory) {
+    return NS_ERROR_INVALID_POINTER;
+  }
+
+  nsCOMPtr<nsIPartialSHistory> partialHistory(aPartialSessionHistory);
+  int32_t index = mPartialHistories.IndexOf(partialHistory);
+  if (NS_WARN_IF(index != mIndexOfActivePartialHistory) ||
+      NS_WARN_IF(index < 0)) {
+    // Non-active or not attached partialHistory
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  PurgePartialHistories(index);
+
+  // Update global count.
+  uint32_t count = partialHistory->GetCount();
+  uint32_t offset = partialHistory->GetGlobalIndexOffset();
+  mCount = count + offset;
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+GroupedSHistory::GotoIndex(uint32_t aGlobalIndex,
+                           nsIFrameLoader** aTargetLoaderToSwap)
+{
+  nsCOMPtr<nsIPartialSHistory> currentPartialHistory =
+    mPartialHistories[mIndexOfActivePartialHistory];
+  if (!currentPartialHistory) {
+    // Cycle collected?
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  for (uint32_t i = 0; i < mPartialHistories.Length(); i++) {
+    nsCOMPtr<nsIPartialSHistory> partialHistory = mPartialHistories[i];
+    if (NS_WARN_IF(!partialHistory)) {
+      // Cycle collected?
+      return NS_ERROR_UNEXPECTED;
+    }
+
+    // Examine index range.
+    uint32_t offset = partialHistory->GetGlobalIndexOffset();
+    uint32_t count = partialHistory->GetCount();
+    if (offset <= aGlobalIndex && (offset + count) > aGlobalIndex) {
+      uint32_t targetIndex = aGlobalIndex - offset;
+      partialHistory->GetOwnerFrameLoader(aTargetLoaderToSwap);
+      if ((size_t)mIndexOfActivePartialHistory == i) {
+        return NS_OK;
+      }
+      mIndexOfActivePartialHistory = i;
+      if (NS_FAILED(currentPartialHistory->OnDeactive()) ||
+          NS_FAILED(partialHistory->OnActive(mCount, targetIndex))) {
+        return NS_ERROR_FAILURE;
+      }
+      return NS_OK;
+    }
+  }
+
+  // Index not found.
+  NS_WARNING("Out of index request!");
+  return NS_ERROR_FAILURE;
+}
+
+void
+GroupedSHistory::PurgePartialHistories(uint32_t aLastPartialIndexToKeep)
+{
+  uint32_t lastIndex = mPartialHistories.Length() - 1;
+  if (aLastPartialIndexToKeep >= lastIndex) {
+    // Nothing to remove.
+    return;
+  }
+
+  // Close tabs.
+  for (uint32_t i = lastIndex; i > aLastPartialIndexToKeep; i--) {
+    nsCOMPtr<nsIPartialSHistory> partialHistory = mPartialHistories[i];
+    if (!partialHistory) {
+      // Cycle collected?
+      return;
+    }
+
+    nsCOMPtr<nsIFrameLoader> loader;
+    partialHistory->GetOwnerFrameLoader(getter_AddRefs(loader));
+    loader->RequestFrameLoaderClose();
+  }
+
+  // Remove references.
+  mPartialHistories.RemoveElementsAt(aLastPartialIndexToKeep + 1,
+                                     lastIndex - aLastPartialIndexToKeep);
+}
+
+/* static */ bool
+GroupedSHistory::GroupedHistoryEnabled() {
+  return Preferences::GetBool("browser.groupedhistory.enabled", false);
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/GroupedSHistory.h
@@ -0,0 +1,105 @@
+/* -*- 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 GroupedSHistory_h
+#define GroupedSHistory_h
+
+#include "nsIFrameLoader.h"
+#include "nsIGroupedSHistory.h"
+#include "nsIPartialSHistory.h"
+#include "nsTArray.h"
+#include "nsWeakReference.h"
+
+namespace mozilla {
+namespace dom {
+
+
+/**
+ * GroupedSHistory connects session histories across multiple frameloaders.
+ * Each frameloader has a PartialSHistory, and GroupedSHistory has an array
+ * refering to all participating PartialSHistory(s).
+ *
+ * The following figure illustrates the idea. In this case, the GroupedSHistory
+ * is composed of 3 frameloaders, and the active one is frameloader 1.
+ * GroupedSHistory is always attached to the active frameloader.
+ *
+ *            +----------------------------------------------------+
+ *            |                                                    |
+ *            |                                                    v
+ *  +------------------+      +-------------------+       +-----------------+
+ *  |  FrameLoader 1   |      | PartialSHistory 1 |       | GroupedSHistory |
+ *  |     (active)     |----->|     (active)      |<--+---|                 |
+ *  +------------------+      +-------------------+   |   +-----------------+
+ *                                                    |
+ *  +------------------+      +-------------------+   |
+ *  |  FrameLoader 2   |      | PartialSHistory 2 |   |
+ *  |    (inactive)    |----->|    (inactive)     |<--+
+ *  +------------------+      +-------------------+   |
+ *                                                    |
+ *  +------------------+      +-------------------+   |
+ *  |  FrameLoader 3   |      | PartialSHistory 3 |   |
+ *  |    (inactive)    |----->|    (inactive)     |<--+
+ *  +------------------+      +-------------------+
+ *
+ * If a history navigation leads to frameloader 3, it becomes the active one,
+ * and GroupedSHistory is re-attached to frameloader 3.
+ *
+ *  +------------------+      +-------------------+
+ *  |  FrameLoader 1   |      | PartialSHistory 1 |
+ *  |    (inactive)    |----->|    (inactive)     |<--+
+ *  +------------------+      +-------------------+   |
+ *                                                    |
+ *  +------------------+      +-------------------+   |
+ *  |  FrameLoader 2   |      | PartialSHistory 2 |   |
+ *  |    (inactive)    |----->|    (inactive)     |<--+
+ *  +------------------+      +-------------------+   |
+ *                                                    |
+ *  +------------------+      +-------------------+   |   +-----------------+
+ *  |  FrameLoader 3   |      | PartialSHistory 3 |   |   | GroupedSHistory |
+ *  |     (active)     |----->|     (active)      |<--+---|                 |
+ *  +------------------+      +-------------------+       +-----------------+
+ *            |                                                    ^
+ *            |                                                    |
+ *            +----------------------------------------------------+
+ */
+class GroupedSHistory final : public nsIGroupedSHistory
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS(GroupedSHistory)
+  NS_DECL_NSIGROUPEDSHISTORY
+  GroupedSHistory();
+
+  /**
+   * Get the value of preference "browser.groupedhistory.enabled" to determine
+   * if grouped session history should be enabled.
+   */
+  static bool GroupedHistoryEnabled();
+
+private:
+  ~GroupedSHistory() {}
+
+  /**
+   * Remove all partial histories and close tabs after the given index (of
+   * mPartialHistories, not the index of session history entries).
+   */
+  void PurgePartialHistories(uint32_t aLastPartialIndexToKeep);
+
+  // The total number of entries in all partial histories.
+  uint32_t mCount;
+
+  // The index of currently active partial history in mPartialHistories.
+  // Use int32_t as we have invalid index and nsCOMArray also uses int32_t.
+  int32_t mIndexOfActivePartialHistory;
+
+  // All participating nsIPartialSHistory objects.
+  nsCOMArray<nsIPartialSHistory> mPartialHistories;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* GroupedSHistory_h */
new file mode 100644
--- /dev/null
+++ b/dom/base/PartialSHistory.cpp
@@ -0,0 +1,292 @@
+/* -*- 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 "PartialSHistory.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION(PartialSHistory, mOwnerFrameLoader)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PartialSHistory)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PartialSHistory)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PartialSHistory)
+  NS_INTERFACE_MAP_ENTRY(nsIPartialSHistory)
+  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPartialSHistory)
+  NS_INTERFACE_MAP_ENTRY(nsISHistoryListener)
+  NS_INTERFACE_MAP_ENTRY(nsIPartialSHistoryListener)
+  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END
+
+PartialSHistory::PartialSHistory(nsIFrameLoader* aOwnerFrameLoader)
+  : mCount(0),
+    mGlobalIndexOffset(0),
+    mOwnerFrameLoader(aOwnerFrameLoader)
+{
+  MOZ_ASSERT(aOwnerFrameLoader);
+}
+
+already_AddRefed<nsISHistory>
+PartialSHistory::GetSessionHistory()
+{
+  if (!mOwnerFrameLoader) {
+    // Cycle collected?
+    return nullptr;
+  }
+
+  nsCOMPtr<nsIDocShell> docShell;
+  mOwnerFrameLoader->GetDocShell(getter_AddRefs(docShell));
+  if (!docShell) {
+    return nullptr;
+  }
+
+  nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(docShell));
+  nsCOMPtr<nsISHistory> shistory;
+  webNav->GetSessionHistory(getter_AddRefs(shistory));
+  return shistory.forget();
+}
+
+already_AddRefed<TabParent>
+PartialSHistory::GetTabParent()
+{
+  if (!mOwnerFrameLoader) {
+    // Cycle collected?
+    return nullptr;
+  }
+
+  nsCOMPtr<nsITabParent> tabParent;
+  mOwnerFrameLoader->GetTabParent(getter_AddRefs(tabParent));
+  return RefPtr<TabParent>(static_cast<TabParent*>(tabParent.get())).forget();
+}
+
+NS_IMETHODIMP
+PartialSHistory::GetCount(uint32_t* aResult)
+{
+  if (!aResult) {
+    return NS_ERROR_INVALID_POINTER;
+  }
+
+  // If we have direct reference to nsISHistory, simply pass through.
+  nsCOMPtr<nsISHistory> shistory(GetSessionHistory());
+  if (shistory) {
+    int32_t count;
+    nsresult rv = shistory->GetCount(&count);
+    if (NS_FAILED(rv) || count < 0) {
+      *aResult = 0;
+      return NS_ERROR_FAILURE;
+    }
+    *aResult = count;
+    return NS_OK;
+  }
+
+  // Otherwise use the cached value.
+  *aResult = mCount;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PartialSHistory::GetGlobalIndexOffset(uint32_t* aResult)
+{
+  if (!aResult) {
+    return NS_ERROR_INVALID_POINTER;
+  }
+
+  // If we have direct reference to nsISHistory, simply pass through.
+  nsCOMPtr<nsISHistory> shistory(GetSessionHistory());
+  if (shistory) {
+    int32_t offset;
+    nsresult rv = shistory->GetGlobalIndexOffset(&offset);
+    if (NS_FAILED(rv) || offset < 0) {
+      *aResult = 0;
+      return NS_ERROR_FAILURE;
+    }
+    *aResult = offset;
+    return NS_OK;
+  }
+
+  // Otherwise use the cached value.
+  *aResult = mGlobalIndexOffset;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PartialSHistory::GetOwnerFrameLoader(nsIFrameLoader** aResult)
+{
+  nsCOMPtr<nsIFrameLoader> loader(mOwnerFrameLoader);
+  loader.forget(aResult);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PartialSHistory::OnAttachGroupedSessionHistory(uint32_t aOffset)
+{
+  mGlobalIndexOffset = aOffset;
+
+  // If we have direct reference to nsISHistory, simply pass through.
+  nsCOMPtr<nsISHistory> shistory(GetSessionHistory());
+  if (shistory) {
+    // nsISHistory uses int32_t
+    if (aOffset > INT32_MAX) {
+      return NS_ERROR_FAILURE;
+    }
+    return shistory->OnAttachGroupedSessionHistory(aOffset);
+  }
+
+  // Otherwise notify through TabParent.
+  RefPtr<TabParent> tabParent(GetTabParent());
+  if (!tabParent) {
+    // We have neither shistory nor tabParent?
+    NS_WARNING("Unable to get shitory nor tabParent!");
+    return NS_ERROR_UNEXPECTED;
+  }
+  Unused << tabParent->SendNotifyAttachGroupedSessionHistory(aOffset);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PartialSHistory::OnSessionHistoryChange(uint32_t aCount)
+{
+  mCount = aCount;
+  return OnLengthChange(aCount);
+}
+
+NS_IMETHODIMP
+PartialSHistory::OnActive(uint32_t aGlobalLength, uint32_t aTargetLocalIndex)
+{
+  // In-process case.
+  nsCOMPtr<nsISHistory> shistory(GetSessionHistory());
+  if (shistory) {
+    // nsISHistory uses int32_t
+    if (aGlobalLength > INT32_MAX || aTargetLocalIndex > INT32_MAX) {
+      return NS_ERROR_FAILURE;
+    }
+    return shistory->OnPartialSessionHistoryActive(aGlobalLength,
+                                                   aTargetLocalIndex);
+  }
+
+  // Cross-process case.
+  RefPtr<TabParent> tabParent(GetTabParent());
+  if (!tabParent) {
+    // We have neither shistory nor tabParent?
+    NS_WARNING("Unable to get shitory nor tabParent!");
+    return NS_ERROR_UNEXPECTED;
+  }
+  Unused << tabParent->SendNotifyPartialSessionHistoryActive(aGlobalLength,
+                                                             aTargetLocalIndex);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PartialSHistory::OnDeactive()
+{
+  // In-process case.
+  nsCOMPtr<nsISHistory> shistory(GetSessionHistory());
+  if (shistory) {
+    if (NS_FAILED(shistory->OnPartialSessionHistoryDeactive())) {
+      return NS_ERROR_FAILURE;
+    }
+    return NS_OK;
+  }
+
+  // Cross-process case.
+  RefPtr<TabParent> tabParent(GetTabParent());
+  if (!tabParent) {
+    // We have neither shistory nor tabParent?
+    NS_WARNING("Unable to get shitory nor tabParent!");
+    return NS_ERROR_UNEXPECTED;
+  }
+  Unused << tabParent->SendNotifyPartialSessionHistoryDeactive();
+  return NS_OK;
+}
+
+/*******************************************************************************
+ * nsIPartialSHistoryListener
+ ******************************************************************************/
+
+NS_IMETHODIMP
+PartialSHistory::OnRequestCrossBrowserNavigation(uint32_t aIndex)
+{
+  if (!mOwnerFrameLoader) {
+    // Cycle collected?
+    return NS_ERROR_UNEXPECTED;
+  }
+  return mOwnerFrameLoader->RequestGroupedHistoryNavigation(aIndex);
+}
+
+/*******************************************************************************
+ * nsISHistoryListener
+ ******************************************************************************/
+
+NS_IMETHODIMP
+PartialSHistory::OnLengthChange(int32_t aCount)
+{
+  if (!mOwnerFrameLoader) {
+    // Cycle collected?
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  if (aCount < 0) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsCOMPtr<nsIGroupedSHistory> groupedHistory;
+  mOwnerFrameLoader->GetGroupedSessionHistory(getter_AddRefs(groupedHistory));
+  if (!groupedHistory) {
+    // Maybe we're not the active partial history, but in this case we shouldn't
+    // receive any update from session history object either.
+    return NS_ERROR_FAILURE;
+  }
+
+  groupedHistory->OnPartialSessionHistoryChange(this);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PartialSHistory::OnHistoryNewEntry(nsIURI *aNewURI, int32_t aOldIndex)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+PartialSHistory::OnHistoryGoBack(nsIURI *aBackURI, bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+PartialSHistory::OnHistoryGoForward(nsIURI *aForwardURI, bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+PartialSHistory::OnHistoryReload(nsIURI *aReloadURI, uint32_t aReloadFlags, bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+PartialSHistory::OnHistoryGotoIndex(int32_t aIndex, nsIURI *aGotoURI, bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+PartialSHistory::OnHistoryPurge(int32_t aNumEntries, bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+PartialSHistory::OnHistoryReplaceEntry(int32_t aIndex)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/PartialSHistory.h
@@ -0,0 +1,61 @@
+/* -*- 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 PartialSHistory_h
+#define PartialSHistory_h
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsFrameLoader.h"
+#include "nsIGroupedSHistory.h"
+#include "nsIPartialSHistoryListener.h"
+#include "nsIPartialSHistory.h"
+#include "nsISHistory.h"
+#include "nsISHistoryListener.h"
+#include "TabParent.h"
+
+namespace mozilla {
+namespace dom {
+
+class PartialSHistory final : public nsIPartialSHistory,
+                              public nsISHistoryListener,
+                              public nsIPartialSHistoryListener,
+                              public nsSupportsWeakReference
+{
+public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(PartialSHistory, nsIPartialSHistory)
+  NS_DECL_NSIPARTIALSHISTORY
+  NS_DECL_NSIPARTIALSHISTORYLISTENER
+  NS_DECL_NSISHISTORYLISTENER
+
+  /**
+   * Note that PartialSHistory must be constructed after frameloader has
+   * created a valid docshell or tabparent.
+   */
+  explicit PartialSHistory(nsIFrameLoader* aOwnerFrameLoader);
+
+private:
+  ~PartialSHistory() {}
+  already_AddRefed<nsISHistory> GetSessionHistory();
+  already_AddRefed<TabParent> GetTabParent();
+
+  // The cache of number of entries in corresponding nsISHistory. It's only
+  // used for remote process case. If nsISHistory is in-process, mCount will not
+  // be used at all.
+  uint32_t mCount;
+
+  // The cache of globalIndexOffset in corresponding nsISHistory. It's only
+  // used for remote process case.
+  uint32_t mGlobalIndexOffset;
+
+  // The frameloader which owns this PartialSHistory.
+  nsCOMPtr<nsIFrameLoader> mOwnerFrameLoader;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* PartialSHistory_h */
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -178,28 +178,30 @@ EXPORTS.mozilla.dom += [
     'ElementInlines.h',
     'EventSource.h',
     'File.h',
     'FileList.h',
     'FileReader.h',
     'FormData.h',
     'FragmentOrElement.h',
     'FromParser.h',
+    'GroupedSHistory.h',
     'ImageEncoder.h',
     'ImageTracker.h',
     'ImportManager.h',
     'Link.h',
     'Location.h',
     'MutableBlobStorage.h',
     'MutableBlobStreamListener.h',
     'NameSpaceConstants.h',
     'Navigator.h',
     'NodeInfo.h',
     'NodeInfoInlines.h',
     'NodeIterator.h',
+    'PartialSHistory.h',
     'ProcessGlobal.h',
     'ResponsiveImageSelector.h',
     'SameProcessMessageQueue.h',
     'ScreenOrientation.h',
     'ScriptSettings.h',
     'ShadowRoot.h',
     'StructuredCloneHolder.h',
     'StructuredCloneTags.h',
@@ -242,16 +244,17 @@ UNIFIED_SOURCES += [
     'DOMStringList.cpp',
     'Element.cpp',
     'EventSource.cpp',
     'File.cpp',
     'FileList.cpp',
     'FileReader.cpp',
     'FormData.cpp',
     'FragmentOrElement.cpp',
+    'GroupedSHistory.cpp',
     'ImageEncoder.cpp',
     'ImageTracker.cpp',
     'ImportManager.cpp',
     'Link.cpp',
     'Location.cpp',
     'MultipartBlobImpl.cpp',
     'MutableBlobStorage.cpp',
     'MutableBlobStreamListener.cpp',
@@ -328,16 +331,17 @@ UNIFIED_SOURCES += [
     'nsTreeSanitizer.cpp',
     'nsViewportInfo.cpp',
     'nsWindowMemoryReporter.cpp',
     'nsWindowRoot.cpp',
     'nsWrapperCache.cpp',
     'nsXHTMLContentSerializer.cpp',
     'nsXMLContentSerializer.cpp',
     'nsXMLNameSpaceMap.cpp',
+    'PartialSHistory.cpp',
     'PostMessageEvent.cpp',
     'ProcessGlobal.cpp',
     'ResponsiveImageSelector.cpp',
     'SameProcessMessageQueue.cpp',
     'ScreenOrientation.cpp',
     'ScriptSettings.cpp',
     'ShadowRoot.cpp',
     'StructuredCloneHolder.cpp',
--- a/dom/base/nsFrameLoader.cpp
+++ b/dom/base/nsFrameLoader.cpp
@@ -53,16 +53,18 @@
 #include "nsIMozBrowserFrame.h"
 #include "nsISHistory.h"
 #include "nsNullPrincipal.h"
 #include "nsIScriptError.h"
 #include "nsGlobalWindow.h"
 #include "nsPIWindowRoot.h"
 #include "nsLayoutUtils.h"
 #include "nsView.h"
+#include "GroupedSHistory.h"
+#include "PartialSHistory.h"
 
 #include "nsIURI.h"
 #include "nsIURL.h"
 #include "nsNetUtil.h"
 
 #include "nsGkAtoms.h"
 #include "nsNameSpaceManager.h"
 
@@ -129,17 +131,23 @@ typedef FrameMetrics::ViewID ViewID;
 // does not count chrome frames when determining depth, nor does it
 // prevent chrome recursion.  Number is fairly arbitrary, but meant to
 // keep number of shells to a reasonable number on accidental recursion with a
 // small (but not 1) branching factor.  With large branching factors the number
 // of shells can rapidly become huge and run us out of memory.  To solve that,
 // we'd need to re-institute a fixed version of bug 98158.
 #define MAX_DEPTH_CONTENT_FRAMES 10
 
-NS_IMPL_CYCLE_COLLECTION(nsFrameLoader, mDocShell, mMessageManager, mChildMessageManager, mOpener)
+NS_IMPL_CYCLE_COLLECTION(nsFrameLoader,
+                         mDocShell,
+                         mMessageManager,
+                         mChildMessageManager,
+                         mOpener,
+                         mPartialSessionHistory,
+                         mGroupedSessionHistory)
 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFrameLoader)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFrameLoader)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsFrameLoader)
   NS_INTERFACE_MAP_ENTRY(nsIFrameLoader)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIFrameLoader)
   NS_INTERFACE_MAP_ENTRY(nsIWebBrowserPersistable)
 NS_INTERFACE_MAP_END
@@ -367,16 +375,127 @@ nsFrameLoader::MakePrerenderedLoaderActi
 
     nsresult rv = mDocShell->SetIsActive(true);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsFrameLoader::GetPartialSessionHistory(nsIPartialSHistory** aResult)
+{
+  if (mRemoteBrowser && !mPartialSessionHistory) {
+    // For remote case we can lazy initialize PartialSHistory since
+    // it doens't need to be registered as a listener to nsISHistory directly.
+    mPartialSessionHistory = new PartialSHistory(this);
+  }
+
+  nsCOMPtr<nsIPartialSHistory> partialHistory(mPartialSessionHistory);
+  partialHistory.forget(aResult);
+  return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsFrameLoader::GetGroupedSessionHistory(nsIGroupedSHistory** aResult)
+{
+  nsCOMPtr<nsIGroupedSHistory> groupedHistory(mGroupedSessionHistory);
+  groupedHistory.forget(aResult);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFrameLoader::AppendPartialSessionHistoryAndSwap(nsIFrameLoader* aOther)
+{
+  if (!aOther) {
+    return NS_ERROR_INVALID_POINTER;
+  }
+
+  nsCOMPtr<nsIGroupedSHistory> otherGroupedHistory;
+  aOther->GetGroupedSessionHistory(getter_AddRefs(otherGroupedHistory));
+  MOZ_ASSERT(!otherGroupedHistory,
+             "Cannot append a GroupedSHistory owner to another.");
+  if (otherGroupedHistory) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  // Append ourselves.
+  nsresult rv;
+  if (!mGroupedSessionHistory) {
+    mGroupedSessionHistory = new GroupedSHistory();
+    rv = mGroupedSessionHistory->AppendPartialSessionHistory(mPartialSessionHistory);
+    if (NS_FAILED(rv)) {
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  if (aOther == this) {
+    return NS_OK;
+  }
+
+  // Append the other.
+  RefPtr<nsFrameLoader> otherLoader = static_cast<nsFrameLoader*>(aOther);
+  rv = mGroupedSessionHistory->
+         AppendPartialSessionHistory(otherLoader->mPartialSessionHistory);
+  if (NS_FAILED(rv)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Swap loaders through our owner, so the owner's listeners will be correctly
+  // setup.
+  nsCOMPtr<nsIBrowser> ourBrowser = do_QueryInterface(mOwnerContent);
+  nsCOMPtr<nsIBrowser> otherBrowser = do_QueryInterface(otherLoader->mOwnerContent);
+  if (!ourBrowser || !otherBrowser) {
+    return NS_ERROR_FAILURE;
+  }
+  if (NS_FAILED(ourBrowser->SwapBrowsers(otherBrowser))) {
+    return NS_ERROR_FAILURE;
+  }
+  mGroupedSessionHistory.swap(otherLoader->mGroupedSessionHistory);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFrameLoader::RequestGroupedHistoryNavigation(uint32_t aGlobalIndex)
+{
+  if (!mGroupedSessionHistory) {
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  nsCOMPtr<nsIFrameLoader> targetLoader;
+  nsresult rv = mGroupedSessionHistory->
+                  GotoIndex(aGlobalIndex, getter_AddRefs(targetLoader));
+  if (NS_FAILED(rv)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  RefPtr<nsFrameLoader> otherLoader = static_cast<nsFrameLoader*>(targetLoader.get());
+  if (!targetLoader) {
+    return NS_ERROR_FAILURE;
+  }
+
+  if (targetLoader == this) {
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIBrowser> ourBrowser = do_QueryInterface(mOwnerContent);
+  nsCOMPtr<nsIBrowser> otherBrowser = do_QueryInterface(otherLoader->mOwnerContent);
+  if (!ourBrowser || !otherBrowser) {
+    return NS_ERROR_FAILURE;
+  }
+  if (NS_FAILED(ourBrowser->SwapBrowsers(otherBrowser))) {
+    return NS_ERROR_FAILURE;
+  }
+  mGroupedSessionHistory.swap(otherLoader->mGroupedSessionHistory);
+
+  return NS_OK;
+}
+
 nsresult
 nsFrameLoader::ReallyStartLoading()
 {
   nsresult rv = ReallyStartLoadingInternal();
   if (NS_FAILED(rv)) {
     FireErrorEvent();
   }
 
@@ -2057,16 +2176,25 @@ nsFrameLoader::MaybeCreateDocShell()
       !mOwnerContent->HasAttr(kNameSpaceID_None, nsGkAtoms::disablehistory)) {
     nsresult rv;
     nsCOMPtr<nsISHistory> sessionHistory =
       do_CreateInstance(NS_SHISTORY_CONTRACTID, &rv);
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(mDocShell));
     webNav->SetSessionHistory(sessionHistory);
+
+
+    if (GroupedSHistory::GroupedHistoryEnabled()) {
+      mPartialSessionHistory = new PartialSHistory(this);
+      nsCOMPtr<nsISHistoryListener> listener(do_QueryInterface(mPartialSessionHistory));
+      nsCOMPtr<nsIPartialSHistoryListener> partialListener(do_QueryInterface(mPartialSessionHistory));
+      sessionHistory->AddSHistoryListener(listener);
+      sessionHistory->SetPartialSHistoryListener(partialListener);
+    }
   }
 
   DocShellOriginAttributes attrs;
   if (docShell->ItemType() == mDocShell->ItemType()) {
     attrs = nsDocShell::Cast(docShell)->GetOriginAttributes();
   }
 
   // Inherit origin attributes from parent document if
@@ -3120,16 +3248,28 @@ nsFrameLoader::RequestNotifyAfterRemoteP
   if (mRemoteBrowser) {
     Unused << mRemoteBrowser->SendRequestNotifyAfterRemotePaint();
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsFrameLoader::RequestFrameLoaderClose()
+{
+  nsCOMPtr<nsIBrowser> browser = do_QueryInterface(mOwnerContent);
+  if (NS_WARN_IF(!browser)) {
+    // OwnerElement other than nsIBrowser is not supported yet.
+    return NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  return browser->CloseBrowser();
+}
+
+NS_IMETHODIMP
 nsFrameLoader::Print(uint64_t aOuterWindowID,
                      nsIPrintSettings* aPrintSettings,
                      nsIWebProgressListener* aProgressListener)
 {
 #if defined(NS_PRINTING)
   if (mRemoteBrowser) {
     RefPtr<embedding::PrintingParent> printingParent =
       mRemoteBrowser->Manager()->AsContentParent()->GetPrintingParent();
--- a/dom/base/nsFrameLoader.h
+++ b/dom/base/nsFrameLoader.h
@@ -20,16 +20,17 @@
 #include "nsIURI.h"
 #include "nsFrameMessageManager.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/Attributes.h"
 #include "nsStubMutationObserver.h"
 #include "Units.h"
 #include "nsIWebBrowserPersistable.h"
 #include "nsIFrame.h"
+#include "nsIGroupedSHistory.h"
 
 class nsIURI;
 class nsSubDocumentFrame;
 class nsView;
 class nsIInProcessContentFrameMessageManager;
 class AutoResetInShow;
 class AutoResetInFrameSwap;
 class nsITabParent;
@@ -365,16 +366,19 @@ private:
 
   // See nsIFrameLoader.idl. EVENT_MODE_NORMAL_DISPATCH automatically
   // forwards some input events to out-of-process content.
   uint32_t mEventMode;
 
   // Holds the last known size of the frame.
   mozilla::ScreenIntSize mLazySize;
 
+  nsCOMPtr<nsIPartialSHistory> mPartialSessionHistory;
+  nsCOMPtr<nsIGroupedSHistory> mGroupedSessionHistory;
+
   bool mIsPrerendered : 1;
   bool mDepthTooGreat : 1;
   bool mIsTopLevelContent : 1;
   bool mDestroyCalled : 1;
   bool mNeedsAsyncDestroy : 1;
   bool mInSwap : 1;
   bool mInShow : 1;
   bool mHideCalled : 1;
--- a/dom/base/nsIFrameLoader.idl
+++ b/dom/base/nsIFrameLoader.idl
@@ -13,16 +13,18 @@ interface nsIFrame;
 interface nsSubDocumentFrame;
 interface nsIMessageSender;
 interface nsIVariant;
 interface nsIDOMElement;
 interface nsITabParent;
 interface nsILoadContext;
 interface nsIPrintSettings;
 interface nsIWebProgressListener;
+interface nsIGroupedSHistory;
+interface nsIPartialSHistory;
 
 [scriptable, builtinclass, uuid(1645af04-1bc7-4363-8f2c-eb9679220ab1)]
 interface nsIFrameLoader : nsISupports
 {
   /**
    * Get the docshell from the frame loader.
    */
   readonly attribute nsIDocShell docShell;
@@ -67,16 +69,27 @@ interface nsIFrameLoader : nsISupports
   void setIsPrerendered();
 
   /**
    * Make the prerendered frameloader being active (and clear isPrerendered flag).
    */
   void makePrerenderedLoaderActive();
 
   /**
+   * Append partial session history from another frame loader.
+   */
+  void appendPartialSessionHistoryAndSwap(in nsIFrameLoader aOther);
+
+  /**
+   * If grouped session history is applied, use this function to navigate to
+   * an entry of session history object of another frameloader.
+   */
+  void requestGroupedHistoryNavigation(in unsigned long aGlobalIndex);
+
+  /**
    * Destroy the frame loader and everything inside it. This will
    * clear the weak owner content reference.
    */
   void destroy();
 
   /**
    * Find out whether the loader's frame is at too great a depth in
    * the frame tree.  This can be used to decide what operations may
@@ -134,16 +147,21 @@ interface nsIFrameLoader : nsISupports
   /**
    * Request that the next time a remote layer transaction has been
    * received by the Compositor, a MozAfterRemoteFrame event be sent
    * to the window.
    */
   void requestNotifyAfterRemotePaint();
 
   /**
+   * Close the window through the ownerElement.
+   */
+  void requestFrameLoaderClose();
+
+  /**
    * Print the current document.
    *
    * @param aOuterWindowID the ID of the outer window to print
    * @param aPrintSettings optional print settings to use; printSilent can be
    *                       set to prevent prompting.
    * @param aProgressListener optional print progress listener.
    */
   void print(in unsigned long long aOuterWindowID,
@@ -223,16 +241,27 @@ interface nsIFrameLoader : nsISupports
 
   /**
    * The last known height of the frame. Reading this property will not trigger
    * a reflow, and therefore may not reflect the current state of things. It
    * should only be used in asynchronous APIs where values are not guaranteed
    * to be up-to-date when received.
    */
   readonly attribute unsigned long lazyHeight;
+
+  /**
+   * The partial session history.
+   */
+  readonly attribute nsIPartialSHistory partialSessionHistory;
+
+  /**
+   * The grouped session history composed of multiple session history objects
+   * across root docshells.
+   */
+  readonly attribute nsIGroupedSHistory groupedSessionHistory;
 };
 
 %{C++
 class nsFrameLoader;
 %}
 
 native alreadyAddRefed_nsFrameLoader(already_AddRefed<nsFrameLoader>);
 
--- a/dom/base/test/chrome/chrome.ini
+++ b/dom/base/test/chrome/chrome.ini
@@ -19,16 +19,17 @@ support-files =
   fileconstructor_file.png
   frame_bug814638.xul
   frame_registerElement_content.html
   registerElement_ep.js
   host_bug814638.xul
   window_nsITextInputProcessor.xul
   title_window.xul
   window_swapFrameLoaders.xul
+  window_groupedSHistory.xul
 
 [test_bug120684.xul]
 [test_bug206691.xul]
 [test_bug289714.xul]
 [test_bug339494.xul]
 [test_bug357450.xul]
 support-files = ../file_bug357450.js
 [test_bug380418.html]
@@ -67,8 +68,9 @@ skip-if = buildapp == 'mulet'
 [test_registerElement_ep.xul]
 [test_domparsing.xul]
 [test_fileconstructor.xul]
 [test_fileconstructor_tempfile.xul]
 [test_nsITextInputProcessor.xul]
 [test_title.xul]
 [test_windowroot.xul]
 [test_swapFrameLoaders.xul]
+[test_groupedSHistory.xul]
new file mode 100644
--- /dev/null
+++ b/dom/base/test/chrome/test_groupedSHistory.xul
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+                 type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1276553
+-->
+<window title="Mozilla Bug 1276553"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        onload="loadTest();">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1276553"
+     target="_blank">Mozilla Bug 1276553</a>
+  </body>
+
+  <!-- test code goes here -->
+  <script type="application/javascript">
+  <![CDATA[
+
+  /** Test for GroupedSHistory **/
+  SimpleTest.waitForExplicitFinish();
+  function loadTest() {
+    window.open("window_groupedSHistory.xul", "", "width=360,height=240,chrome");
+  }
+
+  ]]>
+  </script>
+</window>
new file mode 100644
--- /dev/null
+++ b/dom/base/test/chrome/window_groupedSHistory.xul
@@ -0,0 +1,331 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+                 type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1276553
+-->
+<window title="Mozilla Bug 1276553"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="run();">
+
+  <!-- test code goes here -->
+  <script type="application/javascript">
+  <![CDATA[
+
+  const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components;
+  Cu.import("resource://testing-common/TestUtils.jsm");
+  Cu.import("resource://testing-common/ContentTask.jsm");
+  Cu.import("resource://testing-common/BrowserTestUtils.jsm");
+  Cu.import("resource://gre/modules/Task.jsm");
+  ContentTask.setTestScope(window.opener.wrappedJSObject);
+
+  let imports = ['SimpleTest', 'SpecialPowers', 'ok', 'is', 'info'];
+  for (let name of imports) {
+    window[name] = window.opener.wrappedJSObject[name];
+  }
+
+  /** Test for Bug 1276553 **/
+  function run() {
+    new Promise(resolve => SpecialPowers.pushPrefEnv(
+      {'set' : [[ 'browser.groupedhistory.enabled', true ]]}, resolve))
+    .then(() => test(false))
+    .then(() => test(true))
+    .then(() => {
+      window.close();
+      SimpleTest.finish();
+    });
+  }
+
+  function test(remote) {
+    let act, bg1, bg2;
+    return Promise.resolve()
+
+    // create first browser with 1 entry (which will always be the active one)
+    .then(() => info('TEST-INFO | test create active browser, remote=' + remote))
+    .then(() => createBrowser('pen', remote))
+    .then(b => act = b)
+    .then(() => verifyBrowser(act, 'pen'       /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    false      /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    false      /* partial */ ))
+
+    // create background browser 1 with 1 entry
+    .then(() => info('TEST-INFO | test create background browser 1, remote=' + remote))
+    .then(() => createBrowser('pineapple', remote))
+    .then(b => bg1 = b)
+    .then(() => verifyBrowser(bg1, 'pineapple' /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    false      /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    false      /* partial */ ))
+
+     // create background browser 2 with 2 entries
+    .then(() => info('TEST-INFO | test create background browser 2, remote=' + remote))
+    .then(() => createBrowser('apple', remote))
+    .then(b => bg2 = b)
+    .then(() => verifyBrowser(bg2, 'apple'     /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    false      /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    false      /* partial */ ))
+    .then(() => loadURI(bg2, getDummyHtml('pencil')))
+    .then(() => verifyBrowser(bg2, 'pencil'    /* title */,
+                                    1          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    false      /* partial */ ))
+
+    // merge to 2 entries pen-pineapple
+    .then(() => info('TEST-INFO | test merge history, remote=' + remote))
+    .then(() => mergeHistory(act, bg1))
+    .then(() => verifyBrowser(act, 'pineapple' /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    true       /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    true       /* partial */,
+                                    1          /* offset */,
+                                    2          /* globalLength */ ))
+
+    // merge to 4 entries pen-pineapple-apple-pencil
+    .then(() => mergeHistory(act, bg2))
+    .then(() => verifyBrowser(act, 'pencil'    /* title */,
+                                    1          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */ ))
+
+    // test go back
+    .then(() => info('TEST-INFO | test history go back, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.goBack.bind(act)))
+    .then(() => verifyBrowser(act, 'apple'     /* title */,
+                                    0          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */ ))
+    // XXX The 2nd pageshow comes from reload as current index of the active
+    // partial history remains the same
+    .then(() => wrapHistoryNavFn(act, act.goBack.bind(act), true))
+    .then(() => verifyBrowser(act, 'pineapple' /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    1          /* offset */,
+                                    4          /* globalLength */ ))
+    .then(() => wrapHistoryNavFn(act, act.goBack.bind(act), true))
+    .then(() => verifyBrowser(act, 'pen'       /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    false      /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    0          /* offset */,
+                                    4          /* globalLength */ ))
+
+    // test go forward
+    .then(() => info('TEST-INFO | test history go forward, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.goForward.bind(act), true))
+    .then(() => verifyBrowser(act, 'pineapple' /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    1          /* offset */,
+                                    4          /* globalLength */ ))
+    .then(() => wrapHistoryNavFn(act, act.goForward.bind(act), true))
+    .then(() => verifyBrowser(act, 'apple'     /* title */,
+                                    0          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */ ))
+    .then(() => wrapHistoryNavFn(act, act.goForward.bind(act)))
+    .then(() => verifyBrowser(act, 'pencil'    /* title */,
+                                    1          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */ ))
+
+    // test goto index
+    .then(() => info('TEST-INFO | test history goto index, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 0), true))
+    .then(() => verifyBrowser(act, 'pen'       /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    false      /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    0          /* offset */,
+                                    4          /* globalLength */ ))
+    // expect 2 pageshow since we're also changing mIndex of the partial history
+    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 2), true, 2))
+    .then(() => verifyBrowser(act, 'apple'     /* title */,
+                                    0          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */ ))
+    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 1), true))
+    .then(() => verifyBrowser(act, 'pineapple' /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    1          /* offset */,
+                                    4          /* globalLength */ ))
+    // expect 2 pageshow since we're also changing mIndex of the partial history
+    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 3), true, 2))
+    .then(() => verifyBrowser(act, 'pencil'    /* title */,
+                                    1          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    true       /* partial */,
+                                    2          /* offset */,
+                                    4          /* globalLength */ ))
+
+    // test history change to 3 entries pen-pineapple-banana
+    .then(() => info('TEST-INFO | test history change, remote=' + remote))
+    .then(() => wrapHistoryNavFn(act, act.gotoIndex.bind(act, 1), true))
+    .then(() => verifyBrowser(act, 'pineapple' /* title */,
+                                    0          /* index */,
+                                    1          /* length */,
+                                    true       /* canGoBack */,
+                                    true       /* canGoForward */,
+                                    true       /* partial */,
+                                    1          /* offset */,
+                                    4          /* globalLength */ ))
+    .then(() => loadURI(act, getDummyHtml('banana')))
+    .then(() => verifyBrowser(act, 'banana'    /* title */,
+                                    1          /* index */,
+                                    2          /* length */,
+                                    true       /* canGoBack */,
+                                    false      /* canGoForward */,
+                                    true       /* partial */,
+                                    1          /* offset */,
+                                    3          /* globalLength */ ))
+  }
+
+  function getDummyHtml(title) {
+    return 'data:text/html;charset=UTF-8,' +
+     '<html><head><title>' + title + '</title></head></html>'
+  }
+
+  function createBrowser(title, remote) {
+    let browser = document.createElement('browser');
+    browser.setAttribute('type', 'content');
+    browser.setAttribute('remote', remote);
+    browser.setAttribute('src', getDummyHtml(title));
+    document.getElementById('stack').appendChild(browser);
+    return BrowserTestUtils.browserLoaded(browser)
+           .then(() => {
+             browser.messageManager.loadFrameScript('data:,' +
+               'addEventListener("pageshow", () => sendAsyncMessage("test:pageshow", null), false);' +
+               'addEventListener("pagehide", () => sendAsyncMessage("test:pagehide", null), false);',
+               true);
+           })
+           .then(() => {
+             // a trick to ensure webProgress object is created for e10s case
+             ok(browser.webProgress, 'check browser.webProgress exists');
+             return browser;
+           });
+  }
+
+  function loadURI(browser, uri) {
+    let promise = BrowserTestUtils.browserLoaded(browser, false, uri);
+    browser.loadURI(uri);
+    return promise;
+  }
+
+  function mergeHistory(b1, b2) {
+    let promises = [];
+    let pagehide1, pagehide2;
+
+    // For swapping there should be a pagehide followed by a pageshow.
+    promises.push(BrowserTestUtils.waitForMessage(b1.messageManager, 'test:pagehide', msg => pagehide1 = true));
+    promises.push(BrowserTestUtils.waitForMessage(b2.messageManager, 'test:pagehide', msg => pagehide2 = true));
+    promises.push(BrowserTestUtils.waitForMessage(b1.messageManager, 'test:pageshow', msg => pagehide1));
+    promises.push(BrowserTestUtils.waitForMessage(b2.messageManager, 'test:pageshow', msg => pagehide2));
+    promises.push(Promise.resolve().then(() => {
+      let f1 = b1.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
+      let f2 = b2.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
+      f1.appendPartialSessionHistoryAndSwap(f2);
+    }));
+
+    return Promise.all(promises);
+  }
+
+  function wrapHistoryNavFn(browser, navFn, expectSwap = false, expectPageshowCount = 1) {
+    let promises = [];
+    let pagehide = false;
+    let pageshowCount = 0;
+
+    if (expectSwap) {
+      // For swapping there should be a pagehide followed by a pageshow.
+      promises.push(BrowserTestUtils.waitForMessage(browser.messageManager,
+        'test:pagehide', msg => pagehide = true));
+    }
+    promises.push(BrowserTestUtils.waitForMessage(browser.messageManager,
+      'test:pageshow', msg => {
+        // Only count events after pagehide for swapping case.
+        if (!expectSwap || pagehide) {
+          return !--expectPageshowCount;
+        }
+        return false;
+      }));
+    promises.push(Task.spawn(navFn));
+
+    return Promise.all(promises);
+  }
+
+  function verifyBrowser(browser, title, index, length, canGoBack, canGoForward,
+                         partial, offset = 0, globalLength = length) {
+    is(browser.canGoBack, canGoBack, 'check browser.canGoBack');
+    is(browser.canGoForward, canGoForward, 'check browser.canGoForward');
+    if (partial) {
+      let frameLoader = browser.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
+      is(frameLoader.groupedSessionHistory.count, globalLength, 'check groupedSHistory.count');
+    }
+
+    return ContentTask.spawn(browser,
+                             { title, index, length, canGoBack, canGoForward, partial, offset, globalLength },
+                             ({ title, index, length, canGoBack, canGoForward, partial, offset, globalLength }) => {
+      let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+      let shistory = webNav.sessionHistory;
+      is(content.document.title, title, 'check title');
+      is(webNav.canGoBack, canGoBack, 'check webNav.canGoBack');
+      is(webNav.canGoForward, canGoForward, 'check webNav.canGoForward');
+      is(shistory.index, index, 'check shistory.index');
+      is(shistory.count, length, 'check shistory.count');
+      is(shistory.isPartial, partial, 'check shistory.isPartial');
+      is(shistory.globalIndexOffset, offset, 'check shistory.globalIndexOffset');
+      is(shistory.globalCount, globalLength, 'check shistory.globalCount');
+    });
+  }
+
+  ]]>
+  </script>
+  <stack id="stack" flex="1" />
+</window>
--- a/dom/interfaces/base/moz.build
+++ b/dom/interfaces/base/moz.build
@@ -1,16 +1,17 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPIDL_SOURCES += [
     'domstubs.idl',
+    'nsIBrowser.idl',
     'nsIBrowserDOMWindow.idl',
     'nsIContentPermissionPrompt.idl',
     'nsIContentPrefService.idl',
     'nsIContentPrefService2.idl',
     'nsIContentURIGrouper.idl',
     'nsIDOMChromeWindow.idl',
     'nsIDOMClientRect.idl',
     'nsIDOMClientRectList.idl',
rename from dom/ipc/nsIBrowser.idl
rename to dom/interfaces/base/nsIBrowser.idl
--- a/dom/ipc/nsIBrowser.idl
+++ b/dom/interfaces/base/nsIBrowser.idl
@@ -19,9 +19,28 @@ interface nsIBrowser : nsISupports
    * Called by the child to inform the parent that links are dropped into
    * content area.
    *
    * @param linksCount length of links
    * @param links a flat array of url, name, and type for each link
    */
   void dropLinks(in unsigned long linksCount,
                  [array, size_is(linksCount)] in wstring links);
+
+  /**
+   * Swapping of frameloaders are usually initiated from a frameloader owner
+   * or other components operating on frameloader owners. This is done by calling
+   * swapFrameLoaders at MozFrameLoaderOwner webidl interface.
+   *
+   * This function aimed to provide the other way around -
+   * if the swapping is initiated from frameloader itself or other platform level
+   * components, it uses this interface to delegate the swapping request to
+   * frameloader owners and ask them to re-initiate frameloader swapping, so that
+   * frameloader owners such as <xul:browser> can setup their properties and /
+   * or listeners properly on swapping.
+   */
+  void swapBrowsers(in nsIBrowser aOtherBrowser);
+
+  /**
+   * Close the browser (usually means to remove a tab).
+   */
+  void closeBrowser();
 };
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -595,16 +595,33 @@ parent:
 
     // After a compositor reset, it is necessary to reconnect each layers ID to
     // the compositor of the widget that will render those layers. Note that
     // this is sync so we can ensure that messages to the window compositor
     // arrive before the TabChild attempts to use its cross-process compositor
     // bridge.
     sync EnsureLayersConnected();
 
+    /**
+     * Notify parent that one or more entries have been added / removed from
+     * the child session history.
+     *
+     * @param aCount the updated number of entries in child session history
+     */
+    async NotifySessionHistoryChange(uint32_t aCount);
+
+    /**
+     * When the session history is across multiple root docshells, this function
+     * is used to notify parent that it needs to navigate to an entry out of
+     * local index of the child.
+     *
+     * @param aGlobalIndex The global index of history entry to navigate to.
+     */
+    async RequestCrossBrowserNavigation(uint32_t aGlobalIndex);
+
 child:
     /**
      * Notify the remote browser that it has been Show()n on this
      * side, with the given |visibleRect|.  This message is expected
      * to trigger creation of the remote browser's "widget".
      *
      * |Show()| and |Move()| take IntSizes rather than Rects because
      * content processes always render to a virtual <0, 0> top-left
@@ -855,16 +872,42 @@ child:
     /**
      * Update the child with the tab's current top-level native window handle.
      * This is used by a11y objects who must expose their native window.
      *
      * @param aNewHandle The native window handle of the tab's top-level window.
      */
     async UpdateNativeWindowHandle(uintptr_t aNewHandle);
 
+    /**
+     * Called when the session history of this particular PBrowser has been
+     * attached to a grouped session history.
+     *
+     * @param aOffset           The number of entries in the grouped session
+     *                          history before this session history object.
+     */
+    async NotifyAttachGroupedSessionHistory(uint32_t aOffset);
+
+    /**
+     * Notify that the session history associated to this PBrowser has become
+     * the active history in the grouped session history.
+     *
+     * @param aGlobalLength      The up-to-date number of entries in the grouped
+     *                           session history.
+     * @param aTargetLocalIndex  The target local index to navigate to.
+     */
+    async NotifyPartialSessionHistoryActive(uint32_t aGlobalLength,
+                                            uint32_t aTargetLocalIndex);
+
+    /**
+     * Notify that the session history asssociates to this PBrowser has become
+     * an inactive history in the grouped session history.
+     */
+    async NotifyPartialSessionHistoryDeactive();
+
 /*
  * FIXME: write protocol!
 
 state LIVE:
     send LoadURL goto LIVE;
 //etc.
     send Destroy goto DYING;
 
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -111,16 +111,19 @@
 #include "nsIURILoader.h"
 #include "nsIScriptError.h"
 #include "mozilla/EventForwards.h"
 #include "nsDeviceContext.h"
 #include "nsSandboxFlags.h"
 #include "FrameLayerBuilder.h"
 #include "VRManagerChild.h"
 #include "nsICommandParams.h"
+#include "nsISHistory.h"
+#include "nsQueryObject.h"
+#include "GroupedSHistory.h"
 
 #ifdef NS_PRINTING
 #include "nsIPrintSession.h"
 #include "nsIPrintSettings.h"
 #include "nsIPrintSettingsService.h"
 #include "nsIWebBrowserPrint.h"
 #endif
 
@@ -138,16 +141,20 @@ using namespace mozilla::ipc;
 using namespace mozilla::layers;
 using namespace mozilla::layout;
 using namespace mozilla::docshell;
 using namespace mozilla::widget;
 using namespace mozilla::jsipc;
 using mozilla::layers::GeckoContentController;
 
 NS_IMPL_ISUPPORTS(ContentListener, nsIDOMEventListener)
+NS_IMPL_ISUPPORTS(TabChildSHistoryListener,
+                  nsISHistoryListener,
+                  nsIPartialSHistoryListener,
+                  nsISupportsWeakReference)
 
 static const CSSSize kDefaultViewportSize(980, 480);
 
 static const char BEFORE_FIRST_PAINT[] = "before-first-paint";
 
 typedef nsDataHashtable<nsUint64HashKey, TabChild*> TabChildMap;
 static TabChildMap* sTabChildren;
 
@@ -825,16 +832,30 @@ TabChild::Init()
         if (nsCOMPtr<nsITabChild> tabChild = do_QueryReferent(weakPtrThis)) {
           static_cast<TabChild*>(tabChild.get())->ContentReceivedInputBlock(aGuid, aInputBlockId, aPreventDefault);
         }
       });
   mAPZEventState = new APZEventState(mPuppetWidget, Move(callback));
 
   mIPCOpen = true;
 
+  if (GroupedSHistory::GroupedHistoryEnabled()) {
+    // Set session history listener.
+    nsCOMPtr<nsISHistory> shistory;
+    mWebNav->GetSessionHistory(getter_AddRefs(shistory));
+    if (!shistory) {
+      return NS_ERROR_FAILURE;
+    }
+    mHistoryListener = new TabChildSHistoryListener(this);
+    nsCOMPtr<nsISHistoryListener> listener(do_QueryObject(mHistoryListener));
+    shistory->AddSHistoryListener(listener);
+    nsCOMPtr<nsIPartialSHistoryListener> partialListener(do_QueryObject(mHistoryListener));
+    shistory->SetPartialSHistoryListener(partialListener);
+  }
+
   return NS_OK;
 }
 
 void
 TabChild::NotifyTabContextUpdated(bool aIsPreallocated)
 {
   nsCOMPtr<nsIDocShell> docShell = do_GetInterface(WebNavigation());
   MOZ_ASSERT(docShell);
@@ -1287,22 +1308,26 @@ TabChild::ActorDestroy(ActorDestroyReaso
 
   if (GetTabId() != 0) {
     NestedTabChildMap().erase(GetTabId());
   }
 }
 
 TabChild::~TabChild()
 {
-    DestroyWindow();
-
-    nsCOMPtr<nsIWebBrowser> webBrowser = do_QueryInterface(WebNavigation());
-    if (webBrowser) {
-      webBrowser->SetContainerWindow(nullptr);
-    }
+  DestroyWindow();
+
+  nsCOMPtr<nsIWebBrowser> webBrowser = do_QueryInterface(WebNavigation());
+  if (webBrowser) {
+    webBrowser->SetContainerWindow(nullptr);
+  }
+
+  if (mHistoryListener) {
+    mHistoryListener->ClearTabChild();
+  }
 }
 
 void
 TabChild::SetProcessNameToAppName()
 {
   nsCOMPtr<mozIApplication> app = GetOwnApp();
   if (!app) {
     return;
@@ -1837,16 +1862,58 @@ TabChild::RecvStopIMEStateManagement()
 bool
 TabChild::RecvMenuKeyboardListenerInstalled(const bool& aInstalled)
 {
   IMEStateManager::OnInstalledMenuKeyboardListener(aInstalled);
   return true;
 }
 
 bool
+TabChild::RecvNotifyAttachGroupedSessionHistory(const uint32_t& aOffset)
+{
+  // nsISHistory uses int32_t
+  if (NS_WARN_IF(aOffset > INT32_MAX)) {
+    return false;
+  }
+
+  nsCOMPtr<nsISHistory> shistory;
+  mWebNav->GetSessionHistory(getter_AddRefs(shistory));
+  NS_ENSURE_TRUE(shistory, false);
+
+  return NS_SUCCEEDED(shistory->OnAttachGroupedSessionHistory(aOffset));
+}
+
+bool
+TabChild::RecvNotifyPartialSessionHistoryActive(const uint32_t& aGlobalLength,
+                                                const uint32_t& aTargetLocalIndex)
+{
+  // nsISHistory uses int32_t
+  if (NS_WARN_IF(aGlobalLength > INT32_MAX || aTargetLocalIndex > INT32_MAX)) {
+    return false;
+  }
+
+  nsCOMPtr<nsISHistory> shistory;
+  mWebNav->GetSessionHistory(getter_AddRefs(shistory));
+  NS_ENSURE_TRUE(shistory, false);
+
+  return NS_SUCCEEDED(shistory->OnPartialSessionHistoryActive(aGlobalLength,
+                                                              aTargetLocalIndex));
+}
+
+bool
+TabChild::RecvNotifyPartialSessionHistoryDeactive()
+{
+  nsCOMPtr<nsISHistory> shistory;
+  mWebNav->GetSessionHistory(getter_AddRefs(shistory));
+  NS_ENSURE_TRUE(shistory, false);
+
+  return NS_SUCCEEDED(shistory->OnPartialSessionHistoryDeactive());
+}
+
+bool
 TabChild::RecvMouseEvent(const nsString& aType,
                          const float&    aX,
                          const float&    aY,
                          const int32_t&  aButton,
                          const int32_t&  aClickCount,
                          const int32_t&  aModifiers,
                          const bool&     aIgnoreRootScrollFrame)
 {
@@ -3340,16 +3407,90 @@ TabChild::ForcePaint(uint64_t aLayerObse
     // message on the PContent channel.
     return;
   }
 
   nsAutoScriptBlocker scriptBlocker;
   RecvSetDocShellIsActive(true, false, aLayerObserverEpoch);
 }
 
+/*******************************************************************************
+ * nsISHistoryListener
+ ******************************************************************************/
+
+NS_IMETHODIMP
+TabChildSHistoryListener::OnHistoryNewEntry(nsIURI *aNewURI, int32_t aOldIndex)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+TabChildSHistoryListener::OnHistoryGoBack(nsIURI *aBackURI, bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+TabChildSHistoryListener::OnHistoryGoForward(nsIURI *aForwardURI, bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+TabChildSHistoryListener::OnHistoryReload(nsIURI *aReloadURI, uint32_t aReloadFlags, bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+TabChildSHistoryListener::OnHistoryGotoIndex(int32_t aIndex, nsIURI *aGotoURI, bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+TabChildSHistoryListener::OnHistoryPurge(int32_t aNumEntries, bool *_retval)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+TabChildSHistoryListener::OnHistoryReplaceEntry(int32_t aIndex)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+TabChildSHistoryListener::OnLengthChange(int32_t aCount)
+{
+  RefPtr<TabChild> tabChild(mTabChild);
+  if (!tabChild) {
+    return NS_ERROR_FAILURE;
+  }
+
+  if (aCount < 0) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return tabChild->SendNotifySessionHistoryChange(aCount) ?
+           NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+TabChildSHistoryListener::OnRequestCrossBrowserNavigation(uint32_t aIndex)
+{
+  RefPtr<TabChild> tabChild(mTabChild);
+  if (!tabChild) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return tabChild->SendRequestCrossBrowserNavigation(aIndex) ?
+           NS_OK : NS_ERROR_FAILURE;
+}
+
 TabChildGlobal::TabChildGlobal(TabChildBase* aTabChild)
 : mTabChild(aTabChild)
 {
   SetIsNotDOMBinding();
 }
 
 TabChildGlobal::~TabChildGlobal()
 {
--- a/dom/ipc/TabChild.h
+++ b/dom/ipc/TabChild.h
@@ -33,16 +33,18 @@
 #include "mozilla/EventForwards.h"
 #include "mozilla/layers/CompositorTypes.h"
 #include "mozilla/layers/APZCCallbackHelper.h"
 #include "nsIWebBrowserChrome3.h"
 #include "mozilla/dom/ipc/IdType.h"
 #include "AudioChannelService.h"
 #include "PuppetWidget.h"
 #include "mozilla/layers/GeckoContentController.h"
+#include "nsISHistoryListener.h"
+#include "nsIPartialSHistoryListener.h"
 
 class nsICachedFileDescriptorListener;
 class nsIDOMWindowUtils;
 
 namespace mozilla {
 namespace layout {
 class RenderFrameChild;
 } // namespace layout
@@ -160,16 +162,37 @@ public:
   explicit ContentListener(TabChild* aTabChild) : mTabChild(aTabChild) {}
   NS_DECL_ISUPPORTS
   NS_DECL_NSIDOMEVENTLISTENER
 protected:
   ~ContentListener() {}
   TabChild* mTabChild;
 };
 
+/**
+ * Listens on session history change, and sends NotifySessionHistoryChange to
+ * parent process.
+ */
+class TabChildSHistoryListener final : public nsISHistoryListener,
+                                       public nsIPartialSHistoryListener,
+                                       public nsSupportsWeakReference
+{
+public:
+  explicit TabChildSHistoryListener(TabChild* aTabChild) : mTabChild(aTabChild) {}
+  void ClearTabChild() { mTabChild = nullptr; }
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSISHISTORYLISTENER
+  NS_DECL_NSIPARTIALSHISTORYLISTENER
+
+private:
+  ~TabChildSHistoryListener() {}
+  TabChild* mTabChild;
+};
+
 // This is base clase which helps to share Viewport and touch related
 // functionality between b2g/android FF/embedlite clients implementation.
 // It make sense to place in this class all helper functions, and functionality
 // which could be shared between Cross-process/Cross-thread implmentations.
 class TabChildBase : public nsISupports,
                      public nsMessageManagerScriptExecutor,
                      public ipc::MessageManagerCallback
 {
@@ -677,16 +700,23 @@ protected:
   virtual bool RecvSetKeyboardIndicators(const UIStateChangeType& aShowAccelerators,
                                          const UIStateChangeType& aShowFocusRings) override;
 
   virtual bool RecvStopIMEStateManagement() override;
 
   virtual bool RecvMenuKeyboardListenerInstalled(
                  const bool& aInstalled) override;
 
+  virtual bool RecvNotifyAttachGroupedSessionHistory(const uint32_t& aOffset) override;
+
+  virtual bool RecvNotifyPartialSessionHistoryActive(const uint32_t& aGlobalLength,
+                                                     const uint32_t& aTargetLocalIndex) override;
+
+  virtual bool RecvNotifyPartialSessionHistoryDeactive() override;
+
 private:
   void HandleDoubleTap(const CSSPoint& aPoint, const Modifiers& aModifiers,
                        const ScrollableLayerGuid& aGuid);
 
   // Notify others that our TabContext has been updated.
   //
   // You should call this after calling TabContext::SetTabContext().  We also
   // call this during Init().
@@ -729,16 +759,17 @@ private:
   class DelayedDeleteRunnable;
 
   TextureFactoryIdentifier mTextureFactoryIdentifier;
   nsCOMPtr<nsIWebNavigation> mWebNav;
   RefPtr<PuppetWidget> mPuppetWidget;
   nsCOMPtr<nsIURI> mLastURI;
   RenderFrameChild* mRemoteFrame;
   RefPtr<nsIContentChild> mManager;
+  RefPtr<TabChildSHistoryListener> mHistoryListener;
   uint32_t mChromeFlags;
   int32_t mActiveSuppressDisplayport;
   uint64_t mLayersId;
   CSSRect mUnscaledOuterRect;
   // Whether we have already received a FileDescriptor for the app package.
   bool mAppPackageFileDescriptorRecved;
   // At present only 1 of these is really expected.
   AutoTArray<nsAutoPtr<CachedFileDescriptorInfo>, 1>
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -95,16 +95,18 @@
 #include "nsILoginManagerPrompter.h"
 #include "nsPIWindowRoot.h"
 #include "nsIAuthPrompt2.h"
 #include "gfxDrawable.h"
 #include "ImageOps.h"
 #include "UnitTransforms.h"
 #include <algorithm>
 #include "mozilla/WebBrowserPersistDocumentParent.h"
+#include "nsIGroupedSHistory.h"
+#include "PartialSHistory.h"
 
 #if defined(XP_WIN) && defined(ACCESSIBILITY)
 #include "mozilla/a11y/AccessibleWrap.h"
 #endif
 
 using namespace mozilla::dom;
 using namespace mozilla::ipc;
 using namespace mozilla::layers;
@@ -3458,16 +3460,50 @@ TabParent::RecvLookUpDictionary(const ns
     return true;
   }
 
   widget->LookUpDictionary(aText, aFontRangeArray, aIsVertical,
                            aPoint - GetChildProcessOffset());
   return true;
 }
 
+bool
+TabParent::RecvNotifySessionHistoryChange(const uint32_t& aCount)
+{
+  RefPtr<nsFrameLoader> frameLoader(GetFrameLoader());
+  if (!frameLoader) {
+    // FrameLoader can be nullptr if the it is destroying.
+    // In this case session history change can simply be ignored.
+    return true;
+  }
+
+  nsCOMPtr<nsIPartialSHistory> partialHistory;
+  frameLoader->GetPartialSessionHistory(getter_AddRefs(partialHistory));
+  if (!partialHistory) {
+    // PartialSHistory is not enabled
+    return true;
+  }
+
+  partialHistory->OnSessionHistoryChange(aCount);
+  return true;
+}
+
+bool
+TabParent::RecvRequestCrossBrowserNavigation(const uint32_t& aGlobalIndex)
+{
+  RefPtr<nsFrameLoader> frameLoader(GetFrameLoader());
+  if (!frameLoader) {
+    // FrameLoader can be nullptr if the it is destroying.
+    // In this case we can ignore the request.
+    return true;
+  }
+
+  return NS_SUCCEEDED(frameLoader->RequestGroupedHistoryNavigation(aGlobalIndex));
+}
+
 NS_IMETHODIMP
 FakeChannel::OnAuthAvailable(nsISupports *aContext, nsIAuthInformation *aAuthInfo)
 {
   nsAuthInformationHolder* holder =
     static_cast<nsAuthInformationHolder*>(aAuthInfo);
 
   if (!net::gNeckoChild->SendOnAuthAvailable(mCallbackId,
                                              holder->User(),
--- a/dom/ipc/TabParent.h
+++ b/dom/ipc/TabParent.h
@@ -28,16 +28,17 @@
 #include "nsISecureBrowserUI.h"
 #include "nsITabParent.h"
 #include "nsIWebBrowserPersistable.h"
 #include "nsIXULBrowserWindow.h"
 #include "nsRefreshDriver.h"
 #include "nsWeakReference.h"
 #include "Units.h"
 #include "nsIWidget.h"
+#include "nsIPartialSHistory.h"
 
 class nsFrameLoader;
 class nsIFrameLoader;
 class nsIContent;
 class nsIPrincipal;
 class nsIURI;
 class nsILoadContext;
 class nsIDocShell;
@@ -627,16 +628,20 @@ protected:
                                  const int32_t& aX, const int32_t& aY,
                                  const int32_t& aCx, const int32_t& aCy) override;
 
   virtual bool RecvGetTabCount(uint32_t* aValue) override;
 
   virtual bool RecvAudioChannelActivityNotification(const uint32_t& aAudioChannel,
                                                     const bool& aActive) override;
 
+  virtual bool RecvNotifySessionHistoryChange(const uint32_t& aCount) override;
+
+  virtual bool RecvRequestCrossBrowserNavigation(const uint32_t& aGlobalIndex) override;
+
   ContentCacheInParent mContentCache;
 
   nsIntRect mRect;
   ScreenIntSize mDimensions;
   ScreenOrientationInternal mOrientation;
   float mDPI;
   int32_t mRounding;
   CSSToLayoutDeviceScale mDefaultScale;
--- a/dom/ipc/moz.build
+++ b/dom/ipc/moz.build
@@ -1,16 +1,15 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPIDL_SOURCES += [
-    'nsIBrowser.idl',
     'nsIHangReport.idl',
 ]
 
 XPIDL_MODULE = 'dom'
 
 EXPORTS += [
     'nsICachedFileDescriptorListener.h',
 ]
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -214,16 +214,18 @@ public:
   virtual bool HandleAudioCaptured() { return false; }
 
   virtual RefPtr<ShutdownPromise> HandleShutdown();
 
   virtual void HandleVideoSuspendTimeout() = 0;
 
   virtual void HandleResumeVideoDecoding();
 
+  virtual void HandlePlayStateChanged(MediaDecoder::PlayState aPlayState) {}
+
   virtual void DumpDebugInfo() {}
 
 protected:
   using Master = MediaDecoderStateMachine;
   explicit StateObject(Master* aPtr) : mMaster(aPtr) {}
   TaskQueue* OwnerThread() const { return mMaster->mTaskQueue; }
   MediaResource* Resource() const { return mMaster->mResource; }
   MediaDecoderReaderWrapper* Reader() const { return mMaster->mReader; }
@@ -625,16 +627,24 @@ public:
   {
     if (mMaster->HasVideo()) {
       mMaster->mVideoDecodeSuspended = true;
       mMaster->mOnPlaybackEvent.Notify(MediaEventType::EnterVideoSuspend);
       Reader()->SetVideoBlankDecode(true);
     }
   }
 
+  void HandlePlayStateChanged(MediaDecoder::PlayState aPlayState) override
+  {
+    if (aPlayState == MediaDecoder::PLAY_STATE_PLAYING) {
+      // Schedule Step() to check if we can start playback.
+      mMaster->ScheduleStateMachine();
+    }
+  }
+
   void DumpDebugInfo() override
   {
     SDUMP("mIsPrerolling=%d", mIsPrerolling);
   }
 
 private:
   void MaybeStartBuffering();
 
@@ -1046,16 +1056,24 @@ public:
     return true;
   }
 
   void HandleVideoSuspendTimeout() override
   {
     // Do nothing since no decoding is going on.
   }
 
+  void HandlePlayStateChanged(MediaDecoder::PlayState aPlayState) override
+  {
+    if (aPlayState == MediaDecoder::PLAY_STATE_PLAYING) {
+      // Schedule Step() to check if we can start playback.
+      mMaster->ScheduleStateMachine();
+    }
+  }
+
 private:
   bool mSentPlaybackEndedEvent = false;
 };
 
 /**
  * Purpose: release all resources allocated by MDSM.
  *
  * Transition to:
@@ -2452,44 +2470,25 @@ MediaDecoderStateMachine::Shutdown()
 }
 
 void MediaDecoderStateMachine::PlayStateChanged()
 {
   MOZ_ASSERT(OnTaskQueue());
 
   if (mPlayState != MediaDecoder::PLAY_STATE_PLAYING) {
     mVideoDecodeSuspendTimer.Reset();
-    return;
-  }
-
-  // Once we start playing, we don't want to minimize our prerolling, as we
-  // assume the user is likely to want to keep playing in future. This needs to
-  // happen before we invoke StartDecoding().
-  if (mMinimizePreroll) {
+  } else if (mMinimizePreroll) {
+    // Once we start playing, we don't want to minimize our prerolling, as we
+    // assume the user is likely to want to keep playing in future. This needs to
+    // happen before we invoke StartDecoding().
     mMinimizePreroll = false;
     DispatchDecodeTasksIfNeeded();
   }
 
-  // Some state transitions still happen synchronously on the main thread. So
-  // if the main thread invokes Play() and then Seek(), the seek will initiate
-  // synchronously on the main thread, and the asynchronous PlayInternal task
-  // will arrive when it's no longer valid. The proper thing to do is to move
-  // all state transitions to the state machine task queue, but for now we just
-  // make sure that none of the possible main-thread state transitions (Seek(),
-  // SetDormant(), and Shutdown()) have not occurred.
-  if (mState != DECODER_STATE_DECODING &&
-      mState != DECODER_STATE_DECODING_FIRSTFRAME &&
-      mState != DECODER_STATE_BUFFERING &&
-      mState != DECODER_STATE_COMPLETED)
-  {
-    DECODER_LOG("Unexpected state - Bailing out of PlayInternal()");
-    return;
-  }
-
-  ScheduleStateMachine();
+  mStateObj->HandlePlayStateChanged(mPlayState);
 }
 
 void MediaDecoderStateMachine::VisibilityChanged()
 {
   MOZ_ASSERT(OnTaskQueue());
   DECODER_LOG("VisibilityChanged: mIsVisible=%d, "
               "mVideoDecodeSuspended=%c, mIsReaderSuspended=%d",
               mIsVisible.Ref(), mVideoDecodeSuspended ? 'T' : 'F', mIsReaderSuspended.Ref());
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -554,21 +554,21 @@ RTCPeerConnection.prototype = {
 
     rtcConfig.iceServers.forEach(server => {
       if (!server.urls) {
         throw new this._win.DOMException(msg + " - missing urls", "InvalidAccessError");
       }
       server.urls.forEach(urlStr => {
         let url = nicerNewURI(urlStr);
         if (url.scheme in { turn:1, turns:1 }) {
-          if (!server.username) {
+          if (server.username == undefined) {
             throw new this._win.DOMException(msg + " - missing username: " + urlStr,
                                              "InvalidAccessError");
           }
-          if (!server.credential) {
+          if (server.credential == undefined) {
             throw new this._win.DOMException(msg + " - missing credential: " + urlStr,
                                              "InvalidAccessError");
           }
           if (server.credentialType != "password") {
             this.logWarning("RTCConfiguration TURN credentialType \""+
                             server.credentialType +
                             "\" is not yet implemented. Treating as password."+
                             " https://bugzil.la/1247616");
--- a/dom/media/eme/MediaKeySystemAccess.cpp
+++ b/dom/media/eme/MediaKeySystemAccess.cpp
@@ -32,17 +32,19 @@
 #include "gmp-audio-decode.h"
 #include "gmp-video-decode.h"
 #include "DecoderDoctorDiagnostics.h"
 #include "WebMDecoder.h"
 #include "mozilla/StaticPtr.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "nsUnicharUtils.h"
 #include "mozilla/dom/MediaSource.h"
-
+#ifdef MOZ_WIDGET_ANDROID
+#include "FennecJNIWrappers.h"
+#endif
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeySystemAccess,
                                       mParent)
 NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccess)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccess)
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccess)
@@ -291,112 +293,122 @@ MediaKeySystemAccess::GetKeySystemStatus
         aOutMessage = NS_LITERAL_CSTRING("Minimum Windows version (Vista) not met for Adobe EME");
         return MediaKeySystemStatus::Cdm_not_supported;
       }
 #endif
       return EnsureMinCDMVersion(mps, aKeySystem, aMinCdmVersion, aOutMessage, aOutCdmVersion);
     }
   }
 
-  if (Preferences::GetBool("media.gmp-widevinecdm.visible", false)) {
-    if (IsWidevineKeySystem(aKeySystem)) {
+  if (IsWidevineKeySystem(aKeySystem)) {
+    if (Preferences::GetBool("media.gmp-widevinecdm.visible", false)) {
 #ifdef XP_WIN
       // Win Vista and later only.
       if (!IsVistaOrLater()) {
         aOutMessage = NS_LITERAL_CSTRING("Minimum Windows version (Vista) not met for Widevine EME");
         return MediaKeySystemStatus::Cdm_not_supported;
       }
 #endif
       if (!Preferences::GetBool("media.gmp-widevinecdm.enabled", false)) {
         aOutMessage = NS_LITERAL_CSTRING("Widevine EME disabled");
         return MediaKeySystemStatus::Cdm_disabled;
       }
       return EnsureMinCDMVersion(mps, aKeySystem, aMinCdmVersion, aOutMessage, aOutCdmVersion);
+#ifdef MOZ_WIDGET_ANDROID
+    } else if (Preferences::GetBool("media.mediadrm-widevinecdm.visible", false)) {
+        nsCString keySystem = NS_ConvertUTF16toUTF8(aKeySystem);
+        bool supported = mozilla::java::MediaDrmProxy::IsSchemeSupported(keySystem);
+        if (!supported) {
+          aOutMessage = NS_LITERAL_CSTRING("Widevine CDM is not available");
+          return MediaKeySystemStatus::Cdm_not_installed;
+        }
+        return MediaKeySystemStatus::Available;
+#endif
     }
   }
 
   return MediaKeySystemStatus::Cdm_not_supported;
 }
 
-typedef nsCString GMPCodecString;
+typedef nsCString EMECodecString;
 
-#define GMP_CODEC_AAC NS_LITERAL_CSTRING("aac")
-#define GMP_CODEC_OPUS NS_LITERAL_CSTRING("opus")
-#define GMP_CODEC_VORBIS NS_LITERAL_CSTRING("vorbis")
-#define GMP_CODEC_H264 NS_LITERAL_CSTRING("h264")
-#define GMP_CODEC_VP8 NS_LITERAL_CSTRING("vp8")
-#define GMP_CODEC_VP9 NS_LITERAL_CSTRING("vp9")
+static NS_NAMED_LITERAL_CSTRING(EME_CODEC_AAC, "aac");
+static NS_NAMED_LITERAL_CSTRING(EME_CODEC_OPUS, "opus");
+static NS_NAMED_LITERAL_CSTRING(EME_CODEC_VORBIS, "vorbis");
+static NS_NAMED_LITERAL_CSTRING(EME_CODEC_H264, "h264");
+static NS_NAMED_LITERAL_CSTRING(EME_CODEC_VP8, "vp8");
+static NS_NAMED_LITERAL_CSTRING(EME_CODEC_VP9, "vp9");
 
-GMPCodecString
-ToGMPAPICodecString(const nsString& aCodec)
+EMECodecString
+ToEMEAPICodecString(const nsString& aCodec)
 {
   if (IsAACCodecString(aCodec)) {
-    return GMP_CODEC_AAC;
+    return EME_CODEC_AAC;
   }
   if (aCodec.EqualsLiteral("opus")) {
-    return GMP_CODEC_OPUS;
+    return EME_CODEC_OPUS;
   }
   if (aCodec.EqualsLiteral("vorbis")) {
-    return GMP_CODEC_VORBIS;
+    return EME_CODEC_VORBIS;
   }
   if (IsH264CodecString(aCodec)) {
-    return GMP_CODEC_H264;
+    return EME_CODEC_H264;
   }
   if (IsVP8CodecString(aCodec)) {
-    return GMP_CODEC_VP8;
+    return EME_CODEC_VP8;
   }
   if (IsVP9CodecString(aCodec)) {
-    return GMP_CODEC_VP9;
+    return EME_CODEC_VP9;
   }
   return EmptyCString();
 }
 
 // A codec can be decrypted-and-decoded by the CDM, or only decrypted
 // by the CDM and decoded by Gecko. Not both.
 struct KeySystemContainerSupport
 {
   bool IsSupported() const
   {
     return !mCodecsDecoded.IsEmpty() || !mCodecsDecrypted.IsEmpty();
   }
 
   // CDM decrypts and decodes using a DRM robust decoder, and passes decoded
   // samples back to Gecko for rendering.
-  bool DecryptsAndDecodes(GMPCodecString aCodec) const
+  bool DecryptsAndDecodes(EMECodecString aCodec) const
   {
     return mCodecsDecoded.Contains(aCodec);
   }
 
   // CDM decrypts and passes the decrypted samples back to Gecko for decoding.
-  bool Decrypts(GMPCodecString aCodec) const
+  bool Decrypts(EMECodecString aCodec) const
   {
     return mCodecsDecrypted.Contains(aCodec);
   }
 
-  void SetCanDecryptAndDecode(GMPCodecString aCodec)
+  void SetCanDecryptAndDecode(EMECodecString aCodec)
   {
     // Can't both decrypt and decrypt-and-decode a codec.
     MOZ_ASSERT(!Decrypts(aCodec));
     // Prevent duplicates.
     MOZ_ASSERT(!DecryptsAndDecodes(aCodec));
     mCodecsDecoded.AppendElement(aCodec);
   }
 
-  void SetCanDecrypt(GMPCodecString aCodec)
+  void SetCanDecrypt(EMECodecString aCodec)
   {
     // Prevent duplicates.
     MOZ_ASSERT(!Decrypts(aCodec));
     // Can't both decrypt and decrypt-and-decode a codec.
     MOZ_ASSERT(!DecryptsAndDecodes(aCodec));
     mCodecsDecrypted.AppendElement(aCodec);
   }
 
 private:
-  nsTArray<GMPCodecString> mCodecsDecoded;
-  nsTArray<GMPCodecString> mCodecsDecrypted;
+  nsTArray<EMECodecString> mCodecsDecoded;
+  nsTArray<EMECodecString> mCodecsDecrypted;
 };
 
 enum class KeySystemFeatureSupport
 {
   Prohibited = 1,
   Requestable = 2,
   Required = 3,
 };
@@ -409,155 +421,207 @@ struct KeySystemConfig
   KeySystemFeatureSupport mDistinctiveIdentifier = KeySystemFeatureSupport::Prohibited;
   nsTArray<MediaKeySessionType> mSessionTypes;
   nsTArray<nsString> mVideoRobustness;
   nsTArray<nsString> mAudioRobustness;
   KeySystemContainerSupport mMP4;
   KeySystemContainerSupport mWebM;
 };
 
-StaticAutoPtr<nsTArray<KeySystemConfig>> sKeySystemConfigs;
+bool
+HavePluginForKeySystem(const nsCString& aKeySystem)
+{
+  bool havePlugin = false;
+  nsCOMPtr<mozIGeckoMediaPluginService> mps =
+    do_GetService("@mozilla.org/gecko-media-plugin-service;1");
+  if (mps) {
+    havePlugin = HaveGMPFor(mps,
+                            aKeySystem,
+                            NS_LITERAL_CSTRING(GMP_API_DECRYPTOR));
+  }
+#ifdef MOZ_WIDGET_ANDROID
+  // Check if we can use MediaDrm for this keysystem.
+  if (!havePlugin) {
+     havePlugin = mozilla::java::MediaDrmProxy::IsSchemeSupported(aKeySystem);
+  }
+#endif
+  return havePlugin;
+}
 
-static const nsTArray<KeySystemConfig>&
+static nsTArray<KeySystemConfig>
 GetSupportedKeySystems()
 {
-  if (!sKeySystemConfigs) {
-    sKeySystemConfigs = new nsTArray<KeySystemConfig>();
-    ClearOnShutdown(&sKeySystemConfigs);
+  nsTArray<KeySystemConfig> keySystemConfigs;
 
-    {
+  {
+    if (HavePluginForKeySystem(kEMEKeySystemClearkey)) {
       KeySystemConfig clearkey;
       clearkey.mKeySystem = NS_ConvertUTF8toUTF16(kEMEKeySystemClearkey);
       clearkey.mInitDataTypes.AppendElement(NS_LITERAL_STRING("cenc"));
       clearkey.mInitDataTypes.AppendElement(NS_LITERAL_STRING("keyids"));
       clearkey.mInitDataTypes.AppendElement(NS_LITERAL_STRING("webm"));
       clearkey.mPersistentState = KeySystemFeatureSupport::Requestable;
       clearkey.mDistinctiveIdentifier = KeySystemFeatureSupport::Prohibited;
       clearkey.mSessionTypes.AppendElement(MediaKeySessionType::Temporary);
       if (MediaPrefs::ClearKeyPersistentLicenseEnabled()) {
         clearkey.mSessionTypes.AppendElement(MediaKeySessionType::Persistent_license);
       }
 #if defined(XP_WIN)
       // Clearkey CDM uses WMF decoders on Windows.
       if (WMFDecoderModule::HasAAC()) {
-        clearkey.mMP4.SetCanDecryptAndDecode(GMP_CODEC_AAC);
+        clearkey.mMP4.SetCanDecryptAndDecode(EME_CODEC_AAC);
       } else {
-        clearkey.mMP4.SetCanDecrypt(GMP_CODEC_AAC);
+        clearkey.mMP4.SetCanDecrypt(EME_CODEC_AAC);
       }
       if (WMFDecoderModule::HasH264()) {
-        clearkey.mMP4.SetCanDecryptAndDecode(GMP_CODEC_H264);
+        clearkey.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264);
       } else {
-        clearkey.mMP4.SetCanDecrypt(GMP_CODEC_H264);
+        clearkey.mMP4.SetCanDecrypt(EME_CODEC_H264);
       }
 #else
-      clearkey.mMP4.SetCanDecrypt(GMP_CODEC_AAC);
-      clearkey.mMP4.SetCanDecrypt(GMP_CODEC_H264);
+      clearkey.mMP4.SetCanDecrypt(EME_CODEC_AAC);
+      clearkey.mMP4.SetCanDecrypt(EME_CODEC_H264);
 #endif
-      clearkey.mWebM.SetCanDecrypt(GMP_CODEC_VORBIS);
-      clearkey.mWebM.SetCanDecrypt(GMP_CODEC_OPUS);
-      clearkey.mWebM.SetCanDecrypt(GMP_CODEC_VP8);
-      clearkey.mWebM.SetCanDecrypt(GMP_CODEC_VP9);
-      sKeySystemConfigs->AppendElement(Move(clearkey));
+      clearkey.mWebM.SetCanDecrypt(EME_CODEC_VORBIS);
+      clearkey.mWebM.SetCanDecrypt(EME_CODEC_OPUS);
+      clearkey.mWebM.SetCanDecrypt(EME_CODEC_VP8);
+      clearkey.mWebM.SetCanDecrypt(EME_CODEC_VP9);
+      keySystemConfigs.AppendElement(Move(clearkey));
     }
-    {
+  }
+  {
+    if (HavePluginForKeySystem(kEMEKeySystemWidevine)) {
       KeySystemConfig widevine;
       widevine.mKeySystem = NS_ConvertUTF8toUTF16(kEMEKeySystemWidevine);
       widevine.mInitDataTypes.AppendElement(NS_LITERAL_STRING("cenc"));
       widevine.mInitDataTypes.AppendElement(NS_LITERAL_STRING("keyids"));
       widevine.mInitDataTypes.AppendElement(NS_LITERAL_STRING("webm"));
       widevine.mPersistentState = KeySystemFeatureSupport::Requestable;
       widevine.mDistinctiveIdentifier = KeySystemFeatureSupport::Prohibited;
       widevine.mSessionTypes.AppendElement(MediaKeySessionType::Temporary);
+#ifdef MOZ_WIDGET_ANDROID
+      widevine.mSessionTypes.AppendElement(MediaKeySessionType::Persistent_license);
+#endif
       widevine.mAudioRobustness.AppendElement(NS_LITERAL_STRING("SW_SECURE_CRYPTO"));
       widevine.mVideoRobustness.AppendElement(NS_LITERAL_STRING("SW_SECURE_DECODE"));
 #if defined(XP_WIN)
       // Widevine CDM doesn't include an AAC decoder. So if WMF can't
       // decode AAC, and a codec wasn't specified, be conservative
       // and reject the MediaKeys request, since our policy is to prevent
       //  the Adobe GMP's unencrypted AAC decoding path being used to
       // decode content decrypted by the Widevine CDM.
       if (WMFDecoderModule::HasAAC()) {
-        widevine.mMP4.SetCanDecrypt(GMP_CODEC_AAC);
+        widevine.mMP4.SetCanDecrypt(EME_CODEC_AAC);
+      }
+#elif !defined(MOZ_WIDGET_ANDROID)
+      widevine.mMP4.SetCanDecrypt(EME_CODEC_AAC);
+#endif
+
+#if defined(MOZ_WIDGET_ANDROID)
+      using namespace mozilla::java;
+      // MediaDrm.isCryptoSchemeSupported only allows passing
+      // "video/mp4" or "video/webm" for mimetype string.
+      // See https://developer.android.com/reference/android/media/MediaDrm.html#isCryptoSchemeSupported(java.util.UUID, java.lang.String)
+      // for more detail.
+      typedef struct {
+        const nsCString& mMimeType;
+        const nsCString& mEMECodecType;
+        const char16_t* mCodecType;
+        KeySystemContainerSupport* mSupportType;
+      } DataForValidation;
+
+      DataForValidation validationList[] = {
+        { nsCString("video/mp4"), EME_CODEC_H264, MediaDrmProxy::AVC, &widevine.mMP4 },
+        { nsCString("audio/mp4"), EME_CODEC_AAC, MediaDrmProxy::AAC, &widevine.mMP4 },
+        { nsCString("video/webm"), EME_CODEC_VP8, MediaDrmProxy::VP8, &widevine.mWebM },
+        { nsCString("video/webm"), EME_CODEC_VP9, MediaDrmProxy::VP9, &widevine.mWebM},
+        { nsCString("audio/webm"), EME_CODEC_VORBIS, MediaDrmProxy::VORBIS, &widevine.mWebM},
+        { nsCString("audio/webm"), EME_CODEC_OPUS, MediaDrmProxy::OPUS, &widevine.mWebM},
+      };
+
+      for (const auto& data: validationList) {
+        if (MediaDrmProxy::IsCryptoSchemeSupported(kEMEKeySystemWidevine,
+                                                   data.mMimeType)) {
+          if (MediaDrmProxy::CanDecode(data.mCodecType)) {
+            data.mSupportType->SetCanDecryptAndDecode(data.mEMECodecType);
+          } else {
+            data.mSupportType->SetCanDecrypt(data.mEMECodecType);
+          }
+        }
       }
 #else
-      widevine.mMP4.SetCanDecrypt(GMP_CODEC_AAC);
+      widevine.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264);
+      widevine.mWebM.SetCanDecrypt(EME_CODEC_VORBIS);
+      widevine.mWebM.SetCanDecrypt(EME_CODEC_OPUS);
+      widevine.mWebM.SetCanDecryptAndDecode(EME_CODEC_VP8);
+      widevine.mWebM.SetCanDecryptAndDecode(EME_CODEC_VP9);
 #endif
-      widevine.mMP4.SetCanDecryptAndDecode(GMP_CODEC_H264);
-      widevine.mWebM.SetCanDecrypt(GMP_CODEC_VORBIS);
-      widevine.mWebM.SetCanDecrypt(GMP_CODEC_OPUS);
-      widevine.mWebM.SetCanDecryptAndDecode(GMP_CODEC_VP8);
-      widevine.mWebM.SetCanDecryptAndDecode(GMP_CODEC_VP9);
-      sKeySystemConfigs->AppendElement(Move(widevine));
+      keySystemConfigs.AppendElement(Move(widevine));
     }
-    {
+  }
+  {
+    if (HavePluginForKeySystem(kEMEKeySystemPrimetime)) {
       KeySystemConfig primetime;
       primetime.mKeySystem = NS_ConvertUTF8toUTF16(kEMEKeySystemPrimetime);
       primetime.mInitDataTypes.AppendElement(NS_LITERAL_STRING("cenc"));
       primetime.mPersistentState = KeySystemFeatureSupport::Required;
       primetime.mDistinctiveIdentifier = KeySystemFeatureSupport::Required;
       primetime.mSessionTypes.AppendElement(MediaKeySessionType::Temporary);
-      primetime.mMP4.SetCanDecryptAndDecode(GMP_CODEC_AAC);
-      primetime.mMP4.SetCanDecryptAndDecode(GMP_CODEC_H264);
-      sKeySystemConfigs->AppendElement(Move(primetime));
+      primetime.mMP4.SetCanDecryptAndDecode(EME_CODEC_AAC);
+      primetime.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264);
+      keySystemConfigs.AppendElement(Move(primetime));
     }
   }
-  return *sKeySystemConfigs;
+
+  return keySystemConfigs;
 }
 
-static const KeySystemConfig*
-GetKeySystemConfig(const nsAString& aKeySystem)
+static bool
+GetKeySystemConfig(const nsAString& aKeySystem, KeySystemConfig& aOutKeySystemConfig)
 {
-  for (const KeySystemConfig& config : GetSupportedKeySystems()) {
+  for (auto&& config : GetSupportedKeySystems()) {
     if (config.mKeySystem.Equals(aKeySystem)) {
-      return &config;
+      aOutKeySystemConfig = mozilla::Move(config);
+      return true;
     }
   }
-  return nullptr;
+  // No matching key system found.
+  return false;
 }
 
 /* static */
 bool
 MediaKeySystemAccess::KeySystemSupportsInitDataType(const nsAString& aKeySystem,
                                                     const nsAString& aInitDataType)
 {
-  const KeySystemConfig* implementation = GetKeySystemConfig(aKeySystem);
-  return implementation &&
-         implementation->mInitDataTypes.Contains(aInitDataType);
+  KeySystemConfig implementation;
+  return GetKeySystemConfig(aKeySystem, implementation) &&
+         implementation.mInitDataTypes.Contains(aInitDataType);
 }
 
 enum CodecType
 {
   Audio,
   Video,
   Invalid
 };
 
 static bool
-CanDecryptAndDecode(mozIGeckoMediaPluginService* aGMPService,
-                    const nsString& aKeySystem,
+CanDecryptAndDecode(const nsString& aKeySystem,
                     const nsString& aContentType,
                     CodecType aCodecType,
                     const KeySystemContainerSupport& aContainerSupport,
-                    const nsTArray<GMPCodecString>& aCodecs,
+                    const nsTArray<EMECodecString>& aCodecs,
                     DecoderDoctorDiagnostics* aDiagnostics)
 {
   MOZ_ASSERT(aCodecType != Invalid);
-  MOZ_ASSERT(HaveGMPFor(aGMPService,
-                        NS_ConvertUTF16toUTF8(aKeySystem),
-                        NS_LITERAL_CSTRING(GMP_API_DECRYPTOR)));
-  for (const GMPCodecString& codec : aCodecs) {
+  for (const EMECodecString& codec : aCodecs) {
     MOZ_ASSERT(!codec.IsEmpty());
 
-    nsCString api = (aCodecType == Audio) ? NS_LITERAL_CSTRING(GMP_API_AUDIO_DECODER)
-                                          : NS_LITERAL_CSTRING(GMP_API_VIDEO_DECODER);
-    if (aContainerSupport.DecryptsAndDecodes(codec) &&
-        HaveGMPFor(aGMPService,
-                   NS_ConvertUTF16toUTF8(aKeySystem),
-                   api,
-                   codec)) {
+    if (aContainerSupport.DecryptsAndDecodes(codec)) {
       // GMP can decrypt-and-decode this codec.
       continue;
     }
 
     if (aContainerSupport.Decrypts(codec) &&
         NS_SUCCEEDED(MediaSource::IsTypeSupported(aContentType, aDiagnostics))) {
       // GMP can decrypt and is allowed to return compressed samples to
       // Gecko to decode, and Gecko has a decoder.
@@ -568,17 +632,17 @@ CanDecryptAndDecode(mozIGeckoMediaPlugin
     // support this codec.
 
 #if defined(XP_WIN)
     // Widevine CDM doesn't include an AAC decoder. So if WMF can't
     // decode AAC, and a codec wasn't specified, be conservative
     // and reject the MediaKeys request, since our policy is to prevent
     //  the Adobe GMP's unencrypted AAC decoding path being used to
     // decode content decrypted by the Widevine CDM.
-    if (codec == GMP_CODEC_AAC &&
+    if (codec == EME_CODEC_AAC &&
         IsWidevineKeySystem(aKeySystem) &&
         !WMFDecoderModule::HasAAC()) {
       if (aDiagnostics) {
         aDiagnostics->SetKeySystemIssue(
           DecoderDoctorDiagnostics::eWidevineWithNoWMF);
       }
     }
 #endif
@@ -621,35 +685,35 @@ GetMajorType(const nsAString& aContentTy
   }
   if (CaseInsensitiveFindInReadable(NS_LITERAL_STRING("video/"), aContentType)) {
     return Video;
   }
   return Invalid;
 }
 
 static CodecType
-GetCodecType(const GMPCodecString& aCodec)
+GetCodecType(const EMECodecString& aCodec)
 {
-  if (aCodec.Equals(GMP_CODEC_AAC) ||
-      aCodec.Equals(GMP_CODEC_OPUS) ||
-      aCodec.Equals(GMP_CODEC_VORBIS)) {
+  if (aCodec.Equals(EME_CODEC_AAC) ||
+      aCodec.Equals(EME_CODEC_OPUS) ||
+      aCodec.Equals(EME_CODEC_VORBIS)) {
     return Audio;
   }
-  if (aCodec.Equals(GMP_CODEC_H264) ||
-      aCodec.Equals(GMP_CODEC_VP8) ||
-      aCodec.Equals(GMP_CODEC_VP9)) {
+  if (aCodec.Equals(EME_CODEC_H264) ||
+      aCodec.Equals(EME_CODEC_VP8) ||
+      aCodec.Equals(EME_CODEC_VP9)) {
     return Video;
   }
   return Invalid;
 }
 
 static bool
-AllCodecsOfType(const nsTArray<GMPCodecString>& aCodecs, const CodecType aCodecType)
+AllCodecsOfType(const nsTArray<EMECodecString>& aCodecs, const CodecType aCodecType)
 {
-  for (const GMPCodecString& codec : aCodecs) {
+  for (const EMECodecString& codec : aCodecs) {
     if (GetCodecType(codec) != aCodecType) {
       return false;
     }
   }
   return true;
 }
 
 static bool
@@ -681,17 +745,16 @@ IsParameterUnrecognized(const nsAString&
     }
   }
   return false;
 }
 
 // 3.1.2.3 Get Supported Capabilities for Audio/Video Type
 static Sequence<MediaKeySystemMediaCapability>
 GetSupportedCapabilities(const CodecType aCodecType,
-                         mozIGeckoMediaPluginService* aGMPService,
                          const nsTArray<MediaKeySystemMediaCapability>& aRequestedCapabilities,
                          const MediaKeySystemConfiguration& aPartialConfig,
                          const KeySystemConfig& aKeySystem,
                          DecoderDoctorDiagnostics* aDiagnostics)
 {
   // Let local accumulated configuration be a local copy of partial configuration.
   // (Note: It's not necessary for us to maintain a local copy, as we don't need
   // to test whether capabilites from previous calls to this algorithm work with
@@ -726,31 +789,31 @@ GetSupportedCapabilities(const CodecType
               "MediaKeySystemMediaCapability('%s','%s') unsupported; "
               "failed to parse contentType as MIME type.",
               NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
               NS_ConvertUTF16toUTF8(contentType).get(),
               NS_ConvertUTF16toUTF8(robustness).get());
       continue;
     }
     bool invalid = false;
-    nsTArray<GMPCodecString> codecs;
+    nsTArray<EMECodecString> codecs;
     for (const nsString& codecString : codecStrings) {
-      GMPCodecString gmpCodec = ToGMPAPICodecString(codecString);
-      if (gmpCodec.IsEmpty()) {
+      EMECodecString emeCodec = ToEMEAPICodecString(codecString);
+      if (emeCodec.IsEmpty()) {
         invalid = true;
         EME_LOG("MediaKeySystemConfiguration (label='%s') "
                 "MediaKeySystemMediaCapability('%s','%s') unsupported; "
                 "'%s' is an invalid codec string.",
                 NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
                 NS_ConvertUTF16toUTF8(contentType).get(),
                 NS_ConvertUTF16toUTF8(robustness).get(),
                 NS_ConvertUTF16toUTF8(codecString).get());
         break;
       }
-      codecs.AppendElement(gmpCodec);
+      codecs.AppendElement(emeCodec);
     }
     if (invalid) {
       continue;
     }
 
     // If the user agent does not support container, continue to the next iteration.
     // The case-sensitivity of string comparisons is determined by the appropriate RFC.
     // (Note: Per RFC 6838 [RFC6838], "Both top-level type and subtype names are
@@ -801,25 +864,25 @@ GetSupportedCapabilities(const CodecType
     // (Note: codecs array is 'parameter').
 
     // If media types is empty:
     if (codecs.IsEmpty()) {
       // If container normatively implies a specific set of codecs and codec constraints:
       // Let parameters be that set.
       if (isMP4) {
         if (aCodecType == Audio) {
-          codecs.AppendElement(GMP_CODEC_AAC);
+          codecs.AppendElement(EME_CODEC_AAC);
         } else if (aCodecType == Video) {
-          codecs.AppendElement(GMP_CODEC_H264);
+          codecs.AppendElement(EME_CODEC_H264);
         }
       } else if (isWebM) {
         if (aCodecType == Audio) {
-          codecs.AppendElement(GMP_CODEC_VORBIS);
+          codecs.AppendElement(EME_CODEC_VORBIS);
         } else if (aCodecType == Video) {
-          codecs.AppendElement(GMP_CODEC_VP8);
+          codecs.AppendElement(EME_CODEC_VP8);
         }
       }
       // Otherwise: Continue to the next iteration.
       // (Note: all containers we support have implied codecs, so don't continue here.)
     }
 
     // If content type is not strictly a audio/video type, continue to the next iteration.
     const auto majorType = GetMajorType(container);
@@ -867,18 +930,17 @@ GetSupportedCapabilities(const CodecType
       // Note: specified robustness requirements are satisfied.
     }
 
     // If the user agent and implementation definitely support playback of
     // encrypted media data for the combination of container, media types,
     // robustness and local accumulated configuration in combination with
     // restrictions...
     const auto& containerSupport = isMP4 ? aKeySystem.mMP4 : aKeySystem.mWebM;
-    if (!CanDecryptAndDecode(aGMPService,
-                             aKeySystem.mKeySystem,
+    if (!CanDecryptAndDecode(aKeySystem.mKeySystem,
                              contentType,
                              majorType,
                              containerSupport,
                              codecs,
                              aDiagnostics)) {
         EME_LOG("MediaKeySystemConfiguration (label='%s') "
                 "MediaKeySystemMediaCapability('%s','%s') unsupported; "
                 "codec unsupported by CDM requested.",
@@ -970,18 +1032,17 @@ UnboxSessionTypes(const Optional<Sequenc
     // Note: fallible. Results in an empty array.
     sessionTypes.AppendElement(NS_ConvertUTF8toUTF16(nsDependentCString(temporary)), mozilla::fallible);
   }
   return sessionTypes;
 }
 
 // 3.1.2.2 Get Supported Configuration and Consent
 static bool
-GetSupportedConfig(mozIGeckoMediaPluginService* aGMPService,
-                   const KeySystemConfig& aKeySystem,
+GetSupportedConfig(const KeySystemConfig& aKeySystem,
                    const MediaKeySystemConfiguration& aCandidate,
                    MediaKeySystemConfiguration& aOutConfig,
                    DecoderDoctorDiagnostics* aDiagnostics)
 {
   // Let accumulated configuration be a new MediaKeySystemConfiguration dictionary.
   MediaKeySystemConfiguration config;
   // Set the label member of accumulated configuration to equal the label member of
   // candidate configuration.
@@ -1090,17 +1151,16 @@ GetSupportedConfig(mozIGeckoMediaPluginS
   // If the videoCapabilities member in candidate configuration is non-empty:
   if (!aCandidate.mVideoCapabilities.IsEmpty()) {
     // Let video capabilities be the result of executing the Get Supported
     // Capabilities for Audio/Video Type algorithm on Video, candidate
     // configuration's videoCapabilities member, accumulated configuration,
     // and restrictions.
     Sequence<MediaKeySystemMediaCapability> caps =
       GetSupportedCapabilities(Video,
-                               aGMPService,
                                aCandidate.mVideoCapabilities,
                                config,
                                aKeySystem,
                                aDiagnostics);
     // If video capabilities is null, return NotSupported.
     if (caps.IsEmpty()) {
       EME_LOG("MediaKeySystemConfiguration (label='%s') rejected; "
               "no supported video capabilities.",
@@ -1116,17 +1176,16 @@ GetSupportedConfig(mozIGeckoMediaPluginS
 
   // If the audioCapabilities member in candidate configuration is non-empty:
   if (!aCandidate.mAudioCapabilities.IsEmpty()) {
     // Let audio capabilities be the result of executing the Get Supported Capabilities
     // for Audio/Video Type algorithm on Audio, candidate configuration's audioCapabilities
     // member, accumulated configuration, and restrictions.
     Sequence<MediaKeySystemMediaCapability> caps =
       GetSupportedCapabilities(Audio,
-                               aGMPService,
                                aCandidate.mAudioCapabilities,
                                config,
                                aKeySystem,
                                aDiagnostics);
     // If audio capabilities is null, return NotSupported.
     if (caps.IsEmpty()) {
       EME_LOG("MediaKeySystemConfiguration (label='%s') rejected; "
               "no supported audio capabilities.",
@@ -1199,31 +1258,22 @@ GetSupportedConfig(mozIGeckoMediaPluginS
 
 /* static */
 bool
 MediaKeySystemAccess::GetSupportedConfig(const nsAString& aKeySystem,
                                          const Sequence<MediaKeySystemConfiguration>& aConfigs,
                                          MediaKeySystemConfiguration& aOutConfig,
                                          DecoderDoctorDiagnostics* aDiagnostics)
 {
-  nsCOMPtr<mozIGeckoMediaPluginService> mps =
-    do_GetService("@mozilla.org/gecko-media-plugin-service;1");
-  if (NS_WARN_IF(!mps)) {
-    return false;
-  }
-  const KeySystemConfig* implementation = nullptr;
-  if (!HaveGMPFor(mps,
-                  NS_ConvertUTF16toUTF8(aKeySystem),
-                  NS_LITERAL_CSTRING(GMP_API_DECRYPTOR)) ||
-      !(implementation = GetKeySystemConfig(aKeySystem))) {
+  KeySystemConfig implementation;
+  if (!GetKeySystemConfig(aKeySystem, implementation)) {
     return false;
   }
   for (const MediaKeySystemConfiguration& candidate : aConfigs) {
-    if (mozilla::dom::GetSupportedConfig(mps,
-                                         *implementation,
+    if (mozilla::dom::GetSupportedConfig(implementation,
                                          candidate,
                                          aOutConfig,
                                          aDiagnostics)) {
       return true;
     }
   }
 
   return false;
--- a/dom/media/tests/mochitest/test_peerConnection_bug825703.html
+++ b/dom/media/tests/mochitest/test_peerConnection_bug825703.html
@@ -61,16 +61,18 @@ runNetworkTest(() => {
   makePC({ iceServers: [{ urls:"" }] }, "SyntaxError");
 
   makePC({ iceServers: [
     { urls:"stun:127.0.0.1" },
     { urls:"stun:localhost", foo:"" },
     { urls: ["stun:127.0.0.1", "stun:localhost"] },
     { urls:"stuns:localhost", foo:"" },
     { urls:"turn:[::1]:3478", username:"p", credential:"p" },
+    { urls:"turn:[::1]:3478", username:"", credential:"" },
+    { urls:"turns:[::1]:3478", username:"", credential:"" },
     { urls:"turn:localhost:3478?transport=udp", username:"p", credential:"p" },
     { urls: ["turn:[::1]:3478", "turn:localhost"], username:"p", credential:"p" },
     { urls:"turns:localhost:3478?transport=udp", username:"p", credential:"p" },
     { url:"stun:localhost", foo:"" },
     { url:"turn:localhost", username:"p", credential:"p" }
   ]});
 
   makePC({ iceServers: [{ urls: ["stun:127.0.0.1", ""] }] }, "SyntaxError");
--- a/editor/composer/test/test_bug1205983.html
+++ b/editor/composer/test/test_bug1205983.html
@@ -1,126 +1,126 @@
-<!DOCTYPE html>
-<html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=1205983
--->
-<head>
-  <title>Test for Bug 1205983</title>
-  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
-  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
-</head>
-<body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205983">Mozilla Bug 1205983</a>
-<p id="display"></p>
-</div>
-
-<div contenteditable id="de-DE" lang="de-DE" onfocus="deFocus()">German heute ist ein guter Tag</div>
-<textarea id="en-US" lang="en-US" onfocus="enFocus()">Nogoodword today is a nice day</textarea>
-
-<pre id="test">
-<script class="testbody" type="text/javascript">
-
-function getMisspelledWords(editor) {
-  return editor.selectionController.getSelection(Components.interfaces.nsISelectionController.SELECTION_SPELLCHECK).toString();
-}
-
-var elem_de;
-var editor_de;
-var selcon_de;
-var de_DE;
-var hunspell;
-
-/** Test for Bug 1205983 **/
-SimpleTest.waitForExplicitFinish();
-SimpleTest.waitForFocus(function() {
-  Components.utils.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm");
-
-  var dir = Components.classes["@mozilla.org/file/directory_service;1"]
-                      .getService(Components.interfaces.nsIProperties)
-                      .get("CurWorkD", Components.interfaces.nsIFile);
-  dir.append("tests");
-  dir.append("editor");
-  dir.append("composer");
-  dir.append("test");
-
-  hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
-                       .getService(Components.interfaces.mozISpellCheckingEngine);
-
-  // Install de-DE dictionary.
-  de_DE = dir.clone();
-  de_DE.append("de-DE");
-  is(de_DE.exists(), true, "true expected (de_DE directory should exist)");
-  hunspell.addDirectory(de_DE);
-
-  document.getElementById('de-DE').focus();
-});
-
-function deFocus() {
-  elem_de = document.getElementById('de-DE');
-
-  onSpellCheck(elem_de, function () {
-    var Ci = Components.interfaces;
-    var editingSession = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                               .getInterface(Ci.nsIWebNavigation)
-                               .QueryInterface(Ci.nsIInterfaceRequestor)
-                               .getInterface(Ci.nsIEditingSession);
-    editor_de = editingSession.getEditorForWindow(window);
-    selcon_de = editor_de.selectionController;
-    var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
-
-    // Check that we spelled in German, so there is only one misspelled word.
-    is(sel.toString(), "German", "one misspelled word expected: German");
-
-    // Now focus the textarea, which requires English spelling.
-    document.getElementById('en-US').focus();
-  });
-}
-
-function enFocus() {
-  var elem_en = document.getElementById('en-US');
-  var editor_en = elem_en.QueryInterface(Components.interfaces.nsIDOMNSEditableElement).editor;
-  editor_en.setSpellcheckUserOverride(true);
-  var inlineSpellChecker = editor_en.getInlineSpellChecker(true);
-
-  onSpellCheck(elem_en, function () {
-    var spellchecker = inlineSpellChecker.spellChecker;
-    try {
-      currentDictonary = spellchecker.GetCurrentDictionary();
-    } catch(e) {}
-
-    // Check that the English dictionary is loaded and that the spell check has worked.
-    is(currentDictonary, "en-US", "expected en-US");
-    is(getMisspelledWords(editor_en), "Nogoodword", "one misspelled word expected: Nogoodword");
-
-    // So far all was boring. The important thing is whether the spell check result
-    // in the de-DE editor is still the same. After losing focus, no spell check
-    // updates should take place there.
-    var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
-    is(sel.toString(), "German", "one misspelled word expected: German");
-
-    // Remove the fake de_DE dictionary again.
-    hunspell.removeDirectory(de_DE);
-
-    // Focus again, so the spelling gets updated, but before we need to kill the focus handler.
-    elem_de.onfocus = null;
-    elem_de.blur();
-    elem_de.focus();
-
-    // After removal, the de_DE editor should refresh the spelling with en-US.
-    onSpellCheck(elem_de, function () {
-      var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
-      is(sel.toString(), "heute" + "ist" + "ein" + "guter",
-         "some misspelled words expected: heute ist ein guter");
-
-      // If we don't reset this, we cause massive leaks.
-      selcon_de = null;
-      editor_de = null;
-
-      SimpleTest.finish();
-    });
-  });
-}
-
-</script>
-</pre>
-</body>
-</html>
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1205983
+-->
+<head>
+  <title>Test for Bug 1205983</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1205983">Mozilla Bug 1205983</a>
+<p id="display"></p>
+</div>
+
+<div contenteditable id="de-DE" lang="de-DE" onfocus="deFocus()">German heute ist ein guter Tag</div>
+<textarea id="en-US" lang="en-US" onfocus="enFocus()">Nogoodword today is a nice day</textarea>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function getMisspelledWords(editor) {
+  return editor.selectionController.getSelection(Components.interfaces.nsISelectionController.SELECTION_SPELLCHECK).toString();
+}
+
+var elem_de;
+var editor_de;
+var selcon_de;
+var de_DE;
+var hunspell;
+
+/** Test for Bug 1205983 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+  Components.utils.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm");
+
+  var dir = Components.classes["@mozilla.org/file/directory_service;1"]
+                      .getService(Components.interfaces.nsIProperties)
+                      .get("CurWorkD", Components.interfaces.nsIFile);
+  dir.append("tests");
+  dir.append("editor");
+  dir.append("composer");
+  dir.append("test");
+
+  hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
+                       .getService(Components.interfaces.mozISpellCheckingEngine);
+
+  // Install de-DE dictionary.
+  de_DE = dir.clone();
+  de_DE.append("de-DE");
+  is(de_DE.exists(), true, "true expected (de_DE directory should exist)");
+  hunspell.addDirectory(de_DE);
+
+  document.getElementById('de-DE').focus();
+});
+
+function deFocus() {
+  elem_de = document.getElementById('de-DE');
+
+  onSpellCheck(elem_de, function () {
+    var Ci = Components.interfaces;
+    var editingSession = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIWebNavigation)
+                               .QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIEditingSession);
+    editor_de = editingSession.getEditorForWindow(window);
+    selcon_de = editor_de.selectionController;
+    var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+
+    // Check that we spelled in German, so there is only one misspelled word.
+    is(sel.toString(), "German", "one misspelled word expected: German");
+
+    // Now focus the textarea, which requires English spelling.
+    document.getElementById('en-US').focus();
+  });
+}
+
+function enFocus() {
+  var elem_en = document.getElementById('en-US');
+  var editor_en = elem_en.QueryInterface(Components.interfaces.nsIDOMNSEditableElement).editor;
+  editor_en.setSpellcheckUserOverride(true);
+  var inlineSpellChecker = editor_en.getInlineSpellChecker(true);
+
+  onSpellCheck(elem_en, function () {
+    var spellchecker = inlineSpellChecker.spellChecker;
+    try {
+      currentDictonary = spellchecker.GetCurrentDictionary();
+    } catch(e) {}
+
+    // Check that the English dictionary is loaded and that the spell check has worked.
+    is(currentDictonary, "en-US", "expected en-US");
+    is(getMisspelledWords(editor_en), "Nogoodword", "one misspelled word expected: Nogoodword");
+
+    // So far all was boring. The important thing is whether the spell check result
+    // in the de-DE editor is still the same. After losing focus, no spell check
+    // updates should take place there.
+    var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+    is(sel.toString(), "German", "one misspelled word expected: German");
+
+    // Remove the fake de_DE dictionary again.
+    hunspell.removeDirectory(de_DE);
+
+    // Focus again, so the spelling gets updated, but before we need to kill the focus handler.
+    elem_de.onfocus = null;
+    elem_de.blur();
+    elem_de.focus();
+
+    // After removal, the de_DE editor should refresh the spelling with en-US.
+    onSpellCheck(elem_de, function () {
+      var sel = selcon_de.getSelection(selcon_de.SELECTION_SPELLCHECK);
+      is(sel.toString(), "heute" + "ist" + "ein" + "guter",
+         "some misspelled words expected: heute ist ein guter");
+
+      // If we don't reset this, we cause massive leaks.
+      selcon_de = null;
+      editor_de = null;
+
+      SimpleTest.finish();
+    });
+  });
+}
+
+</script>
+</pre>
+</body>
+</html>
--- a/gfx/cairo/cairo/src/moz.build
+++ b/gfx/cairo/cairo/src/moz.build
@@ -217,16 +217,17 @@ if CONFIG['MOZ_TREE_FREETYPE']:
 if CONFIG['GNU_CC'] or CONFIG['CLANG_CL']:
     CFLAGS += [
         '-Wno-enum-compare',
         '-Wno-int-to-pointer-cast',
         '-Wno-sign-compare',
         '-Wno-type-limits',
         '-Wno-missing-field-initializers',
         '-Wno-conversion',
+        '-Wno-unused-but-set-variable',
     ]
 if CONFIG['CLANG_CXX'] or CONFIG['CLANG_CL']:
     CFLAGS += [
         '-Wno-incompatible-pointer-types',
         '-Wno-tautological-compare',
         '-Wno-tautological-constant-out-of-range-compare',
         '-Wno-error=uninitialized',
     ]
--- a/gfx/gl/GLContext.cpp
+++ b/gfx/gl/GLContext.cpp
@@ -820,16 +820,17 @@ GLContext::InitWithPrefixImpl(const char
         "Adreno (TM) 305",
         "Adreno (TM) 320",
         "Adreno (TM) 330",
         "Adreno (TM) 420",
         "Mali-400 MP",
         "Mali-450 MP",
         "PowerVR SGX 530",
         "PowerVR SGX 540",
+        "PowerVR SGX 544MP",
         "NVIDIA Tegra",
         "Android Emulator",
         "Gallium 0.4 on llvmpipe",
         "Intel HD Graphics 3000 OpenGL Engine",
         "Microsoft Basic Render Driver",
         "Unknown"
     };
 
@@ -1796,16 +1797,27 @@ GLContext::InitExtensions()
 
         if (Vendor() == GLVendor::Imagination &&
             Renderer() == GLRenderer::SGX540)
         {
             // Bug 980048
             MarkExtensionUnsupported(OES_EGL_sync);
         }
 
+#ifdef MOZ_WIDGET_ANDROID
+        if (Vendor() == GLVendor::Imagination &&
+            Renderer() == GLRenderer::SGX544MP &&
+            AndroidBridge::Bridge()->GetAPIVersion() < 21)
+        {
+            // Bug 1026404
+            MarkExtensionUnsupported(OES_EGL_image);
+            MarkExtensionUnsupported(OES_EGL_image_external);
+        }
+#endif
+
         if (Vendor() == GLVendor::ARM &&
             (Renderer() == GLRenderer::Mali400MP ||
              Renderer() == GLRenderer::Mali450MP))
         {
             // Bug 1264505
             MarkExtensionUnsupported(OES_EGL_image_external);
         }
 
--- a/gfx/gl/GLContext.h
+++ b/gfx/gl/GLContext.h
@@ -175,16 +175,17 @@ enum class GLRenderer {
     AdrenoTM305,
     AdrenoTM320,
     AdrenoTM330,
     AdrenoTM420,
     Mali400MP,
     Mali450MP,
     SGX530,
     SGX540,
+    SGX544MP,
     Tegra,
     AndroidEmulator,
     GalliumLlvmpipe,
     IntelHD3000,
     MicrosoftBasicRenderDriver,
     Other
 };
 
--- a/js/src/jsarray.cpp
+++ b/js/src/jsarray.cpp
@@ -2377,16 +2377,20 @@ CanOptimizeForDenseStorage(HandleObject 
     /* If the desired properties overflow dense storage, we can't optimize. */
     if (UINT32_MAX - startingIndex < count)
         return false;
 
     /* There's no optimizing possible if it's not an array. */
     if (!arr->is<ArrayObject>() && !arr->is<UnboxedArrayObject>())
         return false;
 
+    /* If it's a frozen array, always pick the slow path */
+    if (arr->is<ArrayObject>() && arr->as<ArrayObject>().denseElementsAreFrozen())
+        return false;
+
     /*
      * Don't optimize if the array might be in the midst of iteration.  We
      * rely on this to be able to safely move dense array elements around with
      * just a memmove (see NativeObject::moveDenseArrayElements), without worrying
      * about updating any in-progress enumerators for properties implicitly
      * deleted if a hole is moved from one location to another location not yet
      * visited.  See bug 690622.
      */
--- a/js/src/tests/ecma_5/Array/frozen-dense-array.js
+++ b/js/src/tests/ecma_5/Array/frozen-dense-array.js
@@ -4,38 +4,58 @@
  * Author: Emilio Cobos Álvarez <ecoal95@gmail.com>
  */
 var BUGNUMBER = 1310744;
 var summary = "Dense array properties shouldn't be modified when they're frozen";
 
 print(BUGNUMBER + ": " + summary);
 
 var a = Object.freeze([4, 5, 1]);
-var methods = [
-  'reverse',
-  'shift',
-  'pop',
-];
+
+function assertArrayIsExpected() {
+  assertEq(a.length, 3);
+  assertEq(a[0], 4);
+  assertEq(a[1], 5);
+  assertEq(a[2], 1);
+}
 
 assertThrowsInstanceOf(() => a.reverse(), TypeError);
 assertThrowsInstanceOf(() => a.shift(), TypeError);
 assertThrowsInstanceOf(() => a.unshift(0), TypeError);
 assertThrowsInstanceOf(() => a.sort(function() {}), TypeError);
 assertThrowsInstanceOf(() => a.pop(), TypeError);
 assertThrowsInstanceOf(() => a.fill(0), TypeError);
 assertThrowsInstanceOf(() => a.splice(0, 1, 1), TypeError);
 assertThrowsInstanceOf(() => a.push("foo"), TypeError);
 assertThrowsInstanceOf(() => { "use strict"; a.length = 5; }, TypeError);
+assertThrowsInstanceOf(() => { "use strict"; a[2] = "foo"; }, TypeError);
 assertThrowsInstanceOf(() => { "use strict"; delete a[0]; }, TypeError);
+assertThrowsInstanceOf(() => a.splice(Math.a), TypeError);
 
 // Shouldn't throw, since this is not strict mode, but shouldn't change the
 // value of the property.
 a.length = 5;
+a[2] = "foo";
 assertEq(delete a[0], false);
 
+assertArrayIsExpected();
 
-assertEq(a.length, 3);
-assertEq(a[0], 4);
-assertEq(a[1], 5);
-assertEq(a[2], 1);
+var watchpointCalled = false;
+// NOTE: Be careful with the position of this test, since this sparsifies the
+// elements and you might not test what you think you're testing otherwise.
+a.watch(2, function(prop, oldValue, newValue) {
+  watchpointCalled = true;
+  assertEq(prop, 2);
+  assertEq(oldValue, 1);
+  assertEq(newValue, "foo");
+});
+
+assertArrayIsExpected();
+
+a.length = 5;
+a[2] = "foo";
+assertEq(watchpointCalled, true);
+assertEq(delete a[0], false);
+
+assertArrayIsExpected();
 
 if (typeof reportCompare === "function")
   reportCompare(true, true);
--- a/js/src/vm/NativeObject-inl.h
+++ b/js/src/vm/NativeObject-inl.h
@@ -94,17 +94,17 @@ NativeObject::setDenseElementHole(Exclus
 }
 
 /* static */ inline void
 NativeObject::removeDenseElementForSparseIndex(ExclusiveContext* cx,
                                                HandleNativeObject obj, uint32_t index)
 {
     MarkObjectGroupFlags(cx, obj, OBJECT_FLAG_NON_PACKED | OBJECT_FLAG_SPARSE_INDEXES);
     if (obj->containsDenseElement(index))
-        obj->setDenseElement(index, MagicValue(JS_ELEMENTS_HOLE));
+        obj->setDenseElementUnchecked(index, MagicValue(JS_ELEMENTS_HOLE));
 }
 
 inline bool
 NativeObject::writeToIndexWouldMarkNotPacked(uint32_t index)
 {
     return getElementsHeader()->initializedLength < index;
 }
 
--- a/js/src/vm/NativeObject.cpp
+++ b/js/src/vm/NativeObject.cpp
@@ -514,18 +514,29 @@ NativeObject::sparsifyDenseElement(Exclu
         return false;
 
     RootedValue value(cx, obj->getDenseElement(index));
     MOZ_ASSERT(!value.isMagic(JS_ELEMENTS_HOLE));
 
     removeDenseElementForSparseIndex(cx, obj, index);
 
     uint32_t slot = obj->slotSpan();
-    if (!obj->addDataProperty(cx, INT_TO_JSID(index), slot, JSPROP_ENUMERATE)) {
-        obj->setDenseElement(index, value);
+
+    RootedId id(cx, INT_TO_JSID(index));
+
+    ShapeTable::Entry* entry = nullptr;
+    if (obj->inDictionaryMode())
+        entry = &obj->lastProperty()->table().search<MaybeAdding::Adding>(id);
+
+    // NOTE: We don't use addDataProperty because we don't want the
+    // extensibility check if we're, for example, sparsifying frozen objects..
+    if (!addPropertyInternal(cx, obj, id, nullptr, nullptr, slot,
+                             obj->getElementsHeader()->elementAttributes(),
+                             0, entry, true)) {
+        obj->setDenseElementUnchecked(index, value);
         return false;
     }
 
     MOZ_ASSERT(slot == obj->slotSpan() - 1);
     obj->initSlot(slot, value);
 
     return true;
 }
@@ -543,17 +554,17 @@ NativeObject::sparsifyDenseElements(js::
         if (obj->getDenseElement(i).isMagic(JS_ELEMENTS_HOLE))
             continue;
 
         if (!sparsifyDenseElement(cx, obj, i))
             return false;
     }
 
     if (initialized)
-        obj->setDenseInitializedLength(0);
+        obj->setDenseInitializedLengthUnchecked(0);
 
     /*
      * Reduce storage for dense elements which are now holes. Explicitly mark
      * the elements capacity as zero, so that any attempts to add dense
      * elements will be caught in ensureDenseElements.
      */
     if (obj->getDenseCapacity()) {
         obj->shrinkElements(cx, 0);
@@ -1003,17 +1014,17 @@ NativeObject::addDataProperty(ExclusiveC
     MOZ_ASSERT(!(attrs & (JSPROP_GETTER | JSPROP_SETTER)));
     RootedNativeObject self(cx, this);
     RootedId id(cx, idArg);
     return addProperty(cx, self, id, nullptr, nullptr, slot, attrs, 0);
 }
 
 Shape*
 NativeObject::addDataProperty(ExclusiveContext* cx, HandlePropertyName name,
-                          uint32_t slot, unsigned attrs)
+                              uint32_t slot, unsigned attrs)
 {
     MOZ_ASSERT(!(attrs & (JSPROP_GETTER | JSPROP_SETTER)));
     RootedNativeObject self(cx, this);
     RootedId id(cx, NameToId(name));
     return addProperty(cx, self, id, nullptr, nullptr, slot, attrs, 0);
 }
 
 template <AllowGC allowGC>
--- a/js/src/vm/NativeObject.h
+++ b/js/src/vm/NativeObject.h
@@ -300,16 +300,22 @@ class ObjectElements
         flags |= FROZEN;
     }
     void markNotFrozen() {
         MOZ_ASSERT(isFrozen());
         MOZ_ASSERT(!isCopyOnWrite());
         flags &= ~FROZEN;
     }
 
+    uint8_t elementAttributes() const {
+        if (isFrozen())
+            return JSPROP_ENUMERATE | JSPROP_PERMANENT | JSPROP_READONLY;
+        return JSPROP_ENUMERATE;
+    }
+
     // This is enough slots to store an object of this class. See the static
     // assertion below.
     static const size_t VALUES_PER_HEADER = 2;
 };
 
 static_assert(ObjectElements::VALUES_PER_HEADER * sizeof(HeapSlot) == sizeof(ObjectElements),
               "ObjectElements doesn't fit in the given number of slots");
 
@@ -859,34 +865,34 @@ class NativeObject : public ShapedObject
 
     // MAX_FIXED_SLOTS is the biggest number of fixed slots our GC
     // size classes will give an object.
     static const uint32_t MAX_FIXED_SLOTS = 16;
 
   protected:
     inline bool updateSlotsForSpan(ExclusiveContext* cx, size_t oldSpan, size_t newSpan);
 
-  public:
+  private:
+    void prepareElementRangeForOverwrite(size_t start, size_t end) {
+        MOZ_ASSERT(end <= getDenseInitializedLength());
+        MOZ_ASSERT(!denseElementsAreCopyOnWrite());
+        for (size_t i = start; i < end; i++)
+            elements_[i].HeapSlot::~HeapSlot();
+    }
+
     /*
      * Trigger the write barrier on a range of slots that will no longer be
      * reachable.
      */
     void prepareSlotRangeForOverwrite(size_t start, size_t end) {
         for (size_t i = start; i < end; i++)
             getSlotAddressUnchecked(i)->HeapSlot::~HeapSlot();
     }
 
-    void prepareElementRangeForOverwrite(size_t start, size_t end) {
-        MOZ_ASSERT(end <= getDenseInitializedLength());
-        MOZ_ASSERT(!denseElementsAreCopyOnWrite());
-        MOZ_ASSERT(!denseElementsAreFrozen());
-        for (size_t i = start; i < end; i++)
-            elements_[i].HeapSlot::~HeapSlot();
-    }
-
+  public:
     static bool rollbackProperties(ExclusiveContext* cx, HandleNativeObject obj,
                                    uint32_t slotSpan);
 
     inline void setSlotWithType(ExclusiveContext* cx, Shape* shape,
                                 const Value& value, bool overwriting = true);
 
     inline const Value& getReservedSlot(uint32_t index) const {
         MOZ_ASSERT(index < JSSLOT_FREE(getClass()));
@@ -1010,32 +1016,45 @@ class NativeObject : public ShapedObject
                 JS::shadow::Runtime* shadowRuntime = shadowRuntimeFromMainThread();
                 shadowRuntime->gcStoreBufferPtr()->putSlot(this, HeapSlot::Element,
                                                            start + i, count - i);
                 return;
             }
         }
     }
 
-  public:
-    void setDenseInitializedLength(uint32_t length) {
+    // See the comment over setDenseElementUnchecked, this applies in the same way.
+    void setDenseInitializedLengthUnchecked(uint32_t length) {
         MOZ_ASSERT(length <= getDenseCapacity());
         MOZ_ASSERT(!denseElementsAreCopyOnWrite());
-        MOZ_ASSERT(!denseElementsAreFrozen());
         prepareElementRangeForOverwrite(length, getElementsHeader()->initializedLength);
         getElementsHeader()->initializedLength = length;
     }
 
+    // Use this function with care.  This is done to allow sparsifying frozen
+    // objects, but should only be called in a few places, and should be
+    // audited carefully!
+    void setDenseElementUnchecked(uint32_t index, const Value& val) {
+        MOZ_ASSERT(index < getDenseInitializedLength());
+        MOZ_ASSERT(!denseElementsAreCopyOnWrite());
+        elements_[index].set(this, HeapSlot::Element, index, val);
+    }
+
+  public:
+    void setDenseInitializedLength(uint32_t length) {
+        MOZ_ASSERT(!denseElementsAreFrozen());
+        setDenseInitializedLengthUnchecked(length);
+    }
+
     inline void ensureDenseInitializedLength(ExclusiveContext* cx,
                                              uint32_t index, uint32_t extra);
+
     void setDenseElement(uint32_t index, const Value& val) {
-        MOZ_ASSERT(index < getDenseInitializedLength());
-        MOZ_ASSERT(!denseElementsAreCopyOnWrite());
         MOZ_ASSERT(!denseElementsAreFrozen());
-        elements_[index].set(this, HeapSlot::Element, index, val);
+        setDenseElementUnchecked(index, val);
     }
 
     void initDenseElement(uint32_t index, const Value& val) {
         MOZ_ASSERT(index < getDenseInitializedLength());
         MOZ_ASSERT(!denseElementsAreCopyOnWrite());
         MOZ_ASSERT(!denseElementsAreFrozen());
         elements_[index].init(this, HeapSlot::Element, index, val);
     }
--- a/js/src/vm/Shape-inl.h
+++ b/js/src/vm/Shape-inl.h
@@ -170,19 +170,17 @@ AutoRooterGetterSetter::AutoRooterGetter
 static inline uint8_t
 GetShapeAttributes(JSObject* obj, Shape* shape)
 {
     MOZ_ASSERT(obj->isNative());
 
     if (IsImplicitDenseOrTypedArrayElement(shape)) {
         if (obj->is<TypedArrayObject>())
             return JSPROP_ENUMERATE | JSPROP_PERMANENT;
-        if (obj->as<NativeObject>().getElementsHeader()->isFrozen())
-            return JSPROP_ENUMERATE | JSPROP_PERMANENT | JSPROP_READONLY;
-        return JSPROP_ENUMERATE;
+        return obj->as<NativeObject>().getElementsHeader()->elementAttributes();
     }
 
     return shape->attributes();
 }
 
 } /* namespace js */
 
 #endif /* vm_Shape_inl_h */
--- a/layout/reftests/w3c-css/submitted/masking/mask-image-6.html
+++ b/layout/reftests/w3c-css/submitted/masking/mask-image-6.html
@@ -12,16 +12,17 @@
       div {
         width: 100px;
         height: 100px;
       }
       span {
         font-size: 100px;
         line-height: 100px;
         mask-image: url(support/transparent-100x50-blue-100x50.png);
+        mask-position: center;
         mask-repeat: repeat;
       }
 
     </style>
   </head>
   <body>
     <div>
       <span>A B</span>
--- a/layout/reftests/w3c-css/submitted/masking/mask-repeat-1-ref.html
+++ b/layout/reftests/w3c-css/submitted/masking/mask-repeat-1-ref.html
@@ -29,19 +29,17 @@
 
       #repeat-y {
         position: absolute;
         width: 50px; height: 100%;
       }
     </style>
   </head>
   <body>
-    <div class="outer">
-      <div class="color" id="default"></div>
-    </div>
+    <div class="outer color"></div>
     <div class="outer">
       <div class="color" id="default"></div>
     </div>
     <div class="outer color"></div>
     <div class="outer">
       <div class="color" id="repeat-x"></div>
     </div>
     <div class="outer">
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -12241,30 +12241,22 @@ CSSParserImpl::ParseImageLayersItem(
   RefPtr<nsCSSValue::Array> positionXArr = nsCSSValue::Array::Create(2);
   RefPtr<nsCSSValue::Array> positionYArr = nsCSSValue::Array::Create(2);
   aState.mPositionX->mValue.SetArrayValue(positionXArr, eCSSUnit_Array);
   aState.mPositionY->mValue.SetArrayValue(positionYArr, eCSSUnit_Array);
 
   if (eCSSProperty_mask == aTable[nsStyleImageLayers::shorthand]) {
     aState.mOrigin->mValue.SetIntValue(NS_STYLE_IMAGELAYER_ORIGIN_BORDER,
                                        eCSSUnit_Enumerated);
-    aState.mRepeat->mXValue.SetIntValue(NS_STYLE_IMAGELAYER_REPEAT_NO_REPEAT,
-                                        eCSSUnit_Enumerated);
-
-    positionXArr->Item(1).SetPercentValue(0.5f);
-    positionYArr->Item(1).SetPercentValue(0.5f);
   } else {
     aState.mOrigin->mValue.SetIntValue(NS_STYLE_IMAGELAYER_ORIGIN_PADDING,
                                        eCSSUnit_Enumerated);
-    aState.mRepeat->mXValue.SetIntValue(NS_STYLE_IMAGELAYER_REPEAT_REPEAT,
-                                        eCSSUnit_Enumerated);
-
-    positionXArr->Item(1).SetPercentValue(0.0f);
-    positionYArr->Item(1).SetPercentValue(0.0f);
-  }
+  }
+  positionXArr->Item(1).SetPercentValue(0.0f);
+  positionYArr->Item(1).SetPercentValue(0.0f);
 
   aState.mSize->mXValue.SetAutoValue();
   aState.mSize->mYValue.SetAutoValue();
   aState.mComposite->mValue.SetIntValue(NS_STYLE_MASK_COMPOSITE_ADD,
                                         eCSSUnit_Enumerated);
   aState.mMode->mValue.SetIntValue(NS_STYLE_MASK_MODE_MATCH_SOURCE,
                                    eCSSUnit_Enumerated);
   bool haveColor = false,
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -6137,17 +6137,17 @@ nsComputedDOMStyle::DoGetMask()
   // a longhand.
   if (svg->mMask.mImageCount > 1 ||
       firstLayer.mClip != NS_STYLE_IMAGELAYER_CLIP_BORDER ||
       firstLayer.mOrigin != NS_STYLE_IMAGELAYER_ORIGIN_BORDER ||
       firstLayer.mComposite != NS_STYLE_MASK_COMPOSITE_ADD ||
       firstLayer.mMaskMode != NS_STYLE_MASK_MODE_MATCH_SOURCE ||
       !nsStyleImageLayers::IsInitialPositionForLayerType(
         firstLayer.mPosition, nsStyleImageLayers::LayerType::Mask) ||
-      !firstLayer.mRepeat.IsInitialValue(nsStyleImageLayers::LayerType::Mask) ||
+      !firstLayer.mRepeat.IsInitialValue() ||
       !firstLayer.mSize.IsInitialValue() ||
       !(firstLayer.mImage.GetType() == eStyleImageType_Null ||
         firstLayer.mImage.GetType() == eStyleImageType_Image)) {
     return nullptr;
   }
 
   RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
 
--- a/layout/style/nsRuleNode.cpp
+++ b/layout/style/nsRuleNode.cpp
@@ -7254,17 +7254,17 @@ nsRuleNode::ComputeBackgroundData(void* 
                     parentBG->mImage.mLayers,
                     &nsStyleImageLayers::Layer::mImage,
                     initialImage, parentBG->mImage.mImageCount,
                     bg->mImage.mImageCount,
                     maxItemCount, rebuild, conditions);
 
   // background-repeat: enum, inherit, initial [pair list]
   nsStyleImageLayers::Repeat initialRepeat;
-  initialRepeat.SetInitialValues(nsStyleImageLayers::LayerType::Background);
+  initialRepeat.SetInitialValues();
   SetImageLayerPairList(aContext, *aRuleData->ValueForBackgroundRepeat(),
                         bg->mImage.mLayers,
                         parentBG->mImage.mLayers,
                         &nsStyleImageLayers::Layer::mRepeat,
                         initialRepeat, parentBG->mImage.mRepeatCount,
                         bg->mImage.mRepeatCount, maxItemCount, rebuild,
                         conditions);
 
@@ -7303,19 +7303,17 @@ nsRuleNode::ComputeBackgroundData(void* 
                     &nsStyleImageLayers::Layer::mOrigin,
                     uint8_t(NS_STYLE_IMAGELAYER_ORIGIN_PADDING),
                     parentBG->mImage.mOriginCount,
                     bg->mImage.mOriginCount, maxItemCount, rebuild,
                     conditions);
 
   // background-position-x/y: enum, length, percent (flags), inherit [list]
   Position::Coord initialPositionCoord;
-  initialPositionCoord.mPercent =
-    nsStyleImageLayers::GetInitialPositionForLayerType(
-      nsStyleImageLayers::LayerType::Background);
+  initialPositionCoord.mPercent = 0.0f;
   initialPositionCoord.mLength = 0;
   initialPositionCoord.mHasPercent = true;
 
   SetImageLayerPositionCoordList(
                     aContext, *aRuleData->ValueForBackgroundPositionX(),
                     bg->mImage.mLayers,
                     parentBG->mImage.mLayers,
                     &Position::mXPosition,
@@ -9955,17 +9953,17 @@ nsRuleNode::ComputeSVGResetData(void* aS
                     &nsStyleImageLayers::Layer::mSourceURI,
                     RefPtr<css::URLValueData>(),
                     parentSVGReset->mMask.mImageCount,
                     svgReset->mMask.mImageCount,
                     maxItemCount, rebuild, conditions);
 
   // mask-repeat: enum, inherit, initial [pair list]
   nsStyleImageLayers::Repeat initialRepeat;
-  initialRepeat.SetInitialValues(nsStyleImageLayers::LayerType::Mask);
+  initialRepeat.SetInitialValues();
   SetImageLayerPairList(aContext, *aRuleData->ValueForMaskRepeat(),
                         svgReset->mMask.mLayers,
                         parentSVGReset->mMask.mLayers,
                         &nsStyleImageLayers::Layer::mRepeat,
                         initialRepeat, parentSVGReset->mMask.mRepeatCount,
                         svgReset->mMask.mRepeatCount, maxItemCount, rebuild,
                         conditions);
 
@@ -9986,19 +9984,17 @@ nsRuleNode::ComputeSVGResetData(void* aS
                     &nsStyleImageLayers::Layer::mOrigin,
                     uint8_t(NS_STYLE_IMAGELAYER_ORIGIN_BORDER),
                     parentSVGReset->mMask.mOriginCount,
                     svgReset->mMask.mOriginCount, maxItemCount, rebuild,
                     conditions);
 
   // mask-position-x/y: enum, length, percent (flags), inherit [list]
   Position::Coord initialPositionCoord;
-  initialPositionCoord.mPercent =
-    nsStyleImageLayers::GetInitialPositionForLayerType(
-      nsStyleImageLayers::LayerType::Mask);
+  initialPositionCoord.mPercent = 0.0f;
   initialPositionCoord.mLength = 0;
   initialPositionCoord.mHasPercent = true;
 
   SetImageLayerPositionCoordList(
                     aContext, *aRuleData->ValueForMaskPositionX(),
                     svgReset->mMask.mLayers,
                     parentSVGReset->mMask.mLayers,
                     &Position::mXPosition,
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -2620,21 +2620,20 @@ nsStyleImageLayers::HasLayerWithImage() 
   }
 
   return false;
 }
 
 bool
 nsStyleImageLayers::IsInitialPositionForLayerType(Position aPosition, LayerType aType)
 {
-  float intialValue = nsStyleImageLayers::GetInitialPositionForLayerType(aType);
-  if (aPosition.mXPosition.mPercent == intialValue &&
+  if (aPosition.mXPosition.mPercent == 0.0f &&
       aPosition.mXPosition.mLength == 0 &&
       aPosition.mXPosition.mHasPercent &&
-      aPosition.mYPosition.mPercent == intialValue &&
+      aPosition.mYPosition.mPercent == 0.0f &&
       aPosition.mYPosition.mLength == 0 &&
       aPosition.mYPosition.mHasPercent) {
     return true;
   }
 
   return false;
 }
 
@@ -2760,43 +2759,16 @@ nsStyleImageLayers::Size::operator==(con
              "bad mHeightType for aOther");
 
   return mWidthType == aOther.mWidthType &&
          mHeightType == aOther.mHeightType &&
          (mWidthType != eLengthPercentage || mWidth == aOther.mWidth) &&
          (mHeightType != eLengthPercentage || mHeight == aOther.mHeight);
 }
 
-bool
-nsStyleImageLayers::Repeat::IsInitialValue(LayerType aType) const
-{
-  if (aType == LayerType::Background) {
-    return mXRepeat == NS_STYLE_IMAGELAYER_REPEAT_REPEAT &&
-           mYRepeat == NS_STYLE_IMAGELAYER_REPEAT_REPEAT;
-  } else {
-    MOZ_ASSERT(aType == LayerType::Mask);
-    return mXRepeat == NS_STYLE_IMAGELAYER_REPEAT_NO_REPEAT &&
-           mYRepeat == NS_STYLE_IMAGELAYER_REPEAT_NO_REPEAT;
-  }
-}
-
-void
-nsStyleImageLayers::Repeat::SetInitialValues(LayerType aType)
-{
-  if (aType == LayerType::Background) {
-    mXRepeat = NS_STYLE_IMAGELAYER_REPEAT_REPEAT;
-    mYRepeat = NS_STYLE_IMAGELAYER_REPEAT_REPEAT;
-  } else {
-    MOZ_ASSERT(aType == LayerType::Mask);
-
-    mXRepeat = NS_STYLE_IMAGELAYER_REPEAT_NO_REPEAT;
-    mYRepeat = NS_STYLE_IMAGELAYER_REPEAT_NO_REPEAT;
-  }
-}
-
 nsStyleImageLayers::Layer::Layer()
   : mClip(NS_STYLE_IMAGELAYER_CLIP_BORDER)
   , mAttachment(NS_STYLE_IMAGELAYER_ATTACHMENT_SCROLL)
   , mBlendMode(NS_STYLE_BLEND_NORMAL)
   , mComposite(NS_STYLE_MASK_COMPOSITE_ADD)
   , mMaskMode(NS_STYLE_MASK_MODE_MATCH_SOURCE)
 {
   mImage.SetNull();
@@ -2805,21 +2777,19 @@ nsStyleImageLayers::Layer::Layer()
 
 nsStyleImageLayers::Layer::~Layer()
 {
 }
 
 void
 nsStyleImageLayers::Layer::Initialize(nsStyleImageLayers::LayerType aType)
 {
-  mRepeat.SetInitialValues(aType);
-
-  float initialPositionValue =
-    nsStyleImageLayers::GetInitialPositionForLayerType(aType);
-  mPosition.SetInitialPercentValues(initialPositionValue);
+  mRepeat.SetInitialValues();
+
+  mPosition.SetInitialPercentValues(0.0f);
 
   if (aType == LayerType::Background) {
     mOrigin = NS_STYLE_IMAGELAYER_ORIGIN_PADDING;
   } else {
     MOZ_ASSERT(aType == LayerType::Mask, "unsupported layer type.");
     mOrigin = NS_STYLE_IMAGELAYER_ORIGIN_BORDER;
   }
 }
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -665,20 +665,16 @@ struct nsStyleImageLayers {
   explicit nsStyleImageLayers(LayerType aType);
   nsStyleImageLayers(const nsStyleImageLayers &aSource);
   ~nsStyleImageLayers() {
     MOZ_COUNT_DTOR(nsStyleImageLayers);
   }
 
   static bool IsInitialPositionForLayerType(mozilla::Position aPosition, LayerType aType);
 
-  static float GetInitialPositionForLayerType(LayerType aType) {
-    return (aType == LayerType::Background) ? 0.0f : 0.5f;
-  }
-
   struct Size;
   friend struct Size;
   struct Size {
     struct Dimension : public nsStyleCoord::CalcValue {
       nscoord ResolveLengthPercentage(nscoord aAvailable) const {
         double d = double(mPercent) * double(aAvailable) + double(mLength);
         if (d < 0.0) {
           return 0;
@@ -740,25 +736,31 @@ struct nsStyleImageLayers {
   struct Repeat;
   friend struct Repeat;
   struct Repeat {
     uint8_t mXRepeat, mYRepeat;
 
     // Initialize nothing
     Repeat() {}
 
-    bool IsInitialValue(LayerType aType) const;
+    bool IsInitialValue() const {
+      return mXRepeat == NS_STYLE_IMAGELAYER_REPEAT_REPEAT &&
+             mYRepeat == NS_STYLE_IMAGELAYER_REPEAT_REPEAT;
+    }
 
     bool DependsOnPositioningAreaSize() const {
       return mXRepeat == NS_STYLE_IMAGELAYER_REPEAT_SPACE ||
              mYRepeat == NS_STYLE_IMAGELAYER_REPEAT_SPACE;
     }
 
     // Initialize to initial values
-    void SetInitialValues(LayerType aType);
+    void SetInitialValues() {
+      mXRepeat = NS_STYLE_IMAGELAYER_REPEAT_REPEAT;
+      mYRepeat = NS_STYLE_IMAGELAYER_REPEAT_REPEAT;
+    }
 
     bool operator==(const Repeat& aOther) const {
       return mXRepeat == aOther.mXRepeat &&
              mYRepeat == aOther.mYRepeat;
     }
     bool operator!=(const Repeat& aOther) const {
       return !(*this == aOther);
     }
--- a/layout/style/test/property_database.js
+++ b/layout/style/test/property_database.js
@@ -7016,78 +7016,78 @@ function SupportsMaskShorthand() {
 
 if (SupportsMaskShorthand()) {
   gCSSProperties["mask"] = {
     domProp: "mask",
     inherited: false,
     type: CSS_TYPE_SHORTHAND_AND_LONGHAND,
     /* FIXME: All mask-border-* should be added when we implement them. */
     subproperties: ["mask-clip", "mask-image", "mask-mode", "mask-origin", "mask-position-x", "mask-position-y", "mask-repeat", "mask-size" , "mask-composite"],
-    initial_values: [ "match-source", "none", "no-repeat", "add", "50% 50%", "center center", "50% 50% / auto", "center / auto", "center center / auto", "50% 50% / auto auto",
-      "center none", "center center none", "none center", "none center center", "none 50% 50%", "center / auto none",
-      "center center / auto auto none",
-      "match-source none no-repeat add center center", "center center no-repeat none add", "none no-repeat add center center / auto", "center center / auto no-repeat none add match-source", "none no-repeat add 50% 50% / auto auto match-source",
+    initial_values: [ "match-source", "none", "repeat", "add", "0% 0%", "top left", "0% 0% / auto", "top left / auto", "left top / auto", "0% 0% / auto auto",
+      "top left none", "left top none", "none left top", "none top left", "none 0% 0%", "top left / auto none", "left top / auto none",
+      "top left / auto auto none",
+      "match-source none repeat add top left", "top left repeat none add", "none repeat add top left / auto", "top left / auto repeat none add match-source", "none repeat add 0% 0% / auto auto match-source",
       "border-box", "border-box border-box" ],
     other_values: [
-      "none alpha no-repeat add center",
+      "none alpha repeat add left top",
       "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKElEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg==)",
-      "no-repeat url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKElEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg==') alpha center add",
+      "no-repeat url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKElEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg==') alpha left top add",
       "repeat-x",
       "repeat-y",
-      "repeat",
-      "none repeat-y alpha add 50% 50%",
+      "no-repeat",
+      "none repeat-y alpha add 0% 0%",
       "subtract",
-      "50% center subtract alpha no-repeat none",
+      "0% top subtract alpha repeat none",
       "top",
       "left",
-      "0% 0%",
-      "top left",
-      "center / 100px",
-      "center / contain",
-      "center / cover",
+      "50% 50%",
+      "center",
+      "top / 100px",
+      "left / contain",
+      "left / cover",
       "10px / 10%",
       "10em / calc(20px)",
-      "center center / 100px 100px",
-      "center center / 100px auto",
-      "center center / 100px 10%",
-      "center center / 100px calc(20px)",
-      "bottom right add none alpha no-repeat",
-      "0% alpha",
-      "alpha 0%",
-      "0%",
+      "top left / 100px 100px",
+      "top left / 100px auto",
+      "top left / 100px 10%",
+      "top left / 100px calc(20px)",
+      "bottom right add none alpha repeat",
+      "50% alpha",
+      "alpha 50%",
+      "50%",
       "url(#mymask)",
-      "-moz-radial-gradient(10% bottom, #ffffff, black) add repeat",
-      "-moz-linear-gradient(10px 10px -45deg, red, blue) no-repeat",
-      "-moz-linear-gradient(10px 10px -0.125turn, red, blue) no-repeat",
-      "-moz-repeating-radial-gradient(10% bottom, #ffffff, black) add repeat",
-      "-moz-repeating-linear-gradient(10px 10px -45deg, red, blue) no-repeat",
+      "-moz-radial-gradient(10% bottom, #ffffff, black) add no-repeat",
+      "-moz-linear-gradient(10px 10px -45deg, red, blue) repeat",
+      "-moz-linear-gradient(10px 10px -0.125turn, red, blue) repeat",
+      "-moz-repeating-radial-gradient(10% bottom, #ffffff, black) add no-repeat",
+      "-moz-repeating-linear-gradient(10px 10px -45deg, red, blue) repeat",
       "-moz-element(#test) alpha",
       /* multiple mask-image */
       "url(404.png), url(404.png)",
       "repeat-x, subtract, none",
-      "50% top url(404.png), url(404.png) 50% top",
+      "0% top url(404.png), url(404.png) 50% top",
       "subtract repeat-y top left url(404.png), repeat-x alpha",
       "url(404.png), -moz-linear-gradient(20px 20px -45deg, blue, green), -moz-element(#a) alpha",
       "top left / contain, bottom right / cover",
       /* test cases with clip+origin in the shorthand */
       "url(404.png) alpha padding-box",
       "url(404.png) border-box alpha",
       "content-box url(404.png)",
       "url(404.png) alpha padding-box padding-box",
       "url(404.png) alpha padding-box border-box",
       "content-box border-box url(404.png)",
     ],
     invalid_values: [
       /* mixes with keywords have to be in correct order */
       "50% left", "top 50%",
       /* no quirks mode colors */
-      "-moz-radial-gradient(10% bottom, ffffff, black) add repeat",
+      "-moz-radial-gradient(10% bottom, ffffff, black) add no-repeat",
       /* no quirks mode lengths */
-      "-moz-linear-gradient(10 10px -45deg, red, blue) no-repeat",
-      "-moz-linear-gradient(10px 10 -45deg, red, blue) no-repeat",
+      "-moz-linear-gradient(10 10px -45deg, red, blue) repeat",
+      "-moz-linear-gradient(10px 10 -45deg, red, blue) repeat",
       "linear-gradient(red -99, yellow, green, blue 120%)",
       /* bug 258080: don't accept background-position separated */
       "left url(404.png) top", "top url(404.png) left",
       "alpha padding-box url(404.png) border-box",
       "alpha padding-box url(404.png) padding-box",
       "-moz-element(#a rubbish)",
       "left top / match-source"
     ]
@@ -7142,19 +7142,18 @@ if (SupportsMaskShorthand()) {
     initial_values: [ "border-box" ],
     other_values: [ "padding-box", "content-box", "border-box, padding-box", "padding-box, padding-box, padding-box", "border-box, border-box" ],
     invalid_values: [ "margin-box", "padding-box padding-box" ]
   };
   gCSSProperties["mask-position"] = {
     domProp: "maskPosition",
     inherited: false,
     type: CSS_TYPE_SHORTHAND_AND_LONGHAND,
-    initial_values: [ "left 50% top 50%", "left 50% center", "center", "center center", "50% 50%", "50% center", "center 50%" ],
-    other_values: [ "top", "left", "right", "bottom", "center bottom", "bottom center", "center right", "right center", "center top", "top center", "center left", "left center", "right bottom", "bottom right", "0%", "top left, top left", "top left, top right", "top right, top left", "left top, 0% 0%", "10% 20%, 30%, 40%", "top left, bottom right", "right bottom, left top", "0%", "0px", "30px", "0%, 10%, 20%, 30%", "top, top, top, top, top",
-      "top 0% left 0%", "top 0% left", "top left", "left top", "0% 0%", "0% top", "left 0%",
+    initial_values: [ "top 0% left 0%", "top 0% left", "top left", "left top", "0% 0%", "0% top", "left 0%" ],
+    other_values: [ "top", "left", "right", "bottom", "center", "center bottom", "bottom center", "center right", "right center", "center top", "top center", "center left", "left center", "right bottom", "bottom right", "50%", "top left, top left", "top left, top right", "top right, top left", "left top, 0% 0%", "10% 20%, 30%, 40%", "top left, bottom right", "right bottom, left top", "0%", "0px", "30px", "0%, 10%, 20%, 30%", "top, top, top, top, top",
       "calc(20px)",
       "calc(20px) 10px",
       "10px calc(20px)",
       "calc(20px) 25%",
       "25% calc(20px)",
       "calc(20px) calc(20px)",
       "calc(20px + 1em) calc(20px / 2)",
       "calc(20px + 50%) calc(50% - 10px)",
@@ -7186,18 +7185,18 @@ if (SupportsMaskShorthand()) {
                       "top bottom", "left 10% right",
                       "top 20px bottom 20px", "left left",
                       "0px calc(0px + rubbish)"],
   };
   gCSSProperties["mask-position-x"] = {
     domProp: "maskPositionX",
     inherited: false,
     type: CSS_TYPE_LONGHAND,
-    initial_values: [ "center", "50%" ],
-    other_values: [ "right", "left", "0%", "left, left", "left, right", "right, left", "left, 0%", "10%, 20%, 40%", "0px", "30px", "0%, 10%, 20%, 30%", "left, left, left, left, left",
+    initial_values: [ "left", "0%" ],
+    other_values: [ "right", "center", "50%", "center, center", "center, right", "right, center", "center, 50%", "10%, 20%, 40%", "1px", "30px", "50%, 10%, 20%, 30%", "center, center, center, center, center",
       "calc(20px)",
       "calc(20px + 1em)",
       "calc(20px / 2)",
       "calc(20px + 50%)",
       "calc(50% - 10px)",
       "calc(-20px)",
       "calc(-50%)",
       "calc(-20%)",
@@ -7212,18 +7211,18 @@ if (SupportsMaskShorthand()) {
                       "bottom 20px", "top 10%", "bottom 3em",
                       "top", "bottom", "top, top", "top, bottom", "bottom, top", "top, 0%", "top, top, top, top, top",
                       "calc(0px + rubbish)", "center 0%"],
   };
   gCSSProperties["mask-position-y"] = {
     domProp: "maskPositionY",
     inherited: false,
     type: CSS_TYPE_LONGHAND,
-    initial_values: [ "center", "50%" ],
-    other_values: [ "bottom", "top", "0%", "top, top", "top, bottom", "bottom, top", "top, 0%", "10%, 20%, 40%", "0px", "30px", "0%, 10%, 20%, 30%", "top, top, top, top, top",
+    initial_values: [ "top", "0%" ],
+    other_values: [ "bottom", "center", "50%", "center, center", "center, bottom", "bottom, center", "center, 0%", "10%, 20%, 40%", "1px", "30px", "50%, 10%, 20%, 30%", "center, center, center, center, center",
       "calc(20px)",
       "calc(20px + 1em)",
       "calc(20px / 2)",
       "calc(20px + 50%)",
       "calc(50% - 10px)",
       "calc(-20px)",
       "calc(-50%)",
       "calc(-20%)",
@@ -7238,25 +7237,26 @@ if (SupportsMaskShorthand()) {
                       "right 20px", "left 10%", "right 3em",
                       "left", "right", "left, left", "left, right", "right, left", "left, 0%", "left, left, left, left, left",
                       "calc(0px + rubbish)", "center 0%"],
   };
   gCSSProperties["mask-repeat"] = {
     domProp: "maskRepeat",
     inherited: false,
     type: CSS_TYPE_LONGHAND,
-    initial_values: [ "no-repeat", "no-repeat no-repeat" ],
-    other_values: [ "repeat-x", "repeat-y", "repeat",
+    initial_values: [ "repeat", "repeat repeat" ],
+    other_values: [ "repeat-x", "repeat-y", "no-repeat",
       "repeat-x, repeat-x",
-      "no-repeat, repeat",
-      "repeat-y, repeat, repeat-y",
-      "no-repeat, no-repeat, no-repeat",
+      "repeat, no-repeat",
+      "repeat-y, no-repeat, repeat-y",
+      "repeat, repeat, repeat",
+      "repeat no-repeat",
       "no-repeat repeat",
+      "no-repeat no-repeat",
       "repeat no-repeat",
-      "repeat repeat",
       "no-repeat no-repeat, no-repeat no-repeat",
     ],
     invalid_values: [ "repeat repeat repeat",
                       "repeat-x repeat-y",
                       "repeat repeat-x",
                       "repeat repeat-y",
                       "repeat-x repeat",
                       "repeat-y repeat" ]
--- a/layout/style/test/test_computed_style.html
+++ b/layout/style/test/test_computed_style.html
@@ -272,24 +272,24 @@ var noframe_container = document.getElem
     // any mask-composite value other than "add".
     "mask-composite": [
       "subtract", "intersect", "exclude"
     ],
     // any mask-mode value other than "match-source".
     "mask-mode": [
       "alpha", "luminance"
     ],
-    // any mask-position value other then "50%" "50% 50%" "center"
+    // any mask-position value other then "0%" "top" "left"
     // "center center".
     "mask-position": [
-      "left", "right", "top", "bottom", "0%", "100%"
+      "0%", "center", "right", "bottom", "50%", "100%"
     ],
-    // any mask-repeat value other then "no-repeat" "no-repeat no-repeat".
+    // any mask-repeat value other then "repeat" "repeat repeat".
     "mask-repeat": [
-      "repeat-x", "repeat-y", "repeat", "space", "round"
+      "repeat-x", "repeat-y", "no-repeat", "space", "round"
     ],
     // any mask-size value other then "auto" "auto auto".
     "mask-size": [
       "10px", "100%", "cover", "contain", "auto 5px"
     ],
   };
 
   // "masks" object contains initial mask longhand values.
@@ -305,20 +305,20 @@ var noframe_container = document.getElem
     ],
     "mask-composite": [
       "add"
     ],
     "mask-mode": [
       "match-source"
     ],
     "mask-position": [
-      "50%", "50% 50%", "center"
+      "0% 0%", "left top"
     ],
     "mask-repeat": [
-      "no-repeat", "no-repeat no-repeat"
+      "repeat", "repeat repeat"
     ],
     "mask-size": [
       "auto", "auto auto"
     ],
   };
 
   var p = document.createElement("p");
   var cs = getComputedStyle(p, "");
--- a/layout/xul/nsMenuPopupFrame.cpp
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -875,16 +875,23 @@ nsMenuPopupFrame::ShowPopup(bool aIsCont
   InvalidateFrameSubtree();
 
   if (mPopupState == ePopupShowing || mPopupState == ePopupPositioning) {
     mPopupState = ePopupOpening;
     mIsOpenChanged = true;
 
     // Clear mouse capture when a popup is opened.
     if (mPopupType == ePopupTypeMenu) {
+      EventStateManager* activeESM =
+        static_cast<EventStateManager*>(
+          EventStateManager::GetActiveEventStateManager());
+      if (activeESM) {
+        EventStateManager::ClearGlobalActiveContent(activeESM);
+      }
+
       nsIPresShell::SetCapturingContent(nullptr, 0);
     }
 
     nsMenuFrame* menuFrame = do_QueryFrame(GetParent());
     if (menuFrame) {
       nsWeakFrame weakFrame(this);
       menuFrame->PopupOpened();
       if (!weakFrame.IsAlive())
--- a/media/mtransport/nricectx.cpp
+++ b/media/mtransport/nricectx.cpp
@@ -238,21 +238,16 @@ nsresult NrIceStunServer::ToNicerStunStr
 
 nsresult NrIceTurnServer::ToNicerTurnStruct(nr_ice_turn_server *server) const {
   memset(server, 0, sizeof(nr_ice_turn_server));
 
   nsresult rv = ToNicerStunStruct(&server->turn_server);
   if (NS_FAILED(rv))
     return rv;
 
-  if (username_.empty())
-    return NS_ERROR_INVALID_ARG;
-  if (password_.empty())
-    return NS_ERROR_INVALID_ARG;
-
   if (!(server->username=r_strdup(username_.c_str())))
     return NS_ERROR_OUT_OF_MEMORY;
 
   // TODO(ekr@rtfm.com): handle non-ASCII passwords somehow?
   // STUN requires they be SASLpreped, but we don't know if
   // they are at this point.
 
   // C++03 23.2.4, Paragraph 1 stipulates that the elements
--- a/media/mtransport/third_party/nICEr/src/ice/ice_ctx.c
+++ b/media/mtransport/third_party/nICEr/src/ice/ice_ctx.c
@@ -667,26 +667,30 @@ static int nr_ice_get_default_local_addr
   {
     int r,_status;
     nr_transport_addr default_addr;
     int i;
 
     if ((r=nr_ice_get_default_address(ctx, ip_version, &default_addr)))
         ABORT(r);
 
-    for(i=0; i<addr_ct; ++i) {
+    for (i=0; i < addr_ct; ++i) {
       if (!nr_transport_addr_cmp(&default_addr, &addrs[i].addr,
                                  NR_TRANSPORT_ADDR_CMP_MODE_ADDR)) {
         if ((r=nr_local_addr_copy(addrp, &addrs[i])))
           ABORT(r);
         break;
       }
     }
-    if (i==addr_ct)
-      ABORT(R_NOT_FOUND);
+
+    if (i == addr_ct) {
+      if ((r=nr_transport_addr_copy(&addrp->addr, &default_addr)))
+        ABORT(r);
+      strlcpy(addrp->addr.ifname, "default route", sizeof(addrp->addr.ifname));
+    }
 
     _status=0;
   abort:
     return(_status);
   }
 
 static int nr_ice_get_local_addresses(nr_ice_ctx *ctx)
   {
@@ -695,47 +699,50 @@ static int nr_ice_get_local_addresses(nr
     nr_local_addr *addrs = 0;
     int i,addr_ct;
     nr_local_addr default_addrs[2];
     int default_addr_ct = 0;
 
     if (!ctx->local_addrs) {
       /* First, gather all the local addresses we have */
       if((r=nr_stun_find_local_addresses(local_addrs,MAXADDRS,&addr_ct))) {
-        r_log(LOG_ICE,LOG_ERR,"ICE(%s): unable to find local addresses",ctx->label);
-        ABORT(r);
+        r_log(LOG_ICE,LOG_ERR,"ICE(%s): unable to gather local addresses, trying default route",ctx->label);
       }
 
-      if (ctx->force_net_interface[0]) {
+      if (ctx->force_net_interface[0] && addr_ct) {
         /* Limit us to only addresses on a single interface */
         int force_addr_ct = 0;
         for(i=0;i<addr_ct;i++){
           if (!strcmp(local_addrs[i].addr.ifname, ctx->force_net_interface)) {
             // copy it down in the array, if needed
             if (i != force_addr_ct) {
               if (r=nr_local_addr_copy(&local_addrs[force_addr_ct], &local_addrs[i])) {
                 ABORT(r);
               }
             }
             force_addr_ct++;
           }
         }
         addr_ct = force_addr_ct;
       }
 
-      if (ctx->flags & NR_ICE_CTX_FLAGS_ONLY_DEFAULT_ADDRS) {
+      if ((!addr_ct) || (ctx->flags & NR_ICE_CTX_FLAGS_ONLY_DEFAULT_ADDRS)) {
         /* Get just the default IPv4 and IPv6 addrs */
         if(!nr_ice_get_default_local_address(ctx, NR_IPV4, local_addrs, addr_ct,
                                              &default_addrs[default_addr_ct])) {
           ++default_addr_ct;
         }
         if(!nr_ice_get_default_local_address(ctx, NR_IPV6, local_addrs, addr_ct,
                                              &default_addrs[default_addr_ct])) {
           ++default_addr_ct;
         }
+        if (!default_addr_ct) {
+          r_log(LOG_ICE,LOG_ERR,"ICE(%s): failed to find default addresses",ctx->label);
+          ABORT(R_FAILED);
+        }
         addrs = default_addrs;
         addr_ct = default_addr_ct;
       }
       else {
         addrs = local_addrs;
       }
 
       /* Sort interfaces by preference */
--- a/media/mtransport/third_party/nICEr/src/net/local_addr.c
+++ b/media/mtransport/third_party/nICEr/src/net/local_addr.c
@@ -51,10 +51,11 @@ int nr_local_addr_fmt_info_string(nr_loc
 
     const char *type = (addr_type & NR_INTERFACE_TYPE_WIRED) ? "wired" :
                        (addr_type & NR_INTERFACE_TYPE_WIFI) ? "wifi" :
                        (addr_type & NR_INTERFACE_TYPE_MOBILE) ? "mobile" :
                        "unknown";
 
     snprintf(buf, len, "%s%s, estimated speed: %d kbps",
              vpn, type, addr->interface.estimated_speed);
+    buf[len - 1] = '\0';
     return (0);
   }
--- a/media/mtransport/third_party/nICEr/src/stun/addrs.c
+++ b/media/mtransport/third_party/nICEr/src/stun/addrs.c
@@ -144,51 +144,52 @@ abort:
       if (my_fn) free(my_fn);
     }
     return(_status);
 }
 
 static int
 stun_get_win32_addrs(nr_local_addr addrs[], int maxaddrs, int *count)
 {
-    int r,_status;
+    int r, _status;
     PIP_ADAPTER_ADDRESSES AdapterAddresses = NULL, tmpAddress = NULL;
-    ULONG buflen;
+    // recomended per https://msdn.microsoft.com/en-us/library/windows/desktop/aa365915(v=vs.85).aspx
+    static const ULONG initialBufLen = 15000;
+    ULONG buflen = initialBufLen;
     char bin_hashed_ifname[NR_MD5_HASH_LENGTH];
     char hex_hashed_ifname[MAXIFNAME];
     int n = 0;
 
     *count = 0;
 
     if (maxaddrs <= 0)
-      ABORT(R_INTERNAL);
+      ABORT(R_BAD_ARGS);
 
-    /* Call GetAdaptersAddresses() twice.  First, just to get the buf length */
+    /* According to MSDN (see above) we have try GetAdapterAddresses() multiple times */
+    for (n = 0; n < 5; n++) {
+      AdapterAddresses = (PIP_ADAPTER_ADDRESSES) RMALLOC(buflen);
+      if (AdapterAddresses == NULL) {
+        r_log(NR_LOG_STUN, LOG_ERR, "Error allocating buf for GetAdaptersAddresses()");
+        ABORT(R_NO_MEMORY);
+      }
 
-    buflen = 0;
+      r = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, NULL, AdapterAddresses, &buflen);
+      if (r == NO_ERROR) {
+        break;
+      }
+      r_log(NR_LOG_STUN, LOG_ERR, "GetAdaptersAddresses() returned error (%d)", r);
+      RFREE(AdapterAddresses);
+    }
 
-    r = GetAdaptersAddresses(AF_UNSPEC, 0, NULL, AdapterAddresses, &buflen);
-    if (r != ERROR_BUFFER_OVERFLOW) {
-      r_log(NR_LOG_STUN, LOG_ERR, "Error getting buf len from GetAdaptersAddresses()");
+    if (n >= 5) {
+      r_log(NR_LOG_STUN, LOG_ERR, "5 failures calling GetAdaptersAddresses()");
       ABORT(R_INTERNAL);
     }
 
-    AdapterAddresses = (PIP_ADAPTER_ADDRESSES) RMALLOC(buflen);
-    if (AdapterAddresses == NULL) {
-      r_log(NR_LOG_STUN, LOG_ERR, "Error allocating buf for GetAdaptersAddresses()");
-      ABORT(R_NO_MEMORY);
-    }
-
-    /* for real, this time */
-
-    r = GetAdaptersAddresses(AF_UNSPEC, 0, NULL, AdapterAddresses, &buflen);
-    if (r != NO_ERROR) {
-      r_log(NR_LOG_STUN, LOG_ERR, "Error getting addresses from GetAdaptersAddresses()");
-      ABORT(R_INTERNAL);
-    }
+    n = 0;
 
     /* Loop through the adapters */
 
     for (tmpAddress = AdapterAddresses; tmpAddress != NULL; tmpAddress = tmpAddress->Next) {
 
       if (tmpAddress->OperStatus != IfOperStatusUp)
         continue;
 
@@ -253,17 +254,20 @@ nr_stun_is_duplicate_addr(nr_local_addr 
 
 static int
 stun_getifaddrs(nr_local_addr addrs[], int maxaddrs, int *count)
 {
   int r,_status;
   struct ifaddrs* if_addrs_head=NULL;
   struct ifaddrs* if_addr;
 
-  *count=0;
+  *count = 0;
+
+  if (maxaddrs <= 0)
+    ABORT(R_BAD_ARGS);
 
   if (getifaddrs(&if_addrs_head) == -1) {
     r_log(NR_LOG_STUN, LOG_ERR, "getifaddrs error e = %d", errno);
     ABORT(R_INTERNAL);
   }
 
   if_addr = if_addrs_head;
 
@@ -390,16 +394,17 @@ nr_stun_remove_duplicate_addrs(nr_local_
             if ((r=nr_local_addr_copy(&tmp[n], &addrs[i])))
                 ABORT(r);
             ++n;
         }
     }
 
     *count = n;
 
+    memset(addrs, 0, *count * sizeof(*addrs));
     /* copy temporary array into passed in/out array */
     for (i = 0; i < *count; ++i) {
         if ((r=nr_local_addr_copy(&addrs[i], &tmp[i])))
             ABORT(r);
     }
 
     _status = 0;
   abort:
@@ -407,30 +412,32 @@ nr_stun_remove_duplicate_addrs(nr_local_
     return _status;
 }
 
 #ifndef USE_PLATFORM_NR_STUN_GET_ADDRS
 
 int
 nr_stun_get_addrs(nr_local_addr addrs[], int maxaddrs, int drop_loopback, int drop_link_local, int *count)
 {
-    int _status=0;
+    int r,_status=0;
     int i;
     char typestr[100];
 
 #ifdef WIN32
     _status = stun_get_win32_addrs(addrs, maxaddrs, count);
 #else
     _status = stun_getifaddrs(addrs, maxaddrs, count);
 #endif
 
-    nr_stun_remove_duplicate_addrs(addrs, drop_loopback, drop_link_local, count);
+    if ((r=nr_stun_remove_duplicate_addrs(addrs, drop_loopback, drop_link_local, count)))
+      ABORT(r);
 
     for (i = 0; i < *count; ++i) {
-    nr_local_addr_fmt_info_string(addrs+i,typestr,sizeof(typestr));
-        r_log(NR_LOG_STUN, LOG_DEBUG, "Address %d: %s on %s, type: %s\n",
+      nr_local_addr_fmt_info_string(addrs+i,typestr,sizeof(typestr));
+      r_log(NR_LOG_STUN, LOG_DEBUG, "Address %d: %s on %s, type: %s\n",
             i,addrs[i].addr.as_string,addrs[i].addr.ifname,typestr);
     }
 
+abort:
     return _status;
 }
 
 #endif
--- a/media/mtransport/third_party/nICEr/src/stun/stun_util.c
+++ b/media/mtransport/third_party/nICEr/src/stun/stun_util.c
@@ -114,22 +114,22 @@ nr_stun_xor_mapped_address(UINT4 magicCo
   abort:
     return _status;
 }
 
 int
 nr_stun_find_local_addresses(nr_local_addr addrs[], int maxaddrs, int *count)
 {
     int r,_status;
-    NR_registry *children = 0;
+    //NR_registry *children = 0;
+
+    *count = 0;
 
     if ((r=NR_reg_get_child_count(NR_STUN_REG_PREF_ADDRESS_PRFX, (unsigned int*)count)))
-        if (r == R_NOT_FOUND)
-            *count = 0;
-        else
+        if (r != R_NOT_FOUND)
             ABORT(r);
 
     if (*count == 0) {
         char allow_loopback;
         char allow_link_local;
 
         if ((r=NR_reg_get_char(NR_STUN_REG_PREF_ALLOW_LOOPBACK_ADDRS, &allow_loopback))) {
             if (r == R_NOT_FOUND)
@@ -177,17 +177,17 @@ nr_stun_find_local_addresses(nr_local_ad
         }
     }
 #endif
 
   done:
 
      _status=0;
  abort:
-     RFREE(children);
+     //RFREE(children);
      return _status;
 }
 
 int
 nr_stun_different_transaction(UCHAR *msg, int len, nr_stun_message *req)
 {
     int _status;
     nr_stun_message_header header;
--- a/media/webrtc/trunk/webrtc/modules/video_capture/mac/avfoundation/video_capture_avfoundation_info_objc.mm
+++ b/media/webrtc/trunk/webrtc/modules/video_capture/mac/avfoundation/video_capture_avfoundation_info_objc.mm
@@ -157,17 +157,24 @@ using namespace videocapturemodule;
     for ( AVFrameRateRange* range in format.videoSupportedFrameRateRanges ) {
         if ( range.maxFrameRate > maxFrameRateRange.maxFrameRate ) {
             maxFrameRateRange = range;
         }
     }
 
     *width = videoDimensions.width;
     *height = videoDimensions.height;
-    *maxFPS = maxFrameRateRange.maxFrameRate;
+
+    // This is to fix setCaptureHeight() which fails for some webcams supporting non-integer framerates.
+    // In setCaptureHeight(), we match the best framerate range by searching a range whose max framerate
+    // is most close to (but smaller than or equal to) the target. Since maxFPS of capability is integer,
+    // we fill in the capability maxFPS with the floor value (e.g., 29) of the real supported fps
+    // (e.g., 29.97). If the target is set to 29, we failed to match the best format with max framerate
+    // 29.97 since it is over the target. Therefore, we need to return a ceiling value as the maxFPS here.
+    *maxFPS = static_cast<int32_t>(ceil(maxFrameRateRange.maxFrameRate));
     *rawType = [VideoCaptureMacAVFoundationUtility fourCCToRawVideoType:CMFormatDescriptionGetMediaSubType(format.formatDescription)];
 
     return [NSNumber numberWithInt:0];
 }
 
 - (NSNumber*)getDeviceNamesFromIndex:(uint32_t)index
     DefaultName:(char*)deviceName
     WithLength:(uint32_t)deviceNameLength
--- a/media/webrtc/trunk/webrtc/modules/video_capture/mac/avfoundation/video_capture_avfoundation_objc.mm
+++ b/media/webrtc/trunk/webrtc/modules/video_capture/mac/avfoundation/video_capture_avfoundation_objc.mm
@@ -233,50 +233,54 @@ using namespace videocapturemodule;
          fromConnection:(AVCaptureConnection *)connection {
 
   [_lock lock];
   if (!_owner) {
     [_lock unlock];
     return;
   }
 
+  CMFormatDescriptionRef formatDescription =
+      CMSampleBufferGetFormatDescription(sampleBuffer);
+  webrtc::RawVideoType rawType =
+      [VideoCaptureMacAVFoundationUtility fourCCToRawVideoType:CMFormatDescriptionGetMediaSubType(formatDescription)];
+  CMVideoDimensions dimensions =
+      CMVideoFormatDescriptionGetDimensions(formatDescription);
+
   VideoCaptureCapability tempCaptureCapability;
-  tempCaptureCapability.width = _frameWidth;
-  tempCaptureCapability.height = _frameHeight;
+  tempCaptureCapability.width = dimensions.width;
+  tempCaptureCapability.height = dimensions.height;
   tempCaptureCapability.maxFPS = _frameRate;
-  tempCaptureCapability.rawType = _rawType;
-
-  if (webrtc::kVideoMJPEG == _rawType) {
-    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
+  tempCaptureCapability.rawType = rawType;
 
-    if (blockBuffer) {
-      char* baseAddress;
-      size_t frameSize;
-      size_t lengthAtOffset;
-      CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffset, &frameSize, &baseAddress);
+  CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
 
-      NSAssert(lengthAtOffset == frameSize, @"lengthAtOffset != frameSize)");
+  if (blockBuffer) {
+    char* baseAddress;
+    size_t frameSize;
+    size_t lengthAtOffset;
+    CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffset, &frameSize, &baseAddress);
 
-      _owner->IncomingFrame((unsigned char*)baseAddress, frameSize,
+    NSAssert(lengthAtOffset == frameSize, @"lengthAtOffset != frameSize)");
+
+    _owner->IncomingFrame((unsigned char*)baseAddress, frameSize,
                             tempCaptureCapability, 0);
-    }
   } else {
     // Get a CMSampleBuffer's Core Video image buffer for the media data
     CVImageBufferRef videoFrame = CMSampleBufferGetImageBuffer(sampleBuffer);
 
-    const int kFlags = 0;
-    if (CVPixelBufferLockBaseAddress(videoFrame, kFlags) == kCVReturnSuccess) {
+    if (CVPixelBufferLockBaseAddress(videoFrame, kCVPixelBufferLock_ReadOnly) == kCVReturnSuccess) {
       void* baseAddress = CVPixelBufferGetBaseAddress(videoFrame);
       size_t bytesPerRow = CVPixelBufferGetBytesPerRow(videoFrame);
       size_t frameHeight = CVPixelBufferGetHeight(videoFrame);
       size_t frameSize = bytesPerRow * frameHeight;
 
       _owner->IncomingFrame((unsigned char*)baseAddress, frameSize,
                             tempCaptureCapability, 0);
-      CVPixelBufferUnlockBaseAddress(videoFrame, kFlags);
+      CVPixelBufferUnlockBaseAddress(videoFrame, kCVPixelBufferLock_ReadOnly);
     }
   }
   [_lock unlock];
   _framesDelivered++;
   _framesRendered++;
 }
 
 @end
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -619,16 +619,18 @@ pref("media.video-queue.default-size", 3
 pref("media.android-media-codec.enabled", true);
 pref("media.android-media-codec.preferred", true);
 // Run decoder in seperate process.
 pref("media.android-remote-codec.enabled", false);
 
 // Enable MSE
 pref("media.mediasource.enabled", true);
 
+pref("media.mediadrm-widevinecdm.visible", true);
+
 // optimize images memory usage
 pref("image.downscale-during-decode.enabled", true);
 
 pref("browser.safebrowsing.downloads.enabled", false);
 
 pref("browser.safebrowsing.id", @MOZ_APP_UA_NAME@);
 
 // True if this is the first time we are showing about:firstrun
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/MediaDrmProxy.java
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+package org.mozilla.gecko.media;
+
+import java.util.UUID;
+
+import org.mozilla.gecko.annotation.WrapForJNI;
+import org.mozilla.gecko.AppConstants;
+
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.media.MediaCrypto;
+import android.media.MediaDrm;
+import android.util.Log;
+import android.os.Build;
+
+public final class MediaDrmProxy {
+    private static final String LOGTAG = "GeckoMediaDrmProxy";
+    private static final boolean DEBUG = false;
+    private static final UUID WIDEVINE_SCHEME_UUID =
+            new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL);
+
+    private static final String WIDEVINE_KEY_SYSTEM = "com.widevine.alpha";
+    @WrapForJNI
+    private static final String AAC = "audio/mp4a-latm";
+    @WrapForJNI
+    private static final String AVC = "video/avc";
+    @WrapForJNI
+    private static final String VORBIS = "audio/vorbis";
+    @WrapForJNI
+    private static final String VP8 = "video/x-vnd.on2.vp8";
+    @WrapForJNI
+    private static final String VP9 = "video/x-vnd.on2.vp9";
+    @WrapForJNI
+    private static final String OPUS = "audio/opus";
+
+    private static boolean isSystemSupported() {
+        // Support versions >= LOLLIPOP
+        if (AppConstants.Versions.preLollipop) {
+            if (DEBUG) Log.d(LOGTAG, "System Not supported !!, current SDK version is " + Build.VERSION.SDK_INT);
+            return false;
+        }
+        return true;
+    }
+
+    @WrapForJNI
+    public static boolean isSchemeSupported(String keySystem) {
+        if (!isSystemSupported()) {
+            return false;
+        }
+        if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) {
+            return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID)
+                    && MediaCrypto.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID);
+        }
+        if (DEBUG) Log.d(LOGTAG, "isSchemeSupported key sytem = " + keySystem);
+        return false;
+    }
+
+    @WrapForJNI
+    public static boolean IsCryptoSchemeSupported(String keySystem,
+                                                  String container) {
+        if (!isSystemSupported()) {
+            return false;
+        }
+        if (keySystem.equals(WIDEVINE_KEY_SYSTEM)) {
+            return MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID, container);
+        }
+        if (DEBUG) Log.d(LOGTAG, "cannot decrypt key sytem = " + keySystem + ", container = " + container);
+        return false;
+    }
+
+    @WrapForJNI
+    public static boolean CanDecode(String mimeType) {
+        for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
+            MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
+            if (info.isEncoder()) {
+                continue;
+            }
+            for (String m : info.getSupportedTypes()) {
+                if (m.equals(mimeType)) {
+                  return true;
+                }
+            }
+        }
+        if (DEBUG) Log.d(LOGTAG, "cannot decode mimetype = " + mimeType);
+        return false;
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -552,16 +552,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'media/AsyncCodec.java',
     'media/AsyncCodecFactory.java',
     'media/AudioFocusAgent.java',
     'media/Codec.java',
     'media/CodecProxy.java',
     'media/FormatParam.java',
     'media/JellyBeanAsyncCodec.java',
     'media/MediaControlService.java',
+    'media/MediaDrmProxy.java',
     'media/MediaManager.java',
     'media/RemoteManager.java',
     'media/Sample.java',
     'media/SamplePool.java',
     'media/VideoPlayer.java',
     'MediaCastingBar.java',
     'MemoryMonitor.java',
     'menu/GeckoMenu.java',
--- a/python/mozboot/mozboot/archlinux.py
+++ b/python/mozboot/mozboot/archlinux.py
@@ -163,18 +163,18 @@ class ArchlinuxBootstrapper(BaseBootstra
 
         self.run_as_root(command)
 
     def pacman_update(self):
         command = ['pacman', '-S', '--refresh']
 
         self.run_as_root(command)
 
-    def run(self, command):
-        subprocess.check_call(command, stdin=sys.stdin)
+    def run(self, command, env=None):
+        subprocess.check_call(command, stdin=sys.stdin, env=env)
 
     def download(self, uri):
         command = ['curl', '-L', '-O', uri]
         self.run(command)
 
     def unpack(self, path, name, ext):
         if ext == 'gz':
             compression = '-z'
@@ -184,18 +184,20 @@ class ArchlinuxBootstrapper(BaseBootstra
             compression == 'x'
 
         name = os.path.join(path, name) + '.tar.' + ext
         command = ['tar', '-x', compression, '-f', name, '-C', path]
         self.run(command)
 
     def makepkg(self, name):
         command = ['makepkg', '-s']
-        self.run(command)
-        pack = glob.glob(name + '*.tar.xz')[0]
+        makepkg_env = os.environ.copy()
+        makepkg_env['PKGEXT'] = '.pkg.tar.xz'
+        self.run(command, env=makepkg_env)
+        pack = glob.glob(name + '*.pkg.tar.xz')[0]
         command = ['pacman', '-U']
         if self.no_interactive:
             command.append('--noconfirm')
         command.append(pack)
         self.run_as_root(command)
 
     def aur_install(self, *packages):
         path = tempfile.mkdtemp()
rename from browser/locales/searchjson.py
rename to python/mozbuild/mozbuild/action/generate_searchjson.py
rename from browser/locales/searchplugins.py
rename to python/mozbuild/mozbuild/action/output_searchplugins_list.py
--- a/python/mozbuild/mozbuild/backend/common.py
+++ b/python/mozbuild/mozbuild/backend/common.py
@@ -1,14 +1,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, unicode_literals
 
+import cPickle as pickle
 import itertools
 import json
 import os
 
 import mozpack.path as mozpath
 
 from mozbuild.backend.base import BuildBackend
 
@@ -365,29 +366,28 @@ class CommonBackend(BuildBackend):
         self._write_unified_files(unified_source_mapping, ipdl_dir, poison_windows_h=False)
         self._handle_ipdl_sources(ipdl_dir, sorted_ipdl_sources, unified_source_mapping)
 
         for config in self._configs:
             self.backend_input_files.add(config.source)
 
         # Write out a machine-readable file describing every test.
         topobjdir = self.environment.topobjdir
-        with self._write_file(mozpath.join(topobjdir, 'all-tests.json')) as fh:
-            json.dump(self._test_manager.tests_by_path, fh)
+        with self._write_file(mozpath.join(topobjdir, 'all-tests.pkl'), mode='rb') as fh:
+            pickle.dump(dict(self._test_manager.tests_by_path), fh, protocol=2)
 
-        with self._write_file(mozpath.join(topobjdir, 'test-defaults.json')) as fh:
-            json.dump(self._test_manager.manifest_defaults, fh)
+        with self._write_file(mozpath.join(topobjdir, 'test-defaults.pkl'), mode='rb') as fh:
+            pickle.dump(self._test_manager.manifest_defaults, fh, protocol=2)
 
-        path = mozpath.join(self.environment.topobjdir, 'test-installs.json')
-        with self._write_file(path) as fh:
-            json.dump({k: v for k, v in self._test_manager.installs_by_path.items()
-                       if k in self._test_manager.deferred_installs},
-                      fh,
-                      sort_keys=True,
-                      indent=4)
+        path = mozpath.join(self.environment.topobjdir, 'test-installs.pkl')
+        with self._write_file(path, mode='rb') as fh:
+            pickle.dump({k: v for k, v in self._test_manager.installs_by_path.items()
+                         if k in self._test_manager.deferred_installs},
+                        fh,
+                        protocol=2)
 
         # Write out a machine-readable file describing binaries.
         with self._write_file(mozpath.join(topobjdir, 'binaries.json')) as fh:
             d = {
                 'shared_libraries': [s.to_dict() for s in self._binaries.shared_libraries],
                 'programs': [p.to_dict() for p in self._binaries.programs],
             }
             json.dump(d, fh, sort_keys=True, indent=4)
--- a/python/mozbuild/mozbuild/test/backend/test_recursivemake.py
+++ b/python/mozbuild/mozbuild/test/backend/test_recursivemake.py
@@ -1,14 +1,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import unicode_literals
 
+import cPickle as pickle
 import json
 import os
 import unittest
 
 from mozpack.manifests import (
     InstallManifest,
 )
 from mozunit import main
@@ -519,21 +520,21 @@ class TestRecursiveMakeBackend(BackendTe
         lines = [l.strip() for l in open(x_master, 'rt').readlines()]
         self.assertEqual(lines, [
             '; THIS FILE WAS AUTOMATICALLY GENERATED. DO NOT MODIFY BY HAND.',
             '',
             '[include:dir1/xpcshell.ini]',
             '[include:xpcshell.ini]',
         ])
 
-        all_tests_path = mozpath.join(env.topobjdir, 'all-tests.json')
+        all_tests_path = mozpath.join(env.topobjdir, 'all-tests.pkl')
         self.assertTrue(os.path.exists(all_tests_path))
 
-        with open(all_tests_path, 'rt') as fh:
-            o = json.load(fh)
+        with open(all_tests_path, 'rb') as fh:
+            o = pickle.load(fh)
 
             self.assertIn('xpcshell.js', o)
             self.assertIn('dir1/test_bar.js', o)
 
             self.assertEqual(len(o['xpcshell.js']), 1)
 
     def test_test_manifest_pattern_matches_recorded(self):
         """Pattern matches in test manifests' support-files should be recorded."""
@@ -545,39 +546,39 @@ class TestRecursiveMakeBackend(BackendTe
         # done.
         entries = [e for e in m._dests.keys() if '**' in e]
         self.assertEqual(len(entries), 1)
         self.assertIn('support/**', entries[0])
 
     def test_test_manifest_deffered_installs_written(self):
         """Shared support files are written to their own data file by the backend."""
         env = self._consume('test-manifest-shared-support', RecursiveMakeBackend)
-        all_tests_path = mozpath.join(env.topobjdir, 'all-tests.json')
+        all_tests_path = mozpath.join(env.topobjdir, 'all-tests.pkl')
         self.assertTrue(os.path.exists(all_tests_path))
-        test_installs_path = mozpath.join(env.topobjdir, 'test-installs.json')
+        test_installs_path = mozpath.join(env.topobjdir, 'test-installs.pkl')
 
         with open(test_installs_path, 'r') as fh:
-            test_installs = json.load(fh)
+            test_installs = pickle.load(fh)
 
         self.assertEqual(set(test_installs.keys()),
                          set(['child/test_sub.js',
                               'child/data/**',
                               'child/another-file.sjs']))
         for key in test_installs.keys():
             self.assertIn(key, test_installs)
 
         test_files_manifest = mozpath.join(env.topobjdir,
                                            '_build_manifests',
                                            'install',
                                            '_test_files')
 
         # First, read the generated for ini manifest contents.
         m = InstallManifest(path=test_files_manifest)
 
-        # Then, synthesize one from the test-installs.json file. This should
+        # Then, synthesize one from the test-installs.pkl file. This should
         # allow us to re-create a subset of the above.
         synthesized_manifest = InstallManifest()
         for item, installs in test_installs.items():
             for install_info in installs:
                 if len(install_info) == 3:
                     synthesized_manifest.add_pattern_symlink(*install_info)
                 if len(install_info) == 2:
                     synthesized_manifest.add_symlink(*install_info)
@@ -845,21 +846,21 @@ class TestRecursiveMakeBackend(BackendTe
             # Destination and install manifest are relative to topobjdir.
             stem = '%s/android_eclipse/%s' % (env.topobjdir, project_name)
             self.assertIn(command_template % (stem, stem), lines)
 
     def test_install_manifests_package_tests(self):
         """Ensure test suites honor package_tests=False."""
         env = self._consume('test-manifests-package-tests', RecursiveMakeBackend)
 
-        all_tests_path = mozpath.join(env.topobjdir, 'all-tests.json')
+        all_tests_path = mozpath.join(env.topobjdir, 'all-tests.pkl')
         self.assertTrue(os.path.exists(all_tests_path))
 
-        with open(all_tests_path, 'rt') as fh:
-            o = json.load(fh)
+        with open(all_tests_path, 'rb') as fh:
+            o = pickle.load(fh)
             self.assertIn('mochitest.js', o)
             self.assertIn('not_packaged.java', o)
 
         man_dir = mozpath.join(env.topobjdir, '_build_manifests', 'install')
         self.assertTrue(os.path.isdir(man_dir))
 
         full = mozpath.join(man_dir, '_test_files')
         self.assertTrue(os.path.exists(full))
--- a/python/mozbuild/mozbuild/test/test_preprocessor.py
+++ b/python/mozbuild/mozbuild/test/test_preprocessor.py
@@ -614,23 +614,23 @@ class TestPreprocessor(unittest.TestCase
                               '//@line 6 "CWD/f.js"\n'
                               'fin\n').replace('CWD/',
                                                os.getcwd() + os.path.sep))
 
     def test_include_missing_file(self):
         with MockedOpen({'f': '#include foo\n'}):
             with self.assertRaises(Preprocessor.Error) as e:
                 self.pp.do_include('f')
-                self.assertEqual(e.key, 'FILE_NOT_FOUND')
+            self.assertEqual(e.exception.key, 'FILE_NOT_FOUND')
 
     def test_include_undefined_variable(self):
         with MockedOpen({'f': '#filter substitution\n#include @foo@\n'}):
             with self.assertRaises(Preprocessor.Error) as e:
                 self.pp.do_include('f')
-                self.assertEqual(e.key, 'UNDEFINED_VAR')
+            self.assertEqual(e.exception.key, 'UNDEFINED_VAR')
 
     def test_include_literal_at(self):
         files = {
             '@foo@': '#define foo foobarbaz\n',
             'f': '#include @foo@\n#filter substitution\n@foo@\n',
         }
 
         with MockedOpen(files):
--- a/python/mozbuild/mozbuild/test/test_testing.py
+++ b/python/mozbuild/mozbuild/test/test_testing.py
@@ -1,14 +1,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import unicode_literals
 
+import cPickle as pickle
 import os
 import shutil
 import tempfile
 import unittest
 
 import mozpack.path as mozpath
 
 from mozfile import NamedTemporaryFile
@@ -16,18 +17,17 @@ from mozunit import main
 
 from mozbuild.base import MozbuildObject
 from mozbuild.testing import (
     TestMetadata,
     TestResolver,
 )
 
 
-ALL_TESTS_JSON = b'''
-{
+ALL_TESTS = {
     "accessible/tests/mochitest/actions/test_anchors.html": [
         {
             "dir_relpath": "accessible/tests/mochitest/actions",
             "expected": "pass",
             "file_relpath": "accessible/tests/mochitest/actions/test_anchors.html",
             "flavor": "a11y",
             "here": "/Users/gps/src/firefox/accessible/tests/mochitest/actions",
             "manifest": "/Users/gps/src/firefox/accessible/tests/mochitest/actions/a11y.ini",
@@ -149,41 +149,41 @@ ALL_TESTS_JSON = b'''
             "manifest": "/home/chris/m-c/devtools/client/markupview/test/browser.ini",
             "name": "browser_markupview_copy_image_data.js",
             "path": "/home/chris/m-c/obj-dbg/_tests/testing/mochitest/browser/devtools/client/markupview/test/browser_markupview_copy_image_data.js",
             "relpath": "devtools/client/markupview/test/browser_markupview_copy_image_data.js",
             "subsuite": "devtools",
             "tags": "devtools"
         }
    ]
-}'''.strip()
+}
 
-TEST_DEFAULTS = b'''{
-    "/Users/gps/src/firefox/toolkit/mozapps/update/test/unit/xpcshell_updater.ini": {"support-files": "\\ndata/**\\nxpcshell_updater.ini"}
-}'''
+TEST_DEFAULTS = {
+    "/Users/gps/src/firefox/toolkit/mozapps/update/test/unit/xpcshell_updater.ini": {"support-files": "\ndata/**\nxpcshell_updater.ini"}
+}
 
 
 class Base(unittest.TestCase):
     def setUp(self):
         self._temp_files = []
 
     def tearDown(self):
         for f in self._temp_files:
             del f
 
         self._temp_files = []
 
     def _get_test_metadata(self):
-        all_tests = NamedTemporaryFile()
-        all_tests.write(ALL_TESTS_JSON)
+        all_tests = NamedTemporaryFile(mode='wb')
+        pickle.dump(ALL_TESTS, all_tests)
         all_tests.flush()
         self._temp_files.append(all_tests)
 
-        test_defaults = NamedTemporaryFile()
-        test_defaults.write(TEST_DEFAULTS)
+        test_defaults = NamedTemporaryFile(mode='wb')
+        pickle.dump(TEST_DEFAULTS, test_defaults)
         test_defaults.flush()
         self._temp_files.append(test_defaults)
 
         return TestMetadata(all_tests.name, test_defaults=test_defaults.name)
 
 
 class TestTestMetadata(Base):
     def test_load(self):
@@ -246,20 +246,20 @@ class TestTestResolver(Base):
 
         for d in self._temp_dirs:
             shutil.rmtree(d)
 
     def _get_resolver(self):
         topobjdir = tempfile.mkdtemp()
         self._temp_dirs.append(topobjdir)
 
-        with open(os.path.join(topobjdir, 'all-tests.json'), 'wt') as fh:
-            fh.write(ALL_TESTS_JSON)
-        with open(os.path.join(topobjdir, 'test-defaults.json'), 'wt') as fh:
-            fh.write(TEST_DEFAULTS)
+        with open(os.path.join(topobjdir, 'all-tests.pkl'), 'wb') as fh:
+            pickle.dump(ALL_TESTS, fh)
+        with open(os.path.join(topobjdir, 'test-defaults.pkl'), 'wb') as fh:
+            pickle.dump(TEST_DEFAULTS, fh)
 
         o = MozbuildObject(self.FAKE_TOPSRCDIR, None, None, topobjdir=topobjdir)
 
         # Monkey patch the test resolver to avoid tests failing to find make
         # due to our fake topscrdir.
         TestResolver._run_make = lambda *a, **b: None
 
         return o._spawn(TestResolver)
--- a/python/mozbuild/mozbuild/testing.py
+++ b/python/mozbuild/mozbuild/testing.py
@@ -1,15 +1,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, unicode_literals
 
-import json
+import cPickle as pickle
 import os
 import sys
 
 import mozpack.path as mozpath
 
 from mozpack.copier import FileCopier
 from mozpack.manifests import InstallManifest
 
@@ -51,22 +51,22 @@ class TestMetadata(object):
     configuration.
     """
 
     def __init__(self, all_tests, test_defaults=None):
         self._tests_by_path = OrderedDefaultDict(list)
         self._tests_by_flavor = defaultdict(set)
         self._test_dirs = set()
 
-        with open(all_tests, 'rt') as fh:
-            test_data = json.load(fh)
+        with open(all_tests, 'rb') as fh:
+            test_data = pickle.load(fh)
         defaults = None
         if test_defaults:
-            with open(test_defaults, 'rt') as fh:
-                defaults = json.load(fh)
+            with open(test_defaults, 'rb') as fh:
+                defaults = pickle.load(fh)
         for path, tests in test_data.items():
             for metadata in tests:
                 if defaults:
                     manifest = metadata['manifest']
                     manifest_defaults = defaults.get(manifest)
                     if manifest_defaults:
                         metadata = manifestparser.combine_fields(manifest_defaults,
                                                                  metadata)
@@ -173,24 +173,24 @@ class TestMetadata(object):
 class TestResolver(MozbuildObject):
     """Helper to resolve tests from the current environment to test files."""
 
     def __init__(self, *args, **kwargs):
         MozbuildObject.__init__(self, *args, **kwargs)
 
         # If installing tests is going to result in re-generating the build
         # backend, we need to do this here, so that the updated contents of
-        # all-tests.json make it to the set of tests to run.
+        # all-tests.pkl make it to the set of tests to run.
         self._run_make(target='run-tests-deps', pass_thru=True,
                        print_directory=False)
 
         self._tests = TestMetadata(os.path.join(self.topobjdir,
-                                                'all-tests.json'),
+                                                'all-tests.pkl'),
                                    test_defaults=os.path.join(self.topobjdir,
-                                                              'test-defaults.json'))
+                                                              'test-defaults.pkl'))
 
         self._test_rewrites = {
             'a11y': os.path.join(self.topobjdir, '_tests', 'testing',
                 'mochitest', 'a11y'),
             'browser-chrome': os.path.join(self.topobjdir, '_tests', 'testing',
                 'mochitest', 'browser'),
             'jetpack-package': os.path.join(self.topobjdir, '_tests', 'testing',
                 'mochitest', 'jetpack-package'),
@@ -412,19 +412,19 @@ class SupportFilesConverter(object):
                     info.installs.append((full, mozpath.normpath(dest_path)))
         return info
 
 def _resolve_installs(paths, topobjdir, manifest):
     """Using the given paths as keys, find any unresolved installs noted
     by the build backend corresponding to those keys, and add them
     to the given manifest.
     """
-    filename = os.path.join(topobjdir, 'test-installs.json')
-    with open(filename, 'r') as fh:
-        resolved_installs = json.load(fh)
+    filename = os.path.join(topobjdir, 'test-installs.pkl')
+    with open(filename, 'rb') as fh:
+        resolved_installs = pickle.load(fh)
 
     for path in paths:
         path = path[2:]
         if path not in resolved_installs:
             raise Exception('A cross-directory support file path noted in a '
                 'test manifest does not appear in any other manifest.\n "%s" '
                 'must appear in another test manifest to specify an install '
                 'for "!/%s".' % (path, path))
--- a/services/sync/modules/addonsreconciler.js
+++ b/services/sync/modules/addonsreconciler.js
@@ -440,16 +440,17 @@ AddonsReconciler.prototype = {
       this._addons[id] = record;
       this._log.debug("Adding change because add-on not present locally: " +
                       id);
       this._addChange(now, CHANGE_INSTALLED, record);
       return;
     }
 
     let record = this._addons[id];
+    record.isSyncable = addon.isSyncable;
 
     if (!record.installed) {
       // It is possible the record is marked as uninstalled because an
       // uninstall is pending.
       if (!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL)) {
         record.installed = true;
         record.modified = now;
       }
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -3817,16 +3817,23 @@
     "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "58",
     "kind": "exponential",
     "high": 1000,
     "n_buckets": 10,
     "bug_numbers": [1283007],
     "description": "Time spent fallocating Variable-Length PrefixSet (ms)"
   },
+  "URLCLASSIFIER_VLPS_LOAD_CORRUPT": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+    "expires_in_version": "58",
+    "kind": "boolean",
+    "bug_numbers": [1305581],
+    "description": "Whether or not a variable-length prefix set loaded from disk is corrupted (true = file corrupted)."
+  },
   "URLCLASSIFIER_LC_PREFIXES": {
     "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "linear",
     "high": 1500000,
     "n_buckets": 15,
     "description": "Size of the prefix cache in entries"
   },
@@ -3862,17 +3869,17 @@
     "description": "This metric is recorded every time a gethash lookup is performed, `true` is recorded if the lookup times out."
   },
   "URLCLASSIFIER_UPDATE_ERROR_TYPE": {
     "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "58",
     "kind": "enumerated",
     "n_values": 10,
     "bug_numbers": [1305801],
-    "description": "An error was encountered while parsing a partial update returned by a Safe Browsing V4 server (0 = addition of an already existing prefix, 1 = parser got into an infinite loop, 2 = removal index out of bounds)"
+    "description": "An error was encountered while parsing a partial update returned by a Safe Browsing V4 server (0 = addition of an already existing prefix, 1 = parser got into an infinite loop, 2 = removal index out of bounds, 3 = checksum mismatch, 4 = missing checksum)"
   },
   "CSP_DOCUMENTS_COUNT": {
     "alert_emails": ["seceng@mozilla.com"],
     "bug_numbers": [1252829],
     "expires_in_version": "55",
     "kind": "count",
     "description": "Number of unique pages that contain a CSP"
   },
--- a/toolkit/components/telemetry/TelemetrySend.jsm
+++ b/toolkit/components/telemetry/TelemetrySend.jsm
@@ -902,16 +902,19 @@ var TelemetrySendImpl = {
 
     request.open("POST", url, true);
     request.overrideMimeType("text/plain");
     request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
     request.setRequestHeader("Date", Policy.now().toUTCString());
 
     this._pendingPingRequests.set(id, request);
 
+    // Prevent the request channel from running though URLClassifier (bug 1296802)
+    request.channel.loadFlags &= ~Ci.nsIChannel.LOAD_CLASSIFY_URI;
+
     let startTime = new Date();
     let deferred = PromiseUtils.defer();
 
     let onRequestFinished = (success, event) => {
       let onCompletion = () => {
         if (success) {
           deferred.resolve();
         } else {
--- a/toolkit/components/url-classifier/Classifier.cpp
+++ b/toolkit/components/url-classifier/Classifier.cpp
@@ -978,63 +978,59 @@ Classifier::UpdateTableV4(nsTArray<Table
   LookupCacheV4* lookupCache =
     LookupCache::Cast<LookupCacheV4>(GetLookupCache(aTable));
   if (!lookupCache) {
     return NS_ERROR_FAILURE;
   }
 
   nsresult rv = NS_OK;
 
-  // prefixes2 is only used in partial update. If there are multiple
-  // updates for the same table, prefixes1 & prefixes2 will act as
-  // input and output in turn to reduce memory copy overhead.
+  // If there are multiple updates for the same table, prefixes1 & prefixes2
+  // will act as input and output in turn to reduce memory copy overhead.
   PrefixStringMap prefixes1, prefixes2;
-  PrefixStringMap* output = &prefixes1;
+  PrefixStringMap* input = &prefixes1;
+  PrefixStringMap* output = &prefixes2;
 
   TableUpdateV4* lastAppliedUpdate = nullptr;
   for (uint32_t i = 0; i < aUpdates->Length(); i++) {
     TableUpdate *update = aUpdates->ElementAt(i);
     if (!update || !update->TableName().Equals(aTable)) {
       continue;
     }
 
     auto updateV4 = TableUpdate::Cast<TableUpdateV4>(update);
     NS_ENSURE_TRUE(updateV4, NS_ERROR_FAILURE);
 
     if (updateV4->IsFullUpdate()) {
-      TableUpdateV4::PrefixStdStringMap& map = updateV4->Prefixes();
-
+      input->Clear();
       output->Clear();
-      for (auto iter = map.Iter(); !iter.Done(); iter.Next()) {
-        // prefixes is an nsClassHashtable object stores prefix string.
-        // It will take the ownership of the put object.
-        nsCString* prefix = new nsCString(iter.Data()->GetPrefixString());
-        output->Put(iter.Key(), prefix);
+      rv = lookupCache->ApplyUpdate(updateV4, *input, *output);
+      if (NS_FAILED(rv)) {
+        return rv;
       }
     } else {
-      PrefixStringMap* input = nullptr;
       // If both prefix sets are empty, this means we are doing a partial update
       // without a prior full/partial update in the loop. In this case we should
       // get prefixes from the lookup cache first.
       if (prefixes1.IsEmpty() && prefixes2.IsEmpty()) {
         lookupCache->GetPrefixes(prefixes1);
-        input = &prefixes1;
-        output = &prefixes2;
       } else {
         MOZ_ASSERT(prefixes1.IsEmpty() ^ prefixes2.IsEmpty());
 
         // When there are multiple partial updates, input should always point
         // to the non-empty prefix set(filled by previous full/partial update).
         // output should always point to the empty prefix set.
         input = prefixes1.IsEmpty() ? &prefixes2 : &prefixes1;
         output = prefixes1.IsEmpty() ? &prefixes1 : &prefixes2;
       }
 
-      rv = lookupCache->ApplyPartialUpdate(updateV4, *input, *output);
-      NS_ENSURE_SUCCESS(rv, rv);
+      rv = lookupCache->ApplyUpdate(updateV4, *input, *output);
+      if (NS_FAILED(rv)) {
+        return rv;
+      }
 
       input->Clear();
     }
 
     // Keep track of the last applied update.
     lastAppliedUpdate = updateV4;
 
     aUpdates->ElementAt(i) = nullptr;
--- a/toolkit/components/url-classifier/LookupCacheV4.cpp
+++ b/toolkit/components/url-classifier/LookupCacheV4.cpp
@@ -1,15 +1,17 @@
 //* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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 "LookupCacheV4.h"
 #include "HashStore.h"
+#include "mozilla/Unused.h"
+#include <string>
 
 // MOZ_LOG=UrlClassifierDbService:5
 extern mozilla::LazyLogModule gUrlClassifierDbServiceLog;
 #define LOG(args) MOZ_LOG(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug, args)
 #define LOG_ENABLED() MOZ_LOG_TEST(gUrlClassifierDbServiceLog, mozilla::LogLevel::Debug)
 
 #define METADATA_SUFFIX NS_LITERAL_CSTRING(".metadata")
 
@@ -109,17 +111,32 @@ nsresult
 LookupCacheV4::StoreToFile(nsIFile* aFile)
 {
   return mVLPrefixSet->StoreToFile(aFile);
 }
 
 nsresult
 LookupCacheV4::LoadFromFile(nsIFile* aFile)
 {
-  return mVLPrefixSet->LoadFromFile(aFile);
+  nsresult rv = mVLPrefixSet->LoadFromFile(aFile);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  nsCString state, checksum;
+  rv = LoadMetadata(state, checksum);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  rv = VerifyChecksum(checksum);
+  Telemetry::Accumulate(Telemetry::URLCLASSIFIER_VLPS_LOAD_CORRUPT,
+                        rv == NS_ERROR_FILE_CORRUPTED);
+
+  return rv;
 }
 
 size_t
 LookupCacheV4::SizeOfPrefixSet()
 {
   return mVLPrefixSet->SizeOfIncludingThis(moz_malloc_size_of);
 }
 
@@ -132,111 +149,196 @@ AppendPrefixToMap(PrefixStringMap& prefi
 
   nsCString* prefixString = prefixes.LookupOrAdd(prefix.Length());
   prefixString->Append(prefix.BeginReading(), prefix.Length());
 }
 
 // Please see https://bug1287058.bmoattachments.org/attachment.cgi?id=8795366
 // for detail about partial update algorithm.
 nsresult
-LookupCacheV4::ApplyPartialUpdate(TableUpdateV4* aTableUpdate,
-                                  PrefixStringMap& aInputMap,
-                                  PrefixStringMap& aOutputMap)
+LookupCacheV4::ApplyUpdate(TableUpdateV4* aTableUpdate,
+                           PrefixStringMap& aInputMap,
+                           PrefixStringMap& aOutputMap)
 {
   MOZ_ASSERT(aOutputMap.IsEmpty());
 
+  nsCOMPtr<nsICryptoHash> crypto;
+  nsresult rv = InitCrypto(crypto);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
   // oldPSet contains prefixes we already have or we just merged last round.
   // addPSet contains prefixes stored in tableUpdate which should be merged with oldPSet.
   VLPrefixSet oldPSet(aInputMap);
   VLPrefixSet addPSet(aTableUpdate->Prefixes());
 
   // RemovalIndiceArray is a sorted integer array indicating the index of prefix we should
   // remove from the old prefix set(according to lexigraphic order).
   // |removalIndex| is the current index of RemovalIndiceArray.
   // |numOldPrefixPicked| is used to record how many prefixes we picked from the old map.
   TableUpdateV4::RemovalIndiceArray& removalArray = aTableUpdate->RemovalIndices();
   uint32_t removalIndex = 0;
   int32_t numOldPrefixPicked = -1;
 
   nsDependentCSubstring smallestOldPrefix;
   nsDependentCSubstring smallestAddPrefix;
 
+  bool isOldMapEmpty = false, isAddMapEmpty = false;
+
   // This is used to avoid infinite loop for partial update algorithm.
   // The maximum loops will be the number of old prefixes plus the number of add prefixes.
-  uint32_t index = oldPSet.Count() + addPSet.Count() + 1;
+  int32_t index = oldPSet.Count() + addPSet.Count() + 1;
   for(;index > 0; index--) {
     // Get smallest prefix from the old prefix set if we don't have one
-    if (smallestOldPrefix.IsEmpty()) {
-      // If prefixes from the old prefix set are all merged,
-      // then we can merge the entire add prefix set directly.
-      if (!oldPSet.GetSmallestPrefix(smallestOldPrefix)) {
-        AppendPrefixToMap(aOutputMap, smallestAddPrefix);
-        addPSet.Merge(aOutputMap);
-        break;
-      }
+    if (smallestOldPrefix.IsEmpty() && !isOldMapEmpty) {
+      isOldMapEmpty = !oldPSet.GetSmallestPrefix(smallestOldPrefix);
     }
 
     // Get smallest prefix from add prefix set if we don't have one
-    if (smallestAddPrefix.IsEmpty()) {
-      // If add prefixes are all merged and there is no removalIndices left,
-      // then merge the entire old prefix set directly. If there are still
-      // removalIndices left, we should still merge prefixes one by one
-      // to know which prefix from old prefix set should be removed.
-      if (!addPSet.GetSmallestPrefix(smallestAddPrefix) &&
-        removalIndex >= removalArray.Length()) {
-        AppendPrefixToMap(aOutputMap, smallestOldPrefix);
-        oldPSet.Merge(aOutputMap);
-        break;
-      }
+    if (smallestAddPrefix.IsEmpty() && !isAddMapEmpty) {
+      isAddMapEmpty = !addPSet.GetSmallestPrefix(smallestAddPrefix);
     }
 
-    // Compare the smallest string in old prefix set and add prefix set, merge the
-    // smaller one into new map to ensure merged string still follows
-    // lexigraphic order.
-    if (smallestOldPrefix < smallestAddPrefix ||
-        smallestAddPrefix.IsEmpty()) {
+    bool pickOld;
+
+    // If both prefix sets are not empty, then compare to find the smaller one.
+    if (!isOldMapEmpty && !isAddMapEmpty) {
+      if (smallestOldPrefix == smallestAddPrefix) {
+        LOG(("Add prefix should not exist in the original prefix set."));
+        Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR_TYPE,
+                              DUPLICATE_PREFIX);
+        return NS_ERROR_FAILURE;
+      }
+
+      // Compare the smallest string in old prefix set and add prefix set,
+      // merge the smaller one into new map to ensure merged string still
+      // follows lexigraphic order.
+      pickOld = smallestOldPrefix < smallestAddPrefix;
+    } else if (!isOldMapEmpty && isAddMapEmpty) {
+      pickOld = true;
+    } else if (isOldMapEmpty && !isAddMapEmpty) {
+      pickOld = false;
+    // If both maps are empty, then partial update is complete.
+    } else {
+      break;
+    }
+
+    if (pickOld) {
       numOldPrefixPicked++;
 
       // If the number of picks from old map matches the removalIndex, then this prefix
       // will be removed by not merging it to new map.
       if (removalIndex < removalArray.Length() &&
           numOldPrefixPicked == removalArray[removalIndex]) {
         removalIndex++;
       } else {
         AppendPrefixToMap(aOutputMap, smallestOldPrefix);
+
+        crypto->Update(reinterpret_cast<uint8_t*>(const_cast<char*>(
+                       smallestOldPrefix.BeginReading())),
+                       smallestOldPrefix.Length());
       }
       smallestOldPrefix.SetLength(0);
-    } else if (smallestOldPrefix > smallestAddPrefix ||
-               smallestOldPrefix.IsEmpty()){
+    } else {
       AppendPrefixToMap(aOutputMap, smallestAddPrefix);
+
+      crypto->Update(reinterpret_cast<uint8_t*>(const_cast<char*>(
+                     smallestAddPrefix.BeginReading())),
+                     smallestAddPrefix.Length());
+
       smallestAddPrefix.SetLength(0);
-    } else {
-      NS_WARNING("Add prefix should not exist in the original prefix set.");
-      Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR_TYPE,
-                            DUPLICATE_PREFIX);
-      return NS_ERROR_FAILURE;
     }
   }
 
   // We expect index will be greater to 0 because max number of runs will be
   // the number of original prefix plus add prefix.
   if (index <= 0) {
-    NS_WARNING("There are still prefixes remaining after reaching maximum runs.");
+    LOG(("There are still prefixes remaining after reaching maximum runs."));
     Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR_TYPE,
                           INFINITE_LOOP);
     return NS_ERROR_FAILURE;
   }
 
   if (removalIndex < removalArray.Length()) {
-    NS_WARNING("There are still prefixes to remove after exhausting the old PrefixSet.");
+    LOG(("There are still prefixes to remove after exhausting the old PrefixSet."));
     Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR_TYPE,
                           WRONG_REMOVAL_INDICES);
     return NS_ERROR_FAILURE;
   }
 
+  nsAutoCString checksum;
+  crypto->Finish(false, checksum);
+  if (aTableUpdate->Checksum().IsEmpty()) {
+    LOG(("Update checksum missing."));
+    Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR_TYPE,
+                          MISSING_CHECKSUM);
+
+    // Generate our own checksum to tableUpdate to ensure there is always
+    // checksum in .metadata
+    std::string stdChecksum(checksum.BeginReading(), checksum.Length());
+    aTableUpdate->NewChecksum(stdChecksum);
+
+  } else if (aTableUpdate->Checksum() != checksum){
+    LOG(("Checksum mismatch after applying partial update"));
+    Telemetry::Accumulate(Telemetry::URLCLASSIFIER_UPDATE_ERROR_TYPE,
+                          CHECKSUM_MISMATCH);
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+LookupCacheV4::InitCrypto(nsCOMPtr<nsICryptoHash>& aCrypto)
+{
+  nsresult rv;
+  aCrypto = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = aCrypto->Init(nsICryptoHash::SHA256);