Merge m-c to graphics
authorKartikaya Gupta <kgupta@mozilla.com>
Thu, 27 Apr 2017 09:31:17 -0400
changeset 355271 674bad1624a5ac186288273126291f322f4f4e66
parent 355270 8ff62d84dc101d2a125484a545b090070d4266b1 (current diff)
parent 355106 0b77ed3f26c5335503bc16e85b8c067382e7bb1e (diff)
child 355272 0c7e93632f4c1c1035ca8417a83b80f560fbb049
push id89659
push userkwierso@gmail.com
push dateThu, 27 Apr 2017 20:29:29 +0000
treeherdermozilla-inbound@f700a9bf4fd1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to graphics MozReview-Commit-ID: 6UBB4TW6an5
browser/themes/shared/reader/reader-tour.png
browser/themes/shared/reader/reader-tour@2x.png
devtools/client/themes/images/performance-icons.svg
devtools/client/themes/images/sort-arrows.svg
dom/events/DataContainerEvent.cpp
dom/events/DataContainerEvent.h
dom/events/test/test_bug368835.html
dom/html/test/file_mozaudiochannel.html
dom/html/test/test_mozaudiochannel.html
dom/interfaces/events/nsIDOMDataContainerEvent.idl
dom/ipc/ContentParent.cpp
dom/ipc/ContentParent.h
dom/ipc/PContent.ipdl
dom/ipc/TabParent.cpp
dom/media/webaudio/test/test_mozaudiochannel.html
dom/plugins/test/reftest/reftest.list
dom/webidl/DataContainerEvent.webidl
gfx/tests/reftest/reftest.list
layout/generic/nsImageFrame.cpp
layout/painting/nsImageRenderer.cpp
layout/reftests/abs-pos/reftest.list
layout/reftests/backgrounds/reftest.list
layout/reftests/border-radius/reftest.list
layout/reftests/bugs/reftest.list
layout/reftests/columns/reftest.list
layout/reftests/forms/input/file/reftest.list
layout/reftests/web-animations/reftest.list
layout/reftests/xul/reftest.list
layout/style/res/checkmark.svg
layout/style/res/indeterminate-checkmark.svg
layout/style/res/radio.svg
mobile/android/base/resources/drawable-ldrtl-v17/url_bar_translating_edge.xml
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDevice.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/TestFxAccountDeviceRegistrator.java
modules/libpref/init/all.js
python/mozlint/test/linters/badreturncode.lint
python/mozlint/test/linters/explicit_path.lint
python/mozlint/test/linters/external.lint
python/mozlint/test/linters/invalid_exclude.lint
python/mozlint/test/linters/invalid_include.lint
python/mozlint/test/linters/invalid_type.lint
python/mozlint/test/linters/missing_attrs.lint
python/mozlint/test/linters/missing_definition.lint
python/mozlint/test/linters/raises.lint
python/mozlint/test/linters/regex.lint
python/mozlint/test/linters/string.lint
python/mozlint/test/linters/structured.lint
testing/web-platform/meta/selection/removeRange.html.ini
tools/lint/eslint.lint
tools/lint/flake8.lint
tools/lint/wpt.lint
tools/lint/wpt_manifest.lint
--- a/.eslintignore
+++ b/.eslintignore
@@ -67,17 +67,18 @@ browser/components/translation/cld2/**
 # Screenshots is imported as a system add-on and has its own lint rules currently.
 browser/extensions/screenshots/**
 browser/extensions/pdfjs/content/build**
 browser/extensions/pdfjs/content/web**
 # generated or library files in pocket
 browser/extensions/pocket/content/panels/js/tmpl.js
 browser/extensions/pocket/content/panels/js/vendor/**
 browser/locales/**
-# vendor library files in activity-stream
+# generated or library files in activity-stream
+browser/extensions/activity-stream/data/content/activity-stream.bundle.js
 browser/extensions/activity-stream/vendor/**
 # imported from chromium
 browser/extensions/mortar/**
 
 # devtools/ exclusions
 devtools/client/canvasdebugger/**
 devtools/client/commandline/**
 devtools/client/debugger/**
--- a/accessible/.eslintrc.js
+++ b/accessible/.eslintrc.js
@@ -8,10 +8,17 @@ module.exports = {
     "Cc": true,
     "Ci": true,
     "Components": true,
     "console": true,
     "Cu": true,
     "dump": true,
     "Services": true,
     "XPCOMUtils": true
+  },
+  "rules": {
+    // Warn about cyclomatic complexity in functions.
+    "complexity": ["error", 42],
+
+    // Maximum depth callbacks can be nested.
+    "max-nested-callbacks": ["error", 10],
   }
 };
--- a/accessible/base/NotificationController.cpp
+++ b/accessible/base/NotificationController.cpp
@@ -671,17 +671,17 @@ NotificationController::WillRefresh(mozi
         if (logging::IsEnabled(logging::eTree | logging::eText)) {
           logging::MsgBegin("TREE", "text node lost its content; doc: %p", mDocument);
           logging::Node("container", containerElm);
           logging::Node("content", textNode);
           logging::MsgEnd();
         }
   #endif
 
-        mDocument->ContentRemoved(containerElm, textNode);
+        mDocument->ContentRemoved(textAcc);
         continue;
       }
 
       // Update text of the accessible and fire text change events.
   #ifdef A11Y_LOG
       if (logging::IsEnabled(logging::eText)) {
         logging::MsgBegin("TEXT", "text may be changed; doc: %p", mDocument);
         logging::Node("container", containerElm);
--- a/accessible/base/TreeWalker.cpp
+++ b/accessible/base/TreeWalker.cpp
@@ -47,16 +47,28 @@ TreeWalker::
   MOZ_ASSERT(aAnchorNode, "No anchor node for the accessible tree walker");
 
   mChildFilter |= mContext->NoXBLKids() ?
     nsIContent::eAllButXBL : nsIContent::eAllChildren;
 
   MOZ_COUNT_CTOR(TreeWalker);
 }
 
+TreeWalker::
+  TreeWalker(DocAccessible* aDocument, nsIContent* aAnchorNode) :
+  mDoc(aDocument), mContext(nullptr), mAnchorNode(aAnchorNode),
+  mARIAOwnsIdx(0),
+  mChildFilter(nsIContent::eSkipPlaceholderContent | nsIContent::eAllChildren),
+  mFlags(eWalkCache),
+  mPhase(eAtStart)
+{
+  MOZ_ASSERT(aAnchorNode, "No anchor node for the accessible tree walker");
+  MOZ_COUNT_CTOR(TreeWalker);
+}
+
 TreeWalker::~TreeWalker()
 {
   MOZ_COUNT_DTOR(TreeWalker);
 }
 
 Accessible*
 TreeWalker::Scope(nsIContent* aAnchorNode)
 {
--- a/accessible/base/TreeWalker.h
+++ b/accessible/base/TreeWalker.h
@@ -42,16 +42,21 @@ public:
    *
    * @param aContext [in] container accessible for the given node, used to
    *                   define accessible context
    * @param aAnchorNode [in] the node the search will be prepared relative to
    * @param aFlags   [in] flags (see enum above)
    */
   TreeWalker(Accessible* aContext, nsIContent* aAnchorNode, uint32_t aFlags = eWalkCache);
 
+  /**
+   * Navigates the accessible children within the anchor node subtree.
+   */
+  TreeWalker(DocAccessible* aDocument, nsIContent* aAnchorNode);
+
   ~TreeWalker();
 
   /**
    * Resets the walker state, and sets the given node as an anchor. Returns a
    * first accessible element within the node including the node itself.
    */
   Accessible* Scope(nsIContent* aAnchorNode);
 
--- a/accessible/base/nsAccessibilityService.cpp
+++ b/accessible/base/nsAccessibilityService.cpp
@@ -519,17 +519,17 @@ nsAccessibilityService::DeckPanelSwitche
     if (logging::IsEnabled(logging::eTree)) {
       logging::MsgBegin("TREE", "deck panel unselected");
       logging::Node("container", panelNode);
       logging::Node("content", aDeckNode);
       logging::MsgEnd();
     }
 #endif
 
-    document->ContentRemoved(aDeckNode, panelNode);
+    document->ContentRemoved(panelNode);
   }
 
   if (aCurrentBoxFrame) {
     nsIContent* panelNode = aCurrentBoxFrame->GetContent();
 #ifdef A11Y_LOG
     if (logging::IsEnabled(logging::eTree)) {
       logging::MsgBegin("TREE", "deck panel selected");
       logging::Node("container", panelNode);
@@ -577,36 +577,17 @@ nsAccessibilityService::ContentRemoved(n
     logging::MsgBegin("TREE", "content removed; doc: %p", document);
     logging::Node("container node", aChildNode->GetFlattenedTreeParent());
     logging::Node("content node", aChildNode);
     logging::MsgEnd();
   }
 #endif
 
   if (document) {
-    // Flatten hierarchy may be broken at this point so we cannot get a true
-    // container by traversing up the DOM tree. Find a parent of first accessible
-    // from the subtree of the given DOM node, that'll be a container. If no
-    // accessibles in subtree then we don't care about the change.
-    Accessible* child = document->GetAccessible(aChildNode);
-    if (!child) {
-      Accessible* container = document->GetContainerAccessible(aChildNode);
-      a11y::TreeWalker walker(container ? container : document, aChildNode,
-                              a11y::TreeWalker::eWalkCache);
-      child = walker.Next();
-    }
-
-    if (child) {
-      MOZ_DIAGNOSTIC_ASSERT(child->Parent(), "Unattached accessible from tree");
-      document->ContentRemoved(child->Parent(), aChildNode);
-#ifdef A11Y_LOG
-      if (logging::IsEnabled(logging::eTree))
-        logging::AccessibleNNode("real container", child->Parent());
-#endif
-    }
+    document->ContentRemoved(aChildNode);
   }
 
 #ifdef A11Y_LOG
   if (logging::IsEnabled(logging::eTree)) {
     logging::MsgEnd();
     logging::Stack();
   }
 #endif
--- a/accessible/generic/DocAccessible.cpp
+++ b/accessible/generic/DocAccessible.cpp
@@ -1165,20 +1165,17 @@ DocAccessible::ContentRemoved(nsIDocumen
     logging::Node("container node", aContainerNode);
     logging::Node("content node", aChildNode);
     logging::MsgEnd();
   }
 #endif
   // This one and content removal notification from layout may result in
   // double processing of same subtrees. If it pops up in profiling, then
   // consider reusing a document node cache to reject these notifications early.
-  Accessible* container = GetAccessibleOrContainer(aContainerNode);
-  if (container) {
-    UpdateTreeOnRemoval(container, aChildNode);
-  }
+  ContentRemoved(aChildNode);
 }
 
 void
 DocAccessible::ParentChainChanged(nsIContent* aContent)
 {
 }
 
 
@@ -1377,17 +1374,17 @@ DocAccessible::RecreateAccessible(nsICon
 #endif
 
   // XXX: we shouldn't recreate whole accessible subtree, instead we should
   // subclass hide and show events to handle them separately and implement their
   // coalescence with normal hide and show events. Note, in this case they
   // should be coalesced with normal show/hide events.
 
   nsIContent* parent = aContent->GetFlattenedTreeParent();
-  ContentRemoved(parent, aContent);
+  ContentRemoved(aContent);
   ContentInserted(parent, aContent, aContent->GetNextSibling());
 }
 
 void
 DocAccessible::ProcessInvalidationList()
 {
   // Invalidate children of container accessible for each element in
   // invalidation list. Allow invalidation list insertions while container
@@ -1967,43 +1964,46 @@ DocAccessible::FireEventsOnInsertion(Acc
         break;
       }
     }
     while ((ancestor = ancestor->Parent()));
   }
 }
 
 void
-DocAccessible::UpdateTreeOnRemoval(Accessible* aContainer, nsIContent* aChildNode)
+DocAccessible::ContentRemoved(Accessible* aContent)
 {
-  // If child node is not accessible then look for its accessible children.
-  Accessible* child = GetAccessible(aChildNode);
+  MOZ_DIAGNOSTIC_ASSERT(aContent->Parent(), "Unattached accessible from tree");
+
 #ifdef A11Y_LOG
   logging::TreeInfo("process content removal", 0,
-                    "container", aContainer, "child", aChildNode);
+                    "container", aContent->Parent(), "child", aContent, nullptr);
 #endif
 
-  TreeMutation mt(aContainer);
-  if (child) {
-    mt.BeforeRemoval(child);
-    MOZ_ASSERT(aContainer == child->Parent(), "Wrong parent");
-    aContainer->RemoveChild(child);
-    UncacheChildrenInSubtree(child);
-    mt.Done();
-    return;
+  TreeMutation mt(aContent->Parent());
+  mt.BeforeRemoval(aContent);
+  aContent->Parent()->RemoveChild(aContent);
+  UncacheChildrenInSubtree(aContent);
+  mt.Done();
+}
+
+void
+DocAccessible::ContentRemoved(nsIContent* aContentNode)
+{
+  // If child node is not accessible then look for its accessible children.
+  Accessible* acc = GetAccessible(aContentNode);
+  if (acc) {
+    ContentRemoved(acc);
   }
-
-  TreeWalker walker(aContainer, aChildNode, TreeWalker::eWalkCache);
-  while (Accessible* child = walker.Next()) {
-    mt.BeforeRemoval(child);
-    MOZ_ASSERT(aContainer == child->Parent(), "Wrong parent");
-    aContainer->RemoveChild(child);
-    UncacheChildrenInSubtree(child);
+  else {
+    TreeWalker walker(this, aContentNode);
+    while (Accessible* acc = walker.Next()) {
+      ContentRemoved(acc);
+    }
   }
-  mt.Done();
 }
 
 bool
 DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement)
 {
   if (!aElement->HasID())
     return false;
 
@@ -2046,17 +2046,17 @@ DocAccessible::ValidateARIAOwned()
         idx--;
         continue;
       }
 
       NS_ASSERTION(child->Parent(), "No parent for ARIA owned?");
 
       // If DOM node doesn't have a frame anymore then shutdown its accessible.
       if (child->Parent() && !child->GetFrame()) {
-        UpdateTreeOnRemoval(child->Parent(), child->GetContent());
+        ContentRemoved(child);
         children->RemoveElementAt(idx);
         idx--;
         continue;
       }
 
       NS_ASSERTION(child->Parent() == owner,
                    "Illigally stolen ARIA owned child!");
     }
--- a/accessible/generic/DocAccessible.h
+++ b/accessible/generic/DocAccessible.h
@@ -337,28 +337,20 @@ public:
   /**
    * Notify the document accessible that content was inserted.
    */
   void ContentInserted(nsIContent* aContainerNode,
                        nsIContent* aStartChildNode,
                        nsIContent* aEndChildNode);
 
   /**
-   * Notify the document accessible that content was removed.
+   * Update the tree on content removal.
    */
-  void ContentRemoved(Accessible* aContainer, nsIContent* aChildNode)
-  {
-    // Update the whole tree of this document accessible when the container is
-    // null (document element is removed).
-    UpdateTreeOnRemoval((aContainer ? aContainer : this), aChildNode);
-  }
-  void ContentRemoved(nsIContent* aContainerNode, nsIContent* aChildNode)
-  {
-    ContentRemoved(AccessibleOrTrueContainer(aContainerNode), aChildNode);
-  }
+  void ContentRemoved(Accessible* aContent);
+  void ContentRemoved(nsIContent* aContentNode);
 
   /**
    * Updates accessible tree when rendered text is changed.
    */
   void UpdateText(nsIContent* aTextNode);
 
   /**
    * Recreate an accessible, results in hide/show events pair.
@@ -508,21 +500,16 @@ protected:
    *
    * While children are cached we may encounter the case there's no accessible
    * for referred content by related accessible. Store these related nodes to
    * invalidate their containers later.
    */
   void ProcessInvalidationList();
 
   /**
-   * Update the accessible tree for content removal.
-   */
-  void UpdateTreeOnRemoval(Accessible* aContainer, nsIContent* aChildNode);
-
-  /**
    * Validates all aria-owns connections and updates the tree accordingly.
    */
   void ValidateARIAOwned();
 
   /**
    * Steals or puts back accessible subtrees.
    */
   void DoARIAOwnsRelocation(Accessible* aOwner);
--- a/accessible/tests/browser/.eslintrc.js
+++ b/accessible/tests/browser/.eslintrc.js
@@ -57,24 +57,25 @@ module.exports = { // eslint-disable-lin
     "mozilla/var-only-at-top-level": "warn",
 
     "block-scoped-var": "error",
     "brace-style": ["error", "1tbs"],
     "camelcase": "error",
     "comma-dangle": ["error", "never"],
     "comma-spacing": "error",
     "comma-style": ["error", "last"],
-    "complexity": ["error", 35],
+    "complexity": ["error", 20],
     "consistent-this": "off",
     "curly": ["error", "multi-line"],
     "default-case": "off",
     "dot-location": ["error", "property"],
     "dot-notation": "error",
     "eol-last": "error",
     "eqeqeq": "off",
+    "func-call-spacing": "error",
     "func-names": "off",
     "func-style": "off",
     "generator-star": "off",
     "global-strict": "off",
     "handle-callback-err": ["error", "er"],
     "indent": ["error", 2, {"SwitchCase": 1}],
     "key-spacing": ["error", {"beforeColon": false, "afterColon": true}],
     "linebreak-style": "off",
@@ -136,17 +137,16 @@ module.exports = { // eslint-disable-lin
     "no-restricted-modules": "off",
     "no-return-assign": "error",
     "no-script-url": "off",
     "no-self-compare": "error",
     "no-sequences": "error",
     "no-shadow": "error",
     "no-shadow-restricted-names": "error",
     "no-space-before-semi": "off",
-    "no-spaced-func": "error",
     "no-sparse-arrays": "error",
     "no-sync": "off",
     "no-ternary": "off",
     "no-throw-literal": "error",
     "no-trailing-spaces": "error",
     "no-undef": "error",
     "no-underscore-dangle": "off",
     "no-undefined": "off",
--- a/browser/.eslintrc.js
+++ b/browser/.eslintrc.js
@@ -12,19 +12,16 @@ module.exports = {
 
     // Disallow empty statements. This will report an error for:
     // try { something(); } catch (e) {}
     // but will not report it for:
     // try { something(); } catch (e) { /* Silencing the error because ...*/ }
     // which is a valid use case.
     "no-empty": "error",
 
-    // No spaces between function name and parentheses
-    "no-spaced-func": "error",
-
     // Maximum depth callbacks can be nested.
     "max-nested-callbacks": ["error", 8],
 
     // Disallow adding to native types
     "no-extend-native": "error",
 
     "no-mixed-spaces-and-tabs": "error",
     "no-shadow": "error",
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1525,17 +1525,16 @@ pref("dom.ipc.processHangMonitor", true)
 #ifdef DEBUG
 // Don't report hangs in DEBUG builds. They're too slow and often a
 // debugger is attached.
 pref("dom.ipc.reportProcessHangs", false);
 #else
 pref("dom.ipc.reportProcessHangs", true);
 #endif
 
-pref("browser.reader.detectedFirstArticle", false);
 // Don't limit how many nodes we care about on desktop:
 pref("reader.parse-node-limit", 0);
 
 // On desktop, we want the URLs to be included here for ease of debugging,
 // and because (normally) these errors are not persisted anywhere.
 pref("reader.errors.includeURLs", true);
 
 pref("view_source.tab", true);
--- a/browser/base/content/aboutDialog.xul
+++ b/browser/base/content/aboutDialog.xul
@@ -66,17 +66,17 @@
               <hbox id="downloadAndInstall" align="center">
                 <button id="downloadAndInstallButton" align="start"
                         oncommand="gAppUpdater.startDownload();"/>
                         <!-- label and accesskey will be filled by JS -->
                 <spacer flex="1"/>
               </hbox>
               <hbox id="apply" align="center">
                 <button id="updateButton" align="start"
-                        label="&update.updateButton.label2;"
+                        label="&update.updateButton.label3;"
                         accesskey="&update.updateButton.accesskey;"
                         oncommand="gAppUpdater.buttonRestartAfterDownload();"/>
                 <spacer flex="1"/>
               </hbox>
               <hbox id="checkingForUpdates" align="center">
                 <image class="update-throbber"/><label>&update.checkingForUpdates;</label>
               </hbox>
               <hbox id="downloading" align="center">
--- a/browser/base/content/browser-fullScreenAndPointerLock.js
+++ b/browser/base/content/browser-fullScreenAndPointerLock.js
@@ -70,17 +70,17 @@ var PointerlockFsWarning = {
         this._timeoutHide.start();
       }, delay);
     }
 
     // Set the strings on the warning UI.
     if (aOrigin) {
       this._origin = aOrigin;
     }
-    let uri = BrowserUtils.makeURI(this._origin);
+    let uri = Services.io.newURI(this._origin);
     let host = null;
     try {
       host = uri.host;
     } catch (e) { }
     let textElem = this._element.querySelector(".pointerlockfswarning-domain-text");
     if (!host) {
       textElem.setAttribute("hidden", true);
     } else {
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -217,17 +217,17 @@ var gPluginHandler = {
     if (!principal.equals(browser.contentPrincipal)) {
       return;
     }
 
     // Data URIs, when linked to from some page, inherit the principal of that
     // page. That means that we also need to compare the actual locations to
     // ensure we aren't getting a message from a Data URI that we're no longer
     // looking at.
-    let receivedURI = BrowserUtils.makeURI(location);
+    let receivedURI = Services.io.newURI(location);
     if (!browser.documentURI.equalsExceptRef(receivedURI)) {
       return;
     }
 
     let notification = PopupNotifications.getNotification("click-to-play-plugins", browser);
 
     // If this is a new notification, create a pluginData map, otherwise append
     let pluginData;
@@ -313,17 +313,17 @@ var gPluginHandler = {
     if (!principal.equals(browser.contentPrincipal)) {
       return;
     }
 
     // Data URIs, when linked to from some page, inherit the principal of that
     // page. That means that we also need to compare the actual locations to
     // ensure we aren't getting a message from a Data URI that we're no longer
     // looking at.
-    let receivedURI = BrowserUtils.makeURI(location);
+    let receivedURI = Services.io.newURI(location);
     if (!browser.documentURI.equalsExceptRef(receivedURI)) {
       return;
     }
 
     // Set up the icon
     document.getElementById("plugins-notification-icon").classList.
       toggle("plugin-blocked", haveInsecure);
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -463,17 +463,17 @@ const gStoragePressureObserver = {
     }
     this._lastNotificationTime = Date.now();
 
     const BYTES_IN_GIGABYTE = 1073741824;
     const USAGE_THRESHOLD_BYTES = BYTES_IN_GIGABYTE *
       Services.prefs.getIntPref("browser.storageManager.pressureNotification.usageThresholdGB");
     let msg = "";
     let buttons = [];
-    let usage = parseInt(data);
+    let usage = subject.QueryInterface(Ci.nsISupportsPRUint64).data
     let prefStrBundle = document.getElementById("bundle_preferences");
     let brandShortName = document.getElementById("bundle_brand").getString("brandShortName");
     let notificationBox = document.getElementById("high-priority-global-notificationbox");
     buttons.push({
       label: prefStrBundle.getString("spaceAlert.learnMoreButton.label"),
       accessKey: prefStrBundle.getString("spaceAlert.learnMoreButton.accesskey"),
       callback(notificationBar, button) {
         let learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "storage-permissions";
@@ -4083,17 +4083,17 @@ function FillHistoryMenu(aParent) {
 
     for (let j = end - 1; j >= start; j--) {
       let entry = sessionHistory.entries[j];
       let uri = entry.url;
 
       let item = existingIndex < children.length ?
                    children[existingIndex] : document.createElement("menuitem");
 
-      let entryURI = BrowserUtils.makeURI(entry.url, entry.charset, null);
+      let entryURI = Services.io.newURI(entry.url, entry.charset);
       item.setAttribute("uri", uri);
       item.setAttribute("label", entry.title || uri);
       item.setAttribute("index", j);
 
       // Cache this so that gotoHistoryIndex doesn't need the original index
       item.setAttribute("historyindex", j - index);
 
       if (j != index) {
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -571,17 +571,17 @@ var ClickEventHandler = {
       // should we allow mixed content.
       json.allowMixedContent = false;
       let docshell = ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
                              .getInterface(Ci.nsIWebNavigation)
                              .QueryInterface(Ci.nsIDocShell);
       if (docShell.mixedContentChannel) {
         const sm = Services.scriptSecurityManager;
         try {
-          let targetURI = BrowserUtils.makeURI(href);
+          let targetURI = Services.io.newURI(href);
           sm.checkSameOriginURI(docshell.mixedContentChannel.URI, targetURI, false);
           json.allowMixedContent = true;
         } catch (e) {}
       }
       json.originPrincipal = ownerDoc.nodePrincipal;
       json.triggeringPrincipal = ownerDoc.nodePrincipal;
 
       sendAsyncMessage("Content:Click", json);
@@ -687,17 +687,17 @@ var ClickEventHandler = {
         }
       }
       node = node.parentNode;
     }
 
     // In case of XLink, we don't return the node we got href from since
     // callers expect <a>-like elements.
     // Note: makeURI() will throw if aUri is not a valid URI.
-    return [href ? BrowserUtils.makeURI(href, null, baseURI).spec : null, null,
+    return [href ? Services.io.newURI(href, null, baseURI).spec : null, null,
             node && node.ownerDocument.nodePrincipal];
   }
 };
 ClickEventHandler.init();
 
 ContentLinkHandler.init(this);
 
 // TODO: Load this lazily so the JSM is run only if a relevant event/message fires.
@@ -862,22 +862,20 @@ addMessageListener("ContextMenu:Bookmark
                      description: PlacesUIUtils.getDescriptionFromDocument(frame) });
 });
 
 addMessageListener("ContextMenu:SearchFieldBookmarkData", (message) => {
   let node = message.objects.target;
 
   let charset = node.ownerDocument.characterSet;
 
-  let formBaseURI = BrowserUtils.makeURI(node.form.baseURI,
-                                         charset);
+  let formBaseURI = Services.io.newURI(node.form.baseURI, charset);
 
-  let formURI = BrowserUtils.makeURI(node.form.getAttribute("action"),
-                                     charset,
-                                     formBaseURI);
+  let formURI = Services.io.newURI(node.form.getAttribute("action"), charset,
+                                   formBaseURI);
 
   let spec = formURI.spec;
 
   let isURLEncoded =
                (node.form.method.toUpperCase() == "POST"
                 && (node.form.enctype == "application/x-www-form-urlencoded" ||
                     node.form.enctype == ""));
 
@@ -1457,18 +1455,18 @@ let OfflineApps = {
     if (!aWindow.document.documentElement)
       return null;
 
     var attr = aWindow.document.documentElement.getAttribute("manifest");
     if (!attr)
       return null;
 
     try {
-      var contentURI = BrowserUtils.makeURI(aWindow.location.href, null, null);
-      return BrowserUtils.makeURI(attr, aWindow.document.characterSet, contentURI);
+      return Services.io.newURI(attr, aWindow.document.characterSet,
+                                Services.io.newURI(aWindow.location.href));
     } catch (e) {
       return null;
     }
   },
 
   offlineAppRequested(aContentWindow) {
     this.registerWindow(aContentWindow);
     if (!Services.prefs.getBoolPref("browser.offline-apps.notify")) {
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -998,17 +998,17 @@ var RefreshBlocker = {
     let data = message.data;
 
     if (message.name == "RefreshBlocker:Refresh") {
       let win = Services.wm.getOuterWindowWithId(data.outerWindowID);
       let refreshURI = win.QueryInterface(Ci.nsIInterfaceRequestor)
                           .getInterface(Ci.nsIDocShell)
                           .QueryInterface(Ci.nsIRefreshURI);
 
-      let URI = BrowserUtils.makeURI(data.URI, data.originCharset, null);
+      let URI = Services.io.newURI(data.URI, data.originCharset);
 
       refreshURI.forceRefreshURI(URI, data.delay, true);
     }
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener2,
                                          Ci.nsIWebProgressListener,
                                          Ci.nsISupportsWeakReference,
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2103,20 +2103,30 @@
                 case "userTypedValue":
                 case "userTypedClear":
                   getter = () => {
                     return SessionStore.getLazyTabValue(aTab, name);
                   };
                   break;
                 default:
                   getter = () => {
+                    if (AppConstants.NIGHTLY_BUILD) {
+                      let message =
+                        `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`
+                      console.log(message + new Error().stack);
+                    }
                     this._insertBrowser(aTab);
                     return browser[name];
                   };
                   setter = (value) => {
+                    if (AppConstants.NIGHTLY_BUILD) {
+                      let message =
+                        `[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`
+                      console.log(message + new Error().stack);
+                    }
                     this._insertBrowser(aTab);
                     return browser[name] = value;
                   };
               }
               Object.defineProperty(browser, name, {
                 get: getter,
                 set: setter,
                 configurable: true,
--- a/browser/base/content/test/general/browser_storagePressure_notification.js
+++ b/browser/base/content/test/general/browser_storagePressure_notification.js
@@ -1,15 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 function notifyStoragePressure(usage = 100) {
   let notifyPromise = TestUtils.topicObserved("QuotaManager::StoragePressure", () => true);
-  Services.obs.notifyObservers(null, "QuotaManager::StoragePressure", usage);
+  let usageWrapper = Cc["@mozilla.org/supports-PRUint64;1"]
+                     .createInstance(Ci.nsISupportsPRUint64);
+  usageWrapper.data = usage;
+  Services.obs.notifyObservers(usageWrapper, "QuotaManager::StoragePressure");
   return notifyPromise;
 }
 
 function privacyAboutPrefPromise() {
   let promises = [
     BrowserTestUtils.waitForLocationChange(gBrowser, "about:preferences#privacy"),
     TestUtils.topicObserved("advanced-pane-loaded", () => true)
   ];
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -31,20 +31,16 @@ if (AppConstants.platform == "macosx")
 var whitelist = new Set([
   // browser/extensions/pdfjs/content/PdfStreamConverter.jsm
   {file: "chrome://pdf.js/locale/chrome.properties"},
   {file: "chrome://pdf.js/locale/viewer.properties"},
 
   // security/manager/pki/resources/content/device_manager.js
   {file: "chrome://pippki/content/load_device.xul"},
 
-  // browser/modules/ReaderParent.jsm
-  {file: "chrome://browser/skin/reader-tour.png"},
-  {file: "chrome://browser/skin/reader-tour@2x.png"},
-
   // Used by setting this url as a pref in about:config
   {file: "chrome://browser/content/newtab/alternativeDefaultSites.json"},
 
   // Add-on compat
   {file: "chrome://browser/skin/devtools/common.css"},
   {file: "chrome://global/content/XPCNativeWrapper.js"},
   {file: "chrome://global/locale/brand.dtd"},
 
@@ -217,21 +213,16 @@ var whitelist = new Set([
   {file: "chrome://mozapps/skin/extensions/themeGeneric-16.png"},
   // Bug 1348556
   {file: "chrome://mozapps/skin/plugins/pluginBlocked.png"},
   // Bug 1348558
   {file: "chrome://mozapps/skin/update/downloadButtons.png",
    platforms: ["linux"]},
   // Bug 1348559
   {file: "chrome://pippki/content/resetpassword.xul"},
-
-  // Bug 1344257
-  {file: "resource://gre-resources/checkmark.svg"},
-  {file: "resource://gre-resources/indeterminate-checkmark.svg"},
-  {file: "resource://gre-resources/radio.svg"},
   // Bug 1351078
   {file: "resource://gre/modules/Battery.jsm"},
   // Bug 1351070
   {file: "resource://gre/modules/ContentPrefInstance.jsm"},
   // Bug 1351079
   {file: "resource://gre/modules/ISO8601DateUtils.jsm"},
   // Bug 1337345
   {file: "resource://gre/modules/Manifest.jsm"},
--- a/browser/base/content/test/static/browser_parsable_css.js
+++ b/browser/base/content/test/static/browser_parsable_css.js
@@ -60,17 +60,17 @@ let whitelist = [
   {sourceName: /devtools\/skin\/animationinspector\.css$/i,
    intermittent: true,
    errorMessage: /Property contained reference to invalid variable.*color/i,
    isFromDevTools: true},
 ];
 
 if (!Services.prefs.getBoolPref("full-screen-api.unprefix.enabled")) {
   whitelist.push({
-    sourceName: /res\/(ua|html)\.css$/i,
+    sourceName: /(?:res|gre-resources)\/(ua|html)\.css$/i,
     errorMessage: /Unknown pseudo-class .*\bfullscreen\b/i,
     isFromDevTools: false
   });
 }
 
 // Platform can be "linux", "macosx" or "win". If omitted, the exception applies to all platforms.
 let allowedImageReferences = [
   // Bug 1302691
--- a/browser/components/.eslintrc.js
+++ b/browser/components/.eslintrc.js
@@ -1,9 +1,9 @@
 "use strict";
 
 module.exports = {
   rules: {
     // XXX Bug 1326071 - This should be reduced down - probably to 20 or to
     // be removed & synced with the mozilla/recommended value.
-    "complexity": ["error", {"max": 69}],
+    "complexity": ["error", 61],
   }
 };
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -470,17 +470,17 @@
       <description id="update-manual-description">&updateManual.message;
         <label id="update-manual-whats-new" class="text-link" value="&updateManual.whatsnew.label;" />
       </description>
     </popupnotificationcontent>
   </popupnotification>
 
   <popupnotification id="PanelUI-update-restart-notification"
                      popupid="update-restart"
-                     label="&updateRestart.header.message;"
+                     label="&updateRestart.header.message2;"
                      buttonlabel="&updateRestart.acceptButton.label;"
                      buttonaccesskey="&updateRestart.acceptButton.accesskey;"
                      closebuttonhidden="true"
                      secondarybuttonlabel="&updateRestart.cancelButton.label;"
                      secondarybuttonaccesskey="&updateRestart.cancelButton.accesskey;"
                      dropmarkerhidden="true"
                      checkboxhidden="true"
                      hidden="true">
@@ -494,22 +494,52 @@
        class="cui-widget-panel"
        role="group"
        type="arrow"
        hidden="true"
        flip="slide"
        position="bottomcenter topright"
        noautofocus="true">
   <panelmultiview id="appMenu-multiView" mainViewId="appMenu-mainView">
-    <panelview id="appMenu-mainView" class="cui-widget-panelview">
+    <panelview id="appMenu-mainView" class="cui-widget-panelview PanelUI-subView">
       <vbox class="panel-subview-body">
         <toolbarbutton id="appMenu-new-window-button"
                        class="subviewbutton subviewbutton-iconic"
                        label="&newNavigatorCmd.label;"
+                       key="key_newNavigator"
                        command="cmd_newNavigator"/>
         <toolbarbutton id="appMenu-private-window-button"
                        class="subviewbutton subviewbutton-iconic"
                        label="&newPrivateWindow.label;"
+                       key="key_privatebrowsing"
                        command="Tools:PrivateBrowsing"/>
+        <toolbarseparator/>
+        <toolbarbutton id="appMenu-open-file-button"
+                       class="subviewbutton"
+                       label="&openFileCmd.label;"
+                       key="openFileKb"
+                       command="Browser:OpenFile"
+                       />
+        <toolbarbutton id="appMenu-save-file-button"
+                       class="subviewbutton"
+                       label="&savePageCmd.label;"
+                       key="key_savePage"
+                       command="Browser:SavePage"
+                       />
+        <toolbarbutton id="appMenu-page-setup-button"
+                       class="subviewbutton"
+                       label="&printSetupCmd.label;"
+                       command="cmd_pageSetup"
+                       />
+        <toolbarbutton id="appMenu-print-button"
+                       class="subviewbutton subviewbutton-iconic"
+                       label="&printButton.label;"
+                       key="printKb"
+#ifdef XP_MACOSX
+                       command="cmd_print"
+#else
+                       command="cmd_printPreview"
+#endif
+                       />
       </vbox>
     </panelview>
   </panelmultiview>
 </panel>
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -144,16 +144,19 @@ const PanelUI = {
   /**
    * Opens the menu panel. If the event target has a child with the
    * toolbarbutton-icon attribute, the panel will be anchored on that child.
    * Otherwise, the panel is anchored on the event target itself.
    *
    * @param aEvent the event (if any) that triggers showing the menu.
    */
   show(aEvent) {
+    if (gPhotonStructure) {
+      this._ensureShortcutsShown();
+    }
     return new Promise(resolve => {
       this.ensureReady().then(() => {
         if (this.panel.state == "open" ||
             document.documentElement.hasAttribute("customizing")) {
           resolve();
           return;
         }
 
@@ -865,16 +868,32 @@ const PanelUI = {
 
   _getPanelAnchor(candidate) {
     let iconAnchor =
       document.getAnonymousElementByAttribute(candidate, "class",
                                               "toolbarbutton-icon");
     return iconAnchor || candidate;
   },
 
+  _addedShortcuts: false,
+  _ensureShortcutsShown() {
+    if (this._addedShortcuts) {
+      return;
+    }
+    this._addedShortcuts = true;
+    for (let button of this.mainView.querySelectorAll("toolbarbutton[key]")) {
+      let keyId = button.getAttribute("key");
+      let key = document.getElementById(keyId);
+      if (!key) {
+        continue;
+      }
+      button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
+    }
+  },
+
   _notify(status, topic) {
     Services.obs.notifyObservers(window, "panelUI-notification-" + topic, status);
   }
 };
 
 XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
 
 /**
--- a/browser/components/migration/ESEDBReader.jsm
+++ b/browser/components/migration/ESEDBReader.jsm
@@ -230,17 +230,17 @@ function unloadLibraries() {
   delete gLibs.kernel;
 }
 
 function loadLibraries() {
   Services.obs.addObserver(unloadLibraries, "xpcom-shutdown");
   gLibs.ese = ctypes.open("esent.dll");
   gLibs.kernel = ctypes.open("kernel32.dll");
   KERNEL.FileTimeToSystemTime = gLibs.kernel.declare("FileTimeToSystemTime",
-    ctypes.default_abi, ctypes.int, KERNEL.FILETIME.ptr, KERNEL.SYSTEMTIME.ptr);
+    ctypes.winapi_abi, ctypes.int, KERNEL.FILETIME.ptr, KERNEL.SYSTEMTIME.ptr);
 
   declareESEFunctions();
 }
 
 function ESEDB(rootPath, dbPath, logPath) {
   log.info("Created db");
   this.rootPath = rootPath;
   this.dbPath = dbPath;
--- a/browser/components/migration/MSMigrationUtils.jsm
+++ b/browser/components/migration/MSMigrationUtils.jsm
@@ -74,17 +74,17 @@ function CtypesKernelHelpers() {
     {dwHighDateTime: wintypes.DWORD}
   ]);
 
   try {
     this._libs.kernel32 = ctypes.open("Kernel32");
 
     this._functions.FileTimeToSystemTime =
       this._libs.kernel32.declare("FileTimeToSystemTime",
-                                  ctypes.default_abi,
+                                  ctypes.winapi_abi,
                                   wintypes.BOOL,
                                   this._structs.FILETIME.ptr,
                                   this._structs.SYSTEMTIME.ptr);
   } catch (ex) {
     this.finalize();
   }
 }
 
--- a/browser/components/migration/tests/unit/test_Edge_db_migration.js
+++ b/browser/components/migration/tests/unit/test_Edge_db_migration.js
@@ -166,17 +166,17 @@ let initializedESE = false;
 
 let eseDBWritingHelpers = {
   setupDB(dbFile, tables) {
     if (!initializedESE) {
       initializedESE = true;
       loadLibraries();
 
       KERNEL.SystemTimeToFileTime = gLibs.kernel.declare("SystemTimeToFileTime",
-          ctypes.default_abi, ctypes.bool, KERNEL.SYSTEMTIME.ptr, KERNEL.FILETIME.ptr);
+          ctypes.winapi_abi, ctypes.bool, KERNEL.SYSTEMTIME.ptr, KERNEL.FILETIME.ptr);
 
       declareESEFunction("CreateDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR,
                          ESE.JET_PCWSTR, ESE.JET_DBID.ptr, ESE.JET_GRBIT);
       declareESEFunction("CreateTableColumnIndexW", ESE.JET_SESID, ESE.JET_DBID,
                          ESE.JET_TABLECREATE_W.ptr);
       declareESEFunction("BeginTransaction", ESE.JET_SESID);
       declareESEFunction("CommitTransaction", ESE.JET_SESID, ESE.JET_GRBIT);
       declareESEFunction("PrepareUpdate", ESE.JET_SESID, ESE.JET_TABLEID,
--- a/browser/components/migration/tests/unit/test_IE_cookies.js
+++ b/browser/components/migration/tests/unit/test_IE_cookies.js
@@ -17,33 +17,39 @@ add_task(function* () {
 
   /*
   BOOL InternetSetCookieW(
     _In_  LPCTSTR lpszUrl,
     _In_  LPCTSTR lpszCookieName,
     _In_  LPCTSTR lpszCookieData
   );
   */
+  // NOTE: Even though MSDN documentation does not indicate a calling convention,
+  // InternetSetCookieW is declared in SDK headers as __stdcall but is exported
+  // from wininet.dll without name mangling, so it is effectively winapi_abi
   let setIECookie = wininet.declare("InternetSetCookieW",
-                                    ctypes.default_abi,
+                                    ctypes.winapi_abi,
                                     BOOL,
                                     LPCTSTR,
                                     LPCTSTR,
                                     LPCTSTR);
 
   /*
   BOOL InternetGetCookieW(
     _In_    LPCTSTR lpszUrl,
     _In_    LPCTSTR lpszCookieName,
     _Out_   LPCTSTR  lpszCookieData,
     _Inout_ LPDWORD lpdwSize
   );
   */
+  // NOTE: Even though MSDN documentation does not indicate a calling convention,
+  // InternetGetCookieW is declared in SDK headers as __stdcall but is exported
+  // from wininet.dll without name mangling, so it is effectively winapi_abi
   let getIECookie = wininet.declare("InternetGetCookieW",
-                                    ctypes.default_abi,
+                                    ctypes.winapi_abi,
                                     BOOL,
                                     LPCTSTR,
                                     LPCTSTR,
                                     LPCTSTR,
                                     LPDWORD);
 
   // We need to randomize the cookie to avoid clashing with other cookies
   // that might have been set by previous tests and not properly cleared.
--- a/browser/components/preferences/in-content-old/tests/browser_bug731866.js
+++ b/browser/components/preferences/in-content-old/tests/browser_bug731866.js
@@ -1,30 +1,49 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
 Components.utils.import("resource://gre/modules/NetUtil.jsm");
 
+const storageManagerDisabled = !SpecialPowers.getBoolPref("browser.storageManager.enabled");
+const offlineGroupDisabled = !SpecialPowers.getBoolPref("browser.preferences.offlineGroup.enabled");
+
 function test() {
   waitForExplicitFinish();
   open_preferences(runTest);
 }
 
 var gElements;
 
 function checkElements(expectedPane) {
   for (let element of gElements) {
     // keyset and preferences elements fail is_element_visible checks because they are never visible.
     // special-case the drmGroup item because its visibility depends on pref + OS version
     if (element.nodeName == "keyset" ||
         element.nodeName == "preferences" ||
         element.id === "drmGroup") {
       continue;
     }
+    // The siteDataGroup in the Storage Management project is currently only pref-on on Nightly for testing purpose.
+    // During the test and the transition period, we have to check the pref to see if the siteDataGroup
+    // should be hidden always. This would be a bit bothersome, same as the offlineGroup as below.
+    // However, this checking is necessary to make sure we don't leak the siteDataGroup into beta/release build
+    if (element.id == "siteDataGroup" && storageManagerDisabled) {
+      is_element_hidden(element, "Disabled siteDataGroup should be hidden");
+      continue;
+    }
+    // The siteDataGroup in the Storage Management project will replace the offlineGroup eventually,
+    // so during the transition period, we have to check the pref to see if the offlineGroup
+    // should be hidden always. See the bug 1354530 for the details.
+    if (element.id == "offlineGroup" && offlineGroupDisabled) {
+      is_element_hidden(element, "Disabled offlineGroup should be hidden");
+      continue;
+    }
+
     let attributeValue = element.getAttribute("data-category");
     let suffix = " (id=" + element.id + ")";
     if (attributeValue == "pane" + expectedPane) {
       is_element_visible(element, expectedPane + " elements should be visible" + suffix);
     } else {
       is_element_hidden(element, "Elements not in " + expectedPane + " should be hidden" + suffix);
     }
   }
--- a/browser/components/preferences/in-content-old/tests/browser_bug795764_cachedisabled.js
+++ b/browser/components/preferences/in-content-old/tests/browser_bug795764_cachedisabled.js
@@ -2,60 +2,45 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
 Components.utils.import("resource://gre/modules/NetUtil.jsm");
 
 function test() {
   waitForExplicitFinish();
 
-  let prefs = [
-    "browser.cache.offline.enable",
-    "browser.cache.disk.enable",
-    "browser.cache.memory.enable",
-  ];
-  for (let pref of prefs) {
-    Services.prefs.setBoolPref(pref, false);
-  }
-
   // Adding one fake site so that the SiteDataManager would run.
   // Otherwise, without any site then it would just return so we would end up in not testing SiteDataManager.
   let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin("https://www.foo.com");
   Services.perms.addFromPrincipal(principal, "persistent-storage", Ci.nsIPermissionManager.ALLOW_ACTION);
-
   registerCleanupFunction(function() {
-    for (let pref of prefs) {
-      Services.prefs.clearUserPref(pref);
-    }
     Services.perms.removeFromPrincipal(principal, "persistent-storage");
   });
 
-  open_preferences(runTest);
+  SpecialPowers.pushPrefEnv({set: [
+    ["browser.cache.offline.enable", false],
+    ["browser.cache.disk.enable", false],
+    ["browser.cache.memory.enable", false],
+    ["browser.storageManager.enabled", true],
+    ["browser.preferences.offlineGroup.enabled", true]
+  ]}).then(() => open_preferences(runTest));
 }
 
 function runTest(win) {
   is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded");
 
   let tab = win.document;
   let elements = tab.getElementById("mainPrefPane").children;
-  let offlineGroupDisabled = !SpecialPowers.getBoolPref("browser.preferences.offlineGroup.enabled");
 
   // Test if advanced pane is opened correctly
   win.gotoPref("paneAdvanced");
   for (let element of elements) {
     if (element.nodeName == "preferences") {
       continue;
     }
-    // The siteDataGroup in the Storage Management project will replace the offlineGroup eventually,
-    // so during the transition period, we have to check the pref to see if the offlineGroup
-    // should be hidden always. See the bug 1354530 for the details.
-    if (element.id == "offlineGroup" && offlineGroupDisabled) {
-      is_element_hidden(element, "Disabled offlineGroup should be hidden");
-      continue;
-    }
     let attributeValue = element.getAttribute("data-category");
     if (attributeValue == "paneAdvanced") {
       is_element_visible(element, "Advanced elements should be visible");
     } else {
       is_element_hidden(element, "Non-Advanced elements should be hidden");
     }
   }
 
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -143,24 +143,25 @@ function telemetryBucketForCategory(cate
 function onHashChange() {
   gotoPref();
 }
 
 function gotoPref(aCategory) {
   let categories = document.getElementById("categories");
   const kDefaultCategoryInternalName = "paneGeneral";
   let hash = document.location.hash;
+
+  let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName;
+  let breakIndex = category.indexOf("-");
   // Subcategories allow for selecting smaller sections of the preferences
   // until proper search support is enabled (bug 1353954).
-  let breakIndex = hash.indexOf("-");
-  let subcategory = breakIndex != -1 && hash.substring(breakIndex + 1);
+  let subcategory = breakIndex != -1 && category.substring(breakIndex + 1);
   if (subcategory) {
-    hash = hash.substring(0, breakIndex);
+    category = category.substring(0, breakIndex);
   }
-  let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName;
   category = friendlyPrefCategoryNameToInternalName(category);
 
   // Updating the hash (below) or changing the selected category
   // will re-enter gotoPref.
   if (gLastHash == category && !subcategory)
     return;
   let item = categories.querySelector(".category[value=" + category + "]");
   if (!item) {
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -606,16 +606,17 @@
                 label="&browserContainersEnabled.label;"
                 accesskey="&browserContainersEnabled.accesskey;"
                 preference="privacy.userContext.enabled"
                 onsyncfrompreference="return gPrivacyPane.readBrowserContainersCheckbox();"/>
       <label id="browserContainersLearnMore" class="learnMore text-link"
              value="&browserContainersLearnMore.label;"/>
       <spacer flex="1"/>
       <button id="browserContainersSettings"
+              class="accessory-button"
               label="&browserContainersSettings.label;"
               accesskey="&browserContainersSettings.accesskey;"/>
     </hbox>
   </vbox>
 </groupbox>
 
 <!-- Network -->
 <!-- Connection -->
--- a/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js
+++ b/browser/components/preferences/in-content/tests/browser_bug1020245_openPreferences_to_paneContent.js
@@ -2,16 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Services.prefs.setBoolPref("browser.preferences.instantApply", true);
 
 registerCleanupFunction(function() {
   Services.prefs.clearUserPref("browser.preferences.instantApply");
 });
 
+// Test opening to the differerent panes and subcategories in Preferences
 add_task(function*() {
   let prefs = yield openPreferencesViaOpenPreferencesAPI("panePrivacy");
   is(prefs.selectedPane, "panePrivacy", "Privacy pane was selected");
   prefs = yield openPreferencesViaOpenPreferencesAPI("advanced");
   is(prefs.selectedPane, "paneAdvanced", "Advanced pane was selected");
   prefs = yield openPreferencesViaHash("privacy");
   is(prefs.selectedPane, "panePrivacy", "Privacy pane is selected when hash is 'privacy'");
   prefs = yield openPreferencesViaOpenPreferencesAPI("nonexistant-category");
@@ -31,16 +32,38 @@ add_task(function*() {
   is(prefs.selectedPane, "paneGeneral", "General pane is selected by default");
   doc = gBrowser.contentDocument;
   is(doc.location.hash, "#general", "The subcategory should be removed from the URI");
   ok(doc.querySelector("#startupGroup").hidden, "Startup should be hidden when only Search is requested");
   ok(!doc.querySelector("#engineList").hidden, "The search engine list should be visible when Search is requested");
   yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
+// Test opening Preferences with subcategory on an existing Preferences tab. See bug 1358475.
+add_task(function*() {
+  let prefs = yield openPreferencesViaOpenPreferencesAPI("general-search", {leaveOpen: true});
+  is(prefs.selectedPane, "paneGeneral", "General pane is selected by default");
+  let doc = gBrowser.contentDocument;
+  is(doc.location.hash, "#general", "The subcategory should be removed from the URI");
+  ok(doc.querySelector("#startupGroup").hidden, "Startup should be hidden when only Search is requested");
+  ok(!doc.querySelector("#engineList").hidden, "The search engine list should be visible when Search is requested");
+  // The reasons that here just call the `openPreferences` API without the helping function are
+  //   - already opened one about:preferences tab up there and
+  //   - the goal is to test on the existing tab and
+  //   - using `openPreferencesViaOpenPreferencesAPI` would introduce more handling of additional about:blank and unneccessary event
+  openPreferences("privacy-reports");
+  let selectedPane = gBrowser.contentWindow.history.state;
+  is(selectedPane, "panePrivacy", "Privacy pane should be selected");
+  is(doc.location.hash, "#privacy", "The subcategory should be removed from the URI");
+  ok(doc.querySelector("#locationBarGroup").hidden, "Location Bar prefs should be hidden when only Reports are requested");
+  ok(!doc.querySelector("#header-privacy").hidden, "The header should be visible when a subcategory is requested");
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+
 function openPreferencesViaHash(aPane) {
   let deferred = Promise.defer();
   gBrowser.selectedTab = gBrowser.addTab("about:preferences" + (aPane ? "#" + aPane : ""));
   let newTabBrowser = gBrowser.selectedBrowser;
 
   newTabBrowser.addEventListener("Initialized", function() {
     newTabBrowser.contentWindow.addEventListener("load", function() {
       let win = gBrowser.contentWindow;
--- a/browser/components/preferences/in-content/tests/browser_bug731866.js
+++ b/browser/components/preferences/in-content/tests/browser_bug731866.js
@@ -1,37 +1,45 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
 Components.utils.import("resource://gre/modules/NetUtil.jsm");
 
+const storageManagerDisabled = !SpecialPowers.getBoolPref("browser.storageManager.enabled");
+const offlineGroupDisabled = !SpecialPowers.getBoolPref("browser.preferences.offlineGroup.enabled");
+
 function test() {
   waitForExplicitFinish();
-  SpecialPowers.pushPrefEnv({set: [["browser.storageManager.enabled", true]]});
   open_preferences(runTest);
 }
 
 var gElements;
 
 function checkElements(expectedPane) {
   for (let element of gElements) {
     // keyset and preferences elements fail is_element_visible checks because they are never visible.
     // special-case the drmGroup item because its visibility depends on pref + OS version
     if (element.nodeName == "keyset" ||
         element.nodeName == "preferences" ||
         element.id === "drmGroup") {
       continue;
     }
-
+    // The siteDataGroup in the Storage Management project is currently only pref-on on Nightly for testing purpose.
+    // During the test and the transition period, we have to check the pref to see if the siteDataGroup
+    // should be hidden always. This would be a bit bothersome, same as the offlineGroup as below.
+    // However, this checking is necessary to make sure we don't leak the siteDataGroup into beta/release build
+    if (element.id == "siteDataGroup" && storageManagerDisabled) {
+      is_element_hidden(element, "Disabled siteDataGroup should be hidden");
+      continue;
+    }
     // The siteDataGroup in the Storage Management project will replace the offlineGroup eventually,
     // so during the transition period, we have to check the pref to see if the offlineGroup
     // should be hidden always. See the bug 1354530 for the details.
-    if (element.id == "offlineGroup" &&
-        !SpecialPowers.getBoolPref("browser.preferences.offlineGroup.enabled")) {
+    if (element.id == "offlineGroup" && offlineGroupDisabled) {
       is_element_hidden(element, "Disabled offlineGroup should be hidden");
       continue;
     }
 
     let attributeValue = element.getAttribute("data-category");
     let suffix = " (id=" + element.id + ")";
     if (attributeValue == "pane" + expectedPane) {
       is_element_visible(element, expectedPane + " elements should be visible" + suffix);
--- a/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js
+++ b/browser/components/preferences/in-content/tests/browser_bug795764_cachedisabled.js
@@ -3,60 +3,45 @@
 
 const { interfaces: Ci, utils: Cu } = Components;
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 
 function test() {
   waitForExplicitFinish();
 
-  let prefs = [
-    "browser.cache.offline.enable",
-    "browser.cache.disk.enable",
-    "browser.cache.memory.enable",
-  ];
-  for (let pref of prefs) {
-    Services.prefs.setBoolPref(pref, false);
-  }
-
   // Adding one fake site so that the SiteDataManager would run.
   // Otherwise, without any site then it would just return so we would end up in not testing SiteDataManager.
   let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin("https://www.foo.com");
   Services.perms.addFromPrincipal(principal, "persistent-storage", Ci.nsIPermissionManager.ALLOW_ACTION);
-
   registerCleanupFunction(function() {
-    for (let pref of prefs) {
-      Services.prefs.clearUserPref(pref);
-    }
     Services.perms.removeFromPrincipal(principal, "persistent-storage");
   });
 
-  open_preferences(runTest);
+  SpecialPowers.pushPrefEnv({set: [
+    ["browser.cache.offline.enable", false],
+    ["browser.cache.disk.enable", false],
+    ["browser.cache.memory.enable", false],
+    ["browser.storageManager.enabled", true],
+    ["browser.preferences.offlineGroup.enabled", true]
+  ]}).then(() => open_preferences(runTest));
 }
 
 function runTest(win) {
   is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded");
 
   let tab = win.document;
   let elements = tab.getElementById("mainPrefPane").children;
-  let offlineGroupDisabled = !SpecialPowers.getBoolPref("browser.preferences.offlineGroup.enabled");
 
   // Test if privacy pane is opened correctly
   win.gotoPref("panePrivacy");
   for (let element of elements) {
     if (element.nodeName == "preferences") {
       continue;
     }
-    // The siteDataGroup in the Storage Management project will replace the offlineGroup eventually,
-    // so during the transition period, we have to check the pref to see if the offlineGroup
-    // should be hidden always. See the bug 1354530 for the details.
-    if (element.id == "offlineGroup" && offlineGroupDisabled) {
-      is_element_hidden(element, "Disabled offlineGroup should be hidden");
-      continue;
-    }
     let attributeValue = element.getAttribute("data-category");
     if (attributeValue == "panePrivacy") {
       is_element_visible(element, "Privacy elements should be visible");
     } else {
       is_element_hidden(element, "Non-Privacy elements should be hidden");
     }
   }
 
--- a/browser/components/sessionstore/SessionSaver.jsm
+++ b/browser/components/sessionstore/SessionSaver.jsm
@@ -237,17 +237,17 @@ var SessionSaverInternal = {
     if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
       // Don't save (or even collect) anything in permanent private
       // browsing mode
 
       this.updateLastSaveTime();
       return Promise.resolve();
     }
 
-    stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
+    stopWatchStart("COLLECT_DATA_MS");
     let state = SessionStore.getCurrentState(forceUpdateAllWindows);
     PrivacyFilter.filterPrivateWindowsAndTabs(state);
 
     // Make sure we only write worth saving tabs to disk.
     SessionStore.keepOnlyWorthSavingTabs(state);
 
     // Make sure that we keep the previous session if we started with a single
     // private window and no non-private windows have been opened, yet.
@@ -269,36 +269,54 @@ var SessionSaverInternal = {
           break;
         }
 
         delete state._closedWindows[i]._shouldRestore;
         state.windows.unshift(state._closedWindows.pop());
       }
     }
 
-    // Clear all cookies and storage on clean shutdown according to user preferences
-    if (RunState.isClosing) {
-      let expireCookies = Services.prefs.getIntPref("network.cookie.lifetimePolicy") ==
-                          Services.cookies.QueryInterface(Ci.nsICookieService).ACCEPT_SESSION;
-      let sanitizeCookies = Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown") &&
-                            Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies");
-      let restart = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
-      // Don't clear when restarting
-      if ((expireCookies || sanitizeCookies) && !restart) {
-        for (let window of state.windows) {
-          delete window.cookies;
-          for (let tab of window.tabs) {
-            delete tab.storage;
-          }
+    // Clear cookies and storage on clean shutdown.
+    this._maybeClearCookiesAndStorage(state);
+
+    stopWatchFinish("COLLECT_DATA_MS");
+    return this._writeState(state);
+  },
+
+  /**
+   * Purges cookies and DOMSessionStorage data from the session on clean
+   * shutdown, only if requested by the user's preferences.
+   */
+  _maybeClearCookiesAndStorage(state) {
+    // Only do this on shutdown.
+    if (!RunState.isClosing) {
+      return;
+    }
+
+    // Don't clear when restarting.
+    if (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) {
+      return;
+    }
+
+    let expireCookies = Services.prefs.getIntPref("network.cookie.lifetimePolicy") ==
+                        Services.cookies.QueryInterface(Ci.nsICookieService).ACCEPT_SESSION;
+    let sanitizeCookies = Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown") &&
+                          Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies");
+
+    if (expireCookies || sanitizeCookies) {
+      // Remove cookies.
+      delete state.cookies;
+
+      // Remove DOMSessionStorage data.
+      for (let window of state.windows) {
+        for (let tab of window.tabs) {
+          delete tab.storage;
         }
       }
     }
-
-    stopWatchFinish("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
-    return this._writeState(state);
   },
 
   /**
    * Saves the current session state. Collects data asynchronously and calls
    * _saveState() to collect data again (with a cache hit rate of hopefully
    * 100%) and write to disk afterwards.
    */
   _saveStateAsync() {
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -3133,19 +3133,17 @@ var SessionStoreInternal = {
       windows: total,
       selectedWindow: ix + 1,
       _closedWindows: lastClosedWindowsCopy,
       session,
       global: this._globalState.getState()
     };
 
     // Collect and store session cookies.
-    TelemetryStopwatch.start("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
     state.cookies = SessionCookies.collect();
-    TelemetryStopwatch.finish("FX_SESSION_RESTORE_COLLECT_COOKIES_MS");
 
     if (Cu.isModuleLoaded("resource://devtools/client/scratchpad/scratchpad-manager.jsm")) {
       // get open Scratchpad window states too
       let scratchpads = ScratchpadManager.getSessionState();
       if (scratchpads && scratchpads.length) {
         state.scratchpads = scratchpads;
       }
     }
@@ -3331,17 +3329,17 @@ var SessionStoreInternal = {
         numVisibleTabs++;
       }
 
       if (!!winData.tabs[t].muted != tabs[t].linkedBrowser.audioMuted) {
         tabs[t].toggleMuteAudio(winData.tabs[t].muteReason);
       }
     }
 
-    if (selectTab > 0) {
+    if (selectTab > 0 && selectTab <= tabs.length) {
       // The state we're restoring wants to select a particular tab. This
       // implies that we're overwriting tabs.
       let currentIndex = tabbrowser.tabContainer.selectedIndex;
       let targetIndex = selectTab - 1;
 
       if (currentIndex != targetIndex) {
         // We need to change the selected tab. There are two ways of doing this:
         //
@@ -3846,17 +3844,17 @@ var SessionStoreInternal = {
     // If the restored browser wants to show view source content, start up a
     // view source browser that will load the required frame script.
     if (uri && ViewSourceBrowser.isViewSource(uri)) {
       new ViewSourceBrowser(browser);
     }
 
     browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent",
       {loadArguments: aLoadArguments, isRemotenessUpdate,
-       reason: aReason});
+       reason: aReason, requestTime: Services.telemetry.msSystemNow()});
   },
 
   /**
    * Marks a given pending tab as restoring.
    *
    * @param aTab
    *        the pending tab to mark as restoring
    */
@@ -4401,18 +4399,18 @@ var SessionStoreInternal = {
       // We're going to put the state of the window into this object
       let pinnedWindowState = { tabs: [] };
       for (let tIndex = 0; tIndex < window.tabs.length;) {
         if (window.tabs[tIndex].pinned) {
           // Adjust window.selected
           if (tIndex + 1 < window.selected)
             window.selected -= 1;
           else if (tIndex + 1 == window.selected)
-            pinnedWindowState.selected = pinnedWindowState.tabs.length + 2;
-            // + 2 because the tab isn't actually in the array yet
+            pinnedWindowState.selected = pinnedWindowState.tabs.length + 1;
+            // + 1 because the tab isn't actually in the array yet
 
           // Now add the pinned tab to our window
           pinnedWindowState.tabs =
             pinnedWindowState.tabs.concat(window.tabs.splice(tIndex, 1));
           // We don't want to increment tIndex here.
           continue;
         }
         tIndex++;
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -139,16 +139,21 @@ var MessageListener = {
       gCurrentEpoch = data.epoch;
     }
 
     switch (name) {
       case "SessionStore:restoreHistory":
         this.restoreHistory(data);
         break;
       case "SessionStore:restoreTabContent":
+        if (data.isRemotenessUpdate) {
+          let histogram = Services.telemetry.getKeyedHistogramById("FX_TAB_REMOTE_NAVIGATION_DELAY_MS");
+          histogram.add("SessionStore:restoreTabContent",
+                        Services.telemetry.msSystemNow() - data.requestTime);
+        }
         this.restoreTabContent(data);
         break;
       case "SessionStore:resetRestore":
         gContentRestore.resetRestore();
         break;
       case "SessionStore:flush":
         this.flush(data);
         break;
--- a/browser/components/sessionstore/test/browser_async_window_flushing.js
+++ b/browser/components/sessionstore/test/browser_async_window_flushing.js
@@ -99,18 +99,17 @@ add_task(function* test_remove_uninteres
 
   yield BrowserTestUtils.browserLoaded(browser, false, PAGE);
   yield TabStateFlusher.flush(browser);
 
   // Send a message that will cause the content to purge its
   // history entries and make itself seem uninteresting.
   yield ContentTask.spawn(browser, null, function*() {
     // Epic hackery to make this browser seem suddenly boring.
-    Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
-    docShell.setCurrentURI(BrowserUtils.makeURI("about:blank"));
+    docShell.setCurrentURI(Services.io.newURI("about:blank"));
 
     let {sessionHistory} = docShell.QueryInterface(Ci.nsIWebNavigation);
     sessionHistory.PurgeHistory(sessionHistory.count);
   });
 
   // Once the domWindowClosed Promise resolves, the window should
   // have closed, and SessionStore's onClose handler should have just
   // run.
--- a/browser/config/mozconfigs/linux64/code-coverage
+++ b/browser/config/mozconfigs/linux64/code-coverage
@@ -1,13 +1,14 @@
 . "$topsrcdir/browser/config/mozconfigs/linux64/nightly"
 
 TOOLTOOL_DIR=${TOOLTOOL_DIR:-$topsrcdir}
 
 ac_add_options --disable-install-strip
 ac_add_options --disable-jemalloc
 ac_add_options --disable-crashreporter
 ac_add_options --disable-elf-hack
+ac_add_options --enable-debug
 
 MOZ_CODE_COVERAGE=1
 export CFLAGS="--coverage"
 export CXXFLAGS="--coverage"
 export LDFLAGS="--coverage -L$TOOLTOOL_DIR/gtk3/usr/local/lib"
--- a/browser/extensions/activity-stream/bootstrap.js
+++ b/browser/extensions/activity-stream/bootstrap.js
@@ -1,29 +1,32 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-/* globals Components, XPCOMUtils, Preferences, ActivityStream */
+/* globals Components, XPCOMUtils, Preferences, Services, ActivityStream */
 "use strict";
 
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ActivityStream",
   "resource://activity-stream/lib/ActivityStream.jsm");
 
+const BROWSER_READY_NOTIFICATION = "browser-ui-startup-complete";
 const ACTIVITY_STREAM_ENABLED_PREF = "browser.newtabpage.activity-stream.enabled";
 const REASON_STARTUP_ON_PREF_CHANGE = "PREF_ON";
 const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF";
 
 const ACTIVITY_STREAM_OPTIONS = {newTabURL: "about:newtab"};
 
 let activityStream;
 let startupData;
+let startupReason;
 
 /**
  * init - Initializes an instance of ActivityStream. This could be called by
  *        the startup() function exposed by bootstrap.js, or it could be called
  *        when ACTIVITY_STREAM_ENABLED_PREF is changed from false to true.
  *
  * @param  {string} reason - Reason for initialization. Could be install, upgrade, or PREF_ON
  */
@@ -59,36 +62,47 @@ function uninit(reason) {
 function onPrefChanged(isEnabled) {
   if (isEnabled) {
     init(REASON_STARTUP_ON_PREF_CHANGE);
   } else {
     uninit(REASON_SHUTDOWN_ON_PREF_CHANGE);
   }
 }
 
+function observe(subject, topic, data) {
+  switch (topic) {
+    case BROWSER_READY_NOTIFICATION:
+      // Listen for changes to the pref that enables Activity Stream
+      Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+      // Only initialize if the pref is true
+      if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
+        init(startupReason);
+        Services.obs.removeObserver(this, BROWSER_READY_NOTIFICATION);
+      }
+      break;
+  }
+}
+
 // The functions below are required by bootstrap.js
 
 this.install = function install(data, reason) {};
 
 this.startup = function startup(data, reason) {
+  // Only start Activity Stream up when the browser UI is ready
+  Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION);
+
   // Cache startup data which contains stuff like the version number, etc.
   // so we can use it when we init
   startupData = data;
-
-  // Listen for changes to the pref that enables Activity Stream
-  Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
-
-  // Only initialize if the pref is true
-  if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
-    init(reason);
-  }
+  startupReason = reason;
 };
 
 this.shutdown = function shutdown(data, reason) {
   // Uninitialize Activity Stream
   startupData = null;
+  startupReason = null;
   uninit(reason);
 
   // Stop listening to the pref that enables Activity Stream
   Preferences.ignore(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
 };
 
 this.uninstall = function uninstall(data, reason) {};
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -1,22 +1,26 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-this.MAIN_MESSAGE_TYPE = "ActivityStream:Main";
-this.CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
+const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
+const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
 
-this.actionTypes = [
+const actionTypes = [
   "INIT",
   "UNINIT",
   "NEW_TAB_INITIAL_STATE",
   "NEW_TAB_LOAD",
-  "NEW_TAB_UNLOAD"
+  "NEW_TAB_UNLOAD",
+  "PERFORM_SEARCH",
+  "SCREENSHOT_UPDATED",
+  "SEARCH_STATE_UPDATED",
+  "TOP_SITES_UPDATED"
 // The line below creates an object like this:
 // {
 //   INIT: "INIT",
 //   UNINIT: "UNINIT"
 // }
 // It prevents accidentally adding a different key/value name.
 ].reduce((obj, type) => { obj[type] = type; return obj; }, {});
 
@@ -81,16 +85,18 @@ function SendToContent(action, target) {
   }
   return _RouteMessage(action, {
     from: MAIN_MESSAGE_TYPE,
     to: CONTENT_MESSAGE_TYPE,
     toTarget: target
   });
 }
 
+this.actionTypes = actionTypes;
+
 this.actionCreators = {
   SendToMain,
   SendToContent,
   BroadcastToContent
 };
 
 // These are helpers to test for certain kinds of actions
 this.actionUtils = {
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -1,44 +1,65 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-this.INITIAL_STATE = {
+const {actionTypes: at} = Components.utils.import("resource://activity-stream/common/Actions.jsm", {});
+
+const INITIAL_STATE = {
   TopSites: {
-    rows: [
-      {
-        "title": "Facebook",
-        "url": "https://www.facebook.com/"
-      },
-      {
-        "title": "YouTube",
-        "url": "https://www.youtube.com/"
-      },
-      {
-        "title": "Amazon",
-        "url": "http://www.amazon.com/"
-      },
-      {
-        "title": "Yahoo",
-        "url": "https://www.yahoo.com/"
-      },
-      {
-        "title": "eBay",
-        "url": "http://www.ebay.com"
-      },
-      {
-        "title": "Twitter",
-        "url": "https://twitter.com/"
-      }
-    ]
+    init: false,
+    rows: []
+  },
+  Search: {
+    currentEngine: {
+      name: "",
+      icon: ""
+    },
+    engines: []
   }
 };
 
 // TODO: Handle some real actions here, once we have a TopSites feed working
 function TopSites(prevState = INITIAL_STATE.TopSites, action) {
-  return prevState;
+  let hasMatch;
+  let newRows;
+  switch (action.type) {
+    case at.TOP_SITES_UPDATED:
+      if (!action.data) {
+        return prevState;
+      }
+      return Object.assign({}, prevState, {init: true, rows: action.data});
+    case at.SCREENSHOT_UPDATED:
+      newRows = prevState.rows.map(row => {
+        if (row.url === action.data.url) {
+          hasMatch = true;
+          return Object.assign({}, row, {screenshot: action.data.screenshot});
+        }
+        return row;
+      });
+      return hasMatch ? Object.assign({}, prevState, {rows: newRows}) : prevState;
+    default:
+      return prevState;
+  }
 }
 
-this.reducers = {TopSites};
+function Search(prevState = INITIAL_STATE.Search, action) {
+  switch (action.type) {
+    case at.SEARCH_STATE_UPDATED: {
+      if (!action.data) {
+        return prevState;
+      }
+      let {currentEngine, engines} = action.data;
+      return Object.assign({}, prevState, {
+        currentEngine,
+        engines
+      });
+    }
+    default:
+      return prevState;
+  }
+}
+this.INITIAL_STATE = INITIAL_STATE;
+this.reducers = {TopSites, Search};
 
 this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -0,0 +1,570 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ 	// The module cache
+/******/ 	var installedModules = {};
+/******/
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+/******/
+/******/ 		// Check if module is in cache
+/******/ 		if(installedModules[moduleId])
+/******/ 			return installedModules[moduleId].exports;
+/******/
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = installedModules[moduleId] = {
+/******/ 			i: moduleId,
+/******/ 			l: false,
+/******/ 			exports: {}
+/******/ 		};
+/******/
+/******/ 		// Execute the module function
+/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ 		// Flag the module as loaded
+/******/ 		module.l = true;
+/******/
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+/******/
+/******/
+/******/ 	// expose the modules object (__webpack_modules__)
+/******/ 	__webpack_require__.m = modules;
+/******/
+/******/ 	// expose the module cache
+/******/ 	__webpack_require__.c = installedModules;
+/******/
+/******/ 	// identity function for calling harmony imports with the correct context
+/******/ 	__webpack_require__.i = function(value) { return value; };
+/******/
+/******/ 	// define getter function for harmony exports
+/******/ 	__webpack_require__.d = function(exports, name, getter) {
+/******/ 		if(!__webpack_require__.o(exports, name)) {
+/******/ 			Object.defineProperty(exports, name, {
+/******/ 				configurable: false,
+/******/ 				enumerable: true,
+/******/ 				get: getter
+/******/ 			});
+/******/ 		}
+/******/ 	};
+/******/
+/******/ 	// getDefaultExport function for compatibility with non-harmony modules
+/******/ 	__webpack_require__.n = function(module) {
+/******/ 		var getter = module && module.__esModule ?
+/******/ 			function getDefault() { return module['default']; } :
+/******/ 			function getModuleExports() { return module; };
+/******/ 		__webpack_require__.d(getter, 'a', getter);
+/******/ 		return getter;
+/******/ 	};
+/******/
+/******/ 	// Object.prototype.hasOwnProperty.call
+/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ 	// __webpack_public_path__
+/******/ 	__webpack_require__.p = "";
+/******/
+/******/ 	// Load entry module and return exports
+/******/ 	return __webpack_require__(__webpack_require__.s = 10);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports) {
+
+module.exports = React;
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports) {
+
+module.exports = ReactRedux;
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
+const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
+
+const actionTypes = ["INIT", "UNINIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "PERFORM_SEARCH", "SCREENSHOT_UPDATED", "SEARCH_STATE_UPDATED", "TOP_SITES_UPDATED"
+// The line below creates an object like this:
+// {
+//   INIT: "INIT",
+//   UNINIT: "UNINIT"
+// }
+// It prevents accidentally adding a different key/value name.
+].reduce((obj, type) => {
+  obj[type] = type;return obj;
+}, {});
+
+// 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) {
+    throw new Error("Routed Messages must have options as the second parameter, and must at least include a .from and .to property.");
+  }
+  // For each of these fields, if they are passed as an option,
+  // add them to the action. If they are not defined, remove them.
+  ["from", "to", "toTarget", "fromTarget", "skipOrigin"].forEach(o => {
+    if (typeof options[o] !== "undefined") {
+      meta[o] = options[o];
+    } else if (meta[o]) {
+      delete meta[o];
+    }
+  });
+  return Object.assign({}, action, { meta });
+}
+
+/**
+ * SendToMain - Creates a message that will be sent to the Main process.
+ *
+ * @param  {object} action Any redux action (required)
+ * @param  {object} options
+ * @param  {string} options.fromTarget The id of the content port from which the action originated. (optional)
+ * @return {object} An action with added .meta properties
+ */
+function SendToMain(action) {
+  let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+  return _RouteMessage(action, {
+    from: CONTENT_MESSAGE_TYPE,
+    to: MAIN_MESSAGE_TYPE,
+    fromTarget: options.fromTarget
+  });
+}
+
+/**
+ * BroadcastToContent - Creates a message that will be sent to ALL content processes.
+ *
+ * @param  {object} action Any redux action (required)
+ * @return {object} An action with added .meta properties
+ */
+function BroadcastToContent(action) {
+  return _RouteMessage(action, {
+    from: MAIN_MESSAGE_TYPE,
+    to: CONTENT_MESSAGE_TYPE
+  });
+}
+
+/**
+ * SendToContent - Creates a message that will be sent to a particular Content process.
+ *
+ * @param  {object} action Any redux action (required)
+ * @param  {string} target The id of a content port
+ * @return {object} An action with added .meta properties
+ */
+function SendToContent(action, target) {
+  if (!target) {
+    throw new Error("You must provide a target ID as the second parameter of SendToContent. If you want to send to all content processes, use BroadcastToContent");
+  }
+  return _RouteMessage(action, {
+    from: MAIN_MESSAGE_TYPE,
+    to: CONTENT_MESSAGE_TYPE,
+    toTarget: target
+  });
+}
+
+var actionCreators = {
+  SendToMain,
+  SendToContent,
+  BroadcastToContent
+};
+
+// These are helpers to test for certain kinds of actions
+
+var actionUtils = {
+  isSendToMain(action) {
+    if (!action.meta) {
+      return false;
+    }
+    return action.meta.to === MAIN_MESSAGE_TYPE && action.meta.from === CONTENT_MESSAGE_TYPE;
+  },
+  isBroadcastToContent(action) {
+    if (!action.meta) {
+      return false;
+    }
+    if (action.meta.to === CONTENT_MESSAGE_TYPE && !action.meta.toTarget) {
+      return true;
+    }
+    return false;
+  },
+  isSendToContent(action) {
+    if (!action.meta) {
+      return false;
+    }
+    if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) {
+      return true;
+    }
+    return false;
+  },
+  _RouteMessage
+};
+module.exports = {
+  actionTypes,
+  actionCreators,
+  actionUtils,
+  MAIN_MESSAGE_TYPE,
+  CONTENT_MESSAGE_TYPE
+};
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+const TopSites = __webpack_require__(8);
+const Search = __webpack_require__(7);
+
+const Base = () => React.createElement(
+  "div",
+  { className: "outer-wrapper" },
+  React.createElement(
+    "main",
+    null,
+    React.createElement(Search, null),
+    React.createElement(TopSites, null)
+  )
+);
+
+module.exports = Base;
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* globals sendAsyncMessage, addMessageListener */
+
+var _require = __webpack_require__(9);
+
+const createStore = _require.createStore,
+      combineReducers = _require.combineReducers,
+      applyMiddleware = _require.applyMiddleware;
+
+var _require2 = __webpack_require__(2);
+
+const au = _require2.actionUtils;
+
+
+const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE";
+const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain";
+const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent";
+
+/**
+ * A higher-order function which returns a reducer that, on MERGE_STORE action,
+ * will return the action.data object merged into the previous state.
+ *
+ * For all other actions, it merely calls mainReducer.
+ *
+ * Because we want this to merge the entire state object, it's written as a
+ * higher order function which takes the main reducer (itself often a call to
+ * combineReducers) as a parameter.
+ *
+ * @param  {function} mainReducer reducer to call if action != MERGE_STORE_ACTION
+ * @return {function}             a reducer that, on MERGE_STORE_ACTION action,
+ *                                will return the action.data object merged
+ *                                into the previous state, and the result
+ *                                of calling mainReducer otherwise.
+ */
+function mergeStateReducer(mainReducer) {
+  return (prevState, action) => {
+    if (action.type === MERGE_STORE_ACTION) {
+      return Object.assign({}, prevState, action.data);
+    }
+
+    return mainReducer(prevState, action);
+  };
+}
+
+/**
+ * messageMiddleware - Middleware that looks for SentToMain type actions, and sends them if necessary
+ */
+const messageMiddleware = store => next => action => {
+  if (au.isSendToMain(action)) {
+    sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
+  }
+  next(action);
+};
+
+/**
+ * initStore - Create a store and listen for incoming actions
+ *
+ * @param  {object} reducers An object containing Redux reducers
+ * @return {object}          A redux store
+ */
+module.exports = function initStore(reducers) {
+  const store = createStore(mergeStateReducer(combineReducers(reducers)), applyMiddleware(messageMiddleware));
+
+  addMessageListener(INCOMING_MESSAGE_NAME, msg => {
+    store.dispatch(msg.data);
+  });
+
+  return store;
+};
+
+module.exports.MERGE_STORE_ACTION = MERGE_STORE_ACTION;
+module.exports.OUTGOING_MESSAGE_NAME = OUTGOING_MESSAGE_NAME;
+module.exports.INCOMING_MESSAGE_NAME = INCOMING_MESSAGE_NAME;
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* 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/. */
+
+
+var _require = __webpack_require__(2);
+
+const at = _require.actionTypes;
+
+
+const INITIAL_STATE = {
+  TopSites: {
+    init: false,
+    rows: []
+  },
+  Search: {
+    currentEngine: {
+      name: "",
+      icon: ""
+    },
+    engines: []
+  }
+};
+
+// TODO: Handle some real actions here, once we have a TopSites feed working
+function TopSites() {
+  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.TopSites;
+  let action = arguments[1];
+
+  let hasMatch;
+  let newRows;
+  switch (action.type) {
+    case at.TOP_SITES_UPDATED:
+      if (!action.data) {
+        return prevState;
+      }
+      return Object.assign({}, prevState, { init: true, rows: action.data });
+    case at.SCREENSHOT_UPDATED:
+      newRows = prevState.rows.map(row => {
+        if (row.url === action.data.url) {
+          hasMatch = true;
+          return Object.assign({}, row, { screenshot: action.data.screenshot });
+        }
+        return row;
+      });
+      return hasMatch ? Object.assign({}, prevState, { rows: newRows }) : prevState;
+    default:
+      return prevState;
+  }
+}
+
+function Search() {
+  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Search;
+  let action = arguments[1];
+
+  switch (action.type) {
+    case at.SEARCH_STATE_UPDATED:
+      {
+        if (!action.data) {
+          return prevState;
+        }
+        var _action$data = action.data;
+        let currentEngine = _action$data.currentEngine,
+            engines = _action$data.engines;
+
+        return Object.assign({}, prevState, {
+          currentEngine,
+          engines
+        });
+      }
+    default:
+      return prevState;
+  }
+}
+var reducers = { TopSites, Search };
+module.exports = {
+  reducers,
+  INITIAL_STATE
+};
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports) {
+
+module.exports = ReactDOM;
+
+/***/ }),
+/* 7 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+
+var _require = __webpack_require__(1);
+
+const connect = _require.connect;
+
+var _require2 = __webpack_require__(2);
+
+const actionTypes = _require2.actionTypes,
+      actionCreators = _require2.actionCreators;
+
+
+const Search = React.createClass({
+  displayName: "Search",
+
+  getInitialState() {
+    return { searchString: "" };
+  },
+  performSearch(options) {
+    let searchData = {
+      engineName: options.engineName,
+      searchString: options.searchString,
+      searchPurpose: "newtab",
+      healthReportKey: "newtab"
+    };
+    this.props.dispatch(actionCreators.SendToMain({ type: actionTypes.PERFORM_SEARCH, data: searchData }));
+  },
+  onClick(event) {
+    const currentEngine = this.props.Search.currentEngine;
+
+    event.preventDefault();
+    this.performSearch({ engineName: currentEngine.name, searchString: this.state.searchString });
+  },
+  onChange(event) {
+    this.setState({ searchString: event.target.value });
+  },
+  render() {
+    return React.createElement(
+      "form",
+      { className: "search-wrapper" },
+      React.createElement("span", { className: "search-label" }),
+      React.createElement("input", { value: this.state.searchString, type: "search",
+        onChange: this.onChange,
+        maxLength: "256", title: "Submit search",
+        placeholder: "Search the Web" }),
+      React.createElement("button", { onClick: this.onClick })
+    );
+  }
+});
+
+module.exports = connect(state => ({ Search: state.Search }))(Search);
+
+/***/ }),
+/* 8 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+const React = __webpack_require__(0);
+
+var _require = __webpack_require__(1);
+
+const connect = _require.connect;
+
+
+function displayURL(url) {
+  return new URL(url).hostname.replace(/^www./, "");
+}
+
+const TopSites = props => React.createElement(
+  "section",
+  null,
+  React.createElement(
+    "h3",
+    { className: "section-title" },
+    "Top Sites"
+  ),
+  React.createElement(
+    "ul",
+    { className: "top-sites-list" },
+    props.TopSites.rows.map(link => {
+      const title = displayURL(link.url);
+      const className = `screenshot${link.screenshot ? " active" : ""}`;
+      const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
+      return React.createElement(
+        "li",
+        { key: link.url },
+        React.createElement(
+          "a",
+          { href: link.url },
+          React.createElement(
+            "div",
+            { className: "tile" },
+            React.createElement(
+              "span",
+              { className: "letter-fallback", ariaHidden: true },
+              title[0]
+            ),
+            React.createElement("div", { className: className, style: style })
+          ),
+          React.createElement(
+            "div",
+            { className: "title" },
+            title
+          )
+        )
+      );
+    })
+  )
+);
+
+module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSites);
+
+/***/ }),
+/* 9 */
+/***/ (function(module, exports) {
+
+module.exports = Redux;
+
+/***/ }),
+/* 10 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+/* globals addMessageListener, removeMessageListener */
+const React = __webpack_require__(0);
+const ReactDOM = __webpack_require__(6);
+const Base = __webpack_require__(3);
+
+var _require = __webpack_require__(1);
+
+const Provider = _require.Provider;
+
+const initStore = __webpack_require__(4);
+
+var _require2 = __webpack_require__(5);
+
+const reducers = _require2.reducers;
+
+
+const store = initStore(reducers);
+
+ReactDOM.render(React.createElement(
+  Provider,
+  { store: store },
+  React.createElement(Base, null)
+), document.getElementById("root"));
+
+/***/ })
+/******/ ]);
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -0,0 +1,334 @@
+html {
+  box-sizing: border-box; }
+
+*,
+*::before,
+*::after {
+  box-sizing: inherit; }
+
+body {
+  margin: 0; }
+
+button,
+input {
+  font-family: inherit;
+  font-size: inherit; }
+
+[hidden] {
+  display: none !important; }
+
+html,
+body,
+#root {
+  height: 100%; }
+
+body {
+  background: #F6F6F8;
+  color: #383E49;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Ubuntu', 'Helvetica Neue', sans-serif;
+  font-size: 16px; }
+
+h1,
+h2 {
+  font-weight: normal; }
+
+a {
+  color: #00AFF7;
+  text-decoration: none; }
+  a:hover {
+    color: #2bc1ff; }
+
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  border: 0; }
+
+.inner-border {
+  border: 1px solid rgba(0, 0, 0, 0.1);
+  border-radius: 3px;
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+  z-index: 100; }
+
+@keyframes fadeIn {
+  from {
+    opacity: 0; }
+  to {
+    opacity: 1; } }
+
+.show-on-init {
+  opacity: 0;
+  transition: opacity 0.2s ease-in; }
+  .show-on-init.on {
+    opacity: 1;
+    animation: fadeIn 0.2s; }
+
+.actions {
+  border-top: solid 1px rgba(0, 0, 0, 0.1);
+  display: flex;
+  flex-direction: row;
+  margin: 0;
+  padding: 15px 25px;
+  justify-content: flex-start; }
+  .actions button {
+    background: #FBFBFB;
+    border: solid 1px #BFBFBF;
+    border-radius: 5px;
+    color: #858585;
+    cursor: pointer;
+    padding: 10px 30px; }
+    .actions button:hover {
+      box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
+      transition: box-shadow 150ms; }
+    .actions button.done {
+      background: #0695F9;
+      border: solid 1px #1677CF;
+      color: #FFF;
+      margin-inline-start: auto; }
+
+.outer-wrapper {
+  display: flex;
+  flex-grow: 1;
+  padding: 62px 32px 32px;
+  height: 100%; }
+
+main {
+  margin: auto; }
+  @media (min-width: 672px) {
+    main {
+      width: 608px; } }
+  @media (min-width: 800px) {
+    main {
+      width: 736px; } }
+  main section {
+    margin-bottom: 41px; }
+
+.section-title {
+  color: #6E707E;
+  font-size: 13px;
+  font-weight: bold;
+  text-transform: uppercase;
+  margin: 0 0 18px; }
+
+.top-sites-list {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  margin-inline-end: -32px; }
+  @media (min-width: 672px) {
+    .top-sites-list {
+      width: 640px; } }
+  @media (min-width: 800px) {
+    .top-sites-list {
+      width: 768px; } }
+  .top-sites-list li {
+    display: inline-block;
+    margin: 0 0 18px;
+    margin-inline-end: 32px; }
+  .top-sites-list a {
+    display: block;
+    color: inherit; }
+  .top-sites-list .tile {
+    position: relative;
+    height: 96px;
+    width: 96px;
+    border-radius: 6px;
+    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
+    color: #A0A0A0;
+    font-weight: 200;
+    font-size: 32px;
+    text-transform: uppercase;
+    display: flex;
+    align-items: center;
+    justify-content: center; }
+    .top-sites-list .tile:hover {
+      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1), 0 0 0 5px rgba(0, 0, 0, 0.1);
+      transition: box-shadow 150ms; }
+  .top-sites-list .screenshot {
+    position: absolute;
+    top: 0;
+    left: 0;
+    height: 100%;
+    width: 100%;
+    background-color: #FFF;
+    border-radius: 6px;
+    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
+    background-size: 250%;
+    background-position: top left;
+    transition: opacity 1s;
+    opacity: 0; }
+    .top-sites-list .screenshot.active {
+      opacity: 1; }
+  .top-sites-list .title {
+    height: 30px;
+    line-height: 30px;
+    text-align: center;
+    white-space: nowrap;
+    font-size: 11px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    width: 96px; }
+
+.search-wrapper {
+  cursor: default;
+  display: flex;
+  position: relative;
+  margin: 0 0 48px;
+  width: 100%;
+  height: 36px; }
+  .search-wrapper .search-container {
+    z-index: 1001;
+    background: #FFF;
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 100%;
+    margin-top: -2px;
+    border: 1px solid #BFBFBF;
+    font-size: 12px;
+    box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
+    overflow: hidden; }
+    .search-wrapper .search-container .search-title {
+      color: #666;
+      padding: 5px 10px;
+      background-color: #F7F7F7;
+      display: flex;
+      align-items: center;
+      word-break: break-all; }
+      .search-wrapper .search-container .search-title p {
+        margin: 0; }
+      .search-wrapper .search-container .search-title #current-engine-icon {
+        margin-inline-end: 8px; }
+    .search-wrapper .search-container section {
+      border-bottom: 1px solid #EAEAEA;
+      margin-bottom: 0; }
+    .search-wrapper .search-container .search-suggestions ul {
+      padding: 0;
+      margin: 0;
+      list-style: none; }
+      .search-wrapper .search-container .search-suggestions ul li a {
+        cursor: default;
+        color: #000;
+        display: block;
+        padding: 4px 36px; }
+        .search-wrapper .search-container .search-suggestions ul li a:hover, .search-wrapper .search-container .search-suggestions ul li a.active {
+          background: #0996F8;
+          color: #FFF; }
+    .search-wrapper .search-container .history-search-suggestions {
+      border-bottom: 0; }
+      .search-wrapper .search-container .history-search-suggestions ul {
+        padding: 0;
+        margin: 0;
+        list-style: none; }
+        .search-wrapper .search-container .history-search-suggestions ul li a {
+          cursor: default;
+          color: #000;
+          display: block;
+          padding: 4px 10px; }
+          .search-wrapper .search-container .history-search-suggestions ul li a:hover, .search-wrapper .search-container .history-search-suggestions ul li a.active {
+            background: #0996F8;
+            color: #FFF; }
+          .search-wrapper .search-container .history-search-suggestions ul li a:hover > #historyIcon,
+          .search-wrapper .search-container .history-search-suggestions ul li a.active > #historyIcon {
+            background-image: url("assets/glyph-search-history.svg#search-history-active"); }
+    .search-wrapper .search-container .history-search-suggestions #historyIcon {
+      width: 16px;
+      height: 16px;
+      display: inline-block;
+      margin-inline-end: 10px;
+      margin-bottom: -3px;
+      background-image: url("assets/glyph-search-history.svg#search-history"); }
+    .search-wrapper .search-container .search-partners ul {
+      padding: 0;
+      margin: 0;
+      list-style: none; }
+      .search-wrapper .search-container .search-partners ul li {
+        display: inline-block;
+        padding: 5px 0; }
+        .search-wrapper .search-container .search-partners ul li a {
+          display: block;
+          padding: 3px 16px;
+          border-right: 1px solid #BFBFBF; }
+        .search-wrapper .search-container .search-partners ul li:hover, .search-wrapper .search-container .search-partners ul li.active {
+          background: #0996F8;
+          color: #FFF; }
+          .search-wrapper .search-container .search-partners ul li:hover a, .search-wrapper .search-container .search-partners ul li.active a {
+            border-color: transparent; }
+    .search-wrapper .search-container .search-settings button {
+      color: #666;
+      margin: 0;
+      padding: 0;
+      height: 32px;
+      text-align: center;
+      width: 100%;
+      border-style: solid none none;
+      border-radius: 0;
+      background: #F7F7F7;
+      border-top: 0; }
+      .search-wrapper .search-container .search-settings button:hover, .search-wrapper .search-container .search-settings button.active {
+        background: #EBEBEB;
+        box-shadow: none; }
+  .search-wrapper input {
+    border: 0;
+    box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
+    flex-grow: 1;
+    margin: 0;
+    outline: none;
+    padding: 0 12px 0 35px;
+    height: 100%;
+    border-top-left-radius: 4px;
+    border-bottom-left-radius: 4px;
+    padding-inline-start: 35px; }
+    .search-wrapper input:focus {
+      border-color: #0996F8;
+      box-shadow: 0 0 0 2px #0996F8;
+      transition: box-shadow 150ms;
+      z-index: 1; }
+    .search-wrapper input:focus + button {
+      z-index: 1;
+      transition: box-shadow 150ms;
+      box-shadow: 0 0 0 2px #0996F8;
+      background-color: #0996F8;
+      background-image: url("assets/glyph-forward-16-white.svg");
+      color: #FFF; }
+  .search-wrapper input:dir(rtl) {
+    border-radius: 0 4px 4px 0; }
+  .search-wrapper .search-label {
+    background: url("assets/glyph-search-16.svg") no-repeat center center/20px;
+    position: absolute;
+    top: 0;
+    offset-inline-start: 0;
+    height: 100%;
+    width: 35px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 2; }
+  .search-wrapper button {
+    border-radius: 0 3px 3px 0;
+    margin-inline-start: -1px;
+    border: 0;
+    width: 36px;
+    padding: 0;
+    transition: box-shadow 150ms;
+    box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
+    background: #FFF url("assets/glyph-forward-16.svg") no-repeat center center;
+    background-size: 16px 16px; }
+    .search-wrapper button:hover {
+      z-index: 1;
+      transition: box-shadow 150ms;
+      box-shadow: 0 1px 0 0 rgba(0, 0, 1, 0.5);
+      background-color: #0996F8;
+      background-image: url("assets/glyph-forward-16-white.svg");
+      color: #FFF; }
+  .search-wrapper button:dir(rtl) {
+    transform: scaleX(-1); }
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -1,31 +1,16 @@
 <!doctype html>
 <html lang="en-us" dir="ltr">
   <head>
     <meta charset="utf-8">
     <title>New Tab</title>
+    <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
-  <body>
-    <div id="root">
-      <h1>New Tab</h1>
-      <ul id="top-sites"></ul>
-    </div>
-    <script>
-      const topSitesEl = document.getElementById("top-sites");
-      window.addMessageListener("ActivityStream:MainToContent", msg => {
-        if (msg.data.type === "NEW_TAB_INITIAL_STATE") {
-          const fragment = document.createDocumentFragment()
-          for (const row of msg.data.data.TopSites.rows) {
-            const li = document.createElement("li");
-            const a = document.createElement("a");
-            a.href = row.url;
-            a.textContent = row.title;
-            li.appendChild(a);
-            fragment.appendChild(li);
-          }
-          topSitesEl.appendChild(fragment);
-        }
-      });
-
-    </script>
+  <body class="activity-stream">
+    <div id="root"></div>
+    <script src="resource://activity-stream/vendor/react.js"></script>
+    <script src="resource://activity-stream/vendor/react-dom.js"></script>
+    <script src="resource://activity-stream/vendor/redux.js"></script>
+    <script src="resource://activity-stream/vendor/react-redux.js"></script>
+    <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-forward-16-white.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <title>Forward - 16</title>
+  <g>
+    <polyline points="9 2 15 8 9 14" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <line x1="14" y1="8" x2="1" y2="8" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+  </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-forward-16.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <title>Forward - 16</title>
+  <g>
+    <polyline points="9 2 15 8 9 14" fill="none" stroke="#a0a0a0" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+    <line x1="14" y1="8" x2="1" y2="8" fill="none" stroke="#a0a0a0" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
+  </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-search-16.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #a0a0a0;
+        fill-rule: evenodd;
+      }
+    </style>
+  </defs>
+  <g id="glyph-search-16">
+    <path id="Icon_-_Search_-_16" data-name="Icon - Search - 16" class="cls-1" d="M226.989,348.571l-2.2,2.2-9.533-9.534a11.436,11.436,0,1,1,2.2-2.2ZM208.37,323.745a8.407,8.407,0,1,0,8.406,8.406A8.406,8.406,0,0,0,208.37,323.745Z" transform="translate(-196 -320)"/>
+  </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
+  <style>
+    use:not(:target) {
+      display: none;
+    }
+    use {
+      fill: graytext;
+    }
+    use[id$="-active"] {
+      fill: HighlightText;
+    }
+  </style>
+  <defs>
+    <path id="search-history-glyph" d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7 C15,4.1,11.9,1,8,1z M8,13.3c-2.9,0-5.3-2.4-5.3-5.3S5.1,2.7,8,2.7c2.9,0,5.3,2.4,5.3,5.3S10.9,13.3,8,13.3z M10.5,7H9V5 c0-0.6-0.4-1-1-1S7,4.4,7,5v3c0,0.6,0.4,1,1,1h2.5c0.6,0,1-0.4,1-1C11.5,7.4,11.1,7,10.5,7z"/>
+  </defs>
+  <use id="search-history" xlink:href="#search-history-glyph"/>
+  <use id="search-history-active" xlink:href="#search-history-glyph"/>
+</svg>
--- a/browser/extensions/activity-stream/jar.mn
+++ b/browser/extensions/activity-stream/jar.mn
@@ -2,9 +2,13 @@
 # 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/.
 
 [features/activity-stream@mozilla.org] chrome.jar:
 % resource activity-stream %content/
   content/lib/ (./lib/*)
   content/common/ (./common/*)
   content/vendor/Redux.jsm (./vendor/Redux.jsm)
+  content/vendor/react.js (./vendor/react.js)
+  content/vendor/react-dom.js (./vendor/react-dom.js)
+  content/vendor/redux.js (./vendor/redux.js)
+  content/vendor/react-redux.js (./vendor/react-redux.js)
   content/data/ (./data/*)
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -1,39 +1,62 @@
 /* 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/. */
+/* globals XPCOMUtils, NewTabInit, TopSitesFeed, SearchFeed */
+
 "use strict";
 
 const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
+const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 // Feeds
-const {NewTabInit} = Cu.import("resource://activity-stream/lib/NewTabInit.jsm", {});
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabInit",
+  "resource://activity-stream/lib/NewTabInit.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TopSitesFeed",
+  "resource://activity-stream/lib/TopSitesFeed.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SearchFeed",
+  "resource://activity-stream/lib/SearchFeed.jsm");
+
+const feeds = {
+  // When you add a feed here:
+  // 1. The key in this object should directly refer to a pref, not including the
+  //    prefix (so "feeds.newtabinit" refers to the
+  //    "browser.newtabpage.activity-stream.feeds.newtabinit" pref)
+  // 2. The value should be a function that returns a feed.
+  // 3. You should use XPCOMUtils.defineLazyModuleGetter to import the Feed,
+  //    so it isn't loaded until the feed is enabled.
+  "feeds.newtabinit": () => new NewTabInit(),
+  "feeds.topsites": () => new TopSitesFeed(),
+  "feeds.search": () => new SearchFeed()
+};
 
 this.ActivityStream = class ActivityStream {
 
   /**
    * constructor - Initializes an instance of ActivityStream
    *
    * @param  {object} options Options for the ActivityStream instance
    * @param  {string} options.id Add-on ID. e.g. "activity-stream@mozilla.org".
    * @param  {string} options.version Version of the add-on. e.g. "0.1.0"
    * @param  {string} options.newTabURL URL of New Tab page on which A.S. is displayed. e.g. "about:newtab"
    */
   constructor(options) {
     this.initialized = false;
     this.options = options;
     this.store = new Store();
+    this.feeds = feeds;
   }
   init() {
     this.initialized = true;
-    this.store.init([
-      new NewTabInit()
-    ]);
+    this.store.init(this.feeds);
+    this.store.dispatch({type: at.INIT});
   }
   uninit() {
+    this.store.dispatch({type: at.UNINIT});
     this.store.uninit();
     this.initialized = false;
   }
 };
 
 this.EXPORTED_SYMBOLS = ["ActivityStream"];
--- a/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
@@ -28,16 +28,17 @@ const DEFAULT_OPTIONS = {
     throw new Error(`\nMessageChannel: Received action ${action.type}, but no dispatcher was defined.\n`);
   },
   pageURL: ABOUT_NEW_TAB_URL,
   outgoingMessageName: "ActivityStream:MainToContent",
   incomingMessageName: "ActivityStream:ContentToMain"
 };
 
 this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
+
   /**
    * ActivityStreamMessageChannel - This module connects a Redux store to a RemotePageManager in Firefox.
    *                  Call .createChannel to start the connection, and .destroyChannel to destroy it.
    *                  You should use the BroadcastToContent, SendToContent, and SendToMain action creators
    *                  in common/Actions.jsm to help you create actions that will be automatically routed
    *                  to the correct location.
    *
    * @param  {object} options
@@ -178,20 +179,24 @@ this.ActivityStreamMessageChannel = clas
    * onMessage - Handles custom messages from content. It expects all messages to
    *             be formatted as Redux actions, and dispatches them to this.store
    *
    * @param  {obj} msg A custom message from content
    * @param  {obj} msg.action A Redux action (e.g. {type: "HELLO_WORLD"})
    * @param  {obj} msg.target A message target
    */
   onMessage(msg) {
-    const action = msg.data;
     const {portID} = msg.target;
-    if (!action || !action.type) {
+    if (!msg.data || !msg.data.type) {
       Cu.reportError(new Error(`Received an improperly formatted message from ${portID}`));
       return;
     }
-    this.onActionFromContent(action, msg.target.portID);
+    let action = {};
+    Object.assign(action, msg.data);
+    // target is used to access a browser reference that came from the content
+    // and should only be used in feeds (not reducers)
+    action._target = msg.target;
+    this.onActionFromContent(action, portID);
   }
 }
 
 this.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
 this.EXPORTED_SYMBOLS = ["ActivityStreamMessageChannel", "DEFAULT_OPTIONS"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/SearchFeed.jsm
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ /* globals ContentSearch, XPCOMUtils, Services */
+"use strict";
+
+const {utils: Cu} = Components;
+const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
+  "resource:///modules/ContentSearch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+
+this.SearchFeed = class SearchFeed {
+  addObservers() {
+    Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC);
+  }
+  removeObservers() {
+    Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
+  }
+  observe(subject, topic, data) {
+    switch (topic) {
+      case SEARCH_ENGINE_TOPIC:
+        if (data !== "engine-default") {
+          this.getState();
+        }
+        break;
+    }
+  }
+  async getState() {
+    const state = await ContentSearch.currentStateObj(true);
+    const engines = state.engines.map(engine => ({
+      name: engine.name,
+      icon: engine.iconBuffer
+    }));
+    const currentEngine = {
+      name: state.currentEngine.name,
+      icon: state.currentEngine.iconBuffer
+    };
+    const action = {type: at.SEARCH_STATE_UPDATED, data: {engines, currentEngine}};
+    this.store.dispatch(ac.BroadcastToContent(action));
+  }
+  performSearch(browser, data) {
+    ContentSearch.performSearch({target: browser}, data);
+  }
+  onAction(action) {
+    switch (action.type) {
+      case at.INIT:
+        this.addObservers();
+        this.getState();
+        break;
+      case at.PERFORM_SEARCH:
+        this.performSearch(action._target.browser, action.data);
+        break;
+      case at.UNINIT:
+        this.removeObservers();
+        break;
+    }
+  }
+};
+this.EXPORTED_SYMBOLS = ["SearchFeed"];
--- a/browser/extensions/activity-stream/lib/Store.jsm
+++ b/browser/extensions/activity-stream/lib/Store.jsm
@@ -1,20 +1,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* global Preferences */
 "use strict";
 
 const {utils: Cu} = Components;
 
 const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
-const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
 const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
 
+const PREF_PREFIX = "browser.newtabpage.activity-stream.";
+Cu.import("resource://gre/modules/Preferences.jsm");
+
 /**
  * Store - This has a similar structure to a redux store, but includes some extra
  *         functionality to allow for routing of actions between the Main processes
  *         and child processes via a ActivityStreamMessageChannel.
  *         It also accepts an array of "Feeds" on inititalization, which
  *         can listen for any action that is dispatched through the store.
  */
 this.Store = class Store {
@@ -27,17 +30,19 @@ this.Store = class Store {
     this._middleware = this._middleware.bind(this);
     // Bind each redux method so we can call it directly from the Store. E.g.,
     // store.dispatch() will call store._store.dispatch();
     ["dispatch", "getState", "subscribe"].forEach(method => {
       this[method] = function(...args) {
         return this._store[method](...args);
       }.bind(this);
     });
-    this.feeds = new Set();
+    this.feeds = new Map();
+    this._feedFactories = null;
+    this._prefHandlers = new Map();
     this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch});
     this._store = redux.createStore(
       redux.combineReducers(reducers),
       redux.applyMiddleware(this._middleware, this._messageChannel.middleware)
     );
   }
 
   /**
@@ -48,38 +53,98 @@ this.Store = class Store {
   _middleware(store) {
     return next => action => {
       next(action);
       this.feeds.forEach(s => s.onAction && s.onAction(action));
     };
   }
 
   /**
+   * initFeed - Initializes a feed by calling its constructor function
+   *
+   * @param  {string} feedName The name of a feed, as defined in the object
+   *                           passed to Store.init
+   */
+  initFeed(feedName) {
+    const feed = this._feedFactories[feedName]();
+    feed.store = this;
+    this.feeds.set(feedName, feed);
+  }
+
+  /**
+   * uninitFeed - Removes a feed and calls its uninit function if defined
+   *
+   * @param  {string} feedName The name of a feed, as defined in the object
+   *                           passed to Store.init
+   */
+  uninitFeed(feedName) {
+    const feed = this.feeds.get(feedName);
+    if (!feed) {
+      return;
+    }
+    if (feed.uninit) {
+      feed.uninit();
+    }
+    this.feeds.delete(feedName);
+  }
+
+  /**
+   * maybeStartFeedAndListenForPrefChanges - Listen for pref changes that turn a
+   *     feed off/on, and as long as that pref was not explicitly set to
+   *     false, initialize the feed immediately.
+   *
+   * @param  {string} name The name of a feed, as defined in the object passed
+   *                       to Store.init
+   */
+  maybeStartFeedAndListenForPrefChanges(name) {
+    const prefName = PREF_PREFIX + name;
+
+    // If the pref was never set, set it to true by default.
+    if (!Preferences.has(prefName)) {
+      Preferences.set(prefName, true);
+    }
+
+    // Create a listener that turns the feed off/on based on changes
+    // to the pref, and cache it so we can unlisten on shut-down.
+    const onPrefChanged = isEnabled => (isEnabled ? this.initFeed(name) : this.uninitFeed(name));
+    this._prefHandlers.set(prefName, onPrefChanged);
+    Preferences.observe(prefName, onPrefChanged);
+
+    // TODO: This should propbably be done in a generic pref manager for Activity Stream.
+    // If the pref is true, start the feed immediately.
+    if (Preferences.get(prefName)) {
+      this.initFeed(name);
+    }
+  }
+
+  /**
    * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
-   *        After initialization has finished, an INIT action is dispatched.
    *
    * @param  {array} feeds An array of objects with an optional .onAction method
    */
-  init(feeds) {
-    if (feeds) {
-      feeds.forEach(subscriber => {
-        subscriber.store = this;
-        this.feeds.add(subscriber);
-      });
+  init(feedConstructors) {
+    if (feedConstructors) {
+      this._feedFactories = feedConstructors;
+      for (const name of Object.keys(feedConstructors)) {
+        this.maybeStartFeedAndListenForPrefChanges(name);
+      }
     }
     this._messageChannel.createChannel();
-    this.dispatch({type: at.INIT});
   }
 
   /**
-   * uninit - Clears all feeds, dispatches an UNINIT action, and
-   *          destroys the message manager channel.
+   * uninit -  Uninitalizes each feed, clears them, and destroys the message
+   *           manager channel.
    *
    * @return {type}  description
    */
   uninit() {
+    this.feeds.forEach(feed => this.uninitFeed(feed));
+    this._prefHandlers.forEach((handler, pref) => Preferences.ignore(pref, handler));
+    this._prefHandlers.clear();
+    this._feedFactories = null;
     this.feeds.clear();
-    this.dispatch({type: at.UNINIT});
     this._messageChannel.destroyChannel();
   }
 };
 
-this.EXPORTED_SYMBOLS = ["Store"];
+this.PREF_PREFIX = PREF_PREFIX;
+this.EXPORTED_SYMBOLS = ["Store", "PREF_PREFIX"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -0,0 +1,83 @@
+/* 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/. */
+ /* globals PlacesProvider, PreviewProvider */
+"use strict";
+
+const {utils: Cu} = Components;
+const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
+
+Cu.import("resource:///modules/PlacesProvider.jsm");
+Cu.import("resource:///modules/PreviewProvider.jsm");
+
+const TOP_SITES_SHOWMORE_LENGTH = 12;
+const UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
+const DEFAULT_TOP_SITES = [
+  {"url": "https://www.facebook.com/"},
+  {"url": "https://www.youtube.com/"},
+  {"url": "http://www.amazon.com/"},
+  {"url": "https://www.yahoo.com/"},
+  {"url": "http://www.ebay.com"},
+  {"url": "https://twitter.com/"}
+].map(row => Object.assign(row, {isDefault: true}));
+
+this.TopSitesFeed = class TopSitesFeed {
+  constructor() {
+    this.lastUpdated = 0;
+  }
+  async getScreenshot(url) {
+    let screenshot = await PreviewProvider.getThumbnail(url);
+    const action = {type: at.SCREENSHOT_UPDATED, data: {url, screenshot}};
+    this.store.dispatch(ac.BroadcastToContent(action));
+  }
+  async getLinksWithDefaults(action) {
+    let links = await PlacesProvider.links.getLinks();
+
+    if (!links) {
+      links = [];
+    } else {
+      links = links.filter(link => link && link.type !== "affiliate").slice(0, 12);
+    }
+
+    if (links.length < TOP_SITES_SHOWMORE_LENGTH) {
+      links = [...links, ...DEFAULT_TOP_SITES].slice(0, TOP_SITES_SHOWMORE_LENGTH);
+    }
+
+    return links;
+  }
+  async refresh(action) {
+    const links = await this.getLinksWithDefaults();
+    const newAction = {type: at.TOP_SITES_UPDATED, data: links};
+
+    // Send an update to content so the preloaded tab can get the updated content
+    this.store.dispatch(ac.SendToContent(newAction, action.meta.fromTarget));
+    this.lastUpdated = Date.now();
+
+    // Now, get a screenshot for every item
+    for (let link of links) {
+      this.getScreenshot(link.url);
+    }
+  }
+  onAction(action) {
+    let realRows;
+    switch (action.type) {
+      case at.NEW_TAB_LOAD:
+        // Only check against real rows returned from history, not default ones.
+        realRows = this.store.getState().TopSites.rows.filter(row => !row.isDefault);
+        // When a new tab is opened, if we don't have enough top sites yet, refresh the data.
+        if (realRows.length < TOP_SITES_SHOWMORE_LENGTH) {
+          this.refresh(action);
+        } else if (Date.now() - this.lastUpdated >= UPDATE_TIME) {
+          // When a new tab is opened, if the last time we refreshed the data
+          // is greater than 15 minutes, refresh the data.
+          this.refresh(action);
+        }
+        break;
+    }
+  }
+};
+
+this.UPDATE_TIME = UPDATE_TIME;
+this.TOP_SITES_SHOWMORE_LENGTH = TOP_SITES_SHOWMORE_LENGTH;
+this.DEFAULT_TOP_SITES = DEFAULT_TOP_SITES;
+this.EXPORTED_SYMBOLS = ["TopSitesFeed", "UPDATE_TIME", "DEFAULT_TOP_SITES", "TOP_SITES_SHOWMORE_LENGTH"];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/.eslintrc.js
@@ -0,0 +1,11 @@
+module.exports = {
+  "env": {
+    "node": true,
+    "es6": true,
+    "mocha": true
+  },
+  "globals": {
+    "assert": true,
+    "sinon": true
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
@@ -0,0 +1,42 @@
+module.exports = {
+  "globals": {
+    "add_task": false,
+    "Assert": false,
+    "BrowserOpenTab": false,
+    "BrowserTestUtils": false,
+    "content": false,
+    "ContentTask": false,
+    "ContentTaskUtils": false,
+    "Components": false,
+    "EventUtils": false,
+    "executeSoon": false,
+    "expectUncaughtException": false,
+    "export_assertions": false,
+    "extractJarToTmp": false,
+    "finish": false,
+    "getJar": false,
+    "getRootDirectory": false,
+    "getTestFilePath": false,
+    "gBrowser": false,
+    "gTestPath": false,
+    "info": false,
+    "is": false,
+    "isnot": false,
+    "ok": false,
+    "OpenBrowserWindow": false,
+    "Preferences": false,
+    "registerCleanupFunction": false,
+    "requestLongerTimeout": false,
+    "Services": false,
+    "SimpleTest": false,
+    "SpecialPowers": false,
+    "TestUtils": false,
+    "thisTestLeaksUncaughtRejectionsAndShouldBeFixed": false,
+    "todo": false,
+    "todo_is": false,
+    "todo_isnot": false,
+    "waitForClipboard": false,
+    "waitForExplicitFinish": false,
+    "waitForFocus": false
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+# XXX This defaults to forcing activity-stream tests to be skipped in m-c,
+# since, as of this writing, mozilla-central itself is still turned off.
+# The tests can be run locally using 'npm run mochitest' which does various
+# overrides.
+skip-if=!activity_stream
+
+[browser_dummy_test.js]
+skip-if=true
+# XXX The above test is required because having only one test causes
+# The default skip-if to silently fail.  As soon as we add another test here, 
+# we should get rid of it, and the following line.
+[browser_as_load_location.js]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser_as_load_location.js
@@ -0,0 +1,34 @@
+"use strict";
+
+let Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Tests that opening a new tab opens a page with the expected activity stream
+ * content.
+ *
+ * XXX /browser/components/newtab/tests/browser/browser_newtab_overrides in
+ * mozilla-central is where this test was adapted from.  Once we get decide on
+ * and implement how we're going to set the URL in mozilla-central, we may well
+ * want to (separately from this test), clone/adapt that entire file for our
+ * new setup.
+ */
+add_task(async function checkActivityStreamLoads() {
+  const asURL = "resource://activity-stream/data/content/activity-stream.html";
+
+  // simulate a newtab open as a user would
+  BrowserOpenTab();
+
+  // wait until the browser loads
+  let browser = gBrowser.selectedBrowser;
+  await BrowserTestUtils.browserLoaded(browser);
+
+  // check what the content task thinks has been loaded.
+  await ContentTask.spawn(browser, {url: asURL}, args => {
+    Assert.ok(content.document.querySelector("body.activity-stream"),
+      'Got <body class="activity-stream" Element');
+  });
+
+  // avoid leakage
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/functional/mochitest/browser_dummy_test.js
@@ -0,0 +1,34 @@
+"use strict";
+
+let Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Tests that opening a new tab opens a page with the expected activity stream
+ * content.
+ *
+ * XXX /browser/components/newtab/tests/browser/browser_newtab_overrides in
+ * mozilla-central is where this test was adapted from.  Once we get decide on
+ * and implement how we're going to set the URL in mozilla-central, we may well
+ * want to (separately from this test), clone/adapt that entire file for our
+ * new setup.
+ */
+add_task(async function checkActivityStreamLoads() {
+  const asURL = "resource://activity-stream/data/content/activity-stream.html";
+
+  // simulate a newtab open as a user would
+  BrowserOpenTab();
+
+  // wait until the browser loads
+  let browser = gBrowser.selectedBrowser;
+  await BrowserTestUtils.browserLoaded(browser);
+
+  // check what the content task thinks has been loaded.
+  await ContentTask.spawn(browser, {url: asURL}, args => {
+    Assert.ok(content.document.querySelector("body.activity-stream"),
+      'Got <body class="activity-stream" Element');
+  });
+
+  // avoid leakage
+  await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/mozinfo.json
@@ -0,0 +1,3 @@
+{
+  "activity_stream": true
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/common/Actions.test.js
@@ -0,0 +1,93 @@
+const {
+  actionCreators: ac,
+  actionUtils: au,
+  MAIN_MESSAGE_TYPE,
+  CONTENT_MESSAGE_TYPE
+} = require("common/Actions.jsm");
+
+describe("ActionCreators", () => {
+  describe("_RouteMessage", () => {
+    it("should throw if options are not passed as the second param", () => {
+      assert.throws(() => {
+        au._RouteMessage({type: "FOO"});
+      });
+    });
+    it("should set all defined options on the .meta property of the new action", () => {
+      assert.deepEqual(
+        au._RouteMessage({type: "FOO", meta: {hello: "world"}}, {from: "foo", to: "bar"}),
+        {type: "FOO", meta: {hello: "world", from: "foo", to: "bar"}}
+      );
+    });
+    it("should remove any undefined options related to message routing", () => {
+      const action = au._RouteMessage({type: "FOO", meta: {fromTarget: "bar"}}, {from: "foo", to: "bar"});
+      assert.isUndefined(action.meta.fromTarget);
+    });
+  });
+  describe("SendToMain", () => {
+    it("should create the right action", () => {
+      const action = {type: "FOO", data: "BAR"};
+      const newAction = ac.SendToMain(action);
+      assert.deepEqual(newAction, {
+        type: "FOO",
+        data: "BAR",
+        meta: {from: CONTENT_MESSAGE_TYPE, to: MAIN_MESSAGE_TYPE}
+      });
+    });
+    describe("isSendToMain", () => {
+      it("should return true if action is SendToMain", () => {
+        const newAction = ac.SendToMain({type: "FOO"});
+        assert.isTrue(au.isSendToMain(newAction));
+      });
+      it("should return false if action is not SendToMain", () => {
+        assert.isFalse(au.isSendToMain({type: "FOO"}));
+      });
+    });
+  });
+  describe("SendToContent", () => {
+    it("should create the right action", () => {
+      const action = {type: "FOO", data: "BAR"};
+      const targetId = "abc123";
+      const newAction = ac.SendToContent(action, targetId);
+      assert.deepEqual(newAction, {
+        type: "FOO",
+        data: "BAR",
+        meta: {from: MAIN_MESSAGE_TYPE, to: CONTENT_MESSAGE_TYPE, toTarget: targetId}
+      });
+    });
+    it("should throw if no targetId is provided", () => {
+      assert.throws(() => {
+        ac.SendToContent({type: "FOO"});
+      });
+    });
+    describe("isSendToContent", () => {
+      it("should return true if action is SendToContent", () => {
+        const newAction = ac.SendToContent({type: "FOO"}, "foo123");
+        assert.isTrue(au.isSendToContent(newAction));
+      });
+      it("should return false if action is not SendToMain", () => {
+        assert.isFalse(au.isSendToContent({type: "FOO"}));
+        assert.isFalse(au.isSendToContent(ac.BroadcastToContent({type: "FOO"})));
+      });
+    });
+  });
+  describe("BroadcastToContent", () => {
+    it("should create the right action", () => {
+      const action = {type: "FOO", data: "BAR"};
+      const newAction = ac.BroadcastToContent(action);
+      assert.deepEqual(newAction, {
+        type: "FOO",
+        data: "BAR",
+        meta: {from: MAIN_MESSAGE_TYPE, to: CONTENT_MESSAGE_TYPE}
+      });
+    });
+    describe("isBroadcastToContent", () => {
+      it("should return true if action is BroadcastToContent", () => {
+        assert.isTrue(au.isBroadcastToContent(ac.BroadcastToContent({type: "FOO"})));
+      });
+      it("should return false if action is not BroadcastToContent", () => {
+        assert.isFalse(au.isBroadcastToContent({type: "FOO"}));
+        assert.isFalse(au.isBroadcastToContent(ac.SendToContent({type: "FOO"}, "foo123")));
+      });
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -0,0 +1,51 @@
+const {reducers, INITIAL_STATE} = require("common/Reducers.jsm");
+const {TopSites, Search} = reducers;
+const {actionTypes: at} = require("common/Actions.jsm");
+
+describe("Reducers", () => {
+  describe("TopSites", () => {
+    it("should return the initial state", () => {
+      const nextState = TopSites(undefined, {type: "FOO"});
+      assert.equal(nextState, INITIAL_STATE.TopSites);
+    });
+    it("should add top sites on TOP_SITES_UPDATED", () => {
+      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 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"}]};
+      const action = {type: at.SCREENSHOT_UPDATED, data: {url: "baz.com", screenshot: "data:123"}};
+      const nextState = TopSites(oldState, action);
+      assert.deepEqual(nextState, oldState);
+    });
+  });
+  describe("Search", () => {
+    it("should return the initial state", () => {
+      const nextState = Search(undefined, {type: "FOO"});
+      assert.equal(nextState, INITIAL_STATE.Search);
+    });
+    it("should not update state for empty action.data on Search", () => {
+      const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED});
+      assert.equal(nextState, INITIAL_STATE.Search);
+    });
+    it("should update the current engine and the engines on SEARCH_STATE_UPDATED", () => {
+      const newEngine = {name: "Google", iconBuffer: "icon.ico"};
+      const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED, data: {currentEngine: newEngine, engines: [newEngine]}});
+      assert.equal(nextState.currentEngine.name, newEngine.name);
+      assert.equal(nextState.currentEngine.icon, newEngine.icon);
+      assert.equal(nextState.engines[0].name, newEngine.name);
+      assert.equal(nextState.engines[0].icon, newEngine.icon);
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
@@ -0,0 +1,70 @@
+const injector = require("inject!lib/ActivityStream.jsm");
+
+describe("ActivityStream", () => {
+  let sandbox;
+  let as;
+  let ActivityStream;
+  function NewTabInit() {}
+  function TopSitesFeed() {}
+  function SearchFeed() {}
+  before(() => {
+    sandbox = sinon.sandbox.create();
+    ({ActivityStream} = injector({
+      "lib/NewTabInit.jsm": {NewTabInit},
+      "lib/TopSitesFeed.jsm": {TopSitesFeed},
+      "lib/SearchFeed.jsm": {SearchFeed}
+    }));
+  });
+
+  afterEach(() => sandbox.restore());
+
+  beforeEach(() => {
+    as = new ActivityStream();
+    sandbox.stub(as.store, "init");
+    sandbox.stub(as.store, "uninit");
+  });
+
+  it("should exist", () => {
+    assert.ok(ActivityStream);
+  });
+  it("should initialize with .initialized=false", () => {
+    assert.isFalse(as.initialized, ".initialized");
+  });
+  describe("#init", () => {
+    beforeEach(() => {
+      as.init();
+    });
+    it("should set .initialized to true", () => {
+      assert.isTrue(as.initialized, ".initialized");
+    });
+    it("should call .store.init", () => {
+      assert.calledOnce(as.store.init);
+    });
+  });
+  describe("#uninit", () => {
+    beforeEach(() => {
+      as.init();
+      as.uninit();
+    });
+    it("should set .initialized to false", () => {
+      assert.isFalse(as.initialized, ".initialized");
+    });
+    it("should call .store.uninit", () => {
+      assert.calledOnce(as.store.uninit);
+    });
+  });
+  describe("feeds", () => {
+    it("should create a NewTabInit feed", () => {
+      const feed = as.feeds["feeds.newtabinit"]();
+      assert.instanceOf(feed, NewTabInit);
+    });
+    it("should create a TopSites feed", () => {
+      const feed = as.feeds["feeds.topsites"]();
+      assert.instanceOf(feed, TopSitesFeed);
+    });
+    it("should create a Search feed", () => {
+      const feed = as.feeds["feeds.search"]();
+      assert.instanceOf(feed, SearchFeed);
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
@@ -0,0 +1,235 @@
+const {ActivityStreamMessageChannel, DEFAULT_OPTIONS} = require("lib/ActivityStreamMessageChannel.jsm");
+const {addNumberReducer, GlobalOverrider} = require("test/unit/utils");
+const {createStore, applyMiddleware} = require("redux");
+const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
+
+const OPTIONS = ["pageURL, outgoingMessageName", "incomingMessageName", "dispatch"];
+
+describe("ActivityStreamMessageChannel", () => {
+  let globals;
+  let dispatch;
+  let mm;
+  before(() => {
+    function RP(url) {
+      this.url = url;
+      this.messagePorts = [];
+      this.addMessageListener = globals.sandbox.spy();
+      this.sendAsyncMessage = globals.sandbox.spy();
+      this.destroy = globals.sandbox.spy();
+    }
+    globals = new GlobalOverrider();
+    globals.set("AboutNewTab", {
+      override: globals.sandbox.spy(),
+      reset: globals.sandbox.spy()
+    });
+    globals.set("RemotePages", RP);
+    dispatch = globals.sandbox.spy();
+  });
+  beforeEach(() => {
+    mm = new ActivityStreamMessageChannel({dispatch});
+  });
+
+  afterEach(() => globals.reset());
+  after(() => globals.restore());
+
+  it("should exist", () => {
+    assert.ok(ActivityStreamMessageChannel);
+  });
+  it("should apply default options", () => {
+    mm = new ActivityStreamMessageChannel();
+    OPTIONS.forEach(o => assert.equal(mm[o], DEFAULT_OPTIONS[o], o));
+  });
+  it("should add options", () => {
+    const options = {dispatch: () => {}, pageURL: "FOO.html", outgoingMessageName: "OUT", incomingMessageName: "IN"};
+    mm = new ActivityStreamMessageChannel(options);
+    OPTIONS.forEach(o => assert.equal(mm[o], options[o], o));
+  });
+  it("should throw an error if no dispatcher was provided", () => {
+    mm = new ActivityStreamMessageChannel();
+    assert.throws(() => mm.dispatch({type: "FOO"}));
+  });
+  describe("Creating/destroying the channel", () => {
+    describe("#createChannel", () => {
+      it("should create .channel with the correct URL", () => {
+        mm.createChannel();
+        assert.ok(mm.channel);
+        assert.equal(mm.channel.url, mm.pageURL);
+      });
+      it("should add 3 message listeners", () => {
+        mm.createChannel();
+        assert.callCount(mm.channel.addMessageListener, 3);
+      });
+      it("should add the custom message listener to the channel", () => {
+        mm.createChannel();
+        assert.calledWith(mm.channel.addMessageListener, mm.incomingMessageName, mm.onMessage);
+      });
+      it("should override AboutNewTab", () => {
+        mm.createChannel();
+        assert.calledOnce(global.AboutNewTab.override);
+      });
+      it("should not override AboutNewTab if the pageURL is not about:newtab", () => {
+        mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
+        mm.createChannel();
+        assert.notCalled(global.AboutNewTab.override);
+      });
+    });
+    describe("#destroyChannel", () => {
+      let channel;
+      beforeEach(() => {
+        mm.createChannel();
+        channel = mm.channel;
+      });
+      it("should call channel.destroy()", () => {
+        mm.destroyChannel();
+        assert.calledOnce(channel.destroy);
+      });
+      it("should set .channel to null", () => {
+        mm.destroyChannel();
+        assert.isNull(mm.channel);
+      });
+      it("should reset AboutNewTab", () => {
+        mm.destroyChannel();
+        assert.calledOnce(global.AboutNewTab.reset);
+      });
+      it("should not reset AboutNewTab if the pageURL is not about:newtab", () => {
+        mm = new ActivityStreamMessageChannel({pageURL: "foo.html"});
+        mm.createChannel();
+        mm.destroyChannel();
+        assert.notCalled(global.AboutNewTab.reset);
+      });
+    });
+  });
+  describe("Message handling", () => {
+    describe("#getTargetById", () => {
+      it("should get an id if it exists", () => {
+        const t = {portID: "foo"};
+        mm.createChannel();
+        mm.channel.messagePorts.push(t);
+        assert.equal(mm.getTargetById("foo"), t);
+      });
+      it("should return null if the target doesn't exist", () => {
+        const t = {portID: "foo"};
+        mm.createChannel();
+        mm.channel.messagePorts.push(t);
+        assert.equal(mm.getTargetById("bar"), null);
+      });
+    });
+    describe("#onNewTabLoad", () => {
+      it("should dispatch a NEW_TAB_LOAD action", () => {
+        const t = {portID: "foo"};
+        sinon.stub(mm, "onActionFromContent");
+        mm.onNewTabLoad({target: t});
+        assert.calledWith(mm.onActionFromContent, {type: at.NEW_TAB_LOAD}, "foo");
+      });
+    });
+    describe("#onNewTabUnload", () => {
+      it("should dispatch a NEW_TAB_UNLOAD action", () => {
+        const t = {portID: "foo"};
+        sinon.stub(mm, "onActionFromContent");
+        mm.onNewTabUnload({target: t});
+        assert.calledWith(mm.onActionFromContent, {type: at.NEW_TAB_UNLOAD}, "foo");
+      });
+    });
+    describe("#onMessage", () => {
+      it("should report an error if the msg.data is missing", () => {
+        mm.onMessage({target: {portID: "foo"}});
+        assert.calledOnce(global.Components.utils.reportError);
+      });
+      it("should report an error if the msg.data.type is missing", () => {
+        mm.onMessage({target: {portID: "foo"}, data: "foo"});
+        assert.calledOnce(global.Components.utils.reportError);
+      });
+      it("should call onActionFromContent", () => {
+        sinon.stub(mm, "onActionFromContent");
+        const action = {data: {data: {}, type: "FOO"}, target: {portID: "foo"}};
+        const expectedAction = {
+          type: action.data.type,
+          data: action.data.data,
+          _target: {portID: "foo"}
+        };
+        mm.onMessage(action);
+        assert.calledWith(mm.onActionFromContent, expectedAction, "foo");
+      });
+    });
+  });
+  describe("Sending and broadcasting", () => {
+    describe("#send", () => {
+      it("should send a message on the right port", () => {
+        const t = {portID: "foo", sendAsyncMessage: sinon.spy()};
+        mm.createChannel();
+        mm.channel.messagePorts = [t];
+        const action = ac.SendToContent({type: "HELLO"}, "foo");
+        mm.send(action, "foo");
+        assert.calledWith(t.sendAsyncMessage, DEFAULT_OPTIONS.outgoingMessageName, action);
+      });
+      it("should not throw if the target isn't around", () => {
+        mm.createChannel();
+        // port is not added to the channel
+        const action = ac.SendToContent({type: "HELLO"}, "foo");
+
+        assert.doesNotThrow(() => mm.send(action, "foo"));
+      });
+    });
+    describe("#broadcast", () => {
+      it("should send a message on the channel", () => {
+        mm.createChannel();
+        const action = ac.BroadcastToContent({type: "HELLO"});
+        mm.broadcast(action);
+        assert.calledWith(mm.channel.sendAsyncMessage, DEFAULT_OPTIONS.outgoingMessageName, action);
+      });
+    });
+  });
+  describe("Handling actions", () => {
+    describe("#onActionFromContent", () => {
+      beforeEach(() => mm.onActionFromContent({type: "FOO"}, "foo"));
+      it("should dispatch a SendToMain action", () => {
+        assert.calledOnce(dispatch);
+        const action = dispatch.firstCall.args[0];
+        assert.equal(action.type, "FOO", "action.type");
+      });
+      it("should have the right fromTarget", () => {
+        const action = dispatch.firstCall.args[0];
+        assert.equal(action.meta.fromTarget, "foo", "meta.fromTarget");
+      });
+    });
+    describe("#middleware", () => {
+      let store;
+      beforeEach(() => {
+        store = createStore(addNumberReducer, applyMiddleware(mm.middleware));
+      });
+      it("should just call next if no channel is found", () => {
+        store.dispatch({type: "ADD", data: 10});
+        assert.equal(store.getState(), 10);
+      });
+      it("should call .send if the action is SendToContent", () => {
+        sinon.stub(mm, "send");
+        const action = ac.SendToContent({type: "FOO"}, "foo");
+
+        mm.createChannel();
+        store.dispatch(action);
+
+        assert.calledWith(mm.send, action);
+      });
+      it("should call .broadcast if the action is BroadcastToContent", () => {
+        sinon.stub(mm, "broadcast");
+        const action = ac.BroadcastToContent({type: "FOO"});
+
+        mm.createChannel();
+        store.dispatch(action);
+
+        assert.calledWith(mm.broadcast, action);
+      });
+      it("should dispatch other actions normally", () => {
+        sinon.stub(mm, "send");
+        sinon.stub(mm, "broadcast");
+
+        mm.createChannel();
+        store.dispatch({type: "ADD", data: 1});
+
+        assert.equal(store.getState(), 1);
+        assert.notCalled(mm.send);
+        assert.notCalled(mm.broadcast);
+      });
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js
@@ -0,0 +1,77 @@
+"use strict";
+const {SearchFeed} = require("lib/SearchFeed.jsm");
+const {GlobalOverrider} = require("test/unit/utils");
+const {actionTypes: at} = require("common/Actions.jsm");
+const fakeEngines = [{name: "Google", iconBuffer: "icon.ico"}];
+describe("Search Feed", () => {
+  let feed;
+  let globals;
+  before(() => {
+    globals = new GlobalOverrider();
+    globals.set("ContentSearch", {
+      currentStateObj: globals.sandbox.spy(() => Promise.resolve({engines: fakeEngines, currentEngine: {}})),
+      performSearch: globals.sandbox.spy((browser, searchData) => Promise.resolve({browser, searchData}))
+    });
+  });
+  beforeEach(() => {
+    feed = new SearchFeed();
+    feed.store = {dispatch: sinon.spy()};
+  });
+  afterEach(() => globals.reset());
+  after(() => globals.restore());
+
+  it("should call get state (with true) from the content search provider on INIT", () => {
+    feed.onAction({type: at.INIT});
+    // calling currentStateObj with 'true' allows us to return a data uri for the
+    // icon, instead of an array buffer
+    assert.calledWith(global.ContentSearch.currentStateObj, true);
+  });
+  it("should get the the state on INIT", () => {
+    sinon.stub(feed, "getState");
+    feed.onAction({type: at.INIT});
+    assert.calledOnce(feed.getState);
+  });
+  it("should add observers on INIT", () => {
+    sinon.stub(feed, "addObservers");
+    feed.onAction({type: at.INIT});
+    assert.calledOnce(feed.addObservers);
+  });
+  it("should remove observers on UNINIT", () => {
+    sinon.stub(feed, "removeObservers");
+    feed.onAction({type: at.UNINIT});
+    assert.calledOnce(feed.removeObservers);
+  });
+  it("should call services.obs.addObserver on INIT", () => {
+    feed.onAction({type: at.INIT});
+    assert.calledOnce(global.Services.obs.addObserver);
+  });
+  it("should call services.obs.removeObserver on UNINIT", () => {
+    feed.onAction({type: at.UNINIT});
+    assert.calledOnce(global.Services.obs.removeObserver);
+  });
+  it("should dispatch one event with the state", () => (
+    feed.getState().then(() => {
+      assert.calledOnce(feed.store.dispatch);
+    })
+  ));
+  it("should perform a search on PERFORM_SEARCH", () => {
+    sinon.stub(feed, "performSearch");
+    feed.onAction({_target: {browser: {}}, type: at.PERFORM_SEARCH});
+    assert.calledOnce(feed.performSearch);
+  });
+  it("should call performSearch with an action", () => {
+    const action = {_target: {browser: "browser"}, data: {searchString: "hello"}};
+    feed.performSearch(action._target.browser, action.data);
+    assert.calledWith(global.ContentSearch.performSearch, {target: action._target.browser}, action.data);
+  });
+  it("should get the state if we change the search engines", () => {
+    sinon.stub(feed, "getState");
+    feed.observe(null, "browser-search-engine-modified", "engine-current");
+    assert.calledOnce(feed.getState);
+  });
+  it("shouldn't get the state if it's not the right notification", () => {
+    sinon.stub(feed, "getState");
+    feed.observe(null, "some-other-notification", "engine-current");
+    assert.notCalled(feed.getState);
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/Store.test.js
@@ -0,0 +1,210 @@
+const injector = require("inject!lib/Store.jsm");
+const {createStore} = require("redux");
+const {addNumberReducer} = require("test/unit/utils");
+const {GlobalOverrider} = require("test/unit/utils");
+describe("Store", () => {
+  let Store;
+  let Preferences;
+  let sandbox;
+  let store;
+  let globals;
+  let PREF_PREFIX;
+  beforeEach(() => {
+    globals = new GlobalOverrider();
+    sandbox = globals.sandbox;
+    Preferences = new Map();
+    Preferences.observe = sandbox.spy();
+    Preferences.ignore = sandbox.spy();
+    globals.set("Preferences", Preferences);
+    function ActivityStreamMessageChannel(options) {
+      this.dispatch = options.dispatch;
+      this.createChannel = sandbox.spy();
+      this.destroyChannel = sandbox.spy();
+      this.middleware = sandbox.spy(s => next => action => next(action));
+    }
+    ({Store, PREF_PREFIX} = injector({"lib/ActivityStreamMessageChannel.jsm": {ActivityStreamMessageChannel}}));
+    store = new Store();
+  });
+  afterEach(() => {
+    Preferences.clear();
+    globals.restore();
+  });
+  it("should have an .feeds property that is a Map", () => {
+    assert.instanceOf(store.feeds, Map);
+    assert.equal(store.feeds.size, 0, ".feeds.size");
+  });
+  it("should have a redux store at ._store", () => {
+    assert.ok(store._store);
+    assert.property(store, "dispatch");
+    assert.property(store, "getState");
+  });
+  it("should create a ActivityStreamMessageChannel with the right dispatcher", () => {
+    assert.ok(store._messageChannel);
+    assert.equal(store._messageChannel.dispatch, store.dispatch);
+  });
+  it("should connect the ActivityStreamMessageChannel's middleware", () => {
+    store.dispatch({type: "FOO"});
+    assert.calledOnce(store._messageChannel.middleware);
+  });
+  describe("#initFeed", () => {
+    it("should add an instance of the feed to .feeds", () => {
+      class Foo {}
+      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store.init({foo: () => new Foo()});
+      store.initFeed("foo");
+
+      assert.isTrue(store.feeds.has("foo"), "foo is set");
+      assert.instanceOf(store.feeds.get("foo"), Foo);
+    });
+    it("should add a .store property to the feed", () => {
+      class Foo {}
+      store._feedFactories = {foo: () => new Foo()};
+      store.initFeed("foo");
+
+      assert.propertyVal(store.feeds.get("foo"), "store", store);
+    });
+  });
+  describe("#uninitFeed", () => {
+    it("should not throw if no feed with that name exists", () => {
+      assert.doesNotThrow(() => {
+        store.uninitFeed("bar");
+      });
+    });
+    it("should call the feed's uninit function if it is defined", () => {
+      let feed;
+      function createFeed() {
+        feed = {uninit: sinon.spy()};
+        return feed;
+      }
+      store._feedFactories = {foo: createFeed};
+
+      store.initFeed("foo");
+      store.uninitFeed("foo");
+
+      assert.calledOnce(feed.uninit);
+    });
+    it("should remove the feed from .feeds", () => {
+      class Foo {}
+      store._feedFactories = {foo: () => new Foo()};
+
+      store.initFeed("foo");
+      store.uninitFeed("foo");
+
+      assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds");
+    });
+  });
+  describe("maybeStartFeedAndListenForPrefChanges", () => {
+    beforeEach(() => {
+      sinon.stub(store, "initFeed");
+      sinon.stub(store, "uninitFeed");
+    });
+    it("should set the new pref in Preferences to true, if it was never defined", () => {
+      store.maybeStartFeedAndListenForPrefChanges("foo");
+      assert.isTrue(Preferences.get(`${PREF_PREFIX}foo`));
+    });
+    it("should not override the pref if it was already set", () => {
+      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store.maybeStartFeedAndListenForPrefChanges("foo");
+      assert.isFalse(Preferences.get(`${PREF_PREFIX}foo`));
+    });
+    it("should initialize the feed if the Pref is set to true", () => {
+      Preferences.set(`${PREF_PREFIX}foo`, true);
+      store.maybeStartFeedAndListenForPrefChanges("foo");
+      assert.calledWith(store.initFeed, "foo");
+    });
+    it("should not initialize the feed if the Pref is set to false", () => {
+      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store.maybeStartFeedAndListenForPrefChanges("foo");
+      assert.notCalled(store.initFeed);
+    });
+    it("should observe the pref", () => {
+      store.maybeStartFeedAndListenForPrefChanges("foo");
+      assert.calledWith(Preferences.observe, `${PREF_PREFIX}foo`, store._prefHandlers.get(`${PREF_PREFIX}foo`));
+    });
+    describe("handler", () => {
+      let handler;
+      beforeEach(() => {
+        store.maybeStartFeedAndListenForPrefChanges("foo");
+        handler = store._prefHandlers.get(`${PREF_PREFIX}foo`);
+      });
+      it("should initialize the feed if called with true", () => {
+        handler(true);
+        assert.calledWith(store.initFeed, "foo");
+      });
+      it("should uninitialize the feed if called with false", () => {
+        handler(false);
+        assert.calledWith(store.uninitFeed, "foo");
+      });
+    });
+  });
+  describe("#init", () => {
+    it("should call .maybeStartFeedAndListenForPrefChanges with each key", () => {
+      sinon.stub(store, "maybeStartFeedAndListenForPrefChanges");
+      store.init({foo: () => {}, bar: () => {}});
+      assert.calledWith(store.maybeStartFeedAndListenForPrefChanges, "foo");
+      assert.calledWith(store.maybeStartFeedAndListenForPrefChanges, "bar");
+    });
+    it("should initialize the ActivityStreamMessageChannel channel", () => {
+      store.init();
+      assert.calledOnce(store._messageChannel.createChannel);
+    });
+  });
+  describe("#uninit", () => {
+    it("should clear .feeds, ._prefHandlers, and ._feedFactories", () => {
+      store.init({
+        a: () => ({}),
+        b: () => ({}),
+        c: () => ({})
+      });
+
+      store.uninit();
+
+      assert.equal(store.feeds.size, 0);
+      assert.equal(store._prefHandlers.size, 0);
+      assert.isNull(store._feedFactories);
+    });
+    it("should destroy the ActivityStreamMessageChannel channel", () => {
+      store.uninit();
+      assert.calledOnce(store._messageChannel.destroyChannel);
+    });
+  });
+  describe("#getState", () => {
+    it("should return the redux state", () => {
+      store._store = createStore((prevState = 123) => prevState);
+      const {getState} = store;
+      assert.equal(getState(), 123);
+    });
+  });
+  describe("#dispatch", () => {
+    it("should call .onAction of each feed", () => {
+      const {dispatch} = store;
+      const sub = {onAction: sinon.spy()};
+      const action = {type: "FOO"};
+
+      store.init({sub: () => sub});
+
+      dispatch(action);
+
+      assert.calledWith(sub.onAction, action);
+    });
+    it("should call the reducers", () => {
+      const {dispatch} = store;
+      store._store = createStore(addNumberReducer);
+
+      dispatch({type: "ADD", data: 14});
+
+      assert.equal(store.getState(), 14);
+    });
+  });
+  describe("#subscribe", () => {
+    it("should subscribe to changes to the store", () => {
+      const sub = sinon.spy();
+      const action = {type: "FOO"};
+
+      store.subscribe(sub);
+      store.dispatch(action);
+
+      assert.calledOnce(sub);
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
@@ -0,0 +1,271 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+const {GlobalOverrider, FakePrefs} = require("test/unit/utils");
+const {TelemetrySender} = require("lib/TelemetrySender.jsm");
+
+/**
+ * A reference to the fake preferences object created by the TelemetrySender
+ * constructor so that we can use the API.
+ */
+let fakePrefs;
+const prefInitHook = function() {
+  fakePrefs = this; // eslint-disable-line consistent-this
+};
+const tsArgs = {prefInitHook};
+
+describe("TelemetrySender", () => {
+  let globals;
+  let tSender;
+  let fetchStub;
+  const observerTopics = ["user-action-event", "performance-event",
+    "tab-session-complete", "undesired-event"];
+  const fakeEndpointUrl = "http://127.0.0.1/stuff";
+  const fakePingJSON = JSON.stringify({action: "fake_action", monkey: 1});
+  const fakeFetchHttpErrorResponse = {ok: false, status: 400};
+  const fakeFetchSuccessResponse = {ok: true, status: 200};
+
+  function assertNotificationObserversAdded() {
+    observerTopics.forEach(topic => {
+      assert.calledWithExactly(
+        global.Services.obs.addObserver, tSender, topic, true);
+    });
+  }
+
+  function assertNotificationObserversRemoved() {
+    observerTopics.forEach(topic => {
+      assert.calledWithExactly(
+        global.Services.obs.removeObserver, tSender, topic);
+    });
+  }
+
+  before(() => {
+    globals = new GlobalOverrider();
+
+    fetchStub = globals.sandbox.stub();
+
+    globals.set("Preferences", FakePrefs);
+    globals.set("fetch", fetchStub);
+  });
+
+  beforeEach(() => {
+  });
+
+  afterEach(() => {
+    globals.reset();
+    FakePrefs.prototype.prefs = {};
+  });
+
+  after(() => globals.restore());
+
+  it("should construct the Prefs object with the right branch", () => {
+    globals.sandbox.spy(global, "Preferences");
+
+    tSender = new TelemetrySender(tsArgs);
+
+    assert.calledOnce(global.Preferences);
+    assert.calledWith(global.Preferences,
+      sinon.match.has("branch", "browser.newtabpage.activity-stream"));
+  });
+
+  it("should set the enabled prop to false if the pref is false", () => {
+    FakePrefs.prototype.prefs = {telemetry: false};
+
+    tSender = new TelemetrySender(tsArgs);
+
+    assert.isFalse(tSender.enabled);
+  });
+
+  it("should not add notification observers if the enabled pref is false", () => {
+    FakePrefs.prototype.prefs = {telemetry: false};
+
+    tSender = new TelemetrySender(tsArgs);
+
+    assert.notCalled(global.Services.obs.addObserver);
+  });
+
+  it("should set the enabled prop to true if the pref is true", () => {
+    FakePrefs.prototype.prefs = {telemetry: true};
+
+    tSender = new TelemetrySender(tsArgs);
+
+    assert.isTrue(tSender.enabled);
+  });
+
+  it("should add all notification observers if the enabled pref is true", () => {
+    FakePrefs.prototype.prefs = {telemetry: true};
+
+    tSender = new TelemetrySender(tsArgs);
+
+    assertNotificationObserversAdded();
+  });
+
+  describe("#_sendPing()", () => {
+    beforeEach(() => {
+      FakePrefs.prototype.prefs = {
+        "telemetry": true,
+        "telemetry.ping.endpoint": fakeEndpointUrl
+      };
+      tSender = new TelemetrySender(tsArgs);
+    });
+
+    it("should POST given ping data to telemetry.ping.endpoint pref w/fetch",
+    async () => {
+      fetchStub.resolves(fakeFetchSuccessResponse);
+      await tSender._sendPing(fakePingJSON);
+
+      assert.calledOnce(fetchStub);
+      assert.calledWithExactly(fetchStub, fakeEndpointUrl,
+        {method: "POST", body: fakePingJSON});
+    });
+
+    it("should log HTTP failures using Cu.reportError", async () => {
+      fetchStub.resolves(fakeFetchHttpErrorResponse);
+
+      await tSender._sendPing(fakePingJSON);
+
+      assert.called(Components.utils.reportError);
+    });
+
+    it("should log an error using Cu.reportError if fetch rejects", async () => {
+      fetchStub.rejects("Oh noes!");
+
+      await tSender._sendPing(fakePingJSON);
+
+      assert.called(Components.utils.reportError);
+    });
+
+    it("should log if logging is on && if action is not activity_stream_performance", async () => {
+      FakePrefs.prototype.prefs = {
+        "telemetry": true,
+        "performance.log": true
+      };
+      fetchStub.resolves(fakeFetchSuccessResponse);
+      tSender = new TelemetrySender(tsArgs);
+
+      await tSender._sendPing(fakePingJSON);
+
+      assert.called(console.log); // eslint-disable-line no-console
+    });
+  });
+
+  describe("#observe()", () => {
+    before(() => {
+      globals.sandbox.stub(TelemetrySender.prototype, "_sendPing");
+    });
+
+    observerTopics.forEach(topic => {
+      it(`should call this._sendPing with data for ${topic}`, () => {
+        const fakeSubject = "fakeSubject";
+        tSender = new TelemetrySender(tsArgs);
+
+        tSender.observe(fakeSubject, topic, fakePingJSON);
+
+        assert.calledOnce(TelemetrySender.prototype._sendPing);
+        assert.calledWithExactly(TelemetrySender.prototype._sendPing,
+          fakePingJSON);
+      });
+    });
+
+    it("should not call this._sendPing for 'nonexistent-topic'", () => {
+      const fakeSubject = "fakeSubject";
+      tSender = new TelemetrySender(tsArgs);
+
+      tSender.observe(fakeSubject, "nonexistent-topic", fakePingJSON);
+
+      assert.notCalled(TelemetrySender.prototype._sendPing);
+    });
+  });
+
+  describe("#uninit()", () => {
+    it("should remove the telemetry pref listener", () => {
+      tSender = new TelemetrySender(tsArgs);
+      assert.property(fakePrefs.observers, "telemetry");
+
+      tSender.uninit();
+
+      assert.notProperty(fakePrefs.observers, "telemetry");
+    });
+
+    it("should remove all notification observers if telemetry pref is true", () => {
+      FakePrefs.prototype.prefs = {telemetry: true};
+      tSender = new TelemetrySender(tsArgs);
+
+      tSender.uninit();
+
+      assertNotificationObserversRemoved();
+    });
+
+    it("should not remove notification observers if telemetry pref is false", () => {
+      FakePrefs.prototype.prefs = {telemetry: false};
+      tSender = new TelemetrySender(tsArgs);
+
+      tSender.uninit();
+
+      assert.notCalled(global.Services.obs.removeObserver);
+    });
+
+    it("should call Cu.reportError if this._prefs.ignore throws", () => {
+      globals.sandbox.stub(FakePrefs.prototype, "ignore").throws("Some Error");
+      tSender = new TelemetrySender(tsArgs);
+
+      tSender.uninit();
+
+      assert.called(global.Components.utils.reportError);
+    });
+  });
+
+  describe("Misc pref changes", () => {
+    describe("telemetry changes from true to false", () => {
+      beforeEach(() => {
+        FakePrefs.prototype.prefs = {"telemetry": true};
+        tSender = new TelemetrySender(tsArgs);
+        assert.propertyVal(tSender, "enabled", true);
+      });
+
+      it("should set the enabled property to false", () => {
+        fakePrefs.set("telemetry", false);
+
+        assert.propertyVal(tSender, "enabled", false);
+      });
+
+      it("should remove all notification observers", () => {
+        fakePrefs.set("telemetry", false);
+
+        assertNotificationObserversRemoved();
+      });
+    });
+
+    describe("telemetry changes from false to true", () => {
+      beforeEach(() => {
+        FakePrefs.prototype.prefs = {"telemetry": false};
+        tSender = new TelemetrySender(tsArgs);
+        assert.propertyVal(tSender, "enabled", false);
+      });
+
+      it("should set the enabled property to true", () => {
+        fakePrefs.set("telemetry", true);
+
+        assert.propertyVal(tSender, "enabled", true);
+      });
+
+      it("should add all topic observers", () => {
+        fakePrefs.set("telemetry", true);
+
+        assertNotificationObserversAdded();
+      });
+    });
+
+    describe("performance.log changes from false to true", () => {
+      it("should change this.logging from false to true", () => {
+        FakePrefs.prototype.prefs = {"performance.log": false};
+        tSender = new TelemetrySender(tsArgs);
+        assert.propertyVal(tSender, "logging", false);
+
+        fakePrefs.set("performance.log", true);
+
+        assert.propertyVal(tSender, "logging", true);
+      });
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -0,0 +1,116 @@
+"use strict";
+const {TopSitesFeed, UPDATE_TIME, TOP_SITES_SHOWMORE_LENGTH, DEFAULT_TOP_SITES} = require("lib/TopSitesFeed.jsm");
+const {GlobalOverrider} = require("test/unit/utils");
+const action = {meta: {fromTarget: {}}};
+const {actionTypes: at} = require("common/Actions.jsm");
+const FAKE_LINKS = new Array(TOP_SITES_SHOWMORE_LENGTH).fill(null).map((v, i) => ({url: `site${i}.com`}));
+const FAKE_SCREENSHOT = "data123";
+
+describe("Top Sites Feed", () => {
+  let feed;
+  let globals;
+  let sandbox;
+  let links;
+  let clock;
+  before(() => {
+    globals = new GlobalOverrider();
+    sandbox = globals.sandbox;
+  });
+  beforeEach(() => {
+    globals.set("PlacesProvider", {links: {getLinks: sandbox.spy(() => Promise.resolve(links))}});
+    globals.set("PreviewProvider", {getThumbnail: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))});
+    feed = new TopSitesFeed();
+    feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
+    links = FAKE_LINKS;
+    clock = sinon.useFakeTimers();
+  });
+  afterEach(() => {
+    globals.restore();
+    clock.restore();
+  });
+
+  it("should have default sites with .isDefault = true", () => {
+    DEFAULT_TOP_SITES.forEach(link => assert.propertyVal(link, "isDefault", true));
+  });
+
+  describe("#getLinksWithDefaults", () => {
+    it("should get the links from Places Provider", async () => {
+      const result = await feed.getLinksWithDefaults();
+      assert.deepEqual(result, links);
+      assert.calledOnce(global.PlacesProvider.links.getLinks);
+    });
+    it("should add defaults if there are are not enough links", async () => {
+      links = [{url: "foo.com"}];
+      const result = await feed.getLinksWithDefaults();
+      assert.deepEqual(result, [{url: "foo.com"}, ...DEFAULT_TOP_SITES]);
+    });
+    it("should only add defaults up to TOP_SITES_SHOWMORE_LENGTH", async () => {
+      links = new Array(TOP_SITES_SHOWMORE_LENGTH - 1).fill({url: "foo.com"});
+      const result = await feed.getLinksWithDefaults();
+      assert.lengthOf(result, TOP_SITES_SHOWMORE_LENGTH);
+      assert.deepEqual(result, [...links, DEFAULT_TOP_SITES[0]]);
+    });
+    it("should not throw if PlacesProvider returns null", () => {
+      links = null;
+      assert.doesNotThrow(() => {
+        feed.getLinksWithDefaults(action);
+      });
+    });
+  });
+  describe("#refresh", () => {
+    it("should dispatch an action with the links returned", async () => {
+      sandbox.stub(feed, "getScreenshot");
+      await feed.refresh(action);
+      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, links);
+    });
+    it("should call .getScreenshot for each link", async () => {
+      sandbox.stub(feed, "getScreenshot");
+      await feed.refresh(action);
+
+      links.forEach(link => assert.calledWith(feed.getScreenshot, link.url));
+    });
+  });
+  describe("getScreenshot", () => {
+    it("should call PreviewProvider.getThumbnail with the right url", async () => {
+      const url = "foo.com";
+      await feed.getScreenshot(url);
+      assert.calledWith(global.PreviewProvider.getThumbnail, url);
+    });
+  });
+  describe("#onAction", () => {
+    it("should call refresh if there are not enough sites on NEW_TAB_LOAD", () => {
+      feed.store.getState = function() { return {TopSites: {rows: []}}; };
+      sinon.stub(feed, "refresh");
+      feed.onAction({type: at.NEW_TAB_LOAD});
+      assert.calledOnce(feed.refresh);
+    });
+    it("should call refresh if there are not sites on NEW_TAB_LOAD, not counting defaults", () => {
+      feed.store.getState = function() { return {TopSites: {rows: [{url: "foo.com"}, ...DEFAULT_TOP_SITES]}}; };
+      sinon.stub(feed, "refresh");
+      feed.onAction({type: at.NEW_TAB_LOAD});
+      assert.calledOnce(feed.refresh);
+    });
+    it("should not call refresh if there are enough sites on NEW_TAB_LOAD", () => {
+      feed.lastUpdated = Date.now();
+      sinon.stub(feed, "refresh");
+      feed.onAction({type: at.NEW_TAB_LOAD});
+      assert.notCalled(feed.refresh);
+    });
+    it("should call refresh if .lastUpdated is too old on NEW_TAB_LOAD", () => {
+      feed.lastUpdated = 0;
+      clock.tick(UPDATE_TIME);
+      sinon.stub(feed, "refresh");
+      feed.onAction({type: at.NEW_TAB_LOAD});
+      assert.calledOnce(feed.refresh);
+    });
+    it("should not call refresh if .lastUpdated is less than update time on NEW_TAB_LOAD", () => {
+      feed.lastUpdated = 0;
+      clock.tick(UPDATE_TIME - 1);
+      sinon.stub(feed, "refresh");
+      feed.onAction({type: at.NEW_TAB_LOAD});
+      assert.notCalled(feed.refresh);
+    });
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
@@ -0,0 +1,43 @@
+const initStore = require("content-src/lib/init-store");
+const {GlobalOverrider, addNumberReducer} = require("test/unit/utils");
+const {actionCreators: ac} = require("common/Actions.jsm");
+
+describe("initStore", () => {
+  let globals;
+  let store;
+  before(() => {
+    globals = new GlobalOverrider();
+    globals.set("sendAsyncMessage", globals.sandbox.spy());
+    globals.set("addMessageListener", globals.sandbox.spy());
+  });
+  beforeEach(() => {
+    store = initStore({number: addNumberReducer});
+  });
+  afterEach(() => globals.reset());
+  after(() => globals.restore());
+  it("should create a store with the provided reducers", () => {
+    assert.ok(store);
+    assert.property(store.getState(), "number");
+  });
+  it("should add a listener for incoming actions", () => {
+    assert.calledWith(global.addMessageListener, initStore.INCOMING_MESSAGE_NAME);
+    const callback = global.addMessageListener.firstCall.args[1];
+    globals.sandbox.spy(store, "dispatch");
+    const message = {name: initStore.INCOMING_MESSAGE_NAME, data: {type: "FOO"}};
+    callback(message);
+    assert.calledWith(store.dispatch, message.data);
+  });
+  it("should replace the state if a MERGE_STORE_ACTION is dispatched", () => {
+    store.dispatch({type: initStore.MERGE_STORE_ACTION, data: {number: 42}});
+    assert.deepEqual(store.getState(), {number: 42});
+  });
+  it("should send out SendToMain ations", () => {
+    const action = ac.SendToMain({type: "FOO"});
+    store.dispatch(action);
+    assert.calledWith(global.sendAsyncMessage, initStore.OUTGOING_MESSAGE_NAME, action);
+  });
+  it("should not send out other types of ations", () => {
+    store.dispatch({type: "FOO"});
+    assert.notCalled(global.sendAsyncMessage);
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/unit-entry.js
@@ -0,0 +1,38 @@
+const {GlobalOverrider} = require("test/unit/utils");
+
+const req = require.context(".", true, /\.test\.js$/);
+const files = req.keys();
+
+// This exposes sinon assertions to chai.assert
+sinon.assert.expose(assert, {prefix: ""});
+
+let overrider = new GlobalOverrider();
+overrider.set({
+  Components: {
+    interfaces: {},
+    utils: {
+      import: overrider.sandbox.spy(),
+      importGlobalProperties: overrider.sandbox.spy(),
+      reportError: overrider.sandbox.spy()
+    }
+  },
+  XPCOMUtils: {
+    defineLazyModuleGetter: overrider.sandbox.spy(),
+    defineLazyServiceGetter: overrider.sandbox.spy(),
+    generateQI: overrider.sandbox.stub().returns(() => {})
+  },
+  console: {log: overrider.sandbox.spy()},
+  dump: overrider.sandbox.spy(),
+  Services: {
+    obs: {
+      addObserver: overrider.sandbox.spy(),
+      removeObserver: overrider.sandbox.spy()
+    }
+  }
+});
+
+describe("activity-stream", () => {
+  afterEach(() => overrider.reset());
+  after(() => overrider.restore());
+  files.forEach(file => req(file));
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/utils.js
@@ -0,0 +1,122 @@
+/**
+ * GlobalOverrider - Utility that allows you to override properties on the global object.
+ *                   See unit-entry.js for example usage.
+ */
+class GlobalOverrider {
+  constructor() {
+    this.originalGlobals = new Map();
+    this.sandbox = sinon.sandbox.create();
+  }
+
+  /**
+   * _override - Internal method to override properties on the global object.
+   *             The first time a given key is overridden, we cache the original
+   *             value in this.originalGlobals so that later it can be restored.
+   *
+   * @param  {string} key The identifier of the property
+   * @param  {any} value The value to which the property should be reassigned
+   */
+  _override(key, value) {
+    if (key === "Components") {
+      // Components can be reassigned, but it will subsequently throw a deprecation
+      // error in Firefox which will stop execution. Adding the assignment statement
+      // to a try/catch block will prevent this from happening.
+      try {
+        global[key] = value;
+      } catch (e) {} // eslint-disable-line no-empty
+      return;
+    }
+    if (!this.originalGlobals.has(key)) {
+      this.originalGlobals.set(key, global[key]);
+    }
+    global[key] = value;
+  }
+
+  /**
+   * set - Override a given property, or all properties on an object
+   *
+   * @param  {string|object} key If a string, the identifier of the property
+   *                             If an object, a number of properties and values to which they should be reassigned.
+   * @param  {any} value The value to which the property should be reassigned
+   * @return {type}       description
+   */
+  set(key, value) {
+    if (!value && typeof key === "object") {
+      const overrides = key;
+      Object.keys(overrides).forEach(k => this._override(k, overrides[k]));
+    } else {
+      this._override(key, value);
+    }
+  }
+
+  /**
+   * reset - Reset the global sandbox, so all state on spies, stubs etc. is cleared.
+   *         You probably want to call this after each test.
+   */
+  reset() {
+    this.sandbox.reset();
+  }
+
+  /**
+   * restore - Restore the global sandbox and reset all overriden properties to
+   *           their original values. You should call this after all tests have completed.
+   */
+  restore() {
+    this.sandbox.restore();
+    this.originalGlobals.forEach((value, key) => {
+      global[key] = value;
+    });
+  }
+}
+
+/**
+ * Very simple fake for the most basic semantics of Preferences.jsm. Lots of
+ * things aren't yet supported.  Feel free to add them in.
+ *
+ * @param {Object} args - optional arguments
+ * @param {Function} args.initHook - if present, will be called back
+ *                   inside the constructor. Typically used from tests
+ *                   to save off a pointer to the created instance so that
+ *                   stubs and spies can be inspected by the test code.
+ */
+function FakePrefs(args) {
+  if (args) {
+    if ("initHook" in args) {
+      args.initHook.call(this);
+    }
+  }
+}
+FakePrefs.prototype = {
+  observers: {},
+  observe(prefName, callback) {
+    this.observers[prefName] = callback;
+  },
+  ignore(prefName, callback) {
+    if (prefName in this.observers) {
+      delete this.observers[prefName];
+    }
+  },
+
+  prefs: {},
+  get(prefName) { return this.prefs[prefName]; },
+  set(prefName, value) {
+    this.prefs[prefName] = value;
+
+    if (prefName in this.observers) {
+      this.observers[prefName](value);
+    }
+  }
+};
+
+/**
+ * addNumberReducer - a simple dummy reducer for testing that adds a number
+ */
+function addNumberReducer(prevState = 0, action) {
+  return action.type === "ADD" ? prevState + action.data : prevState;
+}
+
+module.exports = {
+  FakePrefs,
+  GlobalOverrider,
+  addNumberReducer
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/vendor/redux.js
@@ -0,0 +1,948 @@
+/**
+ * Redux v.3.6.0
+ */
+(function webpackUniversalModuleDefinition(root, factory) {
+	if(typeof exports === 'object' && typeof module === 'object')
+		module.exports = factory();
+	else if(typeof define === 'function' && define.amd)
+		define([], factory);
+	else if(typeof exports === 'object')
+		exports["Redux"] = factory();
+	else
+		root["Redux"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ 	// The module cache
+/******/ 	var installedModules = {};
+
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+
+/******/ 		// Check if module is in cache
+/******/ 		if(installedModules[moduleId])
+/******/ 			return installedModules[moduleId].exports;
+
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = installedModules[moduleId] = {
+/******/ 			exports: {},
+/******/ 			id: moduleId,
+/******/ 			loaded: false
+/******/ 		};
+
+/******/ 		// Execute the module function
+/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ 		// Flag the module as loaded
+/******/ 		module.loaded = true;
+
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+
+
+/******/ 	// expose the modules object (__webpack_modules__)
+/******/ 	__webpack_require__.m = modules;
+
+/******/ 	// expose the module cache
+/******/ 	__webpack_require__.c = installedModules;
+
+/******/ 	// __webpack_public_path__
+/******/ 	__webpack_require__.p = "";
+
+/******/ 	// Load entry module and return exports
+/******/ 	return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+	'use strict';
+
+	exports.__esModule = true;
+	exports.compose = exports.applyMiddleware = exports.bindActionCreators = exports.combineReducers = exports.createStore = undefined;
+
+	var _createStore = __webpack_require__(2);
+
+	var _createStore2 = _interopRequireDefault(_createStore);
+
+	var _combineReducers = __webpack_require__(7);
+
+	var _combineReducers2 = _interopRequireDefault(_combineReducers);
+
+	var _bindActionCreators = __webpack_require__(6);
+
+	var _bindActionCreators2 = _interopRequireDefault(_bindActionCreators);
+
+	var _applyMiddleware = __webpack_require__(5);
+
+	var _applyMiddleware2 = _interopRequireDefault(_applyMiddleware);
+
+	var _compose = __webpack_require__(1);
+
+	var _compose2 = _interopRequireDefault(_compose);
+
+	var _warning = __webpack_require__(3);
+
+	var _warning2 = _interopRequireDefault(_warning);
+
+	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+	/*
+	* This is a dummy function to check if the function name has been altered by minification.
+	* If the function has been minified and NODE_ENV !== 'production', warn the user.
+	*/
+	function isCrushed() {}
+
+	if (("development") !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed') {
+	  (0, _warning2['default'])('You are currently using minified code outside of NODE_ENV === \'production\'. ' + 'This means that you are running a slower development build of Redux. ' + 'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' + 'or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) ' + 'to ensure you have the correct code for your production build.');
+	}
+
+	exports.createStore = _createStore2['default'];
+	exports.combineReducers = _combineReducers2['default'];
+	exports.bindActionCreators = _bindActionCreators2['default'];
+	exports.applyMiddleware = _applyMiddleware2['default'];
+	exports.compose = _compose2['default'];
+
+/***/ },
+/* 1 */
+/***/ function(module, exports) {
+
+	"use strict";
+
+	exports.__esModule = true;
+	exports["default"] = compose;
+	/**
+	 * Composes single-argument functions from right to left. The rightmost
+	 * function can take multiple arguments as it provides the signature for
+	 * the resulting composite function.
+	 *
+	 * @param {...Function} funcs The functions to compose.
+	 * @returns {Function} A function obtained by composing the argument functions
+	 * from right to left. For example, compose(f, g, h) is identical to doing
+	 * (...args) => f(g(h(...args))).
+	 */
+
+	function compose() {
+	  for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
+	    funcs[_key] = arguments[_key];
+	  }
+
+	  if (funcs.length === 0) {
+	    return function (arg) {
+	      return arg;
+	    };
+	  }
+
+	  if (funcs.length === 1) {
+	    return funcs[0];
+	  }
+
+	  var last = funcs[funcs.length - 1];
+	  var rest = funcs.slice(0, -1);
+	  return function () {
+	    return rest.reduceRight(function (composed, f) {
+	      return f(composed);
+	    }, last.apply(undefined, arguments));
+	  };
+	}
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+	'use strict';
+
+	exports.__esModule = true;
+	exports.ActionTypes = undefined;
+	exports['default'] = createStore;
+
+	var _isPlainObject = __webpack_require__(4);
+
+	var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+	var _symbolObservable = __webpack_require__(12);
+
+	var _symbolObservable2 = _interopRequireDefault(_symbolObservable);
+
+	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+	/**
+	 * These are private action types reserved by Redux.
+	 * For any unknown actions, you must return the current state.
+	 * If the current state is undefined, you must return the initial state.
+	 * Do not reference these action types directly in your code.
+	 */
+	var ActionTypes = exports.ActionTypes = {
+	  INIT: '@@redux/INIT'
+	};
+
+	/**
+	 * Creates a Redux store that holds the state tree.
+	 * The only way to change the data in the store is to call `dispatch()` on it.
+	 *
+	 * There should only be a single store in your app. To specify how different
+	 * parts of the state tree respond to actions, you may combine several reducers
+	 * into a single reducer function by using `combineReducers`.
+	 *
+	 * @param {Function} reducer A function that returns the next state tree, given
+	 * the current state tree and the action to handle.
+	 *
+	 * @param {any} [preloadedState] The initial state. You may optionally specify it
+	 * to hydrate the state from the server in universal apps, or to restore a
+	 * previously serialized user session.
+	 * If you use `combineReducers` to produce the root reducer function, this must be
+	 * an object with the same shape as `combineReducers` keys.
+	 *
+	 * @param {Function} enhancer The store enhancer. You may optionally specify it
+	 * to enhance the store with third-party capabilities such as middleware,
+	 * time travel, persistence, etc. The only store enhancer that ships with Redux
+	 * is `applyMiddleware()`.
+	 *
+	 * @returns {Store} A Redux store that lets you read the state, dispatch actions
+	 * and subscribe to changes.
+	 */
+	function createStore(reducer, preloadedState, enhancer) {
+	  var _ref2;
+
+	  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
+	    enhancer = preloadedState;
+	    preloadedState = undefined;
+	  }
+
+	  if (typeof enhancer !== 'undefined') {
+	    if (typeof enhancer !== 'function') {
+	      throw new Error('Expected the enhancer to be a function.');
+	    }
+
+	    return enhancer(createStore)(reducer, preloadedState);
+	  }
+
+	  if (typeof reducer !== 'function') {
+	    throw new Error('Expected the reducer to be a function.');
+	  }
+
+	  var currentReducer = reducer;
+	  var currentState = preloadedState;
+	  var currentListeners = [];
+	  var nextListeners = currentListeners;
+	  var isDispatching = false;
+
+	  function ensureCanMutateNextListeners() {
+	    if (nextListeners === currentListeners) {
+	      nextListeners = currentListeners.slice();
+	    }
+	  }
+
+	  /**
+	   * Reads the state tree managed by the store.
+	   *
+	   * @returns {any} The current state tree of your application.
+	   */
+	  function getState() {
+	    return currentState;
+	  }
+
+	  /**
+	   * Adds a change listener. It will be called any time an action is dispatched,
+	   * and some part of the state tree may potentially have changed. You may then
+	   * call `getState()` to read the current state tree inside the callback.
+	   *
+	   * You may call `dispatch()` from a change listener, with the following
+	   * caveats:
+	   *
+	   * 1. The subscriptions are snapshotted just before every `dispatch()` call.
+	   * If you subscribe or unsubscribe while the listeners are being invoked, this
+	   * will not have any effect on the `dispatch()` that is currently in progress.
+	   * However, the next `dispatch()` call, whether nested or not, will use a more
+	   * recent snapshot of the subscription list.
+	   *
+	   * 2. The listener should not expect to see all state changes, as the state
+	   * might have been updated multiple times during a nested `dispatch()` before
+	   * the listener is called. It is, however, guaranteed that all subscribers
+	   * registered before the `dispatch()` started will be called with the latest
+	   * state by the time it exits.
+	   *
+	   * @param {Function} listener A callback to be invoked on every dispatch.
+	   * @returns {Function} A function to remove this change listener.
+	   */
+	  function subscribe(listener) {
+	    if (typeof listener !== 'function') {
+	      throw new Error('Expected listener to be a function.');
+	    }
+
+	    var isSubscribed = true;
+
+	    ensureCanMutateNextListeners();
+	    nextListeners.push(listener);
+
+	    return function unsubscribe() {
+	      if (!isSubscribed) {
+	        return;
+	      }
+
+	      isSubscribed = false;
+
+	      ensureCanMutateNextListeners();
+	      var index = nextListeners.indexOf(listener);
+	      nextListeners.splice(index, 1);
+	    };
+	  }
+
+	  /**
+	   * Dispatches an action. It is the only way to trigger a state change.
+	   *
+	   * The `reducer` function, used to create the store, will be called with the
+	   * current state tree and the given `action`. Its return value will
+	   * be considered the **next** state of the tree, and the change listeners
+	   * will be notified.
+	   *
+	   * The base implementation only supports plain object actions. If you want to
+	   * dispatch a Promise, an Observable, a thunk, or something else, you need to
+	   * wrap your store creating function into the corresponding middleware. For
+	   * example, see the documentation for the `redux-thunk` package. Even the
+	   * middleware will eventually dispatch plain object actions using this method.
+	   *
+	   * @param {Object} action A plain object representing “what changed”. It is
+	   * a good idea to keep actions serializable so you can record and replay user
+	   * sessions, or use the time travelling `redux-devtools`. An action must have
+	   * a `type` property which may not be `undefined`. It is a good idea to use
+	   * string constants for action types.
+	   *
+	   * @returns {Object} For convenience, the same action object you dispatched.
+	   *
+	   * Note that, if you use a custom middleware, it may wrap `dispatch()` to
+	   * return something else (for example, a Promise you can await).
+	   */
+	  function dispatch(action) {
+	    if (!(0, _isPlainObject2['default'])(action)) {
+	      throw new Error('Actions must be plain objects. ' + 'Use custom middleware for async actions.');
+	    }
+
+	    if (typeof action.type === 'undefined') {
+	      throw new Error('Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?');
+	    }
+
+	    if (isDispatching) {
+	      throw new Error('Reducers may not dispatch actions.');
+	    }
+
+	    try {
+	      isDispatching = true;
+	      currentState = currentReducer(currentState, action);
+	    } finally {
+	      isDispatching = false;
+	    }
+
+	    var listeners = currentListeners = nextListeners;
+	    for (var i = 0; i < listeners.length; i++) {
+	      listeners[i]();
+	    }
+
+	    return action;
+	  }
+
+	  /**
+	   * Replaces the reducer currently used by the store to calculate the state.
+	   *
+	   * You might need this if your app implements code splitting and you want to
+	   * load some of the reducers dynamically. You might also need this if you
+	   * implement a hot reloading mechanism for Redux.
+	   *
+	   * @param {Function} nextReducer The reducer for the store to use instead.
+	   * @returns {void}
+	   */
+	  function replaceReducer(nextReducer) {
+	    if (typeof nextReducer !== 'function') {
+	      throw new Error('Expected the nextReducer to be a function.');
+	    }
+
+	    currentReducer = nextReducer;
+	    dispatch({ type: ActionTypes.INIT });
+	  }
+
+	  /**
+	   * Interoperability point for observable/reactive libraries.
+	   * @returns {observable} A minimal observable of state changes.
+	   * For more information, see the observable proposal:
+	   * https://github.com/zenparsing/es-observable
+	   */
+	  function observable() {
+	    var _ref;
+
+	    var outerSubscribe = subscribe;
+	    return _ref = {
+	      /**
+	       * The minimal observable subscription method.
+	       * @param {Object} observer Any object that can be used as an observer.
+	       * The observer object should have a `next` method.
+	       * @returns {subscription} An object with an `unsubscribe` method that can
+	       * be used to unsubscribe the observable from the store, and prevent further
+	       * emission of values from the observable.
+	       */
+	      subscribe: function subscribe(observer) {
+	        if (typeof observer !== 'object') {
+	          throw new TypeError('Expected the observer to be an object.');
+	        }
+
+	        function observeState() {
+	          if (observer.next) {
+	            observer.next(getState());
+	          }
+	        }
+
+	        observeState();
+	        var unsubscribe = outerSubscribe(observeState);
+	        return { unsubscribe: unsubscribe };
+	      }
+	    }, _ref[_symbolObservable2['default']] = function () {
+	      return this;
+	    }, _ref;
+	  }
+
+	  // When a store is created, an "INIT" action is dispatched so that every
+	  // reducer returns their initial state. This effectively populates
+	  // the initial state tree.
+	  dispatch({ type: ActionTypes.INIT });
+
+	  return _ref2 = {
+	    dispatch: dispatch,
+	    subscribe: subscribe,
+	    getState: getState,
+	    replaceReducer: replaceReducer
+	  }, _ref2[_symbolObservable2['default']] = observable, _ref2;
+	}
+
+/***/ },
+/* 3 */
+/***/ function(module, exports) {
+
+	'use strict';
+
+	exports.__esModule = true;
+	exports['default'] = warning;
+	/**
+	 * Prints a warning in the console if it exists.
+	 *
+	 * @param {String} message The warning message.
+	 * @returns {void}
+	 */
+	function warning(message) {
+	  /* eslint-disable no-console */
+	  if (typeof console !== 'undefined' && typeof console.error === 'function') {
+	    console.error(message);
+	  }
+	  /* eslint-enable no-console */
+	  try {
+	    // This error was thrown as a convenience so that if you enable
+	    // "break on all exceptions" in your console,
+	    // it would pause the execution at this line.
+	    throw new Error(message);
+	    /* eslint-disable no-empty */
+	  } catch (e) {}
+	  /* eslint-enable no-empty */
+	}
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+	var getPrototype = __webpack_require__(8),
+	    isHostObject = __webpack_require__(9),
+	    isObjectLike = __webpack_require__(11);
+
+	/** `Object#toString` result references. */
+	var objectTag = '[object Object]';
+
+	/** Used for built-in method references. */
+	var funcProto = Function.prototype,
+	    objectProto = Object.prototype;
+
+	/** Used to resolve the decompiled source of functions. */
+	var funcToString = funcProto.toString;
+
+	/** Used to check objects for own properties. */
+	var hasOwnProperty = objectProto.hasOwnProperty;
+
+	/** Used to infer the `Object` constructor. */
+	var objectCtorString = funcToString.call(Object);
+
+	/**
+	 * Used to resolve the
+	 * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
+	 * of values.
+	 */
+	var objectToString = objectProto.toString;
+
+	/**
+	 * Checks if `value` is a plain object, that is, an object created by the
+	 * `Object` constructor or one with a `[[Prototype]]` of `null`.
+	 *
+	 * @static
+	 * @memberOf _
+	 * @since 0.8.0
+	 * @category Lang
+	 * @param {*} value The value to check.
+	 * @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
+	 * @example
+	 *
+	 * function Foo() {
+	 *   this.a = 1;
+	 * }
+	 *
+	 * _.isPlainObject(new Foo);
+	 * // => false
+	 *
+	 * _.isPlainObject([1, 2, 3]);
+	 * // => false
+	 *
+	 * _.isPlainObject({ 'x': 0, 'y': 0 });
+	 * // => true
+	 *
+	 * _.isPlainObject(Object.create(null));
+	 * // => true
+	 */
+	function isPlainObject(value) {
+	  if (!isObjectLike(value) ||
+	      objectToString.call(value) != objectTag || isHostObject(value)) {
+	    return false;
+	  }
+	  var proto = getPrototype(value);
+	  if (proto === null) {
+	    return true;
+	  }
+	  var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
+	  return (typeof Ctor == 'function' &&
+	    Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);
+	}
+
+	module.exports = isPlainObject;
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+	'use strict';
+
+	exports.__esModule = true;
+
+	var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+	exports['default'] = applyMiddleware;
+
+	var _compose = __webpack_require__(1);
+
+	var _compose2 = _interopRequireDefault(_compose);
+
+	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+	/**
+	 * Creates a store enhancer that applies middleware to the dispatch method
+	 * of the Redux store. This is handy for a variety of tasks, such as expressing
+	 * asynchronous actions in a concise manner, or logging every action payload.
+	 *
+	 * See `redux-thunk` package as an example of the Redux middleware.
+	 *
+	 * Because middleware is potentially asynchronous, this should be the first
+	 * store enhancer in the composition chain.
+	 *
+	 * Note that each middleware will be given the `dispatch` and `getState` functions
+	 * as named arguments.
+	 *
+	 * @param {...Function} middlewares The middleware chain to be applied.
+	 * @returns {Function} A store enhancer applying the middleware.
+	 */
+	function applyMiddleware() {
+	  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
+	    middlewares[_key] = arguments[_key];
+	  }
+
+	  return function (createStore) {
+	    return function (reducer, preloadedState, enhancer) {
+	      var store = createStore(reducer, preloadedState, enhancer);
+	      var _dispatch = store.dispatch;
+	      var chain = [];
+
+	      var middlewareAPI = {
+	        getState: store.getState,
+	        dispatch: function dispatch(action) {
+	          return _dispatch(action);
+	        }
+	      };
+	      chain = middlewares.map(function (middleware) {
+	        return middleware(middlewareAPI);
+	      });
+	      _dispatch = _compose2['default'].apply(undefined, chain)(store.dispatch);
+
+	      return _extends({}, store, {
+	        dispatch: _dispatch
+	      });
+	    };
+	  };
+	}
+
+/***/ },
+/* 6 */
+/***/ function(module, exports) {
+
+	'use strict';
+
+	exports.__esModule = true;
+	exports['default'] = bindActionCreators;
+	function bindActionCreator(actionCreator, dispatch) {
+	  return function () {
+	    return dispatch(actionCreator.apply(undefined, arguments));
+	  };
+	}
+
+	/**
+	 * Turns an object whose values are action creators, into an object with the
+	 * same keys, but with every function wrapped into a `dispatch` call so they
+	 * may be invoked directly. This is just a convenience method, as you can call
+	 * `store.dispatch(MyActionCreators.doSomething())` yourself just fine.
+	 *
+	 * For convenience, you can also pass a single function as the first argument,
+	 * and get a function in return.
+	 *
+	 * @param {Function|Object} actionCreators An object whose values are action
+	 * creator functions. One handy way to obtain it is to use ES6 `import * as`
+	 * syntax. You may also pass a single function.
+	 *
+	 * @param {Function} dispatch The `dispatch` function available on your Redux
+	 * store.
+	 *
+	 * @returns {Function|Object} The object mimicking the original object, but with
+	 * every action creator wrapped into the `dispatch` call. If you passed a
+	 * function as `actionCreators`, the return value will also be a single
+	 * function.
+	 */
+	function bindActionCreators(actionCreators, dispatch) {
+	  if (typeof actionCreators === 'function') {
+	    return bindActionCreator(actionCreators, dispatch);
+	  }
+
+	  if (typeof actionCreators !== 'object' || actionCreators === null) {
+	    throw new Error('bindActionCreators expected an object or a function, instead received ' + (actionCreators === null ? 'null' : typeof actionCreators) + '. ' + 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');
+	  }
+
+	  var keys = Object.keys(actionCreators);
+	  var boundActionCreators = {};
+	  for (var i = 0; i < keys.length; i++) {
+	    var key = keys[i];
+	    var actionCreator = actionCreators[key];
+	    if (typeof actionCreator === 'function') {
+	      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
+	    }
+	  }
+	  return boundActionCreators;
+	}
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+	'use strict';
+
+	exports.__esModule = true;
+	exports['default'] = combineReducers;
+
+	var _createStore = __webpack_require__(2);
+
+	var _isPlainObject = __webpack_require__(4);
+
+	var _isPlainObject2 = _interopRequireDefault(_isPlainObject);
+
+	var _warning = __webpack_require__(3);
+
+	var _warning2 = _interopRequireDefault(_warning);
+
+	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+	function getUndefinedStateErrorMessage(key, action) {
+	  var actionType = action && action.type;
+	  var actionName = actionType && '"' + actionType.toString() + '"' || 'an action';
+
+	  return 'Given action ' + actionName + ', reducer "' + key + '" returned undefined. ' + 'To ignore an action, you must explicitly return the previous state.';
+	}
+
+	function getUnexpectedStateShapeWarningMessage(inputState, reducers, action, unexpectedKeyCache) {
+	  var reducerKeys = Object.keys(reducers);
+	  var argumentName = action && action.type === _createStore.ActionTypes.INIT ? 'preloadedState argument passed to createStore' : 'previous state received by the reducer';
+
+	  if (reducerKeys.length === 0) {
+	    return 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.';
+	  }
+
+	  if (!(0, _isPlainObject2['default'])(inputState)) {
+	    return 'The ' + argumentName + ' has unexpected type of "' + {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + '". Expected argument to be an object with the following ' + ('keys: "' + reducerKeys.join('", "') + '"');
+	  }
+
+	  var unexpectedKeys = Object.keys(inputState).filter(function (key) {
+	    return !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key];
+	  });
+
+	  unexpectedKeys.forEach(function (key) {
+	    unexpectedKeyCache[key] = true;
+	  });
+
+	  if (unexpectedKeys.length > 0) {
+	    return 'Unexpected ' + (unexpectedKeys.length > 1 ? 'keys' : 'key') + ' ' + ('"' + unexpectedKeys.join('", "') + '" found in ' + argumentName + '. ') + 'Expected to find one of the known reducer keys instead: ' + ('"' + reducerKeys.join('", "') + '". Unexpected keys will be ignored.');
+	  }
+	}
+
+	function assertReducerSanity(reducers) {
+	  Object.keys(reducers).forEach(function (key) {
+	    var reducer = reducers[key];
+	    var initialState = reducer(undefined, { type: _createStore.ActionTypes.INIT });
+
+	    if (typeof initialState === 'undefined') {
+	      throw new Error('Reducer "' + key + '" returned undefined during initialization. ' + 'If the state passed to the reducer is undefined, you must ' + 'explicitly return the initial state. The initial state may ' + 'not be undefined.');
+	    }
+
+	    var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.');
+	    if (typeof reducer(undefined, { type: type }) === 'undefined') {
+	      throw new Error('Reducer "' + key + '" returned undefined when probed with a random type. ' + ('Don\'t try to handle ' + _createStore.ActionTypes.INIT + ' or other actions in "redux/*" ') + 'namespace. They are considered private. Instead, you must return the ' + 'current state for any unknown actions, unless it is undefined, ' + 'in which case you must return the initial state, regardless of the ' + 'action type. The initial state may not be undefined.');
+	    }
+	  });
+	}
+
+	/**
+	 * Turns an object whose values are different reducer functions, into a single
+	 * reducer function. It will call every child reducer, and gather their results
+	 * into a single state object, whose keys correspond to the keys of the passed
+	 * reducer functions.
+	 *
+	 * @param {Object} reducers An object whose values correspond to different
+	 * reducer functions that need to be combined into one. One handy way to obtain
+	 * it is to use ES6 `import * as reducers` syntax. The reducers may never return
+	 * undefined for any action. Instead, they should return their initial state
+	 * if the state passed to them was undefined, and the current state for any
+	 * unrecognized action.
+	 *
+	 * @returns {Function} A reducer function that invokes every reducer inside the
+	 * passed object, and builds a state object with the same shape.
+	 */
+	function combineReducers(reducers) {
+	  var reducerKeys = Object.keys(reducers);
+	  var finalReducers = {};
+	  for (var i = 0; i < reducerKeys.length; i++) {
+	    var key = reducerKeys[i];
+
+	    if (true) {
+	      if (typeof reducers[key] === 'undefined') {
+	        (0, _warning2['default'])('No reducer provided for key "' + key + '"');
+	      }
+	    }
+
+	    if (typeof reducers[key] === 'function') {
+	      finalReducers[key] = reducers[key];
+	    }
+	  }
+	  var finalReducerKeys = Object.keys(finalReducers);
+
+	  if (true) {
+	    var unexpectedKeyCache = {};
+	  }
+
+	  var sanityError;
+	  try {
+	    assertReducerSanity(finalReducers);
+	  } catch (e) {
+	    sanityError = e;
+	  }
+
+	  return function combination() {
+	    var state = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+	    var action = arguments[1];
+
+	    if (sanityError) {
+	      throw sanityError;
+	    }
+
+	    if (true) {
+	      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);
+	      if (warningMessage) {
+	        (0, _warning2['default'])(warningMessage);
+	      }
+	    }
+
+	    var hasChanged = false;
+	    var nextState = {};
+	    for (var i = 0; i < finalReducerKeys.length; i++) {
+	      var key = finalReducerKeys[i];
+	      var reducer = finalReducers[key];
+	      var previousStateForKey = state[key];
+	      var nextStateForKey = reducer(previousStateForKey, action);
+	      if (typeof nextStateForKey === 'undefined') {
+	        var errorMessage = getUndefinedStateErrorMessage(key, action);
+	        throw new Error(errorMessage);
+	      }
+	      nextState[key] = nextStateForKey;
+	      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
+	    }
+	    return hasChanged ? nextState : state;
+	  };
+	}
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+	var overArg = __webpack_require__(10);
+
+	/** Built-in value references. */
+	var getPrototype = overArg(Object.getPrototypeOf, Object);
+
+	module.exports = getPrototype;
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports) {
+
+	/**
+	 * Checks if `value` is a host object in IE < 9.
+	 *
+	 * @private
+	 * @param {*} value The value to check.
+	 * @returns {boolean} Returns `true` if `value` is a host object, else `false`.
+	 */
+	function isHostObject(value) {
+	  // Many host objects are `Object` objects that can coerce to strings
+	  // despite having improperly defined `toString` methods.
+	  var result = false;
+	  if (value != null && typeof value.toString != 'function') {
+	    try {
+	      result = !!(value + '');
+	    } catch (e) {}
+	  }
+	  return result;
+	}
+
+	module.exports = isHostObject;
+
+
+/***/ },
+/* 10 */
+/***/ function(module, exports) {
+
+	/**
+	 * Creates a unary function that invokes `func` with its argument transformed.
+	 *
+	 * @private
+	 * @param {Function} func The function to wrap.
+	 * @param {Function} transform The argument transform.
+	 * @returns {Function} Returns the new function.
+	 */
+	function overArg(func, transform) {
+	  return function(arg) {
+	    return func(transform(arg));
+	  };
+	}
+
+	module.exports = overArg;
+
+
+/***/ },
+/* 11 */
+/***/ function(module, exports) {
+
+	/**
+	 * Checks if `value` is object-like. A value is object-like if it's not `null`
+	 * and has a `typeof` result of "object".
+	 *
+	 * @static
+	 * @memberOf _
+	 * @since 4.0.0
+	 * @category Lang
+	 * @param {*} value The value to check.
+	 * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
+	 * @example
+	 *
+	 * _.isObjectLike({});
+	 * // => true
+	 *
+	 * _.isObjectLike([1, 2, 3]);
+	 * // => true
+	 *
+	 * _.isObjectLike(_.noop);
+	 * // => false
+	 *
+	 * _.isObjectLike(null);
+	 * // => false
+	 */
+	function isObjectLike(value) {
+	  return !!value && typeof value == 'object';
+	}
+
+	module.exports = isObjectLike;
+
+
+/***/ },
+/* 12 */
+/***/ function(module, exports, __webpack_require__) {
+
+	module.exports = __webpack_require__(13);
+
+
+/***/ },
+/* 13 */
+/***/ function(module, exports, __webpack_require__) {
+
+	/* WEBPACK VAR INJECTION */(function(global) {'use strict';
+
+	Object.defineProperty(exports, "__esModule", {
+		value: true
+	});
+
+	var _ponyfill = __webpack_require__(14);
+
+	var _ponyfill2 = _interopRequireDefault(_ponyfill);
+
+	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+	var root = undefined; /* global window */
+
+	if (typeof global !== 'undefined') {
+		root = global;
+	} else if (typeof window !== 'undefined') {
+		root = window;
+	}
+
+	var result = (0, _ponyfill2['default'])(root);
+	exports['default'] = result;
+	/* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ },
+/* 14 */
+/***/ function(module, exports) {
+
+	'use strict';
+
+	Object.defineProperty(exports, "__esModule", {
+		value: true
+	});
+	exports['default'] = symbolObservablePonyfill;
+	function symbolObservablePonyfill(root) {
+		var result;
+		var _Symbol = root.Symbol;
+
+		if (typeof _Symbol === 'function') {
+			if (_Symbol.observable) {
+				result = _Symbol.observable;
+			} else {
+				result = _Symbol('observable');
+				_Symbol.observable = result;
+			}
+		} else {
+			result = '@@observable';
+		}
+
+		return result;
+	};
+
+/***/ }
+/******/ ])
+});
+;
--- a/browser/extensions/e10srollout/bootstrap.js
+++ b/browser/extensions/e10srollout/bootstrap.js
@@ -6,23 +6,29 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/UpdateUtils.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
- // The amount of people to be part of e10s
+// The amount of people to be part of e10s
 const TEST_THRESHOLD = {
   "beta": 0.9,  // 90%
   "release": 1.0,  // 100%
   "esr": 1.0,  // 100%
 };
 
+// If a user qualifies for the e10s-multi experiement, this is how many
+// content processes to use.
+const MULTI_BUCKETS = {
+  "beta": { 1: .5, 4: 1, },
+};
+
 const ADDON_ROLLOUT_POLICY = {
   "beta": "50allmpc",
   "release": "50allmpc",
   "esr": "esrA", // WebExtensions and Addons with mpc=true
 };
 
 if (AppConstants.RELEASE_OR_BETA) {
   // Bug 1348576 - e10s is never enabled for non-official release builds
@@ -115,24 +121,24 @@ function defineCohort() {
 
   let cohortPrefix = "";
   if (disqualified) {
     cohortPrefix = "disqualified-";
   } else if (hasNonExemptAddon) {
     cohortPrefix = `addons-set${addonPolicy}-`;
   }
 
-  let inMultiExperiment = false;
+  let eligibleForMulti = false;
   if (userOptedOut.e10s || userOptedOut.multi) {
     // If we detected that the user opted out either for multi or e10s, then
     // the proper prefs must already be set.
     setCohort("optedOut");
   } else if (userOptedIn.e10s) {
     setCohort("optedIn");
-    inMultiExperiment = true;
+    eligibleForMulti = true;
   } else if (temporaryDisqualification != "") {
     // Users who are disqualified by the backend (from multiprocessBlockPolicy)
     // can be put into either the test or control groups, because e10s will
     // still be denied by the backend, which is useful so that the E10S_STATUS
     // telemetry probe can be correctly set.
 
     // For these volatile disqualification reasons, however, we must not try
     // to activate e10s because the backend doesn't know about it. E10S_STATUS
@@ -142,56 +148,59 @@ function defineCohort() {
     Preferences.reset(PREF_E10S_PROCESSCOUNT + ".web");
   } else if (!disqualified && testThreshold < 1.0 &&
              temporaryQualification != "") {
     // Users who are qualified for e10s and on channels where some population
     // would not receive e10s can be pushed into e10s anyway via a temporary
     // qualification which overrides the user sample value when non-empty.
     setCohort(`temp-qualified-${temporaryQualification}`);
     Preferences.set(PREF_TOGGLE_E10S, true);
-    inMultiExperiment = true;
+    eligibleForMulti = true;
   } else if (testGroup) {
     setCohort(`${cohortPrefix}test`);
     Preferences.set(PREF_TOGGLE_E10S, true);
-    inMultiExperiment = true;
+    eligibleForMulti = true;
   } else {
     setCohort(`${cohortPrefix}control`);
     Preferences.reset(PREF_TOGGLE_E10S);
     Preferences.reset(PREF_E10S_PROCESSCOUNT + ".web");
   }
 
   // Now determine if this user should be in the e10s-multi experiment.
-  // - We only run the experiment on the beta channel.
+  // - We only run the experiment on channels defined in MULTI_BUCKETS.
   // - We decided above whether this user qualifies for the experiment.
   // - If the user already opted into multi, then their prefs are already set
   //   correctly, we're done.
   // - If the user has addons that disqualify them for multi, leave them with
   //   the default number of content processes (1 on beta) but still in the
   //   test cohort.
-  if (updateChannel !== "beta" ||
-      !inMultiExperiment ||
+  if (!(updateChannel in MULTI_BUCKETS) ||
+      !eligibleForMulti ||
       userOptedIn.multi ||
+      disqualified ||
       getAddonsDisqualifyForMulti()) {
     Preferences.reset(PREF_E10S_PROCESSCOUNT + ".web");
     return;
   }
 
+  // If we got here with a cohortPrefix, it must be "addons-set50allmpc-",
+  // and we know because of getAddonsDisqualifyForMulti that the addons that
+  // are installed must be web extensions.
+  if (cohortPrefix) {
+    cohortPrefix = "webextensions-";
+  }
+
   // The user is in the multi experiment!
   // Decide how many content processes to use for this user.
-  let BUCKETS = {
-    1: .25,
-    2: .5,
-    4: .75,
-    8: 1
-  };
+  let buckets = MULTI_BUCKETS[updateChannel];
 
   let multiUserSample = getUserSample(true);
-  for (let sampleName of Object.getOwnPropertyNames(BUCKETS)) {
-    if (multiUserSample < BUCKETS[sampleName]) {
-      setCohort(`multiBucket${sampleName}`);
+  for (let sampleName of Object.getOwnPropertyNames(buckets)) {
+    if (multiUserSample < buckets[sampleName]) {
+      setCohort(`${cohortPrefix}multiBucket${sampleName}`);
       Preferences.set(PREF_E10S_PROCESSCOUNT + ".web", sampleName);
       break;
     }
   }
 }
 
 function shutdown(data, reason) {
 }
--- a/browser/extensions/e10srollout/install.rdf.in
+++ b/browser/extensions/e10srollout/install.rdf.in
@@ -5,17 +5,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>e10srollout@mozilla.org</em:id>
-    <em:version>1.15</em:version>
+    <em:version>1.50</em:version>
     <em:type>2</em:type>
     <em:bootstrap>true</em:bootstrap>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
 
     <!-- Target Application this theme can install into,
         with minimum and maximum supported versions. -->
     <em:targetApplication>
       <Description>
--- a/browser/extensions/formautofill/content/manageProfiles.js
+++ b/browser/extensions/formautofill/content/manageProfiles.js
@@ -59,16 +59,18 @@ ManageProfileDialog.prototype = {
   /**
    * Load profiles and render them.
    *
    * @returns {promise}
    */
   loadProfiles() {
     return this.getProfiles().then(profiles => {
       log.debug("profiles:", profiles);
+      // Sort by last modified time starting with most recent
+      profiles.sort((a, b) => b.timeLastModified - a.timeLastModified);
       this.renderProfileElements(profiles);
       this.updateButtonsStates(this._selectedOptions.length);
     });
   },
 
   /**
    * Get profiles from storage.
    *
--- a/browser/locales/en-US/chrome/browser/aboutDialog.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutDialog.dtd
@@ -5,17 +5,17 @@
 
 <!-- LOCALIZATION NOTE (update.checkForUpdatesButton.*, update.updateButton.*):
 # Only one button is present at a time.
 # The button when displayed is located directly under the Firefox version in
 # the about dialog (see bug 596813 for screenshots).
 -->
 <!ENTITY update.checkForUpdatesButton.label       "Check for updates">
 <!ENTITY update.checkForUpdatesButton.accesskey   "C">
-<!ENTITY update.updateButton.label2               "Restart &brandShortName; to Update">
+<!ENTITY update.updateButton.label3               "Restart to update &brandShorterName;">
 <!ENTITY update.updateButton.accesskey            "R">
 
 
 <!-- LOCALIZATION NOTE (warningDesc.version): This is a warning about the experimental nature of Nightly and Aurora builds. It is only shown in those versions. -->
 <!ENTITY warningDesc.version        "&brandShortName; is experimental and may be unstable.">
 <!-- LOCALIZATION NOTE (warningDesc.telemetryDesc): This is a notification that Nightly/Aurora builds automatically send Telemetry data back to Mozilla. It is only shown in those versions. "It" refers to brandShortName. -->
 <!ENTITY warningDesc.telemetryDesc  "It automatically sends information about performance, hardware, usage and customizations back to &vendorShortName; to help make &brandShortName; better.">
 
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -915,14 +915,14 @@ you can use these alternative items. Oth
 <!ENTITY updateManual.header.message "&brandShorterName; can’t update to the latest version.">
 <!ENTITY updateManual.acceptButton.label "Download &brandShorterName;">
 <!ENTITY updateManual.acceptButton.accesskey "D">
 <!ENTITY updateManual.cancelButton.label "Not Now">
 <!ENTITY updateManual.cancelButton.accesskey "N">
 <!ENTITY updateManual.panelUI.label "Download a fresh copy of &brandShorterName;">
 
 <!ENTITY updateRestart.message "After a quick restart, &brandShorterName; will restore all your open tabs and windows.">
-<!ENTITY updateRestart.header.message "Restart &brandShorterName; to apply the update.">
+<!ENTITY updateRestart.header.message2 "Restart to update &brandShorterName;.">
 <!ENTITY updateRestart.acceptButton.label "Restart and Restore">
 <!ENTITY updateRestart.acceptButton.accesskey "R">
 <!ENTITY updateRestart.cancelButton.label "Not Now">
 <!ENTITY updateRestart.cancelButton.accesskey "N">
 <!ENTITY updateRestart.panelUI.label "Restart &brandShorterName; to apply the update">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -758,21 +758,16 @@ flashHang.helpButton.accesskey = L
 # be replaced with a hyperlink containing the text defined in customizeTips.tip0.learnMore.
 customizeTips.tip0 = %1$S: You can customize %2$S to work the way you do. Simply drag any of the above to the menu or toolbar. %3$S about customizing %2$S.
 customizeTips.tip0.hint = Hint
 customizeTips.tip0.learnMore = Learn more
 
 # LOCALIZATION NOTE (customizeMode.tabTitle): %S is brandShortName
 customizeMode.tabTitle = Customize %S
 
-# LOCALIZATION NOTE : FILE Reader View is a feature name and therefore typically used as a proper noun.
-
-readingList.promo.firstUse.readerView.title = Reader View
-readingList.promo.firstUse.readerView.body = Remove clutter so you can focus exactly on what you want to read.
-
 # LOCALIZATION NOTE (appMenuRemoteTabs.mobilePromo.text2):
 # %1$S will be replaced with a link, the text of which is
 # appMenuRemoteTabs.mobilePromo.android and the link will be to
 # https://www.mozilla.org/firefox/android/.
 # %2$S will be replaced with a link, the text of which is
 # appMenuRemoteTabs.mobilePromo.ios
 # and the link will be to https://www.mozilla.org/firefox/ios/.
 appMenuRemoteTabs.mobilePromo.text2 = Download %1$S or %2$S and connect them to your Firefox Account.
--- a/browser/locales/shipped-locales
+++ b/browser/locales/shipped-locales
@@ -1,15 +1,16 @@
 ach
 af
 an
 ar
 as
 ast
 az
+be
 bg
 bn-BD
 bn-IN
 br
 bs
 ca
 cak
 cs
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/modules/ContentLinkHandler.jsm
@@ -129,17 +129,17 @@ this.ContentLinkHandler = {
           }
           break;
       }
     }
   },
 
   getLinkIconURI(aLink) {
     let targetDoc = aLink.ownerDocument;
-    var uri = BrowserUtils.makeURI(aLink.href, targetDoc.characterSet);
+    var uri = Services.io.newURI(aLink.href, targetDoc.characterSet);
     try {
       uri.userPass = "";
     } catch (e) {
       // some URIs are immutable
     }
     return uri;
   },
 };
--- a/browser/modules/ReaderParent.jsm
+++ b/browser/modules/ReaderParent.jsm
@@ -15,18 +15,16 @@ Cu.import("resource://gre/modules/Task.j
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITour", "resource:///modules/UITour.jsm");
 
 const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
 
 var ReaderParent = {
-  _readerModeInfoPanelOpen: false,
-
   MESSAGES: [
     "Reader:ArticleGet",
     "Reader:FaviconRequest",
     "Reader:UpdateReaderButton",
   ],
 
   init() {
     let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
@@ -103,30 +101,16 @@ var ReaderParent = {
       button.hidden = !browser.isArticle;
       let enterText = gStringBundle.GetStringFromName("readerView.enter");
       button.setAttribute("tooltiptext", enterText);
       command.setAttribute("label", enterText);
       command.setAttribute("hidden", !browser.isArticle);
       command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.enter.accesskey"));
       key.setAttribute("disabled", !browser.isArticle);
     }
-
-    let currentUriHost = browser.currentURI && browser.currentURI.asciiHost;
-    if (browser.isArticle &&
-        !Services.prefs.getBoolPref("browser.reader.detectedFirstArticle") &&
-        currentUriHost && !currentUriHost.endsWith("mozilla.org")) {
-      this.showReaderModeInfoPanel(browser);
-      Services.prefs.setBoolPref("browser.reader.detectedFirstArticle", true);
-      this._readerModeInfoPanelOpen = true;
-    } else if (this._readerModeInfoPanelOpen) {
-      if (UITour.isInfoOnTarget(win, "readerMode-urlBar")) {
-        UITour.hideInfo(win);
-      }
-      this._readerModeInfoPanelOpen = false;
-    }
   },
 
   forceShowReaderIcon(browser) {
     browser.isArticle = true;
     this.updateReaderButton(browser);
   },
 
   buttonClick(event) {
@@ -138,39 +122,16 @@ var ReaderParent = {
 
   toggleReaderMode(event) {
     let win = event.target.ownerGlobal;
     let browser = win.gBrowser.selectedBrowser;
     browser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
   },
 
   /**
-   * Shows an info panel from the UITour for Reader Mode.
-   *
-   * @param browser The <browser> that the tour should be started for.
-   */
-  showReaderModeInfoPanel(browser) {
-    let win = browser.ownerGlobal;
-    let targetPromise = UITour.getTarget(win, "readerMode-urlBar");
-    targetPromise.then(target => {
-      let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
-      let icon = "chrome://browser/skin/";
-      if (win.devicePixelRatio > 1) {
-        icon += "reader-tour@2x.png";
-      } else {
-        icon += "reader-tour.png";
-      }
-      UITour.showInfo(win, target,
-                      browserBundle.GetStringFromName("readingList.promo.firstUse.readerView.title"),
-                      browserBundle.GetStringFromName("readingList.promo.firstUse.readerView.body"),
-                      icon);
-    });
-  },
-
-  /**
    * Gets an article for a given URL. This method will download and parse a document.
    *
    * @param url The article URL.
    * @param browser The browser where the article is currently loaded.
    * @return {Promise}
    * @resolves JS object representing the article, or null if no article is found.
    */
   _getArticle: Task.async(function* (url, browser) {
--- a/browser/modules/test/browser/browser.ini
+++ b/browser/modules/test/browser/browser.ini
@@ -2,16 +2,17 @@
 support-files =
   head.js
 
 [browser_BrowserUITelemetry_buckets.js]
 [browser_BrowserUITelemetry_defaults.js]
 [browser_BrowserUITelemetry_sidebar.js]
 [browser_BrowserUITelemetry_syncedtabs.js]
 [browser_ContentSearch.js]
+skip-if = (os == "mac" || os == "linux") # Bug 1308343
 support-files =
   contentSearch.js
   contentSearchBadImage.xml
   contentSearchSuggestions.sjs
   contentSearchSuggestions.xml
   !/browser/components/search/test/head.js
   !/browser/components/search/test/testEngine.xml
 [browser_NetworkPrioritizer.js]
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -1793,16 +1793,8 @@ menuitem[checked="true"].subviewbutton >
   #PanelUI-panic-actionlist-newwindow {
     background-image: -moz-image-rect(url(chrome://browser/skin/panic-panel/icons@2x.png), 0, 128, 32, 96);
   }
 }
 
 .subviewbutton-iconic > .toolbarbutton-text {
   padding-inline-start: 5px;
 }
-
-#appMenu-new-window-button {
-  list-style-image: url(chrome://browser/skin/menu-icons/new-window.svg);
-}
-
-#appMenu-private-window-button {
-  list-style-image: url(chrome://browser/skin/menu-icons/private-window.svg);
-}
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -120,18 +120,16 @@
   skin/classic/browser/warning-white.svg                       (../shared/warning-white.svg)
   skin/classic/browser/cert-error.svg                          (../shared/incontent-icons/cert-error.svg)
   skin/classic/browser/wifi.svg                                (../shared/incontent-icons/wifi.svg)
   skin/classic/browser/session-restore.svg                     (../shared/incontent-icons/session-restore.svg)
   skin/classic/browser/tab-crashed.svg                         (../shared/incontent-icons/tab-crashed.svg)
   skin/classic/browser/favicon-search-16.svg                   (../shared/favicon-search-16.svg)
   skin/classic/browser/icon-search-64.svg                      (../shared/incontent-icons/icon-search-64.svg)
   skin/classic/browser/welcome-back.svg                        (../shared/incontent-icons/welcome-back.svg)
-  skin/classic/browser/reader-tour.png                         (../shared/reader/reader-tour.png)
-  skin/classic/browser/reader-tour@2x.png                      (../shared/reader/reader-tour@2x.png)
   skin/classic/browser/readerMode.svg                          (../shared/reader/readerMode.svg)
   skin/classic/browser/panic-panel/header.png                  (../shared/panic-panel/header.png)
   skin/classic/browser/panic-panel/header@2x.png               (../shared/panic-panel/header@2x.png)
   skin/classic/browser/panic-panel/header-small.png            (../shared/panic-panel/header-small.png)
   skin/classic/browser/panic-panel/header-small@2x.png         (../shared/panic-panel/header-small@2x.png)
   skin/classic/browser/panic-panel/icons.png                   (../shared/panic-panel/icons.png)
   skin/classic/browser/panic-panel/icons@2x.png                (../shared/panic-panel/icons@2x.png)
   skin/classic/browser/privatebrowsing/aboutPrivateBrowsing.css (../shared/privatebrowsing/aboutPrivateBrowsing.css)
@@ -141,9 +139,10 @@
   skin/classic/browser/privatebrowsing/tracking-protection-off.svg (../shared/privatebrowsing/tracking-protection-off.svg)
   skin/classic/browser/privatebrowsing/tracking-protection.svg (../shared/privatebrowsing/tracking-protection.svg)
   skin/classic/browser/compacttheme/loading-inverted.png (../shared/compacttheme/loading-inverted.png)
   skin/classic/browser/compacttheme/loading-inverted@2x.png (../shared/compacttheme/loading-inverted@2x.png)
   skin/classic/browser/compacttheme/urlbar-history-dropmarker.svg (../shared/compacttheme/urlbar-history-dropmarker.svg)
   skin/classic/browser/urlbar-star.svg                         (../shared/urlbar-star.svg)
   skin/classic/browser/urlbar-tab.svg                          (../shared/urlbar-tab.svg)
   skin/classic/browser/menu-icons/new-window.svg               (../shared/menu-icons/new-window.svg)
+  skin/classic/browser/menu-icons/print.svg                    (../shared/menu-icons/print.svg)
   skin/classic/browser/menu-icons/private-window.svg           (../shared/menu-icons/private-window.svg)
--- a/browser/themes/shared/menu-icons/new-window.svg
+++ b/browser/themes/shared/menu-icons/new-window.svg
@@ -1,3 +1,6 @@
+<!-- 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/. -->
 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
   <path fill="context-fill" d="M14 1H2a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h5a1 1 0 0 0 0-2H3a1 1 0 0 1-1-1V6h12v2a1 1 0 0 0 2 0V3a2 2 0 0 0-2-2zm0 4H2V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1zm1.5 7H13V9.5a.5.5 0 1 0-1 0V12H9.5a.5.5 0 0 0 0 1H12v2.5a.5.5 0 0 0 1 0V13h2.5a.5.5 0 0 0 0-1z"/>
 </svg>
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/menu-icons/print.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+  <path fill="context-fill" d="M14 5h-1V1a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v4H2a2 2 0 0 0-2 2v5h3v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-3h3V7a2 2 0 0 0-2-2zM2.5 8a.5.5 0 1 1 .5-.5.5.5 0 0 1-.5.5zm9.5 7H4v-5h8zm0-10H4V1h8zm-6.5 7h4a.5.5 0 0 0 0-1h-4a.5.5 0 1 0 0 1zm0 2h5a.5.5 0 0 0 0-1h-5a.5.5 0 1 0 0 1z"/>
+</svg>
--- a/browser/themes/shared/menu-icons/private-window.svg
+++ b/browser/themes/shared/menu-icons/private-window.svg
@@ -1,3 +1,6 @@
+<!-- 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/. -->
 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
   <path fill="context-fill" d="M12.377 11.961c-1.651 0-2.793-1.98-4.377-1.98s-2.824 1.98-4.377 1.98c-2.037 0-3.541-1.924-3.566-5.221-.015-2.047.6-2.7 3.242-2.7S6.719 5.12 8 5.12s2.056-1.08 4.7-1.08 3.257.653 3.242 2.7c-.024 3.297-1.528 5.221-3.565 5.221zM4.6 6.56c-1.607.07-2.269 1.025-2.269 1.26s1.066.9 2.107.9S6.7 8.339 6.7 8a1.889 1.889 0 0 0-2.1-1.44zm6.808 0A1.889 1.889 0 0 0 9.3 8c0 .339 1.228.72 2.269.72s2.107-.665 2.107-.9-.664-1.191-2.276-1.26z"/>
 </svg>
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -176,8 +176,20 @@ toolbarpaletteitem[place="palette"] > #z
   -moz-image-region: rect(0px, 96px, 16px, 80px);
 }
 
 #add-share-provider {
   list-style-image: url(chrome://browser/skin/menuPanel-small.svg);
   -moz-image-region: rect(0px, 96px, 16px, 80px);
 }
 
+
+#appMenu-new-window-button {
+  list-style-image: url(chrome://browser/skin/menu-icons/new-window.svg);
+}
+
+#appMenu-private-window-button {
+  list-style-image: url(chrome://browser/skin/menu-icons/private-window.svg);
+}
+
+#appMenu-print-button {
+  list-style-image: url(chrome://browser/skin/menu-icons/print.svg);
+}
deleted file mode 100644
index be346b3847928f94d7a0bbca019f40d86edd0899..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 1a60d93ca9311f8bf9e0ce3f6f175370a69f648e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm
@@ -12,51 +12,72 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://testing-common/TestUtils.jsm");
 Cu.import("resource://testing-common/ContentTask.jsm");
 
 this.Preferences = {
 
   init(libDir) {
     let panes = [
-      ["paneGeneral", null],
-      ["paneSearch", null],
-      ["paneContent", null],
-      ["paneApplications", null],
-      ["panePrivacy", null],
-      ["panePrivacy", null, DNTDialog],
-      ["panePrivacy", null, clearRecentHistoryDialog],
-      ["paneSecurity", null],
-      ["paneSync", null],
-      ["paneAdvanced", "generalTab"],
-      ["paneAdvanced", "dataChoicesTab"],
-      ["paneAdvanced", "networkTab"],
-      ["paneAdvanced", "networkTab", connectionDialog],
-      ["paneAdvanced", "updateTab"],
-      ["paneAdvanced", "encryptionTab"],
-      ["paneAdvanced", "encryptionTab", certManager],
-      ["paneAdvanced", "encryptionTab", deviceManager],
+      /* The "new" organization */
+      ["paneGeneral"],
+      ["paneGeneral", scrollToBrowsingGroup],
+      ["paneApplications"],
+      ["paneSync"],
+      ["panePrivacy"],
+      ["panePrivacy", scrollToCacheGroup],
+      ["panePrivacy", DNTDialog],
+      ["panePrivacy", clearRecentHistoryDialog],
+      ["panePrivacy", connectionDialog],
+      ["panePrivacy", certManager],
+      ["panePrivacy", deviceManager],
+      ["paneAdvanced"],
+
+      /* The "old" organization. The third argument says to
+         set the pref to show the old organization when
+         opening the preferences. */
+      ["paneGeneral", null, true],
+      ["paneSearch", null, true],
+      ["paneContent", null, true],
+      ["paneApplications", null, true],
+      ["panePrivacy", null, true],
+      ["panePrivacy", DNTDialog, true],
+      ["panePrivacy", clearRecentHistoryDialog, true],
+      ["paneSecurity", null, true],
+      ["paneSync", null, true],
+      ["paneAdvanced", null, true, "generalTab"],
+      ["paneAdvanced", null, true, "dataChoicesTab"],
+      ["paneAdvanced", null, true, "networkTab"],
+      ["paneAdvanced", connectionDialog, true, "networkTab"],
+      ["paneAdvanced", null, true, "updateTab"],
+      ["paneAdvanced", null, true, "encryptionTab"],
+      ["paneAdvanced", certManager, true, "encryptionTab"],
+      ["paneAdvanced", deviceManager, true, "encryptionTab"],
     ];
-    for (let [primary, advanced, customFn] of panes) {
+    for (let [primary, customFn, useOldOrg, advanced] of panes) {
       let configName = primary.replace(/^pane/, "prefs") + (advanced ? "-" + advanced : "");
       if (customFn) {
         configName += "-" + customFn.name;
       }
       this.configurations[configName] = {};
-      this.configurations[configName].applyConfig = prefHelper.bind(null, primary, advanced, customFn);
+      this.configurations[configName].applyConfig = prefHelper.bind(null, primary, customFn, useOldOrg, advanced);
     }
   },
 
   configurations: {},
 };
 
-let prefHelper = Task.async(function*(primary, advanced = null, customFn = null) {
+let prefHelper = Task.async(function*(primary, customFn = null, useOldOrg = false, advanced = null) {
   let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
   let selectedBrowser = browserWindow.gBrowser.selectedBrowser;
 
+  if (useOldOrg) {
+    Services.prefs.setBoolPref("browser.preferences.useOldOrganization", !!useOldOrg);
+  }
+
   // close any dialog that might still be open
   yield ContentTask.spawn(selectedBrowser, null, function*() {
     if (!content.window.gSubDialog) {
       return;
     }
     content.window.gSubDialog.close();
   });
 
@@ -67,39 +88,53 @@ let prefHelper = Task.async(function*(pr
       readyPromise = Promise.resolve();
     } else {
       readyPromise = paintPromise(browserWindow);
     }
   } else {
     readyPromise = TestUtils.topicObserved("advanced-pane-loaded");
   }
 
-  if (primary == "paneAdvanced") {
+  if (useOldOrg && primary == "paneAdvanced") {
     browserWindow.openAdvancedPreferences(advanced);
   } else {
     browserWindow.openPreferences(primary);
   }
 
   yield readyPromise;
 
   if (customFn) {
     let customPaintPromise = paintPromise(browserWindow);
     yield* customFn(selectedBrowser);
     yield customPaintPromise;
   }
+
+  Services.prefs.clearUserPref("browser.preferences.useOldOrganization");
 });
 
 function paintPromise(browserWindow) {
   return new Promise((resolve) => {
     browserWindow.addEventListener("MozAfterPaint", function() {
       resolve();
     }, {once: true});
   });
 }
 
+function* scrollToBrowsingGroup(aBrowser) {
+  yield ContentTask.spawn(aBrowser, null, function* () {
+    content.document.getElementById("browsingGroup").scrollIntoView();
+  });
+}
+
+function* scrollToCacheGroup(aBrowser) {
+  yield ContentTask.spawn(aBrowser, null, function* () {
+    content.document.getElementById("cacheGroup").scrollIntoView();
+  });
+}
+
 function* DNTDialog(aBrowser) {
   yield ContentTask.spawn(aBrowser, null, function* () {
     content.document.getElementById("doNotTrackSettings").click();
   });
 }
 
 function* connectionDialog(aBrowser) {
   yield ContentTask.spawn(aBrowser, null, function* () {
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -969,8 +969,21 @@ set_config('VISIBILITY_FLAGS', visibilit
 
 # We only want to include windows.configure when we are compiling on
 # Windows, for Windows.
 @depends(target, host)
 def is_windows(target, host):
     return host.kernel == 'WINNT' and target.kernel == 'WINNT'
 
 include('windows.configure', when=is_windows)
+
+# Security Hardening
+# ==============================================================
+
+option('--enable-hardening', env='MOZ_SECURITY_HARDENING',
+       help='Enables security hardening compiler options')
+
+@depends('--enable-hardening', c_compiler)
+def security_hardening_cflags(value, c_compiler):
+    if value and c_compiler.type in ['gcc', 'clang']:
+        return '-fstack-protector-strong'
+
+add_old_configure_assignment('HARDENING_CFLAGS', security_hardening_cflags)
--- a/chrome/nsChromeRegistry.cpp
+++ b/chrome/nsChromeRegistry.cpp
@@ -50,23 +50,22 @@ nsChromeRegistry::LogMessage(const char*
 {
   nsCOMPtr<nsIConsoleService> console 
     (do_GetService(NS_CONSOLESERVICE_CONTRACTID));
   if (!console)
     return;
 
   va_list args;
   va_start(args, aMsg);
-  char* formatted = mozilla::Vsmprintf(aMsg, args);
+  mozilla::SmprintfPointer formatted = mozilla::Vsmprintf(aMsg, args);
   va_end(args);
   if (!formatted)
     return;
 
-  console->LogStringMessage(NS_ConvertUTF8toUTF16(formatted).get());
-  mozilla::SmprintfFree(formatted);
+  console->LogStringMessage(NS_ConvertUTF8toUTF16(formatted.get()).get());
 }
 
 void
 nsChromeRegistry::LogMessageWithContext(nsIURI* aURL, uint32_t aLineNumber, uint32_t flags,
                                         const char* aMsg, ...)
 {
   nsresult rv;
 
@@ -75,30 +74,29 @@ nsChromeRegistry::LogMessageWithContext(
 
   nsCOMPtr<nsIScriptError> error
     (do_CreateInstance(NS_SCRIPTERROR_CONTRACTID));
   if (!console || !error)
     return;
 
   va_list args;
   va_start(args, aMsg);
-  char* formatted = mozilla::Vsmprintf(aMsg, args);
+  mozilla::SmprintfPointer formatted = mozilla::Vsmprintf(aMsg, args);
   va_end(args);
   if (!formatted)
     return;
 
   nsCString spec;
   if (aURL)
     aURL->GetSpec(spec);
 
-  rv = error->Init(NS_ConvertUTF8toUTF16(formatted),
+  rv = error->Init(NS_ConvertUTF8toUTF16(formatted.get()),
                    NS_ConvertUTF8toUTF16(spec),
                    EmptyString(),
                    aLineNumber, 0, flags, "chrome registration");
-  mozilla::SmprintfFree(formatted);
 
   if (NS_FAILED(rv))
     return;
 
   console->LogMessage(error);
 }
 
 nsChromeRegistry::~nsChromeRegistry()
--- a/chrome/nsChromeRegistryChrome.cpp
+++ b/chrome/nsChromeRegistryChrome.cpp
@@ -735,17 +735,17 @@ nsChromeRegistryChrome::ManifestLocale(M
     ChromePackageFromPackageEntry(packageName, entry, &chromePackage,
                                   mSelectedSkin);
     SendManifestEntry(chromePackage);
   }
 
   if (strcmp(package, "global") == 0) {
     // We should refresh the LocaleService, since the available
     // locales changed.
-    LocaleService::GetInstance()->Refresh();
+    LocaleService::GetInstance()->OnAvailableLocalesChanged();
   }
 }
 
 void
 nsChromeRegistryChrome::ManifestSkin(ManifestProcessingContext& cx, int lineno,
                                      char *const * argv, int flags)
 {
   char* package = argv[0];
--- a/devtools/client/framework/test/browser_toolbox_options.js
+++ b/devtools/client/framework/test/browser_toolbox_options.js
@@ -100,25 +100,16 @@ function* testOptionsShortcut() {
   is(toolbox.currentToolId, "webconsole", "webconsole is reselected (2)");
   synthesizeKeyShortcut(L10N.getStr("toolbox.help.key"));
   is(toolbox.currentToolId, "options", "Toolbox selected via shortcut key (2)");
 }
 
 function* testOptions() {
   let tool = toolbox.getPanel("options");
   panelWin = tool.panelWin;
-
-  // It's possible that the iframe for options hasn't fully loaded yet,
-  // and might be paint-suppressed, which means that clicking things
-  // might not work just yet. The "load" event is a good indication that
-  // we're ready to proceed.
-  if (tool.panelDoc.readyState != "complete") {
-    yield once(tool.panelWin, "load");
-  }
-
   let prefNodes = tool.panelDoc.querySelectorAll(
     "input[type=checkbox][data-pref]");
 
   // Store modified pref names so that they can be cleared on error.
   for (let node of tool.panelDoc.querySelectorAll("[data-pref]")) {
     let pref = node.getAttribute("data-pref");
     modifiedPrefs.push(pref);
   }
--- a/devtools/client/framework/test/browser_toolbox_races.js
+++ b/devtools/client/framework/test/browser_toolbox_races.js
@@ -1,15 +1,18 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
+// Toggling the toolbox three time can take more than 45s on slow test machine
+requestLongerTimeout(2);
+
 // Test toggling the toolbox quickly and see if there is any race breaking it.
 
 const URL = "data:text/html;charset=utf-8,Toggling devtools quickly";
 
 add_task(function* () {
   // Make sure this test starts with the selectedTool pref cleared. Previous
   // tests select various tools, and that sets this pref.
   Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
--- a/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_pseudo-element.js
@@ -1,15 +1,15 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test that the box model displays the right values for a pseduo-element.
+// Test that the box model displays the right values for a pseudo-element.
 
 const TEST_URI = `
   <style type='text/css'>
     div {
       box-sizing: border-box;
       display: block;
       float: left;
       line-height: 20px;
@@ -48,25 +48,25 @@ const res1 = [
     value: "32"
   },
   {
     selector: ".boxmodel-margin.boxmodel-top > span",
     value: 0
   },
   {
     selector: ".boxmodel-margin.boxmodel-left > span",
-    value: "auto"
+    value: 0
   },
   {
     selector: ".boxmodel-margin.boxmodel-bottom > span",
     value: 6
   },
   {
     selector: ".boxmodel-margin.boxmodel-right > span",
-    value: "auto"
+    value: 0
   },
   {
     selector: ".boxmodel-padding.boxmodel-top > span",
     value: 0
   },
   {
     selector: ".boxmodel-padding.boxmodel-left > span",
     value: 0
--- a/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js
+++ b/devtools/client/inspector/computed/test/browser_computed_pseudo-element_01.js
@@ -28,12 +28,12 @@ function* testTopLeft(inspector, view) {
   let top = getComputedViewPropertyValue(view, "top");
   is(top, "0px", "The computed view shows the correct top");
   let left = getComputedViewPropertyValue(view, "left");
   is(left, "0px", "The computed view shows the correct left");
 
   let afterElement = children.nodes[children.nodes.length - 1];
   yield selectNode(afterElement, inspector);
   top = getComputedViewPropertyValue(view, "top");
-  is(top, "50%", "The computed view shows the correct top");
+  is(top, "96px", "The computed view shows the correct top");
   left = getComputedViewPropertyValue(view, "left");
-  is(left, "50%", "The computed view shows the correct left");
+  is(left, "96px", "The computed view shows the correct left");
 }
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -118,17 +118,19 @@ devtools.jar:
     skin/images/filters.svg (themes/images/filters.svg)
     skin/images/filter-swatch.svg (themes/images/filter-swatch.svg)
     skin/images/grid.svg (themes/images/grid.svg)
     skin/images/angle-swatch.svg (themes/images/angle-swatch.svg)
     skin/images/pseudo-class.svg (themes/images/pseudo-class.svg)
     skin/images/controls.png (themes/images/controls.png)
     skin/images/controls@2x.png (themes/images/controls@2x.png)
     skin/images/animation-fast-track.svg (themes/images/animation-fast-track.svg)
-    skin/images/performance-icons.svg (themes/images/performance-icons.svg)
+    skin/images/performance-details-waterfall.svg (themes/images/performance-details-waterfall.svg)
+    skin/images/performance-details-call-tree.svg (themes/images/performance-details-call-tree.svg)
+    skin/images/performance-details-flamegraph.svg (themes/images/performance-details-flamegraph.svg)
     skin/widgets.css (themes/widgets.css)
     skin/images/power.svg (themes/images/power.svg)
     skin/images/filetypes/dir-close.svg (themes/images/filetypes/dir-close.svg)
     skin/images/filetypes/dir-open.svg (themes/images/filetypes/dir-open.svg)
     skin/images/filetypes/globe.svg (themes/images/filetypes/globe.svg)
     skin/images/commandline-icon.svg (themes/images/commandline-icon.svg)
     skin/images/alerticon-warning.png (themes/images/alerticon-warning.png)
     skin/images/alerticon-warning@2x.png (themes/images/alerticon-warning@2x.png)
@@ -233,17 +235,18 @@ devtools.jar:
     skin/images/vview-delete.png (themes/images/vview-delete.png)
     skin/images/vview-delete@2x.png (themes/images/vview-delete@2x.png)
     skin/images/vview-edit.png (themes/images/vview-edit.png)
     skin/images/vview-edit@2x.png (themes/images/vview-edit@2x.png)
     skin/images/vview-lock.png (themes/images/vview-lock.png)
     skin/images/vview-lock@2x.png (themes/images/vview-lock@2x.png)
     skin/images/vview-open-inspector.png (themes/images/vview-open-inspector.png)
     skin/images/vview-open-inspector@2x.png (themes/images/vview-open-inspector@2x.png)
-    skin/images/sort-arrows.svg (themes/images/sort-arrows.svg)
+    skin/images/sort-ascending-arrow.svg (themes/images/sort-ascending-arrow.svg)
+    skin/images/sort-descending-arrow.svg (themes/images/sort-descending-arrow.svg)
     skin/images/cubic-bezier-swatch.png (themes/images/cubic-bezier-swatch.png)
     skin/images/cubic-bezier-swatch@2x.png (themes/images/cubic-bezier-swatch@2x.png)
     skin/fonts.css (themes/fonts.css)
     skin/computed.css (themes/computed.css)
     skin/layout.css (themes/layout.css)
     skin/images/arrow-e.png (themes/images/arrow-e.png)
     skin/images/arrow-e@2x.png (themes/images/arrow-e@2x.png)
     skin/projecteditor/projecteditor.css (themes/projecteditor/projecteditor.css)
--- a/devtools/client/netmonitor/src/assets/styles/netmonitor.css
+++ b/devtools/client/netmonitor/src/assets/styles/netmonitor.css
@@ -19,33 +19,33 @@
 
   --timing-blocked-color: rgba(235, 83, 104, 0.8);
   --timing-dns-color: rgba(223, 128, 255, 0.8); /* pink */
   --timing-connect-color: rgba(217, 102, 41, 0.8); /* orange */
   --timing-send-color: rgba(70, 175, 227, 0.8); /* light blue */
   --timing-wait-color: rgba(94, 136, 176, 0.8); /* blue grey */
   --timing-receive-color: rgba(112, 191, 83, 0.8); /* green */
 
-  --sort-ascending-image: url(chrome://devtools/skin/images/sort-arrows.svg#ascending);
-  --sort-descending-image: url(chrome://devtools/skin/images/sort-arrows.svg#descending);
+  --sort-ascending-image: url(chrome://devtools/skin/images/sort-ascending-arrow.svg);
+  --sort-descending-image: url(chrome://devtools/skin/images/sort-descending-arrow.svg);
 }
 
 :root.theme-light {
   --table-splitter-color: rgba(0,0,0,0.15);
   --table-zebra-background: rgba(0,0,0,0.05);
 
   --timing-blocked-color: rgba(235, 83, 104, 0.8);
   --timing-dns-color: rgba(223, 128, 255, 0.8); /* pink */
   --timing-connect-color: rgba(217, 102, 41, 0.8); /* orange */
   --timing-send-color: rgba(0, 136, 204, 0.8); /* blue */
   --timing-wait-color: rgba(95, 136, 176, 0.8); /* blue grey */
   --timing-receive-color: rgba(44, 187, 15, 0.8); /* green */
 
-  --sort-ascending-image: url(chrome://devtools/skin/images/sort-arrows.svg#ascending);
-  --sort-descending-image: url(chrome://devtools/skin/images/sort-arrows.svg#descending);
+  --sort-ascending-image: url(chrome://devtools/skin/images/sort-ascending-arrow.svg);
+  --sort-descending-image: url(chrome://devtools/skin/images/sort-descending-arrow.svg);
 }
 
 :root.theme-firebug {
   --sort-ascending-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
   --sort-descending-image: url(chrome://devtools/skin/images/firebug/arrow-down.svg);
 }
 
 /* General */
--- a/devtools/client/netmonitor/src/netmonitor-controller.js
+++ b/devtools/client/netmonitor/src/netmonitor-controller.js
@@ -289,16 +289,17 @@ var NetMonitorController = {
     }
   },
 };
 
 /**
  * Functions handling target network events.
  */
 function NetworkEventsHandler() {
+  this.payloadQueue = [];
   this.addRequest = this.addRequest.bind(this);
   this.updateRequest = this.updateRequest.bind(this);
   this._onNetworkEvent = this._onNetworkEvent.bind(this);
   this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
   this._onDocLoadingMarker = this._onDocLoadingMarker.bind(this);
   this._onRequestHeaders = this._onRequestHeaders.bind(this);
   this._onRequestCookies = this._onRequestCookies.bind(this);
   this._onRequestPostData = this._onRequestPostData.bind(this);
@@ -522,16 +523,37 @@ NetworkEventsHandler.prototype = {
         if (reqCookies.length) {
           payload.requestCookies = reqCookies;
         }
       }
     }
     return payload;
   },
 
+  getPayloadFromQueue(id) {
+    return this.payloadQueue.find((item) => item.id === id);
+  },
+
+  // Packet order of "networkUpdateEvent" is predictable, as a result we can wait for
+  // the last one "eventTimings" packet arrives to check payload is ready
+  isQueuePayloadReady(id) {
+    let queuedPayload = this.getPayloadFromQueue(id);
+    return queuedPayload && queuedPayload.payload.eventTimings;
+  },
+
+  pushPayloadToQueue(id, payload) {
+    let queuedPayload = this.getPayloadFromQueue(id);
+    if (!queuedPayload) {
+      this.payloadQueue.push({ id, payload });
+    } else {
+      // Merge upcoming networkEventUpdate payload into existing one
+      queuedPayload.payload = Object.assign({}, queuedPayload.payload, payload);
+    }
+  },
+
   async updateRequest(id, data) {
     let {
       mimeType,
       responseContent,
       responseCookies,
       responseHeaders,
       requestCookies,
       requestHeaders,
@@ -553,17 +575,22 @@ NetworkEventsHandler.prototype = {
       this.fetchPostData(requestPostData),
       this.fetchRequestCookies(requestCookies),
       this.fetchResponseCookies(responseCookies),
     ]);
 
     let payload = Object.assign({}, data,
                                     imageObj, requestHeadersObj, responseHeadersObj,
                                     postDataObj, requestCookiesObj, responseCookiesObj);
-    await this.actions.updateRequest(id, payload, true);
+
+    this.pushPayloadToQueue(id, payload);
+
+    if (this.isQueuePayloadReady(id)) {
+      await this.actions.updateRequest(id, this.getPayloadFromQueue(id).payload, true);
+    }
   },
 
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param string type
    *        Message type.
    * @param object packet
--- a/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
+++ b/devtools/client/netmonitor/test/browser_net_security-tab-visibility.js
@@ -63,16 +63,17 @@ add_task(function* () {
        "Security state has not yet arrived.");
     is(!!document.querySelector("#security-tab"), testcase.visibleOnNewEvent,
       "Security tab is " + (testcase.visibleOnNewEvent ? "visible" : "hidden") +
       " after new request was added to the menu.");
 
     info("Waiting for security information to arrive.");
     yield onSecurityInfo;
 
+    yield waitUntil(() => !!getSelectedRequest(gStore.getState()).securityState);
     ok(getSelectedRequest(gStore.getState()).securityState,
        "Security state arrived.");
     is(!!document.querySelector("#security-tab"), testcase.visibleOnSecurityInfo,
        "Security tab is " + (testcase.visibleOnSecurityInfo ? "visible" : "hidden") +
        " after security information arrived.");
 
     info("Waiting for request to complete.");
     yield onComplete;
--- a/devtools/client/netmonitor/test/browser_net_simple-request-data.js
+++ b/devtools/client/netmonitor/test/browser_net_simple-request-data.js
@@ -86,18 +86,24 @@ function test() {
         document,
         getDisplayedRequests(gStore.getState()),
         requestItem,
         "GET",
         SIMPLE_SJS
       );
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_REQUEST_HEADERS, () => {
+    monitor.panelWin.once(EVENTS.RECEIVED_REQUEST_HEADERS, async () => {
+      await waitUntil(() => {
+        let requestItem = getSortedRequests(gStore.getState()).get(0);
+        return requestItem.requestHeaders;
+      });
+
       let requestItem = getSortedRequests(gStore.getState()).get(0);
+
       ok(requestItem.requestHeaders,
         "There should be a requestHeaders data available.");
       is(requestItem.requestHeaders.headers.length, 10,
         "The requestHeaders data has an incorrect |headers| property.");
       isnot(requestItem.requestHeaders.headersSize, 0,
         "The requestHeaders data has an incorrect |headersSize| property.");
       // Can't test for the exact request headers size because the value may
       // vary across platforms ("User-Agent" header differs).
@@ -106,17 +112,22 @@ function test() {
         document,
         getDisplayedRequests(gStore.getState()),
         requestItem,
         "GET",
         SIMPLE_SJS
       );
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_REQUEST_COOKIES, () => {
+    monitor.panelWin.once(EVENTS.RECEIVED_REQUEST_COOKIES, async () => {
+      await waitUntil(() => {
+        let requestItem = getSortedRequests(gStore.getState()).get(0);
+        return requestItem.requestCookies;
+      });
+
       let requestItem = getSortedRequests(gStore.getState()).get(0);
 
       ok(requestItem.requestCookies,
         "There should be a requestCookies data available.");
       is(requestItem.requestCookies.cookies.length, 2,
         "The requestCookies data has an incorrect |cookies| property.");
 
       verifyRequestItemTarget(
@@ -127,17 +138,22 @@ function test() {
         SIMPLE_SJS
       );
     });
 
     monitor.panelWin.once(EVENTS.RECEIVED_REQUEST_POST_DATA, () => {
       ok(false, "Trap listener: this request doesn't have any post data.");
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_HEADERS, () => {
+    monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_HEADERS, async () => {
+      await waitUntil(() => {
+        let requestItem = getSortedRequests(gStore.getState()).get(0);
+        return requestItem.responseHeaders;
+      });
+
       let requestItem = getSortedRequests(gStore.getState()).get(0);
 
       ok(requestItem.responseHeaders,
         "There should be a responseHeaders data available.");
       is(requestItem.responseHeaders.headers.length, 10,
         "The responseHeaders data has an incorrect |headers| property.");
       is(requestItem.responseHeaders.headersSize, 330,
         "The responseHeaders data has an incorrect |headersSize| property.");
@@ -146,34 +162,47 @@ function test() {
         document,
         getDisplayedRequests(gStore.getState()),
         requestItem,
         "GET",
         SIMPLE_SJS
       );
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_COOKIES, () => {
+    monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_COOKIES, async () => {
+      await waitUntil(() => {
+        let requestItem = getSortedRequests(gStore.getState()).get(0);
+        return requestItem.responseCookies;
+      });
+
       let requestItem = getSortedRequests(gStore.getState()).get(0);
 
       ok(requestItem.responseCookies,
         "There should be a responseCookies data available.");
       is(requestItem.responseCookies.cookies.length, 2,
         "The responseCookies data has an incorrect |cookies| property.");
 
       verifyRequestItemTarget(
         document,
         getDisplayedRequests(gStore.getState()),
         requestItem,
         "GET",
         SIMPLE_SJS
       );
     });
 
-    monitor.panelWin.once(EVENTS.STARTED_RECEIVING_RESPONSE, () => {
+    monitor.panelWin.once(EVENTS.STARTED_RECEIVING_RESPONSE, async () => {
+      await waitUntil(() => {
+        let requestItem = getSortedRequests(gStore.getState()).get(0);
+        return requestItem.httpVersion &&
+               requestItem.status &&
+               requestItem.statusText &&
+               requestItem.headersSize;
+      });
+
       let requestItem = getSortedRequests(gStore.getState()).get(0);
 
       is(requestItem.httpVersion, "HTTP/1.1",
         "The httpVersion data has an incorrect value.");
       is(requestItem.status, "200",
         "The status data has an incorrect value.");
       is(requestItem.statusText, "Och Aye",
         "The statusText data has an incorrect value.");
@@ -188,17 +217,25 @@ function test() {
         SIMPLE_SJS,
         {
           status: "200",
           statusText: "Och Aye"
         }
       );
     });
 
-    monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT, () => {
+    monitor.panelWin.once(EVENTS.RECEIVED_RESPONSE_CONTENT, async () => {
+      await waitUntil(() => {
+        let requestItem = getSortedRequests(gStore.getState()).get(0);
+        return requestItem.transferredSize &&
+               requestItem.contentSize &&
+               requestItem.mimeType &&
+               requestItem.responseContent;
+      });
+
       let requestItem = getSortedRequests(gStore.getState()).get(0);
 
       is(requestItem.transferredSize, "12",
         "The transferredSize data has an incorrect value.");
       is(requestItem.contentSize, "12",
         "The contentSize data has an incorrect value.");
       is(requestItem.mimeType, "text/plain; charset=utf-8",
         "The mimeType data has an incorrect value.");
@@ -228,17 +265,22 @@ function test() {
           type: "plain",
           fullMimeType: "text/plain; charset=utf-8",
           transferred: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
           size: L10N.getFormatStrWithNumbers("networkMenu.sizeB", 12),
         }
       );
     });
 
-    monitor.panelWin.once(EVENTS.UPDATING_EVENT_TIMINGS, () => {
+    monitor.panelWin.once(EVENTS.UPDATING_EVENT_TIMINGS, async () => {
+      await waitUntil(() => {
+        let requestItem = getSortedRequests(gStore.getState()).get(0);
+        return requestItem.eventTimings;
+      });
+
       let requestItem = getSortedRequests(gStore.getState()).get(0);
 
       is(typeof requestItem.totalTime, "number",
         "The attached totalTime is incorrect.");
       ok(requestItem.totalTime >= 0,
         "The attached totalTime should be positive.");
 
       verifyRequestItemTarget(
--- a/devtools/client/responsive.html/browser/web-navigation.js
+++ b/devtools/client/responsive.html/browser/web-navigation.js
@@ -74,16 +74,17 @@ BrowserElementWebNavigation.prototype = 
       referrer: referrer ? referrer.spec : null,
       referrerPolicy: referrerPolicy,
       postData: postData ? readInputStreamToString(postData) : null,
       headers: headers ? readInputStreamToString(headers) : null,
       baseURI: baseURI ? baseURI.spec : null,
       triggeringPrincipal: triggeringPrincipal
                            ? Utils.serializePrincipal(triggeringPrincipal)
                            : null,
+      requestTime: Services.telemetry.msSystemNow(),
     });
   },
 
   setOriginAttributesBeforeLoading(originAttributes) {
     // No equivalent in the current BrowserElement API
     this._sendMessage("WebNavigation:SetOriginAttributes", {
       originAttributes,
     });
--- a/devtools/client/responsive.html/images/select-arrow.svg
+++ b/devtools/client/responsive.html/images/select-arrow.svg
@@ -1,37 +1,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/. -->
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16">
-  <defs>
-    <style>
-      use:not(:target) {
-        display: none;
-      }
-      #light {
-        fill: #999797;
-      }
-      #light-hovered {
-        fill: #393f4c; /* --theme-body-color */
-      }
-      #light-selected {
-        fill: #3b3b3b;
-      }
-      #dark {
-        fill: #c6ccd0;
-      }
-      #dark-hovered {
-        fill: #dde1e4;
-      }
-      #dark-selected {
-        fill: #fcfcfc;
-      }
-    </style>
-    <path id="base-path" d="M7.9 16.3c-.3 0-.6-.1-.8-.4l-4-4.8c-.2-.3-.3-.5-.1-.8.1-.3.5-.3.9-.3h8c.4 0 .7 0 .9.3.2.4.1.6-.1.9l-4 4.8c-.2.3-.5.3-.8.3zM7.8 0c.3 0 .6.1.7.4L12.4 5c.2.3.3.4.1.7-.1.4-.5.3-.8.3H3.9c-.4 0-.8.1-.9-.2-.2-.4-.1-.6.1-.9L7 .3c.2-.3.5-.3.8-.3z"/>
-  </defs>
-  <use xlink:href="#base-path" id="light"/>
-  <use xlink:href="#base-path" id="light-hovered"/>
-  <use xlink:href="#base-path" id="light-selected"/>
-  <use xlink:href="#base-path" id="dark"/>
-  <use xlink:href="#base-path" id="dark-hovered"/>
-  <use xlink:href="#base-path" id="dark-selected"/>
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="16" height="16" viewBox="0 0 16 16">
+  <path fill="context-fill" d="M7.9 16.3c-.3 0-.6-.1-.8-.4l-4-4.8c-.2-.3-.3-.5-.1-.8.1-.3.5-.3.9-.3h8c.4 0 .7 0 .9.3.2.4.1.6-.1.9l-4 4.8c-.2.3-.5.3-.8.3zM7.8 0c.3 0 .6.1.7.4L12.4 5c.2.3.3.4.1.7-.1.4-.5.3-.8.3H3.9c-.4 0-.8.1-.9-.2-.2-.4-.1-.6.1-.9L7 .3c.2-.3.5-.3.8-.3z"/>
 </svg>
+
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -7,35 +7,27 @@
 
 .theme-light {
   --rdm-box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
   --submit-button-active-background-color: rgba(0,0,0,0.12);
   --submit-button-active-color: var(--theme-body-color);
   --viewport-color: #999797;
   --viewport-hover-color: var(--theme-body-color);
   --viewport-active-color: #3b3b3b;
-  --viewport-selection-arrow: url("./images/select-arrow.svg#light");
-  --viewport-selection-arrow-hovered:
-    url("./images/select-arrow.svg#light-hovered");
-  --viewport-selection-arrow-selected:
-    url("./images/select-arrow.svg#light-selected");
+  --viewport-selection-arrow: url("./images/select-arrow.svg");
 }
 
 .theme-dark {
   --rdm-box-shadow: 0 4px 4px 0 rgba(105, 105, 105, 0.26);
   --submit-button-active-background-color: var(--theme-toolbar-hover-active);
   --submit-button-active-color: var(--theme-selection-color);
   --viewport-color: #c6ccd0;
   --viewport-hover-color: #dde1e4;
   --viewport-active-color: #fcfcfc;
-  --viewport-selection-arrow: url("./images/select-arrow.svg#dark");
-  --viewport-selection-arrow-hovered:
-    url("./images/select-arrow.svg#dark-hovered");
-  --viewport-selection-arrow-selected:
-    url("./images/select-arrow.svg#dark-selected");
+  --viewport-selection-arrow: url("./images/select-arrow.svg");
 }
 
 * {
   box-sizing: border-box;
 }
 
 :root,
 input,
@@ -86,43 +78,42 @@ body,
 .toolbar-button:active::before {
   filter: var(--checked-icon-filter);
 }
 
 select {
   -moz-appearance: none; appearance: none;
   background-color: var(--theme-toolbar-background);
   background-image: var(--viewport-selection-arrow);
+  /* uncomment after bug 1350010 lands: context-properties: fill; */
+  fill: currentColor;
+  color: var(--viewport-color);
   background-position: 100% 50%;
   background-repeat: no-repeat;
   background-size: 7px;
   border: none;
-  color: var(--viewport-color);
   height: 100%;
   padding: 0 8px;
   text-align: center;
   text-overflow: ellipsis;
 }
 
 select.selected {
-  background-image: var(--viewport-selection-arrow-selected);
   color: var(--viewport-active-color);
 }
 
 select:not(:disabled):hover {
-  background-image: var(--viewport-selection-arrow-hovered);
   color: var(--viewport-hover-color);
 }
 
 /* This is (believed to be?) separate from the identical select.selected rule
    set so that it overrides select:hover because of file ordering once the
    select is focused.  It's unclear whether the visual effect that results here
    is intentional and desired. */
 select:focus {
-  background-image: var(--viewport-selection-arrow-selected);
   color: var(--viewport-active-color);
 }
 
 select > option {
   text-align: left;
   padding: 5px 10px;
 }
 
@@ -198,22 +189,20 @@ select > option.divider {
 }
 
 #global-dpr-selector.focused,
 #global-dpr-selector:not(.disabled):hover {
   color: var(--viewport-hover-color);
 }
 
 #global-dpr-selector:not(.disabled):hover > select {
-  background-image: var(--viewport-selection-arrow-hovered);
   color: var(--viewport-hover-color);
 }
 
 #global-dpr-selector:focus > select {
-  background-image: var(--viewport-selection-arrow-selected);
   color: var(--viewport-active-color);
 }
 
 #global-dpr-selector.selected,
 #global-dpr-selector.selected > select {
   color: var(--viewport-active-color);
 }
 
--- a/devtools/client/responsivedesign/test/browser.ini
+++ b/devtools/client/responsivedesign/test/browser.ini
@@ -9,13 +9,14 @@ support-files =
 
 [browser_responsive_cmd.js]
 [browser_responsivecomputedview.js]
 skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s
 [browser_responsiveruleview.js]
 skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s
 [browser_responsiveui.js]
 [browser_responsiveui_touch.js]
+skip-if = true # Bug 1358261 - Intermittent failures, mostly on Windows
 [browser_responsiveuiaddcustompreset.js]
 [browser_responsive_devicewidth.js]
 [browser_responsiveui_customuseragent.js]
 [browser_responsiveui_window_close.js]
 skip-if = (os == 'linux') && e10s && debug # Bug 1277274
--- a/devtools/client/shims/devtools.js
+++ b/devtools/client/shims/devtools.js
@@ -5,11 +5,14 @@
 "use strict";
 
 /**
  * DevTools is a class that represents a set of developer tools, it holds a
  * set of tools and keeps track of open toolboxes in the browser.
  */
 const DevTools = {
   chromeWindowType: "navigator:browser",
+  getToolbox: function () {
+    return {};
+  }
 };
 
 exports.gDevTools = DevTools;
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/performance-details-call-tree.svg
@@ -0,0 +1,12 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="16" height="16" viewBox="0 0 16 16"
+     fill="#0b0b0b">
+  <rect x="1" y="4" width="10" height="1"/>
+  <rect x="5" y="7" width="10" height="1"/>
+  <rect x="1" y="10" width="10" height="1"/>
+  <rect x="5" y="13" width="10" height="1"/>
+</svg>
+
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/performance-details-flamegraph.svg
@@ -0,0 +1,13 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="16" height="16" viewBox="0 0 16 16"
+     fill="#0b0b0b">
+  <rect x="0" y="4" width="16" height="1"/>
+  <rect x="0" y="7" width="8" height="1"/>
+  <rect x="10" y="7" width="6" height="1"/>
+  <rect x="2" y="10" width="6" height="1"/>
+  <rect x="5" y="13" width="3" height="1"/>
+</svg>
+
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/performance-details-waterfall.svg
@@ -0,0 +1,11 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="16" height="16" viewBox="0 0 16 16"
+     fill="#0b0b0b">
+  <rect x="0" y="4" width="9" height="1"/>
+  <rect x="5" y="8" width="8" height="1"/>
+  <rect x="7" y="12" width="9" height="1"/>
+</svg>
+
deleted file mode 100644
--- a/devtools/client/themes/images/performance-icons.svg
+++ /dev/null
@@ -1,42 +0,0 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#0b0b0b">
-  <style>
-    g:not(:target) {
-      display: none;
-    }
-  </style>
-  <g id="overview-markers">
-    <rect x="0" y="4" width="5" height="1"/>
-    <rect x="7" y="4" width="9" height="1"/>
-    <rect x="0" y="8" width="8" height="1"/>
-    <rect x="10" y="8" width="6" height="1"/>
-    <rect x="0" y="12" width="9" height="1"/>
-    <rect x="12" y="12" width="4" height="1"/>
-  </g>
-  <g id="overview-frames">
-    <rect x="1" y="4" width="2" height="12" rx="1" ry="1"/>
-    <rect x="5" y="12" width="2" height="4" rx="1" ry="1"/>
-    <rect x="9" y="9" width="2" height="7" rx="1" ry="1"/>
-    <rect x="13" y="7" width="2" height="9" rx="1" ry="1"/>
-  </g>
-  <g id="details-waterfall">
-    <rect x="0" y="4" width="9" height="1"/>
-    <rect x="5" y="8" width="8" height="1"/>
-    <rect x="7" y="12" width="9" height="1"/>
-  </g>
-  <g id="details-call-tree">
-    <rect x="1" y="4" width="10" height="1"/>
-    <rect x="5" y="7" width="10" height="1"/>
-    <rect x="1" y="10" width="10" height="1"/>
-    <rect x="5" y="13" width="10" height="1"/>
-  </g>
-  <g id="details-flamegraph">
-    <rect x="0" y="4" width="16" height="1"/>
-    <rect x="0" y="7" width="8" height="1"/>
-    <rect x="10" y="7" width="6" height="1"/>
-    <rect x="2" y="10" width="6" height="1"/>
-    <rect x="5" y="13" width="3" height="1"/>
-  </g>
-</svg>
deleted file mode 100644
--- a/devtools/client/themes/images/sort-arrows.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="7" height="4" fill="#edf0f1" fill-opacity="0.8">
-  <style>
-    polygon:not(:target) {
-      display: none;
-    }
-  </style>
-  <polygon points="0,4 3.5,0 7,4" id="ascending"/>
-  <polygon points="0,0 3.5,4 7,0" id="descending"/>
-</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/sort-ascending-arrow.svg
@@ -0,0 +1,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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="7" height="4" viewBox="0 0 7 4">
+  <path d="M0,4 L3.5,0 L7,4" fill="#edf0f1" fill-opacity="0.8"/>
+</svg>
+
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/sort-descending-arrow.svg
@@ -0,0 +1,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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="7" height="4" viewBox="0 0 7 4">
+  <path d="M0,0 L3.5,4 L7,0" fill="#edf0f1" fill-opacity="0.8"/>
+</svg>
+
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/new-webconsole.css
@@ -0,0 +1,596 @@
+
+@import "chrome://devtools/skin/widgets.css";
+@import "resource://devtools/client/themes/light-theme.css";
+
+/* Webconsole specific theme variables */
+.theme-light,
+.theme-firebug {
+  --error-color: #FF0000;
+  --error-background-color: #FFEBEB;
+  --warning-background-color: #FFFFC8;
+}
+
+/* General output styles */
+
+a {
+  -moz-user-focus: normal;
+  -moz-user-input: enabled;
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+/* Workaround for Bug 575675 - FindChildWithRules aRelevantLinkVisited
+ * assertion when loading HTML page with links in XUL iframe */
+*:visited { }
+
+.webconsole-filterbar-wrapper {
+  flex-grow: 0;
+}
+
+.webconsole-filterbar-primary {
+  display: flex;
+}
+
+.devtools-toolbar.webconsole-filterbar-secondary {
+  height: initial;
+}
+
+.webconsole-filterbar-primary .devtools-plaininput {
+  flex: 1 1 100%;
+}
+
+.webconsole-output.hideTimestamps > .message > .timestamp {
+  display: none;
+}
+
+.message.startGroup .message-body > .objectBox-string,
+.message.startGroupCollapsed .message-body > .objectBox-string {
+  color: var(--theme-body-color);
+  font-weight: bold;
+}
+
+.webconsole-output-wrapper .message > .icon {
+  margin: 3px 0 0 0;
+  padding: 0 0 0 6px;
+}
+
+.message.error > .icon::before {
+  background-position: -12px -36px;
+}
+
+.message.warn > .icon::before {
+  background-position: -24px -36px;
+}
+
+.message.info > .icon::before {
+  background-position: -36px -36px;
+}
+
+.message.network .method {
+  margin-inline-end: 5px;
+}
+
+.network .message-flex-body > .message-body {
+  display: flex;
+}
+
+.webconsole-output-wrapper .message .indent {
+  display: inline-block;
+  border-inline-end: solid 1px var(--theme-splitter-color);
+}
+
+.message.startGroup .indent,
+.message.startGroupCollapsed .indent {
+  border-inline-end-color: transparent;
+  margin-inline-end: 5px;
+}
+
+.message.startGroup .icon,
+.message.startGroupCollapsed .icon {
+  display: none;
+}
+
+/* console.table() */
+.new-consoletable {
+  width: 100%;
+  border-collapse: collapse;
+  --consoletable-border: 1px solid var(--table-splitter-color);
+}
+
+.new-consoletable thead,
+.new-consoletable tbody {
+  background-color: var(--theme-body-background);
+}
+
+.new-consoletable th {
+  background-color: var(--theme-selection-background);
+  color: var(--theme-selection-color);
+  margin: 0;
+  padding: 5px 0 0;
+  font-weight: inherit;
+  border-inline-end: var(--consoletable-border);
+  border-bottom: var(--consoletable-border);
+}
+
+.new-consoletable tr:nth-of-type(even) {
+  background-color: var(--table-zebra-background);
+}
+
+.new-consoletable td {
+  padding: 3px 4px;
+  min-width: 100px;
+  -moz-user-focus: normal;
+  color: var(--theme-body-color);
+  border-inline-end: var(--consoletable-border);
+  height: 1.25em;
+  line-height: 1.25em;
+}
+
+
+/* Layout */
+.webconsole-output {
+  flex: 1;
+  direction: ltr;
+  overflow: auto;
+  -moz-user-select: text;
+  position: relative;
+}
+
+:root,
+body,
+#app-wrapper {
+  height: 100%;
+  margin: 0;
+  padding: 0;
+}
+
+body {
+  overflow: hidden;
+}
+
+#app-wrapper {
+  display: flex;
+  flex-direction: column;
+}
+
+:root, body {
+  margin: 0;
+  padding: 0;
+  height: 100%;
+}
+
+#app-wrapper {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+#left-wrapper {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+#output-container {
+  flex: 1;
+  overflow: hidden;
+}
+.webconsole-output-wrapper {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.message {
+  display: flex;
+  padding: 0 7px;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.message > .prefix,
+.message > .timestamp {
+  flex: none;
+  color: var(--theme-comment);
+  margin: 3px 6px 0 0;
+}
+
+.message > .indent {
+  flex: none;
+}
+
+.message > .icon {
+  flex: none;
+  margin: 3px 6px 0 0;
+  padding: 0 4px;
+  height: 1em;
+  align-self: flex-start;
+}
+
+.theme-firebug .message > .icon {
+  margin: 0;
+  margin-inline-end: 6px;
+}
+
+.theme-firebug .message[severity="error"],
+.theme-light .message.error,
+.theme-firebug .message.error {
+  color: var(--error-color);
+  background-color: var(--error-background-color);
+}
+
+.theme-firebug .message[severity="warn"],
+.theme-light .message.warn,
+.theme-firebug .message.warn {
+  background-color: var(--warning-background-color);
+}
+
+.message > .icon::before {
+  content: "";
+  background-image: url(chrome://devtools/skin/images/webconsole.svg);
+  background-position: 12px 12px;
+  background-repeat: no-repeat;
+  background-size: 72px 60px;
+  width: 12px;
+  height: 12px;
+  display: inline-block;
+}
+
+.theme-light .message > .icon::before {
+  background-image: url(chrome://devtools/skin/images/webconsole.svg#light-icons);
+}
+
+.message > .message-body-wrapper {
+  flex: auto;
+  min-width: 0px;
+  margin: 3px;
+}
+
+/* The red bubble that shows the number of times a message is repeated */
+.message-repeats {
+  -moz-user-select: none;
+  flex: none;
+  margin: 2px 6px;
+  padding: 0 6px;
+  height: 1.25em;
+  color: white;
+  background-color: red;
+  border-radius: 40px;
+  font: message-box;
+  font-size: 0.9em;
+  font-weight: 600;
+}
+
+.message-repeats[value="1"] {
+  display: none;
+}
+
+.message-location {
+  max-width: 40%;
+}
+
+.stack-trace {
+  /* The markup contains extra whitespace to improve formatting of clipboard text.
+     Make sure this whitespace doesn't affect the HTML rendering */
+  white-space: normal;
+}
+
+.stack-trace .frame-link-source,
+.message-location .frame-link-source {
+  /* Makes the file name truncated (and ellipsis shown) on the left side */
+  direction: rtl;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.stack-trace .frame-link-source-inner,
+.message-location .frame-link-source-inner {
+  /* Enforce LTR direction for the file name - fixes bug 1290056 */
+  direction: ltr;
+  unicode-bidi: embed;
+}
+
+.stack-trace .frame-link-function-display-name {
+  max-width: 50%;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.message-flex-body {
+  display: flex;
+}
+
+.message-body > * {
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+
+.message-flex-body > .message-body {
+  display: block;
+  flex: auto;
+}
+#output-container.hideTimestamps > .message {
+  padding-inline-start: 0;
+  margin-inline-start: 7px;
+  width: calc(100% - 7px);
+}
+
+#output-container.hideTimestamps > .message > .timestamp {
+  display: none;
+}
+
+#output-container.hideTimestamps > .message > .indent {
+  background-color: var(--theme-body-background);
+}
+.message:hover {
+  background-color: var(--theme-selection-background-semitransparent) !important;
+}
+.theme-light .message.error {
+  background-color: rgba(255, 150, 150, 0.3);
+}
+
+.theme-dark .message.error {
+  background-color: rgba(235, 83, 104, 0.17);
+}
+
+.console-string {
+  color: var(--theme-highlight-lightorange);
+}
+.theme-selected .console-string,
+.theme-selected .cm-number,
+.theme-selected .cm-variable,
+.theme-selected .kind-ArrayLike {
+  color: #f5f7fa !important; /* Selection Text Color */
+}
+
+
+.message.network.error > .icon::before {
+  background-position: -12px 0;
+}
+.message.network > .message-body {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+
+.message.network .method {
+  flex: none;
+}
+.message.network:not(.navigation-marker) .url {
+  flex: 1 1 auto;
+  /* Make sure the URL is very small initially, let flex change width as needed. */
+  width: 100px;
+  min-width: 5em;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.message.network .status {
+  flex: none;
+  margin-inline-start: 6px;
+}
+.message.network.mixed-content .url {
+  color: var(--theme-highlight-red);
+}
+
+.message .learn-more-link {
+  color: var(--theme-highlight-blue);
+  margin: 0 6px;
+}
+
+.message.network .xhr {
+  background-color: var(--theme-body-color-alt);
+  color: var(--theme-body-background);
+  border-radius: 3px;
+  font-weight: bold;
+  font-size: 10px;
+  padding: 2px;
+  line-height: 10px;
+  margin-inline-start: 3px;
+  margin-inline-end: 1ex;
+}
+.message.cssparser > .indent  {
+  border-inline-end: solid #00b6f0 6px;
+}
+.message.cssparser.error > .icon::before {
+  background-position: -12px -12px;
+}
+
+.message.cssparser.warn > .icon::before {
+  background-position: -24px -12px;
+}
+.message.exception > .indent {
+  border-inline-end: solid #fb9500 6px;
+}
+
+.message.exception.error > .icon::before {
+  background-position: -12px -24px;
+}
+.message.exception.warn > .icon::before {
+  background-position: -24px -24px;
+}
+.message.console-api > .indent {
+  border-inline-end: solid #cbcbcb 6px;
+}
+
+.message.server > .indent {
+  border-inline-end: solid #90B090 6px;
+}
+
+/* Input and output styles */
+.message.command > .indent,
+.message.result > .indent {
+  border-inline-end: solid #808080 6px;
+}
+
+.message.command > .icon::before {
+  background-position: -48px -36px;
+}
+
+.message.result > .icon::before {
+  background-position: -60px -36px;
+}
+
+
+
+
+/* JSTerm Styles */
+#jsterm-wrapper {
+  flex: 0;
+}
+.jsterm-input-container {
+  background-color: var(--theme-tab-toolbar-background);
+  border-top: 1px solid var(--theme-splitter-color);
+}
+
+.theme-light .jsterm-input-container {
+  /* For light theme use a white background for the input - it looks better
+     than off-white */
+  background-color: #fff;
+  border-top-color: #e0e0e0;
+}
+
+.theme-firebug .jsterm-input-container {
+  border-top: 1px solid #ccc;
+}
+
+.jsterm-input-node,
+.jsterm-complete-node {
+  border: none;
+  padding: 0;
+  padding-inline-start: 20px;
+  margin: 0;
+  -moz-appearance: none; appearance: none;
+  background-color: transparent;
+}
+
+.jsterm-input-node[focused="true"] {
+  background-image: var(--theme-command-line-image-focus);
+  box-shadow: none;
+}
+
+.jsterm-complete-node {
+  color: var(--theme-comment);
+}
+
+.jsterm-input-node {
+  /* Always allow scrolling on input - it auto expands in js by setting height,
+     but don't want it to get bigger than the window. 24px = toolbar height. */
+  max-height: calc(90vh - 24px);
+  background-image: var(--theme-command-line-image);
+  background-repeat: no-repeat;
+  background-size: 16px 16px;
+  background-position: 4px 50%;
+  color: var(--theme-content-color1);
+}
+
+:-moz-any(.jsterm-input-node,
+          .jsterm-complete-node) > .textbox-input-box > .textbox-textarea {
+  overflow-x: hidden;
+  /* Set padding for console input on textbox to make sure it is inlcuded in
+     scrollHeight that is used when resizing JSTerminal's input. Note: textbox
+     default style has important already */
+  padding: 4px 0 !important;
+}
+#webconsole-notificationbox,
+.jsterm-stack-node {
+  width: 100%;
+}
+
+.message.security > .indent {
+  border-inline-end: solid red 6px;
+}
+
+.message.security.error > .icon::before {
+  background-position: -12px -48px;
+}
+
+.message.security.warn > .icon::before {
+  background-position: -24px -48px;
+}
+
+.navigation-marker {
+  color: #aaa;
+  background: linear-gradient(#aaa, #aaa) no-repeat left 50%;
+  background-size: 100% 2px;
+  margin-top: 6px;
+  margin-bottom: 6px;
+  font-size: 0.9em;
+}
+
+.navigation-marker .url {
+  padding-inline-end: 9px;
+  text-decoration: none;
+  background: var(--theme-body-background);
+}
+
+.theme-light .navigation-marker .url {
+  background: #fff;
+}
+
+.stacktrace {
+  display: none;
+  padding: 5px 10px;
+  margin: 5px 0 0 0;
+  overflow-y: auto;
+  border: 1px solid var(--theme-splitter-color);
+  border-radius: 3px;
+}
+
+.theme-light .message.error .stacktrace {
+  background-color: rgba(255, 255, 255, 0.5);
+}
+
+.theme-dark .message.error .stacktrace {
+  background-color: rgba(0, 0, 0, 0.5);
+}
+
+.message.open .stacktrace {
+  display: block;
+}
+
+.message .theme-twisty {
+  display: inline-block;
+  vertical-align: middle;
+  margin: 3px 0 0 0;
+  flex-shrink: 0;
+}
+
+/*Do not mirror the twisty because container force to ltr */
+.message .theme-twisty:dir(rtl),
+.message .theme-twisty:-moz-locale-dir(rtl) {
+  transform: none;
+}
+
+.cm-s-mozilla a[class] {
+  font-style: italic;
+  text-decoration: none;
+}
+
+.cm-s-mozilla a[class]:hover,
+.cm-s-mozilla a[class]:focus {
+  text-decoration: underline;
+}
+
+a.learn-more-link.webconsole-learn-more-link {
+    font-style: normal;
+}
+
+/* Open DOMNode in inspector button */
+.open-inspector {
+  background: url(chrome://devtools/skin/images/vview-open-inspector.png) no-repeat 0 0;
+  padding-left: 16px;
+  margin-left: 5px;
+  cursor: pointer;
+}
+
+.elementNode:hover .open-inspector,
+.open-inspector:hover {
+  filter: url(images/filters.svg#checked-icon-state);
+}
+
+.elementNode:hover .open-inspector:active,
+.open-inspector:active {
+  filter: url(images/filters.svg#checked-icon-state) brightness(0.9);
+}
+
--- a/devtools/client/themes/performance.css
+++ b/devtools/client/themes/performance.css
@@ -68,27 +68,27 @@
   height: 8px;
   margin: 0 8px;
   border-radius: 1px;
 }
 
 /* Details panel buttons */
 
 #select-waterfall-view {
-  list-style-image: url(images/performance-icons.svg#details-waterfall);
+  list-style-image: url(images/performance-details-waterfall.svg);
 }
 
 #select-js-calltree-view,
 #select-memory-calltree-view {
-  list-style-image: url(images/performance-icons.svg#details-call-tree);
+  list-style-image: url(images/performance-details-call-tree.svg);
 }
 
 #select-js-flamegraph-view,
 #select-memory-flamegraph-view {
-  list-style-image: url(images/performance-icons.svg#details-flamegraph);
+  list-style-image: url(images/performance-details-flamegraph.svg);
 }
 
 #select-optimizations-view {
   list-style-image: url(images/profiler-stopwatch.svg);
 }
 
 /* Recording buttons */
 
--- a/devtools/client/themes/widgets.css
+++ b/devtools/client/themes/widgets.css
@@ -1317,21 +1317,21 @@ widgets.css is overwritten. */
   background-position: right 6px center;
 }
 
 .table-widget-column-header[sorted]:-moz-locale-dir(rtl) {
   background-position: 6px center;
 }
 
 .table-widget-column-header[sorted=ascending] {
-  background-image: url("chrome://devtools/skin/images/sort-arrows.svg#ascending");
+  background-image: url("chrome://devtools/skin/images/sort-ascending-arrow.svg");
 }
 
 .table-widget-column-header[sorted=descending] {
-  background-image: url("chrome://devtools/skin/images/sort-arrows.svg#descending");
+  background-image: url("chrome://devtools/skin/images/sort-descending-arrow.svg");
 }
 
 .theme-dark .table-widget-column[readonly] {
   background-color: rgba(255,255,255,0.1);
 }
 
 .theme-light .table-widget-column[readonly] {
   background-color: rgba(0,0,0,0.1);
--- a/devtools/client/webconsole/.babelrc
+++ b/devtools/client/webconsole/.babelrc
@@ -1,3 +1,7 @@
 {
-  "presets": ["es2015"]
+  "env": {
+    "test": {
+      "presets": ["es2015"]
+    }
+  }
 }
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/bin/configure.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+/* eslint-env node */
+
+"use strict";
+
+const fs = require("fs");
+const path = require("path");
+
+function getConfig() {
+  if (process.env.TARGET === "firefox-panel") {
+    return require("../configs/firefox-panel.json");
+  }
+
+  const developmentConfig = require("../configs/development.json");
+
+  let localConfig = {};
+  if (fs.existsSync(path.resolve(__dirname, "../configs/local.json"))) {
+    localConfig = require("../configs/local.json");
+  }
+
+  return Object.assign({}, developmentConfig, localConfig);
+}
+
+module.exports = {
+  getConfig,
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/bin/dev-server.js
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env node */
+
+"use strict";
+
+const toolbox = require("devtools-launchpad/index");
+const feature = require("devtools-config");
+const { getConfig } = require("./configure");
+
+const envConfig = getConfig();
+
+feature.setConfig(envConfig);
+
+let webpackConfig = require("../webpack.config");
+
+toolbox.startDevServer(envConfig, webpackConfig, __dirname);
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/configs/development.json
@@ -0,0 +1,35 @@
+{
+  "title": "Console",
+  "environment": "development",
+  "baseWorkerURL": "http://localhost:8000/public/build/",
+  "host": "",
+  "theme": "light",
+  "dir": "ltr",
+  "features": {
+  },
+  "logging": {
+    "client": false,
+    "firefoxProxy": false,
+    "actions": false
+  },
+  "chrome": {
+    "debug": false,
+    "host": "localhost",
+    "port": 9222
+  },
+  "node": {
+    "debug": false,
+    "host": "localhost",
+    "port": 9229
+  },
+  "firefox": {
+    "webSocketConnection": false,
+    "proxyHost": "localhost:9000",
+    "webSocketHost": "localhost:6080",
+    "mcPath": "./firefox"
+  },
+  "development": {
+    "serverPort": 8000,
+    "examplesPort": 7999
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/local-dev/index.js
@@ -0,0 +1,95 @@
+/* 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/. */
+
+ /* eslint-env browser */
+
+"use strict";
+
+const React = require("react");
+const ReactDOM = require("react-dom");
+const { EventEmitter } = require("devtools-modules");
+const { Services: { appinfo, pref } } = require("devtools-modules");
+const { bootstrap } = require("devtools-launchpad");
+
+EventEmitter.decorate(window);
+
+require("../../themes/new-webconsole.css");
+require("../../shared/components/reps/reps.css");
+
+pref("devtools.debugger.remote-timeout", 10000);
+pref("devtools.hud.loglimit", 1000);
+pref("devtools.webconsole.filter.error", true);
+pref("devtools.webconsole.filter.warn", true);
+pref("devtools.webconsole.filter.info", true);
+pref("devtools.webconsole.filter.log", true);
+pref("devtools.webconsole.filter.debug", true);
+pref("devtools.webconsole.filter.css", false);
+pref("devtools.webconsole.filter.net", false);
+pref("devtools.webconsole.filter.netxhr", false);
+pref("devtools.webconsole.ui.filterbar", false);
+pref("devtools.webconsole.inputHistoryCount", 50);
+pref("devtools.webconsole.persistlog", false);
+pref("devtools.webconsole.timestampMessages", false);
+pref("devtools.webconsole.autoMultiline", true);
+
+const NewConsoleOutputWrapper = require("../new-console-output/new-console-output-wrapper");
+const NewWebConsoleFrame = require("../new-webconsole").NewWebConsoleFrame;
+
+// Replicate the DOM that the root component lives within
+const el = document.createElement("div");
+el.style.flex = "1";
+el.innerHTML = `
+  <div id="app-wrapper" class="theme-body">
+    <div id="output-container" role="document" aria-live="polite" />
+  </div>
+`;
+document.querySelector("#mount").appendChild(el);
+
+document.documentElement.classList.add("theme-light");
+
+// Copied from netmonitor/index.js:
+window.addEventListener("DOMContentLoaded", () => {
+  for (let link of document.head.querySelectorAll("link")) {
+    link.href = link.href.replace(/(resource|chrome)\:\/\//, "/");
+  }
+
+  if (appinfo.OS === "Darwin") {
+    document.documentElement.setAttribute("platform", "mac");
+  } else if (appinfo.OS === "Linux") {
+    document.documentElement.setAttribute("platform", "linux");
+  } else {
+    document.documentElement.setAttribute("platform", "win");
+  }
+});
+
+let consoleFrame;
+function onConnect(connection) {
+  // If we are on the main dashboard don't render the component
+  if (!connection || !connection.tabConnection || !connection.tabConnection.tabTarget) {
+    return;
+  }
+
+  // Stub out properties that are received from hudservice
+  const owner = {
+    iframeWindow: window,
+    chromeWindow: window,
+    hudId: "hud_0",
+    target: connection.tabConnection.tabTarget,
+    _browserConsole: false,
+    NewConsoleOutputWrapper,
+  };
+  consoleFrame = new NewWebConsoleFrame(owner);
+  consoleFrame.init().then(function () {
+    console.log("NewWebConsoleFrame initialized");
+  });
+}
+
+// This is just a hack until the local dev environment includes jsterm
+window.evaluateJS = function (input) {
+  consoleFrame.webConsoleClient.evaluateJSAsync(`${input}`, function (r) {
+    consoleFrame.newConsoleOutput.dispatchMessageAdd(r);
+  }, {});
+};
+
+bootstrap(React, ReactDOM, el).then(onConnect);
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/local-dev/jsterm-stub.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function JSTerm(webConsoleFrame) {
+  this.hud = webConsoleFrame;
+  this.hudId = this.hud.hudId;
+  this.historyLoaded = new Promise(r => {
+    r();
+  });
+  this.openVariablesView = () => { };
+  this.init = () => { };
+}
+
+module.exports.JSTerm = JSTerm;
--- a/devtools/client/webconsole/new-console-output/actions/index.js
+++ b/devtools/client/webconsole/new-console-output/actions/index.js
@@ -2,17 +2,17 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const actionModules = [
-  "enhancers",
-  "filters",
-  "messages",
-  "ui",
-].map(filename => require(`./${filename}`));
+  require("./enhancers"),
+  require("./filters"),
+  require("./messages"),
+  require("./ui"),
+];
 
 const actions = Object.assign({}, ...actionModules);
 
 module.exports = actions;
--- a/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html
+++ b/devtools/client/webconsole/new-console-output/test/chrome/test_render_perf.html
@@ -9,87 +9,101 @@
      - http://creativecommons.org/publicdomain/zero/1.0/ -->
 </head>
 <body>
 <p>Test for render perf</p>
 <div id="output"></div>
 
 <script type="text/javascript">
 "use strict";
-
-const testPackets = [];
-const numMessages = 1000;
-for (let id = 0; id < numMessages; id++) {
-  let message = "Odd text";
-  if (id % 2 === 0) {
-    message = "Even text";
+const numMessages = 4000;
+const testPackets = Array.from({length: numMessages}).map((el, id) => ({
+  "from": "server1.conn4.child1/consoleActor2",
+  "type": "consoleAPICall",
+  "message": {
+    "arguments": [
+      "foobar",
+      `${id % 2 === 0 ? "Even" : "Odd"} text`,
+      id
+    ],
+    "columnNumber": 1,
+    "counter": null,
+    "filename": "file:///test.html",
+    "functionName": "",
+    "groupName": "",
+    "level": "log",
+    "lineNumber": 1,
+    "private": false,
+    "styles": [],
+    "timeStamp": 1455064271115 + id,
+    "timer": null,
+    "workerType": "none",
+    "category": "webdev"
   }
-  testPackets.push({
-    "from": "server1.conn4.child1/consoleActor2",
-    "type": "consoleAPICall",
-    "message": {
-      "arguments": [
-        "foobar",
-        message,
-        id
-      ],
-      "columnNumber": 1,
-      "counter": null,
-      "filename": "file:///test.html",
-      "functionName": "",
-      "groupName": "",
-      "level": "log",
-      "lineNumber": 1,
-      "private": false,
-      "styles": [],
-      "timeStamp": 1455064271115 + id,
-      "timer": null,
-      "workerType": "none",
-      "category": "webdev"
-    }
-  });
+}));
+
+async function timeit(cb) {
+  // Return a Promise that resolves the number of seconds cb takes.
+  let start = performance.now();
+  await cb();
+  let elapsed = performance.now() - start;
+  return elapsed;
 }
 
-function timeit(cb) {
-  // Return a Promise that resolves the number of seconds cb takes.
-  return new Promise(resolve => {
-    let start = performance.now();
-    cb();
-    let elapsed = performance.now() - start;
-    resolve(elapsed / 1000);
-  });
-}
+window.onload = async function () {
+  // This test does costly work multiple times to have better performance data.
+  SimpleTest.requestLongerTimeout(3);
 
-window.onload = Task.async(function* () {
-  const { configureStore } = browserRequire(
-    "devtools/client/webconsole/new-console-output/store");
-  const { filterTextSet, filtersClear } = browserRequire(
-    "devtools/client/webconsole/new-console-output/actions/index");
-  const NewConsoleOutputWrapper = browserRequire(
-    "devtools/client/webconsole/new-console-output/new-console-output-wrapper");
-  const wrapper = new NewConsoleOutputWrapper(document.querySelector("#output"), {});
+  try {
+    const Services = browserRequire("Services");
+    Services.prefs.setIntPref("devtools.hud.loglimit", numMessages);
+    const NewConsoleOutputWrapper = browserRequire(
+      "devtools/client/webconsole/new-console-output/new-console-output-wrapper");
+    const EventEmitter = browserRequire("devtools/shared/event-emitter");
+
+    const wrapper = new NewConsoleOutputWrapper(
+      document.getElementById("output"),
+      {hud: EventEmitter.decorate({proxy: {}})},
+      {},
+      null,
+      document,
+    );
+    wrapper.init();
 
-  const store = configureStore();
-
-  let time = yield timeit(() => {
-    testPackets.forEach((message) => {
-      wrapper.dispatchMessageAdd(message);
-    });
-  });
-  info("took " + time + " seconds to render messages");
+    let times = [];
+    const iterations = 25;
+    const lastPacket = testPackets.pop();
+    for (let i = 0; i < iterations; i++) {
+      let time = await timeit(async () => {
+        testPackets.forEach((packet) => wrapper.dispatchMessageAdd(packet));
+        // Only wait for the last packet to minimize work.
+        await wrapper.dispatchMessageAdd(lastPacket, true);
+        await new Promise(resolve => requestAnimationFrame(resolve));
+      });
+      info(`took ${time} ms to render messages`);
+      times.push(time);
 
-  time = yield timeit(() => {
-    store.dispatch(filterTextSet("Odd text"));
-  });
-  info("took " + time + " seconds to search filter half the messages");
+      // Clear the console
+      wrapper.dispatchMessagesClear();
+      await new Promise(resolve => requestAnimationFrame(resolve));
+    }
 
-  time = yield timeit(() => {
-    store.dispatch(filtersClear());
-  });
-  info("took " + time + " seconds to clear the filter");
+    times.sort();
+    let totalTime = times.reduce((sum, t) => sum + t);
+    let avg = totalTime / times.length;
+    let median = times.length % 2 !== 0
+      ? times[Math.floor(times.length / 2)]
+      : (times[(times.length / 2) - 1] + times[times.length / 2]) / 2;
+    info(`On average, it took ${avg} ms (median ${median} ms) ` +
+         `to render ${numMessages} messages`);
 
-  ok(true, "Yay, it didn't time out!");
+    ok(true, "Yay, it didn't time out!");
+  } catch (e) {
+    ok(false, `Error :  ${e.message}
+      ${e.stack}
+    `);
+  }
 
   SimpleTest.finish();
-});
+};
 </script>
 </body>
 </html>
--- a/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/WebConsoleUtils.js
@@ -1,14 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const L10n = require("devtools/client/webconsole/new-console-output/test/fixtures/L10n");
 
 const Utils = {
-  L10n
+  L10n,
+  supportsString: function (s) {
+    return s;
+  }
 };
 
 module.exports = {
   Utils
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-webconsole.js
@@ -0,0 +1,269 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const defer = require("devtools/shared/defer");
+const Services = require("Services");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const { JSTerm } = require("devtools/client/webconsole/jsterm");
+const { WebConsoleConnectionProxy } = require("devtools/client/webconsole/webconsole-connection-proxy");
+
+const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
+
+// XXX: This file is incomplete (see bug 1326937).
+// It's used when loading the webconsole with devtools-launchpad, but will ultimately be
+// the entry point for the new frontend
+
+/**
+ * A WebConsoleFrame instance is an interactive console initialized *per target*
+ * that displays console log data as well as provides an interactive terminal to
+ * manipulate the target's document content.
+ *
+ * The WebConsoleFrame is responsible for the actual Web Console UI
+ * implementation.
+ *
+ * @constructor
+ * @param object webConsoleOwner
+ *        The WebConsole owner object.
+ */
+function NewWebConsoleFrame(webConsoleOwner) {
+  this.owner = webConsoleOwner;
+  this.hudId = this.owner.hudId;
+  this.isBrowserConsole = this.owner._browserConsole;
+  this.NEW_CONSOLE_OUTPUT_ENABLED = true;
+  this.window = this.owner.iframeWindow;
+
+  this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this);
+
+  EventEmitter.decorate(this);
+}
+NewWebConsoleFrame.prototype = {
+  /**
+   * Getter for the debugger WebConsoleClient.
+   * @type object
+   */
+  get webConsoleClient() {
+    return this.proxy ? this.proxy.webConsoleClient : null;
+  },
+
+  /**
+   * Initialize the WebConsoleFrame instance.
+   * @return object
+   *         A promise object that resolves once the frame is ready to use.
+   */
+  init() {
+    this._initUI();
+    let connectionInited = this._initConnection();
+
+    // Don't reject if the history fails to load for some reason.
+    // This would be fine, the panel will just start with empty history.
+    let allReady = this.jsterm.historyLoaded.catch(() => {}).then(() => {
+      return connectionInited;
+    });
+
+    // This notification is only used in tests. Don't chain it onto
+    // the returned promise because the console panel needs to be attached
+    // to the toolbox before the web-console-created event is receieved.
+    let notifyObservers = () => {
+      let id = WebConsoleUtils.supportsString(this.hudId);
+      if (Services.obs) {
+        Services.obs.notifyObservers(id, "web-console-created");
+      }
+    };
+    allReady.then(notifyObservers, notifyObservers)
+            .then(this.newConsoleOutput.init);
+
+    return allReady;
+  },
+
+  destroy() {
+    if (this._destroyer) {
+      return this._destroyer.promise;
+    }
+
+    this._destroyer = defer();
+
+    Services.prefs.addObserver(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged);
+    this.React = this.ReactDOM = this.FrameView = null;
+
+    let onDestroy = () => {
+      this._destroyer.resolve(null);
+    };
+    if (this.proxy) {
+      this.proxy.disconnect().then(onDestroy);
+      this.proxy = null;
+    } else {
+      onDestroy();
+    }
+
+    return this._destroyer.promise;
+  },
+
+  _onUpdateListeners() {
+
+  },
+
+  logWarningAboutReplacedAPI() {
+
+  },
+
+  /**
+   * Setter for saving of network request and response bodies.
+   *
+   * @param boolean value
+   *        The new value you want to set.
+   */
+  setSaveRequestAndResponseBodies: function (value) {
+    if (!this.webConsoleClient) {
+      // Don't continue if the webconsole disconnected.
+      return promise.resolve(null);
+    }
+
+    let deferred = defer();
+    let newValue = !!value;
+    let toSet = {
+      "NetworkMonitor.saveRequestAndResponseBodies": newValue,
+    };
+
+    // Make sure the web console client connection is established first.
+    this.webConsoleClient.setPreferences(toSet, response => {
+      if (!response.error) {
+        this._saveRequestAndResponseBodies = newValue;
+        deferred.resolve(response);
+      } else {
+        deferred.reject(response.error);
+      }
+    });
+
+    return deferred.promise;
+  },
+
+  /**
+   * Connect to the server using the remote debugging protocol.
+   *
+   * @private
+   * @return object
+   *         A promise object that is resolved/reject based on the connection
+   *         result.
+   */
+  _initConnection: function () {
+    if (this._initDefer) {
+      return this._initDefer.promise;
+    }
+
+    this._initDefer = defer();
+    this.proxy = new WebConsoleConnectionProxy(this, this.owner.target);
+
+    this.proxy.connect().then(() => {
+      // on success
+      this._initDefer.resolve(this);
+    }, (reason) => {
+      // on failure
+      // TODO Print a message to console
+      this._initDefer.reject(reason);
+    });
+
+    return this._initDefer.promise;
+  },
+
+  _initUI: function () {
+    this.document = this.window.document;
+    this.rootElement = this.document.documentElement;
+
+    this.outputNode = this.document.getElementById("output-container");
+    this.completeNode = this.document.querySelector(".jsterm-complete-node");
+    this.inputNode = this.document.querySelector(".jsterm-input-node");
+
+    this.jsterm = new JSTerm(this);
+    this.jsterm.init();
+
+    let toolbox = gDevTools.getToolbox(this.owner.target);
+
+    // @TODO Remove this once JSTerm is handled with React/Redux.
+    this.window.jsterm = this.jsterm;
+    // @TODO Once the toolbox has been converted to React, see if passing
+    // in JSTerm is still necessary.
+
+    // Handle both launchpad and toolbox loading
+    let Wrapper = this.owner.NewConsoleOutputWrapper || this.window.NewConsoleOutput;
+    this.newConsoleOutput = new Wrapper(
+      this.outputNode, this.jsterm, toolbox, this.owner, this.document);
+
+    // Toggle the timestamp on preference change
+    Services.prefs.addObserver(PREF_MESSAGE_TIMESTAMP, this._onToolboxPrefChanged);
+    this._onToolboxPrefChanged();
+  },
+
+  /**
+   * Handler for page location changes.
+   *
+   * @param string uri
+   *        New page location.
+   * @param string title
+   *        New page title.
+   */
+  onLocationChange: function (uri, title) {
+    this.contentLocation = uri;
+    if (this.owner.onLocationChange) {
+      this.owner.onLocationChange(uri, title);
+    }
+  },
+
+  /**
+   * Release an actor.
+   *
+   * @private
+   * @param string actor
+   *        The actor ID you want to release.
+   */
+  _releaseObject: function (actor) {
+    if (this.proxy) {
+      this.proxy.releaseActor(actor);
+    }
+  },
+
+  /**
+   * Called when the message timestamp pref changes.
+   */
+  _onToolboxPrefChanged: function () {
+    let newValue = Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP);
+    this.newConsoleOutput.dispatchTimestampsToggle(newValue);
+  },
+
+  /**
+   * Handler for the tabNavigated notification.
+   *
+   * @param string event
+   *        Event name.
+   * @param object packet
+   *        Notification packet received from the server.
+   */
+  handleTabNavigated: function (event, packet) {
+    if (event == "will-navigate") {
+      if (this.persistLog) {
+        // Add a _type to hit convertCachedPacket.
+        packet._type = true;
+        this.newConsoleOutput.dispatchMessageAdd(packet);
+      } else {
+        this.jsterm.clearOutput();
+      }
+    }
+
+    if (packet.url) {
+      this.onLocationChange(packet.url, packet.title);
+    }
+
+    if (event == "navigate" && !packet.nativeConsoleAPI) {
+      this.logWarningAboutReplacedAPI();
+    }
+  },
+};
+
+exports.NewWebConsoleFrame = NewWebConsoleFrame;
--- a/devtools/client/webconsole/package.json
+++ b/devtools/client/webconsole/package.json
@@ -1,21 +1,35 @@
 {
   "name": "webconsole",
   "version": "0.0.1",
-  "devDependencies": {
+  "engines": {
+    "node": ">=6.9.0"
+  },
+  "scripts": {
+    "start": "node bin/dev-server",
+    "test": "cross-env NODE_ENV=test NODE_PATH=../../../ mocha new-console-output/test/**/*.test.js --compilers js:babel-register -r jsdom-global/register -r ./new-console-output/test/require-helper.js"
+  },
+  "dependencies": {
     "amd-loader": "0.0.5",
     "babel-preset-es2015": "^6.6.0",
-    "babel-register": "^6.7.2",
+    "babel-register": "^6.24.0",
     "cross-env": "^3.1.3",
+    "devtools-config": "0.0.12",
+    "devtools-launchpad": "0.0.67",
+    "devtools-modules": "0.0.24",
     "enzyme": "^2.4.1",
     "expect": "^1.16.0",
+    "file-loader": "^0.10.1",
+    "immutable": "^3.8.1",
     "jsdom": "^9.4.1",
     "jsdom-global": "^2.0.0",
+    "json-loader": "^0.5.4",
     "mocha": "^2.5.3",
+    "raw-loader": "^0.5.1",
+    "react": "=15.3.2",
+    "react-dom": "=15.3.2",
+    "react-redux": "=5.0.3",
+    "redux": "^3.6.0",
     "require-hacker": "^2.1.4",
     "sinon": "^1.17.5"
-  },
-  "scripts": {
-    "postinstall": "cd ../ && npm install && cd webconsole",
-    "test": "cross-env NODE_PATH=../../../ mocha new-console-output/test/**/*.test.js --compilers js:babel-register -r jsdom-global/register -r ./new-console-output/test/require-helper.js"
   }
 }
--- a/devtools/client/webconsole/webconsole-connection-proxy.js
+++ b/devtools/client/webconsole/webconsole-connection-proxy.js
@@ -1,24 +1,18 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {Cc, Ci, Cu} = require("chrome");
-
-const {Utils: WebConsoleUtils} =
-  require("devtools/client/webconsole/utils");
-const BrowserLoaderModule = {};
-Cu.import("resource://devtools/client/shared/browser-loader.js", BrowserLoaderModule);
-
-const promise = require("promise");
+const {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils");
+const defer = require("devtools/shared/defer");
 const Services = require("Services");
 
 const STRINGS_URI = "devtools/client/locales/webconsole.properties";
 var l10n = new WebConsoleUtils.L10n(STRINGS_URI);
 
 const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout";
 // Web Console connection proxy
 
@@ -122,28 +116,27 @@ WebConsoleConnectionProxy.prototype = {
    *         A promise object that is resolved/rejected based on the success of
    *         the connection initialization.
    */
   connect: function () {
     if (this._connectDefer) {
       return this._connectDefer.promise;
     }
 
-    this._connectDefer = promise.defer();
+    this._connectDefer = defer();
 
     let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT);
-    this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-    this._connectTimer.initWithCallback(this._connectionTimeout,
-                                        timeout, Ci.nsITimer.TYPE_ONE_SHOT);
+    this._connectTimer = setTimeout(this._connectionTimeout, timeout);
 
     let connPromise = this._connectDefer.promise;
     connPromise.then(() => {
-      this._connectTimer.cancel();
+      clearTimeout(this._connectTimer);
       this._connectTimer = null;
     }, () => {
+      clearTimeout(this._connectTimer);
       this._connectTimer = null;
     });
 
     let client = this.client = this.target.client;
 
     client.addListener("logMessage", this._onLogMessage);
     client.addListener("pageError", this._onPageError);
     client.addListener("consoleAPICall", this._onConsoleAPICall);
@@ -470,17 +463,17 @@ WebConsoleConnectionProxy.prototype = {
    * @return object
    *         A promise object that is resolved when disconnect completes.
    */
   disconnect: function () {
     if (this._disconnecter) {
       return this._disconnecter.promise;
     }
 
-    this._disconnecter = promise.defer();
+    this._disconnecter = defer();
 
     if (!this.client) {
       this._disconnecter.resolve(null);
       return this._disconnecter.promise;
     }
 
     this.client.removeListener("logMessage", this._onLogMessage);
     this.client.removeListener("pageError", this._onPageError);
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/webpack.config.js
@@ -0,0 +1,128 @@
+/* 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/. */
+
+/* eslint-env node */
+/* eslint max-len: [0] */
+
+"use strict";
+
+const {toolboxConfig} = require("./node_modules/devtools-launchpad/index");
+const { NormalModuleReplacementPlugin } = require("webpack");
+const {getConfig} = require("./bin/configure");
+
+const path = require("path");
+const projectPath = path.join(__dirname, "local-dev");
+
+let webpackConfig = {
+  entry: {
+    console: [path.join(projectPath, "index.js")],
+  },
+
+  module: {
+    loaders: [
+      {
+        test: /\.(png|svg)$/,
+        loader: "file-loader?name=[path][name].[ext]",
+      },
+    ]
+  },
+
+  output: {
+    path: path.join(__dirname, "assets/build"),
+    filename: "[name].js",
+    publicPath: "/assets/build",
+  },
+
+  externals: [
+    {
+      "promise": "var Promise",
+    }
+  ],
+};
+
+webpackConfig.resolve = {
+  alias: {
+    "Services": "devtools-modules/client/shared/shim/Services",
+
+    "devtools/client/webconsole/jsterm": path.join(projectPath, "jsterm-stub"),
+    "devtools/client/webconsole/utils": path.join(__dirname, "new-console-output/test/fixtures/WebConsoleUtils"),
+    "devtools/client/webconsole/new-console-output": path.join(__dirname, "new-console-output"),
+    "devtools/client/webconsole/webconsole-connection-proxy": path.join(__dirname, "webconsole-connection-proxy"),
+
+    "react": path.join(__dirname, "node_modules/react"),
+    "devtools/client/shared/vendor/immutable": "immutable",
+    "devtools/client/shared/vendor/react": "react",
+    "devtools/client/shared/vendor/react-dom": "react-dom",
+    "devtools/client/shared/vendor/react-redux": "react-redux",
+    "devtools/client/shared/vendor/redux": "redux",
+
+    "devtools/client/locales": path.join(__dirname, "../../client/locales/en-US"),
+    "toolkit/locales": path.join(__dirname, "../../../toolkit/locales/en-US"),
+    "devtools/shared/locales": path.join(__dirname, "../../shared/locales/en-US"),
+    "devtools/shared/plural-form": path.join(__dirname, "../../shared/plural-form"),
+    "devtools/shared/l10n": path.join(__dirname, "../../shared/l10n"),
+
+    "devtools/client/framework/devtools": path.join(__dirname, "../../client/shims/devtools"),
+    "devtools/client/framework/menu": "devtools-modules/client/framework/menu",
+    "devtools/client/framework/menu-item": path.join(__dirname, "../../client/framework/menu-item"),
+
+    "devtools/client/shared/components/reps/reps": path.join(__dirname, "../../client/shared/components/reps/reps"),
+    "devtools/client/shared/redux/middleware/thunk": path.join(__dirname, "../../client/shared/redux/middleware/thunk"),
+    "devtools/client/shared/components/stack-trace": path.join(__dirname, "../../client/shared/components/stack-trace"),
+    "devtools/client/shared/source-utils": path.join(__dirname, "../../client/shared/source-utils"),
+    "devtools/client/shared/components/frame": path.join(__dirname, "../../client/shared/components/frame"),
+
+    "devtools/shared/defer": path.join(__dirname, "../../shared/defer"),
+    "devtools/shared/event-emitter": "devtools-modules/shared/event-emitter",
+    "devtools/shared/client/main": path.join(__dirname, "new-console-output/test/fixtures/ObjectClient"),
+    "devtools/shared/platform/clipboard": path.join(__dirname, "../../shared/platform/content/clipboard"),
+  }
+};
+
+const mappings = [
+  [
+    /utils\/menu/, "devtools-launchpad/src/components/shared/menu"
+  ],
+  [
+    /chrome:\/\/devtools\/skin/,
+    (result) => {
+      result.request = result.request
+        .replace("./chrome://devtools/skin", path.join(__dirname, "../themes"));
+    }
+  ],
+  [
+    /chrome:\/\/devtools\/content/,
+    (result) => {
+      result.request = result.request
+        .replace("./chrome://devtools/content", path.join(__dirname, ".."));
+    }
+  ],
+  [
+    /resource:\/\/devtools/,
+    (result) => {
+      result.request = result.request
+        .replace("./resource://devtools/client", path.join(__dirname, ".."));
+    }
+  ],
+];
+
+webpackConfig.plugins = mappings.map(([regex, res]) =>
+  new NormalModuleReplacementPlugin(regex, res));
+
+// Exclude to transpile all scripts in devtools/ but not for this folder
+const basePath = path.join(__dirname, "../../").replace(/\\/g, "\\\\");
+const baseName = path.basename(__dirname);
+webpackConfig.babelExcludes = new RegExp(`^${basePath}(.(?!${baseName}))*$`);
+
+let config = toolboxConfig(webpackConfig, getConfig());
+
+// Remove loaders from devtools-launchpad's webpack.config.js
+// * For svg-inline loader:
+//   Webconsole uses file loader to bundle image assets instead of svg-inline loader
+// * For raw loader:
+//   devtools/shared/l10n has preloaded raw loader in require.context
+config.module.loaders = config.module.loaders
+  .filter((loader) => !["svg-inline", "raw"].includes(loader.loader));
+
+module.exports = config;
--- a/devtools/server/actors/highlighters/simple-outline.js
+++ b/devtools/server/actors/highlighters/simple-outline.js
@@ -1,30 +1,33 @@
 /* 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 {
-  installHelperSheet,
   isNodeValid,
   addPseudoClassLock,
   removePseudoClassLock
 } = require("./utils/markup");
 
+const { loadSheet } = require("devtools/shared/layout/utils");
+
 // SimpleOutlineHighlighter's stylesheet
 const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
-const SIMPLE_OUTLINE_SHEET = `.__fx-devtools-hide-shortcut__ {
-                                visibility: hidden !important
-                              }
-                              ${HIGHLIGHTED_PSEUDO_CLASS} {
-                                outline: 2px dashed #F06!important;
-                                outline-offset: -2px!important
-                              }`;
+const SIMPLE_OUTLINE_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
+  .__fx-devtools-hide-shortcut__ {
+    visibility: hidden !important
+  }
+  ${HIGHLIGHTED_PSEUDO_CLASS} {
+    outline: 2px dashed #F06!important;
+    outline-offset: -2px!important
+  }`);
+
 /**
  * The SimpleOutlineHighlighter is a class that has the same API than the
  * BoxModelHighlighter, but adds a pseudo-class on the target element itself
  * to draw a simple css outline around the element.
  * It is used by the HighlighterActor when canvasframe-based highlighters can't
  * be used. This is the case for XUL windows.
  */
 function SimpleOutlineHighlighter(highlighterEnv) {
@@ -43,17 +46,17 @@ SimpleOutlineHighlighter.prototype = {
   /**
    * Show the highlighter on a given node
    * @param {DOMNode} node
    */
   show: function (node) {
     if (isNodeValid(node) && (!this.currentNode || node !== this.currentNode)) {
       this.hide();
       this.currentNode = node;
-      installHelperSheet(node.ownerGlobal, SIMPLE_OUTLINE_SHEET);
+      loadSheet(node.ownerGlobal, SIMPLE_OUTLINE_SHEET);
       addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
     }
     return true;
   },
 
   /**
    * Hide the highlighter, the outline and the infobar.
    */
--- a/devtools/server/actors/highlighters/utils/markup.js
+++ b/devtools/server/actors/highlighters/utils/markup.js
@@ -97,30 +97,16 @@ ClassList.prototype = {
  * @return {Boolean}
  */
 function isXUL(window) {
   return window.document.documentElement.namespaceURI === XUL_NS;
 }
 exports.isXUL = isXUL;
 
 /**
- * Inject a helper stylesheet in the window.
- */
-var installedHelperSheets = new WeakSet();
-
-function installHelperSheet(win, url = STYLESHEET_URI, type = "agent") {
-  if (installedHelperSheets.has(win.document)) {
-    return;
-  }
-  loadSheet(win, url, type);
-  installedHelperSheets.add(win.document);
-}
-exports.installHelperSheet = installHelperSheet;
-
-/**
  * Returns true if a DOM node is "valid", where "valid" means that the node isn't a dead
  * object wrapper, is still attached to a document, and is of a given type.
  * @param {DOMNode} node
  * @param {Number} nodeType Optional, defaults to ELEMENT_NODE
  * @return {Boolean}
  */
 function isNodeValid(node, nodeType = Ci.nsIDOMNode.ELEMENT_NODE) {
   // Is it still alive?
@@ -270,17 +256,17 @@ CanvasFrameAnonymousContentHelper.protot
     if (isXUL(this.highlighterEnv.window)) {
       return;
     }
 
     // For now highlighters.css is injected in content as a ua sheet because
     // <style scoped> doesn't work inside anonymous content (see bug 1086532).
     // If it did, highlighters.css would be injected as an anonymous content
     // node using CanvasFrameAnonymousContentHelper instead.
-    installHelperSheet(this.highlighterEnv.window);
+    loadSheet(this.highlighterEnv.window, STYLESHEET_URI);
 
     let node = this.nodeBuilder();
 
     // It was stated that hidden documents don't accept
     // `insertAnonymousContent` calls yet. That doesn't seems the case anymore,
     // at least on desktop. Therefore, removing the code that was dealing with
     // that scenario, fixes when we're adding anonymous content in a tab that
     // is not the active one (see bug 1260043 and bug 1260044)
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -123,26 +123,26 @@ const PSEUDO_SELECTORS = [
   [":empty", 0],
   [":target", 0],
   [":enabled", 0],
   [":disabled", 0],
   [":checked", 1],
   ["::selection", 0]
 ];
 
-var HELPER_SHEET = `data:text/css;charset=utf-8,
+var HELPER_SHEET = "data:text/css;charset=utf-8," + encodeURIComponent(`
   .__fx-devtools-hide-shortcut__ {
     visibility: hidden !important;
   }
 
   :-moz-devtools-highlighted {
     outline: 2px dashed #F06!important;
     outline-offset: -2px !important;
   }
-`;
+`);
 
 const flags = require("devtools/shared/flags");
 
 loader.lazyRequireGetter(this, "DevToolsUtils",
                          "devtools/shared/DevToolsUtils");
 
 loader.lazyRequireGetter(this, "AsyncUtils", "devtools/shared/async-utils");
 
@@ -1859,33 +1859,22 @@ var WalkerActor = protocol.ActorClassWit
       return false;
     }
     DOMUtils.addPseudoClassLock(node.rawNode, pseudo, enabled);
     this._activePseudoClassLocks.add(node);
     this._queuePseudoClassMutation(node);
     return true;
   },
 
-  _installHelperSheet: function (node) {
-    if (!this.installedHelpers) {
-      this.installedHelpers = new WeakSet();
-    }
-    let win = node.rawNode.ownerGlobal;
-    if (!this.installedHelpers.has(win)) {
-      loadSheet(win, HELPER_SHEET, "agent");
-      this.installedHelpers.add(win);
-    }
-  },
-
   hideNode: function (node) {
     if (isNodeDead(node)) {
       return;
     }
 
-    this._installHelperSheet(node);
+    loadSheet(node.rawNode.ownerGlobal, HELPER_SHEET);
     node.rawNode.classList.add(HIDDEN_CLASS);
   },
 
   unhideNode: function (node) {
     if (isNodeDead(node)) {
       return;
     }
 
--- a/devtools/server/actors/stylesheets.js
+++ b/devtools/server/actors/stylesheets.js
@@ -12,17 +12,17 @@ const {Task} = require("devtools/shared/
 const events = require("sdk/event/core");
 const protocol = require("devtools/shared/protocol");
 const {LongStringActor} = require("devtools/server/actors/string");
 const {fetch} = require("devtools/shared/DevToolsUtils");
 const {listenOnce} = require("devtools/shared/async-utils");
 const {originalSourceSpec, mediaRuleSpec, styleSheetSpec,
        styleSheetsSpec} = require("devtools/shared/specs/stylesheets");
 const {SourceMapConsumer} = require("source-map");
-const { installHelperSheet,
+const {
   addPseudoClassLock, removePseudoClassLock } = require("devtools/server/actors/highlighters/utils/markup");
 
 loader.lazyGetter(this, "CssLogic", () => require("devtools/shared/inspector/css-logic"));
 
 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
 
--- a/devtools/shared/heapsnapshot/HeapSnapshot.cpp
+++ b/devtools/shared/heapsnapshot/HeapSnapshot.cpp
@@ -1431,18 +1431,17 @@ WriteHeapGraph(JSContext* cx,
   }
 
   return ok;
 }
 
 static unsigned long
 msSinceProcessCreation(const TimeStamp& now)
 {
-  bool ignored;
-  auto duration = now - TimeStamp::ProcessCreation(ignored);
+  auto duration = now - TimeStamp::ProcessCreation();
   return (unsigned long) duration.ToMilliseconds();
 }
 
 /* static */ already_AddRefed<nsIFile>
 HeapSnapshot::CreateUniqueCoreDumpFile(ErrorResult& rv,
                                        const TimeStamp& now,
                                        nsAString& outFilePath,
                                        nsAString& outSnapshotId)
--- a/devtools/shared/layout/utils.js
+++ b/devtools/shared/layout/utils.js
@@ -725,21 +725,21 @@ function getWindowFor(node) {
 /**
  * Synchronously loads a style sheet from `uri` and adds it to the list of
  * additional style sheets of the document.
  * The sheets added takes effect immediately, and only on the document of the
  * `window` given.
  *
  * @param {DOMWindow} window
  * @param {String} url
- * @param {String} [type="author"]
+ * @param {String} [type="agent"]
  */
-function loadSheet(window, url, type = "author") {
+function loadSheet(window, url, type = "agent") {
   if (!(type in SHEET_TYPE)) {
-    type = "author";
+    type = "agent";
   }
 
   let windowUtils = utilsFor(window);
   try {
     windowUtils.loadSheetUsingURIString(url, windowUtils[SHEET_TYPE[type]]);
   } catch (e) {
     // The method fails if the url is already loaded.
   }
@@ -747,21 +747,21 @@ function loadSheet(window, url, type = "
 exports.loadSheet = loadSheet;
 
 /**
  * Remove the document style sheet at `sheetURI` from the list of additional
  * style sheets of the document. The removal takes effect immediately.
  *
  * @param {DOMWindow} window
  * @param {String} url
- * @param {String} [type="author"]
+ * @param {String} [type="agent"]
  */
-function removeSheet(window, url, type = "author") {
+function removeSheet(window, url, type = "agent") {
   if (!(type in SHEET_TYPE)) {
-    type = "author";
+    type = "agent";
   }
 
   let windowUtils = utilsFor(window);
   try {
     windowUtils.removeSheetUsingURIString(url, windowUtils[SHEET_TYPE[type]]);
   } catch (e) {
     // The method fails if the url is already removed.
   }
--- a/docshell/base/nsDSURIContentListener.cpp
+++ b/docshell/base/nsDSURIContentListener.cpp
@@ -17,16 +17,17 @@
 #include "nsIHttpChannel.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsError.h"
 #include "nsCharSeparatedTokenizer.h"
 #include "nsIConsoleService.h"
 #include "nsIScriptError.h"
 #include "nsDocShellLoadTypes.h"
 #include "nsIMultiPartChannel.h"
+#include "nsContentUtils.h"
 
 using namespace mozilla;
 
 nsDSURIContentListener::nsDSURIContentListener(nsDocShell* aDocShell)
   : mDocShell(aDocShell)
   , mExistingJPEGRequest(nullptr)
   , mParentContentListener(nullptr)
 {
@@ -443,18 +444,23 @@ nsDSURIContentListener::CheckFrameOption
   while (tokenizer.hasMoreTokens()) {
     const nsSubstring& tok = tokenizer.nextToken();
     if (!CheckOneFrameOptionsPolicy(httpChannel, tok)) {
       // cancel the load and display about:blank
       httpChannel->Cancel(NS_BINDING_ABORTED);
       if (mDocShell) {
         nsCOMPtr<nsIWebNavigation> webNav(do_QueryObject(mDocShell));
         if (webNav) {
+          nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->GetLoadInfo();
+          nsCOMPtr<nsIPrincipal> triggeringPrincipal = loadInfo
+            ? loadInfo->TriggeringPrincipal()
+            : nsContentUtils::GetSystemPrincipal();
           webNav->LoadURI(u"about:blank",
-                          0, nullptr, nullptr, nullptr);
+                          0, nullptr, nullptr, nullptr,
+                          triggeringPrincipal);
         }
       }
       return false;
     }
   }
 
   return true;
 }
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -194,16 +194,17 @@
 #include "nsXULAppAPI.h"
 #include "nsDOMNavigationTiming.h"
 #include "nsISecurityUITelemetry.h"
 #include "nsDSURIContentListener.h"
 #include "nsDocShellLoadTypes.h"
 #include "nsDocShellTransferableHooks.h"
 #include "nsICommandManager.h"
 #include "nsIDOMNode.h"
+#include "nsIClassOfService.h"
 #include "nsIDocShellTreeOwner.h"
 #include "nsIHttpChannel.h"
 #include "nsIIDNService.h"
 #include "nsIInputStreamChannel.h"
 #include "nsINestedURI.h"
 #include "nsISHContainer.h"
 #include "nsISHistory.h"
 #include "nsISecureBrowserUI.h"
@@ -3019,19 +3020,17 @@ nsDocShell::PopProfileTimelineMarkers(
   }
 
   return NS_OK;
 }
 
 nsresult
 nsDocShell::Now(DOMHighResTimeStamp* aWhen)
 {
-  bool ignore;
-  *aWhen =
-    (TimeStamp::Now() - TimeStamp::ProcessCreation(ignore)).ToMilliseconds();
+  *aWhen = (TimeStamp::Now() - TimeStamp::ProcessCreation()).ToMilliseconds();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDocShell::SetWindowDraggingAllowed(bool aValue)
 {
   RefPtr<nsDocShell> parent = GetParentDocshell();
   if (!aValue && mItemType == typeChrome && !parent) {
@@ -4741,21 +4740,22 @@ nsDocShell::GotoIndex(int32_t aIndex)
   return rv;
 }
 
 NS_IMETHODIMP
 nsDocShell::LoadURI(const char16_t* aURI,
                     uint32_t aLoadFlags,
                     nsIURI* aReferringURI,
                     nsIInputStream* aPostStream,
-                    nsIInputStream* aHeaderStream)
+                    nsIInputStream* aHeaderStream,
+                    nsIPrincipal* aTriggeringPrincipal)
 {
   return LoadURIWithOptions(aURI, aLoadFlags, aReferringURI,
                             mozilla::net::RP_Unset, aPostStream,
-                            aHeaderStream, nullptr, nullptr);
+                            aHeaderStream, nullptr, aTriggeringPrincipal);
 }
 
 NS_IMETHODIMP
 nsDocShell::LoadURIWithOptions(const char16_t* aURI,
                                uint32_t aLoadFlags,
                                nsIURI* aReferringURI,
                                uint32_t aReferrerPolicy,
                                nsIInputStream* aPostStream,
@@ -7921,21 +7921,26 @@ nsDocShell::EndPageLoad(nsIWebProgress* 
           nsAutoCString newSpec;
           newURI->GetSpec(newSpec);
           NS_ConvertUTF8toUTF16 newSpecW(newSpec);
 
           // This notification is meant for Firefox Health Report so it
           // can increment counts from the search engine
           MaybeNotifyKeywordSearchLoading(keywordProviderName, keywordAsSent);
 
-          return LoadURI(newSpecW.get(),  // URI string
-                         LOAD_FLAGS_NONE, // Load flags
-                         nullptr,         // Referring URI
-                         newPostData,     // Post data stream
-                         nullptr);        // Headers stream
+          nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo();
+          nsCOMPtr<nsIPrincipal> triggeringPrincipal = loadInfo
+            ? loadInfo->TriggeringPrincipal()
+            : nsContentUtils::GetSystemPrincipal();
+          return LoadURI(newSpecW.get(),       // URI string
+                         LOAD_FLAGS_NONE,      // Load flags
+                         nullptr,              // Referring URI
+                         newPostData,          // Post data stream
+                         nullptr,              // Headers stream
+                         triggeringPrincipal); // TriggeringPrincipal
         }
       }
     }
 
     // Well, fixup didn't work :-(
     // It is time to throw an error dialog box, and be done with it...
 
     // Errors to be shown only on top-level frames
@@ -11336,16 +11341,27 @@ nsDocShell::DoURILoad(nsIURI* aURI,
     if (IsFrame() && win) {
       nsCOMPtr<Element> frameElement = win->GetFrameElementInternal();
       if (frameElement) {
         timedChannel->SetInitiatorType(frameElement->LocalName());
       }
     }
   }
 
+  // Mark the http channel as UrgentStart for top level document loading
+  // in active tab.
+  if (mIsActive || (mLoadType & (LOAD_CMD_NORMAL | LOAD_CMD_HISTORY))) {
+    if (httpChannel && isTopLevelDoc) {
+      nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(channel));
+      if (cos) {
+        cos->AddClassFlags(nsIClassOfService::UrgentStart);
+      }
+    }
+  }
+
   rv = DoChannelLoad(channel, uriLoader, aBypassClassifier);
 
   //
   // If the channel load failed, we failed and nsIWebProgress just ain't
   // gonna happen.
   //
   if (NS_SUCCEEDED(rv)) {
     if (aDocShell) {
--- a/docshell/base/nsDocShellTreeOwner.cpp
+++ b/docshell/base/nsDocShellTreeOwner.cpp
@@ -4,16 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // Local Includes
 #include "nsDocShellTreeOwner.h"
 #include "nsWebBrowser.h"
 
 // Helper Classes
+#include "nsContentUtils.h"
 #include "nsStyleCoord.h"
 #include "nsSize.h"
 #include "mozilla/ReflowInput.h"
 #include "nsIServiceManager.h"
 #include "nsComponentManagerUtils.h"
 #include "nsXPIDLString.h"
 #include "nsIAtom.h"
 #include "nsReadableUtils.h"
@@ -999,17 +1000,18 @@ nsDocShellTreeOwner::HandleEvent(nsIDOME
               }
               free(links);
               return rv;
             }
           }
           nsAutoString url;
           if (NS_SUCCEEDED(links[0]->GetUrl(url))) {
             if (!url.IsEmpty()) {
-              webnav->LoadURI(url.get(), 0, nullptr, nullptr, nullptr);
+              webnav->LoadURI(url.get(), 0, nullptr, nullptr, nullptr,
+                              nsContentUtils::GetSystemPrincipal());
             }
           }
 
           for (uint32_t i = 0; i < linksCount; i++) {
             NS_RELEASE(links[i]);
           }
           free(links);
         }
--- a/docshell/base/nsIWebNavigation.idl
+++ b/docshell/base/nsIWebNavigation.idl
@@ -231,22 +231,28 @@ interface nsIWebNavigation : nsISupports
    *        sequence separating any HTTP headers from the HTTP request body.
    *        This parameter is optional and may be null.
    * @param aHeaders
    *        If the URI corresponds to a HTTP request, then any HTTP headers
    *        contained in this stream are set on the HTTP request.  The HTTP
    *        header stream is formatted as:
    *            ( HEADER "\r\n" )*
    *        This parameter is optional and may be null.
+   * @param aTriggeringPrincipal
+   *        The principal that initiated the load of aURI. If omitted docShell
+   *        tries to create a codeBasePrincipal from aReferrer if not null. If
+   *        aReferrer is also null docShell peforms a load using the
+   *        SystemPrincipal as the triggeringPrincipal.
    */
-  void loadURI(in wstring        aURI,
-               in unsigned long  aLoadFlags,
-               in nsIURI         aReferrer,
-               in nsIInputStream aPostData,
-               in nsIInputStream aHeaders);
+  void loadURI(in wstring                  aURI,
+               in unsigned long            aLoadFlags,
+               in nsIURI                   aReferrer,
+               in nsIInputStream           aPostData,
+               in nsIInputStream           aHeaders,
+               [optional] in nsIPrincipal  aTriggeringPrincipal);
 
   /**
    * Loads a given URI.  This will give priority to loading the requested URI
    * in the object implementing this interface.  If it can't be loaded here
    * however, the URI dispatcher will go through its normal process of content
    * loading.
    *
    * Behaves like loadURI, but allows passing of additional parameters.
--- a/docshell/base/timeline/AbstractTimelineMarker.cpp
+++ b/docshell/base/timeline/AbstractTimelineMarker.cpp
@@ -60,18 +60,17 @@ AbstractTimelineMarker::SetCurrentTime()
 {
   TimeStamp now = TimeStamp::Now();
   SetCustomTime(now);
 }
 
 void
 AbstractTimelineMarker::SetCustomTime(const TimeStamp& aTime)
 {
-  bool isInconsistent = false;
-  mTime = (aTime - TimeStamp::ProcessCreation(isInconsistent)).ToMilliseconds();
+  mTime = (aTime - TimeStamp::ProcessCreation()).ToMilliseconds();
 }
 
 void
 AbstractTimelineMarker::SetCustomTime(DOMHighResTimeStamp aTime)
 {
   mTime = aTime;
 }
 
--- a/docshell/shistory/nsSHistory.cpp
+++ b/docshell/shistory/nsSHistory.cpp
@@ -1619,17 +1619,18 @@ nsSHistory::SetOriginAttributesBeforeLoa
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::LoadURI(const char16_t* aURI,
                     uint32_t aLoadFlags,
                     nsIURI* aReferringURI,
                     nsIInputStream* aPostStream,
-                    nsIInputStream* aExtraHeaderStream)
+                    nsIInputStream* aExtraHeaderStream,
+                    nsIPrincipal* aTriggeringPrincipal)
 {
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSHistory::GotoIndex(int32_t aGlobalIndex)
 {
   // We provide abstraction of grouped session history for nsIWebNavigation
--- a/dom/animation/EffectCompositor.cpp
+++ b/dom/animation/EffectCompositor.cpp
@@ -514,34 +514,27 @@ EffectCompositor::GetServoAnimationRule(
 /* static */ dom::Element*
 EffectCompositor::GetElementToRestyle(dom::Element* aElement,
                                       CSSPseudoElementType aPseudoType)
 {
   if (aPseudoType == CSSPseudoElementType::NotPseudo) {
     return aElement;
   }
 
-  nsIFrame* primaryFrame = aElement->GetPrimaryFrame();
-  if (!primaryFrame) {
-    return nullptr;
+  if (aPseudoType == CSSPseudoElementType::before) {
+    return nsLayoutUtils::GetBeforePseudo(aElement);
   }
-  nsIFrame* pseudoFrame;
-  if (aPseudoType == CSSPseudoElementType::before) {
-    pseudoFrame = nsLayoutUtils::GetBeforeFrame(primaryFrame);
-  } else if (aPseudoType == CSSPseudoElementType::after) {
-    pseudoFrame = nsLayoutUtils::GetAfterFrame(primaryFrame);
-  } else {
-    NS_NOTREACHED("Should not try to get the element to restyle for a pseudo "
-                  "other that :before or :after");
-    return nullptr;
+
+  if (aPseudoType == CSSPseudoElementType::after) {
+    return nsLayoutUtils::GetAfterPseudo(aElement);
   }
-  if (!pseudoFrame) {
-    return nullptr;
-  }
-  return pseudoFrame->GetContent()->AsElement();
+
+  NS_NOTREACHED("Should not try to get the element to restyle for a pseudo "
+                "other that :before or :after");
+  return nullptr;
 }
 
 bool
 EffectCompositor::HasPendingStyleUpdates() const
 {
   for (auto& elementSet : mElementsToRestyle) {
     if (elementSet.Count()) {
       return true;
--- a/dom/animation/KeyframeEffectReadOnly.cpp
+++ b/dom/animation/KeyframeEffectReadOnly.cpp
@@ -1459,29 +1459,27 @@ KeyframeEffectReadOnly::CanThrottleTrans
 
 nsIFrame*
 KeyframeEffectReadOnly::GetAnimationFrame() const
 {
   if (!mTarget) {
     return nullptr;
   }
 
-  nsIFrame* frame = mTarget->mElement->GetPrimaryFrame();
-  if (!frame) {
-    return nullptr;
-  }
-
+  nsIFrame* frame;
   if (mTarget->mPseudoType == CSSPseudoElementType::before) {
-    frame = nsLayoutUtils::GetBeforeFrame(frame);
+    frame = nsLayoutUtils::GetBeforeFrame(mTarget->mElement);
   } else if (mTarget->mPseudoType == CSSPseudoElementType::after) {
-    frame = nsLayoutUtils::GetAfterFrame(frame);
+    frame = nsLayoutUtils::GetAfterFrame(mTarget->mElement);
   } else {
+    frame = mTarget->mElement->GetPrimaryFrame();
     MOZ_ASSERT(mTarget->mPseudoType == CSSPseudoElementType::NotPseudo,
                "unknown mTarget->mPseudoType");
   }
+
   if (!frame) {
     return nullptr;
   }
 
   return nsLayoutUtils::GetStyleFrame(frame);
 }
 
 nsIDocument*
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/crashtests/1359658-1.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<html class="reftest-wait">
+  <head>
+    <meta charset=utf-8>
+    <title>Bug 1359658: Animation-only dirty descendants bit should be cleared
+           for display:none content</title>
+  </head>
+  <body>
+  <div id="ancestor">
+    <svg>
+      <rect id="target" width="100%" height="100%" fill="lime"/>
+    </svg>
+  </div>
+  </body>
+  <script>
+'use strict';
+
+const ancestor = document.getElementById('ancestor');
+const target   = document.getElementById('target');
+
+document.addEventListener('DOMContentLoaded', () => {
+  const animation = target.animate({ color: [ 'red', 'lime' ] },
+                                   { duration: 1000, iterations: Infinity });
+  requestAnimationFrame(() => {
+    // Tweak animation to cause animation dirty bit to be set
+    animation.effect.timing.duration = 2000;
+    ancestor.style.display = "none";
+    getComputedStyle(ancestor).display;
+    document.documentElement.className = '';
+  });
+});
+  </script>
+</html>
--- a/dom/animation/test/crashtests/crashtests.list
+++ b/dom/animation/test/crashtests/crashtests.list
@@ -21,8 +21,9 @@ pref(dom.animations-api.core.enabled,tru
 pref(dom.animations-api.core.enabled,true) load 1325193-1.html
 pref(dom.animations-api.core.enabled,true) load 1330190-1.html
 pref(dom.animations-api.core.enabled,true) load 1330190-2.html
 pref(dom.animations-api.core.enabled,true) load 1330513-1.html
 pref(dom.animations-api.core.enabled,true) load 1333539-1.html
 pref(dom.animations-api.core.enabled,true) load 1333539-2.html
 pref(dom.animations-api.core.enabled,true) load 1333418-1.html
 pref(dom.animations-api.core.enabled,true) load 1343589-1.html
+pref(dom.animations-api.core.enabled,true) load 1359658-1.html
--- a/dom/base/ChildIterator.cpp
+++ b/dom/base/ChildIterator.cpp
@@ -312,57 +312,48 @@ ExplicitChildIterator::GetPreviousChild(
   return mChild;
 }
 
 nsIContent*
 AllChildrenIterator::Get() const
 {
   switch (mPhase) {
     case eAtBeforeKid: {
-      nsIFrame* frame = mOriginalContent->GetPrimaryFrame();
-      MOZ_ASSERT(frame, "No frame at eAtBeforeKid phase");
-      nsIFrame* beforeFrame = nsLayoutUtils::GetBeforeFrame(frame);
-      MOZ_ASSERT(beforeFrame, "No content before frame at eAtBeforeKid phase");
-      return beforeFrame->GetContent();
+      Element* before = nsLayoutUtils::GetBeforePseudo(mOriginalContent);
+      MOZ_ASSERT(before, "No content before frame at eAtBeforeKid phase");
+      return before;
     }
 
     case eAtExplicitKids:
       return ExplicitChildIterator::Get();
 
     case eAtAnonKids:
       return mAnonKids[mAnonKidsIdx];
 
     case eAtAfterKid: {
-      nsIFrame* frame = mOriginalContent->GetPrimaryFrame();
-      MOZ_ASSERT(frame, "No frame at eAtAfterKid phase");
-      nsIFrame* afterFrame = nsLayoutUtils::GetAfterFrame(frame);
-      MOZ_ASSERT(afterFrame, "No content before frame at eAtBeforeKid phase");
-      return afterFrame->GetContent();
+      Element* after = nsLayoutUtils::GetAfterPseudo(mOriginalContent);
+      MOZ_ASSERT(after, "No content after frame at eAtAfterKid phase");
+      return after;
     }
 
     default:
       return nullptr;
   }
 }
 
 
 bool
 AllChildrenIterator::Seek(nsIContent* aChildToFind)
 {
   if (mPhase == eAtBegin || mPhase == eAtBeforeKid) {
     mPhase = eAtExplicitKids;
-    nsIFrame* frame = mOriginalContent->GetPrimaryFrame();
-    if (frame) {
-      nsIFrame* beforeFrame = nsLayoutUtils::GetBeforeFrame(frame);
-      if (beforeFrame) {
-        if (beforeFrame->GetContent() == aChildToFind) {
-          mPhase = eAtBeforeKid;
-          return true;
-        }
-      }
+    Element* beforePseudo = nsLayoutUtils::GetBeforePseudo(mOriginalContent);
+    if (beforePseudo && beforePseudo == aChildToFind) {
+      mPhase = eAtBeforeKid;
+      return true;
     }
   }
 
   if (mPhase == eAtExplicitKids) {
     if (ExplicitChildIterator::Seek(aChildToFind)) {
       return true;
     }
     mPhase = eAtAnonKids;
@@ -399,23 +390,20 @@ AllChildrenIterator::AppendNativeAnonymo