Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 14 Jun 2013 13:04:31 -0400
changeset 146585 8c05aa558b45c6a50a71f57fb0e98109b653cb85
parent 146584 81d4522e23858e4a4ae8e16c0084ddfeaa74b237 (current diff)
parent 146552 5ddb1bf962618e8afdbd467a86546e4d5f392019 (diff)
child 146586 f5c81bdff6108d4cf142da7f1b479880e385fbf6
push id2697
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 18:49:53 +0000
treeherdermozilla-beta@dfec938c7b63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone24.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound.
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "7bfb4efb6e67c97e1178fa340a17eff63d0c33d4", 
+    "revision": "0eef3cf5ff4407369e0bfb773672a6e4b5687558", 
     "repo_path": "/integration/gaia-central"
 }
--- a/browser/devtools/styleeditor/StyleEditorPanel.jsm
+++ b/browser/devtools/styleeditor/StyleEditorPanel.jsm
@@ -86,26 +86,26 @@ StyleEditorPanel.prototype = {
   },
 
   /**
    * Select a stylesheet.
    *
    * @param {string} href
    *        Url of stylesheet to find and select in editor
    * @param {number} line
-   *        Line number to jump to after selecting
+   *        Line number to jump to after selecting. One-indexed
    * @param {number} col
-   *        Column number to jump to after selecting
+   *        Column number to jump to after selecting. One-indexed
    */
   selectStyleSheet: function(href, line, col) {
     if (!this._debuggee || !this.UI) {
       return;
     }
     let stylesheet = this._debuggee.styleSheetFromHref(href);
-    this.UI.selectStyleSheet(href, line, col);
+    this.UI.selectStyleSheet(href, line - 1, col - 1);
   },
 
   /**
    * Destroy the style editor.
    */
   destroy: function() {
     if (!this._destroyed) {
       this._destroyed = true;
--- a/browser/devtools/styleeditor/StyleEditorUI.jsm
+++ b/browser/devtools/styleeditor/StyleEditorUI.jsm
@@ -43,17 +43,17 @@ function StyleEditorUI(debuggee, panelDo
   EventEmitter.decorate(this);
 
   this._debuggee = debuggee;
   this._panelDoc = panelDoc;
   this._window = this._panelDoc.defaultView;
   this._root = this._panelDoc.getElementById("style-editor-chrome");
 
   this.editors = [];
-  this.selectedStyleSheetIndex = -1;
+  this.selectedEditor = null;
 
   this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this);
   this._onStyleSheetsCleared = this._onStyleSheetsCleared.bind(this);
   this._onDocumentLoad = this._onDocumentLoad.bind(this);
   this._onError = this._onError.bind(this);
 
   debuggee.on("document-load", this._onDocumentLoad);
   debuggee.on("stylesheets-cleared", this._onStyleSheetsCleared);
@@ -79,16 +79,24 @@ StyleEditorUI.prototype = {
 
   /*
    * Mark the style editor as having or not having unsaved changes.
    */
   set isDirty(value) {
     this._markedDirty = value;
   },
 
+  /*
+   * Index of selected stylesheet in document.styleSheets
+   */
+  get selectedStyleSheetIndex() {
+    return this.selectedEditor ?
+           this.selectedEditor.styleSheet.styleSheetIndex : -1;
+  },
+
   /**
    * Build the initial UI and wire buttons with event handlers.
    */
   createUI: function() {
     let viewRoot = this._root.parentNode.querySelector(".splitview-root");
 
     this._view = new SplitView(viewRoot);
 
@@ -135,20 +143,27 @@ StyleEditorUI.prototype = {
 
     showFilePicker(file, false, parentWindow, onFileSelected);
   },
 
   /**
    * Handler for debuggee's 'stylesheets-cleared' event. Remove all editors.
    */
   _onStyleSheetsCleared: function() {
-    this._clearStyleSheetEditors();
+    // remember selected sheet and line number for next load
+    if (this.selectedEditor) {
+      let href = this.selectedEditor.styleSheet.href;
+      let {line, col} = this.selectedEditor.sourceEditor.getCaretPosition();
+      this.selectStyleSheet(href, line, col);
+    }
 
+    this._clearStyleSheetEditors();
     this._view.removeAll();
-    this.selectedStyleSheetIndex = -1;
+
+    this.selectedEditor = null;
 
     this._root.classList.add("loading");
   },
 
   /**
    * When a new or imported stylesheet has been added to the document.
    * Add an editor for it.
    */
@@ -161,21 +176,32 @@ StyleEditorUI.prototype = {
    * for all style sheets in the document
    *
    * @param {string} event
    *        Event name
    * @param {StyleSheet} styleSheet
    *        StyleSheet object for new sheet
    */
   _onDocumentLoad: function(event, styleSheets) {
+    if (this._styleSheetToSelect) {
+      // if selected stylesheet from previous load isn't here,
+      // just set first stylesheet to be selected instead
+      let selectedExists = styleSheets.some((sheet) => {
+        return this._styleSheetToSelect.href == sheet.href;
+      })
+      if (!selectedExists) {
+        this._styleSheetToSelect = null;
+      }
+    }
     for (let sheet of styleSheets) {
       this._addStyleSheetEditor(sheet);
     }
-    // this might be the first stylesheet, so remove loading indicator
+
     this._root.classList.remove("loading");
+
     this.emit("document-load");
   },
 
   /**
    * Forward any error from a stylesheet.
    *
    * @param  {string} event
    *         Event name
@@ -290,78 +316,78 @@ StyleEditorUI.prototype = {
           this._selectEditor(editor);
         }
 
         this.emit("editor-added", editor);
       }.bind(this),
 
       onShow: function(summary, details, data) {
         let editor = data.editor;
+        this.selectedEditor = editor;
+        this._styleSheetToSelect = null;
+
         if (!editor.sourceEditor) {
           // only initialize source editor when we switch to this view
           let inputElement = details.querySelector(".stylesheet-editor-input");
           editor.load(inputElement);
         }
         editor.onShow();
-      }
+
+        this.emit("editor-selected", editor);
+      }.bind(this)
     });
   },
 
   /**
    * Switch to the editor that has been marked to be selected.
    */
   switchToSelectedSheet: function() {
     let sheet = this._styleSheetToSelect;
 
     for each (let editor in this.editors) {
       if (editor.styleSheet.href == sheet.href) {
         this._selectEditor(editor, sheet.line, sheet.col);
-        this._styleSheetToSelect = null;
         break;
       }
     }
   },
 
   /**
    * Select an editor in the UI.
    *
    * @param  {StyleSheetEditor} editor
    *         Editor to switch to.
    * @param  {number} line
    *         Line number to jump to
    * @param  {number} col
    *         Column number to jump to
    */
   _selectEditor: function(editor, line, col) {
-    line = line || 1;
-    col = col || 1;
-
-    this.selectedStyleSheetIndex = editor.styleSheet.styleSheetIndex;
+    line = line || 0;
+    col = col || 0;
 
     editor.getSourceEditor().then(() => {
-      editor.sourceEditor.setCaretPosition(line - 1, col - 1);
+      editor.sourceEditor.setCaretPosition(line, col);
     });
 
     this._view.activeSummary = editor.summary;
-
-    this.emit("editor-selected", editor);
   },
 
   /**
    * selects a stylesheet and optionally moves the cursor to a selected line
    *
    * @param {string} [href]
    *        Href of stylesheet that should be selected. If a stylesheet is not passed
    *        and the editor is not initialized we focus the first stylesheet. If
    *        a stylesheet is not passed and the editor is initialized we ignore
    *        the call.
    * @param {Number} [line]
-   *        Line to which the caret should be moved (one-indexed).
+   *        Line to which the caret should be moved (zero-indexed).
    * @param {Number} [col]
-   *        Column to which the caret should be moved (one-indexed).
+   *        Column to which the caret should be moved (zero-indexed).
    */
   selectStyleSheet: function(href, line, col)
   {
     let alreadyCalled = !!this._styleSheetToSelect;
 
     this._styleSheetToSelect = {
       href: href,
       line: line,
--- a/browser/devtools/styleeditor/test/Makefile.in
+++ b/browser/devtools/styleeditor/test/Makefile.in
@@ -23,16 +23,17 @@ include $(topsrcdir)/config/rules.mk
                  browser_styleeditor_new.js \
                  browser_styleeditor_pretty.js \
                  browser_styleeditor_private_perwindowpb.js \
                  browser_styleeditor_sv_keynav.js \
                  browser_styleeditor_sv_resize.js \
                  browser_styleeditor_bug_740541_iframes.js \
                  browser_styleeditor_bug_851132_middle_click.js \
                  browser_styleeditor_nostyle.js \
+                 browser_styleeditor_reload.js \
                  head.js \
                  four.html \
                  head.js \
                  import.css \
                  import.html \
                  import2.css \
                  longload.html \
                  media.html \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_reload.js
@@ -0,0 +1,99 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+const NEW_URI = TEST_BASE_HTTPS + "media.html";
+
+const LINE_NO = 5;
+const COL_NO = 3;
+
+let gContentWin;
+let gUI;
+
+function test()
+{
+  waitForExplicitFinish();
+
+  addTabAndOpenStyleEditor(function(panel) {
+    gContentWin = gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject;
+    gUI = panel.UI;
+
+    let count = 0;
+    gUI.on("editor-added", function editorAdded(event, editor) {
+      if (++count == 2) {
+        gUI.off("editor-added", editorAdded);
+        gUI.editors[0].getSourceEditor().then(runTests);
+      }
+    })
+  });
+
+  content.location = TESTCASE_URI;
+}
+
+function runTests()
+{
+  let count = 0;
+  gUI.once("editor-selected", (event, editor) => {
+    editor.getSourceEditor().then(() => {
+      info("selected second editor, about to reload page");
+      reloadPage();
+
+      gUI.on("editor-added", function editorAdded(event, editor) {
+        if (++count == 2) {
+          gUI.off("editor-added", editorAdded);
+          gUI.editors[1].getSourceEditor().then(testRemembered);
+        }
+      })
+    });
+  });
+  gUI.selectStyleSheet(gUI.editors[1].styleSheet.href, LINE_NO, COL_NO);
+}
+
+function testRemembered()
+{
+  is(gUI.selectedEditor, gUI.editors[1], "second editor is selected");
+
+  let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+  is(line, LINE_NO, "correct line selected");
+  is(col, COL_NO, "correct column selected");
+
+  testNewPage();
+}
+
+function testNewPage()
+{
+  let count = 0;
+  gUI.on("editor-added", function editorAdded(event, editor) {
+    info("editor added here")
+    if (++count == 2) {
+      gUI.off("editor-added", editorAdded);
+      gUI.editors[0].getSourceEditor().then(testNotRemembered);
+    }
+  })
+
+  info("navigating to a different page");
+  navigatePage();
+}
+
+function testNotRemembered()
+{
+  is(gUI.selectedEditor, gUI.editors[0], "first editor is selected");
+
+  let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+  is(line, 0, "first line is selected");
+  is(col, 0, "first column is selected");
+
+  gUI = null;
+  finish();
+}
+
+function reloadPage()
+{
+  gContentWin.location.reload();
+}
+
+function navigatePage()
+{
+  gContentWin.location = NEW_URI;
+}
\ No newline at end of file
--- a/dom/network/src/NetworkStatsDB.jsm
+++ b/dom/network/src/NetworkStatsDB.jsm
@@ -20,20 +20,21 @@ const STORE_NAME = "net_stats";
 
 // Constant defining the maximum values allowed per interface. If more, older
 // will be erased.
 const VALUES_MAX_LENGTH = 6 * 30;
 
 // Constant defining the rate of the samples. Daily.
 const SAMPLE_RATE = 1000 * 60 * 60 * 24;
 
-this.NetworkStatsDB = function NetworkStatsDB(aGlobal) {
+this.NetworkStatsDB = function NetworkStatsDB(aGlobal, aConnectionTypes) {
   if (DEBUG) {
     debug("Constructor");
   }
+  this._connectionTypes = aConnectionTypes;
   this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME], aGlobal);
 }
 
 NetworkStatsDB.prototype = {
   __proto__: IndexedDBHelper.prototype,
 
   dbNewTxn: function dbNewTxn(txn_type, callback, txnCb) {
     function successCb(result) {
@@ -62,30 +63,51 @@ NetworkStatsDB.prototype = {
         objectStore.createIndex("timestamp", "timestamp", { unique: false });
         objectStore.createIndex("rxBytes", "rxBytes", { unique: false });
         objectStore.createIndex("txBytes", "txBytes", { unique: false });
         objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
         objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
         if (DEBUG) {
           debug("Created object stores and indexes");
         }
+
+        // There could be a time delay between the point when the network
+        // interface comes up and the point when the database is initialized.
+        // In this short interval some traffic data are generated but are not
+        // registered by the first sample. The initialization of the database
+        // should make up the missing sample.
+        let stats = [];
+        for (let connection in this._connectionTypes) {
+          let connectionType = this._connectionTypes[connection].name;
+          let timestamp = this.normalizeDate(new Date());
+          stats.push({ connectionType: connectionType,
+                       timestamp:      timestamp,
+                       rxBytes:        0,
+                       txBytes:        0,
+                       rxTotalBytes:   0,
+                       txTotalBytes:   0 });
+        }
+        this._saveStats(aTransaction, objectStore, stats);
+        if (DEBUG) {
+          debug("Database initialized");
+        }
       }
     }
   },
 
-  convertDate: function convertDate(aDate) {
+   normalizeDate: function normalizeDate(aDate) {
     // Convert to UTC according to timezone and
     // filter timestamp to get SAMPLE_RATE precission
-    let timestamp = aDate.getTime() - aDate.getTimezoneOffset() * 60 * 1000;
+    let timestamp = aDate.getTime() - (new Date()).getTimezoneOffset() * 60 * 1000;
     timestamp = Math.floor(timestamp / SAMPLE_RATE) * SAMPLE_RATE;
     return timestamp;
   },
 
   saveStats: function saveStats(stats, aResultCb) {
-    let timestamp = this.convertDate(stats.date);
+    let timestamp = this.normalizeDate(stats.date);
 
     stats = {connectionType: stats.connectionType,
              timestamp:      timestamp,
              rxBytes:        0,
              txBytes:        0,
              rxTotalBytes:   stats.rxBytes,
              txTotalBytes:   stats.txBytes};
 
@@ -229,18 +251,19 @@ NetworkStatsDB.prototype = {
       if (DEBUG) {
         debug("Going to clear all!");
       }
       store.clear();
     }, aResultCb);
   },
 
   find: function find(aResultCb, aOptions) {
-    let start = this.convertDate(aOptions.start);
-    let end = this.convertDate(aOptions.end);
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    let start = this.normalizeDate(aOptions.start);
+    let end = this.normalizeDate(aOptions.end);
 
     if (DEBUG) {
       debug("Find: connectionType:" + aOptions.connectionType + " start: " + start + " end: " + end);
       debug("Start time: " + new Date(start));
       debug("End time: " + new Date(end));
     }
 
     this.dbNewTxn("readonly", function(txn, store) {
@@ -254,36 +277,37 @@ NetworkStatsDB.prototype = {
         txn.result = {};
       }
 
       let request = store.openCursor(range).onsuccess = function(event) {
         var cursor = event.target.result;
         if (cursor){
           data.push({ rxBytes: cursor.value.rxBytes,
                       txBytes: cursor.value.txBytes,
-                      date: new Date(cursor.value.timestamp) });
+                      date: new Date(cursor.value.timestamp + offset) });
           cursor.continue();
           return;
         }
 
         // When requested samples (start / end) are not in the range of now and
         // now - VALUES_MAX_LENGTH, fill with empty samples.
-        this.fillResultSamples(start, end, data);
+        this.fillResultSamples(start + offset, end + offset, data);
 
         txn.result.connectionType = aOptions.connectionType;
         txn.result.start = aOptions.start;
         txn.result.end = aOptions.end;
         txn.result.data = data;
       }.bind(this);
     }.bind(this), aResultCb);
   },
 
   findAll: function findAll(aResultCb, aOptions) {
-    let start = this.convertDate(aOptions.start);
-    let end = this.convertDate(aOptions.end);
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    let start = this.normalizeDate(aOptions.start);
+    let end = this.normalizeDate(aOptions.end);
 
     if (DEBUG) {
       debug("FindAll: start: " + start + " end: " + end + "\n");
     }
 
     let self = this;
     this.dbNewTxn("readonly", function(txn, store) {
       let lowFilter = start;
@@ -294,30 +318,31 @@ NetworkStatsDB.prototype = {
 
       if (!txn.result) {
         txn.result = {};
       }
 
       let request = store.index("timestamp").openCursor(range).onsuccess = function(event) {
         var cursor = event.target.result;
         if (cursor) {
-          if (data.length > 0 && data[data.length - 1].date.getTime() == cursor.value.timestamp) {
+          if (data.length > 0 &&
+              data[data.length - 1].date.getTime() == cursor.value.timestamp + offset) {
             // Time is the same, so add values.
             data[data.length - 1].rxBytes += cursor.value.rxBytes;
             data[data.length - 1].txBytes += cursor.value.txBytes;
           } else {
             data.push({ rxBytes: cursor.value.rxBytes,
                         txBytes: cursor.value.txBytes,
-                        date: new Date(cursor.value.timestamp) });
+                        date: new Date(cursor.value.timestamp + offset) });
           }
           cursor.continue();
           return;
         }
 
-        this.fillResultSamples(start, end, data);
+        this.fillResultSamples(start + offset, end + offset, data);
 
         txn.result.connectionType = aOptions.connectionType;
         txn.result.start = aOptions.start;
         txn.result.end = aOptions.end;
         txn.result.data = data;
       }.bind(this);
     }.bind(this), aResultCb);
   },
--- a/dom/network/src/NetworkStatsService.jsm
+++ b/dom/network/src/NetworkStatsService.jsm
@@ -63,17 +63,17 @@ this.NetworkStatsService = {
                      "NetworkStats:SampleRate",
                      "NetworkStats:MaxStorageSamples"];
 
     this.messages.forEach(function(msgName) {
       ppmm.addMessageListener(msgName, this);
     }, this);
 
     gIDBManager.initWindowless(myGlobal);
-    this._db = new NetworkStatsDB(myGlobal);
+    this._db = new NetworkStatsDB(myGlobal, this._connectionTypes);
 
     // Stats for all interfaces are updated periodically
     this.timer.initWithCallback(this, this._db.sampleRate,
                                 Ci.nsITimer.TYPE_REPEATING_PRECISE);
 
     this.updateQueue = [];
     this.isQueueRunning = false;
   },
--- a/dom/network/tests/test_networkstats_basics.html
+++ b/dom/network/tests/test_networkstats_basics.html
@@ -46,18 +46,19 @@ function test() {
    "maxStorageSamples is greater than 0.");
 
   // Test IDL methods
   next();
   return;
 }
 
 function checkDataDates(data, start, end, sampleRate){
-  start = Math.floor(start.getTime() / sampleRate) * sampleRate;
-  end = Math.floor(end.getTime() / sampleRate) * sampleRate;
+  var offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+  start = Math.floor((start.getTime() - offset) / sampleRate) * sampleRate + offset;
+  end = Math.floor((end.getTime() - offset) / sampleRate) * sampleRate + offset;
 
   var counter = 0;
   var date = start;
   var success = true;
 
   do {
     if(data[counter].date.getTime() !=  date) {
       success = false;
@@ -144,17 +145,19 @@ var steps = [
   function () {
     ok(true, "Get stats for a connectionType and dates adapted to samplerate");
     // Prepare get params
     var type = netStats.connectionTypes[0];
     var diff = 2;
     // Get samplerate in millis
     var sampleRate = netStats.sampleRate * 1000;
     // Get date with samplerate's precision
-    var endDate = new Date(Math.floor(new Date().getTime() / sampleRate) * sampleRate);
+    var offset = new Date().getTimezoneOffset() * 60 * 1000;
+    var endDate = new Date(Math.floor((new Date().getTime() - offset) / sampleRate)
+                           * sampleRate + offset);
     var startDate = new Date(endDate.getTime() - (sampleRate * diff));
     // Calculate the number of samples that should be returned based on the
     // the samplerate and including final and initial samples.
     var samples = (endDate.getTime() - startDate.getTime()) / sampleRate + 1;
 
     // Launch request
     req = netStats.getNetworkStats({start: startDate, end: endDate, connectionType: type});
     req.onsuccess = function () {
@@ -174,17 +177,19 @@ var steps = [
   },
   function () {
     ok(true, "Get stats for all connectionTypes and dates adapted to samplerate");
     // Prepare get params
     var diff = 2;
     // Get samplerate in millis
     var sampleRate = netStats.sampleRate * 1000;
     // Get date with samplerate's precision
-    var endDate = new Date(Math.floor(new Date().getTime() / sampleRate) * sampleRate);
+    var offset = new Date().getTimezoneOffset() * 60 * 1000;
+    var endDate = new Date(Math.floor((new Date().getTime() - offset) / sampleRate)
+                           * sampleRate + offset);
     var startDate = new Date(endDate.getTime() - (sampleRate * diff));
     // Calculate the number of samples that should be returned based on the
     // the samplerate and including final and initial samples.
     var samples = (endDate.getTime() - startDate.getTime()) / sampleRate + 1;
 
     // Launch request
     req = netStats.getNetworkStats({start: startDate, end: endDate});
     req.onsuccess = function () {
--- a/python/mach/mach/dispatcher.py
+++ b/python/mach/mach/dispatcher.py
@@ -172,18 +172,25 @@ class CommandAction(argparse.Action):
         # command arguments, we can't simply put all arguments on the same
         # parser instance because argparse would complain. We can't register an
         # argparse subparser here because it won't properly show help for
         # global arguments. So, we employ a strategy similar to command
         # execution where we construct a 2nd, independent ArgumentParser for
         # just the command data then supplement the main help's output with
         # this 2nd parser's. We use a custom formatter class to ignore some of
         # the help output.
-        c_parser = argparse.ArgumentParser(formatter_class=CommandFormatter,
-            add_help=False)
+        parser_args = {
+            'formatter_class': CommandFormatter,
+            'add_help': False,
+        }
+
+        if handler.allow_all_arguments:
+            parser_args['prefix_chars'] = '+'
+
+        c_parser = argparse.ArgumentParser(**parser_args)
 
         group = c_parser.add_argument_group('Command Arguments')
 
         for arg in handler.arguments:
             group.add_argument(*arg[0], **arg[1])
 
         # This will print the description of the command below the usage.
         description = handler.description
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_sourcemaps-08.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Regression test for bug 882986 regarding sourcesContent and absolute source
+ * URLs.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+Components.utils.import("resource:///modules/devtools/SourceMap.jsm");
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-source-map");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTabAndResume(gClient, "test-source-map", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_source_maps();
+    });
+  });
+  do_test_pending();
+}
+
+function test_source_maps()
+{
+  gClient.addOneTimeListener("newSource", function (aEvent, aPacket) {
+    let sourceClient = gThreadClient.source(aPacket.source);
+    sourceClient.source(function ({error, source}) {
+      do_check_true(!error, "should be able to grab the source");
+      do_check_eq(source, "foo",
+                  "Should load the source from the sourcesContent field");
+      finishClient(gClient);
+    });
+  });
+
+  let code = "'nothing here';\n";
+  code += "//# sourceMappingURL=data:text/json," + JSON.stringify({
+    version: 3,
+    file: "foo.js",
+    sources: ["/a"],
+    names: [],
+    mappings: "AACA",
+    sourcesContent: ["foo"]
+  });
+  Components.utils.evalInSandbox(code, gDebuggee, "1.8",
+                                 "http://example.com/foo.js", 1);
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -91,16 +91,17 @@ skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_sourcemaps-05.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_sourcemaps-06.js]
 [test_sourcemaps-07.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
+[test_sourcemaps-08.js]
 [test_objectgrips-01.js]
 [test_objectgrips-02.js]
 [test_objectgrips-03.js]
 [test_objectgrips-04.js]
 [test_objectgrips-05.js]
 [test_objectgrips-06.js]
 [test_objectgrips-07.js]
 [test_interrupt.js]
--- a/toolkit/devtools/sourcemap/SourceMap.jsm
+++ b/toolkit/devtools/sourcemap/SourceMap.jsm
@@ -321,27 +321,31 @@ define('source-map/source-map-consumer',
    */
   SourceMapConsumer.prototype.sourceContentFor =
     function SourceMapConsumer_sourceContentFor(aSource) {
       if (!this.sourcesContent) {
         return null;
       }
 
       if (this.sourceRoot) {
-        // Try to remove the sourceRoot
-        var relativeUrl = util.relative(this.sourceRoot, aSource);
-        if (this._sources.has(relativeUrl)) {
-          return this.sourcesContent[this._sources.indexOf(relativeUrl)];
-        }
+        aSource = util.relative(this.sourceRoot, aSource);
       }
 
       if (this._sources.has(aSource)) {
         return this.sourcesContent[this._sources.indexOf(aSource)];
       }
 
+      var url;
+      if (this.sourceRoot
+          && (url = util.urlParse(this.sourceRoot))
+          && (!url.path || url.path == "/")
+          && this._sources.has("/" + aSource)) {
+        return this.sourcesContent[this._sources.indexOf("/" + aSource)];
+      }
+
       throw new Error('"' + aSource + '" is not in the SourceMap.');
     };
 
   /**
    * Returns the generated line and column information for the original source,
    * line, and column positions provided. The only argument is an object with
    * the following properties:
    *
@@ -480,26 +484,46 @@ define('source-map/util', ['require', 'e
     return {
       scheme: match[1],
       auth: match[3],
       host: match[4],
       port: match[6],
       path: match[7]
     };
   }
+  exports.urlParse = urlParse;
+
+  function urlGenerate(aParsedUrl) {
+    var url = aParsedUrl.scheme + "://";
+    if (aParsedUrl.auth) {
+      url += aParsedUrl.auth + "@"
+    }
+    if (aParsedUrl.host) {
+      url += aParsedUrl.host;
+    }
+    if (aParsedUrl.port) {
+      url += ":" + aParsedUrl.port
+    }
+    if (aParsedUrl.path) {
+      url += aParsedUrl.path;
+    }
+    return url;
+  }
+  exports.urlGenerate = urlGenerate;
 
   function join(aRoot, aPath) {
     var url;
 
     if (aPath.match(urlRegexp)) {
       return aPath;
     }
 
     if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) {
-      return aRoot.replace(url.path, '') + aPath;
+      url.path = aPath;
+      return urlGenerate(url);
     }
 
     return aRoot.replace(/\/$/, '') + '/' + aPath;
   }
   exports.join = join;
 
   /**
    * Because behavior goes wacky when you set `__proto__` on objects, we
@@ -517,16 +541,22 @@ define('source-map/util', ['require', 'e
 
   function fromSetString(aStr) {
     return aStr.substr(1);
   }
   exports.fromSetString = fromSetString;
 
   function relative(aRoot, aPath) {
     aRoot = aRoot.replace(/\/$/, '');
+
+    var url = urlParse(aRoot);
+    if (aPath.charAt(0) == "/" && url && url.path == "/") {
+      return aPath.slice(1);
+    }
+
     return aPath.indexOf(aRoot + '/') === 0
       ? aPath.substr(aRoot.length + 1)
       : aPath;
   }
   exports.relative = relative;
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
@@ -1125,16 +1155,34 @@ define('source-map/source-map-generator'
         // Cases 2 and 3.
         return;
       }
       else {
         throw new Error('Invalid mapping.');
       }
     };
 
+  function cmpLocation(loc1, loc2) {
+    var cmp = (loc1 && loc1.line) - (loc2 && loc2.line);
+    return cmp ? cmp : (loc1 && loc1.column) - (loc2 && loc2.column);
+  }
+
+  function strcmp(str1, str2) {
+    str1 = str1 || '';
+    str2 = str2 || '';
+    return (str1 > str2) - (str1 < str2);
+  }
+
+  function cmpMapping(mappingA, mappingB) {
+    return cmpLocation(mappingA.generated, mappingB.generated) ||
+      cmpLocation(mappingA.original, mappingB.original) ||
+      strcmp(mappingA.source, mappingB.source) ||
+      strcmp(mappingA.name, mappingB.name);
+  }
+
   /**
    * Serialize the accumulated mappings in to the stream of base 64 VLQs
    * specified by the source map format.
    */
   SourceMapGenerator.prototype._serializeMappings =
     function SourceMapGenerator_serializeMappings() {
       var previousGeneratedColumn = 0;
       var previousGeneratedLine = 1;
@@ -1145,35 +1193,33 @@ define('source-map/source-map-generator'
       var result = '';
       var mapping;
 
       // The mappings must be guarenteed to be in sorted order before we start
       // serializing them or else the generated line numbers (which are defined
       // via the ';' separators) will be all messed up. Note: it might be more
       // performant to maintain the sorting as we insert them, rather than as we
       // serialize them, but the big O is the same either way.
-      this._mappings.sort(function (mappingA, mappingB) {
-        var cmp = mappingA.generated.line - mappingB.generated.line;
-        return cmp === 0
-          ? mappingA.generated.column - mappingB.generated.column
-          : cmp;
-      });
+      this._mappings.sort(cmpMapping);
 
       for (var i = 0, len = this._mappings.length; i < len; i++) {
         mapping = this._mappings[i];
 
         if (mapping.generated.line !== previousGeneratedLine) {
           previousGeneratedColumn = 0;
           while (mapping.generated.line !== previousGeneratedLine) {
             result += ';';
             previousGeneratedLine++;
           }
         }
         else {
           if (i > 0) {
+            if (!cmpMapping(mapping, this._mappings[i - 1])) {
+              continue;
+            }
             result += ',';
           }
         }
 
         result += base64VLQ.encode(mapping.generated.column
                                    - previousGeneratedColumn);
         previousGeneratedColumn = mapping.generated.column;
 
--- a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
+++ b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
@@ -260,26 +260,46 @@ define('lib/source-map/util', ['require'
     return {
       scheme: match[1],
       auth: match[3],
       host: match[4],
       port: match[6],
       path: match[7]
     };
   }
+  exports.urlParse = urlParse;
+
+  function urlGenerate(aParsedUrl) {
+    var url = aParsedUrl.scheme + "://";
+    if (aParsedUrl.auth) {
+      url += aParsedUrl.auth + "@"
+    }
+    if (aParsedUrl.host) {
+      url += aParsedUrl.host;
+    }
+    if (aParsedUrl.port) {
+      url += ":" + aParsedUrl.port
+    }
+    if (aParsedUrl.path) {
+      url += aParsedUrl.path;
+    }
+    return url;
+  }
+  exports.urlGenerate = urlGenerate;
 
   function join(aRoot, aPath) {
     var url;
 
     if (aPath.match(urlRegexp)) {
       return aPath;
     }
 
     if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) {
-      return aRoot.replace(url.path, '') + aPath;
+      url.path = aPath;
+      return urlGenerate(url);
     }
 
     return aRoot.replace(/\/$/, '') + '/' + aPath;
   }
   exports.join = join;
 
   /**
    * Because behavior goes wacky when you set `__proto__` on objects, we
@@ -297,16 +317,22 @@ define('lib/source-map/util', ['require'
 
   function fromSetString(aStr) {
     return aStr.substr(1);
   }
   exports.fromSetString = fromSetString;
 
   function relative(aRoot, aPath) {
     aRoot = aRoot.replace(/\/$/, '');
+
+    var url = urlParse(aRoot);
+    if (aPath.charAt(0) == "/" && url && url.path == "/") {
+      return aPath.slice(1);
+    }
+
     return aPath.indexOf(aRoot + '/') === 0
       ? aPath.substr(aRoot.length + 1)
       : aPath;
   }
   exports.relative = relative;
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
--- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
@@ -288,12 +288,27 @@ define("test/source-map/test-source-map-
 
     var sources = map.sources;
     assert.equal(sources.length, 1,
                  'Should only be one source.');
     assert.equal(sources[0], 'http://example.com/original.js',
                  'Source should be relative the host of the source root.');
   };
 
+  exports['test github issue #64'] = function (assert, util) {
+    var map = new SourceMapConsumer({
+      "version": 3,
+      "file": "foo.js",
+      "sourceRoot": "http://example.com/",
+      "sources": ["/a"],
+      "names": [],
+      "mappings": "AACA",
+      "sourcesContent": ["foo"]
+    });
+
+    assert.equal(map.sourceContentFor("a"), "foo");
+    assert.equal(map.sourceContentFor("/a"), "foo");
+  };
+
 });
 function run_test() {
   runSourceMapTests('test/source-map/test-source-map-consumer', do_throw);
 }
--- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_generator.js
@@ -268,12 +268,132 @@ define("test/source-map/test-source-map-
 
     // apply source map "mapStep1" to "mapStep2"
     var generator = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(mapStep2));
     generator.applySourceMap(new SourceMapConsumer(mapStep1));
     var actualMap = generator.toJSON();
 
     util.assertEqualMaps(assert, actualMap, expectedMap);
   };
+
+  exports['test sorting with duplicate generated mappings'] = function (assert, util) {
+    var map = new SourceMapGenerator({
+      file: 'test.js'
+    });
+    map.addMapping({
+      generated: { line: 3, column: 0 },
+      original: { line: 2, column: 0 },
+      source: 'a.js'
+    });
+    map.addMapping({
+      generated: { line: 2, column: 0 }
+    });
+    map.addMapping({
+      generated: { line: 2, column: 0 }
+    });
+    map.addMapping({
+      generated: { line: 1, column: 0 },
+      original: { line: 1, column: 0 },
+      source: 'a.js'
+    });
+
+    util.assertEqualMaps(assert, map.toJSON(), {
+      version: 3,
+      file: 'test.js',
+      sources: ['a.js'],
+      names: [],
+      mappings: 'AAAA;A;AACA'
+    });
+  };
+
+  exports['test ignore duplicate mappings.'] = function (assert, util) {
+    var init = { file: 'min.js', sourceRoot: '/the/root' };
+    var map1, map2;
+
+    // null original source location
+    var nullMapping1 = {
+      generated: { line: 1, column: 0 }
+    };
+    var nullMapping2 = {
+      generated: { line: 2, column: 2 }
+    };
+
+    map1 = new SourceMapGenerator(init);
+    map2 = new SourceMapGenerator(init);
+
+    map1.addMapping(nullMapping1);
+    map1.addMapping(nullMapping1);
+
+    map2.addMapping(nullMapping1);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+
+    map1.addMapping(nullMapping2);
+    map1.addMapping(nullMapping1);
+
+    map2.addMapping(nullMapping2);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+
+    // original source location
+    var srcMapping1 = {
+      generated: { line: 1, column: 0 },
+      original: { line: 11, column: 0 },
+      source: 'srcMapping1.js'
+    };
+    var srcMapping2 = {
+      generated: { line: 2, column: 2 },
+      original: { line: 11, column: 0 },
+      source: 'srcMapping2.js'
+    };
+
+    map1 = new SourceMapGenerator(init);
+    map2 = new SourceMapGenerator(init);
+
+    map1.addMapping(srcMapping1);
+    map1.addMapping(srcMapping1);
+
+    map2.addMapping(srcMapping1);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+
+    map1.addMapping(srcMapping2);
+    map1.addMapping(srcMapping1);
+
+    map2.addMapping(srcMapping2);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+
+    // full original source and name information
+    var fullMapping1 = {
+      generated: { line: 1, column: 0 },
+      original: { line: 11, column: 0 },
+      source: 'fullMapping1.js',
+      name: 'fullMapping1'
+    };
+    var fullMapping2 = {
+      generated: { line: 2, column: 2 },
+      original: { line: 11, column: 0 },
+      source: 'fullMapping2.js',
+      name: 'fullMapping2'
+    };
+
+    map1 = new SourceMapGenerator(init);
+    map2 = new SourceMapGenerator(init);
+
+    map1.addMapping(fullMapping1);
+    map1.addMapping(fullMapping1);
+
+    map2.addMapping(fullMapping1);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+
+    map1.addMapping(fullMapping2);
+    map1.addMapping(fullMapping1);
+
+    map2.addMapping(fullMapping2);
+
+    util.assertEqualMaps(assert, map1.toJSON(), map2.toJSON());
+  };
 });
 function run_test() {
   runSourceMapTests('test/source-map/test-source-map-generator', do_throw);
 }