Merge f-t to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Mon, 19 Jan 2015 18:38:17 -0800
changeset 224576 c1c6840d92554fa49c020db174018cfdb7132dc2
parent 224566 2643429ae5ac40147fd72bc4b2ad6bf1914985cf (current diff)
parent 224575 121f46041abac01c14e099947e1557e11e7b2242 (diff)
child 224595 fea41421e7562a152306acab80030a7d52231563
child 224615 e460a19bec4d94997d066b0636eec76d95b5adef
child 224631 aae8346275344aebcca2afb13342a5caff7b5b64
push id28136
push userphilringnalda@gmail.com
push dateTue, 20 Jan 2015 02:39:27 +0000
treeherdermozilla-central@c1c6840d9255 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge f-t to m-c, a=merge
--- a/browser/devtools/debugger/test/browser_dbg_source-maps-04.js
+++ b/browser/devtools/debugger/test/browser_dbg_source-maps-04.js
@@ -110,26 +110,30 @@ function reloadPage() {
 function testHitBreakpoint() {
   let deferred = promise.defer();
 
   gDebugger.gThreadClient.resume(aResponse => {
     ok(!aResponse.error, "Shouldn't get an error resuming.");
     is(aResponse.type, "resumed", "Type should be 'resumed'.");
 
     waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => {
-      is(gFrames.itemCount, 1, "Should have one frame.");
+      is(gFrames.itemCount, 2, "Should have two frames.");
 
       // This is weird, but we need to let the debugger a chance to
       // update first
       executeSoon(() => {
         gDebugger.gThreadClient.resume(() => {
-          // We also need to make sure the next step doesn't add a
-          // "resumed" handler until this is completely finished
-          executeSoon(() => {
-            deferred.resolve();
+          gDebugger.gThreadClient.addOneTimeListener("paused", () => {
+            gDebugger.gThreadClient.resume(() => {
+              // We also need to make sure the next step doesn't add a
+              // "resumed" handler until this is completely finished
+              executeSoon(() => {
+                deferred.resolve();
+              });
+            });
           });
         });
       });
     });
   });
 
   return deferred.promise;
 }
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -44,17 +44,16 @@ const GRAPH_REGION_LINE_COLOR = "rgba(23
 const GRAPH_STRIPE_PATTERN_WIDTH = 16; // px
 const GRAPH_STRIPE_PATTERN_HEIGHT = 16; // px
 const GRAPH_STRIPE_PATTERN_LINE_WIDTH = 2; // px
 const GRAPH_STRIPE_PATTERN_LINE_SPACING = 4; // px
 
 // Line graph constants.
 
 const LINE_GRAPH_DAMPEN_VALUES = 0.85;
-const LINE_GRAPH_MIN_SQUARED_DISTANCE_BETWEEN_POINTS = 1; // px
 const LINE_GRAPH_TOOLTIP_SAFE_BOUNDS = 8; // px
 const LINE_GRAPH_MIN_MAX_TOOLTIP_DISTANCE = 14; // px
 
 const LINE_GRAPH_BACKGROUND_COLOR = "#0088cc";
 const LINE_GRAPH_STROKE_WIDTH = 1; // px
 const LINE_GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
 const LINE_GRAPH_HELPER_LINES_DASH = [5]; // px
 const LINE_GRAPH_HELPER_LINES_WIDTH = 1; // px
@@ -1228,22 +1227,16 @@ LineGraphWidget.prototype = Heritage.ext
 
   /**
    * The scalar used to multiply the graph values to leave some headroom
    * on the top.
    */
   dampenValuesFactor: LINE_GRAPH_DAMPEN_VALUES,
 
   /**
-   * Points that are too close too each other in the graph will not be rendered.
-   * This scalar specifies the required minimum squared distance between points.
-   */
-  minSquaredDistanceBetweenPoints: LINE_GRAPH_MIN_SQUARED_DISTANCE_BETWEEN_POINTS,
-
-  /**
    * Specifies if min/max/avg tooltips have arrow handlers on their sides.
    */
   withTooltipArrows: true,
 
   /**
    * Specifies if min/max/avg tooltips are positioned based on the actual
    * values, or just placed next to the graph corners.
    */
@@ -1261,21 +1254,16 @@ LineGraphWidget.prototype = Heritage.ext
    * @param number interval
    *        The maximum amount of time to wait between calculations.
    */
   setDataFromTimestamps: Task.async(function*(timestamps, interval) {
     let {
       plottedData,
       plottedMinMaxSum
     } = yield CanvasGraphUtils._performTaskInWorker("plotTimestampsGraph", {
-      width: this._width,
-      height: this._height,
-      dataOffsetX: this.dataOffsetX,
-      dampenValuesFactor: this.dampenValuesFactor,
-      minSquaredDistanceBetweenPoints: this.minSquaredDistanceBetweenPoints,
       timestamps: timestamps,
       interval: interval
     });
 
     this._tempMinMaxSum = plottedMinMaxSum;
     this.setData(plottedData);
   }),
 
@@ -1289,26 +1277,21 @@ LineGraphWidget.prototype = Heritage.ext
     let height = this._height;
 
     let totalTicks = this._data.length;
     let firstTick = totalTicks ? this._data[0].delta : 0;
     let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;
     let maxValue = Number.MIN_SAFE_INTEGER;
     let minValue = Number.MAX_SAFE_INTEGER;
     let avgValue = 0;
-    let forceDrawAllPoints = false;
 
     if (this._tempMinMaxSum) {
       maxValue = this._tempMinMaxSum.maxValue;
       minValue = this._tempMinMaxSum.minValue;
       avgValue = this._tempMinMaxSum.avgValue;
-      // If we use cached `minValue`, `maxValue`, `avgValue` then we can assume
-      // that we've already removed points that did not meet the
-      // `minSquaredDistanceBetweenPoints` requirement.
-      forceDrawAllPoints = true;
     } else {
       let sumValues = 0;
       for (let { delta, value } of this._data) {
         maxValue = Math.max(value, maxValue);
         minValue = Math.min(value, minValue);
         sumValues += value;
       }
       avgValue = sumValues / totalTicks;
@@ -1327,34 +1310,26 @@ LineGraphWidget.prototype = Heritage.ext
     let gradient = ctx.createLinearGradient(0, height / 2, 0, height);
     gradient.addColorStop(0, this.backgroundGradientStart);
     gradient.addColorStop(1, this.backgroundGradientEnd);
     ctx.fillStyle = gradient;
     ctx.strokeStyle = this.strokeColor;
     ctx.lineWidth = this.strokeWidth * this._pixelRatio;
     ctx.beginPath();
 
-    let prevX = 0;
-    let prevY = 0;
-    let minSqDist = this.minSquaredDistanceBetweenPoints;
-
     for (let { delta, value } of this._data) {
       let currX = (delta - this.dataOffsetX) * dataScaleX;
       let currY = height - value * dataScaleY;
 
       if (delta == firstTick) {
         ctx.moveTo(-LINE_GRAPH_STROKE_WIDTH, height);
         ctx.lineTo(-LINE_GRAPH_STROKE_WIDTH, currY);
       }
 
-      if (forceDrawAllPoints || distSquared(prevX, prevY, currX, currY) >= minSqDist) {
-        ctx.lineTo(currX, currY);
-        prevX = currX;
-        prevY = currY;
-      }
+      ctx.lineTo(currX, currY);
 
       if (delta == lastTick) {
         ctx.lineTo(width + LINE_GRAPH_STROKE_WIDTH, currY);
         ctx.lineTo(width + LINE_GRAPH_STROKE_WIDTH, height);
       }
     }
 
     ctx.fill();
--- a/browser/devtools/shared/widgets/GraphsWorker.js
+++ b/browser/devtools/shared/widgets/GraphsWorker.js
@@ -14,27 +14,24 @@ self.onmessage = e => {
       self.postMessage({ id, error: e.message + "\n" + e.stack });
       break;
   }
 };
 
 /**
  * @see LineGraphWidget.prototype.setDataFromTimestamps in Graphs.jsm
  * @param number id
- * @param number width
- * @param number height
  * @param array timestamps
  * @param number interval
  */
 function plotTimestampsGraph(id, args) {
   let plottedData = plotTimestamps(args.timestamps, args.interval);
   let plottedMinMaxSum = getMinMaxSum(plottedData);
-  let sparsifiedData = sparsifyLineData(plottedData, plottedMinMaxSum, args);
 
-  let response = { id, plottedData: sparsifiedData, plottedMinMaxSum };
+  let response = { id, plottedData, plottedMinMaxSum };
   self.postMessage(response);
 }
 
 /**
  * Gets the min, max and average of the values in an array.
  * @param array source
  * @return object
  */
@@ -51,58 +48,16 @@ function getMinMaxSum(source) {
     sumValues += value;
   }
   avgValue = sumValues / totalTicks;
 
   return { minValue, maxValue, avgValue };
 }
 
 /**
- * Reduce a data source for a line graph, based off of a minimum distance
- * between the points to render.
- */
-function sparsifyLineData(plottedData, plottedMinMaxSum, options) {
-  let { width: graphWidth, height: graphHeight } = options;
-  let { dataOffsetX, dampenValuesFactor } = options;
-  let { minSquaredDistanceBetweenPoints } = options;
-
-  let result = [];
-
-  let totalTicks = plottedData.length;
-  let maxValue = plottedMinMaxSum.maxValue;
-
-  let firstTick = totalTicks ? plottedData[0].delta : 0;
-  let lastTick = totalTicks ? plottedData[totalTicks - 1].delta : 0;
-  let dataScaleX = graphWidth / (lastTick - dataOffsetX);
-  let dataScaleY = graphHeight / maxValue * dampenValuesFactor;
-
-  let prevX = 0;
-  let prevY = 0;
-
-  for (let { delta, value } of plottedData) {
-    let currX = (delta - dataOffsetX) * dataScaleX;
-    let currY = graphHeight - value * dataScaleY;
-
-    if (delta == firstTick || delta == lastTick) {
-      result.push({ delta, value });
-      continue;
-    }
-
-    let dist = distSquared(prevX, prevY, currX, currY);
-    if (dist >= minSquaredDistanceBetweenPoints) {
-      result.push({ delta, value });
-      prevX = currX;
-      prevY = currY;
-    }
-  }
-
-  return result;
-}
-
-/**
  * Takes a list of numbers and plots them on a line graph representing
  * the rate of occurences in a specified interval.
  *
  * XXX: Copied almost verbatim from toolkit/devtools/server/actors/framerate.js
  * Remove that dead code after the Performance panel lands, bug 1075567.
  *
  * @param array timestamps
  *        A list of numbers representing time, ordered ascending. For example,
@@ -145,17 +100,8 @@ function plotTimestamps(timestamps, inte
     timeline.push({ delta: currTime, value: rate });
 
     frameCount = 0;
     prevTime = currTime;
   }
 
   return timeline;
 }
-
-/**
- * Calculates the squared distance between two 2D points.
- */
-function distSquared(x0, y0, x1, y1) {
-  let xs = x1 - x0;
-  let ys = y1 - y0;
-  return xs * xs + ys * ys;
-}
--- a/browser/devtools/sourceeditor/autocomplete.js
+++ b/browser/devtools/sourceeditor/autocomplete.js
@@ -1,13 +1,14 @@
 /* vim:set ts=2 sw=2 sts=2 et tw=80:
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+const { Cu } = require("chrome");
 const cssAutoCompleter = require("devtools/sourceeditor/css-autocompleter");
 const { AutocompletePopup } = require("devtools/shared/autocomplete-popup");
 
 const CM_TERN_SCRIPTS = [
   "chrome://browser/content/devtools/codemirror/tern.js",
   "chrome://browser/content/devtools/codemirror/show-hint.js"
 ];
 
@@ -210,17 +211,17 @@ function autoComplete({ ed, cm }) {
     let cursorElement = cm.display.cursorDiv.querySelector(".CodeMirror-cursor");
     let left = suggestions[0].preLabel.length * cm.defaultCharWidth() + 4;
     popup.hidePopup();
     popup.setItems(suggestions);
     popup.openPopup(cursorElement, -1 * left, 0);
     private.suggestionInsertedOnce = false;
     // This event is used in tests.
     ed.emit("after-suggest");
-  });
+  }).then(null, Cu.reportError);
 }
 
 /**
  * Cycles through provided suggestions by the popup in a top to bottom manner
  * when `reverse` is not true. Opposite otherwise.
  */
 function cycleSuggestions(ed, reverse) {
   let private = autocompleteMap.get(ed);
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -158,16 +158,21 @@ ElementStyle.prototype = {
       this.dummyElement = document.createElementNS(namespaceURI,
                                                    this.element.tagName);
       document.documentElement.appendChild(this.dummyElement);
       return this.dummyElement;
     }).then(null, promiseWarn);
   },
 
   destroy: function() {
+    if (this.destroyed) {
+      return;
+    }
+    this.destroyed = true;
+
     this.dummyElement = null;
     this.dummyElementPromise.then(dummyElement => {
       dummyElement.remove();
       this.dummyElementPromise = null;
     }, console.error);
   },
 
   /**
@@ -188,16 +193,20 @@ ElementStyle.prototype = {
    * ready.
    */
   populate: function() {
     let populated = this.pageStyle.getApplied(this.element, {
       inherited: true,
       matchedSelectors: true,
       filter: this.showUserAgentStyles ? "ua" : undefined,
     }).then(entries => {
+      if (this.destroyed) {
+        return;
+      }
+
       // Make sure the dummy element has been created before continuing...
       return this.dummyElementPromise.then(() => {
         if (this.populated != populated) {
           // Don't care anymore.
           return;
         }
 
         // Store the current list of rules (if any) during the population
@@ -1030,17 +1039,17 @@ TextProperty.prototype = {
     if (changed) {
       this.updateEditor();
     }
   },
 
   setValue: function(aValue, aPriority, force=false) {
     let store = this.rule.elementStyle.store;
 
-    if (aValue !== this.editor.committed.value || force) {
+    if (this.editor && aValue !== this.editor.committed.value || force) {
       store.userProperties.setProperty(this.rule.style, this.name, aValue);
     }
 
     this.rule.setPropertyValue(this, aValue, aPriority);
     this.updateEditor();
   },
 
   setName: function(aName) {
@@ -1519,18 +1528,18 @@ CssRuleView.prototype = {
 
     this.tooltips.destroy();
     this.highlighters.destroy();
 
     if (this.element.parentNode) {
       this.element.parentNode.removeChild(this.element);
     }
 
-    if (this.elementStyle) {
-      this.elementStyle.destroy();
+    if (this._elementStyle) {
+      this._elementStyle.destroy();
     }
 
     this.popup.destroy();
   },
 
   /**
    * Update the view with a new selected element.
    *
@@ -1570,24 +1579,33 @@ CssRuleView.prototype = {
    * Update the rules for the currently highlighted element.
    */
   refreshPanel: function() {
     // Ignore refreshes during editing or when no element is selected.
     if (this.isEditing || !this._elementStyle) {
       return;
     }
 
-    // Repopulate the element style.
-    this._populate(true);
+    // Repopulate the element style once the current modifications are done.
+    let promises = [];
+    for (let rule of this._elementStyle.rules) {
+      if (rule._applyingModifications) {
+        promises.push(rule._applyingModifications);
+      }
+    }
+
+    return promise.all(promises).then(() => {
+      return this._populate(true);
+    });
   },
 
   _populate: function(clearRules = false) {
     let elementStyle = this._elementStyle;
     return this._elementStyle.populate().then(() => {
-      if (this._elementStyle != elementStyle) {
+      if (this._elementStyle != elementStyle || this.isDestroyed) {
         return;
       }
 
       if (clearRules) {
         this._clearRules();
       }
       this._createEditors();
 
@@ -1623,17 +1641,21 @@ CssRuleView.prototype = {
   },
 
   /**
    * Clear the rule view.
    */
   clear: function() {
     this._clearRules();
     this._viewedElement = null;
-    this._elementStyle = null;
+
+    if (this._elementStyle) {
+      this._elementStyle.destroy();
+      this._elementStyle = null;
+    }
   },
 
   /**
    * Called when the user has made changes to the ElementStyle.
    * Emits an event that clients can listen to.
    */
   _changed: function() {
     var evt = this.doc.createEvent("Events");
--- a/browser/modules/AboutHome.jsm
+++ b/browser/modules/AboutHome.jsm
@@ -22,17 +22,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 // Url to fetch snippets, in the urlFormatter service format.
 const SNIPPETS_URL_PREF = "browser.aboutHomeSnippets.updateUrl";
 
 // Should be bumped up if the snippets content format changes.
 const STARTPAGE_VERSION = 4;
 
 this.AboutHomeUtils = {
-  get snippetsVersion() STARTPAGE_VERSION,
+  get snippetsVersion() {
+    return STARTPAGE_VERSION;
+  },
 
   /*
    * showKnowYourRights - Determines if the user should be shown the
    * about:rights notification. The notification should *not* be shown if
    * we've already shown the current version, or if the override pref says to
    * never show it. The notification *should* be shown if it's never been seen
    * before, if a newer version is available, or if the override pref says to
    * always show it.
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -208,16 +208,18 @@ size. -->
 <!ENTITY pref_font_size_adjust_char "A">
 
 <!-- Localization note (pref_font_size_preview_text): This paragraph is used as an example to
     demonstrate the font size setting.  It is meant to be whimsical and fun. -->
 <!ENTITY pref_font_size_preview_text "The quick orange fox jumps over your expectations with more speed, more flexibility and more security. As a non-profit, we\'re free to innovate on your behalf without any pressure to compromise. That means a better experience for you and a brighter future for the Web.">
 
 <!ENTITY pref_media_autoplay_enabled "Allow autoplay">
 <!ENTITY pref_media_autoplay_enabled_summary "Control if websites can autoplay videos and other media content">
+<!ENTITY pref_zoom_force_enabled "Always enable zoom">
+<!ENTITY pref_zoom_force_enabled_summary "Force override so you can zoom any page">
 
 <!ENTITY pref_use_master_password "Use master password">
 <!ENTITY pref_sync "Sync">
 <!ENTITY pref_sync_summary "Sync your tabs, bookmarks, passwords, history">
 <!ENTITY pref_search_suggestions "Show search suggestions">
 <!ENTITY pref_import_android "Import from Android">
 <!ENTITY pref_import_android_summary "Import bookmarks and history from the native browser">
 <!ENTITY pref_private_data_history2 "Browsing history">
--- a/mobile/android/base/resources/xml/preferences_display.xml
+++ b/mobile/android/base/resources/xml/preferences_display.xml
@@ -24,16 +24,20 @@
     <CheckBoxPreference android:key="browser.chrome.dynamictoolbar"
                         android:title="@string/pref_scroll_title_bar2"
                         android:summary="@string/pref_scroll_title_bar_summary" />
 
     <CheckBoxPreference android:key="media.autoplay.enabled"
                         android:title="@string/pref_media_autoplay_enabled"
                         android:summary="@string/pref_media_autoplay_enabled_summary" />
 
+    <CheckBoxPreference android:key="browser.ui.zoom.force-user-scalable"
+                        android:title="@string/pref_zoom_force_enabled"
+                        android:summary="@string/pref_zoom_force_enabled_summary" />
+
     <PreferenceCategory android:title="@string/pref_category_advanced">
 
         <CheckBoxPreference
                         android:key="browser.zoom.reflowOnZoom"
                         android:title="@string/pref_reflow_on_zoom"
                         android:defaultValue="false"
                         android:persistent="false" />
 
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -201,16 +201,18 @@
   <string name="pref_font_size_medium">&pref_font_size_medium;</string>
   <string name="pref_font_size_large">&pref_font_size_large;</string>
   <string name="pref_font_size_xlarge">&pref_font_size_xlarge;</string>
   <string name="pref_font_size_set">&pref_font_size_set;</string>
   <string name="pref_font_size_adjust_char">&pref_font_size_adjust_char;</string>
   <string name="pref_font_size_preview_text">&pref_font_size_preview_text;</string>
   <string name="pref_media_autoplay_enabled">&pref_media_autoplay_enabled;</string>
   <string name="pref_media_autoplay_enabled_summary">&pref_media_autoplay_enabled_summary;</string>
+  <string name="pref_zoom_force_enabled">&pref_zoom_force_enabled;</string>
+  <string name="pref_zoom_force_enabled_summary">&pref_zoom_force_enabled_summary;</string>
   <string name="pref_reflow_on_zoom">&pref_reflow_on_zoom4;</string>
   <string name="pref_restore">&pref_restore;</string>
   <string name="pref_restore_always">&pref_restore_always;</string>
   <string name="pref_restore_quit">&pref_restore_quit;</string>
   <string name="pref_sync">&pref_sync;</string>
   <string name="pref_sync_summary">&pref_sync_summary;</string>
   <string name="pref_search_suggestions">&pref_search_suggestions;</string>
   <string name="pref_private_data_history2">&pref_private_data_history2;</string>
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -105,25 +105,25 @@ BreakpointActorMap.prototype = {
       }
       else {
         for (let key of Object.keys(object)) {
           yield key;
         }
       }
     }
 
-    query.actor = query.source ? query.source.actor : undefined;
+    query.sourceActorID = query.sourceActor ? query.sourceActor.actorID : undefined;
     query.beginColumn = query.column ? query.column : undefined;
     query.endColumn = query.column ? query.column + 1 : undefined;
 
-    for (let actor of findKeys(this._actors, query.actor))
-    for (let line of findKeys(this._actors[actor], query.line))
-    for (let beginColumn of findKeys(this._actors[actor][line], query.beginColumn))
-    for (let endColumn of findKeys(this._actors[actor][line][beginColumn], query.endColumn)) {
-      yield this._actors[actor][line][beginColumn][endColumn];
+    for (let sourceActorID of findKeys(this._actors, query.sourceActorID))
+    for (let line of findKeys(this._actors[sourceActorID], query.line))
+    for (let beginColumn of findKeys(this._actors[sourceActorID][line], query.beginColumn))
+    for (let endColumn of findKeys(this._actors[sourceActorID][line][beginColumn], query.endColumn)) {
+      yield this._actors[sourceActorID][line][beginColumn][endColumn];
     }
   },
 
   /**
    * Return the instance of BreakpointActor at the given location in this
    * instance of BreakpointActorMap.
    *
    * @param Object location
@@ -152,65 +152,67 @@ BreakpointActorMap.prototype = {
    *        - source
    *        - line
    *        - column (optional)
    *
    * @param BreakpointActor actor
    *        The instance of BreakpointActor to be set to the given location.
    */
   setActor: function (location, actor) {
-    let { source, line, column } = location;
-
+    let { sourceActor, line, column } = location;
+
+    let sourceActorID = sourceActor.actorID;
     let beginColumn = column ? column : 0;
     let endColumn = column ? column + 1 : Infinity;
 
-    if (!this._actors[source.actor]) {
-      this._actors[source.actor] = [];
-    }
-    if (!this._actors[source.actor][line]) {
-      this._actors[source.actor][line] = [];
-    }
-    if (!this._actors[source.actor][line][beginColumn]) {
-      this._actors[source.actor][line][beginColumn] = [];
-    }
-    if (!this._actors[source.actor][line][beginColumn][endColumn]) {
+    if (!this._actors[sourceActorID]) {
+      this._actors[sourceActorID] = [];
+    }
+    if (!this._actors[sourceActorID][line]) {
+      this._actors[sourceActorID][line] = [];
+    }
+    if (!this._actors[sourceActorID][line][beginColumn]) {
+      this._actors[sourceActorID][line][beginColumn] = [];
+    }
+    if (!this._actors[sourceActorID][line][beginColumn][endColumn]) {
       ++this._size;
     }
-    this._actors[source.actor][line][beginColumn][endColumn] = actor;
+    this._actors[sourceActorID][line][beginColumn][endColumn] = actor;
   },
 
   /**
    * Delete the instance of BreakpointActor from the given location in this
    * instance of BreakpointActorMap.
    *
    * @param Object location
    *        An object with the following properties:
    *        - source
    *        - line
    *        - column (optional)
    */
   deleteActor: function (location) {
-    let { source, line, column } = location;
-
+    let { sourceActor, line, column } = location;
+
+    let sourceActorID = sourceActor.actorID;
     let beginColumn = column ? column : 0;
     let endColumn = column ? column + 1 : Infinity;
 
-    if (this._actors[source.actor]) {
-      if (this._actors[source.actor][line]) {
-        if (this._actors[source.actor][line][beginColumn]) {
-          if (this._actors[source.actor][line][beginColumn][endColumn]) {
+    if (this._actors[sourceActorID]) {
+      if (this._actors[sourceActorID][line]) {
+        if (this._actors[sourceActorID][line][beginColumn]) {
+          if (this._actors[sourceActorID][line][beginColumn][endColumn]) {
             --this._size;
           }
-          delete this._actors[source.actor][line][beginColumn][endColumn];
-          if (Object.keys(this._actors[source.actor][line][beginColumn]).length === 0) {
-            delete this._actors[source.actor][line][beginColumn];
+          delete this._actors[sourceActorID][line][beginColumn][endColumn];
+          if (Object.keys(this._actors[sourceActorID][line][beginColumn]).length === 0) {
+            delete this._actors[sourceActorID][line][beginColumn];
           }
         }
-        if (Object.keys(this._actors[source.actor][line]).length === 0) {
-          delete this._actors[source.actor][line];
+        if (Object.keys(this._actors[sourceActorID][line]).length === 0) {
+          delete this._actors[sourceActorID][line];
         }
       }
     }
   }
 };
 
 exports.BreakpointActorMap = BreakpointActorMap;
 
@@ -1204,17 +1206,17 @@ ThreadActor.prototype = {
         let location = { line: line };
         let resp = sourceActor.setBreakpoint(location);
         dbg_assert(!resp.actualLocation, "No actualLocation should be returned");
         if (resp.error) {
           reportError(new Error("Unable to set breakpoint on event listener"));
           return;
         }
         let bpActor = this.breakpointActorMap.getActor({
-          source: sourceActor.form(),
+          sourceActor: sourceActor,
           line: location.line
         });
         dbg_assert(bpActor, "Breakpoint actor must be created");
         this._hiddenBreakpoints.set(bpActor.actorID, bpActor);
         break;
       }
     }
   },
@@ -2036,21 +2038,21 @@ ThreadActor.prototype = {
   _addScript: function (aScript) {
     if (!this._allowSource(aScript.source)) {
       return false;
     }
 
     // Set any stored breakpoints.
     let endLine = aScript.startLine + aScript.lineCount - 1;
     let source = this.sources.createNonSourceMappedActor(aScript.source);
-    for (let bpActor of this.breakpointActorMap.findActors({ source: source.form() })) {
+    for (let bpActor of this.breakpointActorMap.findActors({ sourceActor: source })) {
       // Limit the search to the line numbers contained in the new script.
       if (bpActor.location.line >= aScript.startLine
           && bpActor.location.line <= endLine) {
-        source.setBreakpoint(bpActor.location, aScript);
+        source.setBreakpoint(bpActor.location);
       }
     }
 
     // Go ahead and establish the source actors for this script, which
     // fetches sourcemaps if available and sends onNewSource
     // notifications
     this.sources.createSourceActors(aScript.source);
 
@@ -2910,77 +2912,107 @@ SourceActor.prototype = {
         return { line, entryPoints };
       }
     }
 
     return null;
   },
 
   /**
-   * Set a breakpoint using the Debugger API. If the line on which the
-   * breakpoint is being set contains no code, then the breakpoint will slide
-   * down to the next line that has runnable code. In this case the server
-   * breakpoint cache will be updated, so callers that iterate over the
-   * breakpoint cache should take that into account.
+   * Get or create a BreakpointActor for the given location, and set it as a
+   * breakpoint handler on all scripts that match the given location for which
+   * the BreakpointActor is not already a breakpoint handler.
+   *
+   * It is possible that no scripts match the given location, because they have
+   * all been garbage collected. In that case, the BreakpointActor is not set as
+   * a breakpoint handler for any script, but is still inserted in the
+   * BreakpointActorMap as a pending breakpoint. Whenever a new script is
+   * introduced, we call this method again to see if there are now any scripts
+   * that matches the given location.
+   *
+   * The first time we find one or more scripts that matches the given location,
+   * we check if any of these scripts has any entry points for the given
+   * location. If not, we assume that the given location does not have any code.
+   *
+   * If the given location does not contain any code, we slide the breakpoint
+   * down to the next closest line that does, and update the BreakpointActorMap
+   * accordingly. Note that we only do so if the breakpoint actor is still
+   * pending (i.e. is not set as a breakpoint handler for any script).
    *
    * @param object aLocation
    *        The location of the breakpoint (in the generated source, if source
    *        mapping).
-   * @param Debugger.Script aOnlyThisScript [optional]
-   *        If provided, only set breakpoints in this Debugger.Script, and
-   *        nowhere else.
    */
-  setBreakpoint: function (aLocation, aOnlyThisScript=null) {
+  setBreakpoint: function (aLocation) {
     const location = {
-      source: this.form(),
+      sourceActor: this,
       line: aLocation.line,
       column: aLocation.column,
       condition: aLocation.condition
     };
     const actor = location.actor = this._getOrCreateBreakpointActor(location);
 
     // Find all scripts matching the given location. We will almost always have
     // a `source` object to query, but multiple inline HTML scripts are all
     // represented by a single SourceActor even though they have separate source
     // objects, so we need to query based on the url of the page for them.
-    const scripts = this.source
+    let scripts = this.source
       ? this.scripts.getScriptsBySourceAndLine(this.source, location.line)
       : this.scripts.getScriptsByURLAndLine(this._originalUrl, location.line);
 
     if (scripts.length === 0) {
       // Since we did not find any scripts to set the breakpoint on now, return
       // early. When a new script that matches this breakpoint location is
       // introduced, the breakpoint actor will already be in the breakpoint
       // store and the breakpoint will be set at that time. This is similar to
       // GDB's "pending" breakpoints for shared libraries that aren't loaded
       // yet.
       return {
         actor: actor.actorID
       }
     }
 
+    // Ignore scripts for which the BreakpointActor is already a breakpoint
+    // handler.
+    scripts = scripts.filter((script) => !actor.hasScript(script));
+
     if (location.column) {
       return this._setBreakpointAtColumn(scripts, location, actor);
     }
 
-    // Select the first line that has offsets, and is greater than or equal to
-    // the requested line. Set breakpoints on each of the offsets that is an
-    // entry point to our selected line.
-
-    const result = this._findNextLineWithOffsets(scripts, location.line);
+    let result;
+    if (actor.scripts.size === 0) {
+      // If the BreakpointActor is not a breakpoint handler for any script, its
+      // location is not yet fixed. Use breakpoint sliding to select the first
+      // line greater than or equal to the requested line that has one or more
+      // offsets.
+      result = this._findNextLineWithOffsets(scripts, location.line);
+    } else {
+      // If the BreakpointActor is a breakpoint handler for at least one script,
+      // breakpoint sliding has already been carried out, so select the
+      // requested line, even if it does not have any offsets.
+      let entryPoints = this._findEntryPointsForLine(scripts, location.line)
+      if (entryPoints) {
+        result = {
+          line: location.line,
+          entryPoints: entryPoints
+        };
+      }
+    }
+
     if (!result) {
       return {
         error: "noCodeAtLineColumn",
         actor: actor.actorID
       };
     }
 
     const { line, entryPoints } = result;
     const actualLocation = line !== location.line
-          ? { source: { actor: this.actorID }, line }
+                         ? { sourceActor: this, line }
       : undefined;
 
     if (actualLocation) {
       // Check whether we already have a breakpoint actor for the actual
       // location. If we do have an existing actor, then the actor we created
       // above is redundant and must be destroyed. If we do not have an existing
       // actor, we need to update the breakpoint store with the new location.
 
@@ -2998,22 +3030,17 @@ SourceActor.prototype = {
           sourceActor: this,
           line: actualLocation.line
         };
         this.breakpointActorMap.deleteActor(location);
         this.breakpointActorMap.setActor(actualLocation, actor);
       }
     }
 
-    this._setBreakpointOnEntryPoints(
-      actor,
-      aOnlyThisScript
-        ? entryPoints.filter(o => o.script === aOnlyThisScript)
-        : entryPoints
-    );
+    this._setBreakpointOnEntryPoints(actor, entryPoints);
 
     return {
       actor: actor.actorID,
       actualLocation
     };
   },
 
   /**
@@ -4701,16 +4728,20 @@ function BreakpointActor(aThreadActor, {
   this.location = { sourceActor, line, column };
   this.condition = condition;
 }
 
 BreakpointActor.prototype = {
   actorPrefix: "breakpoint",
   condition: null,
 
+  hasScript: function (aScript) {
+    return this.scripts.has(aScript);
+  },
+
   /**
    * Called when this same breakpoint is added to another Debugger.Script
    * instance.
    *
    * @param aScript Debugger.Script
    *        The new source script on which the breakpoint has been set.
    * @param ThreadActor aThreadActor
    *        The parent thread actor that contains this breakpoint.
--- a/toolkit/devtools/server/protocol.js
+++ b/toolkit/devtools/server/protocol.js
@@ -2,17 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 let { Cu } = require("chrome");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 let Services = require("Services");
-let promise = require("devtools/toolkit/deprecated-sync-thenables");
+let promise = require("promise");
 let {Class} = require("sdk/core/heritage");
 let {EventTarget} = require("sdk/event/target");
 let events = require("sdk/event/core");
 let object = require("sdk/util/object");
 
 exports.emit = events.emit;
 
 // Waiting for promise.done() to be added, see bug 851321
--- a/toolkit/devtools/server/tests/unit/test_breakpoint-actor-map.js
+++ b/toolkit/devtools/server/tests/unit/test_breakpoint-actor-map.js
@@ -16,21 +16,21 @@ function run_test()
   test_delete_actor();
   test_find_actors();
   test_duplicate_actors();
 }
 
 function test_get_actor() {
   let bpStore = new BreakpointActorMap();
   let location = {
-    source: { actor: 'actor1' },
+    sourceActor: { actor: 'actor1' },
     line: 3
   };
   let columnLocation = {
-    source: { actor: 'actor2' },
+    sourceActor: { actor: 'actor2' },
     line: 5,
     column: 15
   };
 
   // Shouldn't have breakpoint
   do_check_eq(null, bpStore.getActor(location),
               "Breakpoint not added and shouldn't exist.");
 
@@ -54,68 +54,68 @@ function test_get_actor() {
   do_check_eq(null, bpStore.getActor(columnLocation),
               "Breakpoint with column removed but still exists in Breakpoint Store.");
 }
 
 function test_set_actor() {
   // Breakpoint with column
   let bpStore = new BreakpointActorMap();
   let location = {
-    source: { actor: 'actor1' },
+    sourceActor: { actor: 'actor1' },
     line: 10,
     column: 9
   };
   bpStore.setActor(location, {});
   do_check_true(!!bpStore.getActor(location),
                 "We should have the column breakpoint we just added");
 
   // Breakpoint without column (whole line breakpoint)
   location = {
-    source: { actor: 'actor2' },
+    sourceActor: { actor: 'actor2' },
     line: 103
   };
   bpStore.setActor(location, {});
   do_check_true(!!bpStore.getActor(location),
                 "We should have the whole line breakpoint we just added");
 }
 
 function test_delete_actor() {
   // Breakpoint with column
   let bpStore = new BreakpointActorMap();
   let location = {
-    source: { actor: 'actor1' },
+    sourceActor: { actor: 'actor1' },
     line: 10,
     column: 9
   };
   bpStore.setActor(location, {});
   bpStore.deleteActor(location);
   do_check_eq(bpStore.getActor(location), null,
               "We should not have the column breakpoint anymore");
 
   // Breakpoint without column (whole line breakpoint)
   location = {
-    source: { actor: 'actor2' },
+    sourceActor: { actor: 'actor2' },
     line: 103
   };
   bpStore.setActor(location, {});
   bpStore.deleteActor(location);
   do_check_eq(bpStore.getActor(location), null,
               "We should not have the whole line breakpoint anymore");
 }
 
 function test_find_actors() {
   let bps = [
-    { source: { actor: "actor1" }, line: 10 },
-    { source: { actor: "actor1" }, line: 10, column: 3 },
-    { source: { actor: "actor1" }, line: 10, column: 10 },
-    { source: { actor: "actor1" }, line: 23, column: 89 },
-    { source: { actor: "actor2" }, line: 10, column: 1 },
-    { source: { actor: "actor2" }, line: 20, column: 5 },
-    { source: { actor: "actor2" }, line: 30, column: 34 },
-    { source: { actor: "actor2" }, line: 40, column: 56 }
+    { sourceActor: { actor: "actor1" }, line: 10 },
+    { sourceActor: { actor: "actor1" }, line: 10, column: 3 },
+    { sourceActor: { actor: "actor1" }, line: 10, column: 10 },
+    { sourceActor: { actor: "actor1" }, line: 23, column: 89 },
+    { sourceActor: { actor: "actor2" }, line: 10, column: 1 },
+    { sourceActor: { actor: "actor2" }, line: 20, column: 5 },
+    { sourceActor: { actor: "actor2" }, line: 30, column: 34 },
+    { sourceActor: { actor: "actor2" }, line: 40, column: 56 }
   ];
 
   let bpStore = new BreakpointActorMap();
 
   for (let bp of bps) {
     bpStore.setActor(bp, bp);
   }
 
@@ -125,28 +125,28 @@ function test_find_actors() {
   for (let bp of bpStore.findActors()) {
     bpSet.delete(bp);
   }
   do_check_eq(bpSet.size, 0,
               "Should be able to iterate over all breakpoints");
 
   // Breakpoints by URL
 
-  bpSet = Set(bps.filter(bp => { return bp.source.actor === "actor1" }));
-  for (let bp of bpStore.findActors({ source: { actor: "actor1" } })) {
+  bpSet = Set(bps.filter(bp => { return bp.sourceActor.actorID === "actor1" }));
+  for (let bp of bpStore.findActors({ sourceActor: { actorID: "actor1" } })) {
     bpSet.delete(bp);
   }
   do_check_eq(bpSet.size, 0,
               "Should be able to filter the iteration by url");
 
   // Breakpoints by URL and line
 
-  bpSet = Set(bps.filter(bp => { return bp.source.actor === "actor1" && bp.line === 10; }));
+  bpSet = Set(bps.filter(bp => { return bp.sourceActor.actorID === "actor1" && bp.line === 10; }));
   let first = true;
-  for (let bp of bpStore.findActors({ source: { actor: "actor1" }, line: 10 })) {
+  for (let bp of bpStore.findActors({ sourceActor: { actorID: "actor1" }, line: 10 })) {
     if (first) {
       do_check_eq(bp.column, undefined,
                   "Should always get the whole line breakpoint first");
       first = false;
     } else {
       do_check_neq(bp.column, undefined,
                    "Should not get the whole line breakpoint any time other than first.");
     }
@@ -156,27 +156,27 @@ function test_find_actors() {
               "Should be able to filter the iteration by url and line");
 }
 
 function test_duplicate_actors() {
   let bpStore = new BreakpointActorMap();
 
   // Breakpoint with column
   let location = {
-    source: { actor: "foo-actor" },
+    sourceActor: { actorID: "foo-actor" },
     line: 10,
     column: 9
   };
   bpStore.setActor(location, {});
   bpStore.setActor(location, {});
   do_check_eq(bpStore.size, 1, "We should have only 1 column breakpoint");
   bpStore.deleteActor(location);
 
   // Breakpoint without column (whole line breakpoint)
   location = {
-    source: { actor: "foo-actor" },
+    sourceActor: { actorID: "foo-actor" },
     line: 15
   };
   bpStore.setActor(location, {});
   bpStore.setActor(location, {});
   do_check_eq(bpStore.size, 1, "We should have only 1 whole line breakpoint");
   bpStore.deleteActor(location);
 }