merge mozilla-central to mozilla-inbound. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Thu, 21 Sep 2017 15:29:25 +0200
changeset 435527 27a12fb3e64b45f520c43a63c2b856274f0241d7
parent 435526 0c886aceae7f5c8b91ca07eb121c7d0f4bf442c3 (current diff)
parent 434145 f7e9777221a34f9f23c2e4933307eb38b621b679 (diff)
child 435528 69c2f0ea56ddc0114cdc4035d7d16dea9ab3d729
push id1618
push userCallek@gmail.com
push dateThu, 11 Jan 2018 17:45:48 +0000
treeherdermozilla-release@882ca853e05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-central to mozilla-inbound. r=merge a=merge
mobile/android/modules/geckoview/GeckoViewNavigation.jsm
mobile/android/services/src/main/java/org/mozilla/gecko/sync/DelayedWorkTracker.java
servo/components/style/cache.rs
toolkit/components/extensions/schemas/native_host_manifest.json
toolkit/components/extensions/test/xpcshell/test_native_messaging.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -5428,17 +5428,18 @@ function onViewToolbarsPopupShowing(aEve
   // triggerNode can be a nested child element of a toolbaritem.
   let toolbarItem = popup.triggerNode;
 
   if (toolbarItem && toolbarItem.localName == "toolbarpaletteitem") {
     toolbarItem = toolbarItem.firstChild;
   } else if (toolbarItem && toolbarItem.localName != "toolbar") {
     while (toolbarItem && toolbarItem.parentNode) {
       let parent = toolbarItem.parentNode;
-      if ((parent.classList && parent.classList.contains("customization-target")) ||
+      if (parent.nodeType !== Node.ELEMENT_NODE ||
+          (parent.classList && parent.classList.contains("customization-target")) ||
           parent.getAttribute("overflowfortoolbar") || // Needs to work in the overflow list as well.
           parent.localName == "toolbarpaletteitem" ||
           parent.localName == "toolbar")
         break;
       toolbarItem = parent;
     }
   } else {
     toolbarItem = null;
--- a/browser/base/content/test/about/browser.ini
+++ b/browser/base/content/test/about/browser.ini
@@ -13,17 +13,17 @@ support-files =
 [browser_aboutCertError.js]
 [browser_aboutHealthReport.js]
 skip-if = os == "linux" # Bug 924307
 [browser_aboutHome_imitate.js]
 [browser_aboutHome_input.js]
 skip-if = os == "win" && debug && !e10s # Bug 1399648
 [browser_aboutHome_search_POST.js]
 [browser_aboutHome_search_composing.js]
-skip-if = os == "linux" && !debug && bits == 32 # Bug 1399648
+skip-if = !debug && (os == "mac" || (os == "linux" && bits == 32)) # Bug 1400491, bug 1399648
 [browser_aboutHome_search_searchbar.js]
 [browser_aboutHome_search_suggestion.js]
 skip-if = os == "mac" || (os == "linux" && !debug) # Bug 1399648
 [browser_aboutHome_search_telemetry.js]
 [browser_aboutHome_snippets.js]
 [browser_aboutHome_wrapsCorrectly.js]
 skip-if = os == "linux" && !debug # Bug 1395602
 [browser_aboutNetError.js]
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -621,18 +621,22 @@
                        class="subviewbutton"
                        label="&savePageCmd.label;"
                        key="key_savePage"
                        command="Browser:SavePage"
                        />
         <toolbarbutton id="appMenu-print-button"
                        class="subviewbutton subviewbutton-iconic"
                        label="&printCmd.label;"
+#ifdef XP_MACOSX
                        key="printKb"
                        command="cmd_print"
+#else
+                       command="cmd_printPreview"
+#endif
                        />
         <toolbarseparator/>
         <toolbarbutton id="appMenu-find-button"
                        class="subviewbutton subviewbutton-iconic"
                        label="&findOnCmd.label;"
                        key="key_find"
                        command="cmd_find"/>
         <toolbarbutton id="appMenu-more-button"
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -67,16 +67,18 @@ for (const type of [
   "SNIPPETS_DATA",
   "SNIPPETS_RESET",
   "SYSTEM_TICK",
   "TELEMETRY_IMPRESSION_STATS",
   "TELEMETRY_PERFORMANCE_EVENT",
   "TELEMETRY_UNDESIRED_EVENT",
   "TELEMETRY_USER_EVENT",
   "TOP_SITES_ADD",
+  "TOP_SITES_CANCEL_EDIT",
+  "TOP_SITES_EDIT",
   "TOP_SITES_PIN",
   "TOP_SITES_UNPIN",
   "TOP_SITES_UPDATED",
   "UNINIT"
 ]) {
   actionTypes[type] = type;
 }
 
--- a/browser/extensions/activity-stream/common/PerfService.jsm
+++ b/browser/extensions/activity-stream/common/PerfService.jsm
@@ -62,25 +62,34 @@ this._PerfService = function _PerfServic
     return this._perf.getEntriesByName(name, type);
   },
 
   /**
    * The timeOrigin property from the appropriate performance object.
    * Used to ensure that timestamps from the add-on code and the content code
    * are comparable.
    *
+   * @note If this is called from a context without a window
+   * (eg a JSM in chrome), it will return the timeOrigin of the XUL hidden
+   * window, which appears to be the first created window (and thus
+   * timeOrigin) in the browser.  Note also, however, there is also a private
+   * hidden window, presumably for private browsing, which appears to be
+   * created dynamically later.  Exactly how/when that shows up needs to be
+   * investigated.
+   *
    * @return {Number} A double of milliseconds with a precision of 0.5us.
    */
   get timeOrigin() {
     return this._perf.timeOrigin;
   },
 
   /**
    * Returns the "absolute" version of performance.now(), i.e. one that
-   * based on the timeOrigin of the XUL hiddenwindow.
+   * should ([bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406)
+   * be comparable across both chrome and content.
    *
    * @return {Number}
    */
   absNow: function absNow() {
     return this.timeOrigin + this._perf.now();
   },
 
   /**
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -24,17 +24,23 @@ const INITIAL_STATE = {
     // The version of the system-addon
     version: null
   },
   Snippets: {initialized: false},
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
-    rows: []
+    rows: [],
+    // Used in content only to dispatch action from
+    // context menu to TopSitesEdit.
+    editForm: {
+      visible: false,
+      site: null
+    }
   },
   Prefs: {
     initialized: false,
     values: {}
   },
   Dialog: {
     visible: false,
     data: {}
@@ -99,16 +105,20 @@ function TopSites(prevState = INITIAL_ST
   let hasMatch;
   let newRows;
   switch (action.type) {
     case at.TOP_SITES_UPDATED:
       if (!action.data) {
         return prevState;
       }
       return Object.assign({}, prevState, {initialized: true, rows: action.data});
+    case at.TOP_SITES_EDIT:
+      return Object.assign({}, prevState, {editForm: {visible: true, site: action.data}});
+    case at.TOP_SITES_CANCEL_EDIT:
+      return Object.assign({}, prevState, {editForm: {visible: false}});
     case at.SCREENSHOT_UPDATED:
       newRows = prevState.rows.map(row => {
         if (row && row.url === action.data.url) {
           hasMatch = true;
           return Object.assign({}, row, {screenshot: action.data.screenshot});
         }
         return row;
       });
--- a/browser/extensions/activity-stream/data/content/activity-stream-initial-state.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream-initial-state.js
@@ -1,13 +1,17 @@
 // Note - this is a generated file.
   window.gActivityStreamPrerenderedState = {
   "TopSites": {
     "initialized": false,
-    "rows": []
+    "rows": [],
+    "editForm": {
+      "visible": false,
+      "site": null
+    }
   },
   "App": {
     "initialized": false,
     "locale": "en-PRERENDER",
     "strings": {
       "newtab_page_title": " ",
       "default_label_loading": " ",
       "header_top_sites": " ",
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -89,17 +89,17 @@ const globalImportContext = typeof Windo
 
 
 // Create an object that avoids accidental differing key/value pairs:
 // {
 //   INIT: "INIT",
 //   UNINIT: "UNINIT"
 // }
 const actionTypes = {};
-for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SET_PREF", "SHOW_FIREFOX_ACCOUNTS", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_ADD", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
+for (const type of ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "DELETE_HISTORY_URL_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "INIT", "LOCALE_UPDATED", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SET_PREF", "SHOW_FIREFOX_ACCOUNTS", "SNIPPETS_DATA", "SNIPPETS_RESET", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_ADD", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_EDIT", "TOP_SITES_PIN", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "UNINIT"]) {
   actionTypes[type] = type;
 }
 
 // Helper function for creating routed actions between content and main
 // Not intended to be used by consumers
 function _RouteMessage(action, options) {
   const meta = action.meta ? Object.assign({}, action.meta) : {};
   if (!options || !options.from || !options.to) {
@@ -328,17 +328,17 @@ module.exports = g;
 
 
 /***/ }),
 /* 5 */
 /***/ (function(module, exports) {
 
 module.exports = {
   TOP_SITES_SOURCE: "TOP_SITES",
-  TOP_SITES_CONTEXT_MENU_OPTIONS: ["CheckPinTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
+  TOP_SITES_CONTEXT_MENU_OPTIONS: ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"],
   // minimum size necessary to show a rich icon instead of a screenshot
   MIN_RICH_FAVICON_SIZE: 96,
   // minimum size necessary to show any icon in the top left corner with a screenshot
   MIN_CORNER_FAVICON_SIZE: 16
 };
 
 /***/ }),
 /* 6 */
@@ -371,17 +371,23 @@ const INITIAL_STATE = {
     // The version of the system-addon
     version: null
   },
   Snippets: { initialized: false },
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
-    rows: []
+    rows: [],
+    // Used in content only to dispatch action from
+    // context menu to TopSitesEdit.
+    editForm: {
+      visible: false,
+      site: null
+    }
   },
   Prefs: {
     initialized: false,
     values: {}
   },
   Dialog: {
     visible: false,
     data: {}
@@ -449,16 +455,20 @@ function TopSites(prevState = INITIAL_ST
   let hasMatch;
   let newRows;
   switch (action.type) {
     case at.TOP_SITES_UPDATED:
       if (!action.data) {
         return prevState;
       }
       return Object.assign({}, prevState, { initialized: true, rows: action.data });
+    case at.TOP_SITES_EDIT:
+      return Object.assign({}, prevState, { editForm: { visible: true, site: action.data } });
+    case at.TOP_SITES_CANCEL_EDIT:
+      return Object.assign({}, prevState, { editForm: { visible: false } });
     case at.SCREENSHOT_UPDATED:
       newRows = prevState.rows.map(row => {
         if (row && row.url === action.data.url) {
           hasMatch = true;
           return Object.assign({}, row, { screenshot: action.data.screenshot });
         }
         return row;
       });
@@ -726,25 +736,34 @@ var _PerfService = function _PerfService
     return this._perf.getEntriesByName(name, type);
   },
 
   /**
    * The timeOrigin property from the appropriate performance object.
    * Used to ensure that timestamps from the add-on code and the content code
    * are comparable.
    *
+   * @note If this is called from a context without a window
+   * (eg a JSM in chrome), it will return the timeOrigin of the XUL hidden
+   * window, which appears to be the first created window (and thus
+   * timeOrigin) in the browser.  Note also, however, there is also a private
+   * hidden window, presumably for private browsing, which appears to be
+   * created dynamically later.  Exactly how/when that shows up needs to be
+   * investigated.
+   *
    * @return {Number} A double of milliseconds with a precision of 0.5us.
    */
   get timeOrigin() {
     return this._perf.timeOrigin;
   },
 
   /**
    * Returns the "absolute" version of performance.now(), i.e. one that
-   * based on the timeOrigin of the XUL hiddenwindow.
+   * should ([bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406)
+   * be comparable across both chrome and content.
    *
    * @return {Number}
    */
   absNow: function absNow() {
     return this.timeOrigin + this._perf.now();
   },
 
   /**
@@ -1417,16 +1436,17 @@ class TopSitesEdit extends React.PureCom
     }));
   }
   onModalOverlayClick() {
     this.setState({ showEditModal: false, showAddForm: false, showEditForm: false });
     this.props.dispatch(ac.UserEvent({
       source: TOP_SITES_SOURCE,
       event: "TOP_SITES_EDIT_CLOSE"
     }));
+    this.props.dispatch({ type: at.TOP_SITES_CANCEL_EDIT });
   }
   onShowMoreLessClick() {
     const prefIsSetToDefault = this.props.TopSitesCount === TOP_SITES_DEFAULT_LENGTH;
     this.props.dispatch(ac.SendToMain({
       type: at.SET_PREF,
       data: { name: "topSitesCount", value: prefIsSetToDefault ? TOP_SITES_SHOWMORE_LENGTH : TOP_SITES_DEFAULT_LENGTH }
     }));
     this.props.dispatch(ac.UserEvent({
@@ -1438,27 +1458,34 @@ class TopSitesEdit extends React.PureCom
     this.setState({ showAddForm: true });
     this.props.dispatch(ac.UserEvent({
       source: TOP_SITES_SOURCE,
       event: "TOP_SITES_ADD_FORM_OPEN"
     }));
   }
   onFormClose() {
     this.setState({ showAddForm: false, showEditForm: false });
+    this.props.dispatch({ type: at.TOP_SITES_CANCEL_EDIT });
   }
   onEdit(index) {
     this.setState({ showEditForm: true, editIndex: index });
     this.props.dispatch(ac.UserEvent({
       source: TOP_SITES_SOURCE,
       event: "TOP_SITES_EDIT_FORM_OPEN"
     }));
   }
   render() {
     const realTopSites = this.props.TopSites.rows.slice(0, this.props.TopSitesCount);
     const placeholderCount = this.props.TopSitesCount - realTopSites.length;
+    const showEditForm = this.props.TopSites.editForm && this.props.TopSites.editForm.visible || this.state.showEditModal && this.state.showEditForm;
+    let editIndex = this.state.editIndex;
+    if (showEditForm && this.props.TopSites.editForm.visible) {
+      const targetURL = this.props.TopSites.editForm.site.url;
+      editIndex = this.props.TopSites.rows.findIndex(s => s.url === targetURL);
+    }
     return React.createElement(
       "div",
       { className: "edit-topsites-wrapper" },
       React.createElement(
         "div",
         { className: "edit-topsites-button" },
         React.createElement(
           "button",
@@ -1525,27 +1552,27 @@ class TopSitesEdit extends React.PureCom
         { className: "edit-topsites" },
         React.createElement("div", { className: "modal-overlay", onClick: this.onModalOverlayClick }),
         React.createElement(
           "div",
           { className: "modal" },
           React.createElement(TopSiteForm, { onClose: this.onFormClose, dispatch: this.props.dispatch, intl: this.props.intl })
         )
       ),
-      this.state.showEditModal && this.state.showEditForm && React.createElement(
+      showEditForm && React.createElement(
         "div",
         { className: "edit-topsites" },
         React.createElement("div", { className: "modal-overlay", onClick: this.onModalOverlayClick }),
         React.createElement(
           "div",
           { className: "modal" },
           React.createElement(TopSiteForm, {
-            label: this.props.TopSites.rows[this.state.editIndex].label || this.props.TopSites.rows[this.state.editIndex].hostname,
-            url: this.props.TopSites.rows[this.state.editIndex].url,
-            index: this.state.editIndex,
+            label: this.props.TopSites.rows[editIndex].label || this.props.TopSites.rows[editIndex].hostname,
+            url: this.props.TopSites.rows[editIndex].url,
+            index: editIndex,
             editMode: true,
             onClose: this.onFormClose,
             dispatch: this.props.dispatch,
             intl: this.props.intl })
         )
       )
     );
   }
@@ -1927,16 +1954,24 @@ module.exports = {
     }),
     impression: ac.ImpressionStats({
       source: eventSource,
       pocket: 0,
       incognito: true,
       tiles: [{ id: site.guid, pos: index }]
     }),
     userEvent: "SAVE_TO_POCKET"
+  }),
+  EditTopSite: site => ({
+    id: "edit_topsites_button_text",
+    icon: "edit",
+    action: {
+      type: at.TOP_SITES_EDIT,
+      data: { url: site.url, label: site.label }
+    }
   })
 };
 
 module.exports.CheckBookmark = site => site.bookmarkGuid ? module.exports.RemoveBookmark(site) : module.exports.AddBookmark(site);
 module.exports.CheckPinTopSite = (site, index) => site.isPinned ? module.exports.UnpinTopSite(site) : module.exports.PinTopSite(site, index);
 
 /***/ }),
 /* 19 */
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -161,16 +161,19 @@ a {
       padding: 0;
       text-decoration: underline; }
     .actions button.done {
       background: #0060DF;
       border: solid 1px #0060DF;
       color: #FFF;
       margin-inline-start: auto; }
 
+#snippets-container {
+  z-index: 1; }
+
 .outer-wrapper {
   display: flex;
   padding: 40px 32px 32px;
   height: 100%;
   flex-grow: 1; }
   .outer-wrapper.fixed-to-top {
     height: auto; }
 
@@ -279,18 +282,17 @@ main {
       background-clip: padding-box;
       border: 1px solid #B1B1B3;
       border-radius: 100%;
       box-shadow: 0 2px rgba(12, 12, 13, 0.1);
       fill: rgba(12, 12, 13, 0.8);
       transform: scale(0.25);
       opacity: 0;
       transition-property: transform, opacity;
-      transition-duration: 200ms;
-      z-index: 399; }
+      transition-duration: 200ms; }
       .top-sites-list .top-site-outer .context-menu-button:focus, .top-sites-list .top-site-outer .context-menu-button:active {
         transform: scale(1);
         opacity: 1; }
     .top-sites-list .top-site-outer:hover .tile, .top-sites-list .top-site-outer:focus .tile, .top-sites-list .top-site-outer.active .tile {
       box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px #D7D7DB;
       transition: box-shadow 150ms; }
     .top-sites-list .top-site-outer:hover .context-menu-button, .top-sites-list .top-site-outer:focus .context-menu-button, .top-sites-list .top-site-outer.active .context-menu-button {
       transform: scale(1);
@@ -336,17 +338,16 @@ main {
       background-color: #F9F9FA; }
     .top-sites-list .top-site-outer .rich-icon {
       top: 0;
       offset-inline-start: 0;
       height: 100%;
       width: 100%;
       background-size: 96px; }
     .top-sites-list .top-site-outer .default-icon {
-      z-index: 1;
       bottom: -6px;
       height: 42px;
       offset-inline-end: -6px;
       width: 42px;
       background-size: 32px;
       display: flex;
       align-items: center;
       justify-content: center;
@@ -967,18 +968,17 @@ main {
     background-clip: padding-box;
     border: 1px solid #B1B1B3;
     border-radius: 100%;
     box-shadow: 0 2px rgba(12, 12, 13, 0.1);
     fill: rgba(12, 12, 13, 0.8);
     transform: scale(0.25);
     opacity: 0;
     transition-property: transform, opacity;
-    transition-duration: 200ms;
-    z-index: 399; }
+    transition-duration: 200ms; }
     .card-outer .context-menu-button:focus, .card-outer .context-menu-button:active {
       transform: scale(1);
       opacity: 1; }
   .card-outer.placeholder {
     background: transparent; }
     .card-outer.placeholder .card {
       box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); }
   .card-outer .card {
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -6111,16 +6111,17 @@
     "settings_pane_topsites_body": "เข้าถึงเว็บไซต์ที่คุณเยี่ยมชมมากที่สุด",
     "settings_pane_topsites_options_showmore": "แสดงสองแถว",
     "settings_pane_bookmarks_header": "ที่คั่นหน้าเมื่อเร็ว ๆ นี้",
     "settings_pane_bookmarks_body": "ที่คั่นหน้าที่สร้างใหม่ของคุณในตำแหน่งที่ตั้งเดียวที่สะดวก",
     "settings_pane_visit_again_header": "เยี่ยมชมอีกครั้ง",
     "settings_pane_highlights_header": "รายการเด่น",
     "settings_pane_highlights_options_bookmarks": "ที่คั่นหน้า",
     "settings_pane_highlights_options_visited": "ไซต์ที่เยี่ยมชมแล้ว",
+    "settings_pane_snippets_header": "ส่วนย่อย",
     "settings_pane_done_button": "เสร็จสิ้น",
     "edit_topsites_button_text": "แก้ไข",
     "edit_topsites_button_label": "ปรับแต่งส่วนไซต์เด่นของคุณ",
     "edit_topsites_showmore_button": "แสดงเพิ่มเติม",
     "edit_topsites_showless_button": "แสดงน้อยลง",
     "edit_topsites_done_button": "เสร็จสิ้น",
     "edit_topsites_pin_button": "ปักหมุดไซต์นี้",
     "edit_topsites_unpin_button": "ถอนหมุดไซต์นี้",
--- a/browser/extensions/activity-stream/install.rdf.in
+++ b/browser/extensions/activity-stream/install.rdf.in
@@ -3,17 +3,17 @@
 #filter substitution
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>activity-stream@mozilla.org</em:id>
     <em:type>2</em:type>
     <em:bootstrap>true</em:bootstrap>
     <em:unpack>false</em:unpack>
-    <em:version>2017.09.20.0232-e27564ef</em:version>
+    <em:version>2017.09.20.1184-2d88ef77</em:version>
     <em:name>Activity Stream</em:name>
     <em:description>A rich visual history feed and a reimagined home page make it easier than ever to find exactly what you're looking for in Firefox.</em:description>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
 
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -228,26 +228,36 @@ this.ActivityStream = class ActivityStre
   constructor(options = {}) {
     this.initialized = false;
     this.options = options;
     this.store = new Store();
     this.feeds = FEEDS_CONFIG;
     this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG);
   }
   init() {
-    this._updateDynamicPrefs();
-    this._defaultPrefs.init();
+    try {
+      this._updateDynamicPrefs();
+      this._defaultPrefs.init();
+
+      // Hook up the store and let all feeds and pages initialize
+      this.store.init(this.feeds, ac.BroadcastToContent({
+        type: at.INIT,
+        data: {version: this.options.version}
+      }), {type: at.UNINIT});
 
-    // Hook up the store and let all feeds and pages initialize
-    this.store.init(this.feeds, ac.BroadcastToContent({
-      type: at.INIT,
-      data: {version: this.options.version}
-    }), {type: at.UNINIT});
-
-    this.initialized = true;
+      this.initialized = true;
+    } catch (e) {
+      // TelemetryFeed could be unavailable if the telemetry is disabled, or
+      // the telemetry feed is not yet initialized.
+      const telemetryFeed = this.store.feeds.get("feeds.telemetry");
+      if (telemetryFeed) {
+        telemetryFeed.handleUndesiredEvent({data: {event: "ADDON_INIT_FAILED"}});
+      }
+      throw e;
+    }
   }
   uninit() {
     if (this.geo === "") {
       Services.prefs.removeObserver(GEO_PREF, this);
     }
 
     this.store.uninit();
     this.initialized = false;
--- a/browser/extensions/activity-stream/lib/HighlightsFeed.jsm
+++ b/browser/extensions/activity-stream/lib/HighlightsFeed.jsm
@@ -10,31 +10,43 @@ const {actionTypes: at} = Cu.import("res
 
 const {shortURL} = Cu.import("resource://activity-stream/lib/ShortURL.jsm", {});
 const {SectionsManager} = Cu.import("resource://activity-stream/lib/SectionsManager.jsm", {});
 const {TOP_SITES_SHOWMORE_LENGTH} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
 const {Dedupe} = Cu.import("resource://activity-stream/common/Dedupe.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "filterAdult",
   "resource://activity-stream/lib/FilterAdult.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LinksCache",
+  "resource://activity-stream/lib/LinksCache.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
   "resource://gre/modules/NewTabUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Screenshots",
   "resource://activity-stream/lib/Screenshots.jsm");
 
 const HIGHLIGHTS_MAX_LENGTH = 9;
 const HIGHLIGHTS_UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
 const MANY_EXTRA_LENGTH = HIGHLIGHTS_MAX_LENGTH * 5 + TOP_SITES_SHOWMORE_LENGTH;
 const SECTION_ID = "highlights";
 
 this.HighlightsFeed = class HighlightsFeed {
   constructor() {
     this.highlightsLastUpdated = 0;
-    this.highlights = [];
+    this.highlightsLength = 0;
     this.dedupe = new Dedupe(this._dedupeKey);
+    this.linksCache = new LinksCache(NewTabUtils.activityStreamLinks,
+      "getHighlights", (oldLink, newLink) => {
+        // Migrate any pending images or images to the new link
+        for (const property of ["__fetchingScreenshot", "image"]) {
+          const oldValue = oldLink[property];
+          if (oldValue) {
+            newLink[property] = oldValue;
+          }
+        }
+      });
   }
 
   _dedupeKey(site) {
     return site && site.url;
   }
 
   init() {
     SectionsManager.onceInitialized(this.postInit.bind(this));
@@ -45,109 +57,105 @@ this.HighlightsFeed = class HighlightsFe
     this.fetchHighlights(true);
   }
 
   uninit() {
     SectionsManager.disableSection(SECTION_ID);
   }
 
   async fetchHighlights(broadcast = false) {
+    // We broadcast when we want to force an update, so get fresh links
+    if (broadcast) {
+      this.linksCache.expire();
+    }
+
     // We need TopSites to have been initialised for deduping
     if (!this.store.getState().TopSites.initialized) {
       await new Promise(resolve => {
         const unsubscribe = this.store.subscribe(() => {
           if (this.store.getState().TopSites.initialized) {
             unsubscribe();
             resolve();
           }
         });
       });
     }
 
     // Request more than the expected length to allow for items being removed by
     // deduping against Top Sites or multiple history from the same domain, etc.
-    const manyPages = await NewTabUtils.activityStreamLinks.getHighlights({numItems: MANY_EXTRA_LENGTH});
+    const manyPages = await this.linksCache.request({numItems: MANY_EXTRA_LENGTH});
 
     // Remove adult highlights if we need to
     const checkedAdult = this.store.getState().Prefs.values.filterAdult ?
       filterAdult(manyPages) : manyPages;
 
     // Remove any Highlights that are in Top Sites already
     const [, deduped] = this.dedupe.group(this.store.getState().TopSites.rows, checkedAdult);
 
-    // Store existing images in case we need to reuse them
-    const currentImages = {};
-    for (const site of this.highlights) {
-      if (site && site.image) {
-        currentImages[site.url] = site.image;
-      }
-    }
-
     // Keep all "bookmark"s and at most one (most recent) "history" per host
-    this.highlights = [];
+    const highlights = [];
     const hosts = new Set();
     for (const page of deduped) {
       const hostname = shortURL(page);
       // Skip this history page if we already something from the same host
       if (page.type === "history" && hosts.has(hostname)) {
         continue;
       }
 
       // If we already have the image for the card, use that immediately. Else
       // asynchronously fetch the image.
-      const image = currentImages[page.url];
-      if (!image) {
-        this.fetchImage(page.url, page.preview_image_url);
+      if (!page.image) {
+        this.fetchImage(page);
       }
 
       // We want the page, so update various fields for UI
       Object.assign(page, {
-        image,
         hasImage: true, // We always have an image - fall back to a screenshot
         hostname,
         type: page.bookmarkGuid ? "bookmark" : page.type
       });
 
       // Add the "bookmark" or not-skipped "history"
-      this.highlights.push(page);
+      highlights.push(page);
       hosts.add(hostname);
 
+      // Remove any internal properties
+      delete page.__fetchingScreenshot;
+      delete page.__updateCache;
+
       // Skip the rest if we have enough items
-      if (this.highlights.length === HIGHLIGHTS_MAX_LENGTH) {
+      if (highlights.length === HIGHLIGHTS_MAX_LENGTH) {
         break;
       }
     }
 
-    SectionsManager.updateSection(SECTION_ID, {rows: this.highlights}, this.highlightsLastUpdated === 0 || broadcast);
+    SectionsManager.updateSection(SECTION_ID, {rows: highlights}, broadcast);
     this.highlightsLastUpdated = Date.now();
+    this.highlightsLength = highlights.length;
   }
 
   /**
    * Fetch an image for a given highlight and update the card with it. If no
-   * image is available then fallback to fetching a screenshot. Update the card
-   * in `this.highlights` so that the image is cached for the next refresh.
+   * image is available then fallback to fetching a screenshot.
    */
-  async fetchImage(url, imageUrl) {
-    const image = await Screenshots.getScreenshotForURL(imageUrl || url);
-    SectionsManager.updateSectionCard(SECTION_ID, url, {image}, true);
-    if (image) {
-      const highlight = this.highlights.find(site => site.url === url);
-      if (highlight) {
-        highlight.image = image;
-      }
-    }
+  async fetchImage(page) {
+    // Request a screenshot if we don't already have one pending
+    const {preview_image_url: imageUrl, url} = page;
+    Screenshots.maybeGetAndSetScreenshot(page, imageUrl || url, "image", image => {
+      SectionsManager.updateSectionCard(SECTION_ID, url, {image}, true);
+    });
   }
 
   onAction(action) {
     switch (action.type) {
       case at.INIT:
         this.init();
         break;
       case at.NEW_TAB_LOAD:
-        if (this.highlights.length < HIGHLIGHTS_MAX_LENGTH) {
+        if (this.highlightsLength < HIGHLIGHTS_MAX_LENGTH) {
           // If we haven't filled the highlights grid yet, fetch again.
           this.fetchHighlights(true);
         } else if (Date.now() - this.highlightsLastUpdated >= HIGHLIGHTS_UPDATE_TIME) {
           // If the last time we refreshed the data is greater than 15 minutes, fetch again.
           this.fetchHighlights(false);
         }
         break;
       case at.MIGRATION_COMPLETED:
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/LinksCache.jsm
@@ -0,0 +1,114 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["LinksCache"];
+
+const EXPIRATION_TIME = 5 * 60 * 1000; // 5 minutes
+
+/**
+ * Cache link results from a provided object property and refresh after some
+ * amount of time has passed. Allows for migrating data from previously cached
+ * links to the new links with the same url.
+ */
+this.LinksCache = class LinksCache {
+
+  /**
+   * Create a links cache for a given object property.
+   *
+   * @param {object} linkObject Object containing the link property
+   * @param {string} linkProperty Name of property on object to access
+   * @param {function} migrator Optional callback receiving the old and new link
+   *                            to allow custom migrating data from old to new.
+   * @param {function} shouldRefresh Optional callback receiving the old and new
+   *                                 options to refresh even when not expired.
+   */
+  constructor(linkObject, linkProperty, migrator = () => {}, shouldRefresh = () => {}) {
+    this.clear();
+    // Allow getting links from both methods and array properties
+    this.linkGetter = options => {
+      const ret = linkObject[linkProperty];
+      return typeof ret === "function" ? ret.call(linkObject, options) : ret;
+    };
+    this.migrator = migrator;
+    this.shouldRefresh = shouldRefresh;
+  }
+
+  /**
+   * Clear the cached data.
+   */
+  clear() {
+    this.cache = Promise.resolve([]);
+    this.lastOptions = {};
+    this.expire();
+  }
+
+  /**
+   * Force the next request to update the cache.
+   */
+  expire() {
+    delete this.lastUpdate;
+  }
+
+  /**
+   * Request data and update the cache if necessary.
+   *
+   * @param {object} options Optional data to pass to the underlying method.
+   * @returns {promise(array)} Links array with objects that can be modified.
+   */
+  async request(options = {}) {
+    // Update the cache if the data has been expired
+    const now = Date.now();
+    if (this.lastUpdate === undefined ||
+        now > this.lastUpdate + EXPIRATION_TIME ||
+        // Allow custom rules around refreshing based on options
+        this.shouldRefresh(this.lastOptions, options)) {
+      // Update request state early so concurrent requests can refer to it
+      this.lastOptions = options;
+      this.lastUpdate = now;
+
+      // Save a promise before awaits, so other requests wait for correct data
+      this.cache = new Promise(async resolve => {
+        // Allow fast lookup of old links by url that might need to migrate
+        const toMigrate = new Map();
+        for (const oldLink of await this.cache) {
+          if (oldLink) {
+            toMigrate.set(oldLink.url, oldLink);
+          }
+        }
+
+        // Make a shallow copy of each resulting link to allow direct edits
+        const copied = (await this.linkGetter(options)).map(link => link &&
+          Object.assign({}, link));
+
+        // Migrate data to the new link if we have an old link
+        for (const newLink of copied) {
+          if (newLink) {
+            const oldLink = toMigrate.get(newLink.url);
+            if (oldLink) {
+              this.migrator(oldLink, newLink);
+            }
+
+            // Add a method that can be copied to cloned links that will update
+            // the original cached link's property with the current one
+            newLink.__updateCache = function(prop) {
+              const val = this[prop];
+              if (val === undefined) {
+                delete newLink[prop];
+              } else {
+                newLink[prop] = val;
+              }
+            };
+          }
+        }
+
+        // Update cache with the copied links migrated
+        resolve(copied);
+      });
+    }
+
+    // Return the promise of the links array
+    return this.cache;
+  }
+};
--- a/browser/extensions/activity-stream/lib/Screenshots.jsm
+++ b/browser/extensions/activity-stream/lib/Screenshots.jsm
@@ -56,10 +56,45 @@ this.Screenshots = {
       const bytes = await file.read();
       const encodedData = btoa(this._bytesToString(bytes));
       file.close();
       screenshot = `data:${contentType};base64,${encodedData}`;
     } catch (err) {
       Cu.reportError(`getScreenshot error: ${err}`);
     }
     return screenshot;
+  },
+
+  /**
+   * Conditionally get a screenshot for a link if there's no existing pending
+   * screenshot. Updates the link object's desired property with the result.
+   *
+   * @param link {object} Link object to update
+   * @param url {string} Url to get a screenshot of
+   * @param property {string} Name of property on object to set
+   @ @param onScreenshot {function} Callback for when the screenshot loads
+   */
+  async maybeGetAndSetScreenshot(link, url, property, onScreenshot) {
+    // Make a link copy so we can stash internal properties to cache
+    const updateCache = link.__updateCache ? link.__updateCache.bind(link) :
+      () => {};
+
+    // Request a screenshot if we don't already have one pending
+    if (!link.__fetchingScreenshot) {
+      link.__fetchingScreenshot = this.getScreenshotForURL(url);
+      updateCache("__fetchingScreenshot");
+
+      // Trigger this callback only when first requesting
+      link.__fetchingScreenshot.then(onScreenshot).catch();
+    }
+
+    // Clean up now that we got the screenshot
+    const screenshot = await link.__fetchingScreenshot;
+    delete link.__fetchingScreenshot;
+    updateCache("__fetchingScreenshot");
+
+    // Update the link so the screenshot is in the cache
+    if (screenshot) {
+      link[property] = screenshot;
+      updateCache(property);
+    }
   }
 };
--- a/browser/extensions/activity-stream/lib/SectionsManager.jsm
+++ b/browser/extensions/activity-stream/lib/SectionsManager.jsm
@@ -1,17 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/EventEmitter.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
-Cu.import("resource://gre/modules/EventEmitter.jsm");
+const {Dedupe} = Cu.import("resource://activity-stream/common/Dedupe.jsm", {});
 
 /*
  * Generators for built in sections, keyed by the pref name for their feed.
  * Built in sections may depend on options stored as serialised JSON in the pref
  * `${feed_pref_name}.options`.
  */
 const BUILT_IN_SECTIONS = {
   "feeds.section.topstories": options => ({
@@ -31,17 +32,18 @@ const BUILT_IN_SECTIONS = {
       body: {id: options.provider_description || "pocket_feedback_body"},
       link: {href: options.info_link, id: "section_info_privacy_notice"}
     },
     emptyState: {
       message: {id: "topstories_empty_state", values: {provider: options.provider_name}},
       icon: "check"
     },
     shouldSendImpressionStats: true,
-    order: 0
+    order: 0,
+    dedupeFrom: ["highlights"]
   }),
   "feeds.section.highlights": options => ({
     id: "highlights",
     pref: {
       titleString: {id: "settings_pane_highlights_header"},
       descString: {id: "settings_pane_highlights_body2"}
     },
     shouldHidePref:  false,
@@ -68,16 +70,18 @@ const SectionsManager = {
     for (const feedPrefName of Object.keys(BUILT_IN_SECTIONS)) {
       const optionsPrefName = `${feedPrefName}.options`;
       this.addBuiltInSection(feedPrefName, prefs[optionsPrefName]);
     }
 
     Object.keys(this.CONTEXT_MENU_PREFS).forEach(k =>
       Services.prefs.addObserver(this.CONTEXT_MENU_PREFS[k], this));
 
+    this.dedupe = new Dedupe(site => site && site.url);
+
     this.initialized = true;
     this.emit(this.INIT);
   },
   observe(subject, topic, data) {
     switch (topic) {
       case "nsPref:changed":
         for (const pref of Object.keys(this.CONTEXT_MENU_PREFS)) {
           if (data === this.CONTEXT_MENU_PREFS[pref]) {
@@ -118,20 +122,42 @@ const SectionsManager = {
   },
   updateSections() {
     this.sections.forEach((section, id) => this.updateSection(id, section, true));
   },
   updateSection(id, options, shouldBroadcast) {
     this.updateSectionContextMenuOptions(options);
 
     if (this.sections.has(id)) {
-      this.sections.set(id, Object.assign(this.sections.get(id), options));
-      this.emit(this.UPDATE_SECTION, id, options, shouldBroadcast);
+      const dedupedOptions = this.dedupeRows(id, options);
+      this.sections.set(id, Object.assign(this.sections.get(id), dedupedOptions));
+      this.emit(this.UPDATE_SECTION, id, dedupedOptions, shouldBroadcast);
+
+      // Update any sections that dedupe from the updated section
+      this.sections.forEach(section => {
+        if (section.dedupeFrom && section.dedupeFrom.includes(id)) {
+          this.updateSection(section.id, section, shouldBroadcast);
+        }
+      });
     }
   },
+  dedupeRows(id, options) {
+    const newOptions = Object.assign({}, options);
+    const dedupeFrom = this.sections.get(id).dedupeFrom;
+    if (dedupeFrom && dedupeFrom.length > 0 && options.rows) {
+      for (const sectionId of dedupeFrom) {
+        const section = this.sections.get(sectionId);
+        if (section && section.rows) {
+          const [, newRows] = this.dedupe.group(section.rows, options.rows);
+          newOptions.rows = newRows;
+        }
+      }
+    }
+    return newOptions;
+  },
 
   /**
    * Sets the section's context menu options. These are all available context menu
    * options minus the ones that are tied to a pref (see CONTEXT_MENU_PREFS) set
    * to false.
    *
    * @param options section options
    */
--- a/browser/extensions/activity-stream/lib/Store.jsm
+++ b/browser/extensions/activity-stream/lib/Store.jsm
@@ -100,30 +100,38 @@ this.Store = class Store {
         this.uninitFeed(name, this._uninitAction);
       }
     }
   }
 
   /**
    * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
    *
+   * Note that it intentionally initializes the TelemetryFeed first so that the
+   * addon is able to report the init errors from other feeds.
+   *
    * @param  {Map} feedFactories A Map of feeds with the name of the pref for
    *                                the feed as the key and a function that
    *                                constructs an instance of the feed.
    * @param {Action} initAction An optional action that will be dispatched
    *                            to feeds when they're created.
    * @param {Action} uninitAction An optional action for when feeds uninit.
    */
   init(feedFactories, initAction, uninitAction) {
     this._feedFactories = feedFactories;
     this._initAction = initAction;
     this._uninitAction = uninitAction;
 
+    const telemetryKey = "feeds.telemetry";
+    if (feedFactories.has(telemetryKey) && this._prefs.get(telemetryKey)) {
+      this.initFeed(telemetryKey);
+    }
+
     for (const pref of feedFactories.keys()) {
-      if (this._prefs.get(pref)) {
+      if (pref !== telemetryKey && this._prefs.get(pref)) {
         this.initFeed(pref);
       }
     }
 
     this._prefs.observeBranch(this);
     this._messageChannel.createChannel();
 
     // Dispatch an initial action after all enabled feeds are ready
--- a/browser/extensions/activity-stream/lib/TelemetryFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TelemetryFeed.jsm
@@ -99,16 +99,17 @@ this.TelemetryFeed = class TelemetryFeed
     this._impressionStatsLastReset = Date.now();
     this._impressionStats = {
       clicked: new PersistentGuidSet(this._prefs, PREF_IMPRESSION_STATS_CLICKED),
       blocked: new PersistentGuidSet(this._prefs, PREF_IMPRESSION_STATS_BLOCKED),
       pocketed: new PersistentGuidSet(this._prefs, PREF_IMPRESSION_STATS_POCKETED)
     };
 
     this.telemetryEnabled = this._prefs.get(TELEMETRY_PREF);
+    this._aboutHomeSeen = false;
     this._onTelemetryPrefChange = this._onTelemetryPrefChange.bind(this);
     this._prefs.observe(TELEMETRY_PREF, this._onTelemetryPrefChange);
   }
 
   init() {
     Services.obs.addObserver(this.browserOpenNewtabStart, "browser-open-newtab-start");
   }
 
@@ -188,24 +189,69 @@ this.TelemetryFeed = class TelemetryFeed
   /**
    * addSession - Start tracking a new session
    *
    * @param  {string} id the portID of the open session
    * @param  {string} the URL being loaded for this session (optional)
    * @return {obj}    Session object
    */
   addSession(id, url) {
+    // XXX refactor to use setLoadTriggerInfo or saveSessionPerfData
+
+    // "unexpected" will be overwritten when appropriate
+    let load_trigger_type = "unexpected";
+    let load_trigger_ts;
+
+    if (!this._aboutHomeSeen && url === "about:home") {
+      this._aboutHomeSeen = true;
+
+      // XXX note that this will be incorrectly set in the following cases:
+      // session_restore following by clicking on the toolbar button,
+      // or someone who has changed their default home page preference to
+      // something else and later clicks the toolbar.  It will also be
+      // incorrectly unset if someone changes their "Home Page" preference to
+      // about:newtab.
+      //
+      // That said, the ratio of these mistakes to correct cases should
+      // be very small, and these issues should follow away as we implement
+      // the remaining load_trigger_type values for about:home in issue 3556.
+      //
+      // XXX file a bug to implement remaining about:home cases so this
+      // problem will go away and link to it here.
+      load_trigger_type = "first_window_opened";
+
+      // The real perceived trigger of first_window_opened is the OS-level
+      // clicking of the icon.  We use perfService.timeOrigin because it's the
+      // earliest number on this time scale that's easy to get.; We could
+      // actually use 0, but maybe that could be before the browser started?
+      // [bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406)
+      // getting sorted out may help clarify. Even better, presumably, would be
+      // to use the process creation time for the main process, which is
+      // available, but somewhat harder to get. However, these are all more or
+      // less proxies for the same thing, so it's not clear how much the better
+      // numbers really matter, since we (activity stream) only control a
+      // relatively small amount of the code that's executing between the
+      // OS-click and when the first <browser> element starts loading.  That
+      // said, it's conceivable that it could help us catch regressions in the
+      // number of cycles early chrome code takes to execute, but it's likely
+      // that there are more direct ways to measure that.
+      load_trigger_ts = perfService.timeOrigin;
+    }
+
     const session = {
       session_id: String(gUUIDGenerator.generateUUID()),
       // "unknown" will be overwritten when appropriate
       page: url ? url : "unknown",
-      // "unexpected" will be overwritten when appropriate
-      perf: {load_trigger_type: "unexpected"}
+      perf: {load_trigger_type}
     };
 
+    if (load_trigger_ts) {
+      session.perf.load_trigger_ts = load_trigger_ts;
+    }
+
     this.sessions.set(id, session);
     return session;
   }
 
   /**
    * endSession - Stop tracking a session
    *
    * @param  {string} portID the portID of the session that just closed
@@ -339,16 +385,24 @@ this.TelemetryFeed = class TelemetryFeed
 
     // If it is an impression ping, just send it out. For the click, block, and
     // save to pocket pings, it only sends the first ping for the same article.
     if (!guidSet || guidSet.save(payload.tiles[index].id)) {
       this.sendEvent(this.createImpressionStats(action));
     }
   }
 
+  handleUserEvent(action) {
+    this.sendEvent(this.createUserEvent(action));
+  }
+
+  handleUndesiredEvent(action) {
+    this.sendEvent(this.createUndesiredEvent(action));
+  }
+
   resetImpressionStats() {
     for (const key of Object.keys(this._impressionStats)) {
       this._impressionStats[key].clear();
     }
     this._impressionStatsLastReset = Date.now();
   }
 
   onAction(action) {
@@ -369,20 +423,20 @@ this.TelemetryFeed = class TelemetryFeed
         if (Date.now() - this._impressionStatsLastReset >= IMPRESSION_STATS_RESET_TIME) {
           this.resetImpressionStats();
         }
         break;
       case at.TELEMETRY_IMPRESSION_STATS:
         this.handleImpressionStats(action);
         break;
       case at.TELEMETRY_UNDESIRED_EVENT:
-        this.sendEvent(this.createUndesiredEvent(action));
+        this.handleUndesiredEvent(action);
         break;
       case at.TELEMETRY_USER_EVENT:
-        this.sendEvent(this.createUserEvent(action));
+        this.handleUserEvent(action);
         break;
       case at.TELEMETRY_PERFORMANCE_EVENT:
         this.sendEvent(this.createPerformanceEvent(action));
         break;
       case at.UNINIT:
         this.uninit();
         break;
     }
@@ -401,22 +455,28 @@ this.TelemetryFeed = class TelemetryFeed
    * @param {String} port  The session with which this is associated
    * @param {Object} data  The perf data to be
    */
   saveSessionPerfData(port, data) {
     // XXX should use try/catch and send a bad state indicator if this
     // get blows up.
     let session = this.sessions.get(port);
 
-    // Partial workaround for #3118; avoids the worst incorrect associations of
-    // times with browsers, by associating the load trigger with the visibility
-    // event as the user is most likely associating the trigger to the tab just
-    // shown. This helps avoid associateing with a preloaded browser as those
-    // don't get the event until shown. Better fix for more cases forthcoming.
-    if (data.visibility_event_rcvd_ts) {
+    // XXX Partial workaround for #3118; avoids the worst incorrect associations
+    // of times with browsers, by associating the load trigger with the
+    // visibility event as the user is most likely associating the trigger to
+    // the tab just shown. This helps avoid associating with a preloaded
+    // browser as those don't get the event until shown. Better fix for more
+    // cases forthcoming.
+    //
+    // XXX the about:home check (and the corresponding test) should go away
+    // once the load_trigger stuff in addSession is refactored into
+    // setLoadTriggerInfo.
+    //
+    if (data.visibility_event_rcvd_ts && session.page !== "about:home") {
       this.setLoadTriggerInfo(port);
     }
 
     Object.assign(session.perf, data);
   }
 
   uninit() {
     Services.obs.removeObserver(this.browserOpenNewtabStart,
--- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -9,32 +9,40 @@ Cu.import("resource://gre/modules/XPCOMU
 const {actionCreators: ac, actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 const {TippyTopProvider} = Cu.import("resource://activity-stream/lib/TippyTopProvider.jsm", {});
 const {insertPinned, TOP_SITES_SHOWMORE_LENGTH} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
 const {Dedupe} = Cu.import("resource://activity-stream/common/Dedupe.jsm", {});
 const {shortURL} = Cu.import("resource://activity-stream/lib/ShortURL.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "filterAdult",
   "resource://activity-stream/lib/FilterAdult.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LinksCache",
+  "resource://activity-stream/lib/LinksCache.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
   "resource://gre/modules/NewTabUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Screenshots",
   "resource://activity-stream/lib/Screenshots.jsm");
 
 const UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
 const DEFAULT_SITES_PREF = "default.sites";
 const DEFAULT_TOP_SITES = [];
-const FRECENCY_THRESHOLD = 100; // 1 visit (skip first-run/one-time pages)
+const FRECENCY_THRESHOLD = 100 + 1; // 1 visit (skip first-run/one-time pages)
 const MIN_FAVICON_SIZE = 96;
 
 this.TopSitesFeed = class TopSitesFeed {
   constructor() {
     this.lastUpdated = 0;
     this._tippyTopProvider = new TippyTopProvider();
     this.dedupe = new Dedupe(this._dedupeKey);
+    this.frecentCache = new LinksCache(NewTabUtils.activityStreamLinks,
+      "getTopSites", this.getLinkMigrator(), (oldOptions, newOptions) =>
+        // Refresh if no old options or requesting more items
+        !(oldOptions.numItems >= newOptions.numItems));
+    this.pinnedCache = new LinksCache(NewTabUtils.pinnedLinks,
+      "links", this.getLinkMigrator(["favicon", "faviconSize"]));
   }
   _dedupeKey(site) {
     return site && site.hostname;
   }
   refreshDefaults(sites) {
     // Clear out the array of any previous defaults
     DEFAULT_TOP_SITES.length = 0;
 
@@ -45,125 +53,150 @@ this.TopSitesFeed = class TopSitesFeed {
           isDefault: true,
           url
         };
         site.hostname = shortURL(site);
         DEFAULT_TOP_SITES.push(site);
       }
     }
   }
-  async getScreenshot(url) {
-    let screenshot = await Screenshots.getScreenshotForURL(url);
-    const action = {type: at.SCREENSHOT_UPDATED, data: {url, screenshot}};
-    this.store.dispatch(ac.BroadcastToContent(action));
+
+  /**
+   * Make a cached link data migrator by copying over screenshots and others.
+   *
+   * @param others {array} Optional extra properties to copy
+   */
+  getLinkMigrator(others = []) {
+    const properties = ["__fetchingScreenshot", "screenshot", ...others];
+    return (oldLink, newLink) => {
+      for (const property of properties) {
+        const oldValue = oldLink[property];
+        if (oldValue) {
+          newLink[property] = oldValue;
+        }
+      }
+    };
   }
   async getLinksWithDefaults(action) {
     // Get at least SHOWMORE amount so toggling between 1 and 2 rows has sites
     const numItems = Math.max(this.store.getState().Prefs.values.topSitesCount,
       TOP_SITES_SHOWMORE_LENGTH);
-    let frecent = await NewTabUtils.activityStreamLinks.getTopSites({numItems});
-    const notBlockedDefaultSites = DEFAULT_TOP_SITES.filter(site => !NewTabUtils.blockedLinks.isBlocked({url: site.url}));
-    const defaultUrls = notBlockedDefaultSites.map(site => site.url);
-    let pinned = this._getPinnedWithData(frecent);
-    pinned = pinned.map(site => site && Object.assign({}, site, {
-      isDefault: defaultUrls.indexOf(site.url) !== -1,
-      hostname: shortURL(site)
-    }));
+    const frecent = (await this.frecentCache.request({
+      numItems,
+      topsiteFrecency: FRECENCY_THRESHOLD
+    })).map(link => Object.assign({}, link, {hostname: shortURL(link)}));
+
+    // Remove any defaults that have been blocked
+    const notBlockedDefaultSites = DEFAULT_TOP_SITES.filter(link =>
+      !NewTabUtils.blockedLinks.isBlocked({url: link.url}));
+
+    // Get pinned links augmented with desired properties
+    const plainPinned = await this.pinnedCache.request();
+    const pinned = await Promise.all(plainPinned.map(async link => {
+      if (!link) {
+        return link;
+      }
 
-    if (!frecent) {
-      frecent = [];
-    } else {
-      // Get the best history links that pass the frecency threshold
-      frecent = frecent.filter(link => link && link.type !== "affiliate" &&
-        link.frecency > FRECENCY_THRESHOLD).map(site => {
-          site.hostname = shortURL(site);
-          return site;
-        });
-    }
+      // Copy all properties from a frecent link and add more
+      const finder = other => other.url === link.url;
+      const copy = Object.assign({}, frecent.find(finder) || {}, link, {
+        hostname: shortURL(link),
+        isDefault: !!notBlockedDefaultSites.find(finder)
+      });
+
+      // Add in favicons if we don't already have it
+      if (!copy.favicon) {
+        try {
+          NewTabUtils.activityStreamProvider._faviconBytesToDataURI(await
+            NewTabUtils.activityStreamProvider._addFavicons([copy]));
+          copy.__updateCache("favicon");
+          copy.__updateCache("faviconSize");
+        } catch (e) {
+          // Some issue with favicon, so just continue without one
+        }
+      }
+
+      return copy;
+    }));
 
     // Remove any duplicates from frecent and default sites
     const [, dedupedFrecent, dedupedDefaults] = this.dedupe.group(
       pinned, frecent, notBlockedDefaultSites);
     const dedupedUnpinned = [...dedupedFrecent, ...dedupedDefaults];
 
     // Remove adult sites if we need to
     const checkedAdult = this.store.getState().Prefs.values.filterAdult ?
       filterAdult(dedupedUnpinned) : dedupedUnpinned;
 
     // Insert the original pinned sites into the deduped frecent and defaults
-    return insertPinned(checkedAdult, pinned).slice(0, numItems);
+    const withPinned = insertPinned(checkedAdult, pinned).slice(0, numItems);
+
+    // Now, get a tippy top icon, a rich icon, or screenshot for every item
+    for (const link of withPinned) {
+      if (link) {
+        this._fetchIcon(link);
+
+        // Remove any internal properties
+        delete link.__fetchingScreenshot;
+        delete link.__updateCache;
+      }
+    }
+
+    return withPinned;
   }
+
+  /**
+   * Refresh the top sites data for content
+   *
+   * @param target Optional port/channel to receive the update. If not provided,
+   *               the update will be broadcasted.
+   */
   async refresh(target = null) {
     if (!this._tippyTopProvider.initialized) {
       await this._tippyTopProvider.init();
     }
 
     const links = await this.getLinksWithDefaults();
-
-    // First, cache existing screenshots in case we need to reuse them
-    const currentScreenshots = {};
-    for (const link of this.store.getState().TopSites.rows) {
-      if (link && link.screenshot) {
-        currentScreenshots[link.url] = link.screenshot;
-      }
-    }
-
-    // Now, get a tippy top icon, a rich icon, or screenshot for every item
-    for (let link of links) {
-      if (!link) { continue; }
-      this._fetchIcon(link, currentScreenshots);
-    }
     const newAction = {type: at.TOP_SITES_UPDATED, data: links};
-
     if (target) {
       // Send an update to content so the preloaded tab can get the updated content
       this.store.dispatch(ac.SendToContent(newAction, target));
     } else {
       // Broadcast an update to all open content pages
       this.store.dispatch(ac.BroadcastToContent(newAction));
     }
     this.lastUpdated = Date.now();
   }
-  _fetchIcon(link, screenshotCache = {}) {
+
+  /**
+   * Get an image for the link preferring tippy top, rich favicon, screenshots.
+   */
+  async _fetchIcon(link) {
     // Check for tippy top icon or a rich icon.
     this._tippyTopProvider.processSite(link);
-    if (!link.tippyTopIcon && (!link.favicon || link.faviconSize < MIN_FAVICON_SIZE)) {
-      // If no tippy top, then we get a screenshot.
-      if (screenshotCache[link.url]) {
-        link.screenshot = screenshotCache[link.url];
-      } else {
-        this.getScreenshot(link.url);
-      }
+    if (!link.tippyTopIcon &&
+        (!link.favicon || link.faviconSize < MIN_FAVICON_SIZE) &&
+        !link.screenshot) {
+      const {url} = link;
+      Screenshots.maybeGetAndSetScreenshot(link, url, "screenshot", screenshot => {
+        this.store.dispatch(ac.BroadcastToContent({
+          data: {screenshot, url},
+          type: at.SCREENSHOT_UPDATED
+        }));
+      });
     }
   }
-  _getPinnedWithData(links) {
-    // Augment the pinned links with any other extra data we have for them already in the store.
-    // Alternatively you can pass in some links that you know have data you want the pinned links
-    // to also have. This is useful for start up to make sure pinned links have favicons
-    // (See github ticket #3428 fore more details)
-    const originalLinks = links || this.store.getState().TopSites.rows;
-    const pinned = NewTabUtils.pinnedLinks.links;
-    return pinned.map(pinnedLink => {
-      if (pinnedLink) {
-        const hostname = shortURL(pinnedLink);
-        const originalLink = originalLinks.find(link => link && link.url === pinnedLink.url);
-        // If it's a new link then it won't have an icon, so fetch one
-        if (!originalLink) {
-          this._fetchIcon(pinnedLink);
-        }
-        return Object.assign(originalLink || {hostname}, pinnedLink);
-      }
-      return pinnedLink;
-    });
-  }
 
   /**
    * Inform others that top sites data has been updated due to pinned changes.
    */
   _broadcastPinnedSitesUpdated() {
+    // Pinned data changed, so make sure we get latest
+    this.pinnedCache.expire();
+
     // Refresh to update pinned sites with screenshots, trigger deduping, etc.
     this.refresh();
   }
 
   /**
    * Pin a site at a specific position saving only the desired keys.
    */
   _pinSiteAt({label, url}, index) {
@@ -228,16 +261,17 @@ this.TopSitesFeed = class TopSitesFeed {
           this.refresh(action.meta.fromTarget);
         }
         break;
       // All these actions mean we need new top sites
       case at.MIGRATION_COMPLETED:
       case at.PLACES_HISTORY_CLEARED:
       case at.PLACES_LINK_DELETED:
       case at.PLACES_LINK_BLOCKED:
+        this.frecentCache.expire();
         this.refresh();
         break;
       case at.PREF_CHANGED:
         if (action.data.name === DEFAULT_SITES_PREF) {
           this.refreshDefaults(action.data.value);
         }
         break;
       case at.PREFS_INITIAL_VALUES:
--- a/browser/extensions/activity-stream/test/schemas/pings.js
+++ b/browser/extensions/activity-stream/test/schemas/pings.js
@@ -96,17 +96,18 @@ const SessionPing = Joi.object().keys(Ob
     // observer event doesn't fire
     load_trigger_ts: Joi.number().positive()
       .notes(["server counter", "server counter alert"]),
 
     // What was the perceived trigger of the load action?
     //
     // Not required at least for the error cases where the observer event
     // doesn't fire
-    load_trigger_type: Joi.valid(["menu_plus_or_keyboard", "unexpected"])
+    load_trigger_type: Joi.valid(["first_window_opened",
+      "menu_plus_or_keyboard", "unexpected"])
       .notes(["server counter", "server counter alert"]).required(),
 
     // When did the topsites element finish painting?  Note that, at least for
     // the first tab to be loaded, and maybe some others, this will be before
     // topsites has yet to receive screenshots updates from the add-on code,
     // and is therefore just showing placeholder screenshots.
     topsites_first_painted_ts: Joi.number().positive()
       .notes(["server counter", "server counter alert"]),
--- a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -51,16 +51,29 @@ describe("Reducers", () => {
       const newRows = [{url: "foo.com"}, {url: "bar.com"}];
       const nextState = TopSites(undefined, {type: at.TOP_SITES_UPDATED, data: newRows});
       assert.equal(nextState.rows, newRows);
     });
     it("should not update state for empty action.data on TOP_SITES_UPDATED", () => {
       const nextState = TopSites(undefined, {type: at.TOP_SITES_UPDATED});
       assert.equal(nextState, INITIAL_STATE.TopSites);
     });
+    it("should set editForm.visible to true on TOP_SITES_EDIT", () => {
+      const nextState = TopSites(undefined, {type: at.TOP_SITES_EDIT});
+      assert.isTrue(nextState.editForm.visible);
+    });
+    it("should set editForm.site to action.data on TOP_SITES_EDIT", () => {
+      const data = {url: "foo", label: "label"};
+      const nextState = TopSites(undefined, {type: at.TOP_SITES_EDIT, data});
+      assert.equal(nextState.editForm.site, data);
+    });
+    it("should set editForm.visible to false on TOP_SITES_CANCEL_EDIT", () => {
+      const nextState = TopSites(undefined, {type: at.TOP_SITES_CANCEL_EDIT});
+      assert.isFalse(nextState.editForm.visible);
+    });
     it("should add screenshots for SCREENSHOT_UPDATED", () => {
       const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
       const action = {type: at.SCREENSHOT_UPDATED, data: {url: "bar.com", screenshot: "data:123"}};
       const nextState = TopSites(oldState, action);
       assert.deepEqual(nextState.rows, [{url: "foo.com"}, {url: "bar.com", screenshot: "data:123"}]);
     });
     it("should not modify rows if nothing matches the url for SCREENSHOT_UPDATED", () => {
       const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
@@ -470,16 +483,23 @@ describe("Reducers", () => {
       assert.notProperty(result[2], "isPinned");
       assert.notProperty(result[2], "pinIndex");
     });
     it("should handle a link present in both the links and pinned list", () => {
       const pinned = [links[7]];
       const result = insertPinned(links, pinned);
       assert.equal(links.length, result.length);
     });
+    it("should not modify the original data", () => {
+      const pinned = [{url: "http://example.com"}];
+
+      insertPinned(links, pinned);
+
+      assert.equal(typeof pinned[0].isPinned, "undefined");
+    });
   });
   describe("Snippets", () => {
     it("should return INITIAL_STATE by default", () => {
       assert.equal(Snippets(undefined, {type: "some_action"}), INITIAL_STATE.Snippets);
     });
     it("should set initialized to true on a SNIPPETS_DATA action", () => {
       const state = Snippets(undefined, {type: at.SNIPPETS_DATA, data: {}});
       assert.isTrue(state.initialized);
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
@@ -227,9 +227,22 @@ describe("ActivityStream", () => {
       sandbox.stub(global.Services.locale, "getRequestedLocale").returns("en-US");
 
       as._updateDynamicPrefs();
       clock.tick(1);
 
       assert.isTrue(PREFS_CONFIG.get("feeds.section.topstories").value);
     });
   });
+  describe("telemetry reporting on init failure", () => {
+    it("should send a ping on init error", () => {
+      as = new ActivityStream();
+      const telemetry = {handleUndesiredEvent: sandbox.spy()};
+      sandbox.stub(as.store, "init").throws();
+      sandbox.stub(as.store.feeds, "get").returns(telemetry);
+      try {
+        as.init();
+      } catch (e) {
+      }
+      assert.calledOnce(telemetry.handleUndesiredEvent);
+    });
+  });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/HighlightsFeed.test.js
@@ -1,10 +1,11 @@
 "use strict";
 const injector = require("inject!lib/HighlightsFeed.jsm");
+const {Screenshots} = require("lib/Screenshots.jsm");
 const {GlobalOverrider} = require("test/unit/utils");
 const {actionTypes: at} = require("common/Actions.jsm");
 const {Dedupe} = require("common/Dedupe.jsm");
 
 const FAKE_LINKS = new Array(9).fill(null).map((v, i) => ({url: `http://www.site${i}.com`}));
 const FAKE_IMAGE = "data123";
 
 describe("Highlights Feed", () => {
@@ -17,29 +18,37 @@ describe("Highlights Feed", () => {
   let links;
   let clock;
   let fakeScreenshot;
   let fakeNewTabUtils;
   let filterAdultStub;
   let sectionsManagerStub;
   let shortURLStub;
 
+  const fetchHighlights = async() => {
+    await feed.fetchHighlights();
+    return sectionsManagerStub.updateSection.firstCall.args[1].rows;
+  };
+
   beforeEach(() => {
     globals = new GlobalOverrider();
     sandbox = globals.sandbox;
     fakeNewTabUtils = {activityStreamLinks: {getHighlights: sandbox.spy(() => Promise.resolve(links))}};
     sectionsManagerStub = {
       onceInitialized: sinon.stub().callsFake(callback => callback()),
       enableSection: sinon.spy(),
       disableSection: sinon.spy(),
       updateSection: sinon.spy(),
       updateSectionCard: sinon.spy(),
       sections: new Map([["highlights", {}]])
     };
-    fakeScreenshot = {getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_IMAGE))};
+    fakeScreenshot = {
+      getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_IMAGE)),
+      maybeGetAndSetScreenshot: Screenshots.maybeGetAndSetScreenshot
+    };
     filterAdultStub = sinon.stub().returns([]);
     shortURLStub = sinon.stub().callsFake(site => site.url.match(/\/([^/]+)/)[1]);
     globals.set("NewTabUtils", fakeNewTabUtils);
     ({HighlightsFeed, HIGHLIGHTS_UPDATE_TIME, SECTION_ID} = injector({
       "lib/FilterAdult.jsm": {filterAdult: filterAdultStub},
       "lib/ShortURL.jsm": {shortURL: shortURLStub},
       "lib/SectionsManager.jsm": {SectionsManager: sectionsManagerStub},
       "lib/Screenshots.jsm": {Screenshots: fakeScreenshot},
@@ -82,136 +91,168 @@ describe("Highlights Feed", () => {
     it("should fetch highlights on postInit", () => {
       feed.fetchHighlights = sinon.spy();
       feed.postInit();
       assert.calledOnce(feed.fetchHighlights);
     });
   });
   describe("#fetchHighlights", () => {
     it("should wait for TopSites to be initialised", async () => {
-      feed.store.getState = () => ({TopSites: {initialized: false}});
+      feed.store.state.TopSites.initialized = false;
       // Initially TopSites is uninitialised and fetchHighlights should wait
-      feed.fetchHighlights();
+      const firstFetch = feed.fetchHighlights();
+
       assert.calledOnce(feed.store.subscribe);
       assert.notCalled(fakeNewTabUtils.activityStreamLinks.getHighlights);
 
       // Initialisation causes the subscribe callback to be called and
       // fetchHighlights should continue
-      feed.store.getState = () => ({TopSites: {initialized: true}});
+      feed.store.state.TopSites.initialized = true;
       const subscribeCallback = feed.store.subscribe.firstCall.args[0];
       await subscribeCallback();
+      await firstFetch;
       assert.calledOnce(fakeNewTabUtils.activityStreamLinks.getHighlights);
 
       // If TopSites is initialised in the first place it shouldn't wait
+      feed.linksCache.expire();
       feed.store.subscribe.reset();
       fakeNewTabUtils.activityStreamLinks.getHighlights.reset();
-      feed.fetchHighlights();
+      await feed.fetchHighlights();
       assert.notCalled(feed.store.subscribe);
       assert.calledOnce(fakeNewTabUtils.activityStreamLinks.getHighlights);
     });
     it("should add hostname and hasImage to each link", async () => {
       links = [{url: "https://mozilla.org"}];
-      await feed.fetchHighlights();
-      assert.equal(feed.highlights[0].hostname, "mozilla.org");
-      assert.equal(feed.highlights[0].hasImage, true);
+
+      const highlights = await fetchHighlights();
+
+      assert.equal(highlights[0].hostname, "mozilla.org");
+      assert.equal(highlights[0].hasImage, true);
     });
     it("should add an existing image if it exists to the link without calling fetchImage", async () => {
       links = [{url: "https://mozilla.org", image: FAKE_IMAGE}];
-      feed.highlights = links;
       sinon.spy(feed, "fetchImage");
-      await feed.fetchHighlights();
-      assert.equal(feed.highlights[0].image, FAKE_IMAGE);
+
+      const highlights = await fetchHighlights();
+
+      assert.equal(highlights[0].image, FAKE_IMAGE);
       assert.notCalled(feed.fetchImage);
     });
     it("should call fetchImage with the correct arguments for new links", async () => {
       links = [{url: "https://mozilla.org", preview_image_url: "https://mozilla.org/preview.jog"}];
-      feed.highlights = [];
       sinon.spy(feed, "fetchImage");
+
       await feed.fetchHighlights();
+
       assert.calledOnce(feed.fetchImage);
-      assert.calledWith(feed.fetchImage, links[0].url, links[0].preview_image_url);
+      const arg = feed.fetchImage.firstCall.args[0];
+      assert.propertyVal(arg, "url", links[0].url);
+      assert.propertyVal(arg, "preview_image_url", links[0].preview_image_url);
     });
     it("should not include any links already in Top Sites", async () => {
       links = [
         {url: "https://mozilla.org"},
         {url: "http://www.topsite0.com"},
         {url: "http://www.topsite1.com"},
         {url: "http://www.topsite2.com"}
       ];
-      await feed.fetchHighlights();
-      assert.equal(feed.highlights.length, 1);
-      assert.deepEqual(feed.highlights[0], links[0]);
+
+      const highlights = await fetchHighlights();
+
+      assert.equal(highlights.length, 1);
+      assert.equal(highlights[0].url, links[0].url);
     });
     it("should not include history of same hostname as a bookmark", async () => {
       links = [
         {url: "https://site.com/bookmark", type: "bookmark"},
         {url: "https://site.com/history", type: "history"}
       ];
 
-      await feed.fetchHighlights();
+      const highlights = await fetchHighlights();
 
-      assert.equal(feed.highlights.length, 1);
-      assert.deepEqual(feed.highlights[0], links[0]);
+      assert.equal(highlights.length, 1);
+      assert.equal(highlights[0].url, links[0].url);
     });
     it("should take the first history of a hostname", async () => {
       links = [
         {url: "https://site.com/first", type: "history"},
         {url: "https://site.com/second", type: "history"},
         {url: "https://other", type: "history"}
       ];
 
-      await feed.fetchHighlights();
+      const highlights = await fetchHighlights();
 
-      assert.equal(feed.highlights.length, 2);
-      assert.deepEqual(feed.highlights[0], links[0]);
-      assert.deepEqual(feed.highlights[1], links[2]);
+      assert.equal(highlights.length, 2);
+      assert.equal(highlights[0].url, links[0].url);
+      assert.equal(highlights[1].url, links[2].url);
     });
     it("should set type to bookmark if there is a bookmarkGuid", async () => {
       links = [{url: "https://mozilla.org", type: "history", bookmarkGuid: "1234567890"}];
-      await feed.fetchHighlights();
-      assert.equal(feed.highlights[0].type, "bookmark");
+
+      const highlights = await fetchHighlights();
+
+      assert.equal(highlights[0].type, "bookmark");
     });
     it("should not filter out adult pages when pref is false", async() => {
       await feed.fetchHighlights();
 
       assert.notCalled(filterAdultStub);
     });
     it("should filter out adult pages when pref is true", async() => {
       feed.store.state.Prefs.values.filterAdult = true;
 
-      await feed.fetchHighlights();
+      const highlights = await fetchHighlights();
 
       // The stub filters out everything
       assert.calledOnce(filterAdultStub);
-      assert.equal(feed.highlights.length, 0);
+      assert.equal(highlights.length, 0);
+    });
+    it("should not expose internal link properties", async() => {
+      const highlights = await fetchHighlights();
+
+      const internal = Object.keys(highlights[0]).filter(key => key.startsWith("__"));
+      assert.equal(internal.join(""), "");
     });
   });
   describe("#fetchImage", () => {
     const FAKE_URL = "https://mozilla.org";
     const FAKE_IMAGE_URL = "https://mozilla.org/preview.jpg";
     it("should capture the image, if available", async () => {
-      await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
+      await feed.fetchImage({
+        preview_image_url: FAKE_IMAGE_URL,
+        url: FAKE_URL
+      });
+
       assert.calledOnce(fakeScreenshot.getScreenshotForURL);
       assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_IMAGE_URL);
     });
     it("should fall back to capturing a screenshot", async () => {
-      await feed.fetchImage(FAKE_URL, undefined);
+      await feed.fetchImage({url: FAKE_URL});
+
       assert.calledOnce(fakeScreenshot.getScreenshotForURL);
       assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_URL);
     });
     it("should call SectionsManager.updateSectionCard with the right arguments", async () => {
-      await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
+      await feed.fetchImage({
+        preview_image_url: FAKE_IMAGE_URL,
+        url: FAKE_URL
+      });
+
       assert.calledOnce(sectionsManagerStub.updateSectionCard);
       assert.calledWith(sectionsManagerStub.updateSectionCard, "highlights", FAKE_URL, {image: FAKE_IMAGE}, true);
     });
-    it("should update the card in feed.highlights with the image", async () => {
-      feed.highlights = [{url: FAKE_URL}];
-      await feed.fetchImage(FAKE_URL, FAKE_IMAGE_URL);
-      const highlight = feed.highlights.find(({url}) => url === FAKE_URL);
-      assert.propertyVal(highlight, "image", FAKE_IMAGE);
+    it("should update the card with the image", async () => {
+      const card = {
+        preview_image_url: FAKE_IMAGE_URL,
+        url: FAKE_URL
+      };
+
+      await feed.fetchImage(card);
+
+      assert.propertyVal(card, "image", FAKE_IMAGE);
     });
   });
   describe("#uninit", () => {
     it("should disable its section", () => {
       feed.onAction({type: at.UNINIT});
       assert.calledOnce(sectionsManagerStub.disableSection);
       assert.calledWith(sectionsManagerStub.disableSection, SECTION_ID);
     });
--- a/browser/extensions/activity-stream/test/unit/lib/SectionsManager.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/SectionsManager.test.js
@@ -141,25 +141,27 @@ describe("SectionsManager", () => {
       SectionsManager.removeSection(FAKE_ID);
       const spy = sinon.spy();
       SectionsManager.on(SectionsManager.UPDATE_SECTION, spy);
       SectionsManager.updateSection(FAKE_ID, {rows: FAKE_ROWS}, true);
       assert.notCalled(spy);
     });
     it("should update all sections", () => {
       SectionsManager.sections.clear();
+      const updateSectionOrig = SectionsManager.updateSection;
       SectionsManager.updateSection = sinon.spy();
 
       SectionsManager.addSection("ID1", {title: "FAKE_TITLE_1"});
       SectionsManager.addSection("ID2", {title: "FAKE_TITLE_2"});
       SectionsManager.updateSections();
 
       assert.calledTwice(SectionsManager.updateSection);
       assert.calledWith(SectionsManager.updateSection, "ID1", {title: "FAKE_TITLE_1"}, true);
       assert.calledWith(SectionsManager.updateSection, "ID2", {title: "FAKE_TITLE_2"}, true);
+      SectionsManager.updateSection = updateSectionOrig;
     });
     it("context menu pref change should update sections", () => {
       let observer;
       const services = {prefs: {getBoolPref: sinon.spy(), addObserver: (pref, o) => (observer = o), removeObserver: sinon.spy()}};
       globals.set("Services", services);
 
       SectionsManager.updateSections = sinon.spy();
       SectionsManager.CONTEXT_MENU_PREFS = {"MENU_ITEM": "MENU_ITEM_PREF"};
@@ -198,16 +200,37 @@ describe("SectionsManager", () => {
     it("should do nothing if the section doesn't exist", () => {
       SectionsManager.removeSection(FAKE_ID);
       const spy = sinon.spy();
       SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy);
       SectionsManager.updateSectionCard(FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS, true);
       assert.notCalled(spy);
     });
   });
+  describe("#dedupe", () => {
+    it("should dedupe stories from highlights", () => {
+      SectionsManager.init();
+      // Add some rows to highlights
+      SectionsManager.updateSection("highlights", {rows: [{url: "https://highlight.com/abc"}, {url: "https://shared.com/def"}]});
+      // Add some rows to top stories
+      SectionsManager.updateSection("topstories", {rows: [{url: "https://topstory.com/ghi"}, {url: "https://shared.com/def"}]});
+      // Verify deduping
+      assert.deepEqual(SectionsManager.sections.get("topstories").rows, [{url: "https://topstory.com/ghi"}]);
+    });
+    it("should dedupe stories from highlights when updating highlights", () => {
+      SectionsManager.init();
+      // Add some rows to top stories
+      SectionsManager.updateSection("topstories", {rows: [{url: "https://topstory.com/ghi"}, {url: "https://shared.com/def"}]});
+      assert.deepEqual(SectionsManager.sections.get("topstories").rows, [{url: "https://topstory.com/ghi"}, {url: "https://shared.com/def"}]);
+      // Add some rows to highlights
+      SectionsManager.updateSection("highlights", {rows: [{url: "https://highlight.com/abc"}, {url: "https://shared.com/def"}]});
+      // Verify deduping
+      assert.deepEqual(SectionsManager.sections.get("topstories").rows, [{url: "https://topstory.com/ghi"}]);
+    });
+  });
 });
 
 describe("SectionsFeed", () => {
   let feed;
 
   beforeEach(() => {
     SectionsManager.sections.clear();
     SectionsManager.initialized = false;
--- a/browser/extensions/activity-stream/test/unit/lib/Store.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/Store.test.js
@@ -158,16 +158,27 @@ describe("Store", () => {
       sinon.stub(store, "dispatch");
       const action = {type: "FOO"};
 
       store.init(new Map(), action);
 
       assert.calledOnce(store.dispatch);
       assert.calledWith(store.dispatch, action);
     });
+    it("should initialize the telemtry feed first", () => {
+      store._prefs.set("feeds.foo", true);
+      store._prefs.set("feeds.telemetry", true);
+      const telemetrySpy = sandbox.stub().returns({});
+      const fooSpy = sandbox.stub().returns({});
+      // Intentionally put the telemetry feed as the second item.
+      const feedFactories = new Map([["feeds.foo", fooSpy],
+                                     ["feeds.telemetry", telemetrySpy]]);
+      store.init(feedFactories);
+      assert.ok(telemetrySpy.calledBefore(fooSpy));
+    });
   });
   describe("#uninit", () => {
     it("should emit an uninit event if provided on init", () => {
       sinon.stub(store, "dispatch");
       const action = {type: "BAR"};
       store.init(new Map(), null, action);
 
       store.uninit();
--- a/browser/extensions/activity-stream/test/unit/lib/TelemetryFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetryFeed.test.js
@@ -24,16 +24,17 @@ describe("TelemetryFeed", () => {
   };
   let instance;
   let clock;
   class PingCentre {sendPing() {} uninit() {}}
   class PerfService {
     getMostRecentAbsMarkStartByName() { return 1234; }
     mark() {}
     absNow() { return 123; }
+    get timeOrigin() { return 123456; }
   }
   const perfService = new PerfService();
   const {
     TelemetryFeed,
     USER_PREFS_ENCODING,
     IMPRESSION_STATS_RESET_TIME,
     TELEMETRY_PREF,
     PREF_IMPRESSION_STATS_CLICKED,
@@ -109,17 +110,48 @@ describe("TelemetryFeed", () => {
 
       assert.propertyVal(session, "page", "unknown");
     });
     it("should set the perf type when lacking timestamp", () => {
       const session = instance.addSession("foo");
 
       assert.propertyVal(session.perf, "load_trigger_type", "unexpected");
     });
+    it("should set load_trigger_type to first_window_opened on the first about:home seen", () => {
+      const session = instance.addSession("foo", "about:home");
+
+      assert.propertyVal(session.perf, "load_trigger_type",
+        "first_window_opened");
+    });
+    it("should not set load_trigger_type to first_window_opened on the second about:home seen", () => {
+      instance.addSession("foo", "about:home");
+
+      const session2 = instance.addSession("foo", "about:home");
+
+      assert.propertyNotVal(session2.perf, "load_trigger_type",
+        "first_window_opened");
+    });
+    it("should set load_trigger_ts to the value of perfService.timeOrigin", () => {
+      const session = instance.addSession("foo", "about:home");
+
+      assert.propertyVal(session.perf, "load_trigger_ts",
+        123456);
+    });
+    it("should a valid session ping on the first about:home seen", () => {
+      // Add a session
+      const portID = "foo";
+      const session = instance.addSession(portID, "about:home");
+
+      // Create a ping referencing the session
+      const ping = instance.createSessionEndEvent(session);
+      console.log("ping: ", JSON.stringify(ping));
+      assert.validate(ping, SessionPing);
+    });
   });
+
   describe("#browserOpenNewtabStart", () => {
     it("should call perfService.mark with browser-open-newtab-start", () => {
       sandbox.stub(perfService, "mark");
 
       instance.browserOpenNewtabStart();
 
       assert.calledOnce(perfService.mark);
       assert.calledWithExactly(perfService.mark, "browser-open-newtab-start");
@@ -413,16 +445,26 @@ describe("TelemetryFeed", () => {
     it("shouldn't call setLoadTriggerInfo if data has no visibility_event_rcvd_ts", () => {
       sandbox.stub(instance, "setLoadTriggerInfo");
       instance.addSession("port123");
 
       instance.saveSessionPerfData("port123", {monkeys_ts: 444455});
 
       assert.notCalled(instance.setLoadTriggerInfo);
     });
+
+    it("should not call setLoadTriggerInfo when url is about:home", () => {
+      sandbox.stub(instance, "setLoadTriggerInfo");
+      instance.addSession("port123", "about:home");
+      const data = {visibility_event_rcvd_ts: 444455};
+
+      instance.saveSessionPerfData("port123", data);
+
+      assert.notCalled(instance.setLoadTriggerInfo);
+    });
   });
 
   describe("#uninit", () => {
     it("should call .pingCentre.uninit", () => {
       const stub = sandbox.stub(instance.pingCentre, "uninit");
       instance.uninit();
       assert.calledOnce(stub);
     });
--- a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -1,16 +1,19 @@
 "use strict";
 const injector = require("inject!lib/TopSitesFeed.jsm");
+const {Screenshots} = require("lib/Screenshots.jsm");
 const {UPDATE_TIME} = require("lib/TopSitesFeed.jsm");
 const {FakePrefs, GlobalOverrider} = require("test/unit/utils");
 const action = {meta: {fromTarget: {}}};
 const {actionTypes: at} = require("common/Actions.jsm");
 const {insertPinned, TOP_SITES_SHOWMORE_LENGTH} = require("common/Reducers.jsm");
 
+const FAKE_FAVICON = "data987";
+const FAKE_FAVICON_SIZE = 128;
 const FAKE_FRECENCY = 200;
 const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({
   frecency: FAKE_FRECENCY,
   url: `http://www.site${i}.com`
 }));
 const FAKE_SCREENSHOT = "data123";
 
 function FakeTippyTopProvider() {}
@@ -36,24 +39,35 @@ describe("Top Sites Feed", () => {
     globals = new GlobalOverrider();
     sandbox = globals.sandbox;
     fakeNewTabUtils = {
       blockedLinks: {
         links: [],
         isBlocked: () => false
       },
       activityStreamLinks: {getTopSites: sandbox.spy(() => Promise.resolve(links))},
+      activityStreamProvider: {
+        _addFavicons: sandbox.spy(l => Promise.resolve(l.map(link => {
+          link.favicon = FAKE_FAVICON;
+          link.faviconSize = FAKE_FAVICON_SIZE;
+          return link;
+        }))),
+        _faviconBytesToDataURI: sandbox.spy()
+      },
       pinnedLinks: {
         links: [],
         isPinned: () => false,
         pin: sandbox.spy(),
         unpin: sandbox.spy()
       }
     };
-    fakeScreenshot = {getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))};
+    fakeScreenshot = {
+      getScreenshotForURL: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT)),
+      maybeGetAndSetScreenshot: Screenshots.maybeGetAndSetScreenshot
+    };
     filterAdultStub = sinon.stub().returns([]);
     shortURLStub = sinon.stub().callsFake(site => site.url);
     const fakeDedupe = function() {};
     globals.set("NewTabUtils", fakeNewTabUtils);
     FakePrefs.prototype.prefs["default.sites"] = "https://foo.com/";
     ({TopSitesFeed, DEFAULT_TOP_SITES} = injector({
       "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
       "common/Dedupe.jsm": {Dedupe: fakeDedupe},
@@ -115,114 +129,153 @@ describe("Top Sites Feed", () => {
       assert.equal(DEFAULT_TOP_SITES.length, 0);
     });
   });
   describe("#getLinksWithDefaults", () => {
     beforeEach(() => {
       feed.refreshDefaults("https://foo.com");
     });
 
-    it("should get the links from NewTabUtils", async () => {
-      const result = await feed.getLinksWithDefaults();
-      const reference = links.map(site => Object.assign({}, site, {hostname: shortURLStub(site)}));
+    describe("general", () => {
+      beforeEach(() => {
+        sandbox.stub(fakeScreenshot, "maybeGetAndSetScreenshot");
+      });
+      it("should get the links from NewTabUtils", async () => {
+        const result = await feed.getLinksWithDefaults();
+        const reference = links.map(site => Object.assign({}, site, {hostname: shortURLStub(site)}));
 
-      assert.deepEqual(result, reference);
-      assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
-    });
-    it("should filter out low frecency links", async () => {
-      links = [
-        {frecency: FAKE_FRECENCY, url: "https://enough/visited"},
-        {frecency: 100, url: "https://visited/once"},
-        {frecency: 0, url: "https://unvisited/page"}
-      ];
+        assert.deepEqual(result, reference);
+        assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
+      });
+      it("should not filter out adult sites when pref is false", async() => {
+        await feed.getLinksWithDefaults();
+
+        assert.notCalled(filterAdultStub);
+      });
+      it("should filter out non-pinned adult sites when pref is true", async() => {
+        feed.store.state.Prefs.values.filterAdult = true;
+        fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com/"}];
 
-      const result = await feed.getLinksWithDefaults();
+        const result = await feed.getLinksWithDefaults();
 
-      assert.equal(result[0].url, links[0].url);
-      assert.notEqual(result[1].url, links[1].url);
-      assert.notEqual(result[1].url, links[2].url);
-    });
-    it("should not filter out adult sites when pref is false", async() => {
-      await feed.getLinksWithDefaults();
-
-      assert.notCalled(filterAdultStub);
-    });
-    it("should filter out non-pinned adult sites when pref is true", async() => {
-      feed.store.state.Prefs.values.filterAdult = true;
-      fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com/"}];
-
-      const result = await feed.getLinksWithDefaults();
+        // The stub filters out everything
+        assert.calledOnce(filterAdultStub);
+        assert.equal(result.length, 1);
+        assert.equal(result[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);
+      });
+      it("should filter out the defaults that have been blocked", async () => {
+        // make sure we only have one top site, and we block the only default site we have to show
+        const url = "www.myonlytopsite.com";
+        const topsite = {
+          frecency: FAKE_FRECENCY,
+          hostname: shortURLStub({url}),
+          url
+        };
+        const blockedDefaultSite = {url: "https://foo.com"};
+        fakeNewTabUtils.activityStreamLinks.getTopSites = () => [topsite];
+        fakeNewTabUtils.blockedLinks.isBlocked = site => (site.url === blockedDefaultSite.url);
+        const result = await feed.getLinksWithDefaults();
 
-      // The stub filters out everything
-      assert.calledOnce(filterAdultStub);
-      assert.equal(result.length, 1);
-      assert.equal(result[0].url, fakeNewTabUtils.pinnedLinks.links[0].url);
-    });
-    it("should filter out the defaults that have been blocked", async () => {
-      // make sure we only have one top site, and we block the only default site we have to show
-      const url = "www.myonlytopsite.com";
-      const topsite = {
-        frecency: FAKE_FRECENCY,
-        hostname: shortURLStub({url}),
-        url
-      };
-      const blockedDefaultSite = {url: "https://foo.com"};
-      fakeNewTabUtils.activityStreamLinks.getTopSites = () => [topsite];
-      fakeNewTabUtils.blockedLinks.isBlocked = site => (site.url === blockedDefaultSite.url);
-      const result = await feed.getLinksWithDefaults();
+        // what we should be left with is just the top site we added, and not the default site we blocked
+        assert.lengthOf(result, 1);
+        assert.deepEqual(result[0], topsite);
+        assert.notInclude(result, blockedDefaultSite);
+      });
+      it("should call dedupe on the links", async () => {
+        const stub = sinon.stub(feed.dedupe, "group").callsFake((...id) => id);
+
+        await feed.getLinksWithDefaults();
 
-      // what we should be left with is just the top site we added, and not the default site we blocked
-      assert.lengthOf(result, 1);
-      assert.deepEqual(result[0], topsite);
-      assert.notInclude(result, blockedDefaultSite);
-    });
-    it("should call dedupe on the links", async () => {
-      const stub = sinon.stub(feed.dedupe, "group").callsFake((...id) => id);
+        assert.calledOnce(stub);
+      });
+      it("should dedupe the links by hostname", async () => {
+        const site = {url: "foo", hostname: "bar"};
+        const result = feed._dedupeKey(site);
 
-      await feed.getLinksWithDefaults();
+        assert.equal(result, site.hostname);
+      });
+      it("should add defaults if there are are not enough links", async () => {
+        links = [{frecency: FAKE_FRECENCY, url: "foo.com"}];
+
+        const result = await feed.getLinksWithDefaults();
+        const reference = [...links, ...DEFAULT_TOP_SITES].map(s => Object.assign({}, s, {hostname: shortURLStub(s)}));
 
-      assert.calledOnce(stub);
-    });
-    it("should dedupe the links by hostname", async () => {
-      const site = {url: "foo", hostname: "bar"};
-      const result = feed._dedupeKey(site);
-
-      assert.equal(result, site.hostname);
-    });
-    it("should add defaults if there are are not enough links", async () => {
-      links = [{frecency: FAKE_FRECENCY, url: "foo.com"}];
-
-      const result = await feed.getLinksWithDefaults();
-      const reference = [...links, ...DEFAULT_TOP_SITES].map(s => Object.assign({}, s, {hostname: shortURLStub(s)}));
+        assert.deepEqual(result, reference);
+      });
+      it("should only add defaults up to TOP_SITES_SHOWMORE_LENGTH", async () => {
+        links = [];
+        for (let i = 0; i < TOP_SITES_SHOWMORE_LENGTH - 1; i++) {
+          links.push({frecency: FAKE_FRECENCY, url: `foo${i}.com`});
+        }
+        const result = await feed.getLinksWithDefaults();
+        const reference = [...links, DEFAULT_TOP_SITES[0]].map(s => Object.assign({}, s, {hostname: shortURLStub(s)}));
 
-      assert.deepEqual(result, reference);
-    });
-    it("should only add defaults up to TOP_SITES_SHOWMORE_LENGTH", async () => {
-      links = [];
-      for (let i = 0; i < TOP_SITES_SHOWMORE_LENGTH - 1; i++) {
-        links.push({frecency: FAKE_FRECENCY, url: `foo${i}.com`});
-      }
-      const result = await feed.getLinksWithDefaults();
-      const reference = [...links, DEFAULT_TOP_SITES[0]].map(s => Object.assign({}, s, {hostname: shortURLStub(s)}));
+        assert.lengthOf(result, TOP_SITES_SHOWMORE_LENGTH);
+        assert.deepEqual(result, reference);
+      });
+      it("should not throw if NewTabUtils returns null", () => {
+        links = null;
+        assert.doesNotThrow(() => {
+          feed.getLinksWithDefaults(action);
+        });
+      });
+      it("should get more if the user has asked for more", async () => {
+        feed.store.state.Prefs.values.topSitesCount = TOP_SITES_SHOWMORE_LENGTH + 1;
 
-      assert.lengthOf(result, TOP_SITES_SHOWMORE_LENGTH);
-      assert.deepEqual(result, reference);
-    });
-    it("should not throw if NewTabUtils returns null", () => {
-      links = null;
-      assert.doesNotThrow(() => {
-        feed.getLinksWithDefaults(action);
+        const result = await feed.getLinksWithDefaults();
+
+        assert.propertyVal(result, "length", feed.store.state.Prefs.values.topSitesCount);
       });
     });
-    it("should get more if the user has asked for more", async () => {
-      feed.store.state.Prefs.values.topSitesCount = TOP_SITES_SHOWMORE_LENGTH + 1;
+    describe("caching", () => {
+      it("should reuse the cache on subsequent calls", async () => {
+        await feed.getLinksWithDefaults();
+        await feed.getLinksWithDefaults();
+
+        assert.calledOnce(global.NewTabUtils.activityStreamLinks.getTopSites);
+      });
+      it("should ignore the cache when requesting more", async () => {
+        await feed.getLinksWithDefaults();
+        feed.store.state.Prefs.values.topSitesCount *= 3;
+
+        await feed.getLinksWithDefaults();
+
+        assert.calledTwice(global.NewTabUtils.activityStreamLinks.getTopSites);
+      });
+      it("should migrate frecent screenshot data without getting screenshots again", async () => {
+        // Don't add favicons so we fall back to screenshots
+        fakeNewTabUtils.activityStreamProvider._addFavicons = sandbox.stub();
+        await feed.getLinksWithDefaults();
+        const {callCount} = fakeScreenshot.getScreenshotForURL;
+        feed.frecentCache.expire();
 
-      const result = await feed.getLinksWithDefaults();
+        const result = await feed.getLinksWithDefaults();
+
+        assert.calledTwice(global.NewTabUtils.activityStreamLinks.getTopSites);
+        assert.callCount(fakeScreenshot.getScreenshotForURL, callCount);
+        assert.propertyVal(result[0], "screenshot", FAKE_SCREENSHOT);
+      });
+      it("should migrate pinned favicon data without getting favicons again", async () => {
+        fakeNewTabUtils.pinnedLinks.links = [{url: "https://foo.com/"}];
+        await feed.getLinksWithDefaults();
+        const {callCount} = fakeNewTabUtils.activityStreamProvider._addFavicons;
+        feed.pinnedCache.expire();
 
-      assert.propertyVal(result, "length", feed.store.state.Prefs.values.topSitesCount);
+        const result = await feed.getLinksWithDefaults();
+
+        assert.callCount(fakeNewTabUtils.activityStreamProvider._addFavicons, callCount);
+        assert.propertyVal(result[0], "favicon", FAKE_FAVICON);
+        assert.propertyVal(result[0], "faviconSize", FAKE_FAVICON_SIZE);
+      });
+      it("should not expose internal link properties", async() => {
+        const result = await feed.getLinksWithDefaults();
+
+        const internal = Object.keys(result[0]).filter(key => key.startsWith("__"));
+        assert.equal(internal.join(""), "");
+      });
     });
     describe("deduping", () => {
       beforeEach(() => {
         ({TopSitesFeed, DEFAULT_TOP_SITES} = injector({
           "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
           "common/Reducers.jsm": {insertPinned, TOP_SITES_SHOWMORE_LENGTH},
           "lib/Screenshots.jsm": {Screenshots: fakeScreenshot}
         }));
@@ -273,102 +326,104 @@ describe("Top Sites Feed", () => {
         }
       });
       it("should check against null entries", async () => {
         fakeNewTabUtils.pinnedLinks.links = [null];
 
         await feed.getLinksWithDefaults();
       });
     });
+    it("should call _fetchIcon for each link", async () => {
+      sinon.spy(feed, "_fetchIcon");
+
+      const results = await feed.getLinksWithDefaults(action);
+
+      assert.callCount(feed._fetchIcon, results.length);
+      results.forEach(link => {
+        assert.calledWith(feed._fetchIcon, link);
+      });
+    });
   });
   describe("#refresh", () => {
+    beforeEach(() => {
+      sandbox.stub(feed, "_fetchIcon");
+    });
     it("should initialise _tippyTopProvider if it's not already initialised", async () => {
       feed._tippyTopProvider.initialized = false;
       await feed.refresh(action);
       assert.ok(feed._tippyTopProvider.initialized);
     });
     it("should dispatch an action with the links returned", async () => {
-      sandbox.stub(feed, "getScreenshot");
       await feed.refresh(action);
       const reference = links.map(site => Object.assign({}, site, {hostname: shortURLStub(site)}));
 
       assert.calledOnce(feed.store.dispatch);
       assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
       assert.deepEqual(feed.store.dispatch.firstCall.args[0].data, reference);
     });
-    it("should call _fetchIcon for each link and pass in existing screenshots", async () => {
-      feed.store.state.TopSites.rows = [{url: FAKE_LINKS[0].url, screenshot: "foo.jpg"}];
-      const expectedScreenshotCache = {};
-      expectedScreenshotCache[FAKE_LINKS[0].url] = "foo.jpg";
-      sinon.spy(feed, "_fetchIcon");
-      await feed.refresh(action);
-      const results = feed.store.dispatch.firstCall.args[0].data;
-      assert.callCount(feed._fetchIcon, results.length);
-      results.forEach(link => {
-        assert.calledWith(feed._fetchIcon, link, expectedScreenshotCache);
-      });
-    });
     it("should handle empty slots in the resulting top sites array", async () => {
       links = [FAKE_LINKS[0]];
       fakeNewTabUtils.pinnedLinks.links = [null, null, FAKE_LINKS[1], null, null, null, null, null, FAKE_LINKS[2]];
-      sandbox.stub(feed, "getScreenshot");
       await feed.refresh(action);
       assert.calledOnce(feed.store.dispatch);
     });
   });
   describe("#_fetchIcon", () => {
-    it("should reuse screenshots for existing links, and call feed.getScreenshot for others", () => {
-      sandbox.stub(feed, "getScreenshot");
-      const screenshotCache = {};
-      screenshotCache[FAKE_LINKS[0].url] = "foo.jpg";
-      screenshotCache[FAKE_LINKS[1].url] = "bar.png";
+    it("should reuse screenshot on the link", () => {
+      const link = {screenshot: "reuse.png"};
+
+      feed._fetchIcon(link);
+
+      assert.notCalled(fakeScreenshot.getScreenshotForURL);
+      assert.propertyVal(link, "screenshot", "reuse.png");
+    });
+    it("should reuse existing fetching screenshot on the link", async () => {
+      const link = {__fetchingScreenshot: Promise.resolve("fetching.png")};
+
+      await feed._fetchIcon(link);
 
-      feed._fetchIcon(FAKE_LINKS[0], screenshotCache);
-      assert.notCalled(feed.getScreenshot);
-      assert.propertyVal(FAKE_LINKS[0], "screenshot", "foo.jpg");
+      assert.notCalled(fakeScreenshot.getScreenshotForURL);
+      assert.propertyVal(link, "screenshot", "fetching.png");
+    });
+    it("should get a screenshot if the link is missing it", () => {
+      feed._fetchIcon(FAKE_LINKS[0]);
 
-      feed._fetchIcon(FAKE_LINKS[1], screenshotCache);
-      assert.notCalled(feed.getScreenshot);
-      assert.propertyVal(FAKE_LINKS[1], "screenshot", "bar.png");
+      assert.calledOnce(fakeScreenshot.getScreenshotForURL);
+      assert.calledWith(fakeScreenshot.getScreenshotForURL, FAKE_LINKS[0].url);
+    });
+    it("should update the link's cache if it can be updated", () => {
+      const link = {__updateCache: sandbox.stub()};
 
-      feed._fetchIcon(FAKE_LINKS[2], screenshotCache);
-      assert.calledOnce(feed.getScreenshot);
-      assert.calledWith(feed.getScreenshot, FAKE_LINKS[2].url);
+      feed._fetchIcon(link);
+
+      assert.calledOnce(link.__updateCache);
+      assert.calledWith(link.__updateCache, "__fetchingScreenshot");
     });
     it("should skip getting a screenshot if there is a tippy top icon", () => {
-      sandbox.stub(feed, "getScreenshot");
       feed._tippyTopProvider.processSite = site => {
         site.tippyTopIcon = "icon.png";
         site.backgroundColor = "#fff";
         return site;
       };
       const link = {url: "example.com"};
       feed._fetchIcon(link);
       assert.propertyVal(link, "tippyTopIcon", "icon.png");
       assert.notProperty(link, "screenshot");
-      assert.notCalled(feed.getScreenshot);
+      assert.notCalled(fakeScreenshot.getScreenshotForURL);
     });
     it("should skip getting a screenshot if there is an icon of size greater than 96x96 and no tippy top", () => {
-      sandbox.stub(feed, "getScreenshot");
       const link = {
         url: "foo.com",
         favicon: "data:foo",
         faviconSize: 196
       };
       feed._fetchIcon(link);
       assert.notProperty(link, "tippyTopIcon");
       assert.notProperty(link, "screenshot");
-      assert.notCalled(feed.getScreenshot);
-    });
-  });
-  describe("getScreenshot", () => {
-    it("should call Screenshots.getScreenshotForURL with the right url", async () => {
-      const url = "foo.com";
-      await feed.getScreenshot(url);
-      assert.calledWith(fakeScreenshot.getScreenshotForURL, url);
+      assert.notCalled(fakeScreenshot.getScreenshotForURL);
     });
   });
   describe("#onAction", () => {
     const newTabAction = {type: at.NEW_TAB_LOAD, meta: {fromTarget: "target"}};
     it("should not call refresh if there are enough sites on NEW_TAB_LOAD", () => {
       feed.lastUpdated = Date.now();
       sinon.stub(feed, "refresh");
       feed.onAction(newTabAction);
@@ -392,27 +447,16 @@ describe("Top Sites Feed", () => {
       const pinAction = {
         type: at.TOP_SITES_PIN,
         data: {site: {url: "foo.com"}, index: 7}
       };
       feed.onAction(pinAction);
       assert.calledOnce(fakeNewTabUtils.pinnedLinks.pin);
       assert.calledWith(fakeNewTabUtils.pinnedLinks.pin, pinAction.data.site, pinAction.data.index);
     });
-    it("should compare against links if available, instead of getting from store", () => {
-      const frecentSite = {url: "foo.com", faviconSize: 32, favicon: "favicon.png"};
-      const pinnedSite1 = {url: "bar.com"};
-      const pinnedSite2 = {url: "foo.com"};
-      fakeNewTabUtils.pinnedLinks.links = [pinnedSite1, pinnedSite2];
-      feed.store = {getState() { return {TopSites: {rows: sinon.spy()}}; }};
-      let result = feed._getPinnedWithData([frecentSite]);
-      assert.include(result[0], pinnedSite1);
-      assert.include(result[1], Object.assign({}, frecentSite, pinnedSite2));
-      assert.notCalled(feed.store.getState().TopSites.rows);
-    });
     it("should trigger refresh on TOP_SITES_PIN", () => {
       sinon.stub(feed, "refresh");
       const pinExistingAction = {type: at.TOP_SITES_PIN, data: {site: FAKE_LINKS[4], index: 4}};
 
       feed.onAction(pinExistingAction);
 
       assert.calledOnce(feed.refresh);
     });
@@ -436,17 +480,17 @@ describe("Top Sites Feed", () => {
     });
     it("should call refresh without a target if we clear history with PLACES_HISTORY_CLEARED", () => {
       sandbox.stub(feed, "refresh");
       feed.onAction({type: at.PLACES_HISTORY_CLEARED});
       assert.calledOnce(feed.refresh);
       assert.equal(feed.refresh.firstCall.args[0], null);
     });
     it("should still dispatch an action even if there's no target provided", async () => {
-      sandbox.stub(feed, "getScreenshot");
+      sandbox.stub(feed, "_fetchIcon");
       await feed.refresh();
       assert.calledOnce(feed.store.dispatch);
       assert.propertyVal(feed.store.dispatch.firstCall.args[0], "type", at.TOP_SITES_UPDATED);
     });
     it("should call refresh on INIT action", async () => {
       sinon.stub(feed, "refresh");
       await feed.onAction({type: at.INIT});
       assert.calledOnce(feed.refresh);
--- a/browser/modules/PingCentre.jsm
+++ b/browser/modules/PingCentre.jsm
@@ -109,22 +109,27 @@ class PingCentre {
   async sendPing(data) {
     let experiments = TelemetryEnvironment.getActiveExperiments();
     let experimentsString = this._createExperimentsString(experiments);
     if (!this.enabled) {
       return Promise.resolve();
     }
 
     let clientID = data.client_id || await this.telemetryClientId;
+    let locale = data.locale || Services.locale.getAppLocalesAsLangTags().pop();
     const payload = Object.assign({
+      locale,
       topic: this._topic,
       client_id: clientID,
-      shield_id: experimentsString,
+      version: AppConstants.MOZ_APP_VERSION,
       release_channel: AppConstants.MOZ_UPDATE_CHANNEL
     }, data);
+    if (experimentsString) {
+      payload.shield_id = experimentsString;
+    }
 
     if (this.logging) {
       // performance related pings cause a lot of logging, so we mute them
       if (data.action !== "activity_stream_performance") {
         Services.console.logStringMessage(`TELEMETRY PING: ${JSON.stringify(payload)}\n`);
       }
     }
 
--- a/browser/themes/shared/compacttheme.inc.css
+++ b/browser/themes/shared/compacttheme.inc.css
@@ -2,16 +2,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/.
 
 /* compacttheme.css is loaded in browser.xul after browser.css when it is
    preffed on.  The bulk of the styling is here in the shared file, but
    there are overrides for each platform in their compacttheme.css files. */
 
 :root:-moz-lwtheme {
+  text-shadow: none;
+
   --toolbar-bgcolor: var(--chrome-secondary-background-color);
   --toolbar-gbimage: none;
   --toolbar-non-lwt-bgcolor: var(--toolbar-bgcolor);
   --toolbar-non-lwt-textcolor: var(--chrome-color);
   --toolbar-non-lwt-bgimage: none;
 
   --toolbarbutton-icon-fill-opacity: .7;
 
@@ -82,25 +84,16 @@ toolbar[brighttext] .toolbarbutton-1 {
 }
 
 /* Default findbar text color doesn't look good - Bug 1125677 */
 .browserContainer > findbar .findbar-find-status,
 .browserContainer > findbar .found-matches {
   color: inherit;
 }
 
-.browserContainer > findbar .findbar-button,
-#PlacesToolbar toolbarbutton.bookmark-item {
-  text-shadow: none;
-}
-
-#TabsToolbar {
-  text-shadow: none !important;
-}
-
 /* URL bar and search bar*/
 #urlbar:not([focused="true"]),
 .searchbar-textbox:not([focused="true"]) {
   border-color: var(--chrome-nav-bar-controls-border-color);
 }
 
 #urlbar[pageproxystate="valid"] > #identity-box.verifiedIdentity > #identity-icon-labels:-moz-lwtheme-brighttext {
   color: #30e60b;
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -1617,18 +1617,18 @@ toolbarpaletteitem[place="palette"] > .t
 #bookmarks-menu-button[cui-areatype="menu-panel"] > .toolbarbutton-menu-dropmarker,
 #bookmarks-menu-button[overflowedItem] > .toolbarbutton-menu-dropmarker,
 toolbarpaletteitem[place="palette"] > .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker,
 #bookmarks-menu-button[cui-areatype="menu-panel"] > .toolbarbutton-menubutton-dropmarker {
   display: none;
 }
 
 #search-container[cui-areatype="menu-panel"] {
-  margin-top: 6px;
-  margin-bottom: 6px;
+  padding-top: 6px;
+  padding-bottom: 6px;
 }
 
 toolbarpaletteitem[place="palette"] > #search-container {
   min-width: 7em;
   width: 7em;
   min-height: 37px;
 }
 
--- a/browser/themes/shared/toolbarbutton-icons.inc.css
+++ b/browser/themes/shared/toolbarbutton-icons.inc.css
@@ -18,17 +18,18 @@ toolbar[brighttext] {
   fill-opacity: var(--toolbarbutton-icon-fill-opacity);
 }
 
 #back-button:-moz-locale-dir(rtl) > .toolbarbutton-icon,
 #forward-button:-moz-locale-dir(rtl) > .toolbarbutton-icon,
 #reload-button:-moz-locale-dir(rtl) > .toolbarbutton-icon,
 #nav-bar-overflow-button:-moz-locale-dir(rtl) > .toolbarbutton-icon,
 #PlacesChevron:-moz-locale-dir(rtl) > .toolbarbutton-icon,
-#panic-button:-moz-locale-dir(rtl) > .toolbarbutton-icon {
+#panic-button:-moz-locale-dir(rtl) > .toolbarbutton-icon,
+#PanelUI-menu-button:-moz-locale-dir(rtl) > .toolbarbutton-badge-stack > .toolbarbutton-icon {
   transform: scaleX(-1);
 }
 
 #back-button {
   list-style-image: url("chrome://browser/skin/back.svg");
 }
 
 #forward-button {
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -1,21 +1,15 @@
 dnl This Source Code Form is subject to the terms of the Mozilla Public
 dnl License, v. 2.0. If a copy of the MPL was not distributed with this
 dnl file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 AC_DEFUN([MOZ_ANDROID_NDK],
 [
 
-MOZ_ARG_WITH_STRING(android-cxx-stl,
-[  --with-android-cxx-stl=VALUE
-                          use the specified C++ STL (libstdc++, libc++)],
-    android_cxx_stl=$withval,
-    android_cxx_stl=libc++)
-
 case "$target" in
 *-android*|*-linuxandroid*)
     dnl $android_platform will be set for us by Python configure.
     CPPFLAGS="-idirafter $android_platform/usr/include $CPPFLAGS"
     CFLAGS="-fno-short-enums -fno-exceptions $CFLAGS"
     CXXFLAGS="-fno-short-enums -fno-exceptions $CXXFLAGS"
     ASFLAGS="-idirafter $android_platform/usr/include -DANDROID $ASFLAGS"
 
@@ -59,92 +53,63 @@ if test "$OS_TARGET" = "Android"; then
 fi
 ])
 
 AC_DEFUN([MOZ_ANDROID_STLPORT],
 [
 
 if test "$OS_TARGET" = "Android"; then
     if test -z "$STLPORT_CPPFLAGS$STLPORT_LIBS"; then
-        case "$android_cxx_stl" in
-        libstdc++)
-            # android-ndk-r8b and later
-            ndk_base="$android_ndk/sources/cxx-stl/gnu-libstdc++/$android_gnu_compiler_version"
-            ndk_libs_include="$ndk_base/libs/$ANDROID_CPU_ARCH"
-            # NDK r12 removed the arm/thumb library split and just made
-            # everything thumb by default.  Attempt to compensate.
-            if test "$MOZ_THUMB2" = 1 -a -d "$ndk_libs_include/thumb"; then
-                ndk_libs="$ndk_libs_include/thumb"
-            else
-                ndk_libs="$ndk_libs_include"
-            fi
-            ndk_include="$ndk_base/include"
+        # android-ndk-r8b and later
+        ndk_base="$android_ndk/sources/cxx-stl"
+        cxx_base="$ndk_base/llvm-libc++"
+        cxx_libs="$cxx_base/libs/$ANDROID_CPU_ARCH"
+        # NDK r12 removed the arm/thumb library split and just made
+        # everything thumb by default.  Attempt to compensate.
+        if test "$MOZ_THUMB2" = 1 -a -d "$cxx_libs/thumb"; then
+            cxx_libs="$cxx_libs/thumb"
+        fi
+        cxx_include="$cxx_base/libcxx/include"
+        cxxabi_base="$ndk_base/llvm-libc++abi"
+        cxxabi_include="$cxxabi_base/libcxxabi/include"
 
-            if ! test -e "$ndk_libs/libgnustl_static.a"; then
-                AC_MSG_ERROR([Couldn't find path to gnu-libstdc++ in the android ndk])
-            fi
-
-            STLPORT_LIBS="-L$ndk_libs -lgnustl_static"
-            STLPORT_CPPFLAGS="-I$ndk_include -I$ndk_include/backward -I$ndk_libs_include/include"
-            ;;
-        libc++)
-            # android-ndk-r8b and later
-            ndk_base="$android_ndk/sources/cxx-stl"
-            cxx_base="$ndk_base/llvm-libc++"
-            cxx_libs="$cxx_base/libs/$ANDROID_CPU_ARCH"
-            # NDK r12 removed the arm/thumb library split and just made
-            # everything thumb by default.  Attempt to compensate.
-            if test "$MOZ_THUMB2" = 1 -a -d "$cxx_libs/thumb"; then
-                cxx_libs="$cxx_libs/thumb"
-            fi
-            cxx_include="$cxx_base/libcxx/include"
-            cxxabi_base="$ndk_base/llvm-libc++abi"
-            cxxabi_include="$cxxabi_base/libcxxabi/include"
+        if ! test -e "$cxx_libs/libc++_static.a"; then
+            AC_MSG_ERROR([Couldn't find path to llvm-libc++ in the android ndk])
+        fi
 
-            if ! test -e "$cxx_libs/libc++_static.a"; then
-                AC_MSG_ERROR([Couldn't find path to llvm-libc++ in the android ndk])
-            fi
-
+        if ! test -e "$cxx_include"; then
+            # NDK r13 removes the inner "libcxx" directory.
+            cxx_include="$cxx_base/include"
             if ! test -e "$cxx_include"; then
-                # NDK r13 removes the inner "libcxx" directory.
-                cxx_include="$cxx_base/include"
-                if ! test -e "$cxx_include"; then
-                    AC_MSG_ERROR([Couldn't find path to libc++ includes in the android ndk])
-                fi
+                AC_MSG_ERROR([Couldn't find path to libc++ includes in the android ndk])
             fi
+        fi
 
+        if ! test -e "$cxxabi_include"; then
+            # NDK r13 removes the inner "libcxxabi" directory.
+            cxxabi_include="$cxxabi_base/include"
             if ! test -e "$cxxabi_include"; then
-                # NDK r13 removes the inner "libcxxabi" directory.
-                cxxabi_include="$cxxabi_base/include"
-                if ! test -e "$cxxabi_include"; then
-                    AC_MSG_ERROR([Couldn't find path to libc++abi includes in the android ndk])
-                fi
+                AC_MSG_ERROR([Couldn't find path to libc++abi includes in the android ndk])
             fi
+        fi
 
-            STLPORT_LIBS="-L$cxx_libs -lc++_static"
-            # NDK r12 split the libc++ runtime libraries into pieces.
-            for lib in c++abi unwind android_support; do
-                if test -e "$cxx_libs/lib${lib}.a"; then
-                     STLPORT_LIBS="$STLPORT_LIBS -l${lib}"
-                fi
-            done
-            # Add android/support/include/ for prototyping long double math
-            # functions, locale-specific C library functions, multibyte support,
-            # etc.
-            STLPORT_CPPFLAGS="-I$cxx_include -I$android_ndk/sources/android/support/include -I$cxxabi_include"
-            ;;
-        *)
-            AC_MSG_ERROR([Bad value for --enable-android-cxx-stl])
-            ;;
-        esac
+        STLPORT_LIBS="-L$cxx_libs -lc++_static"
+        # NDK r12 split the libc++ runtime libraries into pieces.
+        for lib in c++abi unwind android_support; do
+            if test -e "$cxx_libs/lib${lib}.a"; then
+                 STLPORT_LIBS="$STLPORT_LIBS -l${lib}"
+            fi
+        done
+        # Add android/support/include/ for prototyping long double math
+        # functions, locale-specific C library functions, multibyte support,
+        # etc.
+        STLPORT_CPPFLAGS="-I$cxx_include -I$android_ndk/sources/android/support/include -I$cxxabi_include"
     fi
     CXXFLAGS="$CXXFLAGS $STLPORT_CPPFLAGS"
 fi
-MOZ_ANDROID_CXX_STL=$android_cxx_stl
-AC_SUBST([MOZ_ANDROID_CXX_STL])
 AC_SUBST([STLPORT_LIBS])
 
 ])
 
 
 AC_DEFUN([concat],[$1$2$3$4])
 
 dnl Find a component of an AAR.
--- a/build/docs/mozinfo.rst
+++ b/build/docs/mozinfo.rst
@@ -155,22 +155,8 @@ toolkit
    ``MOZ_WIDGET_TOOLKIT`` ``config.status`` variable.
 
    Always defined.
 
 topsrcdir
    The path to the source directory the build came from.
 
    Always defined.
-
-wave
-   Whether Wave audio support is enabled.
-
-   Values are ``true`` and ``false``.
-
-   Always defined.
-
-webm
-   Whether WebM support is enabled.
-
-   Values are ``true`` and ``false``.
-
-   Always defined.
--- a/build/gecko_templates.mozbuild
+++ b/build/gecko_templates.mozbuild
@@ -53,21 +53,18 @@ def GeckoBinary(linkage='dependent', msv
     elif linkage != None:
         error('`linkage` must be "dependent", "standalone" or None')
 
     if mozglue:
         LDFLAGS += CONFIG['MOZ_GLUE_WRAP_LDFLAGS']
         if mozglue == 'program':
             USE_LIBS += ['mozglue']
             DEFINES['MOZ_HAS_MOZGLUE'] = True
-            if CONFIG['MOZ_GLUE_IN_PROGRAM']:
-                if CONFIG['GNU_CC']:
-                    LDFLAGS += ['-rdynamic']
-                if CONFIG['MOZ_MEMORY']:
-                    USE_LIBS += ['memory']
+            if CONFIG['MOZ_GLUE_IN_PROGRAM'] and CONFIG['GNU_CC']:
+                LDFLAGS += ['-rdynamic']
         elif mozglue == 'library':
             LIBRARY_DEFINES['MOZ_HAS_MOZGLUE'] = True
             if not CONFIG['MOZ_GLUE_IN_PROGRAM']:
                 USE_LIBS += ['mozglue']
         else:
             error('`mozglue` must be "program" or "library"')
 
     if not CONFIG['JS_STANDALONE']:
--- a/build/moz.configure/android-ndk.configure
+++ b/build/moz.configure/android-ndk.configure
@@ -6,19 +6,16 @@
 
 
 js_option('--with-android-ndk', nargs=1,
           help='location where the Android NDK can be found')
 
 js_option('--with-android-toolchain', nargs=1,
           help='location of the Android toolchain')
 
-js_option('--with-android-gnu-compiler-version', nargs=1,
-          help='GNU compiler version to use')
-
 @depends(target)
 def min_android_version(target):
     if target.cpu in ['aarch64', 'x86_64', 'mips64']:
         # 64-bit support was added in API 21.
         return '21'
     return '9'
 
 js_option('--with-android-version',
@@ -136,55 +133,50 @@ add_old_configure_assignment('android_pl
 
 @depends(android_platform)
 def extra_toolchain_flags(platform_dir):
     if not platform_dir:
         return []
     return ['-idirafter',
             os.path.join(platform_dir, 'usr', 'include')]
 
-@depends(target, host, ndk, '--with-android-toolchain',
-         '--with-android-gnu-compiler-version')
+@depends(target, host, ndk, '--with-android-toolchain')
 @checking('for the Android toolchain directory', lambda x: x or 'not found')
 @imports(_from='os.path', _import='isdir')
 @imports(_from='mozbuild.shellutil', _import='quote')
-def android_toolchain(target, host, ndk, toolchain, gnu_compiler_version):
+def android_toolchain(target, host, ndk, toolchain):
     if not ndk:
         return
     if toolchain:
         return toolchain[0]
     else:
         if target.cpu == 'arm' and target.endianness == 'little':
             target_base = 'arm-linux-androideabi'
         elif target.cpu == 'x86':
             target_base = 'x86'
         elif target.cpu == 'mips32' and target.endianness == 'little':
             target_base = 'mipsel-linux-android'
         elif target.cpu == 'aarch64' and target.endianness == 'little':
             target_base = 'aarch64-linux-android'
         else:
             die('Target cpu is not supported.')
 
-        toolchain_format = '%s/toolchains/%s-%s/prebuilt/%s-%s'
+        toolchain_format = '%s/toolchains/%s-4.9/prebuilt/%s-%s'
 
-        for version in gnu_compiler_version or ['4.9', '4.8', '4.7']:
+        toolchain = toolchain_format % (ndk, target_base,
+                                        host.kernel.lower(), host.cpu)
+        log.debug('Trying %s' % quote(toolchain))
+        if not isdir(toolchain) and host.cpu == 'x86_64':
             toolchain = toolchain_format % (ndk, target_base, version,
-                                            host.kernel.lower(), host.cpu)
+                                            host.kernel.lower(), 'x86')
             log.debug('Trying %s' % quote(toolchain))
-            if not isdir(toolchain) and host.cpu == 'x86_64':
-                toolchain = toolchain_format % (ndk, target_base, version,
-                                                host.kernel.lower(), 'x86')
-                log.debug('Trying %s' % quote(toolchain))
-            if isdir(toolchain):
-                return toolchain
-        else:
-            if gnu_compiler_version:
-                die('Your --with-android-gnu-compiler-version may be wrong')
-            die('You have to specify --with-android-toolchain='
-                '/path/to/ndk/toolchain.')
+        if isdir(toolchain):
+            return toolchain
+        die('You have to specify --with-android-toolchain='
+            '/path/to/ndk/toolchain.')
 
 set_config('ANDROID_TOOLCHAIN', android_toolchain)
 
 @depends(target, android_toolchain)
 def android_toolchain_prefix(target, toolchain):
     if toolchain:
         if target.cpu == 'x86':
             # Ideally, the --target should just have the right x86 variant
--- a/build/moz.configure/old.configure
+++ b/build/moz.configure/old.configure
@@ -229,17 +229,16 @@ def old_configure_options(*options):
     '--enable-verify-mar',
     '--enable-webrtc',
     '--enable-xul',
     '--enable-zipwriter',
     '--includedir',
     '--libdir',
     '--no-create',
     '--prefix',
-    '--with-android-cxx-stl',
     '--with-android-distribution-directory',
     '--with-android-max-sdk',
     '--with-android-min-sdk',
     '--with-android-sdk',
     '--with-app-basename',
     '--with-app-name',
     '--with-arch',
     '--with-branding',
--- a/build/templates.mozbuild
+++ b/build/templates.mozbuild
@@ -47,16 +47,18 @@ def SimplePrograms(names, ext='.cpp'):
 
 
 @template
 def CppUnitTests(names, ext='.cpp'):
     '''Template for C++ unit tests.
 
     Those have a single source with the same base name as the executable.
     '''
+    COMPILE_FLAGS['EXTRA_INCLUDES'] = ['-I%s/dist/include' % TOPOBJDIR,
+                                       '-I%s/dist/include/testing' % TOPOBJDIR]
     CPP_UNIT_TESTS += names
     SOURCES += ['%s%s' % (name, ext) for name in names]
 
     Binary()
 
 
 @template
 def Library(name):
--- a/config/config.mk
+++ b/config/config.mk
@@ -248,28 +248,16 @@ CCC = $(CXX)
 
 INCLUDES = \
   -I$(srcdir) \
   -I$(CURDIR) \
   $(LOCAL_INCLUDES) \
   -I$(ABS_DIST)/include \
   $(NULL)
 
-ifndef IS_GYP_DIR
-# NSPR_CFLAGS and NSS_CFLAGS must appear ahead of the other flags to avoid Linux
-# builds wrongly picking up system NSPR/NSS header files.
-OS_INCLUDES := \
-  $(NSPR_CFLAGS) $(NSS_CFLAGS) \
-  $(MOZ_JPEG_CFLAGS) \
-  $(MOZ_PNG_CFLAGS) \
-  $(MOZ_ZLIB_CFLAGS) \
-  $(MOZ_PIXMAN_CFLAGS) \
-  $(NULL)
-endif
-
 include $(MOZILLA_DIR)/config/static-checking-config.mk
 
 CFLAGS		= $(OS_CPPFLAGS) $(OS_CFLAGS)
 CXXFLAGS	= $(OS_CPPFLAGS) $(OS_CXXFLAGS)
 LDFLAGS		= $(OS_LDFLAGS) $(MOZBUILD_LDFLAGS) $(MOZ_FIX_LINK_PATHS)
 
 ifdef MOZ_OPTIMIZE
 ifeq (1,$(MOZ_OPTIMIZE))
@@ -319,18 +307,18 @@ endif # CLANG_CL
 
 # Use warnings-as-errors if ALLOW_COMPILER_WARNINGS is not set to 1 (which
 # includes the case where it's undefined).
 ifneq (1,$(ALLOW_COMPILER_WARNINGS))
 CXXFLAGS += $(WARNINGS_AS_ERRORS)
 CFLAGS   += $(WARNINGS_AS_ERRORS)
 endif # ALLOW_COMPILER_WARNINGS
 
-COMPILE_CFLAGS	= $(COMPUTED_CFLAGS) $(DEFINES) $(INCLUDES) $(OS_INCLUDES) $(DSO_CFLAGS) $(DSO_PIC_CFLAGS) $(RTL_FLAGS) $(OS_COMPILE_CFLAGS) $(_DEPEND_CFLAGS) $(CFLAGS) $(MOZBUILD_CFLAGS) $(MK_COMPILE_DEFINES)
-COMPILE_CXXFLAGS = $(COMPUTED_CXXFLAGS) $(DEFINES) $(INCLUDES) $(OS_INCLUDES) $(DSO_CFLAGS) $(DSO_PIC_CFLAGS) $(RTL_FLAGS) $(OS_COMPILE_CXXFLAGS) $(_DEPEND_CFLAGS) $(CXXFLAGS) $(MOZBUILD_CXXFLAGS) $(MK_COMPILE_DEFINES)
+COMPILE_CFLAGS	= $(COMPUTED_CFLAGS) $(DSO_CFLAGS) $(DSO_PIC_CFLAGS) $(RTL_FLAGS) $(OS_COMPILE_CFLAGS) $(_DEPEND_CFLAGS) $(CFLAGS) $(MOZBUILD_CFLAGS) $(MK_COMPILE_DEFINES)
+COMPILE_CXXFLAGS = $(COMPUTED_CXXFLAGS) $(DSO_CFLAGS) $(DSO_PIC_CFLAGS) $(RTL_FLAGS) $(OS_COMPILE_CXXFLAGS) $(_DEPEND_CFLAGS) $(CXXFLAGS) $(MOZBUILD_CXXFLAGS) $(MK_COMPILE_DEFINES)
 COMPILE_CMFLAGS = $(OS_COMPILE_CMFLAGS) $(MOZBUILD_CMFLAGS)
 COMPILE_CMMFLAGS = $(OS_COMPILE_CMMFLAGS) $(MOZBUILD_CMMFLAGS)
 ASFLAGS += $(MOZBUILD_ASFLAGS)
 
 ifndef CROSS_COMPILE
 HOST_CFLAGS += $(RTL_FLAGS)
 endif
 
--- a/config/rules.mk
+++ b/config/rules.mk
@@ -63,17 +63,16 @@ testxpcobjdir = $(DEPTH)/_tests/xpcshell
 ifdef ENABLE_TESTS
 ifdef CPP_UNIT_TESTS
 ifdef COMPILE_ENVIRONMENT
 
 # Compile the tests to $(DIST)/bin.  Make lots of niceties available by default
 # through TestHarness.h, by modifying the list of includes and the libs against
 # which stuff links.
 SIMPLE_PROGRAMS += $(CPP_UNIT_TESTS)
-INCLUDES += -I$(ABS_DIST)/include/testing
 
 ifndef MOZ_PROFILE_GENERATE
 CPP_UNIT_TESTS_FILES = $(CPP_UNIT_TESTS)
 CPP_UNIT_TESTS_DEST = $(DIST)/cppunittests
 CPP_UNIT_TESTS_TARGET = target
 INSTALL_TARGETS += CPP_UNIT_TESTS
 endif
 
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -435,18 +435,26 @@ function* clickOnAnimation(panel, index,
 
   // Expect a selection event.
   let onSelectionChanged = timeline.once(shouldAlreadySelected
                                          ? "animation-already-selected"
                                          : "animation-selected");
 
   info("Click on animation " + index + " in the timeline");
   let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
-  EventUtils.sendMouseEvent({type: "click"}, timeBlock,
-                            timeBlock.ownerDocument.defaultView);
+  // Scroll to show the timeBlock since the element may be out of displayed area.
+  timeBlock.scrollIntoView(false);
+  let timeBlockBounds = timeBlock.getBoundingClientRect();
+  let x = timeBlockBounds.width / 2;
+  let y = timeBlockBounds.height / 2;
+  if (timeBlock != timeBlock.ownerDocument.elementFromPoint(x, y)) {
+    // Move the mouse pointer a little since scrubber element may be at the point.
+    x += timeBlockBounds.width / 4;
+  }
+  EventUtils.synthesizeMouse(timeBlock, x, y, {}, timeBlock.ownerDocument.defaultView);
 
   return yield onSelectionChanged;
 }
 
 /**
  * Get an instance of the Keyframes component from the timeline.
  * @param {AnimationsPanel} panel The panel instance.
  * @param {String} propertyName The name of the animated property.
--- a/devtools/client/netmonitor/src/components/headers-panel.js
+++ b/devtools/client/netmonitor/src/components/headers-panel.js
@@ -61,22 +61,26 @@ const HeadersPanel = createClass({
   getInitialState() {
     return {
       rawHeadersOpened: false,
     };
   },
 
   getProperties(headers, title) {
     if (headers && headers.headers.length) {
-      return {
-        [`${title} (${getFormattedSize(headers.headersSize, 3)})`]:
+      let headerKey = `${title} (${getFormattedSize(headers.headersSize, 3)})`;
+      let propertiesResult = {
+        [headerKey]:
           headers.headers.reduce((acc, { name, value }) =>
             name ? Object.assign(acc, { [name]: value }) : acc
           , {})
       };
+
+      propertiesResult[headerKey] = this.sortByKey(propertiesResult[headerKey]);
+      return propertiesResult;
     }
 
     return null;
   },
 
   toggleRawHeaders() {
     this.setState({
       rawHeadersOpened: !this.state.rawHeadersOpened,
@@ -119,16 +123,26 @@ const HeadersPanel = createClass({
         })),
         headerDocURL ? MDNLink({
           url: headerDocURL,
         }) : null
       )
     );
   },
 
+  sortByKey: function (object) {
+    let result = {};
+    Object.keys(object).sort(function (left, right) {
+      return left.toLowerCase().localeCompare(right.toLowerCase());
+    }).forEach(function (key) {
+      result[key] = object[key];
+    });
+    return result;
+  },
+
   render() {
     const {
       openLink,
       cloneSelectedRequest,
       request: {
         fromCache,
         fromServiceWorker,
         httpVersion,
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -111,16 +111,17 @@ skip-if = (os == 'linux' && debug && bit
 [browser_net_filter-01.js]
 skip-if = (os == 'linux' && debug && bits == 32) # Bug 1303439
 [browser_net_filter-02.js]
 [browser_net_filter-03.js]
 [browser_net_filter-04.js]
 [browser_net_filter-autocomplete.js]
 [browser_net_filter-flags.js]
 [browser_net_footer-summary.js]
+[browser_net_headers_sorted.js]
 [browser_net_icon-preview.js]
 [browser_net_image-tooltip.js]
 [browser_net_json-b64.js]
 [browser_net_json-null.js]
 [browser_net_json-long.js]
 [browser_net_json-malformed.js]
 [browser_net_json_custom_mime.js]
 [browser_net_json_text_mime.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_headers_sorted.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if Request-Headers and Response-Headers are sorted in Headers tab.
+ */
+add_task(function* () {
+  let { tab, monitor } = yield initNetMonitor(SIMPLE_SJS);
+  info("Starting test... ");
+
+  let { document, store, windowRequire } = monitor.panelWin;
+  let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+
+  store.dispatch(Actions.batchEnable(false));
+
+  tab.linkedBrowser.reload();
+
+  let wait = waitForNetworkEvents(monitor, 1);
+  yield wait;
+
+  wait = waitForDOM(document, ".headers-overview");
+  EventUtils.sendMouseEvent({ type: "mousedown" },
+    document.querySelectorAll(".request-list-item")[0]);
+  yield wait;
+
+  info("Check if Request-Headers and Response-Headers are sorted");
+  let expectedResponseHeaders = ["cache-control", "connection", "content-length",
+                                 "content-type", "date", "expires", "foo-bar",
+                                 "pragma", "server", "set-cookie"];
+  let expectedRequestHeaders = ["Accept", "Accept-Encoding", "Accept-Language",
+                                "Cache-Control", "Connection", "Cookie", "Host",
+                                "Pragma", "Upgrade-Insecure-Requests", "User-Agent"];
+
+  let labelCells = document.querySelectorAll(".treeLabelCell");
+  let actualResponseHeaders = [];
+  let actualRequestHeaders = [];
+
+  for (let i = 1; i < 11; i++) {
+    actualResponseHeaders.push(labelCells[i].innerText);
+  }
+
+  for (let i = 12; i < labelCells.length; i++) {
+    actualRequestHeaders.push(labelCells[i].innerText);
+  }
+
+  is(actualResponseHeaders.toString(), expectedResponseHeaders.toString(),
+    "Response Headers are sorted");
+
+  is(actualRequestHeaders.toString(), expectedRequestHeaders.toString(),
+    "Request Headers are sorted");
+
+  yield teardown(monitor);
+});
--- a/devtools/client/shared/components/notification-box.css
+++ b/devtools/client/shared/components/notification-box.css
@@ -28,17 +28,17 @@
 .notificationbox .details:dir(rtl)
 .notificationbox .notificationInner:dir(rtl) {
   flex-direction: row-reverse;
 }
 
 /* Style */
 
 .notificationbox .notification {
-  color: var(--theme-body-color-alt);
+  color: var(--theme-toolbar-color);
   background-color: var(--theme-body-background);
   text-shadow: none;
   border-bottom: 1px solid var(--theme-splitter-color);
 }
 
 .notificationbox .notification[data-type="info"] {
   color: -moz-DialogText;
   background-color: -moz-Dialog;
--- a/devtools/client/themes/animationinspector.css
+++ b/devtools/client/themes/animationinspector.css
@@ -258,16 +258,21 @@ body {
   right: var(--keyframes-marker-size);
   height: var(--timeline-animation-height);
 }
 
 .animation-detail .track-container {
   height: var(--detail-animation-height);
 }
 
+.time-body.track-container {
+  height: 0;
+  pointer-events: none;
+}
+
 .animation-timeline .scrubber-wrapper {
   position: absolute;
   z-index: 5;
   left: var(--timeline-sidebar-width);
   /* Leave the width of a marker right of a track so the 100% markers can be
      selected easily */
   right: var(--keyframes-marker-size);
   pointer-events: none;
@@ -338,17 +343,16 @@ body {
   top: var(--toolbar-height);
   z-index: 3;
 }
 
 .progress-tick-container .progress-tick,
 .animation-timeline .time-body .time-tick {
   -moz-user-select: none;
   position: absolute;
-  height: 100%;
 }
 
 .progress-tick-container .progress-tick::before,
 .animation-timeline .time-body .time-tick::before {
   content: "";
   position: fixed;
   height: 100vh;
   width: 0;
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -1223,16 +1223,20 @@ DebuggerServer.ObjectActorPreviewers = {
   Boolean: [function (objectActor, grip, rawObj) {
     return wrappedPrimitivePreviewer("Boolean", Boolean, objectActor, grip, rawObj);
   }],
 
   Number: [function (objectActor, grip, rawObj) {
     return wrappedPrimitivePreviewer("Number", Number, objectActor, grip, rawObj);
   }],
 
+  Symbol: [function (objectActor, grip, rawObj) {
+    return wrappedPrimitivePreviewer("Symbol", Symbol, objectActor, grip, rawObj);
+  }],
+
   Function: [function ({obj, hooks}, grip) {
     if (obj.name) {
       grip.name = obj.name;
     }
 
     if (obj.displayName) {
       grip.displayName = obj.displayName.substr(0, 500);
     }
@@ -1503,20 +1507,16 @@ DebuggerServer.ObjectActorPreviewers = {
  *        The object actor
  * @param Object grip
  *        The result grip to fill in
  * @return Booolean true if the object was handled, false otherwise
  */
 function wrappedPrimitivePreviewer(className, classObj, objectActor, grip, rawObj) {
   let {obj, hooks} = objectActor;
 
-  if (!obj.proto || obj.proto.class != className) {
-    return false;
-  }
-
   let v = null;
   try {
     v = classObj.prototype.valueOf.call(rawObj);
   } catch (ex) {
     // valueOf() can throw if the raw JS object is "misbehaved".
     return false;
   }
 
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_objectgrips-19.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+var gDebuggee;
+var gThreadClient;
+
+function run_test() {
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+}
+
+async function run_test_with_server(server, callback) {
+  initTestDebuggerServer(server);
+  gDebuggee = gDebuggee = addTestGlobal("test-grips", server);
+  gDebuggee.eval(function stopMe(arg1) {
+    debugger;
+  }.toString());
+  let client = new DebuggerClient(server.connectPipe());
+  await client.connect();
+  const [,, threadClient] = await attachTestTabAndResume(client, "test-grips");
+  gThreadClient = threadClient;
+  await test_wrapped_primitive_grips();
+  await client.close();
+  callback();
+}
+
+async function test_wrapped_primitive_grips() {
+  let tests = [{
+    value: true,
+    class: "Boolean"
+  }, {
+    value: 123,
+    class: "Number"
+  }, {
+    value: "foo",
+    class: "String"
+  }, {
+    value: Symbol("bar"),
+    class: "Symbol",
+    name: "bar"
+  }];
+  for (let data of tests) {
+    await new Promise(function (resolve) {
+      gThreadClient.addOneTimeListener("paused", async function (event, packet) {
+        let [grip] = packet.frame.arguments;
+        check_wrapped_primitive_grip(grip, data);
+
+        await gThreadClient.resume();
+        resolve();
+      });
+      gDebuggee.primitive = data.value;
+      gDebuggee.eval("stopMe(Object(primitive));");
+    });
+  }
+}
+
+function check_wrapped_primitive_grip(grip, data) {
+  strictEqual(grip.class, data.class, "The grip has the proper class.");
+
+  if (!grip.preview) {
+    // In a worker thread Cu does not exist, the objects are considered unsafe and
+    // can't be unwrapped, so there is no preview.
+    return;
+  }
+
+  let value = grip.preview.wrappedValue;
+  if (data.class === "Symbol") {
+    strictEqual(value.type, "symbol", "The wrapped value grip has symbol type.");
+    strictEqual(value.name, data.name, "The wrapped value grip has the proper name.");
+  } else {
+    strictEqual(value, data.value, "The wrapped value is the primitive one.");
+  }
+}
--- a/devtools/server/tests/unit/xpcshell.ini
+++ b/devtools/server/tests/unit/xpcshell.ini
@@ -169,16 +169,17 @@ reason = only ran on B2G
 [test_objectgrips-11.js]
 [test_objectgrips-12.js]
 [test_objectgrips-13.js]
 [test_objectgrips-14.js]
 [test_objectgrips-15.js]
 [test_objectgrips-16.js]
 [test_objectgrips-17.js]
 [test_objectgrips-18.js]
+[test_objectgrips-19.js]
 [test_promise_state-01.js]
 [test_promise_state-02.js]
 [test_promise_state-03.js]
 [test_interrupt.js]
 [test_stepping-01.js]
 [test_stepping-02.js]
 [test_stepping-03.js]
 [test_stepping-04.js]
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/crashtests/1401809.html
@@ -0,0 +1,14 @@
+<html>
+  <head>
+    <style></style>
+    <script>
+      o1 = document.createElement('t');
+      document.documentElement.appendChild(o1);
+      document.styleSheets[0].insertRule('* { will-change:an }', 0);
+      k = new KeyframeEffect(o1, [{'willChange':'s'}], {'':''});
+      k = null;
+      SpecialPowers.forceGC();
+      SpecialPowers.forceCC();
+    </script>
+  </head>
+</html>
--- a/dom/animation/test/crashtests/crashtests.list
+++ b/dom/animation/test/crashtests/crashtests.list
@@ -28,8 +28,9 @@ pref(dom.animations-api.core.enabled,tru
 pref(dom.animations-api.core.enabled,true) load 1334583-1.html
 pref(dom.animations-api.core.enabled,true) load 1335998-1.html
 pref(dom.animations-api.core.enabled,true) load 1343589-1.html
 pref(dom.animations-api.core.enabled,true) load 1359658-1.html
 pref(dom.animations-api.core.enabled,true) load 1373712-1.html
 pref(dom.animations-api.core.enabled,true) load 1379606-1.html
 pref(dom.animations-api.core.enabled,true) load 1393605-1.html
 load 1400022-1.html
+pref(dom.animations-api.core.enabled,true) load 1401809.html
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -80,24 +80,29 @@ already_AddRefed<nsContentList>
 NS_GetContentList(nsINode* aRootNode,
                   int32_t  aMatchNameSpaceId,
                   const nsAString& aTagname);
 
 #define ELEMENT_FLAG_BIT(n_) NODE_FLAG_BIT(NODE_TYPE_SPECIFIC_BITS_OFFSET + (n_))
 
 // Element-specific flags
 enum {
-  // These two bits are shared by Gecko's and Servo's restyle systems for
+  // These four bits are shared by Gecko's and Servo's restyle systems for
   // different purposes. They should not be accessed directly, and access to
   // them should be properly guarded by asserts.
   ELEMENT_SHARED_RESTYLE_BIT_1 = ELEMENT_FLAG_BIT(0),
   ELEMENT_SHARED_RESTYLE_BIT_2 = ELEMENT_FLAG_BIT(1),
   ELEMENT_SHARED_RESTYLE_BIT_3 = ELEMENT_FLAG_BIT(2),
   ELEMENT_SHARED_RESTYLE_BIT_4 = ELEMENT_FLAG_BIT(3),
 
+  ELEMENT_SHARED_RESTYLE_BITS = ELEMENT_SHARED_RESTYLE_BIT_1 |
+                                ELEMENT_SHARED_RESTYLE_BIT_2 |
+                                ELEMENT_SHARED_RESTYLE_BIT_3 |
+                                ELEMENT_SHARED_RESTYLE_BIT_4,
+
   // Whether this node has dirty descendants for Servo's style system.
   ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO = ELEMENT_SHARED_RESTYLE_BIT_1,
 
   // Whether this node has dirty descendants for animation-only restyle for
   // Servo's style system.
   ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO =
     ELEMENT_SHARED_RESTYLE_BIT_2,
 
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -1488,24 +1488,29 @@ Navigator::GetVRDisplays(ErrorResult& aR
   return p.forget();
 }
 
 void
 Navigator::GetActiveVRDisplays(nsTArray<RefPtr<VRDisplay>>& aDisplays) const
 {
   /**
    * Get only the active VR displays.
-   * Callers do not wish to VRDisplay::RefreshVRDisplays, as the enumeration may
-   * activate hardware that is not yet intended to be used.
+   * GetActiveVRDisplays should only enumerate displays that
+   * are already active without causing any other hardware to be
+   * activated.
+   * We must not call nsGlobalWindow::NotifyVREventListenerAdded here,
+   * as that would cause enumeration and activation of other VR hardware.
+   * Activating VR hardware is intrusive to the end user, as it may
+   * involve physically powering on devices that the user did not
+   * intend to use.
    */
   if (!mWindow || !mWindow->GetDocShell()) {
     return;
   }
   nsGlobalWindow* win = nsGlobalWindow::Cast(mWindow);
-  win->NotifyVREventListenerAdded();
   nsTArray<RefPtr<VRDisplay>> displays;
   if (win->UpdateVRDisplays(displays)) {
     for (auto display : displays) {
       if (display->IsPresenting()) {
         aDisplays.AppendElement(display);
       }
     }
   }
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -7787,16 +7787,39 @@ nsDOMAttributeMap::BlastSubtreeToPieces(
 
   uint32_t count = aNode->GetChildCount();
   for (uint32_t i = 0; i < count; ++i) {
     BlastSubtreeToPieces(aNode->GetFirstChild());
     aNode->RemoveChildAt(0, false);
   }
 }
 
+// Recursively check whether this node or its descendants contain any
+// pre-existing style declaration or any shared restyle flags.
+static bool
+NodeContainsAnyStyleData(nsINode* aNode)
+{
+  if (aNode->IsElement()) {
+    Element* elem = aNode->AsElement();
+    if (elem->GetInlineStyleDeclaration() ||
+        elem->GetSMILOverrideStyleDeclaration() ||
+        elem->HasAnyOfFlags(ELEMENT_SHARED_RESTYLE_BITS) ||
+        elem->HasServoData()) {
+      return true;
+    }
+  }
+  for (nsIContent* child = aNode->GetFirstChild();
+       child; child = child->GetNextSibling()) {
+    if (NodeContainsAnyStyleData(child)) {
+      return true;
+    }
+  }
+  return false;
+}
+
 nsINode*
 nsIDocument::AdoptNode(nsINode& aAdoptedNode, ErrorResult& rv)
 {
   nsINode* adoptedNode = &aAdoptedNode;
 
   // Scope firing mutation events so that we don't carry any state that
   // might be stale
   {
@@ -7902,16 +7925,22 @@ nsIDocument::AdoptNode(nsINode& aAdopted
   }
 
   nsCOMPtr<nsIDocument> oldDocument = adoptedNode->OwnerDoc();
   bool sameDocument = oldDocument == this;
 
   AutoJSContext cx;
   JS::Rooted<JSObject*> newScope(cx, nullptr);
   if (!sameDocument) {
+    if (MOZ_UNLIKELY(oldDocument->GetStyleBackendType() != GetStyleBackendType())) {
+      NS_WARNING("Adopting node across different style backend");
+      MOZ_RELEASE_ASSERT(!NodeContainsAnyStyleData(adoptedNode),
+                         "Must not adopt a node with pre-existing style data "
+                         "into a document with different style backend");
+    }
     newScope = GetWrapper();
     if (!newScope && GetScopeObject() && GetScopeObject()->GetGlobalJSObject()) {
       // Make sure cx is in a semi-sane compartment before we call WrapNative.
       // It's kind of irrelevant, given that we're passing aAllowWrapping =
       // false, and documents should always insist on being wrapped in an
       // canonical scope. But we try to pass something sane anyway.
       JSAutoCompartment ac(cx, GetScopeObject()->GetGlobalJSObject());
       JS::Rooted<JS::Value> v(cx);
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -7542,17 +7542,17 @@ nsGlobalWindow::MakeScriptDialogTitle(ns
         fixedURI->GetHost(host);
 
         if (!host.IsEmpty()) {
           // if this URI has a host we'll show it. For other
           // schemes (e.g. file:) we fall back to the localized
           // generic string
 
           nsAutoCString prepath;
-          fixedURI->GetPrePath(prepath);
+          fixedURI->GetDisplayPrePath(prepath);
 
           NS_ConvertUTF8toUTF16 ucsPrePath(prepath);
           const char16_t *formatStrings[] = { ucsPrePath.get() };
           nsContentUtils::FormatLocalizedString(nsContentUtils::eCOMMON_DIALOG_PROPERTIES,
                                                 "ScriptDlgHeading",
                                                 formatStrings,
                                                 aOutTitle);
         }
--- a/dom/webidl/StyleSheet.webidl
+++ b/dom/webidl/StyleSheet.webidl
@@ -34,9 +34,15 @@ interface StyleSheet {
   //
   // If the style sheet has the special "# sourceMappingURL=" comment,
   // then this is the URL specified there.
   //
   // If the source map URL is not found by either of these methods,
   // then this is an empty string.
   [ChromeOnly, Pure]
   readonly attribute DOMString sourceMapURL;
+  // The source URL for this style sheet.  If the style sheet has the
+  // special "# sourceURL=" comment, then this is the URL specified
+  // there.  If no such comment is found, then this is the empty
+  // string.
+  [ChromeOnly, Pure]
+  readonly attribute DOMString sourceURL;
 };
--- a/js/src/devtools/rootAnalysis/analyzeHeapWrites.js
+++ b/js/src/devtools/rootAnalysis/analyzeHeapWrites.js
@@ -232,19 +232,16 @@ function treatAsSafeArgument(entry, varN
         ["Gecko_nsStyleFont_PrefillDefaultForGeneric", "aFont", null],
         ["Gecko_nsStyleSVG_SetContextPropertiesLength", "aSvg", null],
         ["Gecko_ClearAlternateValues", "aFont", null],
         ["Gecko_AppendAlternateValues", "aFont", null],
         ["Gecko_CopyAlternateValuesFrom", "aDest", null],
         ["Gecko_CounterStyle_GetName", "aResult", null],
         ["Gecko_CounterStyle_GetSingleString", "aResult", null],
         ["Gecko_EnsureMozBorderColors", "aBorder", null],
-        ["Gecko_ClearMozBorderColors", "aBorder", null],
-        ["Gecko_AppendMozBorderColors", "aBorder", null],
-        ["Gecko_CopyMozBorderColors", "aDest", null],
     ];
     for (var [entryMatch, varMatch, csuMatch] of whitelist) {
         assert(entryMatch || varMatch || csuMatch);
         if (entryMatch && !nameMatches(entry.name, entryMatch))
             continue;
         if (varMatch && !nameMatches(varName, varMatch))
             continue;
         if (csuMatch && (!csuName || !nameMatches(csuName, csuMatch)))
@@ -465,19 +462,16 @@ function ignoreContents(entry)
 
         // Unable to analyze safety of linked list initialization.
         "Gecko_NewCSSValueSharedList",
         "Gecko_CSSValue_InitSharedList",
 
         // Unable to trace through dataflow, but straightforward if inspected.
         "Gecko_NewNoneTransform",
 
-        // Bug 1400438
-        "Gecko_AppendMozBorderColors",
-
         // Need main thread assertions or other fixes.
         /EffectCompositor::GetServoAnimationRule/,
         /LookAndFeel::GetColor/,
     ];
     if (entry.matches(whitelist))
         return true;
 
     if (entry.isSafeArgument(0)) {
--- a/layout/base/RestyleManager.cpp
+++ b/layout/base/RestyleManager.cpp
@@ -1563,16 +1563,28 @@ RestyleManager::ProcessRestyledFrames(ns
            !(frame->GetStateBits() & NS_STATE_IS_OUTER_SVG))) {
         SVGObserverUtils::InvalidateRenderingObservers(frame);
       }
       if (hint & nsChangeHint_NeedReflow) {
         StyleChangeReflow(frame, hint);
         didReflowThisFrame = true;
       }
 
+      // Here we need to propagate repaint frame change hint instead of update
+      // opacity layer change hint when we do opacity optimization for SVG.
+      // We can't do it in nsStyleEffects::CalcDifference() just like we do
+      // for the optimization for 0.99 over opacity values since we have no way
+      // to call nsSVGUtils::CanOptimizeOpacity() there.
+      if ((hint & nsChangeHint_UpdateOpacityLayer) &&
+          nsSVGUtils::CanOptimizeOpacity(frame) &&
+          frame->IsFrameOfType(nsIFrame::eSVGGeometry)) {
+        hint &= ~nsChangeHint_UpdateOpacityLayer;
+        hint |= nsChangeHint_RepaintFrame;
+      }
+
       if ((hint & nsChangeHint_UpdateUsesOpacity) &&
           frame->IsFrameOfType(nsIFrame::eTablePart)) {
         NS_ASSERTION(hint & nsChangeHint_UpdateOpacityLayer,
                      "should only return UpdateUsesOpacity hint "
                      "when also returning UpdateOpacityLayer hint");
         // When an internal table part (including cells) changes between
         // having opacity 1 and non-1, it changes whether its
         // backgrounds (and those of table parts inside of it) are
new file mode 100644
--- /dev/null
+++ b/layout/base/crashtests/1400438-1.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<style>
+div {
+  width: 100px; height: 100px;
+  border-left: 10px solid;
+  -moz-border-left-colors: red green;
+}
+</style>
+<div></div>
--- a/layout/base/crashtests/crashtests.list
+++ b/layout/base/crashtests/crashtests.list
@@ -496,9 +496,10 @@ load 1381323.html
 asserts-if(!stylo,1) load 1388625-1.html # bug 1389286
 load 1390389.html
 load 1395591-1.html
 load 1395715-1.html
 load 1397398-1.html
 load 1397398-2.html
 load 1397398-3.html
 load 1398500.html
+load 1400438-1.html
 load 1400599-1.html
--- a/layout/painting/nsCSSRendering.cpp
+++ b/layout/painting/nsCSSRendering.cpp
@@ -756,23 +756,21 @@ ConstructBorderRenderer(nsPresContext* a
   Float borderWidths[4] = { Float(border.top) / oneDevPixel,
                                    Float(border.right) / oneDevPixel,
                                    Float(border.bottom) / oneDevPixel,
                                    Float(border.left) / oneDevPixel };
   Rect dirtyRect = NSRectToRect(aDirtyRect, oneDevPixel);
 
   uint8_t borderStyles[4];
   nscolor borderColors[4];
-  nsBorderColors* compositeColors[4];
-
-  // pull out styles, colors, composite colors
+
+  // pull out styles, colors
   NS_FOR_CSS_SIDES (i) {
     borderStyles[i] = aStyleBorder.GetBorderStyle(i);
     borderColors[i] = ourColor->CalcComplexColor(aStyleBorder.mBorderColor[i]);
-    aStyleBorder.GetCompositeColors(i, &compositeColors[i]);
   }
 
   PrintAsFormatString(" borderStyles: %d %d %d %d\n", borderStyles[0], borderStyles[1], borderStyles[2], borderStyles[3]);
 
   nsIDocument* document = nullptr;
   nsIContent* content = aForFrame->GetContent();
   if (content) {
     document = content->OwnerDoc();
@@ -782,17 +780,17 @@ ConstructBorderRenderer(nsPresContext* a
                              document,
                              aDrawTarget,
                              dirtyRect,
                              joinedBorderAreaPx,
                              borderStyles,
                              borderWidths,
                              bgRadii,
                              borderColors,
-                             compositeColors,
+                             aStyleBorder.mBorderColors.get(),
                              bgColor,
                              !aForFrame->BackfaceIsHidden());
 }
 
 
 DrawResult
 nsCSSRendering::PaintBorderWithStyleBorder(nsPresContext* aPresContext,
                                            gfxContext& aRenderingContext,
--- a/layout/painting/nsCSSRenderingBorders.cpp
+++ b/layout/painting/nsCSSRenderingBorders.cpp
@@ -85,19 +85,16 @@ static Color MakeBorderColor(nscolor aCo
 // border going inwards) and an array of line styles, calculate the
 // color that that stripe of the border should be rendered in.
 static Color ComputeColorForLine(uint32_t aLineIndex,
                                    const BorderColorStyle* aBorderColorStyle,
                                    uint32_t aBorderColorStyleCount,
                                    nscolor aBorderColor,
                                    nscolor aBackgroundColor);
 
-static Color ComputeCompositeColorForLine(uint32_t aLineIndex,
-                                          const nsBorderColors* aBorderColors);
-
 // little helper function to check if the array of 4 floats given are
 // equal to the given value
 static bool
 CheckFourFloatsEqual(const Float *vals, Float k)
 {
   return (vals[0] == k &&
           vals[1] == k &&
           vals[2] == k &&
@@ -173,36 +170,37 @@ nsCSSBorderRenderer::nsCSSBorderRenderer
                                          const nsIDocument* aDocument,
                                          DrawTarget* aDrawTarget,
                                          const Rect& aDirtyRect,
                                          Rect& aOuterRect,
                                          const uint8_t* aBorderStyles,
                                          const Float* aBorderWidths,
                                          RectCornerRadii& aBorderRadii,
                                          const nscolor* aBorderColors,
-                                         nsBorderColors* const* aCompositeColors,
+                                         const nsBorderColors* aCompositeColors,
                                          nscolor aBackgroundColor,
                                          bool aBackfaceIsVisible)
   : mPresContext(aPresContext),
     mDocument(aDocument),
     mDrawTarget(aDrawTarget),
     mDirtyRect(aDirtyRect),
     mOuterRect(aOuterRect),
     mBorderRadii(aBorderRadii),
     mBackgroundColor(aBackgroundColor),
     mBackfaceIsVisible(aBackfaceIsVisible)
 {
   PodCopy(mBorderStyles, aBorderStyles, 4);
   PodCopy(mBorderWidths, aBorderWidths, 4);
   PodCopy(mBorderColors, aBorderColors, 4);
-  if (aCompositeColors) {
-    PodCopy(mCompositeColors, aCompositeColors, 4);
-  } else {
-    static nsBorderColors * const noColors[4] = { nullptr };
-    PodCopy(mCompositeColors, noColors, 4);
+  NS_FOR_CSS_SIDES(side) {
+    if (aCompositeColors && !(*aCompositeColors)[side].IsEmpty()) {
+      mCompositeColors[side] = &(*aCompositeColors)[side];
+    } else {
+      mCompositeColors[side] = nullptr;
+    }
   }
 
   mInnerRect = mOuterRect;
   mInnerRect.Deflate(
       Margin(mBorderStyles[0] != NS_STYLE_BORDER_STYLE_NONE ? mBorderWidths[0] : 0,
              mBorderStyles[1] != NS_STYLE_BORDER_STYLE_NONE ? mBorderWidths[1] : 0,
              mBorderStyles[2] != NS_STYLE_BORDER_STYLE_NONE ? mBorderWidths[2] : 0,
              mBorderStyles[3] != NS_STYLE_BORDER_STYLE_NONE ? mBorderWidths[3] : 0));
@@ -316,19 +314,21 @@ nsCSSBorderRenderer::AreBorderSideFinalS
     }
 
     if (((1 << i) & aSides) == 0) {
       continue;
     }
 
     if (mBorderStyles[firstStyle] != mBorderStyles[i] ||
         mBorderColors[firstStyle] != mBorderColors[i] ||
-        !nsBorderColors::Equal(mCompositeColors[firstStyle],
-                               mCompositeColors[i]))
+        !mCompositeColors[firstStyle] != !mCompositeColors[i] ||
+        (mCompositeColors[firstStyle] &&
+         *mCompositeColors[firstStyle] != *mCompositeColors[i])) {
       return false;
+    }
   }
 
   /* Then if it's one of the two-tone styles and we're not
    * just comparing the TL or BR sides */
   switch (mBorderStyles[firstStyle]) {
     case NS_STYLE_BORDER_STYLE_GROOVE:
     case NS_STYLE_BORDER_STYLE_RIDGE:
     case NS_STYLE_BORDER_STYLE_INSET:
@@ -1262,46 +1262,42 @@ ComputeColorForLine(uint32_t aLineIndex,
                     nscolor aBackgroundColor)
 {
   NS_ASSERTION(aLineIndex < aBorderColorStyleCount, "Invalid lineIndex given");
 
   return MakeBorderColor(aBorderColor, aBackgroundColor,
                          aBorderColorStyle[aLineIndex]);
 }
 
-Color
-ComputeCompositeColorForLine(uint32_t aLineIndex,
-                             const nsBorderColors* aBorderColors)
-{
-  while (aLineIndex-- && aBorderColors->mNext)
-    aBorderColors = aBorderColors->mNext;
-
-  return Color::FromABGR(aBorderColors->mColor);
-}
-
 void
-nsCSSBorderRenderer::DrawBorderSidesCompositeColors(int aSides, const nsBorderColors *aCompositeColors)
+nsCSSBorderRenderer::DrawBorderSidesCompositeColors(
+  int aSides, const nsTArray<nscolor>& aCompositeColors)
 {
   RectCornerRadii radii = mBorderRadii;
 
   // the generic composite colors path; each border is 1px in size
   Rect soRect = mOuterRect;
   Float maxBorderWidth = 0;
   NS_FOR_CSS_SIDES (i) {
     maxBorderWidth = std::max(maxBorderWidth, Float(mBorderWidths[i]));
   }
 
   Float fakeBorderSizes[4];
 
   Point itl = mInnerRect.TopLeft();
   Point ibr = mInnerRect.BottomRight();
 
+  MOZ_ASSERT(!aCompositeColors.IsEmpty());
+  Color compositeColor;
   for (uint32_t i = 0; i < uint32_t(maxBorderWidth); i++) {
-    ColorPattern color(ToDeviceColor(
-                         ComputeCompositeColorForLine(i, aCompositeColors)));
+    // advance to next color if exists.
+    if (i < aCompositeColors.Length()) {
+      compositeColor = ToDeviceColor(Color::FromABGR(aCompositeColors[i]));
+    }
+    ColorPattern color(compositeColor);
 
     Rect siRect = soRect;
     siRect.Deflate(1.0);
 
     // now cap the rects to the real mInnerRect
     Point tl = siRect.TopLeft();
     Point br = siRect.BottomRight();
 
@@ -1331,17 +1327,17 @@ nsCSSBorderRenderer::DrawBorderSides(int
 {
   if (aSides == 0 || (aSides & ~eSideBitsAll) != 0) {
     NS_WARNING("DrawBorderSides: invalid sides!");
     return;
   }
 
   uint8_t borderRenderStyle = NS_STYLE_BORDER_STYLE_NONE;
   nscolor borderRenderColor;
-  const nsBorderColors *compositeColors = nullptr;
+  const nsTArray<nscolor>* compositeColors = nullptr;
 
   uint32_t borderColorStyleCount = 0;
   BorderColorStyle borderColorStyleTopLeft[3], borderColorStyleBottomRight[3];
   BorderColorStyle *borderColorStyle = nullptr;
 
   NS_FOR_CSS_SIDES (i) {
     if ((aSides & (1 << i)) == 0)
       continue;
@@ -1389,17 +1385,17 @@ nsCSSBorderRenderer::DrawBorderSides(int
   // color is used for the remainder of the border's size.  Just
   // hand off to another function to do all that.
   if (compositeColors) {
     Float maxBorderWidth(0);
     NS_FOR_CSS_SIDES (i) {
       maxBorderWidth = std::max(maxBorderWidth, mBorderWidths[i]);
     }
     if (maxBorderWidth <= MAX_COMPOSITE_BORDER_WIDTH) {
-      DrawBorderSidesCompositeColors(aSides, compositeColors);
+      DrawBorderSidesCompositeColors(aSides, *compositeColors);
       return;
     }
     NS_WARNING("DrawBorderSides: too large border width for composite colors");
  }
 
   // We're not doing compositeColors, so we can calculate the
   // borderColorStyle based on the specified style.  The
   // borderColorStyle array goes from the outer to the inner style.
@@ -3142,80 +3138,73 @@ nsCSSBorderRenderer::DrawNoCompositeColo
                  firstColor, secondColor, skirtSize, skirtSlope);
     }
   }
 }
 
 void
 nsCSSBorderRenderer::DrawRectangularCompositeColors()
 {
-  nsBorderColors *currentColors[4];
-  memcpy(currentColors, mCompositeColors, sizeof(nsBorderColors*) * 4);
+  nscolor currentColors[4];
+  NS_FOR_CSS_SIDES(side) {
+    currentColors[side] = mBorderColors[side];
+  }
   Rect rect = mOuterRect;
   rect.Deflate(0.5);
 
   const twoFloats cornerAdjusts[4] = { { +0.5,  0   },
                                         {    0, +0.5 },
                                         { -0.5,  0   },
                                         {    0, -0.5 } };
 
   for (int i = 0; i < mBorderWidths[0]; i++) {
     NS_FOR_CSS_SIDES(side) {
+      // advance to the next composite color if one exists
+      if (mCompositeColors[side] &&
+          uint32_t(i) < mCompositeColors[side]->Length()) {
+        currentColors[side] = (*mCompositeColors[side])[i];
+      }
+    }
+    NS_FOR_CSS_SIDES(side) {
       int sideNext = (side + 1) % 4;
 
       Point firstCorner = rect.CCWCorner(side) + cornerAdjusts[side];
       Point secondCorner = rect.CWCorner(side) - cornerAdjusts[side];
 
-      Color currentColor = Color::FromABGR(
-        currentColors[side] ? currentColors[side]->mColor
-                            : mBorderColors[side]);
+      Color currentColor = Color::FromABGR(currentColors[side]);
 
       mDrawTarget->StrokeLine(firstCorner, secondCorner,
                               ColorPattern(ToDeviceColor(currentColor)));
 
       Point cornerTopLeft = rect.CWCorner(side) - Point(0.5, 0.5);
-      Color nextColor = Color::FromABGR(
-        currentColors[sideNext] ? currentColors[sideNext]->mColor
-                                : mBorderColors[sideNext]);
+      Color nextColor = Color::FromABGR(currentColors[sideNext]);
 
       Color cornerColor((currentColor.r + nextColor.r) / 2.f,
                         (currentColor.g + nextColor.g) / 2.f,
                         (currentColor.b + nextColor.b) / 2.f,
                         (currentColor.a + nextColor.a) / 2.f);
       mDrawTarget->FillRect(Rect(cornerTopLeft, Size(1, 1)),
                             ColorPattern(ToDeviceColor(cornerColor)));
-
-      if (side != 0) {
-        // We'll have to keep side 0 for the color averaging on side 3.
-        if (currentColors[side] && currentColors[side]->mNext) {
-          currentColors[side] = currentColors[side]->mNext;
-        }
-      }
-    }
-    // Now advance the color for side 0.
-    if (currentColors[0] && currentColors[0]->mNext) {
-      currentColors[0] = currentColors[0]->mNext;
     }
     rect.Deflate(1);
   }
 }
 
 void
 nsCSSBorderRenderer::DrawBorders()
 {
   bool forceSeparateCorners = false;
 
   if (mAllBordersSameStyle &&
       ((mCompositeColors[0] == nullptr &&
        (mBorderStyles[0] == NS_STYLE_BORDER_STYLE_NONE ||
         mBorderStyles[0] == NS_STYLE_BORDER_STYLE_HIDDEN ||
         mBorderColors[0] == NS_RGBA(0,0,0,0))) ||
-       (mCompositeColors[0] &&
-        (mCompositeColors[0]->mColor == NS_RGBA(0,0,0,0) &&
-         !mCompositeColors[0]->mNext))))
+       (mCompositeColors[0] && mCompositeColors[0]->Length() == 1 &&
+        (*mCompositeColors[0])[0] == NS_RGBA(0,0,0,0))))
   {
     // All borders are the same style, and the style is either none or hidden, or the color
     // is transparent.
     // This also checks if the first composite color is transparent, and there are
     // no others. It doesn't check if there are subsequent transparent ones, because
     // that would be very silly.
     return;
   }
--- a/layout/painting/nsCSSRenderingBorders.h
+++ b/layout/painting/nsCSSRenderingBorders.h
@@ -95,17 +95,17 @@ public:
                       const nsIDocument* aDocument,
                       DrawTarget* aDrawTarget,
                       const Rect& aDirtyRect,
                       Rect& aOuterRect,
                       const uint8_t* aBorderStyles,
                       const Float* aBorderWidths,
                       RectCornerRadii& aBorderRadii,
                       const nscolor* aBorderColors,
-                      nsBorderColors* const* aCompositeColors,
+                      const nsBorderColors* aCompositeColors,
                       nscolor aBackgroundColor,
                       bool aBackfaceIsVisible);
 
   // draw the entire border
   void DrawBorders();
 
   bool CanCreateWebRenderCommands();
   void CreateWebRenderCommands(mozilla::wr::DisplayListBuilder& aBuilder,
@@ -147,17 +147,19 @@ private:
   uint8_t mBorderStyles[4];
   Float mBorderWidths[4];
   RectCornerRadii mBorderRadii;
 
   // the colors for 'border-top-color' et. al.
   nscolor mBorderColors[4];
 
   // the lists of colors for '-moz-border-top-colors' et. al.
-  nsBorderColors* mCompositeColors[4];
+  // the pointers here are either nullptr, or referring to a non-empty
+  // nsTArray, so no additional empty check is needed.
+  const nsTArray<nscolor>* mCompositeColors[4];
 
   // the background color
   nscolor mBackgroundColor;
 
   // calculated values
   bool mAllBordersSameStyle;
   bool mAllBordersSameWidth;
   bool mOneUnitBorder;
@@ -232,17 +234,18 @@ private:
   // core rendering
   //
 
   // draw the border for the given sides, using the style of the first side
   // present in the bitmask
   void DrawBorderSides (int aSides);
 
   // function used by the above to handle -moz-border-colors
-  void DrawBorderSidesCompositeColors(int aSides, const nsBorderColors *compositeColors);
+  void DrawBorderSidesCompositeColors(
+    int aSides, const nsTArray<nscolor>& compositeColors);
 
   // Setup the stroke options for the given dashed/dotted side
   void SetupDashedOptions(StrokeOptions* aStrokeOptions,
                           Float aDash[2], mozilla::Side aSide,
                           Float aBorderLength, bool isCorner);
 
   // Draw the given dashed/dotte side
   void DrawDashedOrDottedSide(mozilla::Side aSide);
--- a/layout/style/ServoBindingList.h
+++ b/layout/style/ServoBindingList.h
@@ -63,16 +63,18 @@ SERVO_BINDING_FUNC(Servo_StyleSheet_Clon
                    RawServoStyleSheetContentsBorrowed sheet,
                    const mozilla::ServoStyleSheet* reference_sheet);
 SERVO_BINDING_FUNC(Servo_StyleSheet_SizeOfIncludingThis, size_t,
                    mozilla::MallocSizeOf malloc_size_of,
                    mozilla::MallocSizeOf malloc_enclosing_size_of,
                    RawServoStyleSheetContentsBorrowed sheet)
 SERVO_BINDING_FUNC(Servo_StyleSheet_GetSourceMapURL, void,
                    RawServoStyleSheetContentsBorrowed sheet, nsAString* result)
+SERVO_BINDING_FUNC(Servo_StyleSheet_GetSourceURL, void,
+                   RawServoStyleSheetContentsBorrowed sheet, nsAString* result)
 // We'd like to return `OriginFlags` here, but bindgen bitfield enums don't
 // work as return values with the Linux 32-bit ABI at the moment because
 // they wrap the value in a struct.
 SERVO_BINDING_FUNC(Servo_StyleSheet_GetOrigin, uint8_t,
                    RawServoStyleSheetContentsBorrowed sheet)
 SERVO_BINDING_FUNC(Servo_StyleSet_Init, RawServoStyleSet*, RawGeckoPresContextOwned pres_context)
 SERVO_BINDING_FUNC(Servo_StyleSet_RebuildCachedData, void,
                    RawServoStyleSetBorrowed set)
@@ -542,16 +544,18 @@ SERVO_BINDING_FUNC(Servo_ComputedValues_
 // to RawServoStyleRule.
 SERVO_BINDING_FUNC(Servo_ComputedValues_GetStyleRuleList, void,
                    ServoStyleContextBorrowed values,
                    RawGeckoServoStyleRuleListBorrowedMut rules)
 
 // Initialize Servo components. Should be called exactly once at startup.
 SERVO_BINDING_FUNC(Servo_Initialize, void,
                    RawGeckoURLExtraData* dummy_url_data)
+// Initialize Servo on a cooperative Quantum DOM thread.
+SERVO_BINDING_FUNC(Servo_InitializeCooperativeThread, void);
 // Shut down Servo components. Should be called exactly once at shutdown.
 SERVO_BINDING_FUNC(Servo_Shutdown, void)
 
 // Restyle and change hints.
 SERVO_BINDING_FUNC(Servo_NoteExplicitHints, void, RawGeckoElementBorrowed element,
                    nsRestyleHint restyle_hint, nsChangeHint change_hint)
 // We'd like to return `nsChangeHint` here, but bindgen bitfield enums don't
 // work as return values with the Linux 32-bit ABI at the moment because
--- a/layout/style/ServoBindings.cpp
+++ b/layout/style/ServoBindings.cpp
@@ -1251,44 +1251,16 @@ Gecko_AtomEqualsUTF8IgnoreCase(nsIAtom* 
 }
 
 void
 Gecko_EnsureMozBorderColors(nsStyleBorder* aBorder)
 {
   aBorder->EnsureBorderColors();
 }
 
-void Gecko_ClearMozBorderColors(nsStyleBorder* aBorder, mozilla::Side aSide)
-{
-  aBorder->ClearBorderColors(aSide);
-}
-
-void
-Gecko_AppendMozBorderColors(nsStyleBorder* aBorder, mozilla::Side aSide,
-                            nscolor aColor)
-{
-  aBorder->AppendBorderColor(aSide, aColor);
-}
-
-void
-Gecko_CopyMozBorderColors(nsStyleBorder* aDest, const nsStyleBorder* aSrc,
-                          mozilla::Side aSide)
-{
-  if (aSrc->mBorderColors) {
-    aDest->CopyBorderColorsFrom(aSrc->mBorderColors[aSide], aSide);
-  }
-}
-
-const nsBorderColors*
-Gecko_GetMozBorderColors(const nsStyleBorder* aBorder, mozilla::Side aSide)
-{
-  MOZ_ASSERT(aBorder);
-  return aBorder->mBorderColors ? aBorder->mBorderColors[aSide] : nullptr;
-}
-
 void
 Gecko_FontFamilyList_Clear(FontFamilyList* aList) {
   aList->Clear();
 }
 
 void
 Gecko_FontFamilyList_AppendNamed(FontFamilyList* aList, nsIAtom* aName, bool aQuoted)
 {
--- a/layout/style/ServoBindings.h
+++ b/layout/style/ServoBindings.h
@@ -277,23 +277,16 @@ nsIAtom* Gecko_Atomize16(const nsAString
 void Gecko_AddRefAtom(nsIAtom* aAtom);
 void Gecko_ReleaseAtom(nsIAtom* aAtom);
 const uint16_t* Gecko_GetAtomAsUTF16(nsIAtom* aAtom, uint32_t* aLength);
 bool Gecko_AtomEqualsUTF8(nsIAtom* aAtom, const char* aString, uint32_t aLength);
 bool Gecko_AtomEqualsUTF8IgnoreCase(nsIAtom* aAtom, const char* aString, uint32_t aLength);
 
 // Border style
 void Gecko_EnsureMozBorderColors(nsStyleBorder* aBorder);
-void Gecko_ClearMozBorderColors(nsStyleBorder* aBorder, mozilla::Side aSide);
-void Gecko_AppendMozBorderColors(nsStyleBorder* aBorder, mozilla::Side aSide,
-                                 nscolor aColor);
-void Gecko_CopyMozBorderColors(nsStyleBorder* aDest, const nsStyleBorder* aSrc,
-                               mozilla::Side aSide);
-const nsBorderColors* Gecko_GetMozBorderColors(const nsStyleBorder* aBorder,
-                                               mozilla::Side aSide);
 
 // Font style
 void Gecko_FontFamilyList_Clear(FontFamilyList* aList);
 void Gecko_FontFamilyList_AppendNamed(FontFamilyList* aList, nsIAtom* aName, bool aQuoted);
 void Gecko_FontFamilyList_AppendGeneric(FontFamilyList* list, FontFamilyType familyType);
 void Gecko_CopyFontFamilyFrom(nsFont* dst, const nsFont* src);
 // will not run destructors on dst, give it uninitialized memory
 // font_id is LookAndFeel::FontID
--- a/layout/style/ServoBindings.toml
+++ b/layout/style/ServoBindings.toml
@@ -185,17 +185,16 @@ whitelist-types = [
     "gfxFontVariation",
     "GridNamedArea",
     "HalfCorner",
     "Image",
     "ImageURL",
     "Keyframe",
     "nsAttrName",
     "nsAttrValue",
-    "nsBorderColors",
     "nscolor",
     "nsChangeHint",
     "nsCSSCounterDesc",
     "nsCSSCounterStyleRule",
     "nsCSSFontFaceRule",
     "nsCSSKeyword",
     "nsCSSPropertyID",
     "nsCSSPropertyIDSet",
@@ -384,17 +383,16 @@ structs-types = [
     "mozilla::css::GridTemplateAreasValue",
     "mozilla::css::ErrorReporter",
     "mozilla::css::ImageValue",
     "mozilla::css::URLValue",
     "mozilla::css::URLValueData",
     "mozilla::AnonymousCounterStyle",
     "mozilla::MallocSizeOf",
     "mozilla::OriginFlags",
-    "mozilla::Side",
     "mozilla::UniquePtr",
     "ServoRawOffsetArc",
     "nsIContent",
     "nsIDocument",
     "nsIDocument_DocumentTheme",
     "RawGeckoAnimationPropertySegment",
     "RawGeckoComputedTiming",
     "RawGeckoCSSPropertyIDList",
@@ -438,17 +436,16 @@ structs-types = [
     "ServoElementSnapshotTable",
     "ServoStyleSetSizes",
     "SheetParsingMode",
     "StyleBasicShape",
     "StyleBasicShapeType",
     "StyleShapeSource",
     "StyleTransition",
     "gfxFontFeatureValueSet",
-    "nsBorderColors",
     "nsCSSCounterStyleRule",
     "nsCSSFontFaceRule",
     "nsCSSKeyword",
     "nsCSSPropertyID",
     "nsCSSPropertyIDSet",
     "nsCSSShadowArray",
     "nsCSSUnit",
     "nsCSSValue",
--- a/layout/style/ServoStyleSheet.cpp
+++ b/layout/style/ServoStyleSheet.cpp
@@ -220,16 +220,20 @@ ServoStyleSheet::ParseSheet(css::Loader*
                                                       aCompatMode,
                                                       aReusableSheets)
                          .Consume();
 
   nsString sourceMapURL;
   Servo_StyleSheet_GetSourceMapURL(Inner()->mContents, &sourceMapURL);
   SetSourceMapURLFromComment(sourceMapURL);
 
+  nsString sourceURL;
+  Servo_StyleSheet_GetSourceURL(Inner()->mContents, &sourceURL);
+  SetSourceURL(sourceURL);
+
   Inner()->mURLData = extraData.forget();
   return NS_OK;
 }
 
 nsresult
 ServoStyleSheet::ReparseSheet(const nsAString& aInput)
 {
   if (!mInner->mComplete) {
--- a/layout/style/StyleSheet.cpp
+++ b/layout/style/StyleSheet.cpp
@@ -256,16 +256,17 @@ StyleSheetInfo::StyleSheetInfo(StyleShee
   , mCORSMode(aCopy.mCORSMode)
   , mReferrerPolicy(aCopy.mReferrerPolicy)
   , mIntegrity(aCopy.mIntegrity)
   , mComplete(aCopy.mComplete)
   , mFirstChild()  // We don't rebuild the child because we're making a copy
                    // without children.
   , mSourceMapURL(aCopy.mSourceMapURL)
   , mSourceMapURLFromComment(aCopy.mSourceMapURLFromComment)
+  , mSourceURL(aCopy.mSourceURL)
 #ifdef DEBUG
   , mPrincipalSet(aCopy.mPrincipalSet)
 #endif
 {
   AddSheet(aPrimarySheet);
 }
 
 StyleSheetInfo::~StyleSheetInfo()
@@ -525,16 +526,28 @@ StyleSheet::SetSourceMapURL(const nsAStr
 }
 
 void
 StyleSheet::SetSourceMapURLFromComment(const nsAString& aSourceMapURLFromComment)
 {
   mInner->mSourceMapURLFromComment = aSourceMapURLFromComment;
 }
 
+void
+StyleSheet::GetSourceURL(nsAString& aSourceURL)
+{
+  aSourceURL = mInner->mSourceURL;
+}
+
+void
+StyleSheet::SetSourceURL(const nsAString& aSourceURL)
+{
+  mInner->mSourceURL = aSourceURL;
+}
+
 css::Rule*
 StyleSheet::GetDOMOwnerRule() const
 {
   return mOwnerRule;
 }
 
 uint32_t
 StyleSheet::InsertRule(const nsAString& aRule, uint32_t aIndex,
--- a/layout/style/StyleSheet.h
+++ b/layout/style/StyleSheet.h
@@ -211,16 +211,18 @@ public:
   inline StyleSheet* GetParentStyleSheet() const;
   // The XPCOM GetTitle is fine for WebIDL.
   dom::MediaList* Media();
   bool Disabled() const { return mDisabled; }
   // The XPCOM SetDisabled is fine for WebIDL.
   void GetSourceMapURL(nsAString& aTitle);
   void SetSourceMapURL(const nsAString& aSourceMapURL);
   void SetSourceMapURLFromComment(const nsAString& aSourceMapURLFromComment);
+  void GetSourceURL(nsAString& aSourceURL);
+  void SetSourceURL(const nsAString& aSourceURL);
 
   // WebIDL CSSStyleSheet API
   // Can't be inline because we can't include ImportRule here.  And can't be
   // called GetOwnerRule because that would be ambiguous with the ImportRule
   // version.
   css::Rule* GetDOMOwnerRule() const;
   dom::CSSRuleList* GetCssRules(nsIPrincipal& aSubjectPrincipal,
                                 ErrorResult& aRv);
--- a/layout/style/StyleSheetInfo.h
+++ b/layout/style/StyleSheetInfo.h
@@ -65,16 +65,19 @@ struct StyleSheetInfo
   // the value.  If both are seen, SourceMap is preferred.  If neither
   // is seen, this will be an empty string.
   nsString mSourceMapURL;
   // This stores any source map URL that might have been seen in a
   // comment in the style sheet.  This is separate from mSourceMapURL
   // so that the value does not overwrite any value that might have
   // come from a response header.
   nsString mSourceMapURLFromComment;
+  // This stores any source URL that might have been seen in a comment
+  // in the style sheet.
+  nsString mSourceURL;
 
 #ifdef DEBUG
   bool                   mPrincipalSet;
 #endif
 };
 
 } // namespace mozilla
 
new file mode 100644
--- /dev/null
+++ b/layout/style/crashtests/1400035.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html id="landing-page-home">
+<style>
+@keyframes opacity {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+#circle {
+  animation: opacity 1s infinite;
+}
+</style>
+<svg width="100" height="100">
+  <mask id="mask">
+    <circle id="circle" cx="50" cy="50" r="50" fill="red"/>
+  </mask>
+  <rect class="rect" x="0" y="0" width="100" height="100" fill="blue" mask="url(#mask)"/>
+</svg>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/style/crashtests/1400926.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<style>
+@keyframes anim {
+  0% { background-color: black; }
+  100% { background-color: yellow; }
+}
+</style>
+<script>
+document.styleSheets[0].cssRules[0].cssRules[0].style.setProperty('background-color', 'red', 'important');
+</script>
--- a/layout/style/crashtests/crashtests.list
+++ b/layout/style/crashtests/crashtests.list
@@ -213,10 +213,12 @@ load 1384232.html
 load 1395725.html
 load 1396041.html
 load 1397363-1.html
 load 1397439-1.html
 load 1395719.html
 load 1397091.html
 load 1398479.html
 load 1398581.html
+load 1400035.html
 load 1399546.html
 load 1400325.html
+load 1400926.html
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -1643,16 +1643,17 @@ CSSParserImpl::ParseSheet(const nsAStrin
     }
     UngetToken();
     if (ParseRuleSet(AppendRuleToSheet, this)) {
       mSection = eCSSSection_General;
     }
   }
 
   mSheet->SetSourceMapURLFromComment(scanner.GetSourceMapURL());
+  mSheet->SetSourceURL(scanner.GetSourceURL());
   ReleaseScanner();
 
   mParsingMode = css::eAuthorSheetFeatures;
   mIsChrome = false;
   mReusableSheets = nullptr;
 
   return NS_OK;
 }
--- a/layout/style/nsCSSScanner.cpp
+++ b/layout/style/nsCSSScanner.cpp
@@ -538,92 +538,99 @@ nsCSSScanner::SkipWhitespace()
       AdvanceLine();
     } else {
       Advance();
     }
   }
 }
 
 /**
+ * If the given text appears at the current offset in the buffer,
+ * advance over the text and return true.  Otherwise, return false.
+ * mLength is the number of characters in mDirective.
+ */
+bool
+nsCSSScanner::CheckCommentDirective(const nsAString& aDirective)
+{
+  nsDependentSubstring text(&mBuffer[mOffset], &mBuffer[mCount]);
+
+  if (StringBeginsWith(text, aDirective)) {
+    Advance(aDirective.Length());
+    return true;
+  }
+  return false;
+}
+
+/**
  * Skip over one CSS comment starting at the current read position.
  */
 void
 nsCSSScanner::SkipComment()
 {
-  static const char sourceMappingURLDirective[] = "# sourceMappingURL=";
+  // Note that these do not start with "#" or "@" -- that is handled
+  // separately, below.
+  static NS_NAMED_LITERAL_STRING(kSourceMappingURLDirective, " sourceMappingURL=");
+  static NS_NAMED_LITERAL_STRING(kSourceURLDirective, " sourceURL=");
 
   MOZ_ASSERT(Peek() == '/' && Peek(1) == '*', "should not have been called");
   Advance(2);
-  // Look in each comment for a source map directive; using a simple
-  // state machine.  The states are:
-  // * sourceMapIndex >= 0 means that we're still looking for the
-  //   directive and expect the next character to be at that index of
-  //   sourceMappingURLDirective.
-  //   As a special case, when sourceMapIndex == 0, '@' is also recognized.
-  // * sourceMapIndex < 0 means that we don't need to look for the
-  //   directive any more -- whether it was found or not.
-  // * copying == true means that the directive was found and we're
-  //   copying characters into mSourceMapURL.  This stops at the first
-  //   whitespace, or at the end of the comment.
-  int sourceMapIndex = 0;
-  bool copying = false;
+
+  // If we saw one of the directives, this will be non-NULL and will
+  // point to the string into which the URL will be written.
+  nsString* directive = nullptr;
+  if (Peek() == '#' || Peek() == '@') {
+    // Check for the comment directives.
+    Advance();
+    if (CheckCommentDirective(kSourceMappingURLDirective)) {
+      mSourceMapURL.Truncate();
+      directive = &mSourceMapURL;
+    } else if (CheckCommentDirective(kSourceURLDirective)) {
+      mSourceURL.Truncate();
+      directive = &mSourceURL;
+    }
+  }
+
   for (;;) {
     int32_t ch = Peek();
     if (ch < 0) {
       if (mReporter)
         mReporter->ReportUnexpectedEOF("PECommentEOF");
       SetEOFCharacters(eEOFCharacters_Asterisk | eEOFCharacters_Slash);
       return;
     }
-    if (sourceMapIndex >= 0) {
-      if ((sourceMapIndex == 0 && ch == '@') || ch == sourceMappingURLDirective[sourceMapIndex]) {
-        ++sourceMapIndex;
-        if (sourceMappingURLDirective[sourceMapIndex] == '\0') {
-          sourceMapIndex = -1;
-          mSourceMapURL.Truncate();
-          copying = true;
-          Advance();
-          // Make sure we don't copy out the '=' by falling through.
-          continue;
-        }
-      } else {
-        // Did not see the directive.
-        sourceMapIndex = -1;
-      }
-    }
 
     if (ch == '*') {
       Advance();
       ch = Peek();
       if (ch < 0) {
         // In this case, even if we saw a source map directive, leave
         // the "*" out of it.
         if (mReporter)
           mReporter->ReportUnexpectedEOF("PECommentEOF");
         SetEOFCharacters(eEOFCharacters_Slash);
         return;
       }
       if (ch == '/') {
         Advance();
         return;
       }
-      if (copying) {
-        mSourceMapURL.Append('*');
+      if (directive != nullptr) {
+        directive->Append('*');
       }
     } else if (IsVertSpace(ch)) {
       AdvanceLine();
       // Done with the directive, so stop copying.
-      copying = false;
+      directive = nullptr;
     } else if (IsWhitespace(ch)) {
       Advance();
       // Done with the directive, so stop copying.
-      copying = false;
+      directive = nullptr;
     } else {
-      if (copying) {
-        mSourceMapURL.Append(ch);
+      if (directive != nullptr) {
+        directive->Append(ch);
       }
       Advance();
     }
   }
 }
 
 /**
  * If there is a valid escape sequence starting at the current read
--- a/layout/style/nsCSSScanner.h
+++ b/layout/style/nsCSSScanner.h
@@ -230,16 +230,19 @@ class nsCSSScanner {
   { return mTokenOffset; }
 
   uint32_t GetTokenEndOffset() const
   { return mOffset; }
 
   const nsAString& GetSourceMapURL() const
   { return mSourceMapURL; }
 
+  const nsAString& GetSourceURL() const
+  { return mSourceURL; }
+
   // Get the text of the line containing the first character of
   // the most recently processed token.
   nsDependentSubstring GetCurrentLine() const;
 
   // Get the next token.  Return false on EOF.  aTokenResult is filled
   // in with the data for the token.  aSkip controls whether
   // whitespace and/or comment tokens are ever returned.
   bool Next(nsCSSToken& aTokenResult, nsCSSScannerExclude aSkip);
@@ -325,16 +328,17 @@ class nsCSSScanner {
 #endif
 
 protected:
   int32_t Peek(uint32_t n = 0);
   void Advance(uint32_t n = 1);
   void AdvanceLine();
 
   void SkipWhitespace();
+  bool CheckCommentDirective(const nsAString& aDirective);
   void SkipComment();
 
   bool GatherEscape(nsString& aOutput, bool aInString);
   bool GatherText(uint8_t aClass, nsString& aIdent);
 
   bool ScanIdent(nsCSSToken& aResult);
   bool ScanAtKeyword(nsCSSToken& aResult);
   bool ScanHash(nsCSSToken& aResult);
@@ -361,16 +365,17 @@ protected:
 
   mozilla::css::ErrorReporter *mReporter;
 
   bool mRecording;
   bool mSeenBadToken;
   bool mSeenVariableReference;
 
   nsString mSourceMapURL;
+  nsString mSourceURL;
 };
 
 // Token for the grid-template-areas micro-syntax
 // http://dev.w3.org/csswg/css-grid/#propdef-grid-template-areas
 struct MOZ_STACK_CLASS nsCSSGridTemplateAreaToken {
   nsAutoString mName;  // Empty for a null cell, non-empty for a named cell
   bool isTrash;  // True for a trash token, mName is ignored in this case.
 };
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -5485,29 +5485,24 @@ nsComputedDOMStyle::GetLineHeightCoord(n
 }
 
 already_AddRefed<CSSValue>
 nsComputedDOMStyle::GetBorderColorsFor(mozilla::Side aSide)
 {
   const nsStyleBorder *border = StyleBorder();
 
   if (border->mBorderColors) {
-    nsBorderColors* borderColors = border->mBorderColors[aSide];
-    if (borderColors) {
+    const nsTArray<nscolor>& borderColors = (*border->mBorderColors)[aSide];
+    if (!borderColors.IsEmpty()) {
       RefPtr<nsDOMCSSValueList> valueList = GetROCSSValueList(false);
-
-      do {
+      for (nscolor color : borderColors) {
         RefPtr<nsROCSSPrimitiveValue> primitive = new nsROCSSPrimitiveValue;
-
-        SetToRGBAColor(primitive, borderColors->mColor);
-
+        SetToRGBAColor(primitive, color);
         valueList->AppendCSSValue(primitive.forget());
-        borderColors = borderColors->mNext;
-      } while (borderColors);
-
+      }
       return valueList.forget();
     }
   }
 
   RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
   val->SetIdent(eCSSKeyword_none);
   return val.forget();
 }
--- a/layout/style/nsRuleNode.cpp
+++ b/layout/style/nsRuleNode.cpp
@@ -7743,38 +7743,35 @@ nsRuleNode::ComputeBorderData(void* aSta
     case eCSSUnit_Unset:
     case eCSSUnit_None:
       border->ClearBorderColors(side);
       break;
 
     case eCSSUnit_Inherit: {
       conditions.SetUncacheable();
       border->ClearBorderColors(side);
-      if (parentContext) {
-        nsBorderColors *parentColors;
-        parentBorder->GetCompositeColors(side, &parentColors);
-        if (parentColors) {
-          border->EnsureBorderColors();
-          border->mBorderColors[side] = parentColors->Clone();
-        }
+      if (parentBorder->mBorderColors) {
+        border->EnsureBorderColors();
+        border->mBorderColors->mColors[side] =
+          parentBorder->mBorderColors->mColors[side];
       }
       break;
     }
 
     case eCSSUnit_List:
     case eCSSUnit_ListDep: {
       // Some composite border color information has been specified for this
       // border side.
       border->EnsureBorderColors();
       border->ClearBorderColors(side);
       const nsCSSValueList* list = value.GetListValue();
       while (list) {
         if (SetColor(list->mValue, unused, mPresContext,
                      aContext, borderColor, conditions))
-          border->AppendBorderColor(side, borderColor);
+          border->mBorderColors->mColors[side].AppendElement(borderColor);
         else {
           NS_NOTREACHED("unexpected item in -moz-border-*-colors list");
         }
         list = list->mNext;
       }
       break;
     }
 
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -315,18 +315,17 @@ nsStylePadding::CalcDifference(const nsS
   // FIXME: It would be good to return a weaker hint here that doesn't
   // force reflow of all descendants, but the hint would need to force
   // reflow of the frame's children (see how
   // ReflowInput::InitResizeFlags initializes the inline-resize flag).
   return NS_STYLE_HINT_REFLOW & ~nsChangeHint_ClearDescendantIntrinsics;
 }
 
 nsStyleBorder::nsStyleBorder(const nsPresContext* aContext)
-  : mBorderColors(nullptr)
-  , mBorderImageFill(NS_STYLE_BORDER_IMAGE_SLICE_NOFILL)
+  : mBorderImageFill(NS_STYLE_BORDER_IMAGE_SLICE_NOFILL)
   , mBorderImageRepeatH(NS_STYLE_BORDER_IMAGE_REPEAT_STRETCH)
   , mBorderImageRepeatV(NS_STYLE_BORDER_IMAGE_REPEAT_STRETCH)
   , mFloatEdge(StyleFloatEdge::ContentBox)
   , mBoxDecorationBreak(StyleBoxDecorationBreak::Slice)
   , mComputedBorder(0, 0, 0, 0)
 {
   MOZ_COUNT_CTOR(nsStyleBorder);
 
@@ -344,72 +343,45 @@ nsStyleBorder::nsStyleBorder(const nsPre
     mBorder.Side(side) = medium;
     mBorderStyle[side] = NS_STYLE_BORDER_STYLE_NONE;
     mBorderColor[side] = StyleComplexColor::CurrentColor();
   }
 
   mTwipsPerPixel = aContext->DevPixelsToAppUnits(1);
 }
 
-nsBorderColors::~nsBorderColors()
-{
-  NS_CSS_DELETE_LIST_MEMBER(nsBorderColors, this, mNext);
-}
-
-nsBorderColors*
-nsBorderColors::Clone(bool aDeep) const
-{
-  nsBorderColors* result = new nsBorderColors(mColor);
-  if (MOZ_UNLIKELY(!result)) {
-    return result;
-  }
-  if (aDeep) {
-    NS_CSS_CLONE_LIST_MEMBER(nsBorderColors, this, mNext, result, (false));
-  }
-  return result;
-}
-
 nsStyleBorder::nsStyleBorder(const nsStyleBorder& aSrc)
-  : mBorderColors(nullptr)
-  , mBorderRadius(aSrc.mBorderRadius)
+  : mBorderRadius(aSrc.mBorderRadius)
   , mBorderImageSource(aSrc.mBorderImageSource)
   , mBorderImageSlice(aSrc.mBorderImageSlice)
   , mBorderImageWidth(aSrc.mBorderImageWidth)
   , mBorderImageOutset(aSrc.mBorderImageOutset)
   , mBorderImageFill(aSrc.mBorderImageFill)
   , mBorderImageRepeatH(aSrc.mBorderImageRepeatH)
   , mBorderImageRepeatV(aSrc.mBorderImageRepeatV)
   , mFloatEdge(aSrc.mFloatEdge)
   , mBoxDecorationBreak(aSrc.mBoxDecorationBreak)
   , mComputedBorder(aSrc.mComputedBorder)
   , mBorder(aSrc.mBorder)
   , mTwipsPerPixel(aSrc.mTwipsPerPixel)
 {
   MOZ_COUNT_CTOR(nsStyleBorder);
   if (aSrc.mBorderColors) {
-    NS_FOR_CSS_SIDES(side) {
-      CopyBorderColorsFrom(aSrc.mBorderColors[side], side);
-    }
+    mBorderColors.reset(new nsBorderColors(*aSrc.mBorderColors));
   }
 
   NS_FOR_CSS_SIDES(side) {
     mBorderStyle[side] = aSrc.mBorderStyle[side];
     mBorderColor[side] = aSrc.mBorderColor[side];
   }
 }
 
 nsStyleBorder::~nsStyleBorder()
 {
   MOZ_COUNT_DTOR(nsStyleBorder);
-  if (mBorderColors) {
-    for (int32_t i = 0; i < 4; i++) {
-      delete mBorderColors[i];
-    }
-    delete [] mBorderColors;
-  }
 }
 
 void
 nsStyleBorder::FinishStyle(nsPresContext* aPresContext)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aPresContext->StyleSet()->IsServo());
 
@@ -511,19 +483,18 @@ nsStyleBorder::CalcDifference(const nsSt
         mBorderImageWidth   != aNewData.mBorderImageWidth) {
       return nsChangeHint_RepaintFrame;
     }
   }
 
   // Note that at this point if mBorderColors is non-null so is
   // aNewData.mBorderColors
   if (mBorderColors) {
-    NS_FOR_CSS_SIDES(ix) {
-      if (!nsBorderColors::Equal(mBorderColors[ix],
-                                 aNewData.mBorderColors[ix])) {
+    NS_FOR_CSS_SIDES(side) {
+      if ((*mBorderColors)[side] != (*aNewData.mBorderColors)[side]) {
         return nsChangeHint_RepaintFrame;
       }
     }
   }
 
   // mBorder is the specified border value.  Changes to this don't
   // need any change processing, since we operate on the computed
   // border values instead.
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -979,48 +979,16 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
     NS_FOR_CSS_SIDES(side) {
       // Clamp negative calc() to 0.
       aPadding.Side(side) = std::max(mPadding.ToLength(side), 0);
     }
     return true;
   }
 };
 
-struct nsBorderColors
-{
-  nsBorderColors* mNext;
-  nscolor mColor;
-
-  nsBorderColors() : mNext(nullptr), mColor(NS_RGB(0,0,0)) {}
-  explicit nsBorderColors(const nscolor& aColor) : mNext(nullptr), mColor(aColor) {}
-  ~nsBorderColors();
-
-  nsBorderColors* Clone() const { return Clone(true); }
-
-  static bool Equal(const nsBorderColors* c1,
-                      const nsBorderColors* c2) {
-    if (c1 == c2) {
-      return true;
-    }
-    while (c1 && c2) {
-      if (c1->mColor != c2->mColor) {
-        return false;
-      }
-      c1 = c1->mNext;
-      c2 = c2->mNext;
-    }
-    // both should be nullptr if these are equal, otherwise one
-    // has more colors than another
-    return !c1 && !c2;
-  }
-
-private:
-  nsBorderColors* Clone(bool aDeep) const;
-};
-
 struct nsCSSShadowItem
 {
   nscoord mXOffset;
   nscoord mYOffset;
   nscoord mRadius;
   nscoord mSpread;
 
   nscolor      mColor;
@@ -1137,16 +1105,36 @@ private:
 
 // Returns if the given border style type is visible or not
 static bool IsVisibleBorderStyle(uint8_t aStyle)
 {
   return (aStyle != NS_STYLE_BORDER_STYLE_NONE &&
           aStyle != NS_STYLE_BORDER_STYLE_HIDDEN);
 }
 
+struct nsBorderColors
+{
+  nsBorderColors() = default;
+
+  // GCC cannot generate this copy constructor correctly, since nsTArray
+  // has explicit copy constructor, and we use array of nsTArray here.
+  // See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82235
+  nsBorderColors(const nsBorderColors& aOther) {
+    NS_FOR_CSS_SIDES(side) {
+      mColors[side] = aOther.mColors[side];
+    }
+  }
+
+  const nsTArray<nscolor>& operator[](mozilla::Side aSide) const {
+    return mColors[aSide];
+  }
+
+  nsTArray<nscolor> mColors[4];
+};
+
 struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleBorder
 {
   explicit nsStyleBorder(const nsPresContext* aContext);
   nsStyleBorder(const nsStyleBorder& aBorder);
   ~nsStyleBorder();
 
   // Resolves and tracks mBorderImageSource.  Only called with a Servo-backed
   // style system, where those images must be resolved later than the OMT
@@ -1160,37 +1148,23 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
       AllocateByObjectID(mozilla::eArenaObjectID_nsStyleBorder, sz);
   }
   void Destroy(nsPresContext* aContext);
 
   nsChangeHint CalcDifference(const nsStyleBorder& aNewData) const;
 
   void EnsureBorderColors() {
     if (!mBorderColors) {
-      mBorderColors = new nsBorderColors*[4];
-      if (mBorderColors) {
-        for (int32_t i = 0; i < 4; i++) {
-          mBorderColors[i] = nullptr;
-        }
-      }
+      mBorderColors.reset(new nsBorderColors);
     }
   }
 
   void ClearBorderColors(mozilla::Side aSide) {
-    if (mBorderColors && mBorderColors[aSide]) {
-      delete mBorderColors[aSide];
-      mBorderColors[aSide] = nullptr;
-    }
-  }
-
-  void CopyBorderColorsFrom(const nsBorderColors* aSrcBorderColors, mozilla::Side aSide) {
-    if (aSrcBorderColors) {
-      EnsureBorderColors();
-      ClearBorderColors(aSide);
-      mBorderColors[aSide] = aSrcBorderColors->Clone();
+    if (mBorderColors) {
+      mBorderColors->mColors[aSide].Clear();
     }
   }
 
   // Return whether aStyle is a visible style.  Invisible styles cause
   // the relevant computed border width to be 0.
   // Note that this does *not* consider the effects of 'border-image':
   // if border-style is none, but there is a loaded border image,
   // HasVisibleStyle will be false even though there *is* a border.
@@ -1255,50 +1229,27 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
   {
     if (mBorderImageSource.GetType() == eStyleImageType_Image) {
       mBorderImageSource.ResolveImage(aContext);
     }
   }
 
   nsMargin GetImageOutset() const;
 
-  void GetCompositeColors(int32_t aIndex, nsBorderColors** aColors) const
-  {
-    if (!mBorderColors) {
-      *aColors = nullptr;
-    } else {
-      *aColors = mBorderColors[aIndex];
-    }
-  }
-
-  void AppendBorderColor(int32_t aIndex, nscolor aColor)
-  {
-    NS_ASSERTION(aIndex >= 0 && aIndex <= 3, "bad side for composite border color");
-    nsBorderColors* colorEntry = new nsBorderColors(aColor);
-    if (!mBorderColors[aIndex]) {
-      mBorderColors[aIndex] = colorEntry;
-    } else {
-      nsBorderColors* last = mBorderColors[aIndex];
-      while (last->mNext) {
-        last = last->mNext;
-      }
-      last->mNext = colorEntry;
-    }
-  }
-
   imgIRequest* GetBorderImageRequest() const
   {
     if (mBorderImageSource.GetType() == eStyleImageType_Image) {
       return mBorderImageSource.GetImageData();
     }
     return nullptr;
   }
 
 public:
-  nsBorderColors** mBorderColors;     // [reset] composite (stripe) colors
+  // [reset] composite (stripe) colors
+  mozilla::UniquePtr<nsBorderColors> mBorderColors;
   nsStyleCorners mBorderRadius;       // [reset] coord, percent
   nsStyleImage   mBorderImageSource;  // [reset]
   nsStyleSides   mBorderImageSlice;   // [reset] factor, percent
   nsStyleSides   mBorderImageWidth;   // [reset] length, factor, percent, auto
   nsStyleSides   mBorderImageOutset;  // [reset] length, factor
 
   uint8_t        mBorderImageFill;    // [reset]
   uint8_t        mBorderImageRepeatH; // [reset] see nsStyleConsts.h
--- a/layout/style/nsStyleTransformMatrix.cpp
+++ b/layout/style/nsStyleTransformMatrix.cpp
@@ -321,16 +321,23 @@ public:
 
     gfxQuaternion result = gfxQuaternion(scale * aTwo.x,
                                          scale * aTwo.y,
                                          scale * aTwo.z,
                                          cos(theta)) * aOne;
     return result.ToMatrix();
   }
 
+  static Matrix4x4 operateForFallback(const Matrix4x4& aMatrix1,
+                                      const Matrix4x4& aMatrix2,
+                                      double aProgress)
+  {
+    return aMatrix1;
+  }
+
   static Matrix4x4 operateByServo(const Matrix4x4& aMatrix1,
                                   const Matrix4x4& aMatrix2,
                                   double aCount)
   {
     Matrix4x4 result;
     Servo_MatrixTransform_Operate(MatrixTransformOperator::Accumulate,
                                   &aMatrix1.components,
                                   &aMatrix2.components,
@@ -364,16 +371,23 @@ public:
 
   static Matrix4x4 operateForRotate(const gfxQuaternion& aOne,
                                     const gfxQuaternion& aTwo,
                                     double aCoeff)
   {
     return aOne.Slerp(aTwo, aCoeff).ToMatrix();
   }
 
+  static Matrix4x4 operateForFallback(const Matrix4x4& aMatrix1,
+                                      const Matrix4x4& aMatrix2,
+                                      double aProgress)
+  {
+    return aProgress < 0.5 ? aMatrix1 : aMatrix2;
+  }
+
   static Matrix4x4 operateByServo(const Matrix4x4& aMatrix1,
                                   const Matrix4x4& aMatrix2,
                                   double aProgress)
   {
     Matrix4x4 result;
     Servo_MatrixTransform_Operate(MatrixTransformOperator::Interpolate,
                                   &aMatrix1.components,
                                   &aMatrix2.components,
@@ -393,36 +407,44 @@ public:
 template <typename Operator>
 static Matrix4x4
 OperateTransformMatrix(const Matrix4x4 &aMatrix1,
                        const Matrix4x4 &aMatrix2,
                        double aProgress)
 {
   // Decompose both matrices
 
-  // TODO: What do we do if one of these returns false (singular matrix)
   Point3D scale1(1, 1, 1), translate1;
   Point4D perspective1(0, 0, 0, 1);
   gfxQuaternion rotate1;
   nsStyleTransformMatrix::ShearArray shear1{0.0f, 0.0f, 0.0f};
 
   Point3D scale2(1, 1, 1), translate2;
   Point4D perspective2(0, 0, 0, 1);
   gfxQuaternion rotate2;
   nsStyleTransformMatrix::ShearArray shear2{0.0f, 0.0f, 0.0f};
 
+  // Check if both matrices are decomposable.
+  bool wasDecomposed;
   Matrix matrix2d1, matrix2d2;
   if (aMatrix1.Is2D(&matrix2d1) && aMatrix2.Is2D(&matrix2d2)) {
-    Decompose2DMatrix(matrix2d1, scale1, shear1, rotate1, translate1);
-    Decompose2DMatrix(matrix2d2, scale2, shear2, rotate2, translate2);
+    wasDecomposed =
+      Decompose2DMatrix(matrix2d1, scale1, shear1, rotate1, translate1) &&
+      Decompose2DMatrix(matrix2d2, scale2, shear2, rotate2, translate2);
   } else {
-    Decompose3DMatrix(aMatrix1, scale1, shear1,
-                      rotate1, translate1, perspective1);
-    Decompose3DMatrix(aMatrix2, scale2, shear2,
-                      rotate2, translate2, perspective2);
+    wasDecomposed =
+      Decompose3DMatrix(aMatrix1, scale1, shear1,
+                        rotate1, translate1, perspective1) &&
+      Decompose3DMatrix(aMatrix2, scale2, shear2,
+                        rotate2, translate2, perspective2);
+  }
+
+  // Fallback to discrete operation if one of the matrices is not decomposable.
+  if (!wasDecomposed) {
+    return Operator::operateForFallback(aMatrix1, aMatrix2, aProgress);
   }
 
   Matrix4x4 result;
 
   // Operate each of the pieces in response to |Operator|.
   Point4D perspective =
     Operator::operateForPerspective(perspective1, perspective2, aProgress);
   result.SetTransposedVector(3, perspective);
--- a/layout/style/test/browser.ini
+++ b/layout/style/test/browser.ini
@@ -9,8 +9,9 @@ support-files =
   mapped2.css^headers^
   sourcemap_css.html
 
 [browser_bug453896.js]
 [browser_newtab_share_rule_processors.js]
 skip-if = stylo # Gecko-specific test
 [browser_sourcemap.js]
 [browser_sourcemap_comment.js]
+[browser_sourceurl_comment.js]
new file mode 100644
--- /dev/null
+++ b/layout/style/test/browser_sourceurl_comment.js
@@ -0,0 +1,40 @@
+add_task(async function() {
+  // Test text and expected results.
+  let test_cases = [
+    ["/*# sourceURL=here*/", "here"],
+    ["/*# sourceURL=here  */", "here"],
+    ["/*@ sourceURL=here*/", "here"],
+    ["/*@ sourceURL=there*/ /*# sourceURL=here*/", "here"],
+    ["/*# sourceURL=here there  */", "here"],
+
+    ["/*# sourceURL=  here  */", ""],
+    ["/*# sourceURL=*/", ""],
+    ["/*# sourceUR=here  */", ""],
+    ["/*! sourceURL=here  */", ""],
+    ["/*# sourceURL = here  */", ""],
+    ["/*   # sourceURL=here   */", ""],
+  ];
+
+  let page = "<!DOCTYPE HTML>\n<html>\n<head>\n";
+  for (let i = 0; i < test_cases.length; ++i) {
+    page += `<style type="text/css"> #x${i} { color: red; }${test_cases[i][0]}</style>\n`;
+  }
+  page += "</head><body>some text</body></html>";
+
+  let uri = "data:text/html;base64," + btoa(page);
+  info(`URI is ${uri}`);
+
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: uri
+  }, async function(browser) {
+    await ContentTask.spawn(browser, test_cases, function* (tests) {
+      for (let i = 0; i < content.document.styleSheets.length; ++i) {
+        let sheet = content.document.styleSheets[i];
+
+        info(`Checking sheet #${i}`);
+        is(sheet.sourceURL, tests[i][1], `correct source URL for sheet ${i}`);
+      }
+    });
+  });
+});
--- a/layout/svg/SVGGeometryFrame.cpp
+++ b/layout/svg/SVGGeometryFrame.cpp
@@ -186,25 +186,16 @@ SVGGeometryFrame::AttributeChanged(int32
 }
 
 /* virtual */ void
 SVGGeometryFrame::DidSetStyleContext(nsStyleContext* aOldStyleContext)
 {
   nsFrame::DidSetStyleContext(aOldStyleContext);
 
   if (aOldStyleContext) {
-    auto oldStyleEffects = aOldStyleContext->PeekStyleEffects();
-    if (oldStyleEffects &&
-        StyleEffects()->mOpacity != oldStyleEffects->mOpacity &&
-        nsSVGUtils::CanOptimizeOpacity(this)) {
-      // nsIFrame::BuildDisplayListForStackingContext() is not going to create an
-      // nsDisplayOpacity display list item, so DLBI won't invalidate for us.
-      InvalidateFrame();
-    }
-
     SVGGeometryElement* element =
       static_cast<SVGGeometryElement*>(GetContent());
 
     auto oldStyleSVG = aOldStyleContext->PeekStyleSVG();
     if (oldStyleSVG && !SVGContentUtils::ShapeTypeHasNoCorners(GetContent())) {
       if (StyleSVG()->mStrokeLinecap != oldStyleSVG->mStrokeLinecap &&
           element->IsSVGElement(nsGkAtoms::path)) {
         // If the stroke-linecap changes to or from "butt" then our element
--- a/layout/svg/nsSVGUtils.cpp
+++ b/layout/svg/nsSVGUtils.cpp
@@ -1304,16 +1304,21 @@ nsSVGUtils::CanOptimizeOpacity(nsIFrame 
   // XXX The SVG WG is intending to allow fill, stroke and markers on <image>
   if (type == LayoutFrameType::SVGImage) {
     return true;
   }
   const nsStyleSVG *style = aFrame->StyleSVG();
   if (style->HasMarker()) {
     return false;
   }
+
+  if (nsLayoutUtils::HasAnimationOfProperty(aFrame, eCSSProperty_opacity)) {
+    return false;
+  }
+
   if (!style->HasFill() || !HasStroke(aFrame)) {
     return true;
   }
   return false;
 }
 
 gfxMatrix
 nsSVGUtils::AdjustMatrixForUnits(const gfxMatrix &aMatrix,
--- a/memory/build/malloc_decls.h
+++ b/memory/build/malloc_decls.h
@@ -24,40 +24,36 @@
 
 #endif /* malloc_decls_h */
 
 #ifndef MALLOC_FUNCS
 #  define MALLOC_FUNCS (MALLOC_FUNCS_MALLOC | MALLOC_FUNCS_JEMALLOC)
 #endif
 
 #ifdef MALLOC_DECL
-#  ifndef MALLOC_DECL_VOID
-#    define MALLOC_DECL_VOID(func, ...) MALLOC_DECL(func, void, __VA_ARGS__)
-#  endif
-
 #  if MALLOC_FUNCS & MALLOC_FUNCS_INIT
 MALLOC_DECL(init, void, const malloc_table_t *)
 #  endif
 #  if MALLOC_FUNCS & MALLOC_FUNCS_BRIDGE
 MALLOC_DECL(get_bridge, struct ReplaceMallocBridge*)
 #  endif
 #  if MALLOC_FUNCS & MALLOC_FUNCS_MALLOC
 MALLOC_DECL(malloc, void *, size_t)
 MALLOC_DECL(posix_memalign, int, void **, size_t, size_t)
 MALLOC_DECL(aligned_alloc, void *, size_t, size_t)
 MALLOC_DECL(calloc, void *, size_t, size_t)
 MALLOC_DECL(realloc, void *, void *, size_t)
-MALLOC_DECL_VOID(free, void *)
+MALLOC_DECL(free, void, void *)
 MALLOC_DECL(memalign, void *, size_t, size_t)
 MALLOC_DECL(valloc, void *, size_t)
 MALLOC_DECL(malloc_usable_size, size_t, usable_ptr_t)
 MALLOC_DECL(malloc_good_size, size_t, size_t)
 #  endif
 #  if MALLOC_FUNCS & MALLOC_FUNCS_JEMALLOC
-MALLOC_DECL_VOID(jemalloc_stats, jemalloc_stats_t *)
+MALLOC_DECL(jemalloc_stats, void, jemalloc_stats_t *)
 /*
  * On some operating systems (Mac), we use madvise(MADV_FREE) to hand pages
  * back to the operating system.  On Mac, the operating system doesn't take
  * this memory back immediately; instead, the OS takes it back only when the
  * machine is running out of physical memory.
  *
  * This is great from the standpoint of efficiency, but it makes measuring our
  * actual RSS difficult, because pages which we've MADV_FREE'd shouldn't count
@@ -73,35 +69,34 @@ MALLOC_DECL_VOID(jemalloc_stats, jemallo
  *
  * This function is also expensive in that the next time we go to access a page
  * which we've just explicitly decommitted, the operating system has to attach
  * to it a physical page!  If we hadn't run this function, the OS would have
  * less work to do.
  *
  * If MALLOC_DOUBLE_PURGE is not defined, this function does nothing.
  */
-MALLOC_DECL_VOID(jemalloc_purge_freed_pages)
+MALLOC_DECL(jemalloc_purge_freed_pages, void)
 
 /*
  * Free all unused dirty pages in all arenas. Calling this function will slow
  * down subsequent allocations so it is recommended to use it only when
  * memory needs to be reclaimed at all costs (see bug 805855). This function
  * provides functionality similar to mallctl("arenas.purge") in jemalloc 3.
  */
-MALLOC_DECL_VOID(jemalloc_free_dirty_pages)
+MALLOC_DECL(jemalloc_free_dirty_pages, void)
 
 /*
  * Opt in or out of a thread local arena (bool argument is whether to opt-in
  * (true) or out (false)).
  */
-MALLOC_DECL_VOID(jemalloc_thread_local_arena, bool)
+MALLOC_DECL(jemalloc_thread_local_arena, void, bool)
 
 /*
  * Provide information about any allocation enclosing the given address.
  */
-MALLOC_DECL_VOID(jemalloc_ptr_info, const void*, jemalloc_ptr_info_t*)
+MALLOC_DECL(jemalloc_ptr_info, void, const void*, jemalloc_ptr_info_t*)
 #  endif
 
-#  undef MALLOC_DECL_VOID
 #endif /* MALLOC_DECL */
 
 #undef MALLOC_DECL
 #undef MALLOC_FUNCS
--- a/memory/build/moz.build
+++ b/memory/build/moz.build
@@ -28,27 +28,22 @@ UNIFIED_SOURCES += [
 if CONFIG['OS_TARGET'] == 'Darwin' and (CONFIG['MOZ_REPLACE_MALLOC'] or
         CONFIG['MOZ_MEMORY']):
     SOURCES += [
         'zone.c',
     ]
 
 Library('memory')
 
-if CONFIG['MOZ_GLUE_IN_PROGRAM']:
-    DIST_INSTALL = True
-
 if CONFIG['OS_TARGET'] == 'Android' and CONFIG['CC_TYPE'] == 'clang':
     CFLAGS += [
         '-Wno-tautological-pointer-compare',
     ]
 
-# Keep jemalloc separated when mozglue is statically linked
-if CONFIG['MOZ_MEMORY'] and CONFIG['OS_TARGET'] in ('WINNT', 'Darwin', 'Android'):
-    FINAL_LIBRARY = 'mozglue'
+FINAL_LIBRARY = 'mozglue'
 
 if CONFIG['GNU_CXX']:
     # too many warnings from functions generated through rb_wrab from rb.h.
     CXXFLAGS += ['-Wno-unused-function',
                  '-Wno-error=uninitialized']
 
 if CONFIG['_MSC_VER']:
     CXXFLAGS += ['-wd4273'] # inconsistent dll linkage (bug 558163)
--- a/memory/build/mozjemalloc.cpp
+++ b/memory/build/mozjemalloc.cpp
@@ -5211,21 +5211,16 @@ void
 #define TYPED_ARGS2(t1, t2) TYPED_ARGS1(t1), t2 arg2
 #define TYPED_ARGS3(t1, t2, t3) TYPED_ARGS2(t1, t2), t3 arg3
 
 #define ARGS0()
 #define ARGS1(t1) arg1
 #define ARGS2(t1, t2) ARGS1(t1), arg2
 #define ARGS3(t1, t2, t3) ARGS2(t1, t2), arg3
 
-#define GENERIC_MALLOC_DECL(name, return_type, ...) \
-  GENERIC_MALLOC_DECL_HELPER(name, return, return_type, ##__VA_ARGS__)
-#define GENERIC_MALLOC_DECL_VOID(name, ...) \
-  GENERIC_MALLOC_DECL_HELPER(name, , void, ##__VA_ARGS__)
-
 /******************************************************************************/
 #ifdef MOZ_REPLACE_MALLOC
 
 /*
  * Windows doesn't come with weak imports as they are possible with
  * LD_PRELOAD or DYLD_INSERT_LIBRARIES on Linux/OSX. On this platform,
  * the replacement functions are defined as variable pointers to the
  * function resolved with GetProcAddress() instead of weak definitions
@@ -5326,33 +5321,28 @@ init()
   // Set this *before* calling replace_init, otherwise if replace_init calls
   // malloc() we'll get an infinite loop.
   replace_malloc_initialized = 1;
   if (replace_init) {
     replace_init(&malloc_table);
   }
 }
 
-#define GENERIC_MALLOC_DECL_HELPER(name, return, return_type, ...) \
+#define MALLOC_DECL(name, return_type, ...) \
   template<> inline return_type \
   ReplaceMalloc::name(ARGS_HELPER(TYPED_ARGS, ##__VA_ARGS__)) \
   { \
     if (MOZ_UNLIKELY(!replace_malloc_initialized)) { \
       init(); \
     } \
     return replace_malloc_table.name(ARGS_HELPER(ARGS, ##__VA_ARGS__)); \
   }
-
-#define MALLOC_DECL(...) MACRO_CALL(GENERIC_MALLOC_DECL, (__VA_ARGS__))
-#define MALLOC_DECL_VOID(...) MACRO_CALL(GENERIC_MALLOC_DECL_VOID, (__VA_ARGS__))
 #define MALLOC_FUNCS (MALLOC_FUNCS_MALLOC | MALLOC_FUNCS_JEMALLOC)
 #include "malloc_decls.h"
 
-#undef GENERIC_MALLOC_DECL_HELPER
-
 MOZ_JEMALLOC_API struct ReplaceMallocBridge*
 get_bridge(void)
 {
   if (MOZ_UNLIKELY(!replace_malloc_initialized))
     init();
   if (MOZ_LIKELY(!replace_get_bridge))
     return nullptr;
   return replace_get_bridge();
@@ -5398,36 +5388,34 @@ replace_malloc_init_funcs()
   }
 #include "malloc_decls.h"
 }
 
 #endif /* MOZ_REPLACE_MALLOC */
 /******************************************************************************/
 /* Definition of all the _impl functions */
 
-#define GENERIC_MALLOC_DECL_HELPER2(name, name_impl, return, return_type, ...) \
+#define GENERIC_MALLOC_DECL2(name, name_impl, return_type, ...) \
   return_type name_impl(ARGS_HELPER(TYPED_ARGS, ##__VA_ARGS__)) \
   { \
     return DefaultMalloc::name(ARGS_HELPER(ARGS, ##__VA_ARGS__)); \
   }
 
-#define GENERIC_MALLOC_DECL_HELPER(name, return, return_type, ...) \
-  GENERIC_MALLOC_DECL_HELPER2(name, name##_impl, return, return_type, ##__VA_ARGS__)
+#define GENERIC_MALLOC_DECL(name, return_type, ...) \
+  GENERIC_MALLOC_DECL2(name, name##_impl, return_type, ##__VA_ARGS__)
 
 #define MALLOC_DECL(...) MOZ_MEMORY_API MACRO_CALL(GENERIC_MALLOC_DECL, (__VA_ARGS__))
-#define MALLOC_DECL_VOID(...) MOZ_MEMORY_API MACRO_CALL(GENERIC_MALLOC_DECL_VOID, (__VA_ARGS__))
 #define MALLOC_FUNCS MALLOC_FUNCS_MALLOC
 #include "malloc_decls.h"
 
-#undef GENERIC_MALLOC_DECL_HELPER
-#define GENERIC_MALLOC_DECL_HELPER(name, return, return_type, ...) \
-  GENERIC_MALLOC_DECL_HELPER2(name, name, return, return_type, ##__VA_ARGS__)
+#undef GENERIC_MALLOC_DECL
+#define GENERIC_MALLOC_DECL(name, return_type, ...) \
+  GENERIC_MALLOC_DECL2(name, name, return_type, ##__VA_ARGS__)
 
 #define MALLOC_DECL(...) MOZ_JEMALLOC_API MACRO_CALL(GENERIC_MALLOC_DECL, (__VA_ARGS__))
-#define MALLOC_DECL_VOID(...) MOZ_JEMALLOC_API MACRO_CALL(GENERIC_MALLOC_DECL_VOID, (__VA_ARGS__))
 #define MALLOC_FUNCS MALLOC_FUNCS_JEMALLOC
 #include "malloc_decls.h"
 /******************************************************************************/
 
 #ifdef HAVE_DLOPEN
 #  include <dlfcn.h>
 #endif
 
--- a/memory/build/replace_malloc_bridge.h
+++ b/memory/build/replace_malloc_bridge.h
@@ -67,45 +67,59 @@ MFBT_API ReplaceMallocBridge* get_bridge
 
 #define MALLOC_DECL(name, return_type, ...) \
   name ## _impl_t * name;
 
 typedef struct {
 #include "malloc_decls.h"
 } malloc_table_t;
 
+MOZ_END_EXTERN_C
+
+#ifdef __cplusplus
 
 /* Table of malloc hook functions.
  * Those functions are called with the arguments and results of malloc
  * functions after they are called.
  *   e.g. void* (*malloc_hook)(void*, size_t), etc.
  * They can either return the result they're given, or alter it before
  * returning it.
  * The hooks corresponding to functions, like free(void*), that return no
  * value, don't take an extra argument.
  * The table must at least contain a pointer for malloc_hook and free_hook
  * functions. They will be used as fallback if no pointer is given for
  * other allocation functions, like calloc_hook.
  */
+namespace mozilla {
+namespace detail {
+template <typename R, typename... Args>
+struct AllocHookType {
+  using Type = R (*)(R, Args...);
+};
+
+template <typename... Args>
+struct AllocHookType<void, Args...>
+{
+  using Type = void (*)(Args...);
+};
+
+} // namespace detail
+} // namespace mozilla
+
 #define MALLOC_DECL(name, return_type, ...) \
-  return_type (*name ## _hook)(return_type, __VA_ARGS__);
-#define MALLOC_DECL_VOID(name, ...) \
-  void (*name ## _hook)(__VA_ARGS__);
+  typename mozilla::detail::AllocHookType<return_type, ##__VA_ARGS__>::Type \
+    name ## _hook;
 
 typedef struct {
 #include "malloc_decls.h"
   /* Like free_hook, but called before realloc_hook. free_hook is called
    * instead of not given. */
   void (*realloc_hook_before)(void* aPtr);
 } malloc_hook_table_t;
 
-MOZ_END_EXTERN_C
-
-#ifdef __cplusplus
-
 namespace mozilla {
 namespace dmd {
 struct DMDFuncs;
 } // namespace dmd
 
 /* Callbacks to register debug file handles for Poison IO interpose.
  * See Mozilla(|Un)RegisterDebugHandle in xpcom/build/PoisonIOInterposer.h */
 struct DebugFdRegistry
--- a/mobile/android/app/src/main/res/layout/activity_stream_highlights_empty_state.xml
+++ b/mobile/android/app/src/main/res/layout/activity_stream_highlights_empty_state.xml
@@ -1,12 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
+
+<!-- The width and height of this view are always overridden in onBindViewHolder because
+     this view is dynamically shown/hidden.
+
+     See StreamRecyclerAdapter.setViewVisible . -->
+
 <TextView
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_margin="@dimen/activity_stream_base_margin"
         android:text="@string/activity_stream_highlights_empty"
         android:textColor="@color/activity_stream_subtitle"
         />
--- a/mobile/android/app/src/main/res/layout/activity_stream_main_highlightstitle.xml
+++ b/mobile/android/app/src/main/res/layout/activity_stream_main_highlightstitle.xml
@@ -1,39 +1,45 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
+
+<!-- The width and height of this view are always overridden in onBindViewHolder because
+     this view is dynamically shown/hidden.
+
+     See StreamRecyclerAdapter.setViewVisible . -->
+
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_width="0dp"
+    android:layout_height="0dp"
     android:orientation="horizontal">
 
     <TextView
         android:id="@+id/title_highlights"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:layout_margin="@dimen/activity_stream_base_margin"
         android:textColor="#FF858585"
-        android:textSize="16sp"
+        android:textSize="18sp"
         android:textStyle="bold" />
 
     <!-- These *_link items are shown if the constructor includes a link to be shown
     on the right side of the stream title -->
     <TextView
         android:id="@+id/title_link"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:gravity="center_vertical"
         android:paddingStart="0dp"
         android:paddingLeft="0dp"
-        android:paddingEnd="@dimen/activity_stream_base_margin"
-        android:paddingRight="@dimen/activity_stream_base_margin"
+        android:paddingEnd="5dp"
+        android:paddingRight="5dp"
         android:textAppearance="@style/TextAppearance.Link"
         android:textAllCaps="true"
         android:visibility="gone"/>
 
     <ImageView
         android:id="@+id/arrow_link"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
--- a/mobile/android/app/src/main/res/menu-large-v26/browser_app_menu.xml
+++ b/mobile/android/app/src/main/res/menu-large-v26/browser_app_menu.xml
@@ -1,16 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <!-- We disable AlwaysShowAction because we interpret the menu
      attributes ourselves and thus the warning isn't relevant to us. -->
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:gecko="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools"
       tools:ignore="AlwaysShowAction">
 
     <item android:id="@+id/reload"
           android:icon="@drawable/ic_menu_reload"
           android:title="@string/reload"
           android:showAsAction="always"/>
 
@@ -50,16 +51,17 @@
           android:title="@string/find_in_page" />
 
     <item android:id="@+id/desktop_mode"
           android:title="@string/desktop_mode"
           android:checkable="true" />
 
     <item android:id="@+id/addons_top_level"
           android:title="@string/addons"
+          gecko:itemType="icon_menu_item"
           android:visible="false" />
 
     <item android:id="@+id/page"
           android:title="@string/page">
 
         <menu>
 
             <item android:id="@+id/subscribe"
--- a/mobile/android/app/src/main/res/menu-large/browser_app_menu.xml
+++ b/mobile/android/app/src/main/res/menu-large/browser_app_menu.xml
@@ -1,16 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <!-- We disable AlwaysShowAction because we interpret the menu
      attributes ourselves and thus the warning isn't relevant to us. -->
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:gecko="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools"
       tools:ignore="AlwaysShowAction">
 
     <item android:id="@+id/reload"
           android:icon="@drawable/ic_menu_reload"
           android:title="@string/reload"
           android:showAsAction="always"/>
 
@@ -50,16 +51,17 @@
           android:title="@string/find_in_page" />
 
     <item android:id="@+id/desktop_mode"
           android:title="@string/desktop_mode"
           android:checkable="true" />
 
     <item android:id="@+id/addons_top_level"
           android:title="@string/addons"
+          gecko:itemType="icon_menu_item"
           android:visible="false" />
 
     <item android:id="@+id/page"
           android:title="@string/page">
 
         <menu>
 
             <item android:id="@+id/subscribe"
--- a/mobile/android/app/src/main/res/menu-v26/browser_app_menu.xml
+++ b/mobile/android/app/src/main/res/menu-v26/browser_app_menu.xml
@@ -1,16 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <!-- We disable AlwaysShowAction because we interpret the menu
      attributes ourselves and thus the warning isn't relevant to us. -->
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:gecko="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools"
       tools:ignore="AlwaysShowAction">
 
     <item android:id="@+id/back"
           android:icon="@drawable/ic_menu_back"
           android:title="@string/back"
           android:showAsAction="always"/>
 
@@ -50,16 +51,17 @@
           android:title="@string/find_in_page" />
 
     <item android:id="@+id/desktop_mode"
           android:title="@string/desktop_mode"
           android:checkable="true" />
 
     <item android:id="@+id/addons_top_level"
           android:title="@string/addons"
+          gecko:itemType="icon_menu_item"
           android:visible="false" />
 
     <item android:id="@+id/page"
           android:title="@string/page">
 
         <menu>
 
             <item android:id="@+id/subscribe"
--- a/mobile/android/app/src/main/res/menu-xlarge-v26/browser_app_menu.xml
+++ b/mobile/android/app/src/main/res/menu-xlarge-v26/browser_app_menu.xml
@@ -1,16 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <!-- We disable AlwaysShowAction because we interpret the menu
      attributes ourselves and thus the warning isn't relevant to us. -->
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:gecko="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools"
       tools:ignore="AlwaysShowAction">
 
     <item android:id="@+id/reload"
           android:icon="@drawable/ic_menu_reload"
           android:title="@string/reload"
           android:showAsAction="always"/>
 
@@ -46,16 +47,17 @@
     <item android:id="@+id/history_list"
           android:title="@string/history_title"/>
 
     <item android:id="@+id/find_in_page"
           android:title="@string/find_in_page" />
 
     <item android:id="@+id/addons_top_level"
           android:title="@string/addons"
+          gecko:itemType="icon_menu_item"
           android:visible="false" />
 
     <item android:id="@+id/desktop_mode"
           android:title="@string/desktop_mode"
           android:checkable="true" />
 
 
     <item android:id="@+id/page"
--- a/mobile/android/app/src/main/res/menu-xlarge/browser_app_menu.xml
+++ b/mobile/android/app/src/main/res/menu-xlarge/browser_app_menu.xml
@@ -1,16 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <!-- We disable AlwaysShowAction because we interpret the menu
      attributes ourselves and thus the warning isn't relevant to us. -->
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:gecko="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools"
       tools:ignore="AlwaysShowAction">
 
     <item android:id="@+id/reload"
           android:icon="@drawable/ic_menu_reload"
           android:title="@string/reload"
           android:showAsAction="always"/>
 
@@ -46,16 +47,17 @@
     <item android:id="@+id/history_list"
           android:title="@string/history_title"/>
 
     <item android:id="@+id/find_in_page"
           android:title="@string/find_in_page" />
 
     <item android:id="@+id/addons_top_level"
           android:title="@string/addons"
+          gecko:itemType="icon_menu_item"
           android:visible="false" />
 
     <item android:id="@+id/desktop_mode"
           android:title="@string/desktop_mode"
           android:checkable="true" />
 
 
     <item android:id="@+id/page"
--- a/mobile/android/app/src/main/res/xml/preferences_advanced.xml
+++ b/mobile/android/app/src/main/res/xml/preferences_advanced.xml
@@ -73,21 +73,16 @@
                                                                 android:persistent="false"
                                                                 url="https://developer.mozilla.org/docs/Tools/Remote_Debugging/Debugging_Firefox_for_Android_with_WebIDE" />
     </PreferenceCategory>
 
     <PreferenceCategory
         android:key="android.not_a_preference.category_experimental"
         android:title="@string/pref_category_experimental">
 
-        <SwitchPreference android:key="android.not_a_preference.customtabs"
-            android:title="@string/pref_custom_tabs"
-            android:summary="@string/pref_custom_tabs_summary"
-            android:defaultValue="false" />
-
         <SwitchPreference android:key="android.not_a_preference.pwa"
             android:title="@string/pref_pwa"
             android:summary="@string/pref_pwa_summary"
             android:defaultValue="false" />
 
     </PreferenceCategory>
 
 </PreferenceScreen>
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -307,22 +307,20 @@
             <intent-filter>
                 <action android:name="android.intent.action.SEND" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="text/plain" />
             </intent-filter>
 
         </activity>
 
-#ifdef MOZ_ANDROID_CUSTOM_TABS
         <activity android:name="org.mozilla.gecko.customtabs.CustomTabsActivity"
                   android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|locale|layoutDirection|smallestScreenSize|screenLayout"
                   android:windowSoftInputMode="stateUnspecified|adjustResize"
                   android:theme="@style/GeckoCustomTabs" />
-#endif
 
         <activity android:name="org.mozilla.gecko.webapps.WebAppActivity"
             android:theme="@style/Theme.AppCompat.NoActionBar" />
 
         <!-- Declare a predefined number of WebApp<num> activities. These are
              used so that each web app can launch in its own activity. -->
 #define FRAGMENT WebAppManifestFragment.xml.frag.in
 #include WebAppFragmentRepeater.inc
@@ -399,25 +397,23 @@
                 <action android:name="android.intent.action.MY_PACKAGE_REPLACED"></action>
             </intent-filter>
         </receiver>
 
         <service
           android:name="org.mozilla.gecko.telemetry.TelemetryUploadService"
           android:exported="false"/>
 
-#ifdef MOZ_ANDROID_CUSTOM_TABS
         <service
             android:name="org.mozilla.gecko.customtabs.GeckoCustomTabsService"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.support.customtabs.action.CustomTabsService" />
             </intent-filter>
         </service>
-#endif
 
 #include ../services/manifests/FxAccountAndroidManifest_services.xml.in
 
         <service
             android:name="org.mozilla.gecko.tabqueue.TabReceivedService"
             android:exported="false" />
 
 
--- a/mobile/android/base/AppConstants.java.in
+++ b/mobile/android/base/AppConstants.java.in
@@ -300,23 +300,16 @@ public class AppConstants {
 
     public static final boolean MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE =
 //#ifdef MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE
     true;
 //#else
     false;
 //#endif
 
-    public static final boolean MOZ_ANDROID_CUSTOM_TABS =
-//#ifdef MOZ_ANDROID_CUSTOM_TABS
-    true;
-//#else
-    false;
-//#endif
-
     public static final boolean MOZ_ANDROID_PWA =
 //#ifdef MOZ_ANDROID_PWA
     true;
 //#else
     false;
 //#endif
 
     // (bug 1266820) Temporarily disabled since no one is working on it.
--- a/mobile/android/base/generate_build_config.py
+++ b/mobile/android/base/generate_build_config.py
@@ -34,17 +34,16 @@ from mozbuild.android_version_code impor
 def _defines():
     CONFIG = defaultdict(lambda: None)
     CONFIG.update(buildconfig.substs)
     DEFINES = dict(buildconfig.defines)
 
     for var in ('MOZ_ANDROID_ACTIVITY_STREAM'
                 'MOZ_ANDROID_ANR_REPORTER',
                 'MOZ_ANDROID_BEAM',
-                'MOZ_ANDROID_CUSTOM_TABS',
                 'MOZ_ANDROID_DOWNLOADS_INTEGRATION',
                 'MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE',
                 'MOZ_ANDROID_EXCLUDE_FONTS',
                 'MOZ_ANDROID_GCM',
                 'MOZ_ANDROID_MLS_STUMBLER',
                 'MOZ_ANDROID_MMA',
                 'MOZ_ANDROID_MOZILLA_ONLINE',
                 'MOZ_ANDROID_POCKET',
--- a/mobile/android/base/java/org/mozilla/gecko/AccountsHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/AccountsHelper.java
@@ -83,20 +83,16 @@ public class AccountsHelper implements B
             Log.e(LOGTAG, "Profile is not allowed to modify accounts!  Ignoring event: " + event);
             if (callback != null) {
                 callback.sendError("Profile is not allowed to modify accounts!");
             }
             return;
         }
 
         if ("Accounts:CreateFirefoxAccountFromJSON".equals(event)) {
-            // As we are about to create a new account, let's ensure our in-memory accounts cache
-            // is empty so that there are no undesired side-effects.
-            AndroidFxAccount.invalidateCaches();
-
             AndroidFxAccount fxAccount = null;
             try {
                 final GeckoBundle json = message.getBundle("json");
                 final String email = json.getString("email");
                 final String uid = json.getString("uid");
                 final boolean verified = json.getBoolean("verified", false);
                 final byte[] unwrapkB = Utils.hex2Byte(json.getString("unwrapBKey"));
                 final byte[] sessionToken = Utils.hex2Byte(json.getString("sessionToken"));
@@ -149,20 +145,16 @@ public class AccountsHelper implements B
                     return;
                 }
             }
             if (callback != null) {
                 callback.sendSuccess(fxAccount != null);
             }
 
         } else if ("Accounts:UpdateFirefoxAccountFromJSON".equals(event)) {
-            // We might be significantly changing state of the account; let's ensure our in-memory
-            // accounts cache is empty so that there are no undesired side-effects.
-            AndroidFxAccount.invalidateCaches();
-
             final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
             if (account == null) {
                 if (callback != null) {
                     callback.sendError("Could not update Firefox Account since none exists");
                 }
                 return;
             }
 
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -68,17 +68,16 @@ import android.widget.Button;
 import android.widget.ListView;
 import android.widget.ViewFlipper;
 
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.activitystream.ActivityStream;
 import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
-import org.mozilla.gecko.activitystream.homepanel.stream.StreamOverridablePageIconLayout;
 import org.mozilla.gecko.adjust.AdjustBrowserAppDelegate;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.bookmarks.BookmarkEditFragment;
 import org.mozilla.gecko.bookmarks.BookmarkUtils;
 import org.mozilla.gecko.bookmarks.EditBookmarkTask;
 import org.mozilla.gecko.cleanup.FileCleanupController;
 import org.mozilla.gecko.dawn.DawnHelper;
@@ -346,33 +345,25 @@ public class BrowserApp extends GeckoApp
             mTelemetryCorePingDelegate,
             new OfflineTabStatusDelegate(),
             new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate)
     ));
 
     @NonNull
     private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK!
 
-    // Ideally, we would set this cache from the specific places it is used in Activity Stream. However, given that
-    // it's unlikely that StreamOverridablePageIconLayout will be used elsewhere and how messy it is to pass references
-    // from an object with the application lifecycle to the individual views using the cache in activity stream, we settle
-    // for storing it here and setting it on all new instances.
-    private final Set<String> mStreamIconLayoutFailedRequestCache = StreamOverridablePageIconLayout.newFailedRequestCache();
-
     private boolean mHasResumed;
 
     @Override
-    public View onCreateView(final String name, final Context context, final AttributeSet attrs) {
+    public View onCreateView(final View parent, final String name, final Context context, final AttributeSet attrs) {
         final View view;
         if (BrowserToolbar.class.getName().equals(name)) {
             view = BrowserToolbar.create(context, attrs);
         } else if (TabsPanel.TabsLayout.class.getName().equals(name)) {
             view = TabsPanel.createTabsLayout(context, attrs);
-        } else if (StreamOverridablePageIconLayout.class.getName().equals(name)) {
-            view = new StreamOverridablePageIconLayout(context, attrs, mStreamIconLayoutFailedRequestCache);
         } else {
             view = super.onCreateView(name, context, attrs);
         }
         return view;
     }
 
     @Override
     public void onTabChanged(Tab tab, TabEvents msg, String data) {
--- a/mobile/android/base/java/org/mozilla/gecko/Experiments.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Experiments.java
@@ -49,16 +49,19 @@ public class Experiments {
     public static final String URLBAR_SHOW_EV_CERT_OWNER = "urlbar-show-ev-cert-owner";
 
     // Play HLS videos in a VideoView (Bug 1313391)
     public static final String HLS_VIDEO_PLAYBACK = "hls-video-playback";
 
     // Show AddOns menu-item in top level menu
     public static final String TOP_ADDONS_MENU = "top-addons-menu";
 
+    // User in this group will enable Custom Tabs
+    public static final String CUSTOM_TABS = "custom-tabs";
+
     // Enable full bookmark management(full-page dialog, bookmark/folder modification, etc.)
     public static final String FULL_BOOKMARK_MANAGEMENT = "full-bookmark-management";
 
     // Enable Leanplum SDK
     public static final String LEANPLUM = "leanplum-start";
 
     // Enable to use testing user id for Leanplum
     public static final String LEANPLUM_DEBUG = "leanplum-debug";
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -440,17 +440,17 @@ public abstract class GeckoApp extends G
     }
 
     @Override
     public MenuInflater getMenuInflater() {
         return new GeckoMenuInflater(this);
     }
 
     public MenuPanel getMenuPanel() {
-        if (mMenuPanel == null) {
+        if (mMenuPanel == null || mMenu == null) {
             onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, null);
             invalidateOptionsMenu();
         }
         return mMenuPanel;
     }
 
     @Override
     public boolean onMenuItemClick(MenuItem item) {
--- a/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java
+++ b/mobile/android/base/java/org/mozilla/gecko/LauncherActivity.java
@@ -1,24 +1,26 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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;
 
 import android.app.Activity;
+import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.customtabs.CustomTabsIntent;
 import android.util.Log;
 
 import org.mozilla.gecko.home.HomeConfig;
+import org.mozilla.gecko.switchboard.SwitchBoard;
 import org.mozilla.gecko.webapps.WebAppActivity;
 import org.mozilla.gecko.webapps.WebAppIndexer;
 import org.mozilla.gecko.customtabs.CustomTabsActivity;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.tabqueue.TabQueueService;
@@ -65,19 +67,17 @@ public class LauncherActivity extends Ac
         // Is this web app?
         } else if (isWebAppIntent(safeIntent)) {
             dispatchWebAppIntent();
 
         // If it's not a view intent, it won't be a custom tabs intent either. Just launch!
         } else if (!isViewIntentWithURL(safeIntent)) {
             dispatchNormalIntent();
 
-        // Is this a custom tabs intent, and are custom tabs enabled?
-        } else if (AppConstants.MOZ_ANDROID_CUSTOM_TABS && isCustomTabsIntent(safeIntent)
-                && isCustomTabsEnabled()) {
+        } else if (isCustomTabsIntent(safeIntent) && isCustomTabsEnabled(this) ) {
             dispatchCustomTabsIntent();
 
         // Can we dispatch this VIEW action intent to the tab queue service?
         } else if (!safeIntent.getBooleanExtra(BrowserContract.SKIP_TAB_QUEUE_FLAG, false)
                 && TabQueueHelper.TAB_QUEUE_ENABLED
                 && TabQueueHelper.isTabQueueEnabled(this)) {
             dispatchTabQueueIntent();
 
@@ -156,29 +156,29 @@ public class LauncherActivity extends Ac
         intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
     }
 
     private static boolean isViewIntentWithURL(@NonNull final SafeIntent safeIntent) {
         return Intent.ACTION_VIEW.equals(safeIntent.getAction())
                 && safeIntent.getDataString() != null;
     }
 
+    private static boolean isCustomTabsEnabled(@NonNull final Context context) {
+        return SwitchBoard.isInExperiment(context, Experiments.CUSTOM_TABS);
+    }
+
     private static boolean isCustomTabsIntent(@NonNull final SafeIntent safeIntent) {
         return isViewIntentWithURL(safeIntent)
                 && safeIntent.hasExtra(CustomTabsIntent.EXTRA_SESSION);
     }
 
     private static boolean isWebAppIntent(@NonNull final SafeIntent safeIntent) {
         return GeckoApp.ACTION_WEBAPP.equals(safeIntent.getAction());
     }
 
-    private boolean isCustomTabsEnabled() {
-        return GeckoSharedPrefs.forApp(this).getBoolean(GeckoPreferences.PREFS_CUSTOM_TABS, false);
-    }
-
     private static boolean isShutdownIntent(@NonNull final SafeIntent safeIntent) {
         return GeckoApp.ACTION_SHUTDOWN.equals(safeIntent.getAction());
     }
 
     private boolean isDeepLink(SafeIntent intent) {
         if (intent == null || intent.getData() == null || intent.getData().getScheme() == null
                 || intent.getAction() == null) {
             return false;
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/ActivityStreamHomeFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/ActivityStreamHomeFragment.java
@@ -1,33 +1,65 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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.activitystream.homepanel;
 
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.support.annotation.Nullable;
+import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
+import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.activitystream.ActivityStream;
 import org.mozilla.gecko.home.HomeFragment;
 
 /**
  * Simple wrapper around the ActivityStream view that allows embedding as a HomePager panel.
  */
 public class ActivityStreamHomeFragment
-        extends HomeFragment {
+        extends HomeFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
     private ActivityStreamPanel activityStreamPanel;
 
     private boolean isSessionActive;
+    private SharedPreferences sharedPreferences;
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        sharedPreferences = GeckoSharedPrefs.forProfile(getContext());
+        sharedPreferences.registerOnSharedPreferenceChangeListener(this);
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
+        final boolean shouldReload = TextUtils.equals(s, ActivityStreamPanel.PREF_BOOKMARKS_ENABLED)
+                || TextUtils.equals(s, ActivityStreamPanel.PREF_VISITED_ENABLED)
+                || TextUtils.equals(s, ActivityStreamPanel.PREF_POCKET_ENABLED);
+
+        if (shouldReload) {
+            activityStreamPanel.reload(getLoaderManager());
+        }
+
+    }
 
     @Override
     protected void load() {
         activityStreamPanel.load(getLoaderManager());
     }
 
     @Nullable
     @Override
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/ActivityStreamPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/ActivityStreamPanel.java
@@ -56,17 +56,17 @@ public class ActivityStreamPanel extends
 
     private int desiredTileWidth;
     private int tileMargin;
     private final SharedPreferences sharedPreferences;
 
     public ActivityStreamPanel(Context context, AttributeSet attrs) {
         super(context, attrs);
 
-        setBackgroundColor(ContextCompat.getColor(context, R.color.about_page_header_grey));
+        setBackgroundColor(ContextCompat.getColor(context, R.color.photon_browser_toolbar_bg));
 
         inflate(context, R.layout.as_content, this);
 
         adapter = new StreamRecyclerAdapter();
         sharedPreferences = GeckoSharedPrefs.forProfile(context);
 
         final RecyclerView rv = (RecyclerView) findViewById(R.id.activity_stream_main_recyclerview);
 
@@ -108,16 +108,26 @@ public class ActivityStreamPanel extends
     }
 
     public void unload() {
         adapter.swapHighlights(Collections.<Highlight>emptyList());
 
         adapter.swapTopSitesCursor(null);
     }
 
+    public void reload(LoaderManager lm) {
+        adapter.clearAndInit();
+
+        // Destroy loaders so they don't restart loading when returning.
+        lm.destroyLoader(LOADER_ID_HIGHLIGHTS);
+        lm.destroyLoader(LOADER_ID_POCKET);
+
+        load(lm);
+    }
+
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
 
         // This code does two things:
         //  * Calculate the size of the tiles we want to display (using a desired width as anchor point)
         //  * Add padding to the left/right side if there's too much space that we do not need
 
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/HighlightsDividerItemDecoration.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/HighlightsDividerItemDecoration.java
@@ -9,20 +9,22 @@ import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
 
 /**
- * ItemDecoration implementation that draws horizontal divider line between highlight items.
+ * ItemDecoration implementation that draws horizontal divider line between items
+ * in the AS newtab page.
  */
 /* package */ class HighlightsDividerItemDecoration extends RecyclerView.ItemDecoration {
-    // We do not want to draw a divider for the Top Sites panel.
+
+    // We do not want to draw a divider above the first item.
     private static final int START_DRAWING_AT_POSITION = 1;
 
     private static final int[] ATTRS = new int[]{
             android.R.attr.listDivider
     };
     private Drawable divider;
 
     /* package */ HighlightsDividerItemDecoration(Context context) {
@@ -43,21 +45,28 @@ import android.view.View;
             if (parent.getChildAdapterPosition(child) < START_DRAWING_AT_POSITION) {
                 continue;
             }
 
             if (child.getVisibility() == View.GONE) {
                 continue;
             }
 
+            // Do not draw dividers above section title items.
+            final int childViewType = parent.getAdapter().getItemViewType(i);
+            if (childViewType == StreamRecyclerAdapter.RowItemType.HIGHLIGHTS_TITLE.getViewType()
+                    || childViewType == StreamRecyclerAdapter.RowItemType.TOP_STORIES_TITLE.getViewType()) {
+                continue;
+            }
+
             final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                     .getLayoutParams();
-            final int top = child.getBottom() + params.bottomMargin;
-            final int bottom = top + divider.getIntrinsicHeight();
-            divider.setBounds(left, top, right, bottom);
+            final int topOfDivider = child.getTop() + params.topMargin;
+            final int bottomOfDivider = topOfDivider + divider.getIntrinsicHeight();
+            divider.setBounds(left, topOfDivider, right, bottomOfDivider);
             divider.draw(c);
         }
     }
 
     @Override
     public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
         outRect.set(0, 0, 0, divider.getIntrinsicHeight());
     }
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/StreamRecyclerAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/StreamRecyclerAdapter.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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.activitystream.homepanel;
 
+import android.content.Context;
 import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.support.annotation.NonNull;
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -89,16 +90,22 @@ public class StreamRecyclerAdapter exten
                 return type;
             }
         };
     }
 
     public StreamRecyclerAdapter() {
         setHasStableIds(true);
         recyclerViewModel = new LinkedList<>();
+
+        clearAndInit();
+    }
+
+    public void clearAndInit() {
+        recyclerViewModel.clear();
         for (RowItemType type : ACTIVITY_STREAM_SECTIONS) {
             recyclerViewModel.add(makeRowModelFromType(type));
         }
         topStoriesQueue = Collections.emptyList();
     }
 
     void setOnUrlOpenListeners(HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
         this.onUrlOpenListener = onUrlOpenListener;
@@ -120,27 +127,23 @@ public class StreamRecyclerAdapter exten
 
     @Override
     public StreamViewHolder onCreateViewHolder(ViewGroup parent, final int type) {
         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
 
         if (type == RowItemType.TOP_PANEL.getViewType()) {
             return new TopPanelRow(inflater.inflate(TopPanelRow.LAYOUT_ID, parent, false), onUrlOpenListener, onUrlOpenInBackgroundListener);
         } else if (type == RowItemType.TOP_STORIES_TITLE.getViewType()) {
-            final boolean pocketEnabled = GeckoSharedPrefs.forProfile(parent.getContext()).getBoolean(ActivityStreamPanel.PREF_POCKET_ENABLED, true);
-            return new StreamTitleRow(inflater.inflate(StreamTitleRow.LAYOUT_ID, parent, false), R.string.activity_stream_topstories, pocketEnabled, R.string.activity_stream_link_more, LINK_MORE_POCKET, onUrlOpenListener);
+            return new StreamTitleRow(inflater.inflate(StreamTitleRow.LAYOUT_ID, parent, false), R.string.activity_stream_topstories, R.string.activity_stream_link_more, LINK_MORE_POCKET, onUrlOpenListener);
         } else if (type == RowItemType.TOP_STORIES_ITEM.getViewType()) {
             return new WebpageItemRow(inflater.inflate(WebpageItemRow.LAYOUT_ID, parent, false), this);
         } else if (type == RowItemType.HIGHLIGHT_ITEM.getViewType()) {
             return new WebpageItemRow(inflater.inflate(WebpageItemRow.LAYOUT_ID, parent, false), this);
         } else if (type == RowItemType.HIGHLIGHTS_TITLE.getViewType()) {
-            final SharedPreferences sharedPreferences = GeckoSharedPrefs.forProfile(parent.getContext());
-            final boolean bookmarksEnabled = sharedPreferences.getBoolean(ActivityStreamPanel.PREF_BOOKMARKS_ENABLED, true);
-            final boolean visitedEnabled = sharedPreferences.getBoolean(ActivityStreamPanel.PREF_VISITED_ENABLED, true);
-            return new StreamTitleRow(inflater.inflate(StreamTitleRow.LAYOUT_ID, parent, false), R.string.activity_stream_highlights, bookmarksEnabled || visitedEnabled);
+            return new StreamTitleRow(inflater.inflate(StreamTitleRow.LAYOUT_ID, parent, false), R.string.activity_stream_highlights);
         } else if (type == RowItemType.HIGHLIGHTS_EMPTY_STATE.getViewType()) {
             return new HighlightsEmptyStateRow(inflater.inflate(HighlightsEmptyStateRow.LAYOUT_ID, parent, false));
         } else {
             throw new IllegalStateException("Missing inflation for ViewType " + type);
         }
     }
 
     /**
@@ -173,19 +176,62 @@ public class StreamRecyclerAdapter exten
         if (type == RowItemType.HIGHLIGHT_ITEM.getViewType()) {
             final Highlight highlight = (Highlight) recyclerViewModel.get(position);
             ((WebpageItemRow) holder).bind(highlight, position, tilesSize);
         } else if (type == RowItemType.TOP_PANEL.getViewType()) {
             ((TopPanelRow) holder).bind(topSitesCursor, tilesSize);
         } else if (type == RowItemType.TOP_STORIES_ITEM.getViewType()) {
             final TopStory story = (TopStory) recyclerViewModel.get(position);
             ((WebpageItemRow) holder).bind(story, position, tilesSize);
+        } else if (type == RowItemType.HIGHLIGHTS_TITLE.getViewType()
+                || type == RowItemType.HIGHLIGHTS_EMPTY_STATE.getViewType()) {
+            final Context context = holder.itemView.getContext();
+            final SharedPreferences sharedPreferences = GeckoSharedPrefs.forProfile(context);
+            final boolean bookmarksEnabled = sharedPreferences.getBoolean(ActivityStreamPanel.PREF_BOOKMARKS_ENABLED,
+                    context.getResources().getBoolean(R.bool.pref_activitystream_recentbookmarks_enabled_default));
+            final boolean visitedEnabled = sharedPreferences.getBoolean(ActivityStreamPanel.PREF_VISITED_ENABLED,
+                    context.getResources().getBoolean(R.bool.pref_activitystream_visited_enabled_default));
+            setViewVisible(bookmarksEnabled || visitedEnabled, holder.itemView);
+        } else if (type == RowItemType.TOP_STORIES_TITLE.getViewType()) {
+            final Context context = holder.itemView.getContext();
+            final boolean pocketEnabled = GeckoSharedPrefs.forProfile(context).getBoolean(ActivityStreamPanel.PREF_POCKET_ENABLED,
+                    context.getResources().getBoolean(R.bool.pref_activitystream_pocket_enabled_default));
+            setViewVisible(pocketEnabled, holder.itemView);
         }
     }
 
+    /**
+     * This sets a child view of the adapter visible or hidden.
+     *
+     * This only applies to children whose height and width are WRAP_CONTENT and MATCH_PARENT
+     * respectively.
+     *
+     * NB: This is a hack for the views that are included in the RecyclerView adapter even if
+     * they shouldn't be shown, such as the section title views or the empty view for highlights.
+     *
+     * A more correct implementation would dynamically add/remove these title views rather than
+     * showing and hiding them.
+     *
+     * @param toShow true if the view is to be shown, false to be hidden
+     * @param view child View whose visibility is to be changed
+     */
+    private static void setViewVisible(boolean toShow, final View view) {
+        view.setVisibility(toShow ? View.VISIBLE : View.GONE);
+        // We also need to set the layout height and width to 0 for the RecyclerView child.
+        final RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view.getLayoutParams();
+        if (toShow) {
+            layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT;
+            layoutParams.width = RecyclerView.LayoutParams.MATCH_PARENT;
+        } else {
+            layoutParams.height = 0;
+            layoutParams.width = 0;
+        }
+        view.setLayoutParams(layoutParams);
+    }
+
     @Override
     public void onItemClicked(RecyclerView recyclerView, int position, View v) {
         if (!onItemClickIsValidRowItem(position)) {
             return;
         }
 
         final WebpageRowModel model = (WebpageRowModel) recyclerViewModel.get(position);
 
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/stream/StreamOverridablePageIconLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/stream/StreamOverridablePageIconLayout.java
@@ -53,42 +53,29 @@ public class StreamOverridablePageIconLa
 
     /**
      * A cache of URLs that Picasso has failed to load. Picasso will not cache which URLs it has failed to load so
      * this is used to prevent Picasso from making additional requests to failed URLs, which is useful when the
      * given URL does not contain an image.
      *
      * Picasso unfortunately does not implement this functionality: https://github.com/square/picasso/issues/475
      *
-     * A single cache should be shared amongst all interchangeable views (e.g. in a RecyclerView) but could be
-     * shared across the app too.
-     *
      * The consequences of not having highlight images and making requests each time the app is loaded are small,
      * so we keep this cache in memory only.
-     */
-    private final @NonNull Set<String> nonFaviconFailedRequestURLs;
-
-    /**
-     * Create a new cache of failed requests non-favicon for use in
-     * {@link StreamOverridablePageIconLayout(Context, AttributeSet, Set)}.
+     *
+     * HACK: this cache is static because it's messy to create a single instance of this cache and pass it to all
+     * relevant instances at construction time. The downside of being static is that 1) the lifecycle of the cache
+     * no longer related to the Activity and 2) *all* instances share the same cache. The original implementation
+     * fixed these problems by overriding Activity.onCreateView and passing in a single instance to the cache there,
+     * but it crashed on Android O.
      */
-    public static Set<String> newFailedRequestCache() {
-        // To keep things simple and safe, we make this thread safe.
-        return Collections.synchronizedSet(new HashSet<String>());
-    }
+    private final static Set<String> nonFaviconFailedRequestURLs = Collections.synchronizedSet(new HashSet<String>());
 
-    /**
-     * @param nonFaviconFailedRequestCache a cache created by {@link #newFailedRequestCache()} - see that for details.
-     */
-    public StreamOverridablePageIconLayout(final Context context, final AttributeSet attrs,
-            @NonNull final Set<String> nonFaviconFailedRequestCache) {
+    public StreamOverridablePageIconLayout(final Context context, final AttributeSet attrs) {
         super(context, attrs);
-        if (nonFaviconFailedRequestCache == null) { throw new IllegalArgumentException("Expected non-null request cache"); }
-        this.nonFaviconFailedRequestURLs = nonFaviconFailedRequestCache;
-
         LayoutInflater.from(context).inflate(R.layout.activity_stream_overridable_page_icon_layout, this, true);
         initViews();
     }
 
     /**
      * Updates the icon for the view. If a non-null overrideImageURL is provided, this image will be shown.
      * Otherwise, a favicon will be retrieved for the given pageURL.
      */
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/stream/StreamTitleRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/stream/StreamTitleRow.java
@@ -2,42 +2,38 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.activitystream.homepanel.stream;
 
 import android.support.annotation.NonNull;
 import android.support.annotation.StringRes;
-import android.support.v7.widget.RecyclerView;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.TextView;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.util.DrawableUtil;
 
 import java.util.EnumSet;
 
 public class StreamTitleRow extends StreamViewHolder {
     public static final int LAYOUT_ID = R.layout.activity_stream_main_highlightstitle;
 
-    public StreamTitleRow(final View itemView, final @StringRes @NonNull int titleResId, boolean isEnabled) {
+    public StreamTitleRow(final View itemView, final @StringRes @NonNull int titleResId) {
         super(itemView);
         final TextView titleView = (TextView) itemView.findViewById(R.id.title_highlights);
         titleView.setText(titleResId);
-        if (!isEnabled) {
-            hideView(itemView);
-        }
     }
 
-    public StreamTitleRow(final View itemView, final @StringRes @NonNull int titleResId, boolean isEnabled,
+    public StreamTitleRow(final View itemView, final @StringRes @NonNull int titleResId,
                           final @StringRes int linkTitleResId, final String url, final HomePager.OnUrlOpenListener onUrlOpenListener) {
-        this(itemView, titleResId, isEnabled);
+        this(itemView, titleResId);
         // Android 21+ is needed to set RTL-aware compound drawables, so we use a tinted ImageView here.
         final TextView titleLink = (TextView) itemView.findViewById(R.id.title_link);
         titleLink.setVisibility(View.VISIBLE);
         titleLink.setText(linkTitleResId);
 
         final ImageView titleArrow = (ImageView) itemView.findViewById(R.id.arrow_link);
         titleArrow.setImageDrawable(DrawableUtil.tintDrawableWithColorRes(itemView.getContext(), R.drawable.menu_item_more, R.color.ob_click));
         titleArrow.setVisibility(View.VISIBLE);
@@ -47,20 +43,10 @@ public class StreamTitleRow extends Stre
             public void onClick(View view) {
                 onUrlOpenListener.onUrlOpen(url, EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
             }
         };
 
         titleLink.setOnClickListener(clickListener);
         titleArrow.setOnClickListener(clickListener);
     }
-
-    private static void hideView(final View itemView) {
-        itemView.setVisibility(View.GONE);
-        // We also need to set the layout height, width, and margins to 0 for the RecyclerView child.
-        final RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) itemView.getLayoutParams();
-        layoutParams.setMargins(0, 0, 0, 0);
-        layoutParams.height = 0;
-        layoutParams.width = 0;
-        itemView.setLayoutParams(layoutParams);
-    }
 }
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
@@ -629,17 +629,17 @@ public class CombinedHistoryPanel extend
         }
     }
 
     protected class RemoteTabsRefreshListener implements SwipeRefreshLayout.OnRefreshListener {
         @Override
         public void onRefresh() {
             if (FirefoxAccounts.firefoxAccountsExist(getActivity())) {
                 final Account account = FirefoxAccounts.getFirefoxAccount(getActivity());
-                FirefoxAccounts.requestImmediateSync(account, STAGES_TO_SYNC_ON_REFRESH, null);
+                FirefoxAccounts.requestImmediateSync(account, STAGES_TO_SYNC_ON_REFRESH, null, true);
             } else {
                 Log.wtf(LOGTAG, "No Firefox Account found; this should never happen. Ignoring.");
                 mRefreshLayout.setRefreshing(false);
             }
         }
     }
 
     protected class RemoteTabsSyncListener implements SyncStatusListener {
--- a/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java
+++ b/mobile/android/base/java/org/mozilla/gecko/overlays/service/sharemethods/SendTab.java
@@ -288,12 +288,12 @@ public class SendTab extends ShareMethod
             } catch (Exception e) {
                 Log.w(LOGTAG, "Could not get Firefox Account parameters or preferences; aborting.");
                 return null;
             }
         }
 
         @Override
         public void sync() {
-            fxAccount.requestImmediateSync(STAGES_TO_SYNC, null);
+            fxAccount.requestImmediateSync(STAGES_TO_SYNC, null, true);
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -144,17 +144,16 @@ public class GeckoPreferences
     public static final String PREFS_HOMEPAGE_PARTNER_COPY = GeckoPreferences.PREFS_HOMEPAGE + ".partner";
     public static final String PREFS_HISTORY_SAVED_SEARCH = NON_PREF_PREFIX + "search.search_history.enabled";
     private static final String PREFS_FAQ_LINK = NON_PREF_PREFIX + "faq.link";
     private static final String PREFS_FEEDBACK_LINK = NON_PREF_PREFIX + "feedback.link";
     public static final String PREFS_NOTIFICATIONS_WHATS_NEW = NON_PREF_PREFIX + "notifications.whats_new";
     public static final String PREFS_APP_UPDATE_LAST_BUILD_ID = "app.update.last_build_id";
     public static final String PREFS_READ_PARTNER_CUSTOMIZATIONS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_customizations_provider";
     public static final String PREFS_READ_PARTNER_BOOKMARKS_PROVIDER = NON_PREF_PREFIX + "distribution.read_partner_bookmarks_provider";
-    public static final String PREFS_CUSTOM_TABS = NON_PREF_PREFIX + "customtabs";
     public static final String PREFS_PWA = NON_PREF_PREFIX + "pwa";
     public static final String PREFS_CATEGORY_EXPERIMENTAL_FEATURES = NON_PREF_PREFIX + "category_experimental";
     public static final String PREFS_COMPACT_TABS = NON_PREF_PREFIX + "compact_tabs";
     public static final String PREFS_SHOW_QUIT_MENU = NON_PREF_PREFIX + "distribution.show_quit_menu";
     public static final String PREFS_SEARCH_SUGGESTIONS_ENABLED = "browser.search.suggest.enabled";
     public static final String PREFS_DEFAULT_BROWSER = NON_PREF_PREFIX + "default_browser.link";
     public static final String PREFS_SYSTEM_FONT_SIZE = NON_PREF_PREFIX + "font.size.use_system_font_size";
     public static final String PREFS_SET_AS_HOMEPAGE = NON_PREF_PREFIX + "distribution.set_as_homepage";
@@ -641,18 +640,17 @@ public class GeckoPreferences
                         continue;
                     }
                 } else if (PREFS_SCREEN_ADVANCED.equals(key) &&
                         !Restrictions.isAllowed(this, Restrictable.ADVANCED_SETTINGS)) {
                     preferences.removePreference(pref);
                     i--;
                     continue;
                 } else if (PREFS_CATEGORY_EXPERIMENTAL_FEATURES.equals(key)
-                        && !AppConstants.MOZ_ANDROID_PWA
-                        && !AppConstants.MOZ_ANDROID_CUSTOM_TABS) {
+                        && !AppConstants.MOZ_ANDROID_PWA) {
                     preferences.removePreference(pref);
                     i--;
                     continue;
                 }
                 setupPreferences((PreferenceGroup) pref, prefs);
             } else {
                 if (HANDLERS.containsKey(key)) {
                     PrefHandler handler = HANDLERS.get(key);
@@ -805,20 +803,16 @@ public class GeckoPreferences
                     final String url = getResources().getString(R.string.feedback_link, AppConstants.MOZ_APP_VERSION, AppConstants.MOZ_UPDATE_CHANNEL);
                     ((LinkPreference) pref).setUrl(url);
                 } else if (PREFS_DYNAMIC_TOOLBAR.equals(key)) {
                     if (DynamicToolbar.isForceDisabled()) {
                         preferences.removePreference(pref);
                         i--;
                         continue;
                     }
-                } else if (PREFS_CUSTOM_TABS.equals(key) && !AppConstants.MOZ_ANDROID_CUSTOM_TABS) {
-                    preferences.removePreference(pref);
-                    i--;
-                    continue;
                 } else if (PREFS_PWA.equals(key) && !AppConstants.MOZ_ANDROID_PWA) {
                     preferences.removePreference(pref);
                     i--;
                     continue;
                 } else if (PREFS_COMPACT_TABS.equals(key)) {
                     if (HardwareUtils.isTablet()) {
                         preferences.removePreference(pref);
                         i--;
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
@@ -490,17 +490,17 @@ public class SiteIdentityPopup extends A
         addSelectLoginDoorhanger(selectedTab);
 
         if (mSiteIdentity.getSecurityMode() == SecurityMode.CHROMEUI) {
             // For about: pages we display the product icon in place of the verified/globe
             // image, hence we don't also set the favicon (for most about pages the
             // favicon is the product icon, hence we'd be showing the same icon twice).
             mTitle.setText(R.string.moz_app_displayname);
         } else {
-            mTitle.setText(selectedTab.getBaseDomain());
+            mTitle.setText(mSiteIdentity.getHost());
 
             final Bitmap favicon = selectedTab.getFavicon();
             if (favicon != null) {
                 final Drawable faviconDrawable = new BitmapDrawable(mResources, favicon);
                 final int dimen = (int) mResources.getDimension(R.dimen.browser_toolbar_favicon_size);
                 faviconDrawable.setBounds(0, 0, dimen, dimen);
 
                 TextViewCompat.setCompoundDrawablesRelative(mTitle, faviconDrawable, null, null, null);
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -280,21 +280,16 @@
 
 <!ENTITY pref_whats_new_notification "What\'s new in &brandShortName;">
 <!ENTITY pref_whats_new_notification_summary "Learn about new features after an update">
 
 <!-- Localization note (pref_category_experimental): Title of a sub category in the 'advanced' category
      for experimental features. -->
 <!ENTITY pref_category_experimental "Experimental features">
 
-<!-- Custom Tabs is an Android API for allowing third-party apps to open URLs in a customized UI.
-     Instead of switching to the browser it appears as if the user stays in the third-party app.
-     For more see: https://developer.chrome.com/multidevice/android/customtabs -->
-<!ENTITY pref_custom_tabs "Custom Tabs">
-<!ENTITY pref_custom_tabs_summary3 "Allow apps to open websites using a customized version of &brandShortName;">
 <!-- Localization note (custom_tabs_menu_item_open_in): The variable is replaced by the name of
      default browser from user's preference, such as "Open in Firefox" -->
 <!ENTITY custom_tabs_menu_item_open_in "Open in &formatS;">
 <!ENTITY custom_tabs_menu_footer "Powered by &brandShortName;">
 <!-- Long-click title of CustomTabsActivity will copy URL to clipboard and display this hint -->
 <!ENTITY custom_tabs_hint_url_copy "URL copied">
 
 <!ENTITY pref_pwa "Progressive Web Apps">
@@ -826,18 +821,18 @@ just addresses the organization to follo
 <!ENTITY helper_triple_readerview_open_message "Bookmark Reader View items to read them offline.">
 <!ENTITY helper_triple_readerview_open_button "Add to Bookmarks">
 
 <!ENTITY activity_stream_topsites "Top Sites">
 <!-- LOCALIZATION NOTE (activity_stream_topstories): &brandPocket is the brand of the company, Pocket, that is being used to provide suggestions for articles. -->
 <!ENTITY activity_stream_topstories "Recommended by &brandPocket;">
 <!ENTITY activity_stream_highlights "Highlights">
 
-<!-- LOCALIZATION NOET (activity_stream_link_more): Link-like text displayed to take user to a website with more content from Pocket. This string will be displayed in all uppercase. -->
-<!ENTITY activity_stream_link_more "more">
+<!-- LOCALIZATION NOTE (activity_stream_link_more1): Link-like text displayed to take user to a website with more content from Pocket. -->
+<!ENTITY activity_stream_link_more1 "MORE">
 
 <!-- LOCALIZATION NOTE (activity_stream_highlight_label_bookmarked): This label is shown in the Activity
 Stream list for highlights sourced from th user's bookmarks. -->
 <!ENTITY activity_stream_highlight_label_bookmarked "Bookmarked">
 <!-- LOCALIZATION NOTE (activity_stream_highlight_label_visited): This label is shown in the Activity
 Stream list for highlights sourced from th user's bookmarks. -->
 <!ENTITY activity_stream_highlight_label_visited "Visited">
 <!-- LOCALIZATION NOTE (activity_stream_highlight_label_trending): This label is shown in the Activity Stream list for highlights sourced from a recommendations engine. -->
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -223,18 +223,16 @@
   <string name="pref_tracking_protection_enabled_pb">&pref_tracking_protection_enabled_pb;</string>
   <string name="pref_tracking_protection_disabled">&pref_tracking_protection_disabled;</string>
 
   <string name="pref_whats_new_notification">&pref_whats_new_notification;</string>
   <string name="pref_whats_new_notification_summary">&pref_whats_new_notification_summary;</string>
 
   <string name="pref_category_experimental">&pref_category_experimental;</string>
 
-  <string name="pref_custom_tabs">&pref_custom_tabs;</string>
-  <string name="pref_custom_tabs_summary">&pref_custom_tabs_summary3;</string>
   <string name="custom_tabs_menu_item_open_in">&custom_tabs_menu_item_open_in;</string>
   <string name="custom_tabs_menu_footer">&custom_tabs_menu_footer;</string>
   <string name="custom_tabs_hint_url_copy">&custom_tabs_hint_url_copy;</string>
 
   <string name="pref_pwa">&pref_pwa;</string>
   <string name="pref_pwa_summary">&pref_pwa_summary;</string>
 
   <string name="pref_char_encoding">&pref_char_encoding;</string>
@@ -619,17 +617,17 @@
 
   <string name="helper_triple_readerview_open_title">&helper_triple_readerview_open_title;</string>
   <string name="helper_triple_readerview_open_message">&helper_triple_readerview_open_message;</string>
   <string name="helper_triple_readerview_open_button">&helper_triple_readerview_open_button;</string>
 
   <string name="activity_stream_topsites">&activity_stream_topsites;</string>
   <string name="activity_stream_topstories">&activity_stream_topstories;</string>
   <string name="activity_stream_highlights">&activity_stream_highlights;</string>
-  <string name="activity_stream_link_more">&activity_stream_link_more;</string>
+  <string name="activity_stream_link_more">&activity_stream_link_more1;</string>
 
   <string name="activity_stream_highlight_label_bookmarked">&activity_stream_highlight_label_bookmarked;</string>
   <string name="activity_stream_highlight_label_visited">&activity_stream_highlight_label_visited;</string>
   <string name="activity_stream_highlight_label_trending">&activity_stream_highlight_label_trending;</string>
   <string name="activity_stream_remove">&activity_stream_remove;</string>
   <string name="activity_stream_delete_history">&activity_stream_delete_history;</string>
 
   <string name="activity_stream_highlights_empty">&activity_stream_highlights_empty;</string>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1208,22 +1208,16 @@ var BrowserApp = {
     } else {
       this._tabs.push(newTab);
     }
 
     let selected = "selected" in aParams ? aParams.selected : true;
     if (selected)
       this.selectedTab = newTab;
 
-    let pinned = "pinned" in aParams ? aParams.pinned : false;
-    if (pinned) {
-      let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
-      ss.setTabValue(newTab, "appOrigin", aURI);
-    }
-
     let evt = document.createEvent("UIEvents");
     evt.initUIEvent("TabOpen", true, false, window, null);
     newTab.browser.dispatchEvent(evt);
 
     return newTab;
   },
 
   // Use this method to close a tab from JS. This method sends a message
@@ -1347,17 +1341,17 @@ var BrowserApp = {
    * @return the tab with the given URL, or null if no such tab exists
    */
   getTabWithURL: function getTabWithURL(aURL, aOptions) {
     aOptions = aOptions || {};
     let uri = Services.io.newURI(aURL);
     for (let i = 0; i < this._tabs.length; ++i) {
       let tab = this._tabs[i];
       if (aOptions.startsWith) {
-        if (tab.currentURI.spec.startsWith(aURL)) {
+        if (tab.currentURI.spec.startsWith(uri.spec)) {
           return tab;
         }
       } else {
         if (tab.currentURI.equals(uri)) {
           return tab;
         }
       }
     }
@@ -2204,18 +2198,18 @@ var BrowserApp = {
       };
     }
 
     let browser = this.selectedBrowser;
     let hist = browser.sessionHistory;
     for (let i = toIndex; i >= fromIndex; i--) {
       let entry = hist.getEntryAtIndex(i, false);
       let item = {
-        title: entry.title || entry.URI.spec,
-        url: entry.URI.spec,
+        title: entry.title || entry.URI.displaySpec,
+        url: entry.URI.displaySpec,
         selected: (i == selIndex)
       };
       listitems.push(item);
     }
 
     return {
       "historyItems": listitems,
       "toIndex": toIndex
@@ -2830,17 +2824,17 @@ var NativeWindow = {
           (node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) {
         return this._getLinkURL(node);
       } else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) {
         // The image is blocked by Tap-to-load Images
         let originalURL = node.getAttribute("data-ctv-src");
         if (originalURL) {
           return originalURL;
         }
-        return node.currentURI.spec;
+        return node.currentURI.displaySpec;
       } else if (node instanceof Ci.nsIDOMHTMLMediaElement) {
         let srcUrl = node.currentSrc || node.src;
         // If URL prepended with blob or mediasource, we'll remove it.
         return srcUrl.replace(/^(?:blob|mediasource):/, '');
       }
 
       return "";
     },
@@ -3043,17 +3037,17 @@ var NativeWindow = {
         }
         target = target.parentNode;
       }
     },
 
     // XXX - These are stolen from Util.js, we should remove them if we bring it back
     makeURLAbsolute: function makeURLAbsolute(base, url) {
       // Note:  makeURI() will throw if url is not a valid URI
-      return this.makeURI(url, null, this.makeURI(base)).spec;
+      return this.makeURI(url, null, this.makeURI(base)).displaySpec;
     },
 
     makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
       return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
     },
 
     _getLink: function(aElement) {
       if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
@@ -3078,18 +3072,19 @@ var NativeWindow = {
 
           return selector.matches(aElement, aX, aY);
         }
       };
     },
 
     _getLinkURL: function ch_getLinkURL(aLink) {
       let href = aLink.href;
-      if (href)
-        return href;
+      if (href) {
+        return this.makeURI(href).displaySpec;
+      }
 
       href = aLink.getAttribute("href") ||
              aLink.getAttributeNS(kXLinkNamespace, "href");
       if (!href || !href.match(/\S/)) {
         // Without this we try to save as the current doc,
         // for example, HTML case also throws if empty
         throw "Empty href";
       }
@@ -3200,17 +3195,19 @@ var LightWeightThemeWebInstaller = {
       return;
 
     if (this._isAllowed(node)) {
       this._install(data);
       return;
     }
 
     let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton");
-    let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1);
+    let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
+    let hostname = IDNService.convertToDisplayIDN(node.ownerDocument.location.hostname);
+    let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [hostname], 1);
     let buttons = [{
       label: allowButtonText,
       callback: function () {
         LightWeightThemeWebInstaller._install(data);
       },
       positive: true
     }];
 
@@ -3382,17 +3379,17 @@ nsBrowserAccess.prototype = {
       } catch(e) { }
     }
 
     let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
     let pinned = false;
 
     if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) {
       pinned = true;
-      let spec = aURI.spec;
+      let spec = aURI.displaySpec;
       let tabs = BrowserApp.tabs;
       for (let i = 0; i < tabs.length; i++) {
         let appOrigin = ss.getTabValue(tabs[i], "appOrigin");
         if (appOrigin == spec) {
           let tab = tabs[i];
           BrowserApp.selectTab(tab);
           return tab.browser;
         }
@@ -3577,17 +3574,17 @@ Tab.prototype = {
     }
 
     this.browser.stop();
 
     // Only set tab uri if uri is valid
     let uri = null;
     let title = aParams.title || aURL;
     try {
-      uri = Services.io.newURI(aURL).spec;
+      uri = Services.io.newURI(aURL).displaySpec;
     } catch (e) {}
 
     // When the tab is stubbed from Java, there's a window between the stub
     // creation and the tab creation in Gecko where the stub could be removed
     // or the selected tab can change (which is easiest to hit during startup).
     // To prevent these races, we need to differentiate between tab stubs from
     // Java and new tabs from Gecko.
     let stub = false;
@@ -3660,26 +3657,32 @@ Tab.prototype = {
     Services.obs.addObserver(this, "audioFocusChanged", false);
     Services.obs.addObserver(this, "before-first-paint");
     Services.obs.addObserver(this, "media-playback");
 
     // Always initialise new tabs with basic session store data to avoid
     // problems with functions that always expect it to be present
     this.browser.__SS_data = {
       entries: [{
-        url: aURL,
+        url: uri,
         title: truncate(title, MAX_TITLE_LENGTH)
       }],
       index: 1,
       desktopMode: this.desktopMode,
       isPrivate: isPrivate,
       tabId: this.id,
       parentId: this.parentId
     };
 
+    let pinned = "pinned" in aParams ? aParams.pinned : false;
+    if (pinned && uri) {
+      let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+      ss.setTabValue(this, "appOrigin", uri);
+    }
+
     if (aParams.delayLoad) {
       // If this is a zombie tab, mark the browser for delay loading, which will
       // restore the tab when selected using the session data added above
       this.browser.__SS_restore = true;
       this.browser.setAttribute("pending", "true");
     } else {
       let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
       let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null;
@@ -4357,17 +4360,17 @@ Tab.prototype = {
       // true if the page loaded successfully (i.e., no 404s or other errors)
       let success = false;
       let uri = "";
       try {
         // Remember original URI for UA changes on redirected pages
         this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI;
 
         if (this.originalURI != null)
-          uri = this.originalURI.spec;
+          uri = this.originalURI.displaySpec;
       } catch (e) { }
       try {
         success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded;
       } catch (e) {
         // If the request does not handle the nsIHttpChannel interface, use nsIRequest's success
         // status. Used for local files. See bug 948849.
         success = aRequest.status == 0;
       }
@@ -4437,20 +4440,20 @@ Tab.prototype = {
     // This mirrors the desktop logic in TabsProgressListener.
     if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
       this.browser.messageManager.sendAsyncMessage("Reader:PushState", {isArticle: this.browser.isArticle});
     }
 
     let baseDomain = "";
     // For recognized scheme, get base domain from host.
     let principalURI = contentWin.document.nodePrincipal.URI;
-    if (principalURI && ["http", "https", "ftp"].includes(principalURI.scheme) && principalURI.host) {
+    if (principalURI && ["http", "https", "ftp"].includes(principalURI.scheme) && principalURI.displayHost) {
       try {
-        baseDomain = Services.eTLD.getBaseDomainFromHost(principalURI.host);
-        if (!principalURI.host.endsWith(baseDomain)) {
+        baseDomain = Services.eTLD.getBaseDomainFromHost(principalURI.displayHost);
+        if (!principalURI.displayHost.endsWith(baseDomain)) {
           // getBaseDomainFromHost converts its resultant to ACE.
           let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
           baseDomain = IDNService.convertACEtoUTF8(baseDomain);
         }
       } catch (e) {}
     }
 
     // If we are navigating to a new location with a different host,
@@ -4474,17 +4477,17 @@ Tab.prototype = {
     // Update the page actions URI for helper apps.
     if (BrowserApp.selectedTab == this) {
       ExternalApps.updatePageActionUri(fixedURI);
     }
 
     let message = {
       type: "Content:LocationChange",
       tabID: this.id,
-      uri: truncate(fixedURI.spec, MAX_URI_LENGTH),
+      uri: truncate(fixedURI.displaySpec, MAX_URI_LENGTH),
       userRequested: this.userRequested || "",
       baseDomain: baseDomain,
       contentType: (contentType ? contentType : ""),
       sameDocument: sameDocument,
 
       canGoBack: webNav.canGoBack,
       canGoForward: webNav.canGoForward,
     };
@@ -4906,17 +4909,17 @@ var XPInstallObserver = {
   },
 
   observe: function(aSubject, aTopic, aData) {
     let installInfo, tab, host;
     if (aSubject && aSubject.wrappedJSObject) {
       installInfo = aSubject.wrappedJSObject;
       tab = BrowserApp.getTabForBrowser(installInfo.browser);
       if (installInfo.originatingURI) {
-        host = installInfo.originatingURI.host;
+        host = installInfo.originatingURI.displayHost;
       }
     }
 
     let strings = Strings.browser;
     let brandShortName = Strings.brand.GetStringFromName("brandShortName");
 
     switch (aTopic) {
       case "addon-install-started":
@@ -5089,19 +5092,19 @@ var XPInstallObserver = {
   _showErrorMessage: function(aInstall) {
     // Don't create a notification for distribution add-ons.
     if (Distribution.pendingAddonInstalls.has(aInstall)) {
       Cu.reportError("Error installing distribution add-on: " + aInstall.addon.id);
       Distribution.pendingAddonInstalls.delete(aInstall);
       return;
     }
 
-    let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host;
+    let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.displayHost;
     if (!host) {
-      host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host;
+      host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.displayHost;
     }
 
     let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError";
     if (aInstall.error < 0) {
       error += aInstall.error;
     } else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
       error += "Blocklisted";
     } else {
@@ -5584,51 +5587,51 @@ var IdentityHandler = {
       locationObj.origin = location.origin;
     } catch (ex) {
       // Can sometimes throw if the URL being visited has no host/hostname,
       // e.g. about:blank. The _state for these pages means we won't need these
       // properties anyways, though.
     }
     this._lastLocation = locationObj;
 
-    let uri = aBrowser.currentURI;
+    this._uri = aBrowser.currentURI;
     try {
-      uri = Services.uriFixup.createExposableURI(uri);
+      this._uri = Services.uriFixup.createExposableURI(this._uri);
     } catch (e) {}
 
-    let identityMode = this.getIdentityMode(aState, uri);
+    let identityMode = this.getIdentityMode(aState, this._uri);
     let mixedDisplay = this.getMixedDisplayMode(aState);
     let mixedActive = this.getMixedActiveMode(aState);
     let trackingMode = this.getTrackingMode(aState, aBrowser);
     let result = {
       origin: locationObj.origin,
       mode: {
         identity: identityMode,
         mixed_display: mixedDisplay,
         mixed_active: mixedActive,
         tracking: trackingMode
       }
     };
 
+    result.host = this.getEffectiveHost();
+
     // Don't show identity data for pages with an unknown identity or if any
     // mixed content is loaded (mixed display content is loaded by default).
     // We also return for CHROMEUI pages since they don't have any certificate
     // information to load either. result.secure specifically refers to connection
     // security, which is irrelevant for about: pages, as they're loaded locally.
     if (identityMode == this.IDENTITY_MODE_UNKNOWN ||
         identityMode == this.IDENTITY_MODE_CHROMEUI ||
         aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
       result.secure = false;
       return result;
     }
 
     result.secure = true;
 
-    result.host = this.getEffectiveHost();
-
     let iData = this.getIdentityData();
     result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
 
     // If the cert is identified, then we can populate the results with credentials
     if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
       result.owner = iData.subjectOrg;
 
       // Build an appropriate supplemental block out of whatever location data we have
@@ -5971,17 +5974,17 @@ var SearchEngines = {
     let charset = aElement.ownerDocument.characterSet;
     let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset);
     let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
     let method = form.method.toUpperCase();
     let formData = this._getSortedFormData(aElement);
 
     // prompt user for name of search engine
     let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine3");
-    let title = { value: (aElement.ownerDocument.title || docURI.host) };
+    let title = { value: (aElement.ownerDocument.title || docURI.displayHost) };
     if (!Services.prompt.prompt(null, promptTitle, null, title, null, {})) {
       if (resultCallback) {
         resultCallback(false);
       };
       return;
     }
 
     Services.search.init(function addEngine_cb(rv) {
--- a/mobile/android/components/PromptService.js
+++ b/mobile/android/components/PromptService.js
@@ -785,17 +785,17 @@ var PromptUtils = {
     }
     aAuthInfo.password = password;
   },
 
   /**
    * Strip out things like userPass and path for display.
    */
   getFormattedHostname: function pu_getFormattedHostname(uri) {
-    return uri.scheme + "://" + uri.hostPort;
+    return uri.scheme + "://" + uri.displayHostPort;
   },
 
   fireDialogEvent: function(aDomWin, aEventName) {
     // accessing the document object can throw if this window no longer exists. See bug 789888.
     try {
       if (!aDomWin.document)
         return;
       let event = aDomWin.document.createEvent("Events");
--- a/mobile/android/components/TabSource.js
+++ b/mobile/android/components/TabSource.js
@@ -39,19 +39,19 @@ TabSource.prototype = {
     let prompt = new Prompt({
       window: win,
       title: title,
     }).setSingleChoiceItems(tabs.map(function(tab) {
       let label;
       if (tab.browser.contentTitle)
         label = tab.browser.contentTitle;
       else if (tab.browser.contentURI)
-        label = tab.browser.contentURI.spec;
+        label = tab.browser.contentURI.displaySpec;
       else
-        label = tab.originalURI.spec;
+        label = tab.originalURI.displaySpec;
       return { label: label,
                icon: "thumbnail:" + tab.id }
     }));
 
     let result = null;
     prompt.show(function(data) {
       result = data.button;
     });
--- a/mobile/android/components/build/nsAndroidHistory.cpp
+++ b/mobile/android/components/build/nsAndroidHistory.cpp
@@ -60,17 +60,17 @@ nsAndroidHistory::RegisterVisitedCallbac
   bool canAdd;
   nsresult rv = CanAddURI(aURI, &canAdd);
   NS_ENSURE_SUCCESS(rv, rv);
   if (!canAdd) {
     return NS_OK;
   }
 
   nsAutoCString uri;
-  rv = aURI->GetSpec(uri);
+  rv = aURI->GetDisplaySpec(uri);
   if (NS_FAILED(rv)) return rv;
   NS_ConvertUTF8toUTF16 uriString(uri);
 
   nsTArray<Link*>* list = mListeners.Get(uriString);
   if (! list) {
     list = new nsTArray<Link*>();
     mListeners.Put(uriString, list);
   }
@@ -85,17 +85,17 @@ nsAndroidHistory::RegisterVisitedCallbac
 
 NS_IMETHODIMP
 nsAndroidHistory::UnregisterVisitedCallback(nsIURI *aURI, Link *aContent)
 {
   if (!aContent || !aURI)
     return NS_OK;
 
   nsAutoCString uri;
-  nsresult rv = aURI->GetSpec(uri);
+  nsresult rv = aURI->GetDisplaySpec(uri);
   if (NS_FAILED(rv)) return rv;
   NS_ConvertUTF8toUTF16 uriString(uri);
 
   nsTArray<Link*>* list = mListeners.Get(uriString);
   if (! list)
     return NS_OK;
 
   list->RemoveElement(aContent);
@@ -204,17 +204,17 @@ nsAndroidHistory::GetName(nsACString& aN
 void
 nsAndroidHistory::SaveVisitURI(nsIURI* aURI) {
   // Add the URI to our cache so we can take a fast path later
   AppendToRecentlyVisitedURIs(aURI);
 
   if (jni::IsFennec()) {
     // Save this URI in our history
     nsAutoCString spec;
-    (void)aURI->GetSpec(spec);
+    (void)aURI->GetDisplaySpec(spec);
     java::GlobalHistory::MarkURIVisited(NS_ConvertUTF8toUTF16(spec));
   }
 
   // Finally, notify that we've been visited.
   nsCOMPtr<nsIObserverService> obsService = mozilla::services::GetObserverService();
   if (obsService) {
     obsService->NotifyObservers(aURI, NS_LINK_VISITED_EVENT_TOPIC, nullptr);
   }
@@ -287,34 +287,34 @@ nsAndroidHistory::SetURITitle(nsIURI *aU
   }
 
   if (IsEmbedURI(aURI)) {
     return NS_OK;
   }
 
   if (jni::IsFennec()) {
     nsAutoCString uri;
-    nsresult rv = aURI->GetSpec(uri);
+    nsresult rv = aURI->GetDisplaySpec(uri);
     if (NS_FAILED(rv)) return rv;
     if (RemovePendingVisitURI(aURI)) {
       // We have a title, so aURI isn't a redirect, so save the visit now before setting the title.
       SaveVisitURI(aURI);
     }
     NS_ConvertUTF8toUTF16 uriString(uri);
     java::GlobalHistory::SetURITitle(uriString, aTitle);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAndroidHistory::NotifyVisited(nsIURI *aURI)
 {
   if (aURI && sHistory) {
     nsAutoCString spec;
-    (void)aURI->GetSpec(spec);
+    (void)aURI->GetDisplaySpec(spec);
     sHistory->mPendingLinkURIs.Push(NS_ConvertUTF8toUTF16(spec));
     NS_DispatchToMainThread(sHistory);
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAndroidHistory::Run()
--- a/mobile/android/components/geckoview/GeckoViewPermission.js
+++ b/mobile/android/components/geckoview/GeckoViewPermission.js
@@ -103,17 +103,17 @@ GeckoViewPermission.prototype = {
       } else if (constraints.audio && !sources.some(source => source.type === "audio")) {
         throw "no audio source";
       }
 
       let dispatcher = GeckoViewUtils.getDispatcherForWindow(win);
       let uri = win.document.documentURIObject;
       return dispatcher.sendRequestForResult({
         type: "GeckoView:MediaPermission",
-        uri: uri.spec,
+        uri: uri.displaySpec,
         video: constraints.video ? sources.filter(source => source.type === "video") : null,
         audio: constraints.audio ? sources.filter(source => source.type === "audio") : null,
       }).then(response => {
         if (!response) {
           // Rejected.
           denyRequest();
           return;
         }
@@ -180,17 +180,17 @@ GeckoViewPermission.prototype = {
       return;
     }
 
     let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
     let dispatcher = GeckoViewUtils.getDispatcherForWindow(
         aRequest.window ? aRequest.window : aRequest.element.ownerGlobal);
     let promise = dispatcher.sendRequestForResult({
         type: "GeckoView:ContentPermission",
-        uri: aRequest.principal.URI.spec,
+        uri: aRequest.principal.URI.displaySpec,
         perm: perm.type,
         access: perm.access !== "unused" ? perm.access : null,
     }).then(granted => {
       if (!granted) {
         return false;
       }
       // Ask for app permission after asking for content permission.
       if (perm.type === "geolocation") {
--- a/mobile/android/components/geckoview/GeckoViewPrompt.js
+++ b/mobile/android/components/geckoview/GeckoViewPrompt.js
@@ -663,17 +663,17 @@ PromptDelegate.prototype = {
     } else {
       username = aAuthInfo.username;
     }
     return this._addText(/* title */ null, this._getAuthText(aChannel, aAuthInfo), {
       type: "auth",
       mode: aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD ? "password" : "auth",
       options: {
         flags: aAuthInfo.flags,
-        uri: aChannel && aChannel.URI.spec,
+        uri: aChannel && aChannel.URI.displaySpec,
         level: aLevel,
         username: username,
         password: aAuthInfo.password,
       },
     });
   },
 
   _fillAuthInfo: function(aAuthInfo, aCheckState, aResult) {
@@ -796,17 +796,17 @@ PromptDelegate.prototype = {
       let hostname = "moz-proxy://" + idnService.convertUTF8toACE(info.host) + ":" + info.port;
       let realm = aAuthInfo.realm;
       if (!realm) {
         realm = hostname;
       }
       return [hostname, realm];
     }
 
-    let hostname = aChannel.URI.scheme + "://" + aChannel.URI.hostPort;
+    let hostname = aChannel.URI.scheme + "://" + aChannel.URI.displayHostPort;
     // If a HTTP WWW-Authenticate header specified a realm, that value
     // will be available here. If it wasn't set or wasn't HTTP, we'll use
     // the formatted hostname instead.
     let realm = aAuthInfo.realm;
     if (!realm) {
       realm = hostname;
     }
     return [hostname, realm];
--- a/mobile/android/config/mozconfigs/common
+++ b/mobile/android/config/mozconfigs/common
@@ -39,17 +39,16 @@ ANDROID_NDK_VERSION="r10e"
 ANDROID_NDK_VERSION_32BIT="r8c"
 
 # Build Fennec
 ac_add_options --enable-application=mobile/android
 ac_add_options --with-android-sdk="$topsrcdir/android-sdk-linux"
 
 if [ -z "$NO_NDK" ]; then
     ac_add_options --with-android-ndk="$topsrcdir/android-ndk"
-    ac_add_options --with-android-gnu-compiler-version=4.9
 fi
 
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 
 ac_add_options --with-google-api-keyfile=/builds/gapi.data
 ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-fennec-geoloc-api.key
 
 # MOZ_INSTALL_TRACKING does not guarantee MOZ_UPDATE_CHANNEL will be set so we
@@ -103,12 +102,9 @@ fi
 
 export SOCORRO_SYMBOL_UPLOAD_TOKEN_FILE=/builds/crash-stats-api.token
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
 . "$topsrcdir/build/unix/mozconfig.stdcxx"
 
-# Use libc++ as our C++ standard library
-ac_add_options --with-android-cxx-stl=libc++
-
 JS_BINARY="$topsrcdir/mobile/android/config/js_wrapper.sh"
--- a/mobile/android/modules/HelperApps.jsm
+++ b/mobile/android/modules/HelperApps.jsm
@@ -191,17 +191,17 @@ var HelperApps =  {
     if (uri && mimeType == undefined) {
       mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
     }
 
     return {
       type: type,
       mime: mimeType,
       action: options.action || "", // empty action string defaults to android.intent.action.VIEW
-      url: uri ? uri.spec : "",
+      url: uri ? uri.displaySpec : "",
       packageName: options.packageName || "",
       className: options.className || ""
     };
   },
 
   _launchApp: function launchApp(app, uri, callback) {
     if (callback) {
         let msg = this._getMessage("Intent:OpenForResult", uri, {
--- a/mobile/android/modules/geckoview/GeckoViewNavigation.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewNavigation.jsm
@@ -79,17 +79,17 @@ class GeckoViewNavigation extends GeckoV
           " aFlags=" + aFlags);
 
     if (!aUri || !this.isRegistered) {
       return false;
     }
 
     let message = {
       type: "GeckoView:OnLoadUri",
-      uri: aUri.spec,
+      uri: aUri.displaySpec,
       where: aWhere,
       flags: aFlags
     };
 
     debug("dispatch " + JSON.stringify(message));
 
     let handled = undefined;
     this.eventDispatcher.sendRequestForResult(message).then(response => {
@@ -191,17 +191,17 @@ class GeckoViewNavigation extends GeckoV
     let fixedURI = aLocationURI;
 
     try {
       fixedURI = URIFixup.createExposableURI(aLocationURI);
     } catch (ex) { }
 
     let message = {
       type: "GeckoView:LocationChange",
-      uri: fixedURI.spec,
+      uri: fixedURI.displaySpec,
       canGoBack: this.browser.canGoBack,
       canGoForward: this.browser.canGoForward,
     };
 
     debug("dispatch " + JSON.stringify(message));
 
     this.eventDispatcher.sendRequest(message);
   }
--- a/mobile/android/modules/geckoview/GeckoViewProgress.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewProgress.jsm
@@ -266,17 +266,17 @@ class GeckoViewProgress extends GeckoVie
     if (!aWebProgress.isTopLevel) {
       return;
     }
 
     if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
       let uri = aRequest.QueryInterface(Ci.nsIChannel).URI;
       let message = {
         type: "GeckoView:PageStart",
-        uri: uri.spec,
+        uri: uri.displaySpec,
       };
 
       this.eventDispatcher.sendRequest(message);
     } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
                !aWebProgress.isLoadingDocument) {
       let message = {
         type: "GeckoView:PageStop",
         success: !aStatus
--- a/mobile/android/moz.configure
+++ b/mobile/android/moz.configure
@@ -66,20 +66,16 @@ project_flag('MOZ_ANDROID_SEARCH_ACTIVIT
 project_flag('MOZ_ANDROID_MLS_STUMBLER',
              help='Include Mozilla Location Service Stumbler on Android',
              default=True)
 
 project_flag('MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE',
              help='Background service for downloading additional content at runtime',
              default=True)
 
-project_flag('MOZ_ANDROID_CUSTOM_TABS',
-             help='Enable support for Android custom tabs',
-             default=milestone.is_nightly)
-
 project_flag('MOZ_ANDROID_PWA',
              help='Enable support for Progressive Web Apps',
              default=milestone.is_nightly)
 
 # Enable the Switchboard A/B framework code.
 # Note: The framework is always included in the app. This flag controls
 # usage of the framework.
 project_flag('MOZ_SWITCHBOARD',
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FirefoxAccounts.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FirefoxAccounts.java
@@ -143,18 +143,21 @@ public class FirefoxAccounts {
   }
 
   public static void logSyncOptions(Bundle syncOptions) {
     final boolean scheduleNow = syncOptions.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false);
 
     Logger.info(LOG_TAG, "Sync options -- scheduling now: " + scheduleNow);
   }
 
-  public static void requestImmediateSync(final Account account, String[] stagesToSync, String[] stagesToSkip) {
+  public static void requestImmediateSync(final Account account, String[] stagesToSync, String[] stagesToSkip, boolean ignoreSettings) {
     final Bundle syncOptions = new Bundle();
+    if (ignoreSettings) {
+      syncOptions.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true);
+    }
     syncOptions.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true);
     syncOptions.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
     requestSync(account, syncOptions, stagesToSync, stagesToSkip);
   }
 
   public static void requestEventualSync(final Account account, String[] stagesToSync, String[] stagesToSkip) {
     requestSync(account, Bundle.EMPTY, stagesToSync, stagesToSkip);
   }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java
@@ -79,31 +79,31 @@ public class FxAccountPushHandler {
     private static void handleVerification(Context context) {
         AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
         if (fxAccount == null) {
             Log.e(LOG_TAG, "The Android account does not exist anymore");
             return;
         }
         Log.i(LOG_TAG, "Received 'accountVerified' push event, requesting immediate sync");
         // This will trigger an email verification check and a sync.
-        fxAccount.requestImmediateSync(null, null);
+        fxAccount.requestImmediateSync(null, null, true);
     }
 
     private static void handleCollectionChanged(Context context, JSONObject data) throws JSONException {
         JSONArray collections = data.getJSONArray("collections");
         int len = collections.length();
         for (int i = 0; i < len; i++) {
             if (collections.getString(i).equals(CLIENTS_COLLECTION)) {
                 final Account account = FirefoxAccounts.getFirefoxAccount(context);
                 if (account == null) {
                     Log.e(LOG_TAG, "The account does not exist anymore");
                     return;
                 }
                 final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
-                fxAccount.requestImmediateSync(new String[] { CLIENTS_COLLECTION }, null);
+                fxAccount.requestImmediateSync(new String[] { CLIENTS_COLLECTION }, null, true);
                 return;
             }
         }
     }
 
     private static void handleDeviceDisconnection(Context context, JSONObject data) throws JSONException {
         final Account account = FirefoxAccounts.getFirefoxAccount(context);
         if (account == null) {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/activities/FxAccountStatusFragment.java
@@ -8,23 +8,23 @@ import android.accounts.Account;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.Handler;
+import android.preference.CheckBoxPreference;
 import android.preference.EditTextPreference;
 import android.preference.Preference;
 import android.preference.Preference.OnPreferenceChangeListener;
 import android.preference.Preference.OnPreferenceClickListener;
 import android.preference.PreferenceCategory;
 import android.preference.PreferenceScreen;
-import android.preference.SwitchPreference;
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 
 import com.squareup.picasso.Picasso;
 import com.squareup.picasso.Target;
 
 import org.mozilla.gecko.AppConstants;
@@ -94,20 +94,20 @@ public class FxAccountStatusFragment
   protected Preference removeAccountPreference;
 
   protected Preference needsPasswordPreference;
   protected Preference needsUpgradePreference;
   protected Preference needsVerificationPreference;
   protected Preference needsMasterSyncAutomaticallyEnabledPreference;
   protected Preference needsFinishMigratingPreference;
 
-  protected SwitchPreference bookmarksPreference;
-  protected SwitchPreference historyPreference;
-  protected SwitchPreference tabsPreference;
-  protected SwitchPreference passwordsPreference;
+  protected CheckBoxPreference bookmarksPreference;
+  protected CheckBoxPreference historyPreference;
+  protected CheckBoxPreference tabsPreference;
+  protected CheckBoxPreference passwordsPreference;
 
   protected EditTextPreference deviceNamePreference;
   protected Preference syncServerPreference;
   protected Preference syncNowPreference;
 
   protected volatile AndroidFxAccount fxAccount;
   // The contract is: when fxAccount is non-null, then clientsDataDelegate is
   // non-null.  If violated then an IllegalStateException is thrown.
@@ -162,20 +162,20 @@ public class FxAccountStatusFragment
     removeAccountPreference = ensureFindPreference("remove_account");
 
     needsPasswordPreference = ensureFindPreference("needs_credentials");
     needsUpgradePreference = ensureFindPreference("needs_upgrade");
     needsVerificationPreference = ensureFindPreference("needs_verification");
     needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled");
     needsFinishMigratingPreference = ensureFindPreference("needs_finish_migrating");
 
-    bookmarksPreference = (SwitchPreference) ensureFindPreference("bookmarks");
-    historyPreference = (SwitchPreference) ensureFindPreference("history");
-    tabsPreference = (SwitchPreference) ensureFindPreference("tabs");
-    passwordsPreference = (SwitchPreference) ensureFindPreference("passwords");
+    bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks");
+    historyPreference = (CheckBoxPreference) ensureFindPreference("history");
+    tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs");
+    passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords");
 
     profilePreference.setOnPreferenceClickListener(this);
     removeAccountPreference.setOnPreferenceClickListener(this);
 
     needsPasswordPreference.setOnPreferenceClickListener(this);
     needsVerificationPreference.setOnPreferenceClickListener(this);
     needsFinishMigratingPreference.setOnPreferenceClickListener(this);
 
@@ -264,17 +264,17 @@ public class FxAccountStatusFragment
         preference == passwordsPreference ||
         preference == tabsPreference) {
       saveEngineSelections();
       return true;
     }
 
     if (preference == syncNowPreference) {
       if (fxAccount != null) {
-        fxAccount.requestImmediateSync(null, null);
+        fxAccount.requestImmediateSync(null, null, true);
       }
       return true;
     }
 
     if (TextUtils.equals("linktos", preference.getKey())) {
       ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), getResources().getString(R.string.fxaccount_link_tos));
       return true;
     }
@@ -852,17 +852,17 @@ public class FxAccountStatusFragment
       final String key = preference.getKey();
       if ("debug_refresh".equals(key)) {
         Logger.info(LOG_TAG, "Refreshing.");
         refresh();
       } else if ("debug_dump".equals(key)) {
         fxAccount.dump();
       } else if ("debug_force_sync".equals(key)) {
         Logger.info(LOG_TAG, "Force syncing.");
-        fxAccount.requestImmediateSync(null, null);
+        fxAccount.requestImmediateSync(null, null, true);
         // No sense refreshing, since the sync will complete in the future.
       } else if ("debug_forget_certificate".equals(key)) {
         State state = fxAccount.getState();
         try {
           Married married = (Married) state;
           Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
           fxAccount.setState(married.makeCohabitingState());
           refresh();
@@ -938,17 +938,17 @@ public class FxAccountStatusFragment
       if (TextUtils.isEmpty(newClientName)) {
         newClientName = clientsDataDelegate.getDefaultClientName();
       }
       final long now = System.currentTimeMillis();
       clientsDataDelegate.setClientName(newClientName, now);
       // Force sync the client record, we want the user to see the device name change immediately
       // on the FxA Device Manager if possible ( = we are online) to avoid confusion
       // ("I changed my Android's device name but I don't see it on my computer").
-      fxAccount.requestImmediateSync(STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE, null);
+      fxAccount.requestImmediateSync(STAGES_TO_SYNC_ON_DEVICE_NAME_CHANGE, null, true);
       hardRefresh(); // Updates the value displayed to the user, among other things.
       return true;
     }
 
     // For everything else, accept the change.
     return true;
   }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
@@ -148,31 +148,16 @@ public class AndroidFxAccount {
   protected final Context context;
   private final AccountManager accountManager;
 
   // This is really, really meant to be final. Only changed when account name changes.
   // See Bug 1368147.
   protected volatile Account account;
 
   /**
-   * A cache associating Account name (email address) to a representation of the
-   * account's internal bundle.
-   * <p>
-   * The cache is invalidated entirely when <it>any</it> new Account is added,
-   * because there is no reliable way to know that an Account has been removed
-   * and then re-added.
-   */
-  private static final ConcurrentHashMap<String, ExtendedJSONObject> perAccountBundleCache =
-      new ConcurrentHashMap<>();
-
-  public static void invalidateCaches() {
-    perAccountBundleCache.clear();
-  }
-
-  /**
    * Create an Android Firefox Account instance backed by an Android Account
    * instance.
    * <p>
    * We expect a long-lived application context to avoid life-cycle issues that
    * might arise if the internally cached AccountManager instance surfaces UI.
    * <p>
    * We take care to not install any listeners or observers that might outlive
    * the AccountManager; and Android ensures the AccountManager doesn't outlive
@@ -239,71 +224,51 @@ public class AndroidFxAccount {
     // This should never happen. But it's useful to know if it does, so just crash.
     if (unpickledAccountUID == null) {
       throw new IllegalStateException("Unpickled account UID is null");
     }
 
     // Persist our UID into userData, so that we never have to do this dance again.
     accountManager.setUserData(account, ACCOUNT_KEY_UID, unpickledAccountUID);
 
-    // Our internal caches were also keyed by 'email' instead of 'uid', so we blow them away here
-    // so that they're re-populated correctly later on.
-    invalidateCaches();
-
     return unpickledAccountUID;
   }
 
   /**
    * Saves the given data as the internal bundle associated with this account.
    * @param bundle to write to account.
    */
   private synchronized void persistBundle(ExtendedJSONObject bundle) {
-    perAccountBundleCache.put(getAccountUID(), bundle);
     accountManager.setUserData(account, ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
   }
 
-  /* package-private */ ExtendedJSONObject unbundle() {
-    return unbundle(true);
-  }
-
   /**
    * Retrieve the internal bundle associated with this account.
    * @return bundle associated with account.
    */
-  private synchronized ExtendedJSONObject unbundle(boolean allowCachedBundle) {
-    final String accountUID = getAccountUID();
-
-    if (allowCachedBundle) {
-      final ExtendedJSONObject cachedBundle = perAccountBundleCache.get(accountUID);
-      if (cachedBundle != null) {
-        Logger.debug(LOG_TAG, "Returning cached account bundle.");
-        return cachedBundle;
-      }
-    }
+  /* package-private */ synchronized ExtendedJSONObject unbundle() {
+    final String bundleString = accountManager.getUserData(account, ACCOUNT_KEY_DESCRIPTOR);
 
     final int version = getAccountVersion();
     if (version < CURRENT_ACCOUNT_VERSION) {
       // Needs upgrade. For now, do nothing. We'd like to just put your account
       // into the Separated state here and have you update your credentials.
       return null;
     }
 
     if (version > CURRENT_ACCOUNT_VERSION) {
       // Oh dear.
+      throw new IllegalStateException("Invalid account bundle version. Current: " + CURRENT_ACCOUNT_VERSION + ", bundle version: " + version);
+    }
+
+    if (bundleString == null) {
       return null;
     }
 
-    String bundleString = accountManager.getUserData(account, ACCOUNT_KEY_DESCRIPTOR);
-    if (bundleString == null) {
-      return null;
-    }
-    final ExtendedJSONObject bundle = unbundleAccountV2(bundleString);
-    perAccountBundleCache.put(accountUID, bundle);
-    Logger.info(LOG_TAG, "Account bundle persisted to cache.");
-    return bundle;
+    return unbundleAccountV2(bundleString);
   }
 
   private String getBundleData(String key) {
     ExtendedJSONObject o = unbundle();
     if (o == null) {
       return null;
     }
     return o.getString(key);
@@ -677,19 +642,20 @@ public class AndroidFxAccount {
     return active;
   }
 
   /**
    * Request an immediate sync.  Use this to sync as soon as possible in response to user action.
    *
    * @param stagesToSync stage names to sync; can be null to sync <b>all</b> known stages.
    * @param stagesToSkip stage names to skip; can be null to skip <b>no</b> known stages.
+   * @param ignoreSettings whether we should check preferences for syncing over metered connections.
    */
-  public void requestImmediateSync(String[] stagesToSync, String[] stagesToSkip) {
-    FirefoxAccounts.requestImmediateSync(getAndroidAccount(), stagesToSync, stagesToSkip);
+  public void requestImmediateSync(String[] stagesToSync, String[] stagesToSkip, boolean ignoreSettings) {
+    FirefoxAccounts.requestImmediateSync(getAndroidAccount(), stagesToSync, stagesToSkip, ignoreSettings);
   }
 
   /**
    * Request an eventual sync.  Use this to request the system queue a sync for some time in the
    * future.
    *
    * @param stagesToSync stage names to sync; can be null to sync <b>all</b> known stages.
    * @param stagesToSkip stage names to skip; can be null to skip <b>no</b> known stages.
@@ -1104,17 +1070,18 @@ public class AndroidFxAccount {
     // We also need to ensure our regular "account was just deleted" side-effects are not invoked.
     } else {
       doOptionalProfileRenamePre21(email, migrateSyncSettings, callback);
     }
   }
 
   @TargetApi(Build.VERSION_CODES.LOLLIPOP)
   private void doOptionalProfileRename21Plus(final String newEmail, final Runnable migrateSyncSettingsCallback, final Runnable callback) {
-    accountManager.renameAccount(account, newEmail, new AccountManagerCallback<Account>() {
+    final Account currentAccount = new Account(account.name, account.type);
+    accountManager.renameAccount(currentAccount, newEmail, new AccountManagerCallback<Account>() {
       @Override
       public void run(AccountManagerFuture<Account> future) {
         if (future.isCancelled()) {
           Logger.error(LOG_TAG, "Account rename task cancelled.");
           callback.run();
           return;
         }
 
@@ -1129,17 +1096,16 @@ public class AndroidFxAccount {
 
           // We tried, we really did.
           if (!updatedAccount.name.equals(newEmail)) {
             Logger.error(LOG_TAG, "Tried to update account name, but it didn't seem to have changed.");
           } else {
             account = updatedAccount;
             migrateSyncSettingsCallback.run();
 
-            invalidateCaches();
             callback.run();
           }
         } catch (OperationCanceledException | IOException | AuthenticatorException e) {
           Logger.error(LOG_TAG, "Unexpected exception while trying to rename an account", e);
           callback.run();
         }
       }
       // Request that callbacks are posted to the current thread.
@@ -1157,17 +1123,18 @@ public class AndroidFxAccount {
 
     // Ensure our account deletion side-effects will not run. See comments at this key's definition.
     // Note that this key is not copied over to the new account. We do need to ensure this field is
     // reset if we fail to remove the account. Otherwise our regular account deletion logic won't run
     // in the future.
     accountManager.setUserData(account, ACCOUNT_KEY_RENAME_IN_PROGRESS, ACCOUNT_VALUE_RENAME_IN_PROGRESS);
 
     // Then, remove current account.
-    accountManager.removeAccount(account, new AccountManagerCallback<Boolean>() {
+    final Account currentAccount = new Account(account.name, account.type);
+    accountManager.removeAccount(currentAccount, new AccountManagerCallback<Boolean>() {
       @Override
       public void run(AccountManagerFuture<Boolean> future) {
         boolean accountRemovalSucceeded = false;
         boolean removeResult = false;
         try {
           removeResult = future.getResult();
           accountRemovalSucceeded = !future.isCancelled() && future.isDone() && removeResult;
         } catch (OperationCanceledException | IOException | AuthenticatorException e) {
@@ -1187,19 +1154,16 @@ public class AndroidFxAccount {
 
           accountManager.setUserData(account, ACCOUNT_KEY_RENAME_IN_PROGRESS, null);
           callback.run();
           return;
         }
 
         // It appears that we've successfully removed the account. It's now time to re-add it.
 
-        // Purge our internal state.
-        invalidateCaches();
-
         // Finally, add an Android account with new name and old user data.
         final Account newAccount = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
         final boolean didAdd = accountManager.addAccountExplicitly(newAccount, null, currentUserData);
 
         // Rename succeeded, now let's configure the newly added account.
         if (didAdd) {
           account = newAccount;
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java
@@ -54,20 +54,16 @@ public class FxAccountAuthenticator exte
 
   @Override
   public Bundle addAccount(AccountAuthenticatorResponse response,
       String accountType, String authTokenType, String[] requiredFeatures,
       Bundle options)
           throws NetworkErrorException {
     Logger.debug(LOG_TAG, "addAccount");
 
-    // The data associated to each Account should be invalidated when we change
-    // the set of Firefox Accounts on the system.
-    AndroidFxAccount.invalidateCaches();
-
     final Bundle res = new Bundle();
 
     if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
       res.putInt(AccountManager.KEY_ERROR_CODE, -1);
       res.putString(AccountManager.KEY_ERROR_MESSAGE, "Not adding unknown account type.");
       return res;
     }
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDevice.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDevice.java
@@ -10,51 +10,66 @@ public class FxAccountDevice {
 
   private static final String JSON_KEY_NAME = "name";
   private static final String JSON_KEY_ID = "id";
   private static final String JSON_KEY_TYPE = "type";
   private static final String JSON_KEY_ISCURRENTDEVICE = "isCurrentDevice";
   private static final String JSON_KEY_PUSH_CALLBACK = "pushCallback";
   private static final String JSON_KEY_PUSH_PUBLICKEY = "pushPublicKey";
   private static final String JSON_KEY_PUSH_AUTHKEY = "pushAuthKey";
-  private static final String JSON_LAST_ACCESS_TIME = "lastAccessTime";
+  private static final String JSON_KEY_LAST_ACCESS_TIME = "lastAccessTime";
+  private static final String JSON_KEY_PUSH_ENDPOINT_EXPIRED = "pushEndpointExpired";
 
   public final String id;
   public final String name;
   public final String type;
   public final Boolean isCurrentDevice;
   public final Long lastAccessTime;
   public final String pushCallback;
   public final String pushPublicKey;
   public final String pushAuthKey;
+  public final Boolean pushEndpointExpired;
 
   public FxAccountDevice(String name, String id, String type, Boolean isCurrentDevice,
                          Long lastAccessTime, String pushCallback, String pushPublicKey,
-                         String pushAuthKey) {
+                         String pushAuthKey, Boolean pushEndpointExpired) {
     this.name = name;
     this.id = id;
     this.type = type;
     this.isCurrentDevice = isCurrentDevice;
     this.lastAccessTime = lastAccessTime;
     this.pushCallback = pushCallback;
     this.pushPublicKey = pushPublicKey;
     this.pushAuthKey = pushAuthKey;
+    this.pushEndpointExpired = pushEndpointExpired;
   }
 
   public static FxAccountDevice fromJson(ExtendedJSONObject json) {
     final String name = json.getString(JSON_KEY_NAME);
     final String id = json.getString(JSON_KEY_ID);
     final String type = json.getString(JSON_KEY_TYPE);
     final Boolean isCurrentDevice = json.getBoolean(JSON_KEY_ISCURRENTDEVICE);
-    final Long lastAccessTime = json.getLong(JSON_LAST_ACCESS_TIME);
+    final Long lastAccessTime = json.getLong(JSON_KEY_LAST_ACCESS_TIME);
     final String pushCallback = json.getString(JSON_KEY_PUSH_CALLBACK);
     final String pushPublicKey = json.getString(JSON_KEY_PUSH_PUBLICKEY);
     final String pushAuthKey = json.getString(JSON_KEY_PUSH_AUTHKEY);
+    // The FxA server sends this boolean as a number (bug):
+    // https://github.com/mozilla/fxa-auth-server/pull/2122
+    // Use getBoolean directly once the fix is deployed (probably ~Oct-Nov 2017).
+    final Object pushEndpointExpiredRaw = json.get(JSON_KEY_PUSH_ENDPOINT_EXPIRED);
+    final Boolean pushEndpointExpired;
+    if (pushEndpointExpiredRaw instanceof Number) {
+      pushEndpointExpired = ((Number) pushEndpointExpiredRaw).intValue() == 1;
+    } else if (pushEndpointExpiredRaw instanceof Boolean) {
+      pushEndpointExpired = (Boolean) pushEndpointExpiredRaw;
+    } else {
+      pushEndpointExpired = false;
+    }
     return new FxAccountDevice(name, id, type, isCurrentDevice, lastAccessTime, pushCallback,
-                               pushPublicKey, pushAuthKey);
+                               pushPublicKey, pushAuthKey, pushEndpointExpired);
   }
 
   public ExtendedJSONObject toJson() {
     final ExtendedJSONObject body = new ExtendedJSONObject();
     if (this.name != null) {
       body.put(JSON_KEY_NAME, this.name);
     }
     if (this.id != null) {
@@ -104,12 +119,12 @@ public class FxAccountDevice {
     }
 
     public void pushAuthKey(String pushAuthKey) {
       this.pushAuthKey = pushAuthKey;
     }
 
     public FxAccountDevice build() {
       return new FxAccountDevice(this.name, this.id, this.type, null, null,
-                                 this.pushCallback, this.pushPublicKey, this.pushAuthKey);
+                                 this.pushCallback, this.pushPublicKey, this.pushAuthKey, null);
     }
   }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceListUpdater.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceListUpdater.java
@@ -1,37 +1,48 @@
 /* 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.fxa.devices;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClientException;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.RemoteDevices;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.util.ThreadUtils;
 
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.GeneralSecurityException;
 import java.util.concurrent.Executor;
 
 public class FxAccountDeviceListUpdater implements FxAccountClient20.RequestDelegate<FxAccountDevice[]> {
     private static final String LOG_TAG = "FxADeviceListUpdater";
 
     private final AndroidFxAccount fxAccount;
     private final ContentResolver contentResolver;
+    private boolean localDevicePushEndpointExpired = false;
+
+    private final static String SYNC_PREFS_PUSH_LAST_RENEW_REGISTRATION_MS = "push.lastRenewRegistration";
+    private final static long TIME_BETWEEN_RENEW_REGISTRATION_MS = 2 * 7 * 24 * 3600 * 1000;
 
     public FxAccountDeviceListUpdater(final AndroidFxAccount fxAccount, final ContentResolver cr) {
         this.fxAccount = fxAccount;
         this.contentResolver = cr;
     }
 
     @Override
     public void handleSuccess(final FxAccountDevice[] result) {
@@ -41,26 +52,27 @@ public class FxAccountDeviceListUpdater 
                         .build();
 
         final Bundle valuesBundle = new Bundle();
         final ContentValues[] insertValues = new ContentValues[result.length];
 
         final long now = System.currentTimeMillis();
         for (int i = 0; i < result.length; i++) {
             final FxAccountDevice fxADevice = result[i];
+            if (fxADevice.isCurrentDevice && fxADevice.pushEndpointExpired) {
+                this.localDevicePushEndpointExpired = true;
+            }
             final ContentValues deviceValues = new ContentValues();
             deviceValues.put(RemoteDevices.GUID, fxADevice.id);
             deviceValues.put(RemoteDevices.TYPE, fxADevice.type);
             deviceValues.put(RemoteDevices.NAME, fxADevice.name);
             deviceValues.put(RemoteDevices.IS_CURRENT_DEVICE, fxADevice.isCurrentDevice);
             deviceValues.put(RemoteDevices.DATE_CREATED, now);
             deviceValues.put(RemoteDevices.DATE_MODIFIED, now);
-            // TODO: Remove that line once FxA sends lastAccessTime all the time.
-            final Long lastAccessTime = fxADevice.lastAccessTime != null ? fxADevice.lastAccessTime : 0;
-            deviceValues.put(RemoteDevices.LAST_ACCESS_TIME, lastAccessTime);
+            deviceValues.put(RemoteDevices.LAST_ACCESS_TIME, fxADevice.lastAccessTime);
             insertValues[i] = deviceValues;
         }
         valuesBundle.putParcelableArray(BrowserContract.METHOD_PARAM_DATA, insertValues);
         try {
             contentResolver.call(uri, BrowserContract.METHOD_REPLACE_REMOTE_CLIENTS, uri.toString(),
                                  valuesBundle);
             Log.i(LOG_TAG, "FxA Device list update done.");
         } catch (Exception e) {
@@ -103,9 +115,58 @@ public class FxAccountDeviceListUpdater 
         } catch (State.NotASessionTokenState e) {
             // This should never happen, because the caller (FxAccountSyncAdapter) verifies that
             // we are in a token state before calling this method.
             throw new IllegalStateException("Could not get a session token during Sync (?) " + e);
         }
         final FxAccountClient fxaClient = getSynchronousFxaClient();
         fxaClient.deviceList(sessionToken, this);
     }
+
+    // Updates the list of remote devices, and also renews our push registration if the list provider
+    // tells us it's expired.
+    public void updateAndMaybeRenewRegistration(final Context context) {
+        // Synchronous operation, the re-registration will happen right after the refresh if necessary.
+        this.update();
+        if (!this.localDevicePushEndpointExpired) {
+            return;
+        }
+        final SharedPreferences syncPrefs;
+        try {
+            syncPrefs = fxAccount.getSyncPrefs();
+        } catch (UnsupportedEncodingException | GeneralSecurityException e) {
+            Log.e(LOG_TAG, "Could not get sync preferences, skipping push endpoint re-registration.");
+            return;
+        }
+
+        final long lastTryMs = syncPrefs.getLong(SYNC_PREFS_PUSH_LAST_RENEW_REGISTRATION_MS, 0);
+        final long nowMs = System.currentTimeMillis();
+        if (nowMs - lastTryMs < TIME_BETWEEN_RENEW_REGISTRATION_MS) {
+            Log.w(LOG_TAG, "Last renew registration too close, skipping.");
+            return;
+        }
+
+        final SharedPreferences.Editor syncPrefsEditor = syncPrefs.edit();
+        syncPrefsEditor.putLong(SYNC_PREFS_PUSH_LAST_RENEW_REGISTRATION_MS, nowMs);
+        syncPrefsEditor.commit();
+
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    FxAccountDeviceListUpdater.this.renewPushRegistration(context);
+                } catch (Exception e) {
+                    Log.e(LOG_TAG, "Could not renew push registration, continuing anyway", e);
+                }
+                FxAccountDeviceRegistrator.renewRegistration(context);
+            }
+        });
+    }
+
+    private void renewPushRegistration(Context context) throws ClassNotFoundException, NoSuchMethodException,
+                                                InvocationTargetException, IllegalAccessException {
+        final Class<?> pushService = Class.forName("org.mozilla.gecko.push.PushService");
+        final Method getInstance = pushService.getMethod("getInstance", Context.class);
+        final Object instance = getInstance.invoke(null, context);
+        final Method onRefresh = pushService.getMethod("onRefresh");
+        onRefresh.invoke(instance);
+    }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceRegistrator.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceRegistrator.java
@@ -375,17 +375,17 @@ public class FxAccountDeviceRegistrator 
           fxAccount.setFxAUserData(fxaDevice.id, 0, 0L); // Reset device registration version/timestamp
           if (!allowRecursion) {
             Log.d(LOG_TAG, "Failure to register a device on the second try");
             break;
           }
           final FxAccountDevice updatedDevice = new FxAccountDevice(device.name, fxaDevice.id, device.type,
                                                                     null, null,
                                                                     device.pushCallback, device.pushPublicKey,
-                                                                    device.pushAuthKey);
+                                                                    device.pushAuthKey, null);
           doFxaRegistration(context, fxAccount, updatedDevice, false);
           return;
         }
         onError();
       }
     });
   }
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
@@ -44,21 +44,16 @@ public class FxAccountDeletedService ext
   public static final String LOG_TAG = FxAccountDeletedService.class.getSimpleName();
 
   public FxAccountDeletedService() {
     super(LOG_TAG);
   }
 
   @Override
   protected void onHandleIntent(final Intent intent) {
-    // We have an in-memory accounts cache which we use for a variety of tasks; it needs to be cleared.
-    // It should be fine to invalidate it before doing anything else, as the tasks below do not rely
-    // on this data.
-    AndroidFxAccount.invalidateCaches();
-
     // Intent can, in theory, be null. Bug 1025937.
     if (intent == null) {
       Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
       return;
     }
 
     final Context context = this;
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountUpgradeReceiver.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountUpgradeReceiver.java
@@ -86,17 +86,17 @@ public class FxAccountUpgradeReceiver ex
 
     @Override
     public void run() {
       final Account[] accounts = FirefoxAccounts.getFirefoxAccounts(context);
       Logger.info(LOG_TAG, "Trying to sync " + accounts.length + " existing Firefox Accounts.");
       for (Account account : accounts) {
         try {
           final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
-          fxAccount.requestImmediateSync(new String[] { "clients" }, null);
+          fxAccount.requestImmediateSync(new String[] { "clients" }, null, true);
         } catch (Exception e) {
           Logger.warn(LOG_TAG, "Got exception trying to force sync account named like " + Utils.obfuscateEmail(account.name) +
                                "; ignoring.", e);
         }
       }
     }
   }
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
@@ -479,17 +479,17 @@ public class FxAccountSyncAdapter extend
   }
 
   private void onSessionTokenStateReached(Context context, AndroidFxAccount fxAccount) {
     // This does not block the main thread, if work has to be done it is executed in a new thread.
     maybeRegisterDevice(context, fxAccount);
 
     FxAccountDeviceListUpdater deviceListUpdater = new FxAccountDeviceListUpdater(fxAccount, context.getContentResolver());
     // Since the clients stage requires a fresh list of remote devices, we update the device list synchronously.
-    deviceListUpdater.update();
+    deviceListUpdater.updateAndMaybeRenewRegistration(context);
   }
 
   /**
    * A trivial Sync implementation that does not cache client keys,
    * certificates, or tokens.
    *
    * This should be replaced with a full {@link FxAccountAuthenticator}-based
    * token implementation.
@@ -696,17 +696,17 @@ public class FxAccountSyncAdapter extend
     }
 
     // Full sync (of all of stages) is necessary if we hit "concurrent modification" errors while
     // uploading meta/global stage. This is considered both a rare and important event, so it's
     // deemed safe and necessary to request an immediate sync, which will ignore any back-offs and
     // will happen right away.
     if (syncDelegate.fullSyncNecessary) {
       Logger.info(LOG_TAG, "Syncing done. Full follow-up sync necessary, requesting immediate sync.");
-      fxAccount.requestImmediateSync(null, null);
+      fxAccount.requestImmediateSync(null, null, false);
       return;
     }
 
     // If there are any incomplete stages, request a follow-up sync. Otherwise, we're done.
     // Incomplete stage is:
     // - one that hit a 412 error during either upload or download of data, indicating that
     //   its collection has been modified remotely, or
     // - one that hit a sync deadline
@@ -719,11 +719,11 @@ public class FxAccountSyncAdapter extend
 
     if (stagesToSyncAgain.length == 0) {
       Logger.info(LOG_TAG, "Syncing done.");
       return;
     }
 
     // If there are any other stages marked as incomplete, request that they're synced again.
     Logger.info(LOG_TAG, "Syncing done. Requesting an immediate follow-up sync.");
-    fxAccount.requestImmediateSync(stagesToSyncAgain, null);
+    fxAccount.requestImmediateSync(stagesToSyncAgain, null, false);
   }
 }
--- a/mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml
+++ b/mobile/android/services/src/main/res/xml/fxaccount_status_prefscreen.xml
@@ -56,29 +56,29 @@
         android:defaultValue=""
         android:persistent="false"
         android:title="@string/fxaccount_status_sync_now"
         android:summary="" />
 
     <PreferenceCategory
         android:title="@string/fxaccount_status_choose_what">
 
-        <SwitchPreference
+        <CheckBoxPreference
             android:key="bookmarks"
             android:persistent="false"
             android:title="@string/fxaccount_status_bookmarks" />
-        <SwitchPreference
+        <CheckBoxPreference
             android:key="history"
             android:persistent="false"
             android:title="@string/fxaccount_status_history" />
-        <SwitchPreference
+        <CheckBoxPreference
             android:key="tabs"
             android:persistent="false"
             android:title="@string/fxaccount_status_tabs" />
-        <SwitchPreference
+        <CheckBoxPreference
             android:key="passwords"
             android:persistent="false"
             android:title="@string/fxaccount_status_passwords" />
     </PreferenceCategory>
 
     <EditTextPreference
         android:singleLine="true"
         android:key="device_name"
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/devices/TestFxAccountDeviceListUpdater.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/devices/TestFxAccountDeviceListUpdater.java
@@ -76,19 +76,19 @@ public class TestFxAccountDeviceListUpda
         fxaDevicesUpdater.update();
         verify(fxaClient).deviceList(token, fxaDevicesUpdater);
     }
 
     @Test
     public void testSuccessHandler() throws Throwable {
         FxAccountDevice[] result = new FxAccountDevice[2];
         FxAccountDevice device1 = new FxAccountDevice("Current device", "deviceid1", "mobile", true, System.currentTimeMillis(),
-                "https://localhost/push/callback1", "abc123", "321cba");
+                "https://localhost/push/callback1", "abc123", "321cba", false);
         FxAccountDevice device2 = new FxAccountDevice("Desktop PC", "deviceid2", "desktop", true, System.currentTimeMillis(),
-                "https://localhost/push/callback2", "abc123", "321cba");
+                "https://localhost/push/callback2", "abc123", "321cba", false);
         result[0] = device1;
         result[1] = device2;
 
         when(fxAccount.getProfile()).thenReturn("default");
 
         long timeBeforeCall = System.currentTimeMillis();
         fxaDevicesUpdater.handleSuccess(result);
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
@@ -175,27 +175,27 @@ public class MockFxAccountClient impleme
     if (!user.verified) {
       handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified");
       return;
     }
     try {
       String deviceId = deviceToRegister.id;
       if (TextUtils.isEmpty(deviceId)) { // Create
         deviceId = UUID.randomUUID().toString();
-        FxAccountDevice device = new FxAccountDevice(deviceToRegister.name, deviceId, deviceToRegister.type, null, null, null, null, null);
+        FxAccountDevice device = new FxAccountDevice(deviceToRegister.name, deviceId, deviceToRegister.type, null, null, null, null, null, false);
         requestDelegate.handleSuccess(device);
       } else { // Update
         FxAccountDevice existingDevice = user.devices.get(deviceId);
         if (existingDevice != null) {
           String deviceName = existingDevice.name;
           if (!TextUtils.isEmpty(deviceToRegister.name)) {
             deviceName = deviceToRegister.name;
           } // We could also update the other fields..
           FxAccountDevice device = new FxAccountDevice(deviceName, existingDevice.id, existingDevice.type, existingDevice.isCurrentDevice,
-                  existingDevice.lastAccessTime, existingDevice.pushCallback, existingDevice.pushPublicKey,existingDevice.pushAuthKey);
+                  existingDevice.lastAccessTime, existingDevice.pushCallback, existingDevice.pushPublicKey,existingDevice.pushAuthKey, false);
           requestDelegate.handleSuccess(device);
         } else { // Device unknown
           handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.UNKNOWN_DEVICE, "device is unknown");
           return;
         }
       }
     } catch (Exception e) {
       requestDelegate.handleError(e);
--- a/mobile/android/tests/browser/robocop/robocop.ini
+++ b/mobile/android/tests/browser/robocop/robocop.ini
@@ -41,16 +41,17 @@ disabled=bug 1128287
 skip-if = true # Bug 1390059
 [src/org/mozilla/gecko/tests/testFormHistory.java]
 [src/org/mozilla/gecko/tests/testGetUserMedia.java]
 # failures across the board, bug 1092202 & bug 1144926
 skip-if = true
 [src/org/mozilla/gecko/tests/testHistory.java]
 disabled=see bug 915350
 [src/org/mozilla/gecko/tests/testHomeBanner.java]
+[src/org/mozilla/gecko/tests/testIdnSupport.java]
 [src/org/mozilla/gecko/tests/testInputUrlBar.java]
 [src/org/mozilla/gecko/tests/testJarReader.java]
 [src/org/mozilla/gecko/tests/testLinkContextMenu.java]
 [src/org/mozilla/gecko/tests/testHomeListsProvider.java]
 disabled=see bug 952310
 [src/org/mozilla/gecko/tests/testLoad.java]
 skip-if = true # Bug 1390059
 [src/org/mozilla/gecko/tests/testMailToContextMenu.java]
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java
@@ -12,25 +12,28 @@ import static org.mozilla.gecko.tests.he
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.tests.UITestContext;
 import org.mozilla.gecko.tests.helpers.DeviceHelper;
 import org.mozilla.gecko.tests.helpers.NavigationHelper;
 import org.mozilla.gecko.tests.helpers.WaitHelper;
 import org.mozilla.gecko.toolbar.PageActionLayout;
 import org.mozilla.gecko.toolbar.TabCounter;
 
+import android.net.Uri;
 import android.view.View;
 import android.widget.EditText;
 import android.widget.ImageButton;
 import android.widget.TextSwitcher;
 import android.widget.TextView;
 
 import com.robotium.solo.Condition;
 import com.robotium.solo.Solo;
 
+import java.net.IDN;
+
 /**
  * A class representing any interactions that take place on the Toolbar.
  */
 public class ToolbarComponent extends BaseComponent {
     public static final String URL_HTTP_PREFIX = "http://";
 
     // We are waiting up to 30 seconds instead of the default waiting time because reader mode
     // parsing can take quite some time on slower devices (Bug 1142699)
@@ -251,18 +254,21 @@ public class ToolbarComponent extends Ba
         fAssertNotNull("url is not null", url);
 
         assertIsEditing();
 
         final EditText urlEditText = getUrlEditText();
         fAssertTrue("The UrlEditText is the input method target",
                 urlEditText.isInputMethodTarget());
 
+        // Solo doesn't handle typing text with Unicode characters, so if the input looks like a
+        // genuine URL, we work around this by converting it to Punycode beforehand.
+        final String textToType = url.contains("://") ? convertUrlToPunycode(url) : url;
         mSolo.clearEditText(urlEditText);
-        mSolo.typeText(urlEditText, url);
+        mSolo.typeText(urlEditText, textToType);
 
         return this;
     }
 
     public ToolbarComponent pressBackButton() {
         final ImageButton backButton = getBackButton();
         return pressButton(backButton, "back");
     }
@@ -334,9 +340,16 @@ public class ToolbarComponent extends Ba
 
     private boolean isUrlEditTextSelected() {
         return getUrlEditText().isSelected();
     }
 
     private boolean isBackButtonEnabled() {
         return getBackButton().isEnabled();
     }
+
+    private String convertUrlToPunycode(final String url) {
+        final Uri uri = Uri.parse(url);
+        final Uri.Builder uriBuilder = uri.buildUpon();
+        uriBuilder.encodedAuthority(IDN.toASCII(uri.getAuthority()));
+        return uriBuilder.toString();
+    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testIdnSupport.java
@@ -0,0 +1,25 @@
+package org.mozilla.gecko.tests;
+
+import org.mozilla.gecko.tests.helpers.GeckoHelper;
+import org.mozilla.gecko.tests.helpers.NavigationHelper;
+
+public class testIdnSupport extends UITest {
+    public void testToolbarIdnSupport() {
+        GeckoHelper.blockForReady();
+
+        mBaseHostnameUrl = "http://exämple.test/tests";
+        String url = mStringHelper.ROBOCOP_BLANK_PAGE_01_URL;
+        NavigationHelper.enterAndLoadUrl(url);
+        mToolbar.assertTitle(url);
+
+        mBaseHostnameUrl = "http://παράδειγμα.δοκιμή/tests";
+        url = mStringHelper.ROBOCOP_BLANK_PAGE_02_URL;
+        NavigationHelper.enterAndLoadUrl(url);
+        mToolbar.assertTitle(url);
+
+        mBaseHostnameUrl = "http://天気の良い日.w3c-test.org/tests";
+        url = mStringHelper.ROBOCOP_BLANK_PAGE_03_URL;
+        NavigationHelper.enterAndLoadUrl(url);
+        mToolbar.assertTitle(url);
+    }
+}
--- a/python/mozbuild/mozbuild/backend/recursivemake.py
+++ b/python/mozbuild/mozbuild/backend/recursivemake.py
@@ -1215,17 +1215,18 @@ class RecursiveMakeBackend(CommonBackend
         backend_file.write('LOCAL_INCLUDES += -I%s\n' % path)
 
     def _process_per_source_flag(self, per_source_flag, backend_file):
         for flag in per_source_flag.flags:
             backend_file.write('%s_FLAGS += %s\n' % (mozpath.basename(per_source_flag.file_name), flag))
 
     def _process_computed_flags(self, computed_flags, backend_file):
         for var, flags in computed_flags.get_flags():
-            backend_file.write('COMPUTED_%s += %s\n' % (var, make_quote(' '.join(flags))))
+            backend_file.write('COMPUTED_%s += %s\n' % (var,
+                                                        ' '.join(make_quote(shell_quote(f)) for f in flags)))
 
     def _process_java_jar_data(self, jar, backend_file):
         target = jar.name
         backend_file.write('JAVA_JAR_TARGETS += %s\n' % target)
         backend_file.write('%s_DEST := %s.jar\n' % (target, jar.name))
         if jar.sources:
             backend_file.write('%s_JAVAFILES := %s\n' %
                 (target, ' '.join(jar.sources)))
--- a/python/mozbuild/mozbuild/compilation/database.py
+++ b/python/mozbuild/mozbuild/compilation/database.py
@@ -9,17 +9,16 @@ import types
 
 from mozbuild.compilation import util
 from mozbuild.backend.common import CommonBackend
 from mozbuild.frontend.data import (
     ComputedFlags,
     Sources,
     GeneratedSources,
     DirectoryTraversal,
-    Defines,
     Linkable,
     LocalInclude,
     PerSourceFlag,
     VariablePassthru,
     SimpleProgram,
 )
 from mozbuild.shellutil import (
     quote as shell_quote,
@@ -40,33 +39,26 @@ class CompileDBBackend(CommonBackend):
 
         # The database we're going to dump out to.
         self._db = OrderedDict()
 
         # The cache for per-directory flags
         self._flags = {}
 
         self._envs = {}
-        self._includes = defaultdict(list)
-        self._defines = defaultdict(list)
         self._local_flags = defaultdict(dict)
         self._per_source_flags = defaultdict(list)
-        self._extra_includes = defaultdict(list)
-        self._gyp_dirs = set()
-        self._dist_include_testing = '-I%s' % mozpath.join(
-            self.environment.topobjdir, 'dist', 'include', 'testing')
 
     def consume_object(self, obj):
         # Those are difficult directories, that will be handled later.
         if obj.relativedir in (
                 'build/unix/elfhack',
                 'build/unix/elfhack/inject',
                 'build/clang-plugin',
-                'build/clang-plugin/tests',
-                'toolkit/crashreporter/google-breakpad/src/common'):
+                'build/clang-plugin/tests'):
             return True
 
         consumed = CommonBackend.consume_object(self, obj)
 
         if consumed:
             return True
 
         if isinstance(obj, DirectoryTraversal):
@@ -77,35 +69,17 @@ class CompileDBBackend(CommonBackend):
                     self._local_flags[obj.objdir][var] = value
 
         elif isinstance(obj, (Sources, GeneratedSources)):
             # For other sources, include each source file.
             for f in obj.files:
                 self._build_db_line(obj.objdir, obj.relativedir, obj.config, f,
                                     obj.canonical_suffix)
 
-        elif isinstance(obj, LocalInclude):
-            self._includes[obj.objdir].append('-I%s' % mozpath.normpath(
-                obj.path.full_path))
-
-        elif isinstance(obj, Linkable):
-            if isinstance(obj.defines, Defines): # As opposed to HostDefines
-                for d in obj.defines.get_defines():
-                    if d not in self._defines[obj.objdir]:
-                        self._defines[obj.objdir].append(d)
-            self._defines[obj.objdir].extend(obj.lib_defines.get_defines())
-            if isinstance(obj, SimpleProgram) and obj.is_unit_test:
-                if (self._dist_include_testing not in
-                        self._extra_includes[obj.objdir]):
-                    self._extra_includes[obj.objdir].append(
-                        self._dist_include_testing)
-
         elif isinstance(obj, VariablePassthru):
-            if obj.variables.get('IS_GYP_DIR'):
-                self._gyp_dirs.add(obj.objdir)
             for var in ('MOZBUILD_CFLAGS', 'MOZBUILD_CXXFLAGS',
                         'MOZBUILD_CMFLAGS', 'MOZBUILD_CMMFLAGS',
                         'RTL_FLAGS'):
                 if var in obj.variables:
                     self._local_flags[obj.objdir][var] = obj.variables[var]
             if (obj.variables.get('ALLOW_COMPILER_WARNINGS') and
                     'WARNINGS_AS_ERRORS' in self._local_flags[obj.objdir]):
                 del self._local_flags[obj.objdir]['WARNINGS_AS_ERRORS']
@@ -126,33 +100,17 @@ class CompileDBBackend(CommonBackend):
 
         for (directory, filename, unified), cmd in self._db.iteritems():
             env = self._envs[directory]
             cmd = list(cmd)
             if unified is None:
                 cmd.append(filename)
             else:
                 cmd.append(unified)
-            local_extra = list(self._extra_includes[directory])
-            if directory not in self._gyp_dirs:
-                for var in (
-                    'NSPR_CFLAGS',
-                    'NSS_CFLAGS',
-                    'MOZ_JPEG_CFLAGS',
-                    'MOZ_PNG_CFLAGS',
-                    'MOZ_ZLIB_CFLAGS',
-                    'MOZ_PIXMAN_CFLAGS',
-                ):
-                    f = env.substs.get(var)
-                    if f:
-                        local_extra.extend(f)
             variables = {
-                'LOCAL_INCLUDES': self._includes[directory],
-                'DEFINES': self._defines[directory],
-                'EXTRA_INCLUDES': local_extra,
                 'DIST': mozpath.join(env.topobjdir, 'dist'),
                 'DEPTH': env.topobjdir,
                 'MOZILLA_DIR': env.topsrcdir,
                 'topsrcdir': env.topsrcdir,
                 'topobjdir': env.topobjdir,
             }
             variables.update(self._local_flags[directory])
             c = []
@@ -233,24 +191,16 @@ class CompileDBBackend(CommonBackend):
             if not value:
                 return
             if isinstance(value, types.StringTypes):
                 value = value.split()
             db.extend(value)
 
         db.append('$(COMPUTED_%s)' % self.CFLAGS[canonical_suffix])
 
-        db.extend((
-            '$(DEFINES)',
-            '-I%s' % mozpath.join(cenv.topsrcdir, reldir),
-            '-I%s' % objdir,
-            '$(LOCAL_INCLUDES)',
-            '-I%s/dist/include' % cenv.topobjdir,
-            '$(EXTRA_INCLUDES)',
-        ))
         append_var('DSO_CFLAGS')
         append_var('DSO_PIC_CFLAGS')
         if canonical_suffix in ('.c', '.cpp'):
             db.append('$(RTL_FLAGS)')
         append_var('OS_COMPILE_%s' % self.CFLAGS[canonical_suffix])
         append_var('OS_CPPFLAGS')
         append_var('OS_%s' % self.CFLAGS[canonical_suffix])
         append_var('MOZ_DEBUG_FLAGS')
--- a/python/mozbuild/mozbuild/frontend/context.py
+++ b/python/mozbuild/mozbuild/frontend/context.py
@@ -294,37 +294,54 @@ class InitializedDefines(ContextDerivedV
         for define in context.config.substs.get('MOZ_DEBUG_DEFINES', ()):
             self[define] = 1
         if value:
             self.update(value)
 
 
 class CompileFlags(ContextDerivedValue, dict):
     def __init__(self, context):
+        main_src_dir = mozpath.dirname(context.main_path)
+
         self.flag_variables = (
             ('STL', context.config.substs.get('STL_FLAGS'), ('CXXFLAGS',)),
             ('VISIBILITY', context.config.substs.get('VISIBILITY_FLAGS'),
              ('CXXFLAGS', 'CFLAGS')),
+            ('DEFINES', None, ('CXXFLAGS', 'CFLAGS')),
+            ('LIBRARY_DEFINES', None, ('CXXFLAGS', 'CFLAGS')),
+            ('BASE_INCLUDES', ['-I%s' % main_src_dir, '-I%s' % context.objdir],
+             ('CXXFLAGS', 'CFLAGS')),
+            ('LOCAL_INCLUDES', None, ('CXXFLAGS', 'CFLAGS')),
+            ('EXTRA_INCLUDES', ['-I%s/dist/include' % context.config.topobjdir],
+             ('CXXFLAGS', 'CFLAGS')),
+            ('OS_INCLUDES', list(itertools.chain(*(context.config.substs.get(v, []) for v in (
+                'NSPR_CFLAGS', 'NSS_CFLAGS', 'MOZ_JPEG_CFLAGS', 'MOZ_PNG_CFLAGS',
+                'MOZ_ZLIB_CFLAGS', 'MOZ_PIXMAN_CFLAGS')))),
+             ('CXXFLAGS', 'CFLAGS')),
         )
         self._known_keys = set(k for k, v, _ in self.flag_variables)
 
         # Providing defaults here doesn't play well with multiple templates
         # modifying COMPILE_FLAGS from the same moz.build, because the merge
         # done after the template runs can't tell which values coming from
         # a template were set and which were provided as defaults.
         template_name = getattr(context, 'template', None)
         if template_name in (None, 'Gyp'):
-            dict.__init__(self, ((k, TypedList(unicode)(v)) for k, v, _ in self.flag_variables))
+            dict.__init__(self, ((k, v if v is None else TypedList(unicode)(v))
+                                 for k, v, _ in self.flag_variables))
         else:
             dict.__init__(self)
 
     def __setitem__(self, key, value):
         if key not in self._known_keys:
             raise ValueError('Invalid value. `%s` is not a compile flags '
                              'category.' % key)
+        if key in self and self[key] is None:
+            raise ValueError('`%s` may not be set in COMPILE_FLAGS from moz.build, this '
+                             'value is resolved from the emitter.' % key)
         if not (isinstance(value, list) and all(isinstance(v, unicode) for v in value)):
             raise ValueError('A list of strings must be provided as a value for a '
                              'compile flags category.')
         dict.__setitem__(self, key, value)
 
 
 class FinalTargetValue(ContextDerivedValue, unicode):
     def __new__(cls, context, value=""):
--- a/python/mozbuild/mozbuild/frontend/data.py
+++ b/python/mozbuild/mozbuild/frontend/data.py
@@ -164,16 +164,21 @@ class ComputedFlags(ContextDerived):
     """Aggregate flags for consumption by various backends.
     """
     __slots__ = ('flags',)
 
     def __init__(self, context, reader_flags):
         ContextDerived.__init__(self, context)
         self.flags = reader_flags
 
+    def resolve_flags(self, key, value):
+        # Bypass checks done by CompileFlags that would keep us from
+        # setting a value here.
+        dict.__setitem__(self.flags, key, value)
+
     def get_flags(self):
         flags = defaultdict(list)
         for key, _, dest_vars in self.flags.flag_variables:
             value = self.flags.get(key)
             if value:
                 for dest_var in dest_vars:
                     flags[dest_var].extend(value)
         return flags.items()
--- a/python/mozbuild/mozbuild/frontend/emitter.py
+++ b/python/mozbuild/mozbuild/frontend/emitter.py
@@ -125,16 +125,17 @@ class TreeMetadataEmitter(LoggingMixin):
         for k, v in mozinfo.info.items():
             if isinstance(k, unicode):
                 k = k.encode('ascii')
             self.info[k] = v
 
         self._libs = OrderedDefaultDict(list)
         self._binaries = OrderedDict()
         self._compile_dirs = set()
+        self._compile_flags = dict()
         self._linkage = []
         self._static_linking_shared = set()
         self._crate_verified_local = set()
         self._crate_directories = dict()
 
         # Keep track of external paths (third party build systems), starting
         # from what we run a subconfigure in. We'll eliminate some directories
         # as we traverse them with moz.build (e.g. js/src).
@@ -259,16 +260,26 @@ class TreeMetadataEmitter(LoggingMixin):
                         lib.link_into == outerlib.basename):
                     propagate_defines(lib, defines)
 
         for lib in (l for libs in self._libs.values() for l in libs):
             if isinstance(lib, Library):
                 propagate_defines(lib, lib.lib_defines)
             yield lib
 
+
+        for lib in (l for libs in self._libs.values() for l in libs):
+            lib_defines = list(lib.lib_defines.get_defines())
+            if lib_defines:
+                objdir_flags = self._compile_flags[lib.objdir]
+                objdir_flags.resolve_flags('LIBRARY_DEFINES', lib_defines)
+
+        for flags_obj in self._compile_flags.values():
+            yield flags_obj
+
         for obj in self._binaries.values():
             yield obj
 
     LIBRARY_NAME_VAR = {
         'host': 'HOST_LIBRARY_NAME',
         'target': 'LIBRARY_NAME',
     }
 
@@ -754,17 +765,22 @@ class TreeMetadataEmitter(LoggingMixin):
                 lib.lib_defines.update(lib_defines)
 
         # Only emit sources if we have linkables defined in the same context.
         # Note the linkables are not emitted in this function, but much later,
         # after aggregation (because of e.g. USE_LIBS processing).
         if not (linkables or host_linkables):
             return
 
-        self._compile_dirs.add(context.objdir)
+        # Avoid emitting compile flags for directories only containing rust
+        # libraries. Emitted compile flags are only relevant to C/C++ sources
+        # for the time being.
+        if not all(isinstance(l, (RustLibrary, HostRustLibrary))
+                   for l in linkables + host_linkables):
+            self._compile_dirs.add(context.objdir)
 
         sources = defaultdict(list)
         gen_sources = defaultdict(list)
         all_flags = {}
         for symbol in ('SOURCES', 'HOST_SOURCES', 'UNIFIED_SOURCES'):
             srcs = sources[symbol]
             gen_srcs = gen_sources[symbol]
             context_srcs = context.get(symbol, [])
@@ -930,19 +946,16 @@ class TreeMetadataEmitter(LoggingMixin):
                 for dll in context['DELAYLOAD_DLLS']])
             context['OS_LIBS'].append('delayimp')
 
         for v in ['CFLAGS', 'CXXFLAGS', 'CMFLAGS', 'CMMFLAGS', 'ASFLAGS',
                   'LDFLAGS', 'HOST_CFLAGS', 'HOST_CXXFLAGS']:
             if v in context and context[v]:
                 passthru.variables['MOZBUILD_' + v] = context[v]
 
-        if isinstance(context, TemplateContext) and context.template == 'Gyp':
-            passthru.variables['IS_GYP_DIR'] = True
-
         dist_install = context['DIST_INSTALL']
         if dist_install is True:
             passthru.variables['DIST_INSTALL'] = True
         elif dist_install is False:
             passthru.variables['NO_DIST_INSTALL'] = True
 
         # Ideally, this should be done in templates, but this is difficult at
         # the moment because USE_STATIC_LIBS can be set after a template
@@ -966,45 +979,62 @@ class TreeMetadataEmitter(LoggingMixin):
             yield obj
 
         for path in context['CONFIGURE_SUBST_FILES']:
             sub = self._create_substitution(ConfigFileSubstitution, context,
                 path)
             generated_files.add(str(sub.relpath))
             yield sub
 
-        defines = context.get('DEFINES')
-        if defines:
-            yield Defines(context, defines)
+        computed_flags = ComputedFlags(context, context['COMPILE_FLAGS'])
 
-        host_defines = context.get('HOST_DEFINES')
-        if host_defines:
-            yield HostDefines(context, host_defines)
+        for defines_var, cls in (('DEFINES', Defines),
+                                 ('HOST_DEFINES', HostDefines)):
+            defines = context.get(defines_var)
+            if defines:
+                defines_obj = cls(context, defines)
+                yield defines_obj
+            else:
+                # If we don't have explicitly set defines we need to make sure
+                # initialized values if present end up in computed flags.
+                defines_obj = cls(context, context[defines_var])
+
+            if isinstance(defines_obj, Defines):
+                defines_from_obj = list(defines_obj.get_defines())
+                if defines_from_obj:
+                    computed_flags.resolve_flags('DEFINES',
+                                                 defines_from_obj)
+
 
         simple_lists = [
             ('GENERATED_EVENTS_WEBIDL_FILES', GeneratedEventWebIDLFile),
             ('GENERATED_WEBIDL_FILES', GeneratedWebIDLFile),
             ('IPDL_SOURCES', IPDLFile),
             ('PREPROCESSED_TEST_WEBIDL_FILES', PreprocessedTestWebIDLFile),
             ('PREPROCESSED_WEBIDL_FILES', PreprocessedWebIDLFile),
             ('TEST_WEBIDL_FILES', TestWebIDLFile),
             ('WEBIDL_FILES', WebIDLFile),
             ('WEBIDL_EXAMPLE_INTERFACES', ExampleWebIDLInterface),
         ]
         for context_var, klass in simple_lists:
             for name in context.get(context_var, []):
                 yield klass(context, name)
 
+        local_includes = []
         for local_include in context.get('LOCAL_INCLUDES', []):
             if (not isinstance(local_include, ObjDirPath) and
                     not os.path.exists(local_include.full_path)):
                 raise SandboxValidationError('Path specified in LOCAL_INCLUDES '
                     'does not exist: %s (resolved to %s)' % (local_include,
                     local_include.full_path), context)
-            yield LocalInclude(context, local_include)
+            include_obj = LocalInclude(context, local_include)
+            local_includes.append(include_obj.path.full_path)
+            yield include_obj
+
+        computed_flags.resolve_flags('LOCAL_INCLUDES', ['-I%s' % p for p in local_includes])
 
         for obj in self._handle_linkables(context, passthru, generated_files):
             yield obj
 
         generated_files.update(['%s%s' % (k, self.config.substs.get('BIN_SUFFIX', '')) for k in self._binaries.keys()])
 
         components = []
         for var, cls in (
@@ -1128,17 +1158,18 @@ class TreeMetadataEmitter(LoggingMixin):
         android_extra_packages = context.get('ANDROID_EXTRA_PACKAGES')
         if android_extra_packages:
             yield AndroidExtraPackages(context, android_extra_packages)
 
         if passthru.variables:
             yield passthru
 
         if context.objdir in self._compile_dirs:
-            yield ComputedFlags(context, context['COMPILE_FLAGS'])
+            self._compile_flags[context.objdir] = computed_flags
+
 
     def _create_substitution(self, cls, context, path):
         sub = cls(context)
         sub.input_path = '%s.in' % path.full_path
         sub.output_path = path.translated
         sub.relpath = path
 
         return sub
--- a/python/mozbuild/mozbuild/frontend/gyp_reader.py
+++ b/python/mozbuild/mozbuild/frontend/gyp_reader.py
@@ -322,16 +322,17 @@ def process_gyp_result(gyp_result, gyp_d
               '/ipc/chromium/src',
               '/ipc/glue',
           ]
           # These get set via VC project file settings for normal GYP builds.
           if config.substs['OS_TARGET'] == 'WINNT':
               context['DEFINES']['UNICODE'] = True
               context['DEFINES']['_UNICODE'] = True
         context['COMPILE_FLAGS']['STL'] = []
+        context['COMPILE_FLAGS']['OS_INCLUDES'] = []
 
         for key, value in gyp_dir_attrs.sandbox_vars.items():
             if context.get(key) and isinstance(context[key], list):
                 # If we have a key from sanbox_vars that's also been
                 # populated here we use the value from sandbox_vars as our
                 # basis rather than overriding outright.
                 context[key] = value + context[key]
             elif context.get(key) and isinstance(context[key], dict):
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/frontend/data/compile-defines/moz.build
@@ -0,0 +1,14 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+@template
+def Library(name):
+    '''Template for libraries.'''
+    LIBRARY_NAME = name
+
+Library('dummy')
+
+UNIFIED_SOURCES += ['test1.c']
+
+DEFINES['MOZ_TEST_DEFINE'] = True
+LIBRARY_DEFINES['MOZ_LIBRARY_DEFINE'] = 'MOZ_TEST'
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/frontend/data/compile-includes/moz.build
@@ -0,0 +1,13 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+@template
+def Library(name):
+    '''Template for libraries.'''
+    LIBRARY_NAME = name
+
+Library('dummy')
+
+UNIFIED_SOURCES += ['test1.c']
+
+LOCAL_INCLUDES += ['subdir']
\ No newline at end of file
new file mode 100644
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/frontend/data/resolved-flags-error/moz.build
@@ -0,0 +1,15 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+@template
+def Library(name):
+    '''Template for libraries.'''
+    LIBRARY_NAME = name
+
+Library('dummy')
+
+UNIFIED_SOURCES += ['test1.c']
+
+DEFINES['MOZ_TEST_DEFINE'] = True
+LIBRARY_DEFINES['MOZ_LIBRARY_DEFINE'] = 'MOZ_TEST'
+COMPILE_FLAGS['DEFINES'] = ['-DFOO']
\ No newline at end of file
new file mode 100644
--- a/python/mozbuild/mozbuild/test/frontend/test_emitter.py
+++ b/python/mozbuild/mozbuild/test/frontend/test_emitter.py
@@ -202,51 +202,93 @@ class TestEmitterBasic(unittest.TestCase
         variables = objs[0].variables
         maxDiff = self.maxDiff
         self.maxDiff = None
         self.assertEqual(wanted, variables)
         self.maxDiff = maxDiff
 
     def test_compile_flags(self):
         reader = self.reader('compile-flags')
-        sources, flags, lib = self.read_topsrcdir(reader)
+        sources, lib, flags = self.read_topsrcdir(reader)
         self.assertIsInstance(flags, ComputedFlags)
         self.assertEqual(flags.flags['STL'], reader.config.substs['STL_FLAGS'])
         self.assertEqual(flags.flags['VISIBILITY'], reader.config.substs['VISIBILITY_FLAGS'])
 
     def test_compile_flags_validation(self):
         reader = self.reader('compile-flags-field-validation')
 
         with self.assertRaisesRegexp(BuildReaderError, 'Invalid value.'):
             self.read_topsrcdir(reader)
 
         reader = self.reader('compile-flags-type-validation')
         with self.assertRaisesRegexp(BuildReaderError,
                                      'A list of strings must be provided'):
             self.read_topsrcdir(reader)
 
     def test_compile_flags_templates(self):
-        reader = self.reader('compile-flags-templates')
-        sources, flags, lib = self.read_topsrcdir(reader)
+        reader = self.reader('compile-flags-templates', extra_substs={
+            'NSPR_CFLAGS': ['-I/nspr/path'],
+            'NSS_CFLAGS': ['-I/nss/path'],
+            'MOZ_JPEG_CFLAGS': ['-I/jpeg/path'],
+            'MOZ_PNG_CFLAGS': ['-I/png/path'],
+            'MOZ_ZLIB_CFLAGS': ['-I/zlib/path'],
+            'MOZ_PIXMAN_CFLAGS': ['-I/pixman/path'],
+        })
+        sources, lib, flags = self.read_topsrcdir(reader)
         self.assertIsInstance(flags, ComputedFlags)
         self.assertEqual(flags.flags['STL'], [])
         self.assertEqual(flags.flags['VISIBILITY'], [])
+        self.assertEqual(flags.flags['OS_INCLUDES'], [
+            '-I/nspr/path',
+            '-I/nss/path',
+            '-I/jpeg/path',
+            '-I/png/path',
+            '-I/zlib/path',
+            '-I/pixman/path',
+        ])
 
     def test_disable_stl_wrapping(self):
         reader = self.reader('disable-stl-wrapping')
-        sources, flags, lib = self.read_topsrcdir(reader)
+        sources, lib, flags = self.read_topsrcdir(reader)
         self.assertIsInstance(flags, ComputedFlags)
         self.assertEqual(flags.flags['STL'], [])
 
     def test_visibility_flags(self):
         reader = self.reader('visibility-flags')
-        sources, flags, lib = self.read_topsrcdir(reader)
+        sources, lib, flags = self.read_topsrcdir(reader)
         self.assertIsInstance(flags, ComputedFlags)
         self.assertEqual(flags.flags['VISIBILITY'], [])
 
+    def test_defines_in_flags(self):
+        reader = self.reader('compile-defines')
+        defines, sources, lib, flags = self.read_topsrcdir(reader)
+        self.assertIsInstance(flags, ComputedFlags)
+        self.assertEqual(flags.flags['LIBRARY_DEFINES'],
+                         ['-DMOZ_LIBRARY_DEFINE=MOZ_TEST'])
+        self.assertEqual(flags.flags['DEFINES'],
+                         ['-DMOZ_TEST_DEFINE'])
+
+    def test_resolved_flags_error(self):
+        reader = self.reader('resolved-flags-error')
+        with self.assertRaisesRegexp(BuildReaderError,
+            "`DEFINES` may not be set in COMPILE_FLAGS from moz.build"):
+            self.read_topsrcdir(reader)
+
+    def test_includes_in_flags(self):
+        reader = self.reader('compile-includes')
+        defines, sources, lib, flags = self.read_topsrcdir(reader)
+        self.assertIsInstance(flags, ComputedFlags)
+        self.assertEqual(flags.flags['BASE_INCLUDES'],
+                         ['-I%s' % reader.config.topsrcdir,
+                          '-I%s' % reader.config.topobjdir])
+        self.assertEqual(flags.flags['EXTRA_INCLUDES'],
+                         ['-I%s/dist/include' % reader.config.topobjdir])
+        self.assertEqual(flags.flags['LOCAL_INCLUDES'],
+                         ['-I%s/subdir' % reader.config.topsrcdir])
+
     def test_use_yasm(self):
         # When yasm is not available, this should raise.
         reader = self.reader('use-yasm')
         with self.assertRaisesRegexp(SandboxValidationError,
             'yasm is not available'):
             self.read_topsrcdir(reader)
 
         # When yasm is available, this should work.
@@ -832,37 +874,42 @@ class TestEmitterBasic(unittest.TestCase
             self.read_topsrcdir(reader)
 
     def test_library_defines(self):
         """Test that LIBRARY_DEFINES is propagated properly."""
         reader = self.reader('library-defines')
         objs = self.read_topsrcdir(reader)
 
         libraries = [o for o in objs if isinstance(o,StaticLibrary)]
+        library_flags = [o for o in objs if isinstance(o, ComputedFlags)]
         expected = {
             'liba': '-DIN_LIBA',
             'libb': '-DIN_LIBA -DIN_LIBB',
             'libc': '-DIN_LIBA -DIN_LIBB',
             'libd': ''
         }
         defines = {}
         for lib in libraries:
             defines[lib.basename] = ' '.join(lib.lib_defines.get_defines())
         self.assertEqual(expected, defines)
+        defines_in_flags = {}
+        for flags in library_flags:
+            defines_in_flags[flags.relobjdir] = ' '.join(flags.flags['LIBRARY_DEFINES'] or [])
+        self.assertEqual(expected, defines_in_flags)
 
     def test_sources(self):
         """Test that SOURCES works properly."""
         reader = self.reader('sources')
         objs = self.read_topsrcdir(reader)
 
-        # The last object is a Linkable.
+        computed_flags = objs.pop()
+        self.assertIsInstance(computed_flags, ComputedFlags)
+        # The second to last object is a Linkable.
         linkable = objs.pop()
         self.assertTrue(linkable.cxx_link)
-        computed_flags = objs.pop()
-        self.assertIsInstance(computed_flags, ComputedFlags)
         self.assertEqual(len(objs), 6)
         for o in objs:
             self.assertIsInstance(o, Sources)
 
         suffix_map = {obj.canonical_suffix: obj for obj in objs}
         self.assertEqual(len(suffix_map), 6)
 
         expected = {
@@ -879,17 +926,19 @@ class TestEmitterBasic(unittest.TestCase
                 sources.files,
                 [mozpath.join(reader.config.topsrcdir, f) for f in files])
 
     def test_sources_just_c(self):
         """Test that a linkable with no C++ sources doesn't have cxx_link set."""
         reader = self.reader('sources-just-c')
         objs = self.read_topsrcdir(reader)
 
-        # The last object is a Linkable.
+        flags = objs.pop()
+        self.assertIsInstance(flags, ComputedFlags)
+        # The second to last object is a Linkable.
         linkable = objs.pop()
         self.assertFalse(linkable.cxx_link)
 
     def test_linkables_cxx_link(self):
         """Test that linkables transitively set cxx_link properly."""
         reader = self.reader('test-linkables-cxx-link')
         got_results = 0
         for obj in self.read_topsrcdir(reader):
@@ -902,20 +951,22 @@ class TestEmitterBasic(unittest.TestCase
                     got_results += 1
         self.assertEqual(got_results, 2)
 
     def test_generated_sources(self):
         """Test that GENERATED_SOURCES works properly."""
         reader = self.reader('generated-sources')
         objs = self.read_topsrcdir(reader)
 
-        # The last object is a Linkable.
+        flags = objs.pop()
+        self.assertIsInstance(flags, ComputedFlags)
+        # The second to last object is a Linkable.
         linkable = objs.pop()
         self.assertTrue(linkable.cxx_link)
-        self.assertEqual(len(objs), 7)
+        self.assertEqual(len(objs), 6)
 
         generated_sources = [o for o in objs if isinstance(o, GeneratedSources)]
         self.assertEqual(len(generated_sources), 6)
 
         suffix_map = {obj.canonical_suffix: obj for obj in generated_sources}
         self.assertEqual(len(suffix_map), 6)
 
         expected = {
@@ -932,21 +983,21 @@ class TestEmitterBasic(unittest.TestCase
                 sources.files,
                 [mozpath.join(reader.config.topobjdir, f) for f in files])
 
     def test_host_sources(self):
         """Test that HOST_SOURCES works properly."""
         reader = self.reader('host-sources')
         objs = self.read_topsrcdir(reader)
 
-        # The last object is a Linkable
+        flags = objs.pop()
+        self.assertIsInstance(flags, ComputedFlags)
+        # The second to last object is a Linkable
         linkable = objs.pop()
         self.assertTrue(linkable.cxx_link)
-        computed_flags = objs.pop()
-        self.assertIsInstance(computed_flags, ComputedFlags)
         self.assertEqual(len(objs), 3)
         for o in objs:
             self.assertIsInstance(o, HostSources)
 
         suffix_map = {obj.canonical_suffix: obj for obj in objs}
         self.assertEqual(len(suffix_map), 3)
 
         expected = {
@@ -1089,19 +1140,18 @@ class TestEmitterBasic(unittest.TestCase
             self.read_topsrcdir(reader)
 
     def test_rust_library_dash_folding(self):
         '''Test that on-disk names of RustLibrary objects convert dashes to underscores.'''
         reader = self.reader('rust-library-dash-folding',
                              extra_substs=dict(RUST_TARGET='i686-pc-windows-msvc'))
         objs = self.read_topsrcdir(reader)
 
-        self.assertEqual(len(objs), 2)
-        flags, lib = objs
-        self.assertIsInstance(flags, ComputedFlags)
+        self.assertEqual(len(objs), 1)
+        lib = objs[0]
         self.assertIsInstance(lib, RustLibrary)
         self.assertRegexpMatches(lib.lib_name, "random_crate")
         self.assertRegexpMatches(lib.import_name, "random_crate")
         self.assertRegexpMatches(lib.basename, "random-crate")
 
     def test_multiple_rust_libraries(self):
         '''Test that linking multiple Rust libraries throws an error'''
         reader = self.reader('multiple-rust-libraries',
@@ -1110,19 +1160,18 @@ class TestEmitterBasic(unittest.TestCase
              'Cannot link multiple Rust libraries'):
             self.read_topsrcdir(reader)
 
     def test_rust_library_features(self):
         '''Test that RustLibrary features are correctly emitted.'''
         reader = self.reader('rust-library-features',
                              extra_substs=dict(RUST_TARGET='i686-pc-windows-msvc'))
         objs = self.read_topsrcdir(reader)
-        self.assertEqual(len(objs), 2)
-        flags, lib = objs
-        self.assertIsInstance(flags, ComputedFlags)
+        self.assertEqual(len(objs), 1)
+        lib = objs[0]
         self.assertIsInstance(lib, RustLibrary)
         self.assertEqual(lib.features, ['musthave', 'cantlivewithout'])
 
     def test_rust_library_duplicate_features(self):
         '''Test that duplicate RustLibrary features are rejected.'''
         reader = self.reader('rust-library-duplicate-features')
         with self.assertRaisesRegexp(SandboxValidationError,
              'features for .* should not contain duplicates'):
@@ -1181,29 +1230,29 @@ class TestEmitterBasic(unittest.TestCase
         self.assertEqual(objs[0].name, 'some')
 
     def test_host_rust_libraries(self):
         '''Test HOST_RUST_LIBRARIES emission.'''
         reader = self.reader('host-rust-libraries',
                              extra_substs=dict(RUST_HOST_TARGET='i686-pc-windows-msvc',
                                                HOST_BIN_SUFFIX='.exe'))
         objs = self.read_topsrcdir(reader)
-        self.assertEqual(len(objs), 2)
-        self.assertIsInstance(objs[1], HostRustLibrary)
-        self.assertRegexpMatches(objs[1].lib_name, 'host_lib')
-        self.assertRegexpMatches(objs[1].import_name, 'host_lib')
+        self.assertEqual(len(objs), 1)
+        self.assertIsInstance(objs[0], HostRustLibrary)
+        self.assertRegexpMatches(objs[0].lib_name, 'host_lib')
+        self.assertRegexpMatches(objs[0].import_name, 'host_lib')
 
     def test_crate_dependency_path_resolution(self):
         '''Test recursive dependencies resolve with the correct paths.'''
         reader = self.reader('crate-dependency-path-resolution',
                              extra_substs=dict(RUST_TARGET='i686-pc-windows-msvc'))
         objs = self.read_topsrcdir(reader)
 
-        self.assertEqual(len(objs), 2)
-        self.assertIsInstance(objs[1], RustLibrary)
+        self.assertEqual(len(objs), 1)
+        self.assertIsInstance(objs[0], RustLibrary)
 
     def test_android_res_dirs(self):
         """Test that ANDROID_RES_DIRS works properly."""
         reader = self.reader('android-res-dirs')
         objs = self.read_topsrcdir(reader)
 
         self.assertEqual(len(objs), 1)
         self.assertIsInstance(objs[0], AndroidResDirs)
@@ -1217,38 +1266,38 @@ class TestEmitterBasic(unittest.TestCase
         self.assertEquals([p.full_path for p in objs[0].paths], expected)
 
     def test_install_shared_lib(self):
         """Test that we can install a shared library with TEST_HARNESS_FILES"""
         reader = self.reader('test-install-shared-lib')
         objs = self.read_topsrcdir(reader)
         self.assertIsInstance(objs[0], TestHarnessFiles)
         self.assertIsInstance(objs[1], VariablePassthru)
-        self.assertIsInstance(objs[2], ComputedFlags)
-        self.assertIsInstance(objs[3], SharedLibrary)
+        self.assertIsInstance(objs[2], SharedLibrary)
+        self.assertIsInstance(objs[3], ComputedFlags)
         for path, files in objs[0].files.walk():
             for f in files:
                 self.assertEqual(str(f), '!libfoo.so')
                 self.assertEqual(path, 'foo/bar')
 
     def test_symbols_file(self):
         """Test that SYMBOLS_FILE works"""
         reader = self.reader('test-symbols-file')
-        genfile, flags, shlib = self.read_topsrcdir(reader)
+        genfile, shlib, flags = self.read_topsrcdir(reader)
         self.assertIsInstance(genfile, GeneratedFile)
         self.assertIsInstance(flags, ComputedFlags)
         self.assertIsInstance(shlib, SharedLibrary)
         # This looks weird but MockConfig sets DLL_{PREFIX,SUFFIX} and
         # the reader method in this class sets OS_TARGET=WINNT.
         self.assertEqual(shlib.symbols_file, 'libfoo.so.def')
 
     def test_symbols_file_objdir(self):
         """Test that a SYMBOLS_FILE in the objdir works"""
         reader = self.reader('test-symbols-file-objdir')
-        genfile, flags, shlib = self.read_topsrcdir(reader)
+        genfile, shlib, flags = self.read_topsrcdir(reader)
         self.assertIsInstance(genfile, GeneratedFile)
         self.assertEqual(genfile.script,
                          mozpath.join(reader.config.topsrcdir, 'foo.py'))
         self.assertIsInstance(flags, ComputedFlags)
         self.assertIsInstance(shlib, SharedLibrary)
         self.assertEqual(shlib.symbols_file, 'foo.symbols')
 
     def test_symbols_file_objdir_missing_generated(self):
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -814,18 +814,19 @@ Collection.prototype = {
       max_post_records: getConfig("max_post_records", Infinity),
 
       max_batch_bytes: getConfig("max_total_bytes", Infinity),
       max_batch_records: getConfig("max_total_records", Infinity),
       max_record_payload_bytes: getConfig("max_record_payload_bytes", 256 * 1024),
     };
 
     if (config.max_post_bytes <= config.max_record_payload_bytes) {
-      this._log.error("Server configuration max_post_bytes is too low", config);
-      throw new Error("Server configuration max_post_bytes is too low");
+      this._log.warn("Server configuration max_post_bytes is too low for max_record_payload_bytes", config);
+      // Assume 4k of extra is enough. See also getMaxRecordPayloadSize in service.js
+      config.max_record_payload_bytes = config.max_post_bytes - 4096;
     }
 
     this._log.trace("new PostQueue created with config", config);
     return new PostQueue(poster, timestamp, config, log, postCallback);
   },
 };
 
 /* A helper to manage the posting of records while respecting the various
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -620,17 +620,21 @@ Sync11Service.prototype = {
 
   getMaxRecordPayloadSize() {
     let config = this.serverConfiguration;
     if (!config || !config.max_record_payload_bytes) {
       this._log.warn("No config or incomplete config in getMaxRecordPayloadSize."
                      + " Are we running tests?");
       return 256 * 1024;
     }
-    return config.max_record_payload_bytes;
+    let payloadMax = config.max_record_payload_bytes;
+    if (config.max_post_bytes && payloadMax <= config.max_post_bytes) {
+      return config.max_post_bytes - 4096;
+    }
+    return payloadMax;
   },
 
   async verifyLogin(allow40XRecovery = true) {
     if (!this.identity.username) {
       this._log.warn("No username in verifyLogin.");
       this.status.login = LOGIN_FAILED_NO_USERNAME;
       return false;
     }
--- a/servo/Cargo.lock
+++ b/servo/Cargo.lock
@@ -317,34 +317,34 @@ dependencies = [
  "cssparser 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "euclid 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "gleam 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "ipc-channel 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "offscreen_gl_context 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "webrender 0.50.0 (git+https://github.com/servo/webrender)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender 0.51.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "canvas_traits"
 version = "0.0.1"
 dependencies = [
  "cssparser 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "euclid 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize_derive 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "ipc-channel 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "offscreen_gl_context 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo_config 0.0.1",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "caseless"
 version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -485,18 +485,18 @@ dependencies = [
  "net_traits 0.0.1",
  "profile_traits 0.0.1",
  "script_traits 0.0.1",
  "servo_config 0.0.1",
  "servo_geometry 0.0.1",
  "servo_url 0.0.1",
  "style_traits 0.0.1",
  "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
- "webrender 0.50.0 (git+https://github.com/servo/webrender)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender 0.51.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "constellation"
 version = "0.0.1"
 dependencies = [
  "backtrace 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "bluetooth_traits 0.0.1",
@@ -522,17 +522,17 @@ dependencies = [
  "profile_traits 0.0.1",
  "script_traits 0.0.1",
  "serde 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo_config 0.0.1",
  "servo_rand 0.0.1",
  "servo_remutex 0.0.1",
  "servo_url 0.0.1",
  "style_traits 0.0.1",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
  "webvr_traits 0.0.1",
 ]
 
 [[package]]
 name = "cookie"
 version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
@@ -797,17 +797,17 @@ dependencies = [
  "msg 0.0.1",
  "net_traits 0.0.1",
  "objc 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "script_traits 0.0.1",
  "servo_config 0.0.1",
  "servo_geometry 0.0.1",
  "servo_url 0.0.1",
  "style_traits 0.0.1",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
  "x11 2.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "encoding"
 version = "0.2.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
@@ -1120,17 +1120,17 @@ dependencies = [
  "simd 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "smallvec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "style 0.0.1",
  "style_traits 0.0.1",
  "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "truetype 0.26.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicode-bidi 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicode-script 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
  "xi-unicode 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "xml5ever 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "gfx_tests"
 version = "0.0.1"
 dependencies = [
@@ -1201,17 +1201,17 @@ dependencies = [
  "script_traits 0.0.1",
  "servo-egl 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo-glutin 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo_config 0.0.1",
  "servo_geometry 0.0.1",
  "servo_url 0.0.1",
  "style_traits 0.0.1",
  "user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
  "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "x11 2.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "glx"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1531,17 +1531,17 @@ dependencies = [
  "servo_config 0.0.1",
  "servo_geometry 0.0.1",
  "servo_url 0.0.1",
  "smallvec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "style 0.0.1",
  "style_traits 0.0.1",
  "unicode-bidi 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicode-script 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "layout_tests"
 version = "0.0.1"
 dependencies = [
  "layout 0.0.1",
  "size_of_test 0.0.1",
@@ -1578,32 +1578,32 @@ dependencies = [
  "serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo_arc 0.0.1",
  "servo_atoms 0.0.1",
  "servo_config 0.0.1",
  "servo_geometry 0.0.1",
  "servo_url 0.0.1",
  "style 0.0.1",
  "style_traits 0.0.1",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "layout_traits"
 version = "0.0.1"
 dependencies = [
  "gfx 0.0.1",
  "ipc-channel 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "metrics 0.0.1",
  "msg 0.0.1",
  "net_traits 0.0.1",
  "profile_traits 0.0.1",
  "script_traits 0.0.1",
  "servo_url 0.0.1",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "lazy_static"
 version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
@@ -1677,18 +1677,18 @@ dependencies = [
  "script_layout_interface 0.0.1",
  "script_traits 0.0.1",
  "servo_config 0.0.1",
  "servo_geometry 0.0.1",
  "servo_url 0.0.1",
  "style 0.0.1",
  "style_traits 0.0.1",
  "webdriver_server 0.0.1",
- "webrender 0.50.0 (git+https://github.com/servo/webrender)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender 0.51.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
  "webvr 0.0.1",
  "webvr_traits 0.0.1",
 ]
 
 [[package]]
 name = "libz-sys"
 version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1700,16 +1700,23 @@ dependencies = [
 ]
 
 [[package]]
 name = "log"
 version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
+name = "lru_cache"
+version = "0.0.1"
+dependencies = [
+ "arrayvec 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "lzw"
 version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "mac"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1893,17 +1900,17 @@ dependencies = [
 [[package]]
 name = "msg"
 version = "0.0.1"
 dependencies = [
  "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize_derive 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "msg_tests"
 version = "0.0.1"
 dependencies = [
  "msg 0.0.1",
  "size_of_test 0.0.1",
@@ -1951,17 +1958,17 @@ dependencies = [
  "servo_config 0.0.1",
  "servo_url 0.0.1",
  "threadpool 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "tinyfiledialogs 2.5.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "uuid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "net2"
 version = "0.2.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2008,17 +2015,17 @@ dependencies = [
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "msg 0.0.1",
  "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo_config 0.0.1",
  "servo_url 0.0.1",
  "url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "uuid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "net_traits_tests"
 version = "0.0.1"
 dependencies = [
  "net_traits 0.0.1",
 ]
@@ -2620,17 +2627,17 @@ dependencies = [
  "style_traits 0.0.1",
  "swapper 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "tinyfiledialogs 2.5.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicode-segmentation 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "utf-8 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "uuid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
  "webvr_traits 0.0.1",
  "xml5ever 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "script_layout_interface"
 version = "0.0.1"
 dependencies = [
@@ -2652,17 +2659,17 @@ dependencies = [
  "profile_traits 0.0.1",
  "range 0.0.1",
  "script_traits 0.0.1",
  "selectors 0.19.0",
  "servo_arc 0.0.1",
  "servo_atoms 0.0.1",
  "servo_url 0.0.1",
  "style 0.0.1",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "script_plugins"
 version = "0.0.1"
 
 [[package]]
 name = "script_tests"
@@ -2695,28 +2702,29 @@ dependencies = [
  "profile_traits 0.0.1",
  "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo_atoms 0.0.1",
  "servo_url 0.0.1",
  "style_traits 0.0.1",
  "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
  "webvr_traits 0.0.1",
 ]
 
 [[package]]
 name = "selectors"
 version = "0.19.0"
 dependencies = [
  "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cssparser 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lru_cache 0.0.1",
  "malloc_size_of 0.0.1",
  "malloc_size_of_derive 0.0.1",
  "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
  "phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
  "precomputed-hash 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo_arc 0.0.1",
  "size_of_test 0.0.1",
@@ -3123,16 +3131,17 @@ dependencies = [
  "heapsize 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize_derive 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "html5ever 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "itertools 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lru_cache 0.0.1",
  "malloc_size_of 0.0.1",
  "malloc_size_of_derive 0.0.1",
  "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "nsstring_vendor 0.1.0",
  "num-integer 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "num_cpus 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "ordered-float 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3201,17 +3210,17 @@ dependencies = [
  "euclid 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "heapsize_derive 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "malloc_size_of 0.0.1",
  "malloc_size_of_derive 0.0.1",
  "selectors 0.19.0",
  "serde 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "servo_atoms 0.0.1",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "stylo_tests"
 version = "0.0.1"
 dependencies = [
  "atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cssparser 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -3582,45 +3591,46 @@ dependencies = [
  "servo_url 0.0.1",
  "url 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "uuid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "webdriver 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "webrender"
-version = "0.50.0"
-source = "git+https://github.com/servo/webrender#68e2e7cd0e39d216bb6c35dfb353dc0700bb6948"
+version = "0.51.0"
+source = "git+https://github.com/servo/webrender#2a005f156b9f25862a2dc8443b57be37168233f2"
 dependencies = [
  "app_units 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "bincode 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-foundation 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "core-graphics 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "core-text 6.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "dwrote 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "euclid 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "freetype 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "gamma-lut 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "gleam 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
  "plane-split 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "rayon 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "thread_profiler 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "time 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
- "webrender_api 0.50.0 (git+https://github.com/servo/webrender)",
+ "webrender_api 0.51.0 (git+https://github.com/servo/webrender)",
 ]
 
 [[package]]
 name = "webrender_api"
-version = "0.50.0"
-source = "git+https://github.com/servo/webrender#68e2e7cd0e39d216bb6c35dfb353dc0700bb6948"
+version = "0.51.0"
+source = "git+https://github.com/servo/webrender#2a005f156b9f25862a2dc8443b57be37168233f2"
 dependencies = [
  "app_units 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "bincode 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "core-foundation 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "core-graphics 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "dwrote 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "euclid 0.15.3 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -4028,18 +4038,18 @@ dependencies = [
 "checksum utf-8 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b6f923c601c7ac48ef1d66f7d5b5b2d9a7ba9c51333ab75a3ddf8d0309185a56"
 "checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122"
 "checksum uuid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b5d0f5103675a280a926ec2f9b7bcc2ef49367df54e8c570c3311fec919f9a8b"
 "checksum vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e0a7d8bed3178a8fb112199d466eeca9ed09a14ba8ad67718179b4fd5487d0b"
 "checksum vec_map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cac5efe5cb0fa14ec2f84f83c701c562ee63f6dcc680861b21d65c682adfb05f"
 "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
 "checksum walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "bb08f9e670fab86099470b97cd2b252d6527f0b3cc1401acdb595ffc9dd288ff"
 "checksum webdriver 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d548aabf87411b1b4ba91fd07eacd8b238135c7131a452b8a9f6386209167e18"
-"checksum webrender 0.50.0 (git+https://github.com/servo/webrender)" = "<none>"
-"checksum webrender_api 0.50.0 (git+https://github.com/servo/webrender)" = "<none>"
+"checksum webrender 0.51.0 (git+https://github.com/servo/webrender)" = "<none>"
+"checksum webrender_api 0.51.0 (git+https://github.com/servo/webrender)" = "<none>"
 "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
 "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
 "checksum ws 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "04614a58714f3fd4a8b1da4bcae9f031c532d35988c3d39627619248113f8be8"
 "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
 "checksum x11 2.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db27c597c187da52194a4b8232e7d869503911aab9ff726fefb76d7a830f78ed"
 "checksum x11-clipboard 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "731230b8edcbb9d99247105e4c9ec0a538594d50ad68d2afa8662195f9db2973"
 "checksum x11-dl 2.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "326c500cdc166fd7c70dd8c8a829cd5c0ce7be5a5d98c25817de2b9bdc67faf8"
 "checksum xcb 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)" = "7cede38417fcdf2f0a9d8abf1cea1c1b066320a8a316e9583a0d717c334fafb2"
--- a/servo/components/canvas/webgl_mode/inprocess.rs
+++ b/servo/components/canvas/webgl_mode/inprocess.rs
@@ -3,35 +3,38 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use ::gl_context::GLContextFactory;
 use ::webgl_thread::{WebGLExternalImageApi, WebGLExternalImageHandler, WebGLThreadObserver, WebGLThread};
 use canvas_traits::webgl::{WebGLChan, WebGLContextId, WebGLMsg, WebGLPipeline, WebGLReceiver};
 use canvas_traits::webgl::{WebGLSender, WebVRCommand, WebVRRenderHandler};
 use canvas_traits::webgl::webgl_channel;
 use euclid::Size2D;
+use gleam::gl;
 use std::marker::PhantomData;
+use std::rc::Rc;
 use webrender;
 use webrender_api;
 
 /// WebGL Threading API entry point that lives in the constellation.
 pub struct WebGLThreads(WebGLSender<WebGLMsg>);
 
 impl WebGLThreads {
     /// Creates a new WebGLThreads object
     pub fn new(gl_factory: GLContextFactory,
+               webrender_gl: Rc<gl::Gl>,
                webrender_api_sender: webrender_api::RenderApiSender,
                webvr_compositor: Option<Box<WebVRRenderHandler>>)
                -> (WebGLThreads, Box<webrender::ExternalImageHandler>) {
         // This implementation creates a single `WebGLThread` for all the pipelines.
         let channel = WebGLThread::start(gl_factory,
                                          webrender_api_sender,
                                          webvr_compositor.map(|c| WebVRRenderWrapper(c)),
                                          PhantomData);
-        let external = WebGLExternalImageHandler::new(WebGLExternalImages::new(channel.clone()));
+        let external = WebGLExternalImageHandler::new(WebGLExternalImages::new(webrender_gl, channel.clone()));
         (WebGLThreads(channel), Box::new(external))
     }
 
     /// Gets the WebGLThread handle for each script pipeline.
     pub fn pipeline(&self) -> WebGLPipeline {
         // This mode creates a single thread, so the existing WebGLChan is just cloned.
         WebGLPipeline(WebGLChan(self.0.clone()))
     }
@@ -39,34 +42,43 @@ impl WebGLThreads {
     /// Sends a exit message to close the WebGLThreads and release all WebGLContexts.
     pub fn exit(&self) -> Result<(), &'static str> {
         self.0.send(WebGLMsg::Exit).map_err(|_| "Failed to send Exit message")
     }
 }
 
 /// Bridge between the webrender::ExternalImage callbacks and the WebGLThreads.
 struct WebGLExternalImages {
+    webrender_gl: Rc<gl::Gl>,
     webgl_channel: WebGLSender<WebGLMsg>,
     // Used to avoid creating a new channel on each received WebRender request.
-    lock_channel: (WebGLSender<(u32, Size2D<i32>)>, WebGLReceiver<(u32, Size2D<i32>)>),
+    lock_channel: (WebGLSender<(u32, Size2D<i32>, usize)>, WebGLReceiver<(u32, Size2D<i32>, usize)>),
 }
 
 impl WebGLExternalImages {
-    fn new(channel: WebGLSender<WebGLMsg>) -> Self {
+    fn new(webrender_gl: Rc<gl::Gl>, channel: WebGLSender<WebGLMsg>) -> Self {
         Self {
+            webrender_gl,
             webgl_channel: channel,
             lock_channel: webgl_channel().unwrap(),
         }
     }
 }
 
 impl WebGLExternalImageApi for WebGLExternalImages {
     fn lock(&mut self, ctx_id: WebGLContextId) -> (u32, Size2D<i32>) {
+        // WebGL Thread has it's own GL command queue that we need to synchronize with the WR GL command queue.
+        // The WebGLMsg::Lock message inserts a fence in the WebGL command queue.
         self.webgl_channel.send(WebGLMsg::Lock(ctx_id, self.lock_channel.0.clone())).unwrap();
-        self.lock_channel.1.recv().unwrap()
+        let (image_id, size, gl_sync) = self.lock_channel.1.recv().unwrap();
+        // The next glWaitSync call is run on the WR thread and it's used to synchronize the two
+        // flows of OpenGL commands in order to avoid WR using a semi-ready WebGL texture.
+        // glWaitSync doesn't block WR thread, it affects only internal OpenGL subsystem.
+        self.webrender_gl.wait_sync(gl_sync as gl::GLsync, 0, gl::TIMEOUT_IGNORED);
+        (image_id, size)
     }
 
     fn unlock(&mut self, ctx_id: WebGLContextId) {
         self.webgl_channel.send(WebGLMsg::Unlock(ctx_id)).unwrap();
     }
 }
 
 /// Custom observer used in a `WebGLThread`.
--- a/servo/components/canvas/webgl_thread.rs
+++ b/servo/components/canvas/webgl_thread.rs
@@ -140,36 +140,37 @@ impl<VR: WebVRRenderHandler + 'static, O
                 self.cached_context_info.get(&context_id)
             },
             _ => None
         };
         self.webvr_compositor.as_mut().unwrap().handle(command, texture.map(|t| (t.texture_id, t.size)));
     }
 
     /// Handles a lock external callback received from webrender::ExternalImageHandler
-    fn handle_lock(&mut self, context_id: WebGLContextId, sender: WebGLSender<(u32, Size2D<i32>)>) {
+    fn handle_lock(&mut self, context_id: WebGLContextId, sender: WebGLSender<(u32, Size2D<i32>, usize)>) {
         let ctx = Self::make_current_if_needed(context_id, &self.contexts, &mut self.bound_context_id)
                         .expect("WebGLContext not found in a WebGLMsg::Lock message");
         let info = self.cached_context_info.get_mut(&context_id).unwrap();
-        // Use a OpenGL Fence to perform the lock.
-        info.gl_sync = Some(ctx.gl().fence_sync(gl::SYNC_GPU_COMMANDS_COMPLETE, 0));
+        // Insert a OpenGL Fence sync object that sends a signal when all the WebGL commands are finished.
+        // The related gl().wait_sync call is performed in the WR thread. See WebGLExternalImageApi for mor details.
+        let gl_sync = ctx.gl().fence_sync(gl::SYNC_GPU_COMMANDS_COMPLETE, 0);
+        info.gl_sync = Some(gl_sync);
+        // It is important that the fence sync is properly flushed into the GPU's command queue.
+        // Without proper flushing, the sync object may never be signaled.
+        ctx.gl().flush();
 
-        sender.send((info.texture_id, info.size)).unwrap();
+        sender.send((info.texture_id, info.size, gl_sync as usize)).unwrap();
     }
 
     /// Handles an unlock external callback received from webrender::ExternalImageHandler
     fn handle_unlock(&mut self, context_id: WebGLContextId) {
         let ctx = Self::make_current_if_needed(context_id, &self.contexts, &mut self.bound_context_id)
                         .expect("WebGLContext not found in a WebGLMsg::Unlock message");
         let info = self.cached_context_info.get_mut(&context_id).unwrap();
         if let Some(gl_sync) = info.gl_sync.take() {
-            // glFlush must be called before glWaitSync.
-            ctx.gl().flush();
-            // Wait until the GLSync object is signaled.
-            ctx.gl().wait_sync(gl_sync, 0, gl::TIMEOUT_IGNORED);
             // Release the GLSync object.
             ctx.gl().delete_sync(gl_sync);
         }
     }
 
     /// Creates a new WebGLContext
     fn create_webgl_context(&mut self,
                             size: Size2D<i32>,
--- a/servo/components/canvas_traits/webgl.rs
+++ b/servo/components/canvas_traits/webgl.rs
@@ -34,17 +34,17 @@ pub enum WebGLMsg {
     WebGLCommand(WebGLContextId, WebGLCommand),
     /// Runs a WebVRCommand in a specific WebGLContext.
     WebVRCommand(WebGLContextId, WebVRCommand),
     /// Locks a specific WebGLContext. Lock messages are used for a correct synchronization
     /// with WebRender external image API.
     /// WR locks a external texture when it wants to use the shared texture contents.
     /// The WR client should not change the shared texture content until the Unlock call.
     /// Currently OpenGL Sync Objects are used to implement the synchronization mechanism.
-    Lock(WebGLContextId, WebGLSender<(u32, Size2D<i32>)>),
+    Lock(WebGLContextId, WebGLSender<(u32, Size2D<i32>, usize)>),
     /// Unlocks a specific WebGLContext. Unlock messages are used for a correct synchronization
     /// with WebRender external image API.
     /// The WR unlocks a context when it finished reading the shared texture contents.
     /// Unlock messages are always sent after a Lock message.
     Unlock(WebGLContextId),
     /// Creates or updates the image keys required for WebRender.
     UpdateWebRenderImage(WebGLContextId, WebGLSender<webrender_api::ImageKey>),
     /// Frees all resources and closes the thread.
--- a/servo/components/compositing/compositor.rs
+++ b/servo/components/compositing/compositor.rs
@@ -629,17 +629,20 @@ impl<Window: WindowMethods> IOCompositor
 
     fn remove_pipeline_root_layer(&mut self, pipeline_id: PipelineId) {
         self.pipeline_details.remove(&pipeline_id);
     }
 
     fn send_window_size(&self, size_type: WindowSizeType) {
         let dppx = self.page_zoom * self.hidpi_factor();
 
-        self.webrender_api.set_window_parameters(self.webrender_document, self.frame_size, self.window_rect);
+        self.webrender_api.set_window_parameters(self.webrender_document,
+                                                 self.frame_size,
+                                                 self.window_rect,
+                                                 self.hidpi_factor().get());
 
         let initial_viewport = self.window_rect.size.to_f32() / dppx;
 
         let data = WindowSizeData {
             device_pixel_ratio: dppx,
             initial_viewport: initial_viewport,
         };
         let top_level_browsing_context_id = match self.root_pipeline {
--- a/servo/components/gfx/font_cache_thread.rs
+++ b/servo/components/gfx/font_cache_thread.rs
@@ -182,17 +182,18 @@ impl FontCache {
                                             .entry((font_key, size))
                                             .or_insert_with(|| {
                                                 let key = webrender_api.generate_font_instance_key();
                                                 let mut updates = webrender_api::ResourceUpdates::new();
                                                 updates.add_font_instance(key,
                                                                           font_key,
                                                                           size,
                                                                           None,
-                                                                          None);
+                                                                          None,
+                                                                          Vec::new());
                                                 webrender_api.update_resources(updates);
                                                 key
                                             });
 
                     let _ = result.send(instance_key);
                 }
                 Command::AddWebFont(family_name, sources, result) => {
                     self.handle_add_web_font(family_name, sources, result);
new file mode 100644
--- /dev/null
+++ b/servo/components/lru_cache/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "lru_cache"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+license = "MPL-2.0"
+publish = false # We should publish this at some point
+
+[lib]
+name = "lru_cache"
+path = "lib.rs"
+
+[dependencies]
+arrayvec = "0.3.20"
rename from servo/components/style/cache.rs
rename to servo/components/lru_cache/lib.rs
--- a/servo/components/style/cache.rs
+++ b/servo/components/lru_cache/lib.rs
@@ -1,14 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! A simple LRU cache.