Merge fx-team to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 09 Sep 2015 13:43:01 -0700
changeset 294120 7c8e40e00bfe521953b69051e5d59551a684c346
parent 294108 e21caed7a7f4bd1c5eed351997e07959d7b86320 (current diff)
parent 294119 8968d387064d001fe3a8eac583d9b48a26c58140 (diff)
child 294191 dd2a1d737a64d9a3f23714ec5cc623ec8933b51f
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to central, a=merge
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -123,20 +123,16 @@
         <!-- Connection is Not Secure -->
         <description when-connection="not-secure">&identity.description.insecure;</description>
 
         <!-- Weak Cipher -->
         <description when-ciphers="weak">&identity.description.weakCipher;</description>
         <description class="identity-popup-warning-yellow"
                      when-ciphers="weak">&identity.description.weakCipher2;</description>
 
-        <!-- More Security Information -->
-        <button label="&identity.moreInfoLinkText2;"
-                oncommand="gIdentityHandler.handleMoreInfoClick(event);"/>
-
         <!-- Active Mixed Content Blocked -->
         <description class="identity-popup-warning-gray"
                      when-mixedcontent="active-blocked">&identity.description.activeBlocked; <label observes="identity-popup-mcb-learn-more"/></description>
 
         <!-- Passive Mixed Content Loaded -->
         <description when-mixedcontent="passive-loaded">&identity.description.passiveLoaded;</description>
         <description class="identity-popup-warning-yellow"
                      when-mixedcontent="passive-loaded">&identity.description.passiveLoaded2; <label observes="identity-popup-mcb-learn-more"/></description>
@@ -154,16 +150,20 @@
         <button when-mixedcontent="active-blocked"
                 label="&identity.disableMixedContentBlocking.label;"
                 accesskey="&identity.disableMixedContentBlocking.accesskey;"
                 oncommand="gIdentityHandler.disableMixedContentProtection()"/>
         <button when-mixedcontent="active-loaded"
                 label="&identity.enableMixedContentBlocking.label;"
                 accesskey="&identity.enableMixedContentBlocking.accesskey;"
                 oncommand="gIdentityHandler.enableMixedContentProtection()"/>
+
+        <!-- More Security Information -->
+        <button label="&identity.moreInfoLinkText2;"
+                oncommand="gIdentityHandler.handleMoreInfoClick(event);"/>
       </vbox>
 
     </panelview>
 
     <!-- Permissions SubView -->
     <panelview id="identity-popup-permissionsView" flex="1">
       <vbox id="identity-popup-permissionsView-header">
         <label class="identity-popup-headline"
--- a/browser/devtools/animationinspector/components.js
+++ b/browser/devtools/animationinspector/components.js
@@ -975,16 +975,16 @@ AnimationsTimeline.prototype = {
     // Delay.
     if (delay) {
       // Negative delays need to start at 0.
       let x = TimeScale.durationToDistance((delay < 0 ? 0 : delay) / rate, width);
       let w = TimeScale.durationToDistance(Math.abs(delay) / rate, width);
       createNode({
         parent: iterations,
         attributes: {
-          "class": "delay",
+          "class": "delay" + (delay < 0 ? " negative" : ""),
           "style": `left:-${x}px;
                     width:${w}px;`
         }
       });
     }
   }
 };
--- a/browser/devtools/animationinspector/test/browser.ini
+++ b/browser/devtools/animationinspector/test/browser.ini
@@ -1,20 +1,23 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   doc_body_animation.html
   doc_frame_script.js
   doc_modify_playbackRate.html
+  doc_negative_animation.html
   doc_simple_animation.html
   head.js
 
+[browser_animation_controller_exposes_document_currentTime.js]
 [browser_animation_empty_on_invalid_nodes.js]
 [browser_animation_iterationCount_hidden_by_default.js]
+[browser_animation_mutations_with_same_names.js]
 [browser_animation_panel_exists.js]
 [browser_animation_participate_in_inspector_update.js]
 [browser_animation_play_pause_button.js]
 [browser_animation_playerFronts_are_refreshed.js]
 [browser_animation_playerWidgets_appear_on_panel_init.js]
 [browser_animation_playerWidgets_compositor_icon.js]
 [browser_animation_playerWidgets_destroy.js]
 [browser_animation_playerWidgets_disables_on_finished.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_controller_exposes_document_currentTime.js
@@ -0,0 +1,43 @@
+/* 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 controller provides the document.timeline currentTime (at least
+// the last known version since new animations were added).
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {panel, controller} = yield openAnimationInspectorNewUI();
+
+  ok(controller.documentCurrentTime, "The documentCurrentTime getter exists");
+  checkDocumentTimeIsCorrect(controller);
+  let time1 = controller.documentCurrentTime;
+
+  yield startNewAnimation(controller, panel);
+  checkDocumentTimeIsCorrect(controller);
+  let time2 = controller.documentCurrentTime;
+  ok(time2 > time1, "The new documentCurrentTime is higher than the old one");
+});
+
+function checkDocumentTimeIsCorrect(controller) {
+  let time = 0;
+  for (let {state} of controller.animationPlayers) {
+    time = Math.max(time, state.documentCurrentTime);
+  }
+  is(controller.documentCurrentTime, time,
+     "The documentCurrentTime is correct");
+}
+
+function* startNewAnimation(controller, panel) {
+  info("Add a new animation to the page and check the time again");
+  let onPlayerAdded = controller.once(controller.PLAYERS_UPDATED_EVENT);
+  yield executeInContent("devtools:test:setAttribute", {
+    selector: ".still",
+    attributeName: "class",
+    attributeValue: "ball still short"
+  });
+  yield onPlayerAdded;
+  yield waitForAllAnimationTargets(panel);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_mutations_with_same_names.js
@@ -0,0 +1,31 @@
+/* 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";
+
+// Check that when animations are added later (through animation mutations) and
+// if these animations have the same names, then all of them are still being
+// displayed (which should be true as long as these animations apply to
+// different nodes).
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_negative_animation.html");
+  let {controller, panel} = yield openAnimationInspectorNewUI();
+
+  info("Wait until all animations have been added " +
+       "(they're added with setTimeout)");
+  while (controller.animationPlayers.length < 3) {
+    yield controller.once(controller.PLAYERS_UPDATED_EVENT);
+  }
+  yield waitForAllAnimationTargets(panel);
+
+  is(panel.animationsTimelineComponent.animations.length, 3,
+     "The timeline shows 3 animations too");
+
+  // Reduce the known nodeFronts to a set to make them unique.
+  let nodeFronts = new Set(panel.animationsTimelineComponent
+                                .targetNodes.map(n => n.nodeFront));
+  is(nodeFronts.size, 3,
+     "The animations are applied to 3 different node fronts");
+});
--- a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_enabled.js
+++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_scrubber_enabled.js
@@ -15,27 +15,25 @@ add_task(function*() {
   yield selectNode(".animated", inspector);
 
   info("Get the player widget's timeline element");
   let widget = panel.playerWidgets[0];
   let timeline = widget.currentTimeEl;
 
   ok(!timeline.hasAttribute("disabled"), "The timeline input[range] is enabled");
   ok(widget.setCurrentTime, "The widget has the setCurrentTime method");
-  ok(widget.player.setCurrentTime, "The associated player front has the setCurrentTime method");
+  ok(widget.player.setCurrentTime,
+     "The associated player front has the setCurrentTime method");
 
   info("Faking an older server version by setting " +
-    "AnimationsController.traits.hasSetCurrentTime to false");
+       "AnimationsController.traits.hasSetCurrentTime to false");
 
   yield selectNode("body", inspector);
   controller.traits.hasSetCurrentTime = false;
 
   yield selectNode(".animated", inspector);
 
   info("Get the player widget's timeline element");
   widget = panel.playerWidgets[0];
   timeline = widget.currentTimeEl;
 
   ok(timeline.hasAttribute("disabled"), "The timeline input[range] is disabled");
-
-  yield selectNode("body", inspector);
-  controller.traits.hasSetCurrentTime = true;
 });
--- a/browser/devtools/animationinspector/test/browser_animation_refresh_on_removed_animation.js
+++ b/browser/devtools/animationinspector/test/browser_animation_refresh_on_removed_animation.js
@@ -42,16 +42,17 @@ function* testRefreshOnRemove(inspector,
   info("Add an finite animation on the node again, and wait for it to appear");
   onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
   yield executeInContent("devtools:test:setAttribute", {
     selector: ".test-node",
     attributeName: "class",
     attributeValue: "ball short test-node"
   });
   yield onPanelUpdated;
+  yield waitForAllAnimationTargets(panel);
 
   assertAnimationsDisplayed(panel, 1);
 }
 
 function* testAddedAnimationWorks(inspector, panel) {
   info("Now wait until the animation finishes");
   let widget = panel.playerWidgets[0];
   yield waitForPlayState(widget.player, "finished");
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_exists.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_exists.js
@@ -4,16 +4,15 @@
 
 "use strict";
 
 // Check that the timeline-based UI does have a scrubber element.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
   let {panel} = yield openAnimationInspectorNewUI();
-  yield waitForAllAnimationTargets(panel);
 
   let timeline = panel.animationsTimelineComponent;
   let scrubberEl = timeline.scrubberEl;
 
   ok(scrubberEl, "The scrubber element exists");
   ok(scrubberEl.classList.contains("scrubber"), "It has the right classname");
 });
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_movable.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_movable.js
@@ -6,31 +6,30 @@
 
 // Check that the scrubber in the timeline-based UI can be moved by clicking &
 // dragging in the header area.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
 
   let {panel} = yield openAnimationInspectorNewUI();
-  yield waitForAllAnimationTargets(panel);
 
   let timeline = panel.animationsTimelineComponent;
   let win = timeline.win;
   let timeHeaderEl = timeline.timeHeaderEl;
   let scrubberEl = timeline.scrubberEl;
 
   info("Mousedown in the header to move the scrubber");
   EventUtils.synthesizeMouse(timeHeaderEl, 50, 1, {type: "mousedown"}, win);
-  let newPos = parseInt(scrubberEl.style.left);
+  let newPos = parseInt(scrubberEl.style.left, 10);
   is(newPos, 50, "The scrubber moved on mousedown");
 
   info("Continue moving the mouse and verify that the scrubber tracks it");
   EventUtils.synthesizeMouse(timeHeaderEl, 100, 1, {type: "mousemove"}, win);
-  newPos = parseInt(scrubberEl.style.left);
+  newPos = parseInt(scrubberEl.style.left, 10);
   is(newPos, 100, "The scrubber followed the mouse");
 
   info("Release the mouse and move again and verify that the scrubber stays");
   EventUtils.synthesizeMouse(timeHeaderEl, 100, 1, {type: "mouseup"}, win);
   EventUtils.synthesizeMouse(timeHeaderEl, 200, 1, {type: "mousemove"}, win);
-  newPos = parseInt(scrubberEl.style.left);
+  newPos = parseInt(scrubberEl.style.left, 10);
   is(newPos, 100, "The scrubber stopped following the mouse");
 });
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_moves.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_scrubber_moves.js
@@ -7,25 +7,20 @@
 // Check that the scrubber in the timeline-based UI moves when animations are
 // playing.
 // The animations in the test page last for a very long time, so the test just
 // measures the position of the scrubber once, then waits for some time to pass
 // and measures its position again.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
-
   let {panel} = yield openAnimationInspectorNewUI();
-  yield waitForAllAnimationTargets(panel);
 
   let timeline = panel.animationsTimelineComponent;
-  let win = timeline.win;
-  let timeHeaderEl = timeline.timeHeaderEl;
   let scrubberEl = timeline.scrubberEl;
-
   let startPos = scrubberEl.getBoundingClientRect().left;
 
   info("Wait for some time to check that the scrubber moves");
   yield new Promise(r => setTimeout(r, 2000));
 
   let endPos = scrubberEl.getBoundingClientRect().left;
 
   ok(endPos > startPos, "The scrubber has moved");
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_shows_delay.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_shows_delay.js
@@ -1,30 +1,52 @@
 /* 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";
 
 // Check that animation delay is visualized in the timeline-based UI when the
 // animation is delayed.
+// Also check that negative delays do not overflow the UI, and are shown like
+// positive delays.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel} = yield openAnimationInspectorNewUI();
 
   info("Selecting a delayed animated node");
   yield selectNode(".delayed", inspector);
-
-  info("Getting the animation and delay elements from the panel");
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
-  let delay = timelineEl.querySelector(".delay");
-
-  ok(delay, "The animation timeline contains the delay element");
+  checkDelayAndName(timelineEl, true);
 
   info("Selecting a no-delay animated node");
   yield selectNode(".animated", inspector);
+  checkDelayAndName(timelineEl, false);
 
-  info("Getting the animation and delay elements from the panel again");
-  delay = timelineEl.querySelector(".delay");
+  info("Selecting a negative-delay animated node");
+  yield selectNode(".negative-delay", inspector);
+  checkDelayAndName(timelineEl, true);
+});
+
+function checkDelayAndName(timelineEl, hasDelay) {
+  let delay = timelineEl.querySelector(".delay");
+
+  is(!!delay, hasDelay, "The timeline " +
+                        (hasDelay ? "contains" : "does not contain") +
+                        " a delay element, as expected");
 
-  ok(!delay, "The animation timeline contains no delay element");
-});
+  if (hasDelay) {
+    let name = timelineEl.querySelector(".name");
+    let targetNode = timelineEl.querySelector(".target");
+
+    // Check that the delay element does not cause the timeline to overflow.
+    let delayRect = delay.getBoundingClientRect();
+    let sidebarWidth = targetNode.getBoundingClientRect().width;
+    ok(delayRect.x >= sidebarWidth,
+       "The delay element isn't displayed over the sidebar");
+
+    // Check that the delay is not displayed on top of the name.
+    let nameLeft = name.getBoundingClientRect().left;
+    ok(delayRect.right <= nameLeft,
+       "The delay element does not span over the name element");
+  }
+}
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_shows_time_info.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_shows_time_info.js
@@ -5,25 +5,24 @@
 "use strict";
 
 // Check that the timeline-based UI displays animations' duration, delay and
 // iteration counts in tooltips.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
   let {panel} = yield openAnimationInspectorNewUI();
-  yield waitForAllAnimationTargets(panel);
 
   info("Getting the animation element from the panel");
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
   let timeBlockNameEls = timelineEl.querySelectorAll(".time-block .name");
 
   // Verify that each time-block's name element has a tooltip that looks sort of
   // ok. We don't need to test the actual content.
   for (let el of timeBlockNameEls) {
     ok(el.hasAttribute("title"), "The tooltip is defined");
 
     let title = el.getAttribute("title");
-    ok(title.match(/Delay: [\d.]+s/), "The tooltip shows the delay");
-    ok(title.match(/Duration: [\d.]+s/), "The tooltip shows the delay");
+    ok(title.match(/Delay: [\d.-]+s/), "The tooltip shows the delay");
+    ok(title.match(/Duration: [\d.]+s/), "The tooltip shows the duration");
     ok(title.match(/Repeats: /), "The tooltip shows the iterations");
   }
 });
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_takes_rate_into_account.js
@@ -10,17 +10,16 @@
 // because there might be multiple animations displayed at the same time, some
 // of which may have a different rate than others. Those that have had their
 // rate changed have a delay = delay/rate and a duration = duration/rate.
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_modify_playbackRate.html");
 
   let {panel} = yield openAnimationInspectorNewUI();
-  yield waitForAllAnimationTargets(panel);
 
   let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
 
   let timeBlocks = timelineEl.querySelectorAll(".time-block");
   is(timeBlocks.length, 2, "2 animations are displayed");
 
   info("The first animation has its rate set to 1, let's measure it");
 
--- a/browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js
+++ b/browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js
@@ -18,19 +18,19 @@ add_task(function*() {
   yield testDataUpdates(ui, true);
 });
 
 function* testDataUpdates({panel, controller, inspector}, isNewUI=false) {
   info("Select the test node");
   yield selectNode(".animated", inspector);
 
   let animation = controller.animationPlayers[0];
-  yield setStyle(animation, "animationDuration", "5.5s", isNewUI);
-  yield setStyle(animation, "animationIterationCount", "300", isNewUI);
-  yield setStyle(animation, "animationDelay", "45s", isNewUI);
+  yield setStyle(animation, panel, "animationDuration", "5.5s", isNewUI);
+  yield setStyle(animation, panel, "animationIterationCount", "300", isNewUI);
+  yield setStyle(animation, panel, "animationDelay", "45s", isNewUI);
 
   if (isNewUI) {
     let animationsEl = panel.animationsTimelineComponent.animationsEl;
     let timeBlockEl = animationsEl.querySelector(".time-block");
 
     // 45s delay + (300 * 5.5)s duration
     let expectedTotalDuration = 1695 * 1000;
     let timeRatio = expectedTotalDuration / timeBlockEl.offsetWidth;
@@ -47,26 +47,30 @@ function* testDataUpdates({panel, contro
       "The widget shows the new duration");
     is(widget.metaDataComponent.iterationValue.textContent, "300",
       "The widget shows the new iteration count");
     is(widget.metaDataComponent.delayValue.textContent, "45s",
       "The widget shows the new delay");
   }
 }
 
-function* setStyle(animation, name, value, isNewUI=false) {
+function* setStyle(animation, panel, name, value, isNewUI=false) {
   info("Change the animation style via the content DOM. Setting " +
     name + " to " + value);
 
   let onAnimationChanged = once(animation, "changed");
   yield executeInContent("devtools:test:setStyle", {
     selector: ".animated",
     propertyName: name,
     propertyValue: value
   });
   yield onAnimationChanged;
 
+  // Also wait for the target node previews to be loaded if the panel got
+  // refreshed as a result of this animation mutation.
+  yield waitForAllAnimationTargets(panel);
+
   // If this is the playerWidget-based UI, wait for the auto-refresh event too
   // to make sure the UI has updated.
   if (!isNewUI) {
     yield once(animation, animation.AUTO_REFRESH_EVENT);
   }
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/doc_negative_animation.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <style>
+  html, body {
+    margin: 0;
+    height: 100%;
+    overflow: hidden;
+  }
+
+  div {
+    position: absolute;
+    top: 0;
+    left: -500px;
+    height: 20px;
+    width: 500px;
+    color: red;
+    background: linear-gradient(to left, currentColor, currentColor 2px, transparent);
+  }
+
+  .zero {
+    color: blue;
+    top: 20px;
+  }
+
+  .positive {
+    color: green;
+    top: 40px;
+  }
+
+  .negative.move { animation: 5s -1s move linear forwards; }
+  .zero.move { animation: 5s 0s move linear forwards; }
+  .positive.move { animation: 5s 1s move linear forwards; }
+
+  @keyframes move {
+    to {
+      transform: translateX(500px);
+    }
+  }
+  </style>
+</head>
+<body>
+  <div class="negative"></div>
+  <div class="zero"></div>
+  <div class="positive"></div>
+  <script>
+    var negative = document.querySelector(".negative");
+    var zero = document.querySelector(".zero");
+    var positive = document.querySelector(".positive");
+
+    // The non-delayed animation starts now.
+    zero.classList.add("move");
+    // The negative-delayed animation starts in 1 second.
+    setTimeout(function() {
+      negative.classList.add("move");
+    }, 1000);
+    // The positive-delayed animation starts in 200 ms.
+    setTimeout(function() {
+      positive.classList.add("move");
+    }, 200);
+  </script>
+</body>
+</html>
\ No newline at end of file
--- a/browser/devtools/animationinspector/test/doc_simple_animation.html
+++ b/browser/devtools/animationinspector/test/doc_simple_animation.html
@@ -60,16 +60,25 @@
     .long {
       top: 600px;
       left: 10px;
       background: blue;
 
       animation: simple-animation 120s;
     }
 
+    .negative-delay {
+      top: 700px;
+      left: 10px;
+      background: gray;
+
+      animation: simple-animation 15s -10s;
+      animation-fill-mode: forwards;
+    }
+
     @keyframes simple-animation {
       100% {
         transform: translateX(300px);
       }
     }
 
     @keyframes other-animation {
       100% {
@@ -82,10 +91,11 @@
   <!-- Comment node -->
   <div class="ball still"></div>
   <div class="ball animated"></div>
   <div class="ball multi"></div>
   <div class="ball delayed"></div>
   <div class="ball multi-finite"></div>
   <div class="ball short"></div>
   <div class="ball long"></div>
+  <div class="ball negative-delay"></div>
 </body>
 </html>
--- a/browser/devtools/animationinspector/test/head.js
+++ b/browser/devtools/animationinspector/test/head.js
@@ -16,16 +16,17 @@ const DevToolsUtils = require("devtools/
 // All tests are asynchronous
 waitForExplicitFinish();
 
 const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/animationinspector/test/";
 const ROOT_TEST_DIR = getRootDirectory(gTestPath);
 const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
 const COMMON_FRAME_SCRIPT_URL = "chrome://browser/content/devtools/frame-script-utils.js";
 const NEW_UI_PREF = "devtools.inspector.animationInspectorV3";
+const TAB_NAME = "animationinspector";
 
 // Auto clean-up when a test ends
 registerCleanupFunction(function*() {
   yield closeAnimationInspector();
 
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
@@ -124,16 +125,23 @@ let selectNode = Task.async(function*(da
   info("Selecting the node for '" + data + "'");
   let nodeFront = data;
   if (!data._form) {
     nodeFront = yield getNodeFront(data, inspector);
   }
   let updated = inspector.once("inspector-updated");
   inspector.selection.setNodeFront(nodeFront, reason);
   yield updated;
+
+  // 99% of the times, selectNode is called to select an animated node, and we
+  // want to make sure the rest of the test waits for the animations to be
+  // properly displayed (wait for all target DOM nodes to be previewed).
+  // Even if there are no animations, this is safe to do.
+  let {AnimationsPanel} = inspector.sidebar.getWindowForTab(TAB_NAME);
+  yield waitForAllAnimationTargets(AnimationsPanel);
 });
 
 /**
  * Check if there are the expected number of animations being displayed in the
  * panel right now.
  * @param {AnimationsPanel} panel
  * @param {Number} nbAnimations The expected number of animations.
  * @param {String} msg An optional string to be used as the assertion message.
@@ -154,17 +162,17 @@ function assertAnimationsDisplayed(panel
  * Takes an Inspector panel that was just created, and waits
  * for a "inspector-updated" event as well as the animation inspector
  * sidebar to be ready. Returns a promise once these are completed.
  *
  * @param {InspectorPanel} inspector
  * @return {Promise}
  */
 let waitForAnimationInspectorReady = Task.async(function*(inspector) {
-  let win = inspector.sidebar.getWindowForTab("animationinspector");
+  let win = inspector.sidebar.getWindowForTab(TAB_NAME);
   let updated = inspector.once("inspector-updated");
 
   // In e10s, if we wait for underlying toolbox actors to
   // load (by setting DevToolsUtils.testing to true), we miss the
   // "animationinspector-ready" event on the sidebar, so check to see if the
   // iframe is already loaded.
   let tabReady = win.document.readyState === "complete" ?
                  promise.resolve() :
@@ -187,31 +195,36 @@ let openAnimationInspector = Task.async(
   info("Switching to the animationinspector");
   let inspector = toolbox.getPanel("inspector");
 
   let panelReady = waitForAnimationInspectorReady(inspector);
 
   info("Waiting for toolbox focus");
   yield waitForToolboxFrameFocus(toolbox);
 
-  inspector.sidebar.select("animationinspector");
+  inspector.sidebar.select(TAB_NAME);
 
   info("Waiting for the inspector and sidebar to be ready");
   yield panelReady;
 
-  let win = inspector.sidebar.getWindowForTab("animationinspector");
+  let win = inspector.sidebar.getWindowForTab(TAB_NAME);
   let {AnimationsController, AnimationsPanel} = win;
 
   info("Waiting for the animation controller and panel to be ready");
   if (AnimationsPanel.initialized) {
     yield AnimationsPanel.initialized;
   } else {
     yield AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED);
   }
 
+  // Make sure we wait for all animations to be loaded (especially their target
+  // nodes to be lazily displayed). This is safe to do even if there are no
+  // animations displayed.
+  yield waitForAllAnimationTargets(AnimationsPanel);
+
   return {
     toolbox: toolbox,
     inspector: inspector,
     controller: AnimationsController,
     panel: AnimationsPanel,
     window: win
   };
 });
@@ -245,21 +258,19 @@ let closeAnimationInspector = Task.async
  * @return a promise that resolves when the animation inspector is ready.
  */
 let closeAnimationInspectorAndRestartWithNewUI = Task.async(function*(reload) {
   info("Close the toolbox and test again with the new UI");
   yield closeAnimationInspector();
   if (reload) {
     yield reloadTab();
   }
-  enableNewUI();
-  return yield openAnimationInspector();
+  return yield openAnimationInspectorNewUI();
 });
 
-
 /**
  * Wait for the toolbox frame to receive focus after it loads
  * @param {Toolbox} toolbox
  * @return a promise that resolves when focus has been received
  */
 function waitForToolboxFrameFocus(toolbox) {
   info("Making sure that the toolbox's frame is focused");
   let def = promise.defer();
--- a/browser/devtools/animationinspector/test/unit/test_timeScale.js
+++ b/browser/devtools/animationinspector/test/unit/test_timeScale.js
@@ -5,36 +5,61 @@
 
 "use strict";
 
 const Cu = Components.utils;
 const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 const {TimeScale} = require("devtools/animationinspector/components");
 
 const TEST_ANIMATIONS = [{
-  startTime: 500,
-  delay: 0,
-  duration: 1000,
-  iterationCount: 1,
-  playbackRate: 1
+  desc: "Testing a few standard animations",
+  animations: [{
+    startTime: 500,
+    delay: 0,
+    duration: 1000,
+    iterationCount: 1,
+    playbackRate: 1
+  }, {
+    startTime: 400,
+    delay: 100,
+    duration: 10,
+    iterationCount: 100,
+    playbackRate: 1
+  }, {
+    startTime: 50,
+    delay: 1000,
+    duration: 100,
+    iterationCount: 20,
+    playbackRate: 1
+  }],
+  expectedMinStart: 50,
+  expectedMaxEnd: 3050
 }, {
-  startTime: 400,
-  delay: 100,
-  duration: 10,
-  iterationCount: 100,
-  playbackRate: 1
+  desc: "Testing a single negative-delay animation",
+  animations: [{
+    startTime: 100,
+    delay: -100,
+    duration: 100,
+    iterationCount: 1,
+    playbackRate: 1
+  }],
+  expectedMinStart: 0,
+  expectedMaxEnd: 100
 }, {
-  startTime: 50,
-  delay: 1000,
-  duration: 100,
-  iterationCount: 20,
-  playbackRate: 1
+  desc: "Testing a single negative-delay animation with a different rate",
+  animations: [{
+    startTime: 3500,
+    delay: -1000,
+    duration: 10000,
+    iterationCount: 2,
+    playbackRate: 2
+  }],
+  expectedMinStart: 3000,
+  expectedMaxEnd: 13000
 }];
-const EXPECTED_MIN_START = 50;
-const EXPECTED_MAX_END = 3050;
 
 const TEST_STARTTIME_TO_DISTANCE = [{
   time: 50,
   width: 100,
   expectedDistance: 0
 }, {
   time: 50,
   width: 0,
@@ -121,34 +146,37 @@ const TEST_FORMAT_TIME_S = [{
   expectedFormattedTime: "102.9s"
 }];
 
 function run_test() {
   do_print("Check the default min/max range values");
   equal(TimeScale.minStartTime, Infinity);
   equal(TimeScale.maxEndTime, 0);
 
-  do_print("Test adding a few animations");
-  for (let state of TEST_ANIMATIONS) {
+  for (let {desc, animations, expectedMinStart, expectedMaxEnd} of
+       TEST_ANIMATIONS) {
+    do_print("Test adding a few animations: " + desc);
+    for (let state of animations) {
+      TimeScale.addAnimation(state);
+    }
+
+    do_print("Checking the time scale range");
+    equal(TimeScale.minStartTime, expectedMinStart);
+    equal(TimeScale.maxEndTime, expectedMaxEnd);
+
+    do_print("Test reseting the animations");
+    TimeScale.reset();
+    equal(TimeScale.minStartTime, Infinity);
+    equal(TimeScale.maxEndTime, 0);
+  }
+
+  do_print("Add a set of animations again");
+  for (let state of TEST_ANIMATIONS[0].animations) {
     TimeScale.addAnimation(state);
   }
-  equal(TimeScale.minStartTime, EXPECTED_MIN_START);
-  equal(TimeScale.maxEndTime, EXPECTED_MAX_END);
-
-  do_print("Test reseting the animations");
-  TimeScale.reset();
-  equal(TimeScale.minStartTime, Infinity);
-  equal(TimeScale.maxEndTime, 0);
-
-  do_print("Test adding the animations again");
-  for (let state of TEST_ANIMATIONS) {
-    TimeScale.addAnimation(state);
-  }
-  equal(TimeScale.minStartTime, EXPECTED_MIN_START);
-  equal(TimeScale.maxEndTime, EXPECTED_MAX_END);
 
   do_print("Test converting start times to distances");
   for (let {time, width, expectedDistance} of TEST_STARTTIME_TO_DISTANCE) {
     let distance = TimeScale.startTimeToDistance(time, width);
     equal(distance, expectedDistance);
   }
 
   do_print("Test converting durations to distances");
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -115,18 +115,18 @@
 }
 
 #PanelUI-menu-button[badge-status="update-failed"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
   background: #D90000 url(chrome://browser/skin/update-badge-failed.svg) no-repeat center;
   height: 13px;
 }
 
 #PanelUI-menu-button[badge-status="fxa-needs-authentication"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
-  background-color: transparent;
-  background-image: url(chrome://browser/skin/warning.svg);
+  background: transparent url(chrome://browser/skin/warning.svg) no-repeat center;
+  height: 13px;
   box-shadow: none;
   filter: drop-shadow(0 1px 0 hsla(206, 50%, 10%, .15));
 }
 
 #PanelUI-menu-button[badge-status="fxa-needs-authentication"] > .toolbarbutton-badge-stack > .toolbarbutton-badge:-moz-window-inactive {
   filter: none;
 }
 
--- a/browser/themes/shared/devtools/animationinspector.css
+++ b/browser/themes/shared/devtools/animationinspector.css
@@ -8,17 +8,22 @@
   --even-animation-timeline-background-color: rgba(255,255,255,0.03);
 }
 
 .theme-light {
   --even-animation-timeline-background-color: rgba(128,128,128,0.03);
 }
 
 :root {
+  /* How high should toolbars be */
+  --toolbar-height: 20px;
+  /* How wide should the sidebar be */
   --timeline-sidebar-width: 150px;
+  /* How high should animations displayed in the timeline be */
+  --timeline-animation-height: 20px;
 }
 
 html {
   height: 100%;
 }
 
 body {
   margin: 0;
@@ -33,32 +38,32 @@ body {
 /* The top toolbar, containing the toggle-all button */
 
 #toolbar {
   border-bottom: 1px solid var(--theme-splitter-color);
   display: flex;
   flex-direction: row;
   align-items: center;
   justify-content: flex-end;
-  height: 20px;
+  height: var(--toolbar-height);
 }
 
 #toolbar .label {
   padding: 1px 4px;
 }
 
 #toggle-all {
   border-width: 0 0 0 1px;
-  min-height: 20px;
+  min-height: var(--toolbar-height);
 }
 
 /* The main animations container */
 
 #players {
-  height: calc(100% - 20px);
+  height: calc(100% - var(--toolbar-height));
   overflow: auto;
 }
 
 /* The error message, shown when an invalid/unanimated element is selected */
 
 #error-message {
   padding-top: 10%;
   text-align: center;
@@ -154,17 +159,17 @@ body {
   right: -6px;
   border-top: 5px solid red;
   border-left: 5px solid transparent;
   border-right: 5px solid transparent;
 }
 
 .animation-timeline .time-header {
   margin-left: var(--timeline-sidebar-width);
-  height: 20px;
+  min-height: var(--toolbar-height);
   overflow: hidden;
   position: relative;
   /* This is the same color as the time graduations in
      browser/devtools/animationinspector/utils.js */
   border-bottom: 1px solid rgba(128, 136, 144, .5);
   cursor: col-resize;
   -moz-user-select: none;
 }
@@ -182,17 +187,17 @@ body {
   padding: 0;
   list-style-type: none;
 }
 
 /* Animation block widgets */
 
 .animation-timeline .animation {
   margin: 4px 0;
-  height: 20px;
+  height: var(--timeline-animation-height);
   position: relative;
 }
 
 .animation-timeline .animation:nth-child(2n) {
   background-color: var(--even-animation-timeline-background-color);
 }
 
 .animation-timeline .animation .target {
@@ -284,24 +289,35 @@ body {
   padding: 0 2px;
 }
 
 .animation-timeline .animation .delay {
   position: absolute;
   top: 0;
   /* Make sure the delay covers up the animation border */
   transform: translate(-1px, -1px);
-  height: 100%;
+  box-sizing: border-box;
+  height: calc(100% + 2px);
+
+  border: 1px solid var(--timelime-border-color);
+  border-width: 1px 0 1px 1px;
   background-image: repeating-linear-gradient(45deg,
                                               transparent,
                                               transparent 1px,
                                               var(--theme-selection-color) 1px,
                                               var(--theme-selection-color) 4px);
   background-color: var(--timelime-border-color);
-  border: 1px solid var(--timelime-border-color);
+}
+
+.animation-timeline .animation .delay.negative {
+  /* Negative delays are displayed on top of the animation, so they need a
+     right border. Whereas normal delays are displayed just before the
+     animation, so there's already the animation's left border that serves as
+     a separation. */
+  border-width: 1px;
 }
 
 /* Animation target node gutter, contains a preview of the dom node */
 
 .animation-target {
   background-color: var(--theme-toolbar-background);
   padding: 1px 4px;
   box-sizing: border-box;
@@ -356,17 +372,17 @@ body {
        its tooltip appears on hover */
     z-index: 1;
     position: relative;
 }
 
 /* Timeline wiget */
 
 .timeline {
-  height: 20px;
+  height: var(--timeline-animation-height);
   width: 100%;
   display: flex;
   flex-direction: row;
   border-bottom: 1px solid var(--theme-splitter-color);
 }
 
 .timeline .playback-controls {
   display: flex;
--- a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
@@ -94,25 +94,26 @@
             android:id="@+id/tabs"
             style="@style/UrlBar.ImageButton"
             android:layout_toLeftOf="@id/menu"
             android:layout_alignWithParentIfMissing="true"
             android:background="@drawable/browser_toolbar_action_bar_button"/>
 
     <!-- In a 56x60dp space, centering 24dp image will leave 16x18dp. -->
     <org.mozilla.gecko.toolbar.TabCounter android:id="@+id/tabs_counter"
-                        style="@style/UrlBar.ImageButton.TabCount"
+                        style="@style/UrlBar.ImageButton"
                         android:layout_alignLeft="@id/tabs"
                         android:layout_alignRight="@id/tabs"
                         android:layout_alignTop="@id/tabs"
                         android:layout_alignBottom="@id/tabs"
                         android:layout_marginTop="18dp"
                         android:layout_marginBottom="18dp"
                         android:layout_marginLeft="16dp"
-                        android:layout_marginRight="16dp"/>
+                        android:layout_marginRight="16dp"
+                        android:background="@drawable/tabs_count"/>
 
     <org.mozilla.gecko.widget.themed.ThemedFrameLayout
             android:id="@+id/menu"
             style="@style/UrlBar.ImageButton"
             android:layout_alignParentRight="true"
             android:layout_marginRight="6dp"
             android:contentDescription="@string/menu"
             android:background="@drawable/browser_toolbar_action_bar_button"
@@ -128,15 +129,18 @@
                 android:layout_gravity="center"/>
 
     </org.mozilla.gecko.widget.themed.ThemedFrameLayout>
 
     <!-- We draw after the menu items so when they are hidden, the cancel button,
          which is thus drawn on top, may be pressed. -->
     <org.mozilla.gecko.widget.themed.ThemedImageView
             android:id="@+id/edit_cancel"
-            style="@style/UrlBar.ImageButton.Icon"
+            style="@style/UrlBar.ImageButton"
+            android:layout_width="@dimen/browser_toolbar_icon_width"
+            android:layout_height="@dimen/browser_toolbar_height"
+            android:layout_weight="0.0"
             android:layout_alignParentRight="true"
             android:src="@drawable/close_edit_mode_selector"
             android:contentDescription="@string/edit_mode_cancel"
             android:visibility="gone"/>
 
 </merge>
--- a/mobile/android/base/resources/layout/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout/browser_toolbar.xml
@@ -65,22 +65,26 @@
 
     <!-- The TextSwitcher should be shifted 24dp on the left, to avoid
          the curve. On a 48dp space, centering 24dp image will leave
          12dp on all sides. However this image has a perception of
          2 layers. Hence to center this, an additional 4dp is added to the left.
          The margins will be 40dp on left, 8dp on right, instead of ideal 30dp
          and 12dp. -->
     <org.mozilla.gecko.toolbar.TabCounter android:id="@+id/tabs_counter"
-                        style="@style/UrlBar.ImageButton.TabCount"
+                        style="@style/UrlBar.ImageButton"
                         android:layout_width="24dip"
                         android:layout_height="24dip"
                         android:layout_centerVertical="true"
                         android:layout_marginRight="8dip"
-                        android:layout_alignRight="@id/tabs"/>
+                        android:layout_alignRight="@id/tabs"
+                        android:background="@drawable/tabs_count"
+                        android:gravity="center_horizontal"
+                        android:clipChildren="false"
+                        android:clipToPadding="false"/>
 
     <!-- Note that the edit components are invisible so that the views
          depending on their location can properly layout. -->
     <org.mozilla.gecko.widget.themed.ThemedImageView
             android:id="@+id/edit_cancel"
             style="@style/UrlBar.ImageButton"
             android:layout_alignParentRight="true"
             android:src="@drawable/close_edit_mode_selector"
--- a/mobile/android/base/resources/values-large-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-v11/styles.xml
@@ -10,20 +10,16 @@
     </style>
 
     <!-- If this style wasn't actually shared outside the
          url bar, this name could be improved (bug 1197424). -->
     <style name="UrlBar.ImageButton.BrowserToolbarColors">
         <item name="drawableTintList">@color/action_bar_menu_item_colors</item>
     </style>
 
-    <style name="UrlBar.ImageButton.TabCount">
-        <item name="android:background">@drawable/tabs_count</item>
-    </style>
-
     <style name="UrlBar.Button.Container">
         <item name="android:layout_marginTop">6dp</item>
         <item name="android:layout_marginBottom">6dp</item>
         <!-- Start with forward hidden -->
         <item name="android:orientation">horizontal</item>
     </style>
 
     <style name="TabsLayout" parent="TabsLayoutBase">
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -452,23 +452,16 @@
 
     <!-- BrowserToolbar -->
     <style name="BrowserToolbar">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">@dimen/browser_toolbar_height</item>
         <item name="android:orientation">horizontal</item>
     </style>
 
-    <style name="UrlBar.ImageButton.TabCount">
-        <item name="android:background">@drawable/tabs_count</item>
-        <item name="android:gravity">center_horizontal</item>
-        <item name="android:clipChildren">false</item>
-        <item name="android:clipToPadding">false</item>
-    </style>
-
     <!-- URL bar -->
     <style name="UrlBar">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">match_parent</item>
         <item name="android:orientation">horizontal</item>
     </style>
 
     <!-- URL bar - Button -->
@@ -498,23 +491,16 @@
         <item name="android:layout_gravity">center_vertical</item>
         <item name="android:background">@android:color/transparent</item>
     </style>
 
     <style name="UrlBar.ImageButton" parent="UrlBar.ImageButtonBase">
         <item name="android:layout_width">@dimen/browser_toolbar_height</item>
     </style>
 
-    <!-- URL bar - Image Button - Icon -->
-    <style name="UrlBar.ImageButton.Icon">
-         <item name="android:layout_width">@dimen/browser_toolbar_icon_width</item>
-         <item name="android:layout_height">@dimen/browser_toolbar_height</item>
-         <item name="android:layout_weight">0.0</item>
-    </style>
-
     <!-- TabsLayout -->
     <style name="TabsLayoutBase">
          <item name="android:background">@android:color/transparent</item>
          <item name="android:listSelector">@android:color/transparent</item>
     </style>
 
     <style name="TabsLayout" parent="TabsLayoutBase">
          <item name="android:orientation">vertical</item>
--- a/mobile/android/base/toolbar/PageActionLayout.java
+++ b/mobile/android/base/toolbar/PageActionLayout.java
@@ -157,17 +157,17 @@ public class PageActionLayout extends Li
             }
         }
     }
 
     private ImageButton createImageButton() {
         ThreadUtils.assertOnUiThread();
 
         final int width = mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width);
-        ImageButton imageButton = new ImageButton(mContext, null, R.style.UrlBar_ImageButton_Icon);
+        ImageButton imageButton = new ImageButton(mContext, null, R.style.UrlBar_ImageButton);
         imageButton.setLayoutParams(new LayoutParams(width, LayoutParams.MATCH_PARENT));
         imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
         imageButton.setOnClickListener(this);
         imageButton.setOnLongClickListener(this);
         return imageButton;
     }
 
     @Override
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -1072,20 +1072,24 @@ EnvironmentCache.prototype = {
 
   /**
    * Get the CPU information.
    * @return Object containing the CPU information data.
    */
   _getCpuData: function () {
     let cpuData = {
       count: getSysinfoProperty("cpucount", null),
-      vendor: null, // TODO: bug 1128472
-      family: null, // TODO: bug 1128472
-      model: null, // TODO: bug 1128472
-      stepping: null, // TODO: bug 1128472
+      cores: getSysinfoProperty("cpucores", null),
+      vendor: getSysinfoProperty("cpuvendor", null),
+      family: getSysinfoProperty("cpufamily", null),
+      model: getSysinfoProperty("cpumodel", null),
+      stepping: getSysinfoProperty("cpustepping", null),
+      l2cacheKB: getSysinfoProperty("cpucachel2", null),
+      l3cacheKB: getSysinfoProperty("cpucachel3", null),
+      speedMHz: getSysinfoProperty("cpuspeed", null),
     };
 
     const CPU_EXTENSIONS = ["hasMMX", "hasSSE", "hasSSE2", "hasSSE3", "hasSSSE3",
                             "hasSSE4A", "hasSSE4_1", "hasSSE4_2", "hasEDSP", "hasARMv6",
                             "hasARMv7", "hasNEON"];
 
     // Enumerate the available CPU extensions.
     let availableExts = [];
@@ -1218,18 +1222,26 @@ EnvironmentCache.prototype = {
   _getSystem: function () {
     let memoryMB = getSysinfoProperty("memsize", null);
     if (memoryMB) {
       // Send RAM size in megabytes. Rounding because sysinfo doesn't
       // always provide RAM in multiples of 1024.
       memoryMB = Math.round(memoryMB / 1024 / 1024);
     }
 
+    let virtualMB = getSysinfoProperty("virtualmemsize", null);
+    if (virtualMB) {
+      // Send the total virtual memory size in megabytes. Rounding because
+      // sysinfo doesn't always provide RAM in multiples of 1024.
+      virtualMB = Math.round(virtualMB / 1024 / 1024);
+    }
+
     return {
       memoryMB: memoryMB,
+      virtualMaxMB: virtualMB,
 #ifdef XP_WIN
       isWow64: getSysinfoProperty("isWow64", null),
 #endif
       cpu: this._getCpuData(),
 #if defined(MOZ_WIDGET_GONK) || defined(MOZ_WIDGET_ANDROID)
       device: this._getDeviceData(),
 #endif
       os: this._getOSData(),
--- a/toolkit/components/telemetry/docs/environment.rst
+++ b/toolkit/components/telemetry/docs/environment.rst
@@ -70,23 +70,28 @@ Structure::
         distributor: <string>, // pref app.distributor, null on failure
         distributorChannel: <string>, // pref app.distributor.channel, null on failure
         partnerNames: [
           // list from prefs app.partner.<name>=<name>
         ],
       },
       system: {
         memoryMB: <number>,
+        virtualMaxMB: <number>, // windows-only
         isWow64: <bool>, // windows-only
         cpu: {
-            count: <number>,  // e.g. 8, or null on failure
-            vendor: <string>, // e.g. "GenuineIntel", or null on failure
-            family: <string>, // null on failure
-            model: <string>, // null on failure
-            stepping: <string>, // null on failure
+            count: <number>,  // desktop only, e.g. 8, or null on failure - logical cpus
+            cores: <number>, // desktop only, e.g., 4, or null on failure - physical cores
+            vendor: <string>, // desktop only, e.g. "GenuineIntel", or null on failure
+            family: <string>, // desktop only, null on failure
+            model: <string>, // desktop only, null on failure
+            stepping: <string>, // desktop only, null on failure
+            l2cacheKB: <number>, // L2 cache size in KB, only on windows & mac
+            l3cacheKB: <number>, // desktop only, L3 cache size in KB
+            speedMHz: <number>, // desktop only, cpu clock speed in MHz
             extensions: [
               <string>,
               ...
               // as applicable:
               // "MMX", "SSE", "SSE2", "SSE3", "SSSE3", "SSE4A", "SSE4_1",
               // "SSE4_2", "EDSP", "ARMv6", "ARMv7", "NEON"
             ],
         },
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -3,21 +3,23 @@
 
 const { classes: Cc, utils: Cu, interfaces: Ci, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://testing-common/httpd.js", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
 
-const gIsWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
-const gIsMac = ("@mozilla.org/xpcom/mac-utils;1" in Cc);
-const gIsAndroid =  ("@mozilla.org/android/bridge;1" in Cc);
-const gIsGonk = ("@mozilla.org/cellbroadcast/gonkservice;1" in Cc);
+const gIsWindows = AppConstants.platform == "win";
+const gIsMac = AppConstants.platform == "macosx";
+const gIsAndroid = AppConstants.platform == "android";
+const gIsGonk = AppConstants.platform == "gonk";
+const gIsLinux = AppConstants.platform == "linux";
 
 const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
 
 const MILLISECONDS_PER_MINUTE = 60 * 1000;
 const MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE;
 const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR;
 
 const HAS_DATAREPORTINGSERVICE = "@mozilla.org/datareporting/service;1" in Cc;
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -360,19 +360,50 @@ function checkSystemSection(data) {
   Assert.ok("system" in data, "There must be a system section in Environment.");
 
   // Make sure we have all the top level sections and fields.
   for (let f of EXPECTED_FIELDS) {
     Assert.ok(f in data.system, f + " must be available.");
   }
 
   Assert.ok(Number.isFinite(data.system.memoryMB), "MemoryMB must be a number.");
-  if (gIsWindows) {
-    Assert.equal(typeof data.system.isWow64, "boolean",
-              "isWow64 must be available on Windows and have the correct type.");
+
+  if (gIsWindows || gIsMac || gIsLinux) {
+    let EXTRA_CPU_FIELDS = ["cores", "model", "family", "stepping",
+			    "l2cacheKB", "l3cacheKB", "speedMHz", "vendor"];
+
+    for (let f of EXTRA_CPU_FIELDS) {
+      // Note this is testing TelemetryEnvironment.js only, not that the
+      // values are valid - null is the fallback.
+      Assert.ok(f in data.system.cpu, f + " must be available under cpu.");
+    }
+
+    if (gIsWindows) {
+      Assert.equal(typeof data.system.isWow64, "boolean",
+             "isWow64 must be available on Windows and have the correct type.");
+      Assert.ok("virtualMaxMB" in data.system, "virtualMaxMB must be available.");
+      Assert.ok(Number.isFinite(data.system.virtualMaxMB),
+                "virtualMaxMB must be a number.");
+    }
+
+    // We insist these are available
+    for (let f of ["cores"]) {
+	Assert.ok(!(f in data.system.cpu) ||
+		  Number.isFinite(data.system.cpu[f]),
+		  f + " must be a number if non null.");
+    }
+
+    // These should be numbers if they are not null
+    for (let f of ["model", "family", "stepping", "l2cacheKB",
+		   "l3cacheKB", "speedMHz"]) {
+	Assert.ok(!(f in data.system.cpu) ||
+		  data.system.cpu[f] === null ||
+		  Number.isFinite(data.system.cpu[f]),
+		  f + " must be a number if non null.");
+    }
   }
 
   let cpuData = data.system.cpu;
   Assert.ok(Number.isFinite(cpuData.count), "CPU count must be a number.");
   Assert.ok(Array.isArray(cpuData.extensions), "CPU extensions must be available.");
 
   // Device data is only available on Android or Gonk.
   if (gIsAndroid || gIsGonk) {
--- a/toolkit/devtools/server/tests/browser/browser_animation_actors_03.js
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_03.js
@@ -36,16 +36,17 @@ function* playerHasAnInitialState(walker
   ok("playState" in player.initialState, "Player's state has playState");
   ok("playbackRate" in player.initialState, "Player's state has playbackRate");
   ok("name" in player.initialState, "Player's state has name");
   ok("duration" in player.initialState, "Player's state has duration");
   ok("delay" in player.initialState, "Player's state has delay");
   ok("iterationCount" in player.initialState, "Player's state has iterationCount");
   ok("isRunningOnCompositor" in player.initialState, "Player's state has isRunningOnCompositor");
   ok("type" in player.initialState, "Player's state has type");
+  ok("documentCurrentTime" in player.initialState, "Player's state has documentCurrentTime");
 }
 
 function* playerStateIsCorrect(walker, front) {
   info("Checking the state of the simple animation");
 
   let state = yield getAnimationStateForNode(walker, front, ".simple-animation", 0);
   is(state.name, "move", "Name is correct");
   is(state.duration, 2000, "Duration is correct");
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -2993,16 +2993,18 @@ this.AddonManager = {
   // our trusted certificate.
   SIGNEDSTATE_UNKNOWN: -1,
   // Add-on is unsigned.
   SIGNEDSTATE_MISSING: 0,
   // Add-on is preliminarily reviewed.
   SIGNEDSTATE_PRELIMINARY: 1,
   // Add-on is fully reviewed.
   SIGNEDSTATE_SIGNED: 2,
+  // Add-on is system add-on.
+  SIGNEDSTATE_SYSTEM: 3,
 
   // Constants for the Addon.userDisabled property
   // Indicates that the userDisabled state of this add-on is currently
   // ask-to-activate. That is, it can be conditionally enabled on a
   // case-by-case basis.
   STATE_ASK_TO_ACTIVATE: "askToActivate",
 
 #ifdef MOZ_EM_DEBUG
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -612,16 +612,22 @@ function isUsableAddon(aAddon) {
   if (aAddon.type == "theme" && aAddon.internalName == XPIProvider.defaultSkin)
     return true;
 
   if (mustSign(aAddon.type)) {
     if (aAddon.signedState <= AddonManager.SIGNEDSTATE_MISSING)
       return false;
     if (aAddon.foreignInstall && aAddon.signedState < AddonManager.SIGNEDSTATE_SIGNED)
       return false;
+
+    if (aAddon._installLocation.name == KEY_APP_SYSTEM_ADDONS ||
+        aAddon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS) {
+      if (aAddon.signedState != AddonManager.SIGNEDSTATE_SYSTEM)
+        return false;
+    }
   }
 
   if (aAddon.blocklistState == Blocklist.STATE_BLOCKED)
     return false;
 
   if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely)
     return false;
 
@@ -1150,17 +1156,17 @@ function defineSyncGUID(aAddon) {
 /**
  * Loads an AddonInternal object from an add-on extracted in a directory.
  *
  * @param  aDir
  *         The nsIFile directory holding the add-on
  * @return an AddonInternal object
  * @throws if the directory does not contain a valid install manifest
  */
-let loadManifestFromDir = Task.async(function* loadManifestFromDir(aDir) {
+let loadManifestFromDir = Task.async(function* loadManifestFromDir(aDir, aInstallLocation) {
   function getFileSize(aFile) {
     if (aFile.isSymlink())
       return 0;
 
     if (!aFile.isDirectory())
       return aFile.fileSize;
 
     let size = 0;
@@ -1197,16 +1203,17 @@ let loadManifestFromDir = Task.async(fun
   bis.init(fis, 4096);
 
   try {
     let addon = file.leafName == FILE_WEB_MANIFEST ?
                 loadManifestFromWebManifest(bis) :
                 loadFromRDF(file, bis);
 
     addon._sourceBundle = aDir.clone();
+    addon._installLocation = aInstallLocation;
     addon.size = getFileSize(aDir);
     addon.signedState = yield verifyDirSignedState(aDir, addon);
     addon.appDisabled = !isUsableAddon(addon);
 
     defineSyncGUID(addon);
 
     return addon;
   }
@@ -1219,17 +1226,17 @@ let loadManifestFromDir = Task.async(fun
 /**
  * Loads an AddonInternal object from an nsIZipReader for an add-on.
  *
  * @param  aZipReader
  *         An open nsIZipReader for the add-on's files
  * @return an AddonInternal object
  * @throws if the XPI file does not contain a valid install manifest
  */
-let loadManifestFromZipReader = Task.async(function* loadManifestFromZipReader(aZipReader) {
+let loadManifestFromZipReader = Task.async(function* loadManifestFromZipReader(aZipReader, aInstallLocation) {
   function loadFromRDF(aStream) {
     let uri = buildJarURI(aZipReader.file, FILE_RDF_MANIFEST);
     let addon = loadManifestFromRDF(uri, aStream);
 
     // Binary components can only be loaded from unpacked addons.
     if (addon.unpack) {
       uri = buildJarURI(aZipReader.file, "chrome.manifest");
       let chromeManifest = ChromeManifestParser.parseSync(uri);
@@ -1254,16 +1261,17 @@ let loadManifestFromZipReader = Task.asy
   bis.init(zis, 4096);
 
   try {
     let addon = entry == FILE_WEB_MANIFEST ?
                 loadManifestFromWebManifest(bis) :
                 loadFromRDF(bis);
 
     addon._sourceBundle = aZipReader.file;
+    addon._installLocation = aInstallLocation;
 
     addon.size = 0;
     let entries = aZipReader.findEntries(null);
     while (entries.hasMore())
       addon.size += aZipReader.getEntry(entries.getNext()).realSize;
 
     addon.signedState = yield verifyZipSignedState(aZipReader.file, addon);
     addon.appDisabled = !isUsableAddon(addon);
@@ -1281,49 +1289,49 @@ let loadManifestFromZipReader = Task.asy
 /**
  * Loads an AddonInternal object from an add-on in an XPI file.
  *
  * @param  aXPIFile
  *         An nsIFile pointing to the add-on's XPI file
  * @return an AddonInternal object
  * @throws if the XPI file does not contain a valid install manifest
  */
-let loadManifestFromZipFile = Task.async(function* loadManifestFromZipFile(aXPIFile) {
+let loadManifestFromZipFile = Task.async(function* loadManifestFromZipFile(aXPIFile, aInstallLocation) {
   let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
                   createInstance(Ci.nsIZipReader);
   try {
     zipReader.open(aXPIFile);
 
     // Can't return this promise because that will make us close the zip reader
     // before it has finished loading the manifest. Wait for the result and then
     // return.
-    let manifest = yield loadManifestFromZipReader(zipReader);
+    let manifest = yield loadManifestFromZipReader(zipReader, aInstallLocation);
     return manifest;
   }
   finally {
     zipReader.close();
   }
 });
 
-function loadManifestFromFile(aFile) {
+function loadManifestFromFile(aFile, aInstallLocation) {
   if (aFile.isFile())
-    return loadManifestFromZipFile(aFile);
+    return loadManifestFromZipFile(aFile, aInstallLocation);
   else
-    return loadManifestFromDir(aFile);
+    return loadManifestFromDir(aFile, aInstallLocation);
 }
 
 /**
  * A synchronous method for loading an add-on's manifest. This should only ever
  * be used during startup or a sync load of the add-ons DB
  */
-function syncLoadManifestFromFile(aFile) {
+function syncLoadManifestFromFile(aFile, aInstallLocation) {
   let success = undefined;
   let result = null;
 
-  loadManifestFromFile(aFile).then(val => {
+  loadManifestFromFile(aFile, aInstallLocation).then(val => {
     success = true;
     result = val;
   }, val => {
     success = false;
     result = val
   });
 
   let thread = Services.tm.currentThread;
@@ -1461,16 +1469,19 @@ function getSignedStatus(aRv, aCert, aEx
         }
         catch (e) {
           logger.warn("The hotfix add-on was not signed by the expected " +
                       "certificate and so will not be installed.", e);
           return AddonManager.SIGNEDSTATE_BROKEN;
         }
       }
 
+      if (aCert.organizationalUnit == "Mozilla Components")
+        return AddonManager.SIGNEDSTATE_SYSTEM;
+
       return /preliminary/i.test(aCert.organizationalUnit)
                ? AddonManager.SIGNEDSTATE_PRELIMINARY
                : AddonManager.SIGNEDSTATE_SIGNED;
       break;
     case Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED:
       return AddonManager.SIGNEDSTATE_MISSING;
       break;
     case Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID:
@@ -2917,17 +2928,17 @@ this.XPIProvider = {
 
         let jsonfile = stagingDir.clone();
         jsonfile.append(id + ".json");
         // Assume this was a foreign install if there is no cached metadata file
         let foreignInstall = !jsonfile.exists();
         let addon;
 
         try {
-          addon = syncLoadManifestFromFile(stageDirEntry);
+          addon = syncLoadManifestFromFile(stageDirEntry, aLocation);
         }
         catch (e) {
           logger.error("Unable to read add-on manifest from " + stageDirEntry.path, e);
           // This add-on can't be installed so just remove it now
           seenFiles.push(stageDirEntry.leafName);
           seenFiles.push(jsonfile.leafName);
           continue;
         }
@@ -3091,17 +3102,17 @@ this.XPIProvider = {
       if (!gIDTest.test(id)) {
         logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " +
             entry.path);
         continue;
       }
 
       let addon;
       try {
-        addon = syncLoadManifestFromFile(entry);
+        addon = syncLoadManifestFromFile(entry, profileLocation);
       }
       catch (e) {
         logger.warn("File entry " + entry.path + " contains an invalid add-on", e);
         continue;
       }
 
       if (addon.id != id) {
         logger.warn("File entry " + entry.path + " contains an add-on with an " +
@@ -3114,17 +3125,17 @@ this.XPIProvider = {
         existingEntry = profileLocation.getLocationForID(id);
       }
       catch (e) {
       }
 
       if (existingEntry) {
         let existingAddon;
         try {
-          existingAddon = syncLoadManifestFromFile(existingEntry);
+          existingAddon = syncLoadManifestFromFile(existingEntry, profileLocation);
 
           if (Services.vc.compare(addon.version, existingAddon.version) <= 0)
             continue;
         }
         catch (e) {
           // Bad add-on in the profile so just proceed and install over the top
           logger.warn("Profile contains an add-on with a bad or missing install " +
                "manifest at " + existingEntry.path + ", overwriting", e);
@@ -4953,17 +4964,17 @@ AddonInstall.prototype = {
     }
     catch (e) {
       zipreader.close();
       return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
     }
 
     try {
       // loadManifestFromZipReader performs the certificate verification for us
-      this.addon = yield loadManifestFromZipReader(zipreader);
+      this.addon = yield loadManifestFromZipReader(zipreader, this.installLocation);
     }
     catch (e) {
       zipreader.close();
       return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
     }
 
     if (mustSign(this.addon.type)) {
       if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
@@ -5501,17 +5512,16 @@ AddonInstall.prototype = {
 
         // Install the new add-on into its final location
         let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
         let file = this.installLocation.installAddon(this.addon.id, stagedAddon,
                                                      existingAddonID);
 
         // Update the metadata in the database
         this.addon._sourceBundle = file;
-        this.addon._installLocation = this.installLocation;
         this.addon.visible = true;
 
         if (isUpgrade) {
           this.addon =  XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
                                                         file.persistentDescriptor);
           let state = XPIStates.getAddon(this.installLocation.name, this.addon.id);
           if (state) {
             state.syncWithDB(this.addon, true);
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -1599,17 +1599,17 @@ this.XPIDatabaseReconcile = {
     let isDetectedInstall = isNewInstall && !aNewAddon;
 
     // Load the manifest if necessary and sanity check the add-on ID
     try {
       if (!aNewAddon) {
         // Load the manifest from the add-on.
         let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
         file.persistentDescriptor = aAddonState.descriptor;
-        aNewAddon = syncLoadManifestFromFile(file);
+        aNewAddon = syncLoadManifestFromFile(file, aInstallLocation);
       }
       // The add-on in the manifest should match the add-on ID.
       if (aNewAddon.id != aId) {
         throw new Error("Invalid addon ID: expected addon ID " + aId +
                         ", found " + aNewAddon.id + " in manifest");
       }
     }
     catch (e) {
@@ -1620,17 +1620,16 @@ this.XPIDatabaseReconcile = {
       if (!aInstallLocation.locked)
         aInstallLocation.uninstallAddon(aId);
       else
         logger.warn("Could not uninstall invalid item from locked install location");
       return null;
     }
 
     // Update the AddonInternal properties.
-    aNewAddon._installLocation = aInstallLocation;
     aNewAddon.installDate = aAddonState.mtime;
     aNewAddon.updateDate = aAddonState.mtime;
 
     // Assume that add-ons in the system add-ons install location aren't
     // foreign and should default to enabled.
     aNewAddon.foreignInstall = isDetectedInstall &&
                                aInstallLocation.name != KEY_APP_SYSTEM_ADDONS &&
                                aInstallLocation.name != KEY_APP_SYSTEM_DEFAULTS;
@@ -1723,17 +1722,17 @@ this.XPIDatabaseReconcile = {
   updateMetadata(aInstallLocation, aOldAddon, aAddonState, aNewAddon) {
     logger.debug("Add-on " + aOldAddon.id + " modified in " + aInstallLocation.name);
 
     try {
       // If there isn't an updated install manifest for this add-on then load it.
       if (!aNewAddon) {
         let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
         file.persistentDescriptor = aAddonState.descriptor;
-        aNewAddon = syncLoadManifestFromFile(file);
+        aNewAddon = syncLoadManifestFromFile(file, aInstallLocation);
         applyBlocklistChanges(aOldAddon, aNewAddon);
 
         // Carry over any pendingUninstall state to add-ons modified directly
         // in the profile. This is important when the attempt to remove the
         // add-on in processPendingFileChanges failed and caused an mtime
         // change to the add-ons files.
         aNewAddon.pendingUninstall = aOldAddon.pendingUninstall;
       }
@@ -1751,17 +1750,16 @@ this.XPIDatabaseReconcile = {
         aInstallLocation.uninstallAddon(aOldAddon.id);
       else
         logger.warn("Could not uninstall invalid item from locked install location");
 
       return null;
     }
 
     // Set the additional properties on the new AddonInternal
-    aNewAddon._installLocation = aInstallLocation;
     aNewAddon.updateDate = aAddonState.mtime;
 
     // Update the database
     return XPIDatabase.updateAddonMetadata(aOldAddon, aNewAddon, aAddonState.descriptor);
   },
 
   /**
    * Updates an add-on's descriptor for when the add-on has moved in the
@@ -1809,17 +1807,17 @@ this.XPIDatabaseReconcile = {
     logger.debug("Updating compatibility for add-on " + aOldAddon.id + " in " + aInstallLocation.name);
 
     // If updating from a version of the app that didn't support signedState
     // then fetch that property now
     if (aOldAddon.signedState === undefined && ADDON_SIGNING &&
         SIGNED_TYPES.has(aOldAddon.type)) {
       let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
       file.persistentDescriptor = aAddonState.descriptor;
-      let manifest = syncLoadManifestFromFile(file);
+      let manifest = syncLoadManifestFromFile(file, aInstallLocation);
       aOldAddon.signedState = manifest.signedState;
     }
     // This updates the addon's JSON cached data in place
     applyBlocklistChanges(aOldAddon, aOldAddon, aOldAppVersion,
                           aOldPlatformVersion);
     aOldAddon.appDisabled = !isUsableAddon(aOldAddon);
 
     return aOldAddon;
index 838b1b6584a825747fd808c9d177f8013598eca7..d04bca194bd05f9b4802bdb5eae81ab58de2dd19
GIT binary patch
literal 4815
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
index c346cf3b7d07a9ee7e33bee28f1829bf263a3244..a4d7f4d18e71702b384c759874919c7560b6bd15
GIT binary patch
literal 4820
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
DB__NB
index f82f607f9c76b85f3bef538b6fd7a5ea9f6cb320..b95192c26fabe6adb4a541eb4731caef4d414fad
GIT binary patch
literal 4820
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
index 7fa67926282ea1dc82d7f05d6bf2cfeb132d8508..60bcde626ffd9307c052b3cecc45812f716c5c0d
GIT binary patch
literal 4825
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
DmuZwQ
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d6a8e727a1b8b54188b6407bfd7b8643ecb085c1
GIT binary patch
literal 4810
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..60bcde626ffd9307c052b3cecc45812f716c5c0d
GIT binary patch
literal 4825
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
DmuZwQ
--- a/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
@@ -1,12 +1,15 @@
 // Tests that we reset to the default system add-ons correctly when switching
 // application versions
 const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet";
 
+// Enable signature checks for these tests
+Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
+
 const featureDir = gProfD.clone();
 featureDir.append("features");
 
 const distroDir = do_get_file("data/system_addons/app0");
 registerDirectory("XREAppDist", distroDir);
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "0");
 
@@ -42,23 +45,31 @@ function* check_installed(inProfile, ...
       file.append(id + ".xpi");
       do_check_true(file.exists());
       do_check_true(file.isFile());
 
       let uri = addon.getResourceURI(null);
       do_check_true(uri instanceof AM_Ci.nsIFileURL);
       do_check_eq(uri.file.path, file.path);
 
+      do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SYSTEM);
+
       // Verify the add-on actually started
       let installed = Services.prefs.getCharPref("bootstraptest." + id + ".active_version");
       do_check_eq(installed, versions[i]);
     }
     else {
-      // Add-on should not be installed
-      do_check_eq(addon, null);
+      if (inProfile) {
+        // Add-on should not be installed
+        do_check_eq(addon, null);
+      }
+      else {
+        // Either add-on should not be installed or it shouldn't be active
+        do_check_true(!addon || !addon.isActive);
+      }
 
       try {
         Services.prefs.getCharPref("bootstraptest." + id + ".active_version");
         do_throw("Expected pref to be missing");
       }
       catch (e) {
       }
     }
@@ -126,17 +137,17 @@ add_task(function* test_updated() {
   let file = do_get_file("data/system_addons/app1/features/system2@tests.mozilla.org.xpi");
   file.copyTo(featureDir, file.leafName);
   file = do_get_file("data/system_addons/app2/features/system3@tests.mozilla.org.xpi");
   file.copyTo(featureDir, file.leafName);
 
   // Inject it into the system set
   let addonSet = {
     schema: 1,
-    directory: dirname,
+    directory: featureDir.leafName,
     addons: {
       "system2@tests.mozilla.org": {
         version: "1.0"
       },
       "system3@tests.mozilla.org": {
         version: "1.0"
       },
     }
@@ -193,8 +204,55 @@ add_task(function* test_corrupt_pref() {
   Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "foo");
 
   startupManager(false);
 
   yield check_installed(false, "1.0", "1.0", null);
 
   yield promiseShutdownManager();
 });
+
+// An add-on with a bad certificate should cause us to use the default set
+add_task(function* test_bad_profile_cert() {
+  let file = do_get_file("data/system_addons/app3/features/system1@tests.mozilla.org.xpi");
+  file.copyTo(featureDir, file.leafName);
+
+  // Inject it into the system set
+  let addonSet = {
+    schema: 1,
+    directory: featureDir.leafName,
+    addons: {
+      "system1@tests.mozilla.org": {
+        version: "2.0"
+      },
+      "system2@tests.mozilla.org": {
+        version: "1.0"
+      },
+      "system3@tests.mozilla.org": {
+        version: "1.0"
+      },
+    }
+  };
+  Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(addonSet));
+
+  startupManager(false);
+
+  yield check_installed(false, "1.0", "1.0", null);
+
+  yield promiseShutdownManager();
+});
+
+// Switching to app defaults that contain a bad certificate should ignore the
+// bad add-on
+add_task(function* test_bad_app_cert() {
+  gAppInfo.version = "3";
+  distroDir.leafName = "app3";
+  startupManager();
+
+  // Add-on will still be present just not active
+  let addon = yield promiseAddonByID("system1@tests.mozilla.org");
+  do_check_neq(addon, null);
+  do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED);
+
+  yield check_installed(false, null, null, "1.0");
+
+  yield promiseShutdownManager();
+});
--- a/xpcom/base/nsSystemInfo.cpp
+++ b/xpcom/base/nsSystemInfo.cpp
@@ -26,16 +26,23 @@
 #endif
 
 #ifdef XP_MACOSX
 #include "MacHelpers.h"
 #endif
 
 #ifdef MOZ_WIDGET_GTK
 #include <gtk/gtk.h>
+#include <unistd.h>
+#include <fstream>
+#include "mozilla/Tokenizer.h"
+#include "nsCharSeparatedTokenizer.h"
+
+#include <map>
+#include <string>
 #endif
 
 #ifdef MOZ_WIDGET_ANDROID
 #include "AndroidBridge.h"
 #endif
 
 #ifdef MOZ_WIDGET_GONK
 #include <sys/system_properties.h>
@@ -44,27 +51,55 @@
 #endif
 
 #ifdef ANDROID
 extern "C" {
 NS_EXPORT int android_sdk_version;
 }
 #endif
 
+#ifdef XP_MACOSX
+#include <sys/sysctl.h>
+#endif
+
 #if defined(XP_LINUX) && defined(MOZ_SANDBOX)
 #include "mozilla/SandboxInfo.h"
 #endif
 
 // Slot for NS_InitXPCOM2 to pass information to nsSystemInfo::Init.
 // Only set to nonzero (potentially) if XP_UNIX.  On such systems, the
 // system call to discover the appropriate value is not thread-safe,
 // so we must call it before going multithreaded, but nsSystemInfo::Init
 // only happens well after that point.
 uint32_t nsSystemInfo::gUserUmask = 0;
 
+#if defined (MOZ_WIDGET_GTK)
+static void
+SimpleParseKeyValuePairs(const std::string& aFilename,
+                         std::map<nsCString, nsCString>& aKeyValuePairs)
+{
+  std::ifstream input(aFilename.c_str());
+  for (std::string line; std::getline(input, line); ) {
+    nsAutoCString key, value;
+
+    nsCCharSeparatedTokenizer tokens(nsDependentCString(line.c_str()), ':');
+    if (tokens.hasMoreTokens()) {
+      key = tokens.nextToken();
+      if (tokens.hasMoreTokens()) {
+        value = tokens.nextToken();
+      }
+      // We want the value even if there was just one token, to cover the
+      // case where we had the key, and the value was blank (seems to be
+      // a valid scenario some files.)
+      aKeyValuePairs[key] = value;
+    }
+  }
+}
+#endif
+
 #if defined(XP_WIN)
 namespace {
 nsresult
 GetHDDInfo(const char* aSpecialDirName, nsAutoCString& aModel,
            nsAutoCString& aRevision)
 {
   aModel.Truncate();
   aRevision.Truncate();
@@ -230,16 +265,76 @@ static const struct PropItems
   { "hasSSE4_2", mozilla::supports_sse4_2 },
   // ARM-specific bits.
   { "hasEDSP", mozilla::supports_edsp },
   { "hasARMv6", mozilla::supports_armv6 },
   { "hasARMv7", mozilla::supports_armv7 },
   { "hasNEON", mozilla::supports_neon }
 };
 
+#ifdef XP_WIN
+// Lifted from media/webrtc/trunk/webrtc/base/systeminfo.cc,
+// so keeping the _ instead of switching to camel case for now.
+typedef BOOL (WINAPI *LPFN_GLPI)(
+    PSYSTEM_LOGICAL_PROCESSOR_INFORMATION,
+    PDWORD);
+static void
+GetProcessorInformation(int* physical_cpus, int* cache_size_L2, int* cache_size_L3)
+{
+  MOZ_ASSERT(physical_cpus && cache_size_L2 && cache_size_L3);
+
+  *physical_cpus = 0;
+  *cache_size_L2 = 0; // This will be in kbytes
+  *cache_size_L3 = 0; // This will be in kbytes
+
+  // GetLogicalProcessorInformation() is available on Windows XP SP3 and beyond.
+  LPFN_GLPI glpi = reinterpret_cast<LPFN_GLPI>(GetProcAddress(
+      GetModuleHandle(L"kernel32"),
+      "GetLogicalProcessorInformation"));
+  if (nullptr == glpi) {
+    return;
+  }
+  // Determine buffer size, allocate and get processor information.
+  // Size can change between calls (unlikely), so a loop is done.
+  SYSTEM_LOGICAL_PROCESSOR_INFORMATION info_buffer[32];
+  SYSTEM_LOGICAL_PROCESSOR_INFORMATION* infos = &info_buffer[0];
+  DWORD return_length = sizeof(info_buffer);
+  while (!glpi(infos, &return_length)) {
+    if (GetLastError() == ERROR_INSUFFICIENT_BUFFER && infos == &info_buffer[0]) {
+      infos = new SYSTEM_LOGICAL_PROCESSOR_INFORMATION[return_length / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION)];
+    } else {
+      return;
+    }
+  }
+
+  for (size_t i = 0;
+      i < return_length / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION); ++i) {
+    if (infos[i].Relationship == RelationProcessorCore) {
+      ++*physical_cpus;
+    } else if (infos[i].Relationship == RelationCache) {
+      // Only care about L2 and L3 cache
+      switch (infos[i].Cache.Level) {
+        case 2:
+          *cache_size_L2 = static_cast<int>(infos[i].Cache.Size/1024);
+          break;
+        case 3:
+          *cache_size_L3 = static_cast<int>(infos[i].Cache.Size/1024);
+          break;
+        default:
+          break;
+      }
+    }
+  }
+  if (infos != &info_buffer[0]) {
+    delete [] infos;
+  }
+  return;
+}
+#endif
+
 nsresult
 nsSystemInfo::Init()
 {
   nsresult rv;
 
   static const struct
   {
     PRSysInfo cmd;
@@ -267,20 +362,256 @@ nsSystemInfo::Init()
   rv = SetPropertyAsBool(NS_ConvertASCIItoUTF16("hasWindowsTouchInterface"),
                          false);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Additional informations not available through PR_GetSystemInfo.
   SetInt32Property(NS_LITERAL_STRING("pagesize"), PR_GetPageSize());
   SetInt32Property(NS_LITERAL_STRING("pageshift"), PR_GetPageShift());
   SetInt32Property(NS_LITERAL_STRING("memmapalign"), PR_GetMemMapAlignment());
-  SetInt32Property(NS_LITERAL_STRING("cpucount"), PR_GetNumberOfProcessors());
   SetUint64Property(NS_LITERAL_STRING("memsize"), PR_GetPhysicalMemorySize());
   SetUint32Property(NS_LITERAL_STRING("umask"), nsSystemInfo::gUserUmask);
 
+  uint64_t virtualMem = 0;
+  nsAutoCString cpuVendor;
+  int cpuSpeed = -1;
+  int cpuFamily = -1;
+  int cpuModel = -1;
+  int cpuStepping = -1;
+  int logicalCPUs = -1;
+  int physicalCPUs = -1;
+  int cacheSizeL2 = -1;
+  int cacheSizeL3 = -1;
+
+#if defined (XP_WIN)
+  // Virtual memory:
+  MEMORYSTATUSEX memStat;
+  memStat.dwLength = sizeof(memStat);
+  if (GlobalMemoryStatusEx(&memStat)) {
+    virtualMem = memStat.ullTotalVirtual;
+  }
+
+  // CPU speed
+  HKEY key;
+  static const WCHAR keyName[] =
+    L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0";
+
+  if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, keyName , 0, KEY_QUERY_VALUE, &key)
+      == ERROR_SUCCESS) {
+    DWORD data, len, vtype;
+    len = sizeof(data);
+
+    if (RegQueryValueEx(key, L"~Mhz", 0, 0, reinterpret_cast<LPBYTE>(&data),
+                        &len) == ERROR_SUCCESS) {
+      cpuSpeed = static_cast<int>(data);
+    }
+
+    // Limit to 64 double byte characters, should be plenty, but create
+    // a buffer one larger as the result may not be null terminated. If
+    // it is more than 64, we will not get the value.
+    wchar_t cpuVendorStr[64+1];
+    len = sizeof(cpuVendorStr)-2;
+    if (RegQueryValueExW(key, L"VendorIdentifier",
+                         0, &vtype,
+                         reinterpret_cast<LPBYTE>(cpuVendorStr),
+                         &len) == ERROR_SUCCESS &&
+        vtype == REG_SZ && len % 2 == 0 && len > 1) {
+      cpuVendorStr[len/2] = 0; // In case it isn't null terminated
+      CopyUTF16toUTF8(nsDependentString(cpuVendorStr), cpuVendor);
+    }
+
+    RegCloseKey(key);
+  }
+
+  // Other CPU attributes:
+  SYSTEM_INFO si;
+  GetNativeSystemInfo(&si);
+  logicalCPUs = si.dwNumberOfProcessors;
+  GetProcessorInformation(&physicalCPUs, &cacheSizeL2, &cacheSizeL3);
+  if (physicalCPUs <= 0) {
+    physicalCPUs = logicalCPUs;
+  }
+  cpuFamily = si.wProcessorLevel;
+  cpuModel = si.wProcessorRevision >> 8;
+  cpuStepping = si.wProcessorRevision & 0xFF;
+#elif defined (XP_MACOSX)
+  // CPU speed
+  uint64_t sysctlValue64 = 0;
+  uint32_t sysctlValue32 = 0;
+  size_t len = 0;
+  len = sizeof(sysctlValue64);
+  if (!sysctlbyname("hw.cpufrequency_max", &sysctlValue64, &len, NULL, 0)) {
+    cpuSpeed = static_cast<int>(sysctlValue64/1000000);
+  }
+  MOZ_ASSERT(sizeof(sysctlValue64) == len);
+
+  len = sizeof(sysctlValue32);
+  if (!sysctlbyname("hw.physicalcpu_max", &sysctlValue32, &len, NULL, 0)) {
+    physicalCPUs = static_cast<int>(sysctlValue32);
+  }
+  MOZ_ASSERT(sizeof(sysctlValue32) == len);
+
+  len = sizeof(sysctlValue32);
+  if (!sysctlbyname("hw.logicalcpu_max", &sysctlValue32, &len, NULL, 0)) {
+    logicalCPUs = static_cast<int>(sysctlValue32);
+  }
+  MOZ_ASSERT(sizeof(sysctlValue32) == len);
+
+  len = sizeof(sysctlValue64);
+  if (!sysctlbyname("hw.l2cachesize", &sysctlValue64, &len, NULL, 0)) {
+    cacheSizeL2 = static_cast<int>(sysctlValue64/1024);
+  }
+  MOZ_ASSERT(sizeof(sysctlValue64) == len);
+
+  len = sizeof(sysctlValue64);
+  if (!sysctlbyname("hw.l3cachesize", &sysctlValue64, &len, NULL, 0)) {
+    cacheSizeL3 = static_cast<int>(sysctlValue64/1024);
+  }
+  MOZ_ASSERT(sizeof(sysctlValue64) == len);
+
+  if (!sysctlbyname("machdep.cpu.vendor", NULL, &len, NULL, 0)) {
+    char* cpuVendorStr = new char[len];
+    if (!sysctlbyname("machdep.cpu.vendor", cpuVendorStr, &len, NULL, 0)) {
+      cpuVendor = cpuVendorStr;
+    }
+    delete [] cpuVendorStr;
+  }
+
+  len = sizeof(sysctlValue32);
+  if (!sysctlbyname("machdep.cpu.family", &sysctlValue32, &len, NULL, 0)) {
+    cpuFamily = static_cast<int>(sysctlValue32);
+  }
+  MOZ_ASSERT(sizeof(sysctlValue32) == len);
+
+  len = sizeof(sysctlValue32);
+  if (!sysctlbyname("machdep.cpu.model", &sysctlValue32, &len, NULL, 0)) {
+    cpuModel = static_cast<int>(sysctlValue32);
+  }
+  MOZ_ASSERT(sizeof(sysctlValue32) == len);
+
+  len = sizeof(sysctlValue32);
+  if (!sysctlbyname("machdep.cpu.stepping", &sysctlValue32, &len, NULL, 0)) {
+    cpuStepping = static_cast<int>(sysctlValue32);
+  }
+  MOZ_ASSERT(sizeof(sysctlValue32) == len);
+
+#elif defined (MOZ_WIDGET_GTK)
+  // Get vendor, family, model, stepping, physical cores, L3 cache size
+  // from /proc/cpuinfo file
+  {
+    std::map<nsCString, nsCString> keyValuePairs;
+    SimpleParseKeyValuePairs("/proc/cpuinfo", keyValuePairs);
+
+    // cpuVendor from "vendor_id"
+    cpuVendor.Assign(keyValuePairs[NS_LITERAL_CSTRING("vendor_id")]);
+
+    {
+      // cpuFamily from "cpu family"
+      Tokenizer::Token t;
+      Tokenizer p(keyValuePairs[NS_LITERAL_CSTRING("cpu family")]);
+      if (p.Next(t) && t.Type() == Tokenizer::TOKEN_INTEGER &&
+          t.AsInteger() <= INT32_MAX) {
+        cpuFamily = static_cast<int>(t.AsInteger());
+      }
+    }
+
+    {
+      // cpuModel from "model"
+      Tokenizer::Token t;
+      Tokenizer p(keyValuePairs[NS_LITERAL_CSTRING("model")]);
+      if (p.Next(t) && t.Type() == Tokenizer::TOKEN_INTEGER &&
+          t.AsInteger() <= INT32_MAX) {
+        cpuModel = static_cast<int>(t.AsInteger());
+      }
+    }
+
+    {
+      // cpuStepping from "stepping"
+      Tokenizer::Token t;
+      Tokenizer p(keyValuePairs[NS_LITERAL_CSTRING("stepping")]);
+      if (p.Next(t) && t.Type() == Tokenizer::TOKEN_INTEGER &&
+          t.AsInteger() <= INT32_MAX) {
+        cpuStepping = static_cast<int>(t.AsInteger());
+      }
+    }
+
+    {
+      // physicalCPUs from "cpu cores"
+      Tokenizer::Token t;
+      Tokenizer p(keyValuePairs[NS_LITERAL_CSTRING("cpu cores")]);
+      if (p.Next(t) && t.Type() == Tokenizer::TOKEN_INTEGER &&
+          t.AsInteger() <= INT32_MAX) {
+        physicalCPUs = static_cast<int>(t.AsInteger());
+      }
+    }
+
+    {
+      // cacheSizeL3 from "cache size"
+      Tokenizer::Token t;
+      Tokenizer p(keyValuePairs[NS_LITERAL_CSTRING("cache size")]);
+      if (p.Next(t) && t.Type() == Tokenizer::TOKEN_INTEGER &&
+          t.AsInteger() <= INT32_MAX) {
+        cacheSizeL3 = static_cast<int>(t.AsInteger());
+        if (p.Next(t) && t.Type() == Tokenizer::TOKEN_WORD &&
+            t.AsString() != NS_LITERAL_CSTRING("KB")) {
+          // If we get here, there was some text after the cache size value
+          // and that text was not KB.  For now, just don't report the
+          // L3 cache.
+          cacheSizeL3 = -1;
+        }
+      }
+    }
+  }
+
+  {
+    // Get cpuSpeed from another file.
+    std::ifstream input("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq");
+    std::string line;
+    if (getline(input, line)) {
+      Tokenizer::Token t;
+      Tokenizer p(line.c_str());
+      if (p.Next(t) && t.Type() == Tokenizer::TOKEN_INTEGER &&
+          t.AsInteger() <= INT32_MAX) {
+        cpuSpeed = static_cast<int>(t.AsInteger()/1000);
+      }
+    }
+  }
+
+  {
+    // Get cacheSizeL2 from yet another file
+    std::ifstream input("/sys/devices/system/cpu/cpu0/cache/index2/size");
+    std::string line;
+    if (getline(input, line)) {
+      Tokenizer::Token t;
+      Tokenizer p(line.c_str(), nullptr, "K");
+      if (p.Next(t) && t.Type() == Tokenizer::TOKEN_INTEGER &&
+          t.AsInteger() <= INT32_MAX) {
+        cacheSizeL2 = static_cast<int>(t.AsInteger());
+      }
+    }
+  }
+
+  SetInt32Property(NS_LITERAL_STRING("cpucount"), PR_GetNumberOfProcessors());
+#else
+  SetInt32Property(NS_LITERAL_STRING("cpucount"), PR_GetNumberOfProcessors());
+#endif
+
+  if (virtualMem) SetUint64Property(NS_LITERAL_STRING("virtualmemsize"), virtualMem);
+  if (cpuSpeed >= 0) SetInt32Property(NS_LITERAL_STRING("cpuspeed"), cpuSpeed);
+  if (!cpuVendor.IsEmpty()) SetPropertyAsACString(NS_LITERAL_STRING("cpuvendor"), cpuVendor);
+  if (cpuFamily >= 0) SetInt32Property(NS_LITERAL_STRING("cpufamily"), cpuFamily);
+  if (cpuModel >= 0) SetInt32Property(NS_LITERAL_STRING("cpumodel"), cpuModel);
+  if (cpuStepping >= 0) SetInt32Property(NS_LITERAL_STRING("cpustepping"), cpuStepping);
+
+  if (logicalCPUs >= 0) SetInt32Property(NS_LITERAL_STRING("cpucount"), logicalCPUs);
+  if (physicalCPUs >= 0) SetInt32Property(NS_LITERAL_STRING("cpucores"), physicalCPUs);
+
+  if (cacheSizeL2 >= 0) SetInt32Property(NS_LITERAL_STRING("cpucachel2"), cacheSizeL2);
+  if (cacheSizeL3 >= 0) SetInt32Property(NS_LITERAL_STRING("cpucachel3"), cacheSizeL3);
+
   for (uint32_t i = 0; i < ArrayLength(cpuPropItems); i++) {
     rv = SetPropertyAsBool(NS_ConvertASCIItoUTF16(cpuPropItems[i].name),
                            cpuPropItems[i].propfun());
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }