Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 10 Jun 2014 18:47:36 -0700
changeset 188023 75377bac231e3017bef14f1d1a5942ae67c08777
parent 188022 18293c58c89a978910bcfbb424359e5600eb8ccb (current diff)
parent 187894 ebbdf5f1a4a0955b34668ee249fc8928de0c94ed (diff)
child 188024 510548c5318114009c3d671d44e7e861d0726e69
child 188026 18c21532848a1673c6c3fe57e710fbf6e9cb35e1
child 188069 a6db5975136ad661319ca54010359a8703c571c3
child 188127 5695da975597ce941290f80e70e4df7910342e0c
push id1036
push userrnewman@mozilla.com
push dateWed, 06 Aug 2014 02:14:58 +0000
treeherderservices-central@3fd543e150c8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone33.0a1
Merge fx-team to m-c a=merge
browser/themes/linux/tabbrowser/tab-overflow-border.png
browser/themes/osx/tabbrowser/tab-overflow-border.png
browser/themes/windows/tabbrowser/tab-overflow-border.png
mobile/android/base/WebappAllocator.java
mobile/android/base/WebappImpl.java
toolkit/components/telemetry/Histograms.json
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -1924,23 +1924,31 @@ let CustomizableUIInternal = {
       if (cache) {
         cache.delete(widget.id);
       }
     }
 
     this.notifyListeners("onWidgetCreated", widget.id);
 
     if (widget.defaultArea) {
+      let addToDefaultPlacements = false;
       let area = gAreas.get(widget.defaultArea);
-      //XXXgijs this won't have any effect for legacy items. Sort of OK because
-      // consumers can modify currentset? Maybe?
-      if (area.has("defaultPlacements")) {
-        area.get("defaultPlacements").push(widget.id);
-      } else {
-        area.set("defaultPlacements", [widget.id]);
+      if (widget.source == CustomizableUI.SOURCE_BUILTIN) {
+        addToDefaultPlacements = true;
+      } else if (!CustomizableUI.isBuiltinToolbar(widget.defaultArea) &&
+                 widget.defaultArea != CustomizableUI.AREA_PANEL) {
+        addToDefaultPlacements = true;
+      }
+
+      if (addToDefaultPlacements) {
+        if (area.has("defaultPlacements")) {
+          area.get("defaultPlacements").push(widget.id);
+        } else {
+          area.set("defaultPlacements", [widget.id]);
+        }
       }
     }
 
     // Look through previously saved state to see if we're restoring a widget.
     let seenAreas = new Set();
     let widgetMightNeedAutoAdding = true;
     for (let [area, placements] of gPlacements) {
       seenAreas.add(area);
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -660,17 +660,17 @@ CustomizeMode.prototype = {
     let numberOfAreas = areas.length;
     for (let i = 0; i < numberOfAreas; i++) {
       let area = areas[i];
       let areaNode = aNode.ownerDocument.getElementById(area);
       let customizationTarget = areaNode && areaNode.customizationTarget;
       if (customizationTarget && customizationTarget != areaNode) {
         areas.push(customizationTarget.id);
       }
-      let overflowTarget = areaNode.getAttribute("overflowtarget");
+      let overflowTarget = areaNode && areaNode.getAttribute("overflowtarget");
       if (overflowTarget) {
         areas.push(overflowTarget);
       }
     }
     areas.push(kPaletteId);
 
     while (aNode && aNode.parentNode) {
       let parent = aNode.parentNode;
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -89,16 +89,17 @@ skip-if = e10s # Bug ?????? - test uses 
 [browser_975719_customtoolbars_behaviour.js]
 
 [browser_976792_insertNodeInWindow.js]
 skip-if = os == "linux"
 
 [browser_978084_dragEnd_after_move.js]
 [browser_980155_add_overflow_toolbar.js]
 [browser_981418-widget-onbeforecreated-handler.js]
+[browser_982656_restore_defaults_builtin_widgets.js]
 
 [browser_984455_bookmarks_items_reparenting.js]
 skip-if = os == "linux"
 
 [browser_985815_propagate_setToolbarVisibility.js]
 [browser_981305_separator_insertion.js]
 [browser_988072_sidebar_events.js]
 [browser_989751_subviewbutton_class.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_982656_restore_defaults_builtin_widgets.js
@@ -0,0 +1,57 @@
+/* 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";
+
+// Restoring default should not place addon widgets back in the toolbar
+add_task(function() {
+  ok(CustomizableUI.inDefaultState, "Default state to begin");
+
+  const kWidgetId = "bug982656-add-on-widget-should-not-restore-to-default-area";
+  let widgetSpec = {
+    id: kWidgetId,
+    defaultArea: CustomizableUI.AREA_NAVBAR
+  };
+  CustomizableUI.createWidget(widgetSpec);
+
+  ok(!CustomizableUI.inDefaultState, "Not in default state after widget added");
+  is(CustomizableUI.getPlacementOfWidget(kWidgetId).area, CustomizableUI.AREA_NAVBAR, "Widget should be in navbar");
+
+  yield resetCustomization();
+
+  ok(CustomizableUI.inDefaultState, "Back in default state after reset");
+  is(CustomizableUI.getPlacementOfWidget(kWidgetId), null, "Widget now in palette");
+  CustomizableUI.destroyWidget(kWidgetId);
+});
+
+
+// resetCustomization shouldn't move 3rd party widgets out of custom toolbars
+add_task(function() {
+  const kToolbarId = "bug982656-toolbar-with-defaultset";
+  const kWidgetId = "bug982656-add-on-widget-should-restore-to-default-area-when-area-is-not-builtin";
+  ok(CustomizableUI.inDefaultState, "Everything should be in its default state.");
+  let toolbar = createToolbarWithPlacements(kToolbarId);
+  ok(CustomizableUI.areas.indexOf(kToolbarId) != -1,
+     "Toolbar has been registered.");
+  is(CustomizableUI.getAreaType(kToolbarId), CustomizableUI.TYPE_TOOLBAR,
+     "Area should be registered as toolbar");
+
+  let widgetSpec = {
+    id: kWidgetId,
+    defaultArea: kToolbarId
+  };
+  CustomizableUI.createWidget(widgetSpec);
+
+  ok(!CustomizableUI.inDefaultState, "No longer in default state after toolbar is registered and visible.");
+  is(CustomizableUI.getPlacementOfWidget(kWidgetId).area, kToolbarId, "Widget should be in custom toolbar");
+
+  yield resetCustomization();
+  ok(CustomizableUI.inDefaultState, "Back in default state after reset");
+  is(CustomizableUI.getPlacementOfWidget(kWidgetId).area, kToolbarId, "Widget still in custom toolbar");
+  ok(toolbar.collapsed, "Custom toolbar should be collapsed after reset");
+
+  toolbar.remove();
+  CustomizableUI.destroyWidget(kWidgetId);
+  CustomizableUI.unregisterArea(kToolbarId);
+});
--- a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
@@ -36,35 +36,45 @@ let startTests = Task.async(function*() 
   yield performTests(inspector, view);
 
   yield finishUp(toolbox);
   finish();
 });
 
 function* performTests(inspector, ruleview) {
   yield togglePseudoClass(inspector);
-  yield testAdded(inspector, ruleview);
+  yield assertPseudoAddedToNode(inspector, ruleview);
 
   yield togglePseudoClass(inspector);
-  yield testRemoved();
-  yield testRemovedFromUI(inspector, ruleview);
+  yield assertPseudoRemovedFromNode();
+  yield assertPseudoRemovedFromView(inspector, ruleview);
 
   yield togglePseudoClass(inspector);
   yield testNavigate(inspector, ruleview);
 }
 
 function* togglePseudoClass(inspector) {
-  info("Toggle the pseudoclass, wait for the pseudoclass event and wait for the refresh of the rule view");
+  info("Toggle the pseudoclass, wait for it to be applied");
 
+  // Give the inspector panels a chance to update when the pseudoclass changes
   let onPseudo = inspector.selection.once("pseudoclass");
   let onRefresh = inspector.once("rule-view-refreshed");
-  inspector.togglePseudoClass(PSEUDO);
+  let onMutations = waitForMutation(inspector);
+
+  yield inspector.togglePseudoClass(PSEUDO);
 
   yield onPseudo;
   yield onRefresh;
+  yield onMutations;
+}
+
+function waitForMutation(inspector) {
+  let def = promise.defer();
+  inspector.walker.once("mutations", def.resolve);
+  return def.promise;
 }
 
 function* testNavigate(inspector, ruleview) {
   yield selectNode("#parent-div", inspector);
 
   info("Make sure the pseudoclass is still on after navigating to a parent");
   is(DOMUtils.hasPseudoClassLock(getNode("#div-1"), PSEUDO), true,
     "pseudo-class lock is still applied after inspecting ancestor");
@@ -82,17 +92,17 @@ function* testNavigate(inspector, rulevi
   yield inspector.once("computed-view-refreshed");
 }
 
 function showPickerOn(node, inspector) {
   let highlighter = inspector.toolbox.highlighter;
   return highlighter.showBoxModel(getNodeFront(node));
 }
 
-function* testAdded(inspector, ruleview) {
+function* assertPseudoAddedToNode(inspector, ruleview) {
   info("Make sure the pseudoclass lock is applied to #div-1 and its ancestors");
   let node = getNode("#div-1");
   do {
     is(DOMUtils.hasPseudoClassLock(node, PSEUDO), true,
       "pseudo-class lock has been applied");
     node = node.parentNode;
   } while (node.parentNode)
 
@@ -105,38 +115,38 @@ function* testAdded(inspector, ruleview)
   yield showPickerOn(getNode("#div-1"), inspector);
 
   info("Check that the infobar selector contains the pseudo-class");
   let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
   is(pseudoClassesBox.textContent, PSEUDO, "pseudo-class in infobar selector");
   yield inspector.toolbox.highlighter.hideBoxModel();
 }
 
-function* testRemoved() {
+function* assertPseudoRemovedFromNode() {
   info("Make sure the pseudoclass lock is removed from #div-1 and its ancestors");
   let node = getNode("#div-1");
   do {
     is(DOMUtils.hasPseudoClassLock(node, PSEUDO), false,
        "pseudo-class lock has been removed");
     node = node.parentNode;
   } while (node.parentNode)
 }
 
-function* testRemovedFromUI(inspector, ruleview) {
+function* assertPseudoRemovedFromView(inspector, ruleview) {
   info("Check that the ruleview no longer contains the pseudo-class rule");
   let rules = ruleview.element.querySelectorAll(".ruleview-rule.theme-separator");
   is(rules.length, 2, "rule view is showing 2 rules after removing lock");
 
   yield showPickerOn(getNode("#div-1"), inspector);
 
   let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
   is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector");
   yield inspector.toolbox.highlighter.hideBoxModel();
 }
 
 function* finishUp(toolbox) {
   let onDestroy = gDevTools.once("toolbox-destroyed");
   toolbox.destroy();
   yield onDestroy;
 
-  yield testRemoved(getNode("#div-1"));
+  yield assertPseudoRemovedFromNode(getNode("#div-1"));
   gBrowser.removeCurrentTab();
 }
--- a/browser/devtools/shared/test/browser_graphs-01.js
+++ b/browser/devtools/shared/test/browser_graphs-01.js
@@ -15,17 +15,22 @@ let test = Task.async(function*() {
   finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
 
   let graph = new LineGraphWidget(doc.body, "fps");
-  yield graph.once("ready");
+
+  let readyEventEmitted;
+  graph.once("ready", () => readyEventEmitted = true);
+
+  yield graph.ready();
+  ok(readyEventEmitted, "The 'ready' event should have been emitted");
 
   testGraph(host, graph);
 
   graph.destroy();
   host.destroy();
 }
 
 function testGraph(host, graph) {
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -1,17 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const Cu = Components.utils;
 
-Cu.import("resource://gre/modules/devtools/event-emitter.js");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
 
 this.EXPORTED_SYMBOLS = ["LineGraphWidget"];
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
 
 // Generic constants.
 
@@ -111,16 +112,17 @@ GraphSelectionResizer.prototype = {
  *        The graph type, used for setting the correct class names.
  *        Currently supported: "line-graph" only.
  * @param number sharpness [optional]
  *        Defaults to the current device pixel ratio.
  */
 this.AbstractCanvasGraph = function(parent, name, sharpness) {
   EventEmitter.decorate(this);
 
+  this._ready = promise.defer();
   this._parent = parent;
   this._uid = "canvas-graph-" + Date.now();
 
   AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
     this._iframe = iframe;
     this._window = iframe.contentWindow;
     this._document = iframe.contentDocument;
     this._pixelRatio = sharpness || this._window.devicePixelRatio;
@@ -160,16 +162,17 @@ this.AbstractCanvasGraph = function(pare
     container.addEventListener("MozMousePixelScroll", this._onMouseWheel);
     container.addEventListener("mouseout", this._onMouseOut);
 
     let ownerWindow = this._parent.ownerDocument.defaultView;
     ownerWindow.addEventListener("resize", this._onResize);
 
     this._animationId = this._window.requestAnimationFrame(this._onAnimationFrame);
 
+    this._ready.resolve(this);
     this.emit("ready", this);
   });
 }
 
 AbstractCanvasGraph.prototype = {
   /**
    * Read-only width and height of the canvas.
    * @return number
@@ -177,16 +180,23 @@ AbstractCanvasGraph.prototype = {
   get width() {
     return this._width;
   },
   get height() {
     return this._height;
   },
 
   /**
+   * Returns a promise resolved once this graph is ready to receive data.
+   */
+  ready: function() {
+    return this._ready.promise;
+  },
+
+  /**
    * Destroys this graph.
    */
   destroy: function() {
     let container = this._container;
     container.removeEventListener("mousemove", this._onMouseMove);
     container.removeEventListener("mousedown", this._onMouseDown);
     container.removeEventListener("mouseup", this._onMouseUp);
     container.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
@@ -496,25 +506,26 @@ AbstractCanvasGraph.prototype = {
   _drawWidget: function() {
     if (!this._shouldRedraw) {
       return;
     }
 
     let ctx = this._ctx;
     ctx.clearRect(0, 0, this._width, this._height);
 
+    // Draw the graph underneath the cursor and selection.
+    if (this.hasData()) {
+      ctx.drawImage(this._cachedGraphImage, 0, 0, this._width, this._height);
+    }
     if (this.hasCursor()) {
       this._drawCliphead();
     }
     if (this.hasSelection() || this.hasSelectionInProgress()) {
       this._drawSelection();
     }
-    if (this.hasData()) {
-      ctx.drawImage(this._cachedGraphImage, 0, 0, this._width, this._height);
-    }
 
     this._shouldRedraw = false;
   },
 
   /**
    * Draws the cliphead, if available and necessary.
    */
   _drawCliphead: function() {
@@ -952,34 +963,27 @@ LineGraphWidget.prototype = Heritage.ext
    * @see AbstractCanvasGraph.prototype.buildGraphImage
    */
   buildGraphImage: function() {
     let canvas = this._document.createElementNS(HTML_NS, "canvas");
     let ctx = canvas.getContext("2d");
     let width = canvas.width = this._width;
     let height = canvas.height = this._height;
 
+    let totalTicks = this._data.length;
+    let firstTick = this._data[0].delta;
+    let lastTick = this._data[totalTicks - 1].delta;
     let maxValue = Number.MIN_SAFE_INTEGER;
     let minValue = Number.MAX_SAFE_INTEGER;
     let sumValues = 0;
-    let totalTicks = 0;
-    let firstTick;
-    let lastTick;
 
     for (let { delta, value } of this._data) {
       maxValue = Math.max(value, maxValue);
       minValue = Math.min(value, minValue);
       sumValues += value;
-      totalTicks++;
-
-      if (!firstTick) {
-        firstTick = delta;
-      } else {
-        lastTick = delta;
-      }
     }
 
     let dataScaleX = this.dataScaleX = width / lastTick;
     let dataScaleY = this.dataScaleY = height / maxValue * GRAPH_DAMPEN_VALUES;
 
     /**
      * Calculates the squared distance between two 2D points.
      */
@@ -992,17 +996,16 @@ LineGraphWidget.prototype = Heritage.ext
     // Draw the graph.
 
     let gradient = ctx.createLinearGradient(0, height / 2, 0, height);
     gradient.addColorStop(0, LINE_GRAPH_BACKGROUND_GRADIENT_START);
     gradient.addColorStop(1, LINE_GRAPH_BACKGROUND_GRADIENT_END);
     ctx.fillStyle = gradient;
     ctx.strokeStyle = LINE_GRAPH_STROKE_COLOR;
     ctx.lineWidth = LINE_GRAPH_STROKE_WIDTH;
-    ctx.setLineDash([]);
     ctx.beginPath();
 
     let prevX = 0;
     let prevY = 0;
 
     for (let { delta, value } of this._data) {
       let currX = delta * dataScaleX;
       let currY = height - value * dataScaleY;
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -372,31 +372,27 @@ exports.AppManager = AppManager = {
         };
         yield AppActorFront.installHosted(client,
                                           actor,
                                           appId,
                                           metadata,
                                           project.manifest);
       }
 
-      function waitUntilProjectRuns() {
+      let manifest = self.getProjectManifestURL(project);
+      if (!self._runningApps.has(manifest)) {
         let deferred = promise.defer();
         self.on("app-manager-update", function onUpdate(event, what) {
           if (what == "project-is-running") {
             self.off("app-manager-update", onUpdate);
             deferred.resolve();
           }
         });
-        return deferred.promise;
-      }
-
-      let manifest = self.getProjectManifestURL(project);
-      if (!self._runningApps.has(manifest)) {
         yield AppActorFront.launchApp(client, actor, manifest);
-        yield waitUntilProjectRuns();
+        yield deferred.promise;
 
       } else {
         yield AppActorFront.reloadApp(client, actor, manifest);
       }
     });
   },
 
   stopRunningApp: function() {
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1824,47 +1824,26 @@ richlistitem[type~="action"][actiontype=
 
 /* Tabbrowser arrowscrollbox arrows */
 .tabbrowser-arrowscrollbox > .scrollbutton-up,
 .tabbrowser-arrowscrollbox > .scrollbutton-down {
   -moz-appearance: none;
   margin: 0 0 @tabToolbarNavbarOverlap@;
 }
 
-.tabbrowser-arrowscrollbox > .scrollbutton-up {
-  -moz-border-start: 0;
-  -moz-border-end: 2px solid transparent;
-}
-
 .tabbrowser-arrowscrollbox > .scrollbutton-down {
-  -moz-border-start: 2px solid transparent;
-  -moz-border-end: 0;
   transition: 1s box-shadow ease-out;
   border-radius: 4px;
 }
 
 .tabbrowser-arrowscrollbox > .scrollbutton-down[notifybgtab] {
   box-shadow: 0 0 5px 5px Highlight inset;
   transition: none;
 }
 
-.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):-moz-locale-dir(ltr),
-.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]):-moz-locale-dir(rtl) {
-  border-width: 0 2px 0 0;
-  border-style: solid;
-  border-image: url("chrome://browser/skin/tabbrowser/tab-overflow-border.png") 0 2 0 2 fill;
-}
-
-.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]):-moz-locale-dir(ltr),
-.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):-moz-locale-dir(rtl) {
-  border-width: 0 0 0 2px;
-  border-style: solid;
-  border-image: url("chrome://browser/skin/tabbrowser/tab-overflow-border.png") 0 2 0 2 fill;
-}
-
 #TabsToolbar .toolbarbutton-1 {
   margin-bottom: @tabToolbarNavbarOverlap@;
 }
 
 #TabsToolbar .toolbarbutton-1 > .toolbarbutton-icon,
 #TabsToolbar .toolbarbutton-1 > .toolbarbutton-menu-dropmarker,
 #TabsToolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
   margin-top: -2px;
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -160,17 +160,17 @@ browser.jar:
   skin/classic/browser/social/gear_default.png        (../shared/social/gear_default.png)
   skin/classic/browser/social/gear_clicked.png        (../shared/social/gear_clicked.png)
   skin/classic/browser/tabbrowser/connecting.png      (tabbrowser/connecting.png)
   skin/classic/browser/tabbrowser/loading.png         (tabbrowser/loading.png)
   skin/classic/browser/tabbrowser/tab-active-middle.png     (tabbrowser/tab-active-middle.png)
   skin/classic/browser/tabbrowser/tab-background-end.png    (tabbrowser/tab-background-end.png)
   skin/classic/browser/tabbrowser/tab-background-middle.png (tabbrowser/tab-background-middle.png)
   skin/classic/browser/tabbrowser/tab-background-start.png  (tabbrowser/tab-background-start.png)
-  skin/classic/browser/tabbrowser/tab-overflow-border.png   (tabbrowser/tab-overflow-border.png)
+  skin/classic/browser/tabbrowser/tab-overflow-indicator.png (../shared/tabbrowser/tab-overflow-indicator.png)
 
 # NOTE: The following two files (tab-selected-end.svg, tab-selected-start.svg) get pre-processed in
 #       Makefile.in with a non-default marker of "%" and the result of that gets packaged.
   skin/classic/browser/tabbrowser/tab-selected-end.svg      (tab-selected-end.svg)
   skin/classic/browser/tabbrowser/tab-selected-start.svg    (tab-selected-start.svg)
 
   skin/classic/browser/tabbrowser/tab-stroke-end.png        (tabbrowser/tab-stroke-end.png)
   skin/classic/browser/tabbrowser/tab-stroke-start.png      (tabbrowser/tab-stroke-start.png)
deleted file mode 100644
index 77f2462e5bfda652074261e2e160fcbc99c3e04e..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -2966,30 +2966,16 @@ toolbarbutton.chevron > .toolbarbutton-m
   }
 
   .tabbrowser-arrowscrollbox > .scrollbutton-up > .toolbarbutton-icon,
   .tabbrowser-arrowscrollbox > .scrollbutton-down > .toolbarbutton-icon {
     width: 13px;
   }
 }
 
-.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):-moz-locale-dir(ltr),
-.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]):-moz-locale-dir(rtl) {
-  border-width: 0 2px 0 0;
-  border-style: solid;
-  border-image: url("chrome://browser/skin/tabbrowser/tab-overflow-border.png") 0 2 0 2 fill;
-}
-
-.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]):-moz-locale-dir(ltr),
-.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):-moz-locale-dir(rtl) {
-  border-width: 0 0 0 2px;
-  border-style: solid;
-  border-image: url("chrome://browser/skin/tabbrowser/tab-overflow-border.png") 0 2 0 2 fill;
-}
-
 /**
  * Tabstrip & add-on bar toolbar buttons
  */
 
 #TabsToolbar .toolbarbutton-1,
 #TabsToolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button,
 #TabsToolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-appearance: none;
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -272,27 +272,27 @@ browser.jar:
   skin/classic/browser/tabbrowser/tab-arrow-right-inverted.png           (tabbrowser/tab-arrow-right-inverted.png)
   skin/classic/browser/tabbrowser/tab-arrow-right-inverted@2x.png        (tabbrowser/tab-arrow-right-inverted@2x.png)
   skin/classic/browser/tabbrowser/tab-background-end.png                 (tabbrowser/tab-background-end.png)
   skin/classic/browser/tabbrowser/tab-background-end@2x.png              (tabbrowser/tab-background-end@2x.png)
   skin/classic/browser/tabbrowser/tab-background-middle.png              (tabbrowser/tab-background-middle.png)
   skin/classic/browser/tabbrowser/tab-background-middle@2x.png           (tabbrowser/tab-background-middle@2x.png)
   skin/classic/browser/tabbrowser/tab-background-start.png               (tabbrowser/tab-background-start.png)
   skin/classic/browser/tabbrowser/tab-background-start@2x.png            (tabbrowser/tab-background-start@2x.png)
+  skin/classic/browser/tabbrowser/tab-overflow-indicator.png             (../shared/tabbrowser/tab-overflow-indicator.png)
 
 # NOTE: The following two files (tab-selected-end.svg, tab-selected-start.svg) get pre-processed in
 #       Makefile.in with a non-default marker of "%" and the result of that gets packaged.
   skin/classic/browser/tabbrowser/tab-selected-end.svg                   (tab-selected-end.svg)
   skin/classic/browser/tabbrowser/tab-selected-start.svg                 (tab-selected-start.svg)
 
   skin/classic/browser/tabbrowser/tab-stroke-end.png                     (tabbrowser/tab-stroke-end.png)
   skin/classic/browser/tabbrowser/tab-stroke-end@2x.png                  (tabbrowser/tab-stroke-end@2x.png)
   skin/classic/browser/tabbrowser/tab-stroke-start.png                   (tabbrowser/tab-stroke-start.png)
   skin/classic/browser/tabbrowser/tab-stroke-start@2x.png                (tabbrowser/tab-stroke-start@2x.png)
-  skin/classic/browser/tabbrowser/tab-overflow-border.png                (tabbrowser/tab-overflow-border.png)
   skin/classic/browser/tabbrowser/tabDragIndicator.png                   (tabbrowser/tabDragIndicator.png)
   skin/classic/browser/tabbrowser/tabDragIndicator@2x.png                (tabbrowser/tabDragIndicator@2x.png)
   skin/classic/browser/tabbrowser/tab-separator.png                      (tabbrowser/tab-separator.png)
   skin/classic/browser/tabbrowser/tab-separator@2x.png                   (tabbrowser/tab-separator@2x.png)
   skin/classic/browser/tabview/close.png                    (tabview/close.png)
   skin/classic/browser/tabview/edit-light.png               (tabview/edit-light.png)
   skin/classic/browser/tabview/search.png                   (tabview/search.png)
   skin/classic/browser/tabview/stack-expander.png           (tabview/stack-expander.png)
deleted file mode 100644
index 73c778af033150dac75d515b11763bf2567395bc..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
index 0000000000000000000000000000000000000000..17d27c99eede5f41ab7114e3e97762c0d6daf5b1
GIT binary patch
literal 578
zc$@)30=@l-P)<h;3K|Lk000e1NJLTq000~S002k`1ONa4Pa9qS0006BNkl<Zcmb8u
zL5|xn429wMQ_f^OMK9I+14WO}Bh0RwhNUJ12seODiAYxd@JN|9|M<E8c-LKv$Fyn4
zWb5$V-yio#WEJj@{qby-S$+87yYJd%B8z6CA}`8jC8Gns=|g+K5!Ip9DmycCiz?bl
zjM}5Nq$4Ta8E;WRGqZE6lP1i>PNoH#>@D+xCTncY-J{iH?Yp&_Pa|1TokDI*W(nJ^
zzE<;c@*GgIGi>rKqO7Wsm=BP}YOBSnRT?m<6-~wktj7<O%S0p7>Zj(6$V}1@RSjsH
zZEo0+by~dz9hf2OTu;HbpaBaNyM$~EF=}sGx4MzdI$~vWzVfK7?tA=V!?7U>H)afG
zm~EBTU4Y1wAvZBrRt|zh8Q@2RI9+n0G`@JvNAK&f>5H{y1R3z;)>-nBli)R3Z*wxm
zgjs^#uEsH7GX1wXT+JBe^=Nki93+fl-OJH4@tHglHQpoSAIM%8j?=`+9GT?DJLUX6
zJ`ur=uY-#jJ$a?HFNew~+JN$Elzt+)E|?C7MxyFkSZNhO?MyMQVUYr@8BzNjZ#6$>
zJLF$N5EJ`hTN*1@JFqn4Ny0fSVC%aodH&}qel_C}v{w#MVCg;PZxx>BaRO_&{;k$$
zwccvs4mWumy`Jk2`L91hqQztSzU{YBZvndSZW|=5V6Ne>&)=Vav}y7D1M@LJXyFJ_
QS^xk507*qoM6N<$g1NyF761SM
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -125,16 +125,53 @@
   -moz-margin-start: -@tabCurveHalfWidth@;
 }
 
 .tabbrowser-arrowscrollbox > .arrowscrollbox-scrollbox {
   -moz-padding-end: @tabCurveHalfWidth@;
   -moz-padding-start: @tabCurveHalfWidth@;
 }
 
+/* Tab Overflow */
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:not([collapsed]),
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:not([collapsed]) {
+  background-image: url(chrome://browser/skin/tabbrowser/tab-overflow-indicator.png);
+  background-size: 100% 100%;
+  width: 14px;
+  margin-bottom: @tabToolbarNavbarOverlap@;
+  pointer-events: none;
+  position: relative;
+  z-index: 3; /* the selected tab's z-index + 1 */
+}
+
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:-moz-locale-dir(rtl),
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:-moz-locale-dir(ltr) {
+  transform: scaleX(-1);
+}
+
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:not([collapsed]) {
+  -moz-margin-start: -2px;
+  -moz-margin-end: -12px;
+}
+
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:not([collapsed]) {
+  -moz-margin-start: -12px;
+  -moz-margin-end: -2px;
+}
+
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator[collapsed],
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator[collapsed] {
+  opacity: 0;
+}
+
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator,
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator {
+  transition: opacity 150ms ease;
+}
+
 .tab-background-start[selected=true]::after,
 .tab-background-start[selected=true]::before,
 .tab-background-start,
 .tab-background-end,
 .tab-background-end[selected=true]::after,
 .tab-background-end[selected=true]::before {
   min-height: @tabMinHeight@;
   width: @tabCurveWidth@;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1850,19 +1850,16 @@ toolbarbutton[type="socialmark"] > .tool
 }
 
 /* Tab scrollbox arrow, tabstrip new tab and all-tabs buttons */
 
 .tabbrowser-arrowscrollbox > .scrollbutton-up,
 .tabbrowser-arrowscrollbox > .scrollbutton-down {
   list-style-image: url("chrome://browser/skin/tabbrowser/tab-arrow-left.png");
   margin: 0 0 @tabToolbarNavbarOverlap@;
-  padding-right: 2px;
-  border-right: 2px solid transparent;
-  background-origin: border-box;
 }
 
 #TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .scrollbutton-up,
 #TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .scrollbutton-down {
   list-style-image: url(chrome://browser/skin/tabbrowser/tab-arrow-left-inverted.png);
 }
 
 .tabbrowser-arrowscrollbox > .scrollbutton-up[disabled],
@@ -1879,23 +1876,16 @@ toolbarbutton[type="socialmark"] > .tool
   transition: 1s background-color ease-out;
 }
 
 .tabbrowser-arrowscrollbox > .scrollbutton-down[notifybgtab] {
   background-color: Highlight;
   transition: none;
 }
 
-.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]),
-.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]) {
-  border-width: 0 2px 0 0;
-  border-style: solid;
-  border-image: url("chrome://browser/skin/tabbrowser/tab-overflow-border.png") 0 2 0 2 fill;
-}
-
 .tabs-newtab-button > .toolbarbutton-icon {
   margin-top: -1px;
   margin-bottom: -1px;
 }
 
 .tabs-newtab-button,
 #TabsToolbar > #new-tab-button,
 #TabsToolbar > toolbarpaletteitem > #new-tab-button {
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -191,17 +191,17 @@ browser.jar:
         skin/classic/browser/tabbrowser/tab-arrow-left.png           (tabbrowser/tab-arrow-left.png)
         skin/classic/browser/tabbrowser/tab-arrow-left-inverted.png  (tabbrowser/tab-arrow-left-inverted.png)
         skin/classic/browser/tabbrowser/tab-background-start.png     (tabbrowser/tab-background-start.png)
         skin/classic/browser/tabbrowser/tab-background-start@2x.png  (tabbrowser/tab-background-start@2x.png)
         skin/classic/browser/tabbrowser/tab-background-middle.png    (tabbrowser/tab-background-middle.png)
         skin/classic/browser/tabbrowser/tab-background-middle@2x.png (tabbrowser/tab-background-middle@2x.png)
         skin/classic/browser/tabbrowser/tab-background-end.png       (tabbrowser/tab-background-end.png)
         skin/classic/browser/tabbrowser/tab-background-end@2x.png    (tabbrowser/tab-background-end@2x.png)
-        skin/classic/browser/tabbrowser/tab-overflow-border.png      (tabbrowser/tab-overflow-border.png)
+        skin/classic/browser/tabbrowser/tab-overflow-indicator.png   (../shared/tabbrowser/tab-overflow-indicator.png)
 
 # NOTE: The following two files (tab-selected-end.svg, tab-selected-start.svg) get pre-processed in
 #       Makefile.in with a non-default marker of "%" and the result of that gets packaged.
         skin/classic/browser/tabbrowser/tab-selected-end.svg         (tab-selected-end.svg)
         skin/classic/browser/tabbrowser/tab-selected-start.svg       (tab-selected-start.svg)
 
         skin/classic/browser/tabbrowser/tab-stroke-end.png           (tabbrowser/tab-stroke-end.png)
         skin/classic/browser/tabbrowser/tab-stroke-end@2x.png        (tabbrowser/tab-stroke-end@2x.png)
@@ -596,17 +596,17 @@ browser.jar:
         skin/classic/aero/browser/tabbrowser/tab-arrow-left.png      (tabbrowser/tab-arrow-left.png)
         skin/classic/aero/browser/tabbrowser/tab-arrow-left-inverted.png (tabbrowser/tab-arrow-left-inverted.png)
         skin/classic/aero/browser/tabbrowser/tab-background-start.png    (tabbrowser/tab-background-start.png)
         skin/classic/aero/browser/tabbrowser/tab-background-start@2x.png (tabbrowser/tab-background-start@2x.png)
         skin/classic/aero/browser/tabbrowser/tab-background-middle.png   (tabbrowser/tab-background-middle.png)
         skin/classic/aero/browser/tabbrowser/tab-background-middle@2x.png (tabbrowser/tab-background-middle@2x.png)
         skin/classic/aero/browser/tabbrowser/tab-background-end.png      (tabbrowser/tab-background-end.png)
         skin/classic/aero/browser/tabbrowser/tab-background-end@2x.png   (tabbrowser/tab-background-end@2x.png)
-        skin/classic/aero/browser/tabbrowser/tab-overflow-border.png (tabbrowser/tab-overflow-border.png)
+        skin/classic/aero/browser/tabbrowser/tab-overflow-indicator.png  (../shared/tabbrowser/tab-overflow-indicator.png)
 
 # NOTE: The following two files (tab-selected-end.svg, tab-selected-start.svg) get pre-processed in
 #       Makefile.in with a non-default marker of "%" and the result of that gets packaged.
         skin/classic/aero/browser/tabbrowser/tab-selected-end.svg    (tab-selected-end-aero.svg)
         skin/classic/aero/browser/tabbrowser/tab-selected-start.svg  (tab-selected-start-aero.svg)
 
         skin/classic/aero/browser/tabbrowser/tab-stroke-end.png      (tabbrowser/tab-stroke-end.png)
         skin/classic/aero/browser/tabbrowser/tab-stroke-end@2x.png   (tabbrowser/tab-stroke-end@2x.png)
deleted file mode 100644
index 77f2462e5bfda652074261e2e160fcbc99c3e04e..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001
--- a/configure.in
+++ b/configure.in
@@ -3936,17 +3936,16 @@ if test -n "$MOZ_RTSP"; then
   NECKO_PROTOCOLS_DEFAULT="$NECKO_PROTOCOLS_DEFAULT rtsp"
 fi
 USE_ARM_KUSER=
 BUILD_CTYPES=1
 MOZ_USE_NATIVE_POPUP_WINDOWS=
 MOZ_ANDROID_HISTORY=
 MOZ_WEBSMS_BACKEND=
 MOZ_ANDROID_BEAM=
-MOZ_ANDROID_SYNTHAPKS=
 MOZ_LOCALE_SWITCHER=
 ACCESSIBILITY=1
 MOZ_TIME_MANAGER=
 MOZ_PAY=
 MOZ_AUDIO_CHANNEL_MANAGER=
 NSS_NO_LIBPKIX=
 MOZ_CONTENT_SANDBOX=
 MOZ_CONTENT_SANDBOX_REPORTER=1
@@ -4962,28 +4961,16 @@ fi
 dnl ========================================================
 dnl = Enable NFC permission on Android
 dnl ========================================================
 if test -n "$MOZ_ANDROID_BEAM"; then
     AC_DEFINE(MOZ_ANDROID_BEAM)
 fi
 
 dnl ========================================================
-dnl = Synthesized Webapp APKs on Android
-dnl ========================================================
-MOZ_ARG_ENABLE_BOOL(android-synthapks,
-[  --enable-android-synthapks       Enable synthesized APKs],
-    MOZ_ANDROID_SYNTHAPKS=1,
-    MOZ_ANDROID_SYNTHAPKS=)
-
-if test -n "$MOZ_ANDROID_SYNTHAPKS"; then
-    AC_DEFINE(MOZ_ANDROID_SYNTHAPKS)
-fi
-
-dnl ========================================================
 dnl = JS Debugger XPCOM component (js/jsd)
 dnl ========================================================
 MOZ_ARG_DISABLE_BOOL(jsd,
 [  --disable-jsd           Disable JavaScript debug library],
     MOZ_JSDEBUGGER=,
     MOZ_JSDEBUGGER=1)
 
 
@@ -8569,17 +8556,16 @@ AC_SUBST(MOZ_DIRECTX_SDK_PATH)
 AC_SUBST(MOZ_D3DCOMPILER_XP_DLL)
 AC_SUBST(MOZ_D3DCOMPILER_XP_CAB)
 
 AC_SUBST(MOZ_METRO)
 
 AC_SUBST(MOZ_ANDROID_HISTORY)
 AC_SUBST(MOZ_WEBSMS_BACKEND)
 AC_SUBST(MOZ_ANDROID_BEAM)
-AC_SUBST(MOZ_ANDROID_SYNTHAPKS)
 AC_SUBST(MOZ_LOCALE_SWITCHER)
 AC_SUBST(MOZ_DISABLE_GECKOVIEW)
 AC_SUBST(ENABLE_STRIP)
 AC_SUBST(PKG_SKIP_STRIP)
 AC_SUBST(STRIP_FLAGS)
 AC_SUBST(USE_ELF_HACK)
 AC_SUBST(INCREMENTAL_LINKER)
 AC_SUBST(MOZ_COMPONENTS_VERSION_SCRIPT_LDFLAGS)
--- a/content/media/webrtc/nsITabSource.idl
+++ b/content/media/webrtc/nsITabSource.idl
@@ -1,17 +1,17 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 #include "nsIDOMWindow.idl"
 
-[scriptable,uuid(ff9c0e45-4646-45ec-b2f0-3b16d9e41875)]
+[scriptable,uuid(b3a7a402-2760-4583-b4a3-af095fe00c84)]
 interface nsITabSource : nsISupports
 {
   nsIDOMWindow getTabToStream();
   void notifyStreamStart(in nsIDOMWindow window);
   void notifyStreamStop(in nsIDOMWindow window);
 };
 
 %{C++
--- a/dom/apps/src/AppsUtils.jsm
+++ b/dom/apps/src/AppsUtils.jsm
@@ -66,17 +66,17 @@ mozIApplication.prototype = {
   }
 }
 
 function _setAppProperties(aObj, aApp) {
   aObj.name = aApp.name;
   aObj.csp = aApp.csp;
   aObj.installOrigin = aApp.installOrigin;
   aObj.origin = aApp.origin;
-#ifdef MOZ_ANDROID_SYNTHAPKS
+#ifdef MOZ_WIDGET_ANDROID
   aObj.apkPackageName = aApp.apkPackageName;
 #endif
   aObj.receipts = aApp.receipts ? JSON.parse(JSON.stringify(aApp.receipts)) : null;
   aObj.installTime = aApp.installTime;
   aObj.manifestURL = aApp.manifestURL;
   aObj.appStatus = aApp.appStatus;
   aObj.removable = aApp.removable;
   aObj.id = aApp.id;
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -1085,17 +1085,17 @@ this.DOMApplicationRegistry = {
     }
 
     let msg = aMessage.data || {};
     let mm = aMessage.target;
     msg.mm = mm;
 
     switch (aMessage.name) {
       case "Webapps:Install": {
-#ifdef MOZ_ANDROID_SYNTHAPKS
+#ifdef MOZ_WIDGET_ANDROID
         Services.obs.notifyObservers(mm, "webapps-runtime-install", JSON.stringify(msg));
 #else
         this.doInstall(msg, mm);
 #endif
         break;
       }
       case "Webapps:GetSelf":
         this.getSelf(msg, mm);
@@ -1114,17 +1114,17 @@ this.DOMApplicationRegistry = {
         break;
       case "Webapps:GetNotInstalled":
         this.getNotInstalled(msg, mm);
         break;
       case "Webapps:GetAll":
         this.doGetAll(msg, mm);
         break;
       case "Webapps:InstallPackage": {
-#ifdef MOZ_ANDROID_SYNTHAPKS
+#ifdef MOZ_WIDGET_ANDROID
         Services.obs.notifyObservers(mm, "webapps-runtime-install-package", JSON.stringify(msg));
 #else
         this.doInstallPackage(msg, mm);
 #endif
         break;
       }
       case "Webapps:RegisterForMessages":
         this.addMessageListener(msg.messages, msg.app, mm);
@@ -2505,25 +2505,25 @@ this.DOMApplicationRegistry = {
       this.queuedDownload[app.manifestURL] = {
         manifest: manifest,
         app: appObject,
         profileDir: aProfileDir
       }
     } else if (manifest.package_path) {
       // If it is a local app then it must been installed from a local file
       // instead of web.
-#ifdef MOZ_ANDROID_SYNTHAPKS
       // In that case, we would already have the manifest, not just the update
       // manifest.
+#ifdef MOZ_WIDGET_ANDROID
       dontNeedNetwork = !!aData.app.manifest;
 #else
       if (aData.app.localInstallPath) {
         dontNeedNetwork = true;
         jsonManifest.package_path = "file://" + aData.app.localInstallPath;
-      }
+      }   
 #endif
 
       // origin for install apps is meaningless here, since it's app:// and this
       // can't be used to resolve package paths.
       manifest = new ManifestHelper(jsonManifest, app.manifestURL);
 
       this.queuedPackageDownload[app.manifestURL] = {
         manifest: manifest,
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -820,17 +820,16 @@ pref("browser.snippets.geoUrl", "https:/
 
 // URL used to ping metrics with stats about which snippets have been shown
 pref("browser.snippets.statsUrl", "https://snippets-stats.mozilla.org/mobile");
 
 // These prefs require a restart to take effect.
 pref("browser.snippets.enabled", true);
 pref("browser.snippets.syncPromo.enabled", true);
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 // The URL of the APK factory from which we obtain APKs for webapps.
 pref("browser.webapps.apkFactoryUrl", "https://controller.apk.firefox.com/application.apk");
 
 // How frequently to check for webapp updates, in seconds (86400 is daily).
 pref("browser.webapps.updateInterval", 86400);
 
 // Whether or not to check for updates.  Enabled by default, but the runtime
 // disables it for webapp profiles on firstrun, so only the main Fennec process
@@ -845,17 +844,15 @@ pref("browser.webapps.updateInterval", 8
 //   1: do check for updates
 pref("browser.webapps.checkForUpdates", 1);
 
 // The URL of the service that checks for updates.
 // To test updates, set this to http://apk-update-checker.paas.allizom.org,
 // which is a test server that always reports all apps as having updates.
 pref("browser.webapps.updateCheckUrl", "https://controller.apk.firefox.com/app_updates");
 
-#endif
-
 // The mode of home provider syncing.
 // 0: Sync always
 // 1: Sync only when on wifi
 pref("home.sync.updateMode", 0);
 
 // How frequently to check if we should sync home provider data.
 pref("home.sync.checkIntervalSecs", 3600);
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -183,17 +183,16 @@
 
             <!-- For debugging -->
             <intent-filter>
                 <action android:name="org.mozilla.gecko.DEBUG" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
         <activity android:name="org.mozilla.gecko.webapp.Dispatcher"
             android:noHistory="true" >
             <intent-filter>
                 <!-- catch links from synthetic apks -->
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="application/webapp" />
             </intent-filter>
@@ -207,17 +206,16 @@
         </receiver>
 
         <receiver android:name="org.mozilla.gecko.webapp.TaskKiller">
           <intent-filter>
              <action android:name="org.mozilla.webapp.TASK_REMOVED" />
              <category android:name="android.intent.category.DEFAULT" />
           </intent-filter>
         </receiver>
-#endif
 
         <activity android:name=".Webapp"
                   android:label="@string/webapp_generic_name"
                   android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize"
                   android:windowSoftInputMode="stateUnspecified|adjustResize"
                   android:launchMode="singleTask"
                   android:taskAffinity="org.mozilla.gecko.WEBAPP"
                   android:process=":@ANDROID_PACKAGE_NAME@.Webapp"
--- a/mobile/android/base/AppConstants.java.in
+++ b/mobile/android/base/AppConstants.java.in
@@ -144,23 +144,16 @@ public class AppConstants {
 
     public static final boolean MOZ_ANDROID_BEAM =
 #ifdef MOZ_ANDROID_BEAM
     true;
 #else
     false;
 #endif
 
-    public static final boolean MOZ_ANDROID_SYNTHAPKS =
-#ifdef MOZ_ANDROID_SYNTHAPKS
-    true;
-#else
-    false;
-#endif
-
     // See this wiki page for more details about channel specific build defines:
     // https://wiki.mozilla.org/Platform/Channel-specific_build_defines
     public static final boolean RELEASE_BUILD =
 #ifdef RELEASE_BUILD
     true;
 #else
     false;
 #endif
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -788,51 +788,29 @@ public class GeckoAppShell
     @WrapElementForJNI
     static void scheduleRestart() {
         restartScheduled = true;
     }
 
     public static Intent getWebappIntent(String aURI, String aOrigin, String aTitle, Bitmap aIcon) {
         Intent intent;
 
-        if (AppConstants.MOZ_ANDROID_SYNTHAPKS) {
-            Allocator slots = Allocator.getInstance(getContext());
-            int index = slots.getIndexForOrigin(aOrigin);
-
-            if (index == -1) {
-                return null;
-            }
-            String packageName = slots.getAppForIndex(index);
-            intent = getContext().getPackageManager().getLaunchIntentForPackage(packageName);
-            if (aURI != null) {
-                intent.setData(Uri.parse(aURI));
-            }
-        } else {
-            int index;
-            if (aIcon != null && !TextUtils.isEmpty(aTitle))
-                index = WebappAllocator.getInstance(getContext()).findAndAllocateIndex(aOrigin, aTitle, aIcon);
-            else
-                index = WebappAllocator.getInstance(getContext()).getIndexForApp(aOrigin);
-
-            if (index == -1)
-                return null;
-
-            intent = getWebappIntent(index, aURI);
+        Allocator slots = Allocator.getInstance(getContext());
+        int index = slots.getIndexForOrigin(aOrigin);
+
+        if (index == -1) {
+            return null;
         }
 
-        return intent;
-    }
-
-    // The old implementation of getWebappIntent.  Not used by MOZ_ANDROID_SYNTHAPKS.
-    public static Intent getWebappIntent(int aIndex, String aURI) {
-        Intent intent = new Intent();
-        intent.setAction(GeckoApp.ACTION_WEBAPP_PREFIX + aIndex);
-        intent.setData(Uri.parse(aURI));
-        intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
-                            AppConstants.ANDROID_PACKAGE_NAME + ".WebApps$WebApp" + aIndex);
+        String packageName = slots.getAppForIndex(index);
+        intent = getContext().getPackageManager().getLaunchIntentForPackage(packageName);
+        if (aURI != null) {
+            intent.setData(Uri.parse(aURI));
+        }
+
         return intent;
     }
 
     // "Installs" an application by creating a shortcut
     // This is the entry point from AndroidBridge.h
     @WrapElementForJNI
     static void createShortcut(String aTitle, String aURI, String aIconData, String aType) {
         if ("webapp".equals(aType)) {
--- a/mobile/android/base/GlobalHistory.java
+++ b/mobile/android/base/GlobalHistory.java
@@ -1,33 +1,37 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Handler;
-import android.util.Log;
-
 import java.lang.ref.SoftReference;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.Queue;
 import java.util.Set;
 
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.database.Cursor;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.Log;
+
 class GlobalHistory {
     private static final String LOGTAG = "GeckoGlobalHistory";
 
-    private static GlobalHistory sInstance = new GlobalHistory();
+    private static final String TELEMETRY_HISTOGRAM_ADD = "FENNEC_GLOBALHISTORY_ADD_MS";
+    private static final String TELEMETRY_HISTOGRAM_UPDATE = "FENNEC_GLOBALHISTORY_UPDATE_MS";
+    private static final String TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK = "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS";
+
+    private static final GlobalHistory sInstance = new GlobalHistory();
 
     static GlobalHistory getInstance() {
         return sInstance;
     }
 
     // this is the delay between receiving a URI check request and processing it.
     // this allows batching together multiple requests and processing them together,
     // which is more efficient.
@@ -43,45 +47,48 @@ class GlobalHistory {
         mHandler = ThreadUtils.getBackgroundHandler();
         mPendingUris = new LinkedList<String>();
         mVisitedCache = new SoftReference<Set<String>>(null);
         mNotifierRunnable = new Runnable() {
             @Override
             public void run() {
                 Set<String> visitedSet = mVisitedCache.get();
                 if (visitedSet == null) {
-                    // the cache was wiped away, repopulate it
+                    // The cache was wiped. Repopulate it.
                     Log.w(LOGTAG, "Rebuilding visited link set...");
-                    visitedSet = new HashSet<String>();
-                    Cursor c = null;
+                    final long start = SystemClock.uptimeMillis();
+                    final Cursor c = BrowserDB.getAllVisitedHistory(GeckoAppShell.getContext().getContentResolver());
+                    if (c == null) {
+                        return;
+                    }
+
                     try {
-                        c = BrowserDB.getAllVisitedHistory(GeckoAppShell.getContext().getContentResolver());
-                        if (c == null) {
-                            return;
-                        }
-
+                        visitedSet = new HashSet<String>();
                         if (c.moveToFirst()) {
                             do {
                                 visitedSet.add(c.getString(0));
                             } while (c.moveToNext());
                         }
                         mVisitedCache = new SoftReference<Set<String>>(visitedSet);
+                        final long end = SystemClock.uptimeMillis();
+                        final long took = end - start;
+                        Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK, (int) Math.min(took, Integer.MAX_VALUE));
                     } finally {
-                        if (c != null)
-                            c.close();
+                        c.close();
                     }
                 }
 
-                // this runs on the same handler thread as the checkUriVisited code,
-                // so no synchronization needed
+                // This runs on the same handler thread as the checkUriVisited code,
+                // so no synchronization is needed.
                 while (true) {
-                    String uri = mPendingUris.poll();
+                    final String uri = mPendingUris.poll();
                     if (uri == null) {
                         break;
                     }
+
                     if (visitedSet.contains(uri)) {
                         GeckoAppShell.notifyUriVisited(uri);
                     }
                 }
                 mProcessing = false;
             }
         };
     }
@@ -90,22 +97,33 @@ class GlobalHistory {
         Set<String> visitedSet = mVisitedCache.get();
         if (visitedSet != null) {
             visitedSet.add(uri);
         }
         GeckoAppShell.notifyUriVisited(uri);
     }
 
     public void add(String uri) {
+        final long start = SystemClock.uptimeMillis();
         BrowserDB.updateVisitedHistory(GeckoAppShell.getContext().getContentResolver(), uri);
+        final long end = SystemClock.uptimeMillis();
+        final long took = end - start;
+        Log.d(LOGTAG, "GlobalHistory.add took " + took + "msec.");
+        Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_ADD, (int) Math.min(took, Integer.MAX_VALUE));
         addToGeckoOnly(uri);
     }
 
+    @SuppressWarnings("static-method")
     public void update(String uri, String title) {
+        final long start = SystemClock.uptimeMillis();
         BrowserDB.updateHistoryTitle(GeckoAppShell.getContext().getContentResolver(), uri, title);
+        final long end = SystemClock.uptimeMillis();
+        final long took = end - start;
+        Log.d(LOGTAG, "GlobalHistory.update took " + took + "msec.");
+        Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_UPDATE, (int) Math.min(took, Integer.MAX_VALUE));
     }
 
     public void checkUriVisited(final String uri) {
         mHandler.post(new Runnable() {
             @Override
             public void run() {
                 // this runs on the same handler thread as the processing loop,
                 // so no synchronization needed
--- a/mobile/android/base/Webapp.java.in
+++ b/mobile/android/base/Webapp.java.in
@@ -1,18 +1,14 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #filter substitution
 package @ANDROID_PACKAGE_NAME@;
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 import org.mozilla.gecko.webapp.WebappImpl;
-#else
-import org.mozilla.gecko.WebappImpl;
-#endif
 
 /**
  * This class serves only as a namespace wrapper for WebappImpl.
  */
 public class Webapp extends WebappImpl {}
deleted file mode 100644
--- a/mobile/android/base/WebappAllocator.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.graphics.Bitmap;
-import android.util.Log;
-
-import java.util.ArrayList;
-
-public class WebappAllocator {
-    private final String LOGTAG = "GeckoWebappAllocator";
-    // The number of Webapp# and WEBAPP# activites/apps/intents
-    private final static int MAX_WEB_APPS = 100;
-
-    protected static WebappAllocator sInstance = null;
-    public static WebappAllocator getInstance() {
-        return getInstance(GeckoAppShell.getContext());
-    }
-
-    public static synchronized WebappAllocator getInstance(Context cx) {
-        if (sInstance == null) {
-            sInstance = new WebappAllocator(cx);
-        }
-
-        return sInstance;
-    }
-
-    SharedPreferences mPrefs;
-
-    protected WebappAllocator(Context context) {
-        mPrefs = context.getSharedPreferences("webapps", Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
-    }
-
-    public static String appKey(int index) {
-        return "app" + index;
-    }
-
-    static public String iconKey(int index) {
-        return "icon" + index;
-    }
-
-    public synchronized int findAndAllocateIndex(String app, String name, String aIconData) {
-        Bitmap icon = (aIconData != null) ? BitmapUtils.getBitmapFromDataURI(aIconData) : null;
-        return findAndAllocateIndex(app, name, icon);
-    }
-
-    public synchronized int findAndAllocateIndex(final String app, final String name, final Bitmap aIcon) {
-        int index = getIndexForApp(app);
-        if (index != -1)
-            return index;
-
-        for (int i = 0; i < MAX_WEB_APPS; ++i) {
-            if (!mPrefs.contains(appKey(i))) {
-                // found unused index i
-                updateAppAllocation(app, i, aIcon);
-                return i;
-            }
-        }
-
-        // no more apps!
-        return -1;
-    }
-
-    public synchronized void updateAppAllocation(final String app,
-                                                 final int index,
-                                                 final Bitmap aIcon) {
-        if (aIcon != null) {
-            ThreadUtils.getBackgroundHandler().post(new Runnable() {
-                @Override
-                public void run() {
-                    int color = 0;
-                    try {
-                        color = BitmapUtils.getDominantColor(aIcon);
-                    } catch (Exception e) {
-                        Log.e(LOGTAG, "Exception during getDominantColor", e);
-                    }
-                    mPrefs.edit()
-                          .putString(appKey(index), app)
-                          .putInt(iconKey(index), color).commit();
-                }
-            });
-        } else {
-            mPrefs.edit()
-                  .putString(appKey(index), app)
-                  .putInt(iconKey(index), 0).commit();
-        }
-    }
-
-    public synchronized int getIndexForApp(String app) {
-        for (int i = 0; i < MAX_WEB_APPS; ++i) {
-            if (mPrefs.getString(appKey(i), "").equals(app)) {
-                return i;
-            }
-        }
-
-        return -1;
-    }
-
-    public synchronized String getAppForIndex(int index) {
-        return mPrefs.getString(appKey(index), null);
-    }
-
-    public synchronized int releaseIndexForApp(String app) {
-        int index = getIndexForApp(app);
-        if (index == -1)
-            return -1;
-
-        releaseIndex(index);
-        return index;
-    }
-
-    public synchronized void releaseIndex(final int index) {
-        ThreadUtils.postToBackgroundThread(new Runnable() {
-            @Override
-            public void run() {
-                mPrefs.edit()
-                    .remove(appKey(index))
-                    .remove(iconKey(index))
-                    .commit();
-            }
-        });
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/WebappImpl.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-import android.view.MenuItem;
-import android.widget.TextView;
-import android.widget.RelativeLayout;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.view.animation.AnimationUtils;
-import android.view.animation.Animation;
-import android.widget.ImageView;
-import android.view.Display;
-
-import java.io.File;
-import java.net.URI;
-
-public class WebappImpl extends GeckoApp {
-    private static final String LOGTAG = "GeckoWebappImpl";
-
-    private URI mOrigin;
-    private TextView mTitlebarText = null;
-    private View mTitlebar = null;
-
-    private View mSplashscreen;
-
-    protected int getIndex() { return 0; }
-
-    @Override
-    public int getLayout() { return R.layout.web_app; }
-
-    @Override
-    public boolean hasTabsSideBar() { return false; }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState)
-    {
-        super.onCreate(savedInstanceState);
-
-        mSplashscreen = (RelativeLayout) findViewById(R.id.splashscreen);
-        if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
-            overridePendingTransition(R.anim.grow_fade_in_center, android.R.anim.fade_out);
-            showSplash();
-        }
-
-        String action = getIntent().getAction();
-        Bundle extras = getIntent().getExtras();
-        String title = extras != null ? extras.getString(Intent.EXTRA_SHORTCUT_NAME) : null;
-        setTitle(title != null ? title : "Web App");
-
-        mTitlebarText = (TextView)findViewById(R.id.webapp_title);
-        mTitlebar = findViewById(R.id.webapp_titlebar);
-        if (!action.startsWith(ACTION_WEBAPP_PREFIX)) {
-            Log.e(LOGTAG, "Webapp launch, but intent action is " + action + "!");
-            return;
-        }
-
-        // Try to use the origin stored in the WebappAllocator first
-        String origin = WebappAllocator.getInstance(this).getAppForIndex(getIndex());
-        try {
-            mOrigin = new URI(origin);
-        } catch (java.net.URISyntaxException ex) {
-            // If we can't parse the this is an app protocol, just settle for not having an origin
-            if (!origin.startsWith("app://")) {
-                return;
-            }
-
-            // If that failed fall back to the origin stored in the shortcut
-            Log.i(LOGTAG, "Webapp is not registered with allocator");
-            try {
-                mOrigin = new URI(getIntent().getData().toString());
-            } catch (java.net.URISyntaxException ex2) {
-                Log.e(LOGTAG, "Unable to parse intent url: ", ex);
-            }
-        }
-    }
-
-    @Override
-    protected void loadStartupTab(String uri) {
-        String action = getIntent().getAction();
-        if (GeckoApp.ACTION_WEBAPP_PREFIX.equals(action)) {
-            // This action assumes the uri is not an installed Webapp. We will
-            // use the WebappAllocator to register the uri with an Android
-            // process so it can run chromeless.
-            int index = WebappAllocator.getInstance(this).findAndAllocateIndex(uri, "App", (Bitmap) null);
-            Intent appIntent = GeckoAppShell.getWebappIntent(index, uri);
-            startActivity(appIntent);
-            finish();
-        }
-    }
-
-    private void showSplash() {
-        SharedPreferences prefs = getSharedPreferences("webapps", Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
-
-        // get the favicon dominant color, stored when the app was installed
-        int[] colors = new int[2];
-        int dominantColor = prefs.getInt(WebappAllocator.iconKey(getIndex()), -1);
-
-        // now lighten it, to ensure that the icon stands out in the center
-        float[] f = new float[3];
-        Color.colorToHSV(dominantColor, f);
-        f[2] = Math.min(f[2]*2, 1.0f);
-        colors[0] = Color.HSVToColor(255, f);
-
-        // now generate a second, slightly darker version of the same color
-        f[2] *= 0.75;
-        colors[1] = Color.HSVToColor(255, f);
-
-        // Draw the background gradient
-        GradientDrawable gd = new GradientDrawable(GradientDrawable.Orientation.TL_BR, colors);
-        gd.setGradientType(GradientDrawable.RADIAL_GRADIENT);
-        Display display = getWindowManager().getDefaultDisplay();
-        gd.setGradientCenter(0.5f, 0.5f);
-        gd.setGradientRadius(Math.max(display.getWidth()/2, display.getHeight()/2));
-        mSplashscreen.setBackgroundDrawable((Drawable)gd);
-
-        // look for a logo.png in the profile dir and show it. If we can't find a logo show nothing
-        File profile = getProfile().getDir();
-        File logoFile = new File(profile, "logo.png");
-        if (logoFile.exists()) {
-            ImageView image = (ImageView)findViewById(R.id.splashscreen_icon);
-            Drawable d = Drawable.createFromPath(logoFile.getPath());
-            image.setImageDrawable(d);
-
-            Animation fadein = AnimationUtils.loadAnimation(this, R.anim.grow_fade_in_center);
-            fadein.setStartOffset(500);
-            fadein.setDuration(1000);
-            image.startAnimation(fadein);
-        }
-    }
-
-    @Override
-    protected String getDefaultProfileName() {
-        String action = getIntent().getAction();
-        if (!action.startsWith(ACTION_WEBAPP_PREFIX)) {
-            Log.e(LOGTAG, "Webapp launch, but intent action is " + action + "!");
-            return null;
-        }
-
-        return "webapp" + action.substring(ACTION_WEBAPP_PREFIX.length());
-    }
-
-    @Override
-    protected boolean getSessionRestoreState(Bundle savedInstanceState) {
-        // for now webapps never restore your session
-        return false;
-    }
-
-    @Override
-    public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
-        switch(msg) {
-            case SELECTED:
-            case LOCATION_CHANGE:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
-                    final String urlString = tab.getURL();
-                    final URI uri;
-
-                    try {
-                        uri = new URI(urlString);
-                    } catch (java.net.URISyntaxException ex) {
-                        mTitlebarText.setText(urlString);
-
-                        // If we can't parse the url, and its an app protocol hide
-                        // the titlebar and return, otherwise show the titlebar
-                        // and the full url
-                        if (!urlString.startsWith("app://")) {
-                            mTitlebar.setVisibility(View.VISIBLE);
-                        } else {
-                            mTitlebar.setVisibility(View.GONE);
-                        }
-                        return;
-                    }
-
-                    if (mOrigin != null && mOrigin.getHost().equals(uri.getHost())) {
-                        mTitlebar.setVisibility(View.GONE);
-                    } else {
-                        mTitlebarText.setText(uri.getScheme() + "://" + uri.getHost());
-                        mTitlebar.setVisibility(View.VISIBLE);
-                    }
-                }
-                break;
-            case LOADED:
-                if (mSplashscreen != null && mSplashscreen.getVisibility() == View.VISIBLE) {
-                    Animation fadeout = AnimationUtils.loadAnimation(this, android.R.anim.fade_out);
-                    fadeout.setAnimationListener(new Animation.AnimationListener() {
-                        @Override
-                        public void onAnimationEnd(Animation animation) {
-                          mSplashscreen.setVisibility(View.GONE);
-                        }
-                        @Override
-                        public void onAnimationRepeat(Animation animation) { }
-                        @Override
-                        public void onAnimationStart(Animation animation) { }
-                    });
-                    mSplashscreen.startAnimation(fadeout);
-                }
-                break;
-            case START:
-                if (mSplashscreen != null && mSplashscreen.getVisibility() == View.VISIBLE) {
-                    View area = findViewById(R.id.splashscreen_progress);
-                    area.setVisibility(View.VISIBLE);
-                    Animation fadein = AnimationUtils.loadAnimation(this, android.R.anim.fade_in);
-                    fadein.setDuration(1000);
-                    area.startAnimation(fadein);
-                }
-                break;
-        }
-        super.onTabChanged(tab, msg, data);
-    }
-};
--- a/mobile/android/base/WebappManifestFragment.xml.frag.in
+++ b/mobile/android/base/WebappManifestFragment.xml.frag.in
@@ -1,22 +1,9 @@
         <activity android:name=".WebApps$WebApp@APPNUM@"
                   android:label="@string/webapp_generic_name"
                   android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize"
                   android:windowSoftInputMode="stateUnspecified|adjustResize"
                   android:process=":@ANDROID_PACKAGE_NAME@.Webapp@APPNUM@"
                   android:theme="@style/Gecko.App"
-#ifdef MOZ_ANDROID_SYNTHAPKS
                   android:launchMode="singleTop"
                   android:exported="true"
         />
-#else
-                  android:launchMode="singleTask"
-                  android:taskAffinity="org.mozilla.gecko.WEBAPP@APPNUM@"
-                  android:excludeFromRecents="true">
-            <intent-filter>
-                <action android:name="org.mozilla.gecko.WEBAPP@APPNUM@" />
-            </intent-filter>
-            <intent-filter>
-                <action android:name="org.mozilla.gecko.ACTION_ALERT_CALLBACK" />
-            </intent-filter>
-        </activity>
-#endif
--- a/mobile/android/base/home/SearchLoader.java
+++ b/mobile/android/base/home/SearchLoader.java
@@ -1,29 +1,33 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
+import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.db.BrowserDB;
 
 import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.Loader;
+import android.util.Log;
 
 /**
  * Encapsulates the implementation of the search cursor loader.
  */
 class SearchLoader {
-    // Key for search terms
+    public static final String LOGTAG = "GeckoSearchLoader";
+
     private static final String KEY_SEARCH_TERM = "search_term";
 
     private SearchLoader() {
     }
 
     public static Loader<Cursor> createInstance(Context context, Bundle args) {
         if (args != null) {
             final String searchTerm = args.getString(KEY_SEARCH_TERM);
@@ -48,30 +52,37 @@ class SearchLoader {
 
     public static void restart(LoaderManager manager, int loaderId,
                                LoaderCallbacks<Cursor> callbacks, String searchTerm) {
         final Bundle args = createArgs(searchTerm);
         manager.restartLoader(loaderId, args, callbacks);
     }
 
     public static class SearchCursorLoader extends SimpleCursorLoader {
+        private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_SEARCH_LOADER_TIME_MS";
+
         // Max number of search results
         private static final int SEARCH_LIMIT = 100;
 
         // The target search term associated with the loader
         private final String mSearchTerm;
 
         public SearchCursorLoader(Context context, String searchTerm) {
             super(context);
             mSearchTerm = searchTerm;
         }
 
         @Override
         public Cursor loadCursor() {
-            return BrowserDB.filter(getContext().getContentResolver(), mSearchTerm, SEARCH_LIMIT);
+            final long start = SystemClock.uptimeMillis();
+            final Cursor cursor = BrowserDB.filter(getContext().getContentResolver(), mSearchTerm, SEARCH_LIMIT);
+            final long end = SystemClock.uptimeMillis();
+            final long took = end - start;
+            Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE));
+            return cursor;
         }
 
         public String getSearchTerm() {
             return mSearchTerm;
         }
     }
 
 }
--- a/mobile/android/base/home/TopSitesPanel.java
+++ b/mobile/android/base/home/TopSitesPanel.java
@@ -26,19 +26,19 @@ import org.mozilla.gecko.home.TopSitesGr
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.graphics.Color;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.AsyncTaskLoader;
 import android.support.v4.content.Loader;
 import android.support.v4.widget.CursorAdapter;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.ContextMenu;
@@ -425,29 +425,34 @@ public class TopSitesPanel extends HomeF
         }
 
         // Once thumbnails have finished loading, the UI is ready. Reset
         // Gecko to normal priority.
         ThreadUtils.resetGeckoPriority();
     }
 
     private static class TopSitesLoader extends SimpleCursorLoader {
-        // Max number of search results
+        // Max number of search results.
         private static final int SEARCH_LIMIT = 30;
+        private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_TOPSITES_LOADER_TIME_MS";
         private int mMaxGridEntries;
 
         public TopSitesLoader(Context context) {
             super(context);
             mMaxGridEntries = context.getResources().getInteger(R.integer.number_of_top_sites);
         }
 
         @Override
         public Cursor loadCursor() {
-            trace("TopSitesLoader.loadCursor()");
-            return BrowserDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT);
+            final long start = SystemClock.uptimeMillis();
+            final Cursor cursor = BrowserDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT);
+            final long end = SystemClock.uptimeMillis();
+            final long took = end - start;
+            Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE));
+            return cursor;
         }
     }
 
     private class VisitedAdapter extends CursorAdapter {
         public VisitedAdapter(Context context, Cursor cursor) {
             super(context, cursor, 0);
         }
 
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -399,18 +399,16 @@ gbjar.sources += [
     'webapp/ApkResources.java',
     'webapp/Dispatcher.java',
     'webapp/EventListener.java',
     'webapp/InstallHelper.java',
     'webapp/InstallListener.java',
     'webapp/TaskKiller.java',
     'webapp/UninstallListener.java',
     'webapp/WebappImpl.java',
-    'WebappAllocator.java',
-    'WebappImpl.java',
     'widget/ActivityChooserModel.java',
     'widget/AllCapsTextView.java',
     'widget/AnimatedHeightLayout.java',
     'widget/ArrowPopup.java',
     'widget/BasicColorPicker.java',
     'widget/ButtonToast.java',
     'widget/CheckableLinearLayout.java',
     'widget/ClickableWhenDisabledEditText.java',
--- a/mobile/android/base/webapp/EventListener.java
+++ b/mobile/android/base/webapp/EventListener.java
@@ -1,50 +1,42 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.webapp;
 
-import org.mozilla.gecko.ActivityHandlerHelper;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.util.ActivityResultHandler;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.WebappAllocator;
-
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.util.Log;
-
 import java.io.File;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.ActivityHandlerHelper;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.util.ActivityResultHandler;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.util.Log;
 
 public class EventListener implements NativeEventListener  {
 
     private static final String LOGTAG = "GeckoWebappEventListener";
 
     public void registerEvents() {
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
             "Webapps:Preinstall",
@@ -63,81 +55,39 @@ public class EventListener implements Na
             "Webapps:Open",
             "Webapps:Uninstall",
             "Webapps:GetApkVersions");
     }
 
     @Override
     public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
         try {
-            if (AppConstants.MOZ_ANDROID_SYNTHAPKS && event.equals("Webapps:InstallApk")) {
+            if (event.equals("Webapps:InstallApk")) {
                 installApk(GeckoAppShell.getGeckoInterface().getActivity(), message, callback);
             } else if (event.equals("Webapps:Postinstall")) {
-                if (AppConstants.MOZ_ANDROID_SYNTHAPKS) {
-                    postInstallWebapp(message.getString("apkPackageName"), message.getString("origin"));
-                } else {
-                    postInstallWebapp(message.getString("name"),
-                                      message.getString("manifestURL"),
-                                      message.getString("origin"),
-                                      message.getString("iconURL"),
-                                      message.getString("originalOrigin"));
-                }
+                postInstallWebapp(message.getString("apkPackageName"), message.getString("origin"));
             } else if (event.equals("Webapps:Open")) {
                 Intent intent = GeckoAppShell.getWebappIntent(message.getString("manifestURL"),
                                                               message.getString("origin"),
                                                               "", null);
                 if (intent == null) {
                     return;
                 }
                 GeckoAppShell.getGeckoInterface().getActivity().startActivity(intent);
-            } else if (!AppConstants.MOZ_ANDROID_SYNTHAPKS && event.equals("Webapps:Uninstall")) {
-                uninstallWebapp(message.getString("origin"));
-            } else if (!AppConstants.MOZ_ANDROID_SYNTHAPKS && event.equals("Webapps:Preinstall")) {
-                String name = message.getString("name");
-                String manifestURL = message.getString("manifestURL");
-                String origin = message.getString("origin");
-
-                JSONObject obj = new JSONObject();
-                obj.put("profile", preInstallWebapp(name, manifestURL, origin).toString());
-                callback.sendSuccess(obj);
             } else if (event.equals("Webapps:GetApkVersions")) {
                 JSONObject obj = new JSONObject();
                 obj.put("versions", getApkVersions(GeckoAppShell.getGeckoInterface().getActivity(),
                                                    message.getStringArray("packageNames")));
                 callback.sendSuccess(obj);
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
-    // Not used by MOZ_ANDROID_SYNTHAPKS.
-    public static File preInstallWebapp(String aTitle, String aURI, String aOrigin) {
-        int index = WebappAllocator.getInstance(GeckoAppShell.getContext()).findAndAllocateIndex(aOrigin, aTitle, (String) null);
-        GeckoProfile profile = GeckoProfile.get(GeckoAppShell.getContext(), "webapp" + index);
-        return profile.getDir();
-    }
-
-    // Not used by MOZ_ANDROID_SYNTHAPKS.
-    public static void postInstallWebapp(String aTitle, String aURI, String aOrigin, String aIconURL, String aOriginalOrigin) {
-        WebappAllocator allocator = WebappAllocator.getInstance(GeckoAppShell.getContext());
-        int index = allocator.getIndexForApp(aOriginalOrigin);
-
-        assert aIconURL != null;
-
-        final int preferredSize = GeckoAppShell.getPreferredIconSize();
-        Bitmap icon = FaviconDecoder.getMostSuitableBitmapFromDataURI(aIconURL, preferredSize);
-
-        assert aOrigin != null && index != -1;
-        allocator.updateAppAllocation(aOrigin, index, icon);
-
-        GeckoAppShell.createShortcut(aTitle, aURI, aOrigin, icon, "webapp");
-    }
-
-    // Used by MOZ_ANDROID_SYNTHAPKS.
     public static void postInstallWebapp(String aPackageName, String aOrigin) {
         Allocator allocator = Allocator.getInstance(GeckoAppShell.getContext());
         int index = allocator.findOrAllocatePackage(aPackageName);
         allocator.putOrigin(index, aOrigin);
     }
 
     public static void uninstallWebapp(final String packageName) {
         // On uninstall, we need to do a couple of things:
--- a/mobile/android/base/webapp/UninstallListener.java
+++ b/mobile/android/base/webapp/UninstallListener.java
@@ -142,14 +142,12 @@ public class UninstallListener extends B
             mApp = app;
         }
 
         @Override
         public void run() {
             ThreadUtils.assertOnBackgroundThread();
 
             // Perform webapp uninstalls as appropiate.
-            if (AppConstants.MOZ_ANDROID_SYNTHAPKS) {
-                UninstallListener.initUninstallPackageScan(mApp.getApplicationContext());
-            }
+            UninstallListener.initUninstallPackageScan(mApp.getApplicationContext());
         }
     }
 }
--- a/mobile/android/chrome/content/WebappRT.js
+++ b/mobile/android/chrome/content/WebappRT.js
@@ -6,21 +6,19 @@ let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/PermissionsInstaller.jsm");
 Cu.import("resource://gre/modules/PermissionPromptHelper.jsm");
 Cu.import("resource://gre/modules/ContactService.jsm");
-#ifdef MOZ_ANDROID_SYNTHAPKS
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
-#endif
 
 function pref(name, value) {
   return {
     name: name,
     value: value
   }
 }
 
@@ -65,24 +63,22 @@ let WebappRT = {
     if (aStatus == "new" || aStatus == "upgrade") {
       this.getManifestFor(aUrl, function (aManifest, aApp) {
         if (aManifest) {
           PermissionsInstaller.installPermissions(aApp, true);
         }
       });
     }
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
     // If the app is in debug mode, configure and enable the remote debugger.
     sendMessageToJava({ type: "NativeApp:IsDebuggable" }, (response) => {
       if (response.isDebuggable) {
         this._enableRemoteDebugger(aUrl);
       }
     });
-#endif
 
     this.findManifestUrlFor(aUrl, aCallback);
   },
 
   getManifestFor: function (aUrl, aCallback) {
     DOMApplicationRegistry.registryReady.then(() => {
       let request = navigator.mozApps.mgmt.getAll();
       request.onsuccess = function() {
@@ -158,17 +154,16 @@ let WebappRT = {
         Services.prefs.setBoolPref(aPref.name, aPref.value);
         break;
       case "number":
         Services.prefs.setIntPref(aPref.name, aPref.value);
         break;
     }
   },
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
   _enableRemoteDebugger: function(aUrl) {
     // Skip the connection prompt in favor of notifying the user below.
     Services.prefs.setBoolPref("devtools.debugger.prompt-connection", false);
 
     // Automagically find a free port and configure the debugger to use it.
     let serv = Cc['@mozilla.org/network/server-socket;1'].createInstance(Ci.nsIServerSocket);
     serv.init(-1, true, -1);
     let port = serv.port;
@@ -190,17 +185,16 @@ let WebappRT = {
 
       Notifications.create({
         title: Strings.browser.formatStringFromName("remoteNotificationTitle", [name], 1),
         message: Strings.browser.formatStringFromName("remoteNotificationMessage", [port], 1),
         icon: "drawable://warning_doorhanger",
       });
     });
   },
-#endif
 
   handleEvent: function(event) {
     let target = event.target;
 
     // walk up the tree to find the nearest link tag
     while (target && !(target instanceof HTMLAnchorElement)) {
       target = target.parentNode;
     }
--- a/mobile/android/chrome/content/aboutApps.js
+++ b/mobile/android/chrome/content/aboutApps.js
@@ -7,19 +7,17 @@
  * ***** END LICENSE BLOCK ***** */
 
 let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm")
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 XPCOMUtils.defineLazyModuleGetter(this, "WebappManager", "resource://gre/modules/WebappManager.jsm");
-#endif
 
 const DEFAULT_ICON = "chrome://browser/skin/images/default-app-icon.png";
 
 let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutApps.properties");
 
 XPCOMUtils.defineLazyGetter(window, "gChromeWin", function()
   window.QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIWebNavigation)
@@ -40,79 +38,34 @@ function openLink(aEvent) {
   try {
     let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
     let url = formatter.formatURLPref(aEvent.currentTarget.getAttribute("pref"));
     let BrowserApp = gChromeWin.BrowserApp;
     BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id });
   } catch (ex) {}
 }
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 function checkForUpdates(aEvent) {
   WebappManager.checkForUpdates(true);
 }
-#endif
-
-#ifndef MOZ_ANDROID_SYNTHAPKS
-var ContextMenus = {
-  target: null,
-
-  init: function() {
-    document.addEventListener("contextmenu", this, false);
-    document.getElementById("addToHomescreenLabel").addEventListener("click", this.addToHomescreen, false);
-    document.getElementById("uninstallLabel").addEventListener("click", this.uninstall, false);
-  },
-
-  handleEvent: function(event) {
-    // store the target of context menu events so that we know which app to act on
-    this.target = event.target;
-    while (!this.target.hasAttribute("contextmenu")) {
-      this.target = this.target.parentNode;
-    }
-  },
-
-  addToHomescreen: function() {
-    let manifest = this.target.manifest;
-    gChromeWin.WebappsUI.createShortcut(manifest.name, manifest.fullLaunchPath(), manifest.biggestIconURL || DEFAULT_ICON, "webapp");
-    this.target = null;
-  },
-
-  uninstall: function() {
-    navigator.mozApps.mgmt.uninstall(this.target.app);
-
-    let manifest = this.target.manifest;
-    gChromeWin.sendMessageToJava({
-      type: "Shortcut:Remove",
-      title: manifest.name,
-      url: manifest.fullLaunchPath(),
-      origin: this.target.app.origin,
-      shortcutType: "webapp"
-    });
-    this.target = null;
-  }
-}
-#endif
 
 function onLoad(aEvent) {
   let elmts = document.querySelectorAll("[pref]");
   for (let i = 0; i < elmts.length; i++) {
     elmts[i].addEventListener("click",  openLink,  false);
   }
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
   document.getElementById("update-item").addEventListener("click", checkForUpdates, false);
-#endif
 
   navigator.mozApps.mgmt.oninstall = onInstall;
   navigator.mozApps.mgmt.onuninstall = onUninstall;
   updateList();
 
-#ifndef MOZ_ANDROID_SYNTHAPKS
-  ContextMenus.init();
-#endif
+  // XXX - Hack to fix bug 985867 for now
+  document.addEventListener("touchstart", function() { });
 }
 
 function updateList() {
   let grid = document.getElementById("appgrid");
   while (grid.lastChild) {
     grid.removeChild(grid.lastChild);
   }
 
@@ -126,19 +79,16 @@ function updateList() {
 }
 
 function addApplication(aApp) {
   let list = document.getElementById("appgrid");
   let manifest = new ManifestHelper(aApp.manifest, aApp.origin);
 
   let container = document.createElement("div");
   container.className = "app list-item";
-#ifndef MOZ_ANDROID_SYNTHAPKS
-  container.setAttribute("contextmenu", "appmenu");
-#endif
   container.setAttribute("id", "app-" + aApp.origin);
   container.setAttribute("mozApp", aApp.origin);
   container.setAttribute("title", manifest.name);
 
   let img = document.createElement("img");
   img.src = manifest.biggestIconURL || DEFAULT_ICON;
   img.onerror = function() {
     // If the image failed to load, and it was not our default icon, attempt to
--- a/mobile/android/chrome/content/aboutApps.xhtml
+++ b/mobile/android/chrome/content/aboutApps.xhtml
@@ -23,24 +23,16 @@
     <meta name="viewport" content="width=device-width; user-scalable=0" />
     <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
     <link rel="stylesheet" type="text/css" href="chrome://browser/skin/aboutBase.css" media="all" />
     <link rel="stylesheet" type="text/css" href="chrome://browser/skin/aboutApps.css" media="all" />
     <script type="text/javascript;version=1.8" src="chrome://browser/content/aboutApps.js"></script>
   </head>
 
   <body dir="&locale.dir;">
-
-#ifndef MOZ_ANDROID_SYNTHAPKS
-    <menu type="context" id="appmenu">
-      <menuitem id="addToHomescreenLabel" label="&aboutApps.addToHomescreen;"></menuitem>
-      <menuitem id="uninstallLabel" label="&aboutApps.uninstall;"></menuitem>
-    </menu>
-#endif
-
     <div class="header">
       <div>&aboutApps.header;</div>
       <div id="header-button" role="button" aria-label="&aboutApps.browseMarketplace;" pref="app.marketplaceURL"/>
     </div>
 
     <div id="main-container" class="hidden">
       <div>
         <div class="spacer" id="spacer1"> </div>
@@ -50,18 +42,16 @@
     </div>
 
     <div class="list-item" role="button" pref="app.marketplaceURL">
       <img class="icon" src="chrome://browser/skin/images/marketplace-logo.png" />
       <div class="inner">
         <div id="browse-title" class="title">&aboutApps.browseMarketplace;</div>
       </div>
     </div>
-#ifdef MOZ_ANDROID_SYNTHAPKS
     <div class="list-item" id="update-item" role="button">
       <img class="icon" src="chrome://browser/skin/images/update.png" />
       <div class="inner">
         <div id="browse-title" class="title">&aboutApps.checkForUpdates;</div>
       </div>
     </div>
-#endif
   </body>
 </html>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -72,20 +72,18 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
                                   "resource://gre/modules/SimpleServiceDiscovery.jsm");
 
 #ifdef NIGHTLY_BUILD
 XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils",
                                   "resource://shumway/ShumwayUtils.jsm");
 #endif
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 XPCOMUtils.defineLazyModuleGetter(this, "WebappManager",
                                   "resource://gre/modules/WebappManager.jsm");
-#endif
 
 XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
                                   "resource://gre/modules/CharsetMenu.jsm");
 
 // Lazily-loaded browser scripts:
 [
   ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
   ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
@@ -321,26 +319,24 @@ var BrowserApp = {
     Services.obs.addObserver(this, "FullScreen:Exit", false);
     Services.obs.addObserver(this, "Viewport:Change", false);
     Services.obs.addObserver(this, "Viewport:Flush", false);
     Services.obs.addObserver(this, "Viewport:FixedMarginsChanged", false);
     Services.obs.addObserver(this, "Passwords:Init", false);
     Services.obs.addObserver(this, "FormHistory:Init", false);
     Services.obs.addObserver(this, "gather-telemetry", false);
     Services.obs.addObserver(this, "keyword-search", false);
-#ifdef MOZ_ANDROID_SYNTHAPKS
     Services.obs.addObserver(this, "webapps-runtime-install", false);
     Services.obs.addObserver(this, "webapps-runtime-install-package", false);
     Services.obs.addObserver(this, "webapps-ask-install", false);
     Services.obs.addObserver(this, "webapps-launch", false);
     Services.obs.addObserver(this, "webapps-uninstall", false);
     Services.obs.addObserver(this, "Webapps:AutoInstall", false);
     Services.obs.addObserver(this, "Webapps:Load", false);
     Services.obs.addObserver(this, "Webapps:AutoUninstall", false);
-#endif
     Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
 
     function showFullScreenWarning() {
       NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
     }
 
     window.addEventListener("fullscreen", function() {
       sendMessageToJava({
@@ -372,23 +368,19 @@ var BrowserApp = {
     LightWeightThemeWebInstaller.init();
     Downloads.init();
     FormAssistant.init();
     IndexedDB.init();
     HealthReportStatusListener.init();
     XPInstallObserver.init();
     CharacterEncoding.init();
     ActivityObserver.init();
-#ifdef MOZ_ANDROID_SYNTHAPKS
     // TODO: replace with Android implementation of WebappOSUtils.isLaunchable.
     Cu.import("resource://gre/modules/Webapps.jsm");
     DOMApplicationRegistry.allAppsLaunchable = true;
-#else
-    WebappsUI.init();
-#endif
     RemoteDebugger.init();
     Reader.init();
     UserAgentOverrides.init();
     DesktopUserAgent.init();
     CastingApps.init();
     Distribution.init();
     Tabs.init();
 #ifdef ACCESSIBILITY
@@ -765,19 +757,16 @@ var BrowserApp = {
     LightWeightThemeWebInstaller.uninit();
     FormAssistant.uninit();
     IndexedDB.uninit();
     ViewportHandler.uninit();
     XPInstallObserver.uninit();
     HealthReportStatusListener.uninit();
     CharacterEncoding.uninit();
     SearchEngines.uninit();
-#ifndef MOZ_ANDROID_SYNTHAPKS
-    WebappsUI.uninit();
-#endif
     RemoteDebugger.uninit();
     Reader.uninit();
     UserAgentOverrides.uninit();
     DesktopUserAgent.uninit();
     ExternalApps.uninit();
     CastingApps.uninit();
     Distribution.uninit();
     Tabs.uninit();
@@ -956,25 +945,23 @@ var BrowserApp = {
 
     let message = {
       type: "Tab:Close",
       tabID: aTab.id
     };
     sendMessageToJava(message);
   },
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
   _loadWebapp: function(aMessage) {
 
     this._initRuntime(this._startupStatus, aMessage.url, aUrl => {
       this.manifestUrl = aMessage.url;
       this.addTab(aUrl, { title: aMessage.name });
     });
   },
-#endif
 
   // Calling this will update the state in BrowserApp after a tab has been
   // closed in the Java UI.
   _handleTabClosed: function _handleTabClosed(aTab, aShowUndoToast) {
     if (aTab == this.selectedTab)
       this.selectedTab = null;
 
     let tabIndex = this._tabs.indexOf(aTab);
@@ -1622,17 +1609,16 @@ var BrowserApp = {
         gViewportMargins = JSON.parse(aData);
         this.selectedTab.updateViewportSize(gScreenWidth);
         break;
 
       case "nsPref:changed":
         this.notifyPrefObservers(aData);
         break;
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
       case "webapps-runtime-install":
         WebappManager.install(JSON.parse(aData), aSubject);
         break;
 
       case "webapps-runtime-install-package":
         WebappManager.installPackage(JSON.parse(aData), aSubject);
         break;
 
@@ -1656,31 +1642,38 @@ var BrowserApp = {
 
       case "Webapps:Load":
         this._loadWebapp(JSON.parse(aData));
         break;
 
       case "Webapps:AutoUninstall":
         WebappManager.autoUninstall(JSON.parse(aData));
         break;
-#endif
 
       case "Locale:Changed":
         if (aData) {
           // The value provided to Locale:Changed should be a BCP47 language tag
           // understood by Gecko -- for example, "es-ES" or "de".
           console.log("Locale:Changed: " + aData);
           Services.prefs.setCharPref("general.useragent.locale", aData);
         } else {
           // Resetting.
           console.log("Switching to system locale.");
           Services.prefs.clearUserPref("general.useragent.locale");
         }
 
         Services.prefs.setBoolPref("intl.locale.matchOS", !aData);
+
+        // Ensure that this choice is immediately persisted, because
+        // Gecko won't be told again if it forgets.
+        Services.prefs.savePrefFile(null);
+
+        // Blow away the string cache so that future lookups get the
+        // correct locale.
+        Services.strings.flushBundles();
         break;
 
       default:
         dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
         break;
 
     }
   },
@@ -2285,26 +2278,56 @@ var NativeWindow = {
         if (!this._findMenuItem(item.id) && item.matches(element, x, y)) {
           res.push(item);
         }
       }
 
       return res;
     },
 
+    _findTarget: function(x, y) {
+      let isDescendant = function(parent, child) {
+        let node = child;
+        while (node) {
+          if (node === parent) {
+            return true;
+          }
+
+          node = node.parentNode;
+        }
+
+        return false;
+      };
+
+      let target = BrowserEventHandler._highlightElement;
+      let touchTarget = ElementTouchHelper.anyElementFromPoint(x, y);
+
+      // If we have a highlighted element that has a click handler, we want to ensure our target is inside it
+      if (isDescendant(target, touchTarget)) {
+        target = touchTarget;
+      } else if (!target) {
+        // Otherwise, let's try to find something clickable
+        target = ElementTouchHelper.elementFromPoint(x, y);
+
+        // If that failed, we'll just fall back to anything under the user's finger
+        if (!target) {
+          target = touchTarget;
+        }
+      }
+
+      return target;
+    },
+
     /* Checks if there are context menu items to show, and if it finds them
      * sends a contextmenu event to content. We also send showing events to
      * any html5 context menus we are about to show, and fire some local notifications
      * for chrome consumers to do lazy menuitem construction
      */
     _sendToContent: function(x, y) {
-      let target = BrowserEventHandler._highlightElement || ElementTouchHelper.elementFromPoint(x, y);
-      if (!target)
-        target = ElementTouchHelper.anyElementFromPoint(x, y);
-
+      let target = this._findTarget(x, y);
       if (!target)
         return;
 
       this._target = target;
 
       Services.obs.notifyObservers(null, "before-build-contextmenu", "");
       this._buildMenu(x, y);
 
@@ -7025,256 +7048,16 @@ var ActivityObserver = {
     }
 
     if (tab && tab.getActive() != isForeground) {
       tab.setActive(isForeground);
     }
   }
 };
 
-#ifndef MOZ_ANDROID_SYNTHAPKS
-var WebappsUI = {
-  init: function init() {
-    Cu.import("resource://gre/modules/Webapps.jsm");
-    Cu.import("resource://gre/modules/AppsUtils.jsm");
-    DOMApplicationRegistry.allAppsLaunchable = true;
-
-    Services.obs.addObserver(this, "webapps-ask-install", false);
-    Services.obs.addObserver(this, "webapps-launch", false);
-    Services.obs.addObserver(this, "webapps-uninstall", false);
-    Services.obs.addObserver(this, "webapps-install-error", false);
-  },
-
-  uninit: function unint() {
-    Services.obs.removeObserver(this, "webapps-ask-install");
-    Services.obs.removeObserver(this, "webapps-launch");
-    Services.obs.removeObserver(this, "webapps-uninstall");
-    Services.obs.removeObserver(this, "webapps-install-error");
-  },
-
-  DEFAULT_ICON: "chrome://browser/skin/images/default-app-icon.png",
-  DEFAULT_PREFS_FILENAME: "default-prefs.js",
-
-  observe: function observe(aSubject, aTopic, aData) {
-    let data = {};
-    try {
-      data = JSON.parse(aData);
-      data.mm = aSubject;
-    } catch(ex) { }
-    switch (aTopic) {
-      case "webapps-install-error":
-        let msg = "";
-        switch (aData) {
-          case "INVALID_MANIFEST":
-          case "MANIFEST_PARSE_ERROR":
-            msg = Strings.browser.GetStringFromName("webapps.manifestInstallError");
-            break;
-          case "NETWORK_ERROR":
-          case "MANIFEST_URL_ERROR":
-            msg = Strings.browser.GetStringFromName("webapps.networkInstallError");
-            break;
-          default:
-            msg = Strings.browser.GetStringFromName("webapps.installError");
-        }
-        NativeWindow.toast.show(msg, "short");
-        console.log("Error installing app: " + aData);
-        break;
-      case "webapps-ask-install":
-        this.doInstall(data);
-        break;
-      case "webapps-launch":
-        this.openURL(data.manifestURL, data.origin);
-        break;
-      case "webapps-uninstall":
-        sendMessageToJava({
-          type: "Webapps:Uninstall",
-          origin: data.origin
-        });
-        break;
-    }
-  },
-
-  doInstall: function doInstall(aData) {
-    let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
-    let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
-
-    if (Services.prompt.confirm(null, Strings.browser.GetStringFromName("webapps.installTitle"), manifest.name + "\n" + aData.app.origin)) {
-      // Get a profile for the app to be installed in. We'll download everything before creating the icons.
-      let origin = aData.app.origin;
-      sendMessageToJava({
-         type: "Webapps:Preinstall",
-         name: manifest.name,
-         manifestURL: aData.app.manifestURL,
-         origin: origin
-      }, (data) => {
-        let profilePath = data.profile;
-        if (!profilePath)
-          return;
-
-        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
-        file.initWithPath(profilePath);
-
-        let self = this;
-        DOMApplicationRegistry.confirmInstall(aData, file,
-          function (aManifest) {
-            let localeManifest = new ManifestHelper(aManifest, aData.app.origin);
-
-            // the manifest argument is the manifest from within the zip file,
-            // TODO so now would be a good time to ask about permissions.
-            self.makeBase64Icon(localeManifest.biggestIconURL || this.DEFAULT_ICON,
-              function(scaledIcon, fullsizeIcon) {
-                // if java returned a profile path to us, try to use it to pre-populate the app cache
-                // also save the icon so that it can be used in the splash screen
-                try {
-                  let iconFile = file.clone();
-                  iconFile.append("logo.png");
-                  let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist);
-                  persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
-                  persist.persistFlags |= Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
-
-                  let source = Services.io.newURI(fullsizeIcon, "UTF8", null);
-                  persist.saveURI(source, null, null, null, null, iconFile, null);
-
-                  // aData.app.origin may now point to the app: url that hosts this app
-                  sendMessageToJava({
-                    type: "Webapps:Postinstall",
-                    name: localeManifest.name,
-                    manifestURL: aData.app.manifestURL,
-                    originalOrigin: origin,
-                    origin: aData.app.origin,
-                    iconURL: fullsizeIcon
-                  });
-                  if (!!aData.isPackage) {
-                    // For packaged apps, put a notification in the notification bar.
-                    let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
-                    let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
-                    alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", {
-                      observe: function () {
-                        self.openURL(aData.app.manifestURL, aData.app.origin);
-                      }
-                    }, "webapp");
-                  }
-
-                  // Create a system notification allowing the user to launch the app
-                  let observer = {
-                    observe: function (aSubject, aTopic) {
-                      if (aTopic == "alertclickcallback") {
-                        WebappsUI.openURL(aData.app.manifestURL, origin);
-                      }
-                    }
-                  };
-
-                  let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
-                  let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
-                  alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", observer, "webapp");
-                } catch(ex) {
-                  console.log(ex);
-                }
-                self.writeDefaultPrefs(file, localeManifest);
-              }
-            );
-          }
-        );
-      });
-    } else {
-      DOMApplicationRegistry.denyInstall(aData);
-    }
-  },
-
-  writeDefaultPrefs: function webapps_writeDefaultPrefs(aProfile, aManifest) {
-      // build any app specific default prefs
-      let prefs = [];
-      if (aManifest.orientation) {
-        prefs.push({name:"app.orientation.default", value: aManifest.orientation.join(",") });
-      }
-
-      // write them into the app profile
-      let defaultPrefsFile = aProfile.clone();
-      defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME);
-      this._writeData(defaultPrefsFile, prefs);
-  },
-
-  _writeData: function(aFile, aPrefs) {
-    if (aPrefs.length > 0) {
-      let array = new TextEncoder().encode(JSON.stringify(aPrefs));
-      OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) {
-        console.log("Error writing default prefs: " + reason);
-      });
-    }
-  },
-
-  openURL: function openURL(aManifestURL, aOrigin) {
-    sendMessageToJava({
-      type: "Webapps:Open",
-      manifestURL: aManifestURL,
-      origin: aOrigin
-    });
-  },
-
-  get iconSize() {
-    let iconSize = 64;
-    try {
-      let jni = new JNI();
-      let cls = jni.findClass("org/mozilla/gecko/GeckoAppShell");
-      let method = jni.getStaticMethodID(cls, "getPreferredIconSize", "()I");
-      iconSize = jni.callStaticIntMethod(cls, method);
-      jni.close();
-    } catch(ex) {
-      console.log(ex);
-    }
-
-    delete this.iconSize;
-    return this.iconSize = iconSize;
-  },
-
-  makeBase64Icon: function loadAndMakeBase64Icon(aIconURL, aCallbackFunction) {
-    let size = this.iconSize;
-
-    let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-    canvas.width = canvas.height = size;
-    let ctx = canvas.getContext("2d");
-    let favicon = new Image();
-    favicon.onload = function() {
-      ctx.drawImage(favicon, 0, 0, size, size);
-      let scaledIcon = canvas.toDataURL("image/png", "");
-
-      canvas.width = favicon.width;
-      canvas.height = favicon.height;
-      ctx.drawImage(favicon, 0, 0, favicon.width, favicon.height);
-      let fullsizeIcon = canvas.toDataURL("image/png", "");
-
-      canvas = null;
-      aCallbackFunction.call(null, scaledIcon, fullsizeIcon);
-    };
-    favicon.onerror = function() {
-      Cu.reportError("CreateShortcut: favicon image load error");
-
-      // if the image failed to load, and it was not our default icon, attempt to
-      // use our default as a fallback
-      if (favicon.src != WebappsUI.DEFAULT_ICON) {
-        favicon.src = WebappsUI.DEFAULT_ICON;
-      }
-    };
-  
-    favicon.src = aIconURL;
-  },
-
-  createShortcut: function createShortcut(aTitle, aURL, aIconURL, aType) {
-    this.makeBase64Icon(aIconURL, function _createShortcut(icon) {
-      try {
-        let shell = Cc["@mozilla.org/browser/shell-service;1"].createInstance(Ci.nsIShellService);
-        shell.createShortcut(aTitle, aURL, icon, aType);
-      } catch(e) {
-        Cu.reportError(e);
-      }
-    });
-  }
-}
-#endif
-
 var RemoteDebugger = {
   init: function rd_init() {
     Services.prefs.addObserver("devtools.debugger.", this, false);
 
     if (this._isEnabled())
       this._start();
   },
 
--- a/mobile/android/components/MobileComponents.manifest
+++ b/mobile/android/components/MobileComponents.manifest
@@ -108,22 +108,20 @@ contract @mozilla.org/tab-source-service
 #endif
 
 # Snippets.js
 component {a78d7e59-b558-4321-a3d6-dffe2f1e76dd} Snippets.js
 contract @mozilla.org/snippets;1 {a78d7e59-b558-4321-a3d6-dffe2f1e76dd}
 category profile-after-change Snippets @mozilla.org/snippets;1
 category update-timer Snippets @mozilla.org/snippets;1,getService,snippets-update-timer,browser.snippets.updateInterval,86400
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 # WebappsUpdateTimer.js
 component {8f7002cb-e959-4f0a-a2e8-563232564385} WebappsUpdateTimer.js
 contract @mozilla.org/webapps-update-timer;1 {8f7002cb-e959-4f0a-a2e8-563232564385}
 category update-timer WebappsUpdateTimer @mozilla.org/webapps-update-timer;1,getService,webapp-background-update-timer,browser.webapps.updateInterval,86400
-#endif
 
 # ColorPicker.js
 component {430b987f-bb9f-46a3-99a5-241749220b29} ColorPicker.js
 contract @mozilla.org/colorpicker;1 {430b987f-bb9f-46a3-99a5-241749220b29}
 
 # ActivitiesGlue.js
 component {e4deb5f6-d5e3-4fce-bc53-901dd9951c48} ActivitiesGlue.js
 contract @mozilla.org/dom/activities/ui-glue;1 {e4deb5f6-d5e3-4fce-bc53-901dd9951c48}
--- a/mobile/android/confvars.sh
+++ b/mobile/android/confvars.sh
@@ -62,13 +62,10 @@ MOZ_SERVICES_HEALTHREPORT=1
 MOZ_SERVICES_FXACCOUNTS=1
 
 # Enable Wifi-AP/cell tower data reporting
 MOZ_DATA_REPORTING=1
 
 # Enable runtime locale switching.
 MOZ_LOCALE_SWITCHER=1
 
-# Enable the "synthetic APKs" implementation of Open Web Apps.
-MOZ_ANDROID_SYNTHAPKS=1
-
 # Enable second screen and casting support for external devices.
 MOZ_DEVICES=1
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -616,15 +616,12 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DL
 
 #ifdef ENABLE_MARIONETTE
 @BINPATH@/chrome/marionette@JAREXT@ 
 @BINPATH@/chrome/marionette.manifest
 @BINPATH@/components/MarionetteComponents.manifest
 @BINPATH@/components/marionettecomponent.js
 #endif
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 @BINPATH@/components/WebappsUpdateTimer.js
-#endif
-
 @BINPATH@/components/DataStore.manifest
 @BINPATH@/components/DataStoreImpl.js
 @BINPATH@/components/dom_datastore.xpt
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -237,26 +237,16 @@ stacktrace.anonymousFunction=<anonymous>
 stacktrace.outputMessage=Stack trace from %S, function %S, line %S.
 timer.start=%S: timer started
 
 # LOCALIZATION NOTE (timer.end):
 # This string is used to display the result of the console.timeEnd() call.
 # %1$S=name of timer, %2$S=number of milliseconds
 timer.end=%1$S: %2$Sms
 
-# Webapps
-webapps.installTitle=Install Application
-webapps.alertSuccess=Successfully installed
-# Shown when there is a generic problem installing an app
-webapps.installError=Error installing application
-# Shown when there is something wrong with an apps manifest
-webapps.manifestInstallError=Invalid application manifest
-# Shown when a network error prevented installing an app
-webapps.networkInstallError=Could not download manifest
-
 # Click to play plugins
 clickToPlayPlugins.message2=%S contains plugin content. Would you like to activate it?
 clickToPlayPlugins.activate=Activate
 clickToPlayPlugins.dontActivate=Don't activate
 # LOCALIZATION NOTE (clickToPlayPlugins.dontAskAgain): This label appears next to a
 # checkbox to indicate whether or not the user wants to make a permanent decision.
 clickToPlayPlugins.dontAskAgain=Don't ask again for this site
 # LOCALIZATION NOTE (clickToPlayPlugins.playPlugins): Label that
--- a/mobile/android/locales/jar.mn
+++ b/mobile/android/locales/jar.mn
@@ -32,19 +32,17 @@
   locale/@AB_CD@/browser/pippki.properties        (%chrome/pippki.properties)
   locale/@AB_CD@/browser/sync.dtd                 (%chrome/sync.dtd)
   locale/@AB_CD@/browser/sync.properties          (%chrome/sync.properties)
   locale/@AB_CD@/browser/prompt.dtd               (%chrome/prompt.dtd)
   locale/@AB_CD@/browser/feedback.dtd             (%chrome/feedback.dtd)
   locale/@AB_CD@/browser/phishing.dtd             (%chrome/phishing.dtd)
   locale/@AB_CD@/browser/payments.properties      (%chrome/payments.properties)
   locale/@AB_CD@/browser/handling.properties      (%chrome/handling.properties)
-#ifdef MOZ_ANDROID_SYNTHAPKS
   locale/@AB_CD@/browser/webapp.properties        (%chrome/webapp.properties)
-#endif
 
 # overrides for toolkit l10n, also for en-US
 relativesrcdir toolkit/locales:
   locale/@AB_CD@/browser/overrides/about.dtd                       (%chrome/global/about.dtd)
   locale/@AB_CD@/browser/overrides/aboutAbout.dtd                  (%chrome/global/aboutAbout.dtd)
   locale/@AB_CD@/browser/overrides/aboutRights.dtd                 (%chrome/global/aboutRights.dtd)
   locale/@AB_CD@/browser/overrides/charsetMenu.properties          (%chrome/global/charsetMenu.properties)
   locale/@AB_CD@/browser/overrides/commonDialogs.properties        (%chrome/global/commonDialogs.properties)
--- a/mobile/android/modules/moz.build
+++ b/mobile/android/modules/moz.build
@@ -17,21 +17,15 @@ EXTRA_JS_MODULES += [
     'Messaging.jsm',
     'Notifications.jsm',
     'OrderedBroadcast.jsm',
     'Prompt.jsm',
     'Sanitizer.jsm',
     'SharedPreferences.jsm',
     'SimpleServiceDiscovery.jsm',
     'SSLExceptions.jsm',
+    'WebappManagerWorker.js',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'RokuApp.jsm',
+    'WebappManager.jsm',
 ]
-
-if CONFIG['MOZ_ANDROID_SYNTHAPKS']:
-    EXTRA_PP_JS_MODULES += [
-        'WebappManager.jsm',
-    ]
-    EXTRA_JS_MODULES += [
-        'WebappManagerWorker.js',
-    ]
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -644,16 +644,17 @@ FxAccountsInternal.prototype = {
 
       if (logPII) {
         log.debug("kB_hex: " + kB_hex);
       }
       data.kA = CommonUtils.bytesAsHex(kA);
       data.kB = CommonUtils.bytesAsHex(kB_hex);
 
       delete data.keyFetchToken;
+      delete data.unwrapBKey;
 
       log.debug("Keys Obtained: kA=" + !!data.kA + ", kB=" + !!data.kB);
       if (logPII) {
         log.debug("Keys Obtained: kA=" + data.kA + ", kB=" + data.kB);
       }
 
       yield currentState.setUserAccountData(data);
       // We are now ready for business. This should only be invoked once
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -289,27 +289,29 @@ add_test(function test_getKeys() {
   // Once email has been verified, we will be able to get keys
   user.verified = true;
 
   fxa.setSignedInUser(user).then(() => {
     fxa.getSignedInUser().then((user) => {
       // Before getKeys, we have no keys
       do_check_eq(!!user.kA, false);
       do_check_eq(!!user.kB, false);
-      // And we still have a key-fetch token to use
+      // And we still have a key-fetch token and unwrapBKey to use
       do_check_eq(!!user.keyFetchToken, true);
+      do_check_eq(!!user.unwrapBKey, true);
 
       fxa.internal.getKeys().then(() => {
         fxa.getSignedInUser().then((user) => {
           // Now we should have keys
           do_check_eq(fxa.internal.isUserEmailVerified(user), true);
           do_check_eq(!!user.verified, true);
           do_check_eq(user.kA, expandHex("11"));
           do_check_eq(user.kB, expandHex("66"));
           do_check_eq(user.keyFetchToken, undefined);
+          do_check_eq(user.unwrapBKey, undefined);
           do_test_finished();
           run_next_test();
         });
       });
     });
   });
 });
 
--- a/toolkit/components/places/PlacesBackups.jsm
+++ b/toolkit/components/places/PlacesBackups.jsm
@@ -58,18 +58,17 @@ function getHashFromFilename(aFilename) 
 /**
  * Given two filenames, checks if they contain the same date.
  */
 function isFilenameWithSameDate(aSourceName, aTargetName) {
   let sourceMatches = aSourceName.match(filenamesRegex);
   let targetMatches = aTargetName.match(filenamesRegex);
 
   return sourceMatches && targetMatches &&
-         sourceMatches[1] == targetMatches[1] &&
-         sourceMatches[4] == targetMatches[4];
+         sourceMatches[1] == targetMatches[1];
 }
 
 /**
  * Given a filename, searches for another backup with the same date.
  *
  * @return OS.File path string or null.
  */
 function getBackupFileForSameDate(aFilename) {
@@ -433,16 +432,20 @@ this.PlacesBackups = {
                                                            { count: nodeCount,
                                                              hash: hash });
       } catch (ex if ex.becauseSameHash) {
         // The last backup already contained up-to-date information, just
         // rename it as if it was today's backup.
         this._backupFiles.shift();
         this._entries.shift();
         newBackupFile = mostRecentBackupFile;
+        // Ensure we retain the proper extension when renaming
+        // the most recent backup file.
+        if (/\.json$/.test(OS.Path.basename(mostRecentBackupFile)))
+          newBackupFilename = this.getFilenameForDate();
         newFilenameWithMetaData = appendMetaDataToFilename(
           newBackupFilename,
           { count: this.getBookmarkCountForFile(mostRecentBackupFile),
             hash: mostRecentHash });
       }
 
       // Append metadata to the backup filename.
       let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js
@@ -0,0 +1,103 @@
+function run_test() {
+  run_next_test();
+}
+
+/* Bug 1016953 - When a previous bookmark backup exists with the same hash
+regardless of date, an automatic backup should attempt to either rename it to
+today's date if the backup was for an old date or leave it alone if it was for
+the same date. However if the file ext was json it will accidentally rename it
+to jsonlz4 while keeping the json contents
+*/
+
+add_task(function* test_same_date_same_hash() {
+  // If old file has been created on the same date and has the same hash
+  // the file should be left alone
+  let backupFolder = yield PlacesBackups.getBackupFolder();
+  // Save to profile dir to obtain hash and nodeCount to append to filename
+  let tempPath = OS.Path.join(OS.Constants.Path.profileDir,
+                              "bug10169583_bookmarks.json");
+  let {count, hash} = yield BookmarkJSONUtils.exportToFile(tempPath);
+
+  // Save JSON file in backup folder with hash appended
+  let dateObj = new Date();
+  let filename = "bookmarks-" + dateObj.toLocaleFormat("%Y-%m-%d") + "_" +
+                  count + "_" + hash + ".json";
+  let backupFile = OS.Path.join(backupFolder, filename);
+  yield OS.File.move(tempPath, backupFile);
+
+  // Force a compressed backup which fallbacks to rename
+  yield PlacesBackups.create();
+  let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+  // check to ensure not renamed to jsonlz4
+  Assert.equal(mostRecentBackupFile, backupFile);
+  // inspect contents and check if valid json
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+                        createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  let result = yield OS.File.read(mostRecentBackupFile);
+  let jsonString = converter.convertFromByteArray(result, result.length);
+  do_log_info("Check is valid JSON");
+  JSON.parse(jsonString);
+
+  // Cleanup
+  yield OS.File.remove(backupFile);
+  yield OS.File.remove(tempPath);
+  PlacesBackups._backupFiles = null; // To force re-cache of backupFiles
+});
+
+add_task(function* test_same_date_diff_hash() {
+  // If the old file has been created on the same date, but has a different hash
+  // the existing file should be overwritten with the newer compressed version
+  let backupFolder = yield PlacesBackups.getBackupFolder();
+  let tempPath = OS.Path.join(OS.Constants.Path.profileDir,
+                              "bug10169583_bookmarks.json");
+  let {count, hash} = yield BookmarkJSONUtils.exportToFile(tempPath);
+  let dateObj = new Date();
+  let filename = "bookmarks-" + dateObj.toLocaleFormat("%Y-%m-%d") + "_" +
+                  count + "_" + "differentHash==" + ".json";
+  let backupFile = OS.Path.join(backupFolder, filename);
+  yield OS.File.move(tempPath, backupFile);
+  yield PlacesBackups.create(); // Force compressed backup
+  mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+
+  // Decode lz4 compressed file to json and check if json is valid
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+                        createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  let result = yield OS.File.read(mostRecentBackupFile, { compression: "lz4" });
+  let jsonString = converter.convertFromByteArray(result, result.length);
+  do_log_info("Check is valid JSON");
+  JSON.parse(jsonString);
+
+  // Cleanup
+  yield OS.File.remove(mostRecentBackupFile);
+  yield OS.File.remove(tempPath);
+  PlacesBackups._backupFiles = null; // To force re-cache of backupFiles
+});
+
+add_task(function* test_diff_date_same_hash() {
+  // If the old file has been created on an older day but has the same hash
+  // it should be renamed with today's date without altering the contents.
+  let backupFolder = yield PlacesBackups.getBackupFolder();
+  let tempPath = OS.Path.join(OS.Constants.Path.profileDir,
+                              "bug10169583_bookmarks.json");
+  let {count, hash} = yield BookmarkJSONUtils.exportToFile(tempPath);
+  let oldDate = new Date(2014, 1, 1);
+  let curDate = new Date();
+  let oldFilename = "bookmarks-" + oldDate.toLocaleFormat("%Y-%m-%d") + "_" +
+                  count + "_" + hash + ".json";
+  let newFilename = "bookmarks-" + curDate.toLocaleFormat("%Y-%m-%d") + "_" +
+                  count + "_" + hash + ".json";
+  let backupFile = OS.Path.join(backupFolder, oldFilename);
+  let newBackupFile = OS.Path.join(backupFolder, newFilename);
+  yield OS.File.move(tempPath, backupFile);
+
+  // Ensure file has been renamed correctly
+  yield PlacesBackups.create();
+  let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+  Assert.equal(mostRecentBackupFile, newBackupFile);
+
+  // Cleanup
+  yield OS.File.remove(mostRecentBackupFile);
+  yield OS.File.remove(tempPath);
+});
--- a/toolkit/components/places/tests/bookmarks/xpcshell.ini
+++ b/toolkit/components/places/tests/bookmarks/xpcshell.ini
@@ -28,8 +28,9 @@ tail =
 [test_675416.js]
 [test_711914.js]
 [test_protectRoots.js]
 [test_818593-store-backup-metadata.js]
 [test_818584-discard-duplicate-backups.js]
 [test_818587_compress-bookmarks-backups.js]
 [test_992901-backup-unsorted-hierarchy.js]
 [test_997030-bookmarks-html-encode.js]
+[test_1016953-renaming-uncompressed.js]
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -2943,26 +2943,26 @@
   },
   "FENNEC_FAVICONS_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "2000",
     "n_buckets": 10,
     "cpp_guard": "ANDROID",
     "extended_statistics_ok": true,
-    "description": "FENNEC: (Places) Number of favicons stored"
+    "description": "Number of favicons stored in the browser DB"
   },
   "FENNEC_THUMBNAILS_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "2000",
     "n_buckets": 10,
     "cpp_guard": "ANDROID",
     "extended_statistics_ok": true,
-    "description": "FENNEC: (Places) Number of thumbnails stored"
+    "description": "Number of thumbnails stored in the browser DB"
   },
   "PLACES_SORTED_BOOKMARKS_PERC": {
     "expires_in_version": "never",
     "kind": "linear",
     "high": "100",
     "n_buckets": 10,
     "description": "PLACES: Percentage of bookmarks organized in folders"
   },
@@ -4276,46 +4276,71 @@
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "1000000",
     "n_buckets": 20,
     "extended_statistics_ok": true,
     "description": "Number of history entries in the original XUL places database",
     "cpp_guard": "ANDROID"
   },
-  "FENNEC_AWESOMEBAR_ALLPAGES_EMPTY_TIME": {
+  "FENNEC_GLOBALHISTORY_ADD_MS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 10,
     "high": "20000",
     "n_buckets": 20,
-    "description": "Fennec: Time for the Awesomebar Top Sites query to return with no filter set (ms)",
+    "description": "Time for a record to be added to history (ms)",
+    "cpp_guard": "ANDROID"
+  },
+  "FENNEC_GLOBALHISTORY_UPDATE_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 10,
+    "high": "20000",
+    "n_buckets": 20,
+    "description": "Time for a record to be updated in history (ms)",
+    "cpp_guard": "ANDROID"
+  },
+  "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 10,
+    "high": "20000",
+    "n_buckets": 20,
+    "description": "Time to update the visited link set (ms)",
     "cpp_guard": "ANDROID"
   },
   "FENNEC_LOWMEM_TAB_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 100,
     "n_buckets": 30,
     "description": "How many tabs were open when a low-memory event was received",
     "cpp_guard": "ANDROID"
   },
   "FENNEC_RESTORING_ACTIVITY": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "Fennec is starting up but the Gecko thread was still running",
     "cpp_guard": "ANDROID"
   },
-  "FENNEC_STARTUP_TIME_JAVAUI": {
-    "expires_in_version": "never",
-    "kind": "exponential",
-    "low": 100,
-    "high": "5000",
+  "FENNEC_SEARCH_LOADER_TIME_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 10,
+    "high": "20000",
     "n_buckets": 20,
-    "description": "Time for the Java UI to load (ms)",
+    "description": "Time for a URL bar DB search to return (ms)",
+    "cpp_guard": "ANDROID"
+  },
+  "FENNEC_STARTUP_GECKOAPP_ACTION": {
+    "expires_in_version": "never",
+    "kind": "enumerated",
+    "n_values": 4,
+    "description": "The way the GeckoApp was launched. (Normal, URL, Prefetch, Redirector)",
     "cpp_guard": "ANDROID"
   },
   "FENNEC_STARTUP_TIME_ABOUTHOME": {
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 100,
     "high": "10000",
     "n_buckets": 20,
@@ -4326,21 +4351,23 @@
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 500,
     "high": "20000",
     "n_buckets": 20,
     "description": "Time for the Gecko:Ready message to arrive (ms)",
     "cpp_guard": "ANDROID"
   },
-  "FENNEC_STARTUP_GECKOAPP_ACTION": {
-    "expires_in_version": "never",
-    "kind": "enumerated",
-    "n_values": 4,
-    "description": "The way the GeckoApp was launched. (Normal, URL, Prefetch, Redirector)",
+  "FENNEC_STARTUP_TIME_JAVAUI": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 100,
+    "high": "5000",
+    "n_buckets": 20,
+    "description": "Time for the Java UI to load (ms)",
     "cpp_guard": "ANDROID"
   },
   "FENNEC_TAB_EXPIRED": {
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 10,
     "high": 604800,
     "n_buckets": 20,
@@ -4353,16 +4380,25 @@
     "kind": "exponential",
     "low": 10,
     "high": 604800,
     "n_buckets": 20,
     "extended_statistics_ok": true,
     "description": "How long (in seconds) a tab was inactive when it was OOM-zombified",
     "cpp_guard": "ANDROID"
   },
+  "FENNEC_TOPSITES_LOADER_TIME_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 10,
+    "high": "20000",
+    "n_buckets": 20,
+    "description": "Time for the home screen Top Sites query to return with no filter set (ms)",
+    "cpp_guard": "ANDROID"
+  },
   "FENNEC_WAS_KILLED": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "Killed, likely due to an OOM condition",
     "cpp_guard": "ANDROID"
   },
   "SECURITY_UI": {
     "expires_in_version": "never",
--- a/toolkit/content/widgets/scrollbox.xml
+++ b/toolkit/content/widgets/scrollbox.xml
@@ -22,33 +22,40 @@
       </xul:box>
     </content>
   </binding>
 
   <binding id="arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
     <content>
       <xul:autorepeatbutton class="autorepeatbutton-up"
                             anonid="scrollbutton-up"
-                            collapsed="true"
-                            xbl:inherits="orient"
+                            xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
                             oncommand="_autorepeatbuttonScroll(event);"/>
+      <xul:spacer class="arrowscrollbox-overflow-start-indicator"
+                  xbl:inherits="collapsed=scrolledtostart"/>
       <xul:scrollbox class="arrowscrollbox-scrollbox"
                      anonid="scrollbox"
                      flex="1"
                      xbl:inherits="orient,align,pack,dir">
         <children/>
       </xul:scrollbox>
+      <xul:spacer class="arrowscrollbox-overflow-end-indicator"
+                  xbl:inherits="collapsed=scrolledtoend"/>
       <xul:autorepeatbutton class="autorepeatbutton-down"
                             anonid="scrollbutton-down"
-                            collapsed="true"
-                            xbl:inherits="orient"
+                            xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
                             oncommand="_autorepeatbuttonScroll(event);"/>
     </content>
 
     <implementation>
+      <constructor><![CDATA[
+        this.setAttribute("notoverflowing", "true");
+        this._updateScrollButtonsDisabledState();
+      ]]></constructor>
+
       <destructor><![CDATA[
         this._stopSmoothScroll();
       ]]></destructor>
 
       <field name="_scrollbox">
         document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
       </field>
       <field name="_scrollButtonUp">
@@ -449,38 +456,49 @@
             this._isScrolling = 0;
             this._scrollTarget = null;
           }
         ]]></body>
       </method>
 
       <method name="_updateScrollButtonsDisabledState">
         <body><![CDATA[
-          var disableUpButton = false;
-          var disableDownButton = false;
+          var scrolledToStart = false;
+          var scrolledToEnd = false;
 
-          if (this.scrollPosition == 0) {
+          if (this.hasAttribute("notoverflowing")) {
+            scrolledToStart = true;
+            scrolledToEnd = true;
+          }
+          else if (this.scrollPosition == 0) {
             // In the RTL case, this means the _last_ element in the
             // scrollbox is visible
             if (this._isRTLScrollbox) 
-              disableDownButton = true;
+              scrolledToEnd = true;
             else
-              disableUpButton = true;
+              scrolledToStart = true;
           }
           else if (this.scrollClientSize + this.scrollPosition == this.scrollSize) {
             // In the RTL case, this means the _first_ element in the
             // scrollbox is visible
             if (this._isRTLScrollbox)
-              disableUpButton = true;
+              scrolledToStart = true;
             else
-              disableDownButton = true;
+              scrolledToEnd = true;
           }
 
-          this._scrollButtonUp.disabled = disableUpButton;
-          this._scrollButtonDown.disabled = disableDownButton;
+          if (scrolledToEnd)
+            this.setAttribute("scrolledtoend", "true");
+          else
+            this.removeAttribute("scrolledtoend");
+
+          if (scrolledToStart)
+            this.setAttribute("scrolledtostart", "true");
+          else
+            this.removeAttribute("scrolledtostart");
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="DOMMouseScroll"><![CDATA[
         if (this.orient == "vertical") {
           // prevent horizontal scrolling from scrolling a vertical scrollbox
@@ -530,28 +548,27 @@
           if (event.detail == 1)
             return;
         }
         else {    // horizontal scrollbox
           if (event.detail == 0)
             return;
         }
 
-        this._scrollButtonUp.collapsed = true;
-        this._scrollButtonDown.collapsed = true;
+        this.setAttribute("notoverflowing", "true");
+
         try {
           // See bug 341047 and comments in overflow handler as to why 
           // try..catch is needed here
           let childNodes = this._getScrollableElements();
           if (childNodes && childNodes.length)
             this.ensureElementIsVisible(childNodes[0], false);
         }
         catch(e) {
-          this._scrollButtonUp.collapsed = false;
-          this._scrollButtonDown.collapsed = false;
+          this.removeAttribute("notoverflowing");
         }
       ]]></handler>
 
       <handler event="overflow" phase="capturing"><![CDATA[
         // filter underflow events which were dispatched on nested scrollboxes
         if (event.target != this)
           return;
 
@@ -564,61 +581,63 @@
           if (event.detail == 1)
             return;
         }
         else {    // horizontal scrollbox
           if (event.detail == 0)
             return;
         }
 
-        this._scrollButtonUp.collapsed = false;
-        this._scrollButtonDown.collapsed = false;
+        this.removeAttribute("notoverflowing");
+
         try {
           // See bug 341047, the overflow event is dispatched when the 
           // scrollbox already is mostly destroyed. This causes some code in
           // _updateScrollButtonsDisabledState() to throw an error. It also
-          // means that the scrollbarbuttons were uncollapsed when that should
-          // not be happening, because the whole overflow event should not be
-          // happening in that case.
+          // means that the notoverflowing attribute was removed erroneously,
+          // as the whole overflow event should not be happening in that case.
           this._updateScrollButtonsDisabledState();
         } 
         catch(e) {
-          this._scrollButtonUp.collapsed = true;
-          this._scrollButtonDown.collapsed = true;
+          this.setAttribute("notoverflowing", "true");
         }
       ]]></handler>
 
       <handler event="scroll" action="this._updateScrollButtonsDisabledState()"/>
     </handlers>
   </binding>
 
   <binding id="autorepeatbutton" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
     <content repeat="hover">
       <xul:image class="autorepeatbutton-icon"/>
     </content>
   </binding>
 
   <binding id="arrowscrollbox-clicktoscroll" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox">
     <content>
-      <xul:toolbarbutton class="scrollbutton-up" collapsed="true"
-                         xbl:inherits="orient"
+      <xul:toolbarbutton class="scrollbutton-up"
+                         xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
                          anonid="scrollbutton-up"
                          onclick="_distanceScroll(event);"
                          onmousedown="if (event.button == 0) _startScroll(-1);"
                          onmouseup="if (event.button == 0) _stopScroll();"
                          onmouseover="_continueScroll(-1);"
                          onmouseout="_pauseScroll();"/>
+      <xul:spacer class="arrowscrollbox-overflow-start-indicator"
+                  xbl:inherits="collapsed=scrolledtostart"/>
       <xul:scrollbox class="arrowscrollbox-scrollbox"
                      anonid="scrollbox"
                      flex="1"
                      xbl:inherits="orient,align,pack,dir">
         <children/>
       </xul:scrollbox>
-      <xul:toolbarbutton class="scrollbutton-down" collapsed="true"
-                         xbl:inherits="orient"
+      <xul:spacer class="arrowscrollbox-overflow-end-indicator"
+                  xbl:inherits="collapsed=scrolledtoend"/>
+      <xul:toolbarbutton class="scrollbutton-down"
+                         xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
                          anonid="scrollbutton-down"
                          onclick="_distanceScroll(event);"
                          onmousedown="if (event.button == 0) _startScroll(1);"
                          onmouseup="if (event.button == 0) _stopScroll();"
                          onmouseover="_continueScroll(1);"
                          onmouseout="_pauseScroll();"/>
     </content>
     <implementation implements="nsITimerCallback, nsIDOMEventListener">
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -285,16 +285,22 @@ exports.hasSafeGetter = function hasSafe
  * See bugs 945920 and 946752 for discussion.
  *
  * @type Object aObj
  *       The object to check.
  * @return Boolean
  *         True if it is safe to read properties from aObj, or false otherwise.
  */
 exports.isSafeJSObject = function isSafeJSObject(aObj) {
+  // If we are running on a worker thread, Cu is not available. In this case,
+  // we always return false, just to be on the safe side.
+  if (!Cu) {
+    return false;
+  }
+
   if (Cu.getGlobalForObject(aObj) ==
       Cu.getGlobalForObject(exports.isSafeJSObject)) {
     return true; // aObj is not a cross-compartment wrapper.
   }
 
   let principal = Cu.getObjectPrincipal(aObj);
   if (Services.scriptSecurityManager.isSystemPrincipal(principal)) {
     return true; // allow chrome objects
--- a/toolkit/devtools/event-emitter.js
+++ b/toolkit/devtools/event-emitter.js
@@ -1,29 +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/. */
 
 /**
  * EventEmitter.
  */
 
-this.EventEmitter = function EventEmitter() {};
+(function (factory) { // Module boilerplate
+  if (this.module && module.id.indexOf("event-emitter") >= 0) { // require
+    factory.call(this, require, exports, module);
+  } else { // Cu.import
+      const Cu = Components.utils;
+      const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+      this.promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+      factory.call(this, devtools.require, this, { exports: this });
+      this.EXPORTED_SYMBOLS = ["EventEmitter"];
+  }
+}).call(this, function (require, exports, module) {
 
-if (typeof(require) === "function") {
-   module.exports = EventEmitter;
-   var {Cu, components} = require("chrome");
-} else {
-  var EXPORTED_SYMBOLS = ["EventEmitter"];
-  var Cu = this["Components"].utils;
-  var components = Components;
-}
+this.EventEmitter = function EventEmitter() {};
+module.exports = EventEmitter;
 
-const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
-const { Services } = Cu.import("resource://gre/modules/Services.jsm");
+const { Cu, components } = require("chrome");
+const Services = require("Services");
 
 /**
  * Decorate an object with event emitter functionality.
  *
  * @param Object aObjectToDecorate
  *        Bind all public methods of EventEmitter to
  *        the aObjectToDecorate object.
  */
@@ -185,8 +189,10 @@ EventEmitter.prototype = {
 
       argOut += ")";
       out += "emit" + argOut + " from " + func + "() -> " + path + "\n";
 
       dump(out);
     }
   },
 };
+
+});
--- a/toolkit/devtools/server/actors/framerate.js
+++ b/toolkit/devtools/server/actors/framerate.js
@@ -49,28 +49,32 @@ let FramerateActor = exports.FramerateAc
     this._startTime = this._contentWin.performance.now();
     this._contentWin.requestAnimationFrame(this._onRefreshDriverTick);
   }, {
   }),
 
   /**
    * Stops monitoring framerate, returning the recorded values.
    */
-  stopRecording: method(function() {
+  stopRecording: method(function(beginAt = 0, endAt = Number.MAX_SAFE_INTEGER) {
     if (!this._recording) {
       return [];
     }
     this._recording = false;
 
     // We don't need to store the ticks array for future use, release it.
-    let ticks = this._ticks;
+    let ticks = this._ticks.filter(e => e >= beginAt && e <= endAt);
     this._ticks = null;
     return ticks;
   }, {
-    response: { timeline: RetVal("array:number") }
+    request: {
+      beginAt: Arg(0, "nullable:number"),
+      endAt: Arg(1, "nullable:number")
+    },
+    response: { ticks: RetVal("array:number") }
   }),
 
   /**
    * Function invoked along with the refresh driver.
    */
   _onRefreshDriverTick: function() {
     if (!this._recording) {
       return;
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -930,19 +930,22 @@ var WalkerActor = protocol.ActorClass({
     }
   },
 
   toString: function() {
     return "[WalkerActor " + this.actorID + "]";
   },
 
   destroy: function() {
-    this._hoveredNode = null;
+    this._destroyed = true;
+
     this.clearPseudoClassLocks();
     this._activePseudoClassLocks = null;
+
+    this._hoveredNode = null;
     this.rootDoc = null;
 
     this.reflowObserver.off("reflows", this._onReflows);
     this.reflowObserver = null;
     releaseLayoutChangesObserver(this.tabActor);
 
     events.emit(this, "destroyed");
     protocol.Actor.prototype.destroy.call(this);
@@ -1706,23 +1709,24 @@ var WalkerActor = protocol.ActorClass({
   _removePseudoClassLock: function(node, pseudo) {
     if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
       return false;
     }
     DOMUtils.removePseudoClassLock(node.rawNode, pseudo);
     if (!node.writePseudoClassLocks()) {
       this._activePseudoClassLocks.delete(node);
     }
+
     this._queuePseudoClassMutation(node);
     return true;
   },
 
   /**
-   * Clear all the pseudo-classes on a given node
-   * or all nodes.
+   * Clear all the pseudo-classes on a given node or all nodes.
+   * @param {NodeActor} node Optional node to clear pseudo-classes on
    */
   clearPseudoClassLocks: method(function(node) {
     if (node) {
       DOMUtils.clearPseudoClassLocks(node.rawNode);
       this._activePseudoClassLocks.delete(node);
       this._queuePseudoClassMutation(node);
     } else {
       for (let locked of this._activePseudoClassLocks) {
@@ -1924,17 +1928,17 @@ var WalkerActor = protocol.ActorClass({
       cleanup: Option(0)
     },
     response: {
       mutations: RetVal("array:dommutation")
     }
   }),
 
   queueMutation: function(mutation) {
-    if (!this.actorID) {
+    if (!this.actorID || this._destroyed) {
       // We've been destroyed, don't bother queueing this mutation.
       return;
     }
     // We only send the `new-mutations` notification once, until the client
     // fetches mutations with the `getMutations` packet.
     let needEvent = this._pendingMutations.length === 0;
 
     this._pendingMutations.push(mutation);
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -16,16 +16,17 @@ support-files =
 [test_Debugger.Source.prototype.introductionType.html]
 [test_Debugger.Source.prototype.element.html]
 [test_Debugger.Script.prototype.global.html]
 [test_connection-manager.html]
 [test_css-logic.html]
 [test_device.html]
 [test_framerate_01.html]
 [test_framerate_02.html]
+[test_framerate_03.html]
 [test_inspector-changeattrs.html]
 [test_inspector-changevalue.html]
 [test_inspector-hide.html]
 [test_inspector-insert.html]
 [test_inspector-mutations-attr.html]
 [test_inspector-mutations-childlist.html]
 [test_inspector-mutations-frameload.html]
 [test_inspector-mutations-value.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_framerate_03.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1023018 - Tests whether or not the framerate actor can handle time ranges.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Framerate actor test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+
+window.onload = function() {
+  var Cu = Components.utils;
+  var Cc = Components.classes;
+  var Ci = Components.interfaces;
+
+  Cu.import("resource://gre/modules/Services.jsm");
+
+  // Always log packets when running tests.
+  Services.prefs.setBoolPref("devtools.debugger.log", true);
+  SimpleTest.registerCleanupFunction(function() {
+    Services.prefs.clearUserPref("devtools.debugger.log");
+  });
+
+  Cu.import("resource://gre/modules/devtools/Loader.jsm");
+  Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+  Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+
+  SimpleTest.waitForExplicitFinish();
+
+  var {FramerateFront} = devtools.require("devtools/server/actors/framerate");
+  var START_TICK = 2000;
+  var STOP_TICK = 3000;
+  var TOTAL_TIME = 5000;
+
+  DebuggerServer.init(function () { return true; });
+  DebuggerServer.addBrowserActors();
+
+  var client = new DebuggerClient(DebuggerServer.connectPipe());
+  client.connect(function onConnect() {
+    client.listTabs(function onListTabs(aResponse) {
+      var form = aResponse.tabs[aResponse.selected];
+      var front = FramerateFront(client, form);
+
+      front.startRecording().then(() => {
+        window.setTimeout(() => {
+          front.stopRecording(START_TICK, STOP_TICK).then(rawData => {
+            onRecordingStopped(front, rawData);
+          });
+        }, TOTAL_TIME);
+      });
+    });
+  });
+
+  function onRecordingStopped(front, rawData) {
+    ok(rawData, "There should be a recording available.");
+
+    ok(!rawData.find(e => e < START_TICK),
+      "There should be no tick before 2000ms.");
+    ok(!rawData.find(e => e > STOP_TICK),
+      "There should be no tick after 3000ms.");
+
+    for (var tick of rawData) {
+      info("Testing tick: " + tick);
+      is(typeof tick, "number", "All values should be numbers.");
+    }
+
+    client.close(() => {
+      DebuggerServer.destroy();
+      SimpleTest.finish()
+    });
+  }
+}
+</script>
+</pre>
+</body>
+</html>
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-01.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-01.js
@@ -1,31 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     do_check_eq(args[0].class, "Object");
@@ -33,17 +42,17 @@ function test_object_grip()
     let objClient = gThreadClient.pauseGrip(args[0]);
     objClient.getOwnPropertyNames(function(aResponse) {
       do_check_eq(aResponse.ownPropertyNames.length, 3);
       do_check_eq(aResponse.ownPropertyNames[0], "a");
       do_check_eq(aResponse.ownPropertyNames[1], "b");
       do_check_eq(aResponse.ownPropertyNames[2], "c");
 
       gThreadClient.resume(function() {
-        finishClient(gClient);
+        gClient.close(gCallback);
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ a: 1, b: true, c: 'foo' })");
 }
 
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-02.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-02.js
@@ -1,31 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     do_check_eq(args[0].class, "Object");
@@ -36,17 +45,17 @@ function test_object_grip()
 
       let protoClient = gThreadClient.pauseGrip(aResponse.prototype);
       protoClient.getOwnPropertyNames(function(aResponse) {
         do_check_eq(aResponse.ownPropertyNames.length, 2);
         do_check_eq(aResponse.ownPropertyNames[0], "b");
         do_check_eq(aResponse.ownPropertyNames[1], "c");
 
         gThreadClient.resume(function() {
-          finishClient(gClient);
+          gClient.close(gCallback);
         });
       });
     });
 
   });
 
   gDebuggee.eval(function Constr() {
     this.a = 1;
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-03.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-03.js
@@ -1,31 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     do_check_eq(args[0].class, "Object");
@@ -46,17 +55,17 @@ function test_object_grip()
         objClient.getProperty("a", function(aResponse) {
           do_check_eq(aResponse.descriptor.configurable, true);
           do_check_eq(aResponse.descriptor.enumerable, true);
           do_check_eq(aResponse.descriptor.get.type, "object");
           do_check_eq(aResponse.descriptor.get.class, "Function");
           do_check_eq(aResponse.descriptor.set.type, "undefined");
 
           gThreadClient.resume(function() {
-            finishClient(gClient);
+            gClient.close(gCallback);
           });
         });
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })");
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-04.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-04.js
@@ -1,31 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     do_check_eq(args[0].class, "Object");
@@ -50,17 +59,17 @@ function test_object_grip()
 
       do_check_true(aResponse.prototype != undefined);
 
       let protoClient = gThreadClient.pauseGrip(aResponse.prototype);
       protoClient.getOwnPropertyNames(function(aResponse) {
         do_check_true(aResponse.ownPropertyNames.toString != undefined);
 
         gThreadClient.resume(function() {
-          finishClient(gClient);
+          gClient.close(gCallback);
         });
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })");
 }
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-05.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-05.js
@@ -4,33 +4,42 @@
 /**
  * This test checks that frozen objects report themselves as frozen in their
  * grip.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1, arg2) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let obj1 = aPacket.frame.arguments[0];
     do_check_true(obj1.frozen);
 
@@ -39,17 +48,17 @@ function test_object_grip()
 
     let obj2 = aPacket.frame.arguments[1];
     do_check_false(obj2.frozen);
 
     let obj2Client = gThreadClient.pauseGrip(obj2);
     do_check_false(obj2Client.isFrozen);
 
     gThreadClient.resume(_ => {
-      finishClient(gClient);
+      gClient.close(gCallback);
     });
   });
 
   gDebuggee.eval("(" + function () {
     let obj1 = {};
     Object.freeze(obj1);
     stopMe(obj1, {});
   } + "())");
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-06.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-06.js
@@ -4,33 +4,42 @@
 /**
  * This test checks that sealed objects report themselves as sealed in their
  * grip.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1, arg2) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let obj1 = aPacket.frame.arguments[0];
     do_check_true(obj1.sealed);
 
@@ -39,17 +48,17 @@ function test_object_grip()
 
     let obj2 = aPacket.frame.arguments[1];
     do_check_false(obj2.sealed);
 
     let obj2Client = gThreadClient.pauseGrip(obj2);
     do_check_false(obj2Client.isSealed);
 
     gThreadClient.resume(_ => {
-      finishClient(gClient);
+      gClient.close(gCallback);
     });
   });
 
   gDebuggee.eval("(" + function () {
     let obj1 = {};
     Object.seal(obj1);
     stopMe(obj1, {});
   } + "())");
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-07.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-07.js
@@ -4,33 +4,42 @@
 /**
  * This test checks that objects which are not extensible report themselves as
  * such.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1, arg2, arg3, arg4) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let [f, s, ne, e] = aPacket.frame.arguments;
     let [fClient, sClient, neClient, eClient] = aPacket.frame.arguments.map(
       a => gThreadClient.pauseGrip(a));
@@ -43,17 +52,17 @@ function test_object_grip()
 
     do_check_false(ne.extensible);
     do_check_false(neClient.isExtensible);
 
     do_check_true(e.extensible);
     do_check_true(eClient.isExtensible);
 
     gThreadClient.resume(_ => {
-      finishClient(gClient);
+      gClient.close(gCallback);
     });
   });
 
   gDebuggee.eval("(" + function () {
     let f = {};
     Object.freeze(f);
     let s = {};
     Object.seal(s);
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-08.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-08.js
@@ -1,31 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     do_check_eq(args[0].class, "Object");
@@ -48,16 +57,16 @@ function test_object_grip()
       do_check_eq(aResponse.ownProperties.c.value.type, "NaN");
 
       do_check_eq(aResponse.ownProperties.d.configurable, true);
       do_check_eq(aResponse.ownProperties.d.enumerable, true);
       do_check_eq(aResponse.ownProperties.d.writable, true);
       do_check_eq(aResponse.ownProperties.d.value.type, "-0");
 
       gThreadClient.resume(function() {
-        finishClient(gClient);
+        gClient.close(gCallback);
       });
     });
   });
 
   gDebuggee.eval("stopMe({ a: Infinity, b: -Infinity, c: NaN, d: -0 })");
 }
 
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-09.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-09.js
@@ -3,33 +3,42 @@
 /**
  * This tests exercises getProtypesAndProperties message accepted
  * by a thread actor.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1, arg2) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     gThreadClient.getPrototypesAndProperties([args[0].actor, args[1].actor], function(aResponse) {
@@ -49,17 +58,17 @@ function test_object_grip()
       do_check_eq(obj2.ownProperties.z.enumerable, true);
       do_check_eq(obj2.ownProperties.z.writable, true);
       do_check_eq(obj2.ownProperties.z.value, 123);
 
       do_check_true(obj1.prototype != undefined);
       do_check_true(obj2.prototype != undefined);
 
       gThreadClient.resume(function() {
-        finishClient(gClient);
+        gClient.close(gCallback);
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ x: 10, y: 'kaiju'}, { z: 123 })");
 }
 
--- a/toolkit/devtools/server/tests/unit/test_stepping-01.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-01.js
@@ -3,29 +3,38 @@
 
 /**
  * Check basic step-over functionality.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_simple_stepping();
     });
   });
-  do_test_pending();
 }
 
 function test_simple_stepping()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check the return value.
       do_check_eq(aPacket.type, "paused");
@@ -50,17 +59,17 @@ function test_simple_stepping()
           // When leaving a stack frame the line number doesn't change.
           do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3);
           do_check_eq(aPacket.why.type, "resumeLimit");
           // Check that stepping worked.
           do_check_eq(gDebuggee.a, 1);
           do_check_eq(gDebuggee.b, 2);
 
           gThreadClient.resume(function () {
-            finishClient(gClient);
+            gClient.close(gCallback);
           });
         });
         gThreadClient.stepOver();
       });
       gThreadClient.stepOver();
 
     });
     gThreadClient.stepOver();
--- a/toolkit/devtools/server/tests/unit/test_stepping-02.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-02.js
@@ -3,29 +3,38 @@
 
 /**
  * Check basic step-in functionality.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_simple_stepping();
     });
   });
-  do_test_pending();
 }
 
 function test_simple_stepping()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check the return value.
       do_check_eq(aPacket.type, "paused");
@@ -50,17 +59,17 @@ function test_simple_stepping()
           // When leaving a stack frame the line number doesn't change.
           do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3);
           do_check_eq(aPacket.why.type, "resumeLimit");
           // Check that stepping worked.
           do_check_eq(gDebuggee.a, 1);
           do_check_eq(gDebuggee.b, 2);
 
           gThreadClient.resume(function () {
-            finishClient(gClient);
+            gClient.close(gCallback);
           });
         });
         gThreadClient.stepIn();
       });
       gThreadClient.stepIn();
 
     });
     gThreadClient.stepIn();
--- a/toolkit/devtools/server/tests/unit/test_stepping-03.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-03.js
@@ -3,45 +3,54 @@
 
 /**
  * Check basic step-out functionality.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_simple_stepping();
     });
   });
-  do_test_pending();
 }
 
 function test_simple_stepping()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check the return value.
       do_check_eq(aPacket.type, "paused");
       do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 4);
       do_check_eq(aPacket.why.type, "resumeLimit");
       // Check that stepping worked.
       do_check_eq(gDebuggee.a, 1);
       do_check_eq(gDebuggee.b, 2);
 
       gThreadClient.resume(function () {
-        finishClient(gClient);
+        gClient.close(gCallback);
       });
     });
     gThreadClient.stepOut();
 
   });
 
   gDebuggee.eval("var line0 = Error().lineNumber;\n" +
                  "function f() {\n" + // line0 + 1
--- a/toolkit/devtools/server/tests/unit/test_stepping-04.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-04.js
@@ -3,29 +3,38 @@
 
 /**
  * Check that stepping over a function call does not pause inside the function.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_simple_stepping();
     });
   });
-  do_test_pending();
 }
 
 function test_simple_stepping()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check the return value.
       do_check_eq(aPacket.type, "paused");
@@ -40,17 +49,17 @@ function test_simple_stepping()
         do_check_eq(aPacket.type, "paused");
         do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 6);
         do_check_eq(aPacket.why.type, "resumeLimit");
         // Check that stepping worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         gThreadClient.resume(function () {
-          finishClient(gClient);
+          gClient.close(gCallback);
         });
       });
       gThreadClient.stepOver();
 
     });
     gThreadClient.stepOver();
 
   });
--- a/toolkit/devtools/server/tests/unit/test_stepping-05.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-05.js
@@ -5,29 +5,38 @@
  * Make sure that stepping in the last statement of the last frame doesn't
  * cause an unexpected pause, when another JS frame is pushed on the stack
  * (bug 785689).
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_stepping_last();
     });
   });
-  do_test_pending();
 }
 
 function test_stepping_last()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check the return value.
       do_check_eq(aPacket.type, "paused");
@@ -79,14 +88,14 @@ function test_next_pause()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     // Check the return value.
     do_check_eq(aPacket.type, "paused");
     // Before fixing bug 785689, the type was resumeLimit.
     do_check_eq(aPacket.why.type, "debuggerStatement");
 
     gThreadClient.resume(function () {
-      finishClient(gClient);
+      gClient.close(gCallback);
     });
   });
 
   gDebuggee.eval("debugger;");
 }
--- a/toolkit/devtools/server/tests/unit/test_stepping-06.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-06.js
@@ -3,34 +3,43 @@
 
 /**
  * Check that stepping out of a function returns the right return value.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       // XXX: We have to do an executeSoon so that the error isn't caught and
       // reported by DebuggerClient.requester (because we are using the local
       // transport and share a stack) which causes the test to fail.
       Services.tm.mainThread.dispatch({
         run: test_simple_stepping
       }, Ci.nsIThread.DISPATCH_NORMAL);
     });
   });
-  do_test_pending();
 }
 
 function test_simple_stepping()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check that the return value is 10.
       do_check_eq(aPacket.type, "paused");
@@ -50,17 +59,17 @@ function test_simple_stepping()
             gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
               // Check that the exception was thrown.
               do_check_eq(aPacket.type, "paused");
               do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 12);
               do_check_eq(aPacket.why.type, "resumeLimit");
               do_check_eq(aPacket.why.frameFinished.throw, "ah");
 
               gThreadClient.resume(function () {
-                finishClient(gClient);
+                gClient.close(gCallback);
               });
             });
             gThreadClient.stepOut();
           });
           gThreadClient.resume();
         });
         gThreadClient.stepOut();
       });
--- a/toolkit/devtools/transport/transport.js
+++ b/toolkit/devtools/transport/transport.js
@@ -255,18 +255,17 @@ DebuggerTransport.prototype = {
    */
   onOutputStreamReady: DevToolsUtils.makeInfallible(function(stream) {
     if (this._outgoing.length === 0) {
       return;
     }
 
     try {
       this._currentOutgoing.write(stream);
-    } catch(e if e.result == Cr.NS_BASE_STREAM_CLOSED ||
-                 e.result == Cr.NS_ERROR_NET_RESET) {
+    } catch(e if e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
       this.close(e.result);
       return;
     }
 
     this._flushOutgoing();
   }, "DebuggerTransport.prototype.onOutputStreamReady"),
 
   /**
@@ -333,19 +332,17 @@ DebuggerTransport.prototype = {
    * Called when the stream is either readable or closed.
    */
   onInputStreamReady:
   DevToolsUtils.makeInfallible(function(stream) {
     try {
       while(stream.available() && this._incomingEnabled &&
             this._processIncoming(stream, stream.available())) {}
       this._waitForIncoming();
-    } catch(e if e.result == Cr.NS_BASE_STREAM_CLOSED ||
-                 e.result == Cr.NS_ERROR_CONNECTION_REFUSED ||
-                 e.result == Cr.NS_ERROR_OFFLINE) {
+    } catch(e if e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
       this.close(e.result);
     }
   }, "DebuggerTransport.prototype.onInputStreamReady"),
 
   /**
    * Process the incoming data.  Will create a new currently incoming Packet if
    * needed.  Tells the incoming Packet to read as much data as it can, but
    * reading may not complete.  The Packet signals that its data is ready for
--- a/toolkit/devtools/worker-loader.js
+++ b/toolkit/devtools/worker-loader.js
@@ -89,28 +89,16 @@ function createModule(id) {
       configurable: false,
       enumerable: true,
       value: Object.create(null),
       writable: true
     }
   });
 };
 
-// A whitelist of modules from which the built-in chrome module may be
-// required. The idea is add all modules that depend on chrome to the whitelist
-// initially, and then remove them one by one, fixing any errors as we go along.
-// Once the whitelist is empty, we can remove the built-in chrome module from
-// the loader entirely.
-//
-// TODO: Remove this when the whitelist becomes empty
-let chromeWhitelist = [
-  "devtools/toolkit/DevToolsUtils",
-  "devtools/toolkit/event-emitter",
-];
-
 // Create a CommonJS loader with the following options:
 // - createSandbox:
 //     A function that will be used to create sandboxes. It takes the name and
 //     prototype of the sandbox to be created, and should return the newly
 //     created sandbox as result. This option is mandatory.
 // - globals:
 //     A map of built-in globals that will be exposed to every module. Defaults
 //     to the empty map.
@@ -183,27 +171,16 @@ function WorkerDebuggerLoader(options) {
   // create a require function for top-level modules instead.
   function createRequire(requirer) {
     return function require(id) {
       // Make sure an id was passed.
       if (id === undefined) {
         throw new Error("can't require module without id!");
       }
 
-      // If the module to be required is the built-in chrome module, and the
-      // requirer is not in the whitelist, return a vacuous object as if the
-      // module was unavailable.
-      //
-      // TODO: Remove this when the whitelist becomes empty
-      if (id === "chrome" && chromeWhitelist.indexOf(requirer.id) < 0) {
-        return { CC: undefined, Cc: undefined,
-                 ChromeWorker: undefined, Cm: undefined, Ci: undefined, Cu: undefined,
-                 Cr: undefined, components: undefined };
-      }
-
       // Built-in modules are cached by id rather than URL, so try to find the
       // module to be required by id first.
       let module = modules[id];
       if (module === undefined) {
         // Failed to find the module to be required by id, so convert the id to
         // a URL and try again.
 
         // If the id is relative, resolve it to an absolute id.
@@ -327,19 +304,19 @@ if (typeof Components === "object") {
 
     // TODO: Either replace these built-in modules with vacuous objects, or
     // provide them in a way that does not depend on the use of Components.
     const { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});;
     const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});;
     let SourceMap = {};
     Cu.import("resource://gre/modules/devtools/SourceMap.jsm", SourceMap);
     const Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
-    const chrome = { CC: Function.bind.call(CC, Components), Cc: Cc,
-                     ChromeWorker: ChromeWorker, Cm: Cm, Ci: Ci, Cu: Cu,
-                     Cr: Cr, components: Components };
+    const chrome = { CC: undefined, Cc: undefined, ChromeWorker: undefined,
+                     Cm: undefined, Ci: undefined, Cu: undefined,
+                     Cr: undefined, components: undefined };
     const xpcInspector = Cc["@mozilla.org/jsinspector;1"].
                          getService(Ci.nsIJSInspector);
 
     this.worker = new WorkerDebuggerLoader({
       createSandbox: createSandbox,
       globals: {
         "promise": Promise,
         "reportError": Cu.reportError,
--- a/toolkit/modules/NewTabUtils.jsm
+++ b/toolkit/modules/NewTabUtils.jsm
@@ -912,68 +912,65 @@ let Links = {
     let links = this._providerLinks.get(aProvider);
     if (!links)
       // This is not an error, it just means that between the time the provider
       // was added and the future time we call getLinks on it, it notified us of
       // a change.
       return;
 
     let { sortedLinks, linkMap } = links;
-
-    // Nothing to do if the list is full and the link isn't in it and shouldn't
-    // be in it.
-    if (!linkMap.has(aLink.url) &&
-        sortedLinks.length &&
-        sortedLinks.length == aProvider.maxNumLinks) {
-      let lastLink = sortedLinks[sortedLinks.length - 1];
-      if (this.compareLinks(lastLink, aLink) < 0)
-        return;
-    }
-
+    let existingLink = linkMap.get(aLink.url);
+    let insertionLink = null;
     let updatePages = false;
 
-    // Update the title in O(1).
-    if ("title" in aLink) {
-      let link = linkMap.get(aLink.url);
-      if (link && link.title != aLink.title) {
-        link.title = aLink.title;
+    if (existingLink) {
+      // Update our copy's position in O(lg n) by first removing it from its
+      // list.  It's important to do this before modifying its properties.
+      if (this._sortProperties.some(prop => prop in aLink)) {
+        let idx = this._indexOf(sortedLinks, existingLink);
+        if (idx < 0) {
+          throw new Error("Link should be in _sortedLinks if in _linkMap");
+        }
+        sortedLinks.splice(idx, 1);
+        // Update our copy's properties.
+        for (let prop of this._sortProperties) {
+          if (prop in aLink) {
+            existingLink[prop] = aLink[prop];
+          }
+        }
+        // Finally, reinsert our copy below.
+        insertionLink = existingLink;
+      }
+      // Update our copy's title in O(1).
+      if ("title" in aLink && aLink.title != existingLink.title) {
+        existingLink.title = aLink.title;
         updatePages = true;
       }
     }
-
-    // Update the link's position in O(lg n).
-    if (this._sortProperties.some((prop) => prop in aLink)) {
-      let link = linkMap.get(aLink.url);
-      if (link) {
-        // The link is already in the list.
-        let idx = this._indexOf(sortedLinks, link);
-        if (idx < 0)
-          throw new Error("Link should be in _sortedLinks if in _linkMap");
-        sortedLinks.splice(idx, 1);
-        for (let prop of this._sortProperties) {
-          if (prop in aLink)
-            link[prop] = aLink[prop];
+    else if (this._sortProperties.every(prop => prop in aLink)) {
+      // Before doing the O(lg n) insertion below, do an O(1) check for the
+      // common case where the new link is too low-ranked to be in the list.
+      if (sortedLinks.length && sortedLinks.length == aProvider.maxNumLinks) {
+        let lastLink = sortedLinks[sortedLinks.length - 1];
+        if (this.compareLinks(lastLink, aLink) < 0) {
+          return;
         }
       }
-      else {
-        // The link is new.
-        for (let prop of this._sortProperties) {
-          if (!(prop in aLink))
-            throw new Error("New link missing required sort property: " + prop);
-        }
-        // Copy the link object so that if the caller changes it, it doesn't
-        // screw up our bookkeeping.
-        link = {};
-        for (let [prop, val] of Iterator(aLink)) {
-          link[prop] = val;
-        }
-        linkMap.set(link.url, link);
+      // Copy the link object so that changes later made to it by the caller
+      // don't affect our copy.
+      insertionLink = {};
+      for (let prop in aLink) {
+        insertionLink[prop] = aLink[prop];
       }
-      let idx = this._insertionIndexOf(sortedLinks, link);
-      sortedLinks.splice(idx, 0, link);
+      linkMap.set(aLink.url, insertionLink);
+    }
+
+    if (insertionLink) {
+      let idx = this._insertionIndexOf(sortedLinks, insertionLink);
+      sortedLinks.splice(idx, 0, insertionLink);
       if (sortedLinks.length > aProvider.maxNumLinks) {
         let lastLink = sortedLinks.pop();
         linkMap.delete(lastLink.url);
       }
       updatePages = true;
     }
 
     if (updatePages)
--- a/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
+++ b/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
@@ -46,22 +46,17 @@ add_test(function changeLinks() {
   NewTabUtils.links.addProvider(provider);
 
   // This is sync since the provider's getLinks is sync.
   NewTabUtils.links.populateCache(function () {}, false);
 
   do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
 
   // Notify of a new link.
-  let newLink = {
-    url: "http://example.com/19",
-    title: "My frecency is 19",
-    frecency: 19,
-    lastVisitDate: 0,
-  };
+  let newLink = makeLink(19);
   expectedLinks.splice(1, 0, newLink);
   provider.notifyLinkChanged(newLink);
   do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
 
   // Notify of a link that's changed sort criteria.
   newLink.frecency = 17;
   expectedLinks.splice(1, 1);
   expectedLinks.splice(2, 0, newLink);
@@ -76,21 +71,17 @@ add_test(function changeLinks() {
   provider.notifyLinkChanged({
     url: newLink.url,
     title: newLink.title,
   });
   do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
 
   // Notify of a new link again, but this time make it overflow maxNumLinks.
   provider.maxNumLinks = expectedLinks.length;
-  newLink = {
-    url: "http://example.com/21",
-    frecency: 21,
-    lastVisitDate: 0,
-  };
+  newLink = makeLink(21);
   expectedLinks.unshift(newLink);
   expectedLinks.pop();
   do_check_eq(expectedLinks.length, provider.maxNumLinks); // Sanity check.
   provider.notifyLinkChanged(newLink);
   do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
 
   // Notify of many links changed.
   expectedLinks = makeLinks(0, 3, 1);
@@ -120,16 +111,44 @@ add_task(function oneProviderAlreadyCach
 
   NewTabUtils.links.populateCache(function () {}, false);
   do_check_links(NewTabUtils.links.getLinks(), links2.concat(links1));
 
   NewTabUtils.links.removeProvider(provider1);
   NewTabUtils.links.removeProvider(provider2);
 });
 
+add_task(function newLowRankedLink() {
+  // Init a provider with 10 links and make its maximum number also 10.
+  let links = makeLinks(0, 10, 1);
+  let provider = new TestProvider(done => done(links));
+  provider.maxNumLinks = links.length;
+
+  NewTabUtils.initWithoutProviders();
+  NewTabUtils.links.addProvider(provider);
+
+  // This is sync since the provider's getLinks is sync.
+  NewTabUtils.links.populateCache(function () {}, false);
+  do_check_links(NewTabUtils.links.getLinks(), links);
+
+  // Notify of a new link that's low-ranked enough not to make the list.
+  let newLink = makeLink(0);
+  provider.notifyLinkChanged(newLink);
+  do_check_links(NewTabUtils.links.getLinks(), links);
+
+  // Notify about the new link's title change.
+  provider.notifyLinkChanged({
+    url: newLink.url,
+    title: "a new title",
+  });
+  do_check_links(NewTabUtils.links.getLinks(), links);
+
+  NewTabUtils.links.removeProvider(provider);
+});
+
 function TestProvider(getLinksFn) {
   this.getLinks = getLinksFn;
   this._observers = new Set();
 }
 
 TestProvider.prototype = {
   addObserver: function (observer) {
     this._observers.add(observer);
@@ -160,17 +179,21 @@ function do_check_links(actualLinks, exp
     do_check_eq(actual.lastVisitDate, expected.lastVisitDate);
   }
 }
 
 function makeLinks(frecRangeStart, frecRangeEnd, step) {
   let links = [];
   // Remember, links are ordered by frecency descending.
   for (let i = frecRangeEnd; i > frecRangeStart; i -= step) {
-    links.push({
-      url: "http://example.com/" + i,
-      title: "My frecency is " + i,
-      frecency: i,
-      lastVisitDate: 0,
-    });
+    links.push(makeLink(i));
   }
   return links;
 }
+
+function makeLink(frecency) {
+  return {
+    url: "http://example.com/" + frecency,
+    title: "My frecency is " + frecency,
+    frecency: frecency,
+    lastVisitDate: 0,
+  };
+}