Bug 762761 - add prettyPrint request to the remote debugging protocol server/client; r=past
authorNick Fitzgerald <fitzgen@gmail.com>
Wed, 11 Sep 2013 10:15:51 -0700
changeset 146681 9929cf38b486956b530650ad2998d811218f510a
parent 146680 0b479df9a59a0a3acb7fbdccedd4b3e5dbba00c3
child 146682 3d36387df33f40a858bcd1bf5daa316070001772
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerspast
bugs762761
milestone26.0a1
Bug 762761 - add prettyPrint request to the remote debugging protocol server/client; r=past
toolkit/devtools/client/dbg-client.jsm
toolkit/devtools/server/actors/script.js
toolkit/devtools/server/main.js
toolkit/devtools/server/tests/unit/head_dbg.js
toolkit/devtools/server/tests/unit/test_pretty_print-01.js
toolkit/devtools/server/tests/unit/test_pretty_print-02.js
toolkit/devtools/server/tests/unit/test_pretty_print-03.js
toolkit/devtools/server/tests/unit/xpcshell.ini
toolkit/devtools/sourcemap/SourceMap.jsm
toolkit/devtools/sourcemap/tests/unit/Utils.jsm
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -2005,42 +2005,60 @@ SourceClient.prototype = {
   /**
    * Get a long string grip for this SourceClient's source.
    */
   source: function SC_source(aCallback) {
     let packet = {
       to: this._form.actor,
       type: "source"
     };
-    this._client.request(packet, function (aResponse) {
+    this._client.request(packet, aResponse => {
+      this._onSourceResponse(aResponse, aCallback)
+    });
+  },
+
+  /**
+   * Pretty print this source's text.
+   */
+  prettyPrint: function SC_prettyPrint(aIndent, aCallback) {
+    const packet = {
+      to: this._form.actor,
+      type: "prettyPrint",
+      indent: aIndent
+    };
+    this._client.request(packet, aResponse => {
+      this._onSourceResponse(aResponse, aCallback);
+    });
+  },
+
+  _onSourceResponse: function SC__onSourceResponse(aResponse, aCallback) {
+    if (aResponse.error) {
+      aCallback(aResponse);
+      return;
+    }
+
+    if (typeof aResponse.source === "string") {
+      aCallback(aResponse);
+      return;
+    }
+
+    let { contentType, source } = aResponse;
+    let longString = this._client.activeThread.threadLongString(
+      source);
+    longString.substring(0, longString.length, function (aResponse) {
       if (aResponse.error) {
         aCallback(aResponse);
         return;
       }
 
-      if (typeof aResponse.source === "string") {
-        aCallback(aResponse);
-        return;
-      }
-
-      let { contentType, source } = aResponse;
-      let longString = this._client.activeThread.threadLongString(
-        source);
-      longString.substring(0, longString.length, function (aResponse) {
-        if (aResponse.error) {
-          aCallback(aResponse);
-          return;
-        }
-
-        aCallback({
-          source: aResponse.substring,
-          contentType: contentType
-        });
+      aCallback({
+        source: aResponse.substring,
+        contentType: contentType
       });
-    }.bind(this));
+    });
   }
 };
 
 /**
  * Breakpoint clients are used to remove breakpoints that are no longer used.
  *
  * @param aClient DebuggerClient
  *        The debugger client parent.
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -2293,21 +2293,29 @@ PauseScopedActor.prototype = {
  * A SourceActor provides information about the source of a script.
  *
  * @param aUrl String
  *        The url of the source we are representing.
  * @param aThreadActor ThreadActor
  *        The current thread actor.
  * @param aSourceMap SourceMapConsumer
  *        Optional. The source map that introduced this source, if available.
+ * @param aGeneratedSource String
+ *        Optional, passed in when aSourceMap is also passed in. The generated
+ *        source url that introduced this source.
  */
-function SourceActor(aUrl, aThreadActor, aSourceMap=null) {
+function SourceActor(aUrl, aThreadActor, aSourceMap=null, aGeneratedSource=null) {
   this._threadActor = aThreadActor;
   this._url = aUrl;
   this._sourceMap = aSourceMap;
+  this._generatedSource = aGeneratedSource;
+
+  this.onSource = this.onSource.bind(this);
+  this._invertSourceMap = this._invertSourceMap.bind(this);
+  this._saveMap = this._saveMap.bind(this);
 }
 
 SourceActor.prototype = {
   constructor: SourceActor,
   actorPrefix: "source",
 
   get threadActor() this._threadActor,
   get url() this._url,
@@ -2316,43 +2324,45 @@ SourceActor.prototype = {
     return {
       actor: this.actorID,
       url: this._url,
       isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url)
       // TODO bug 637572: introductionScript
     };
   },
 
-  disconnect: function LSA_disconnect() {
+  disconnect: function SA_disconnect() {
     if (this.registeredPool && this.registeredPool.sourceActors) {
       delete this.registeredPool.sourceActors[this.actorID];
     }
   },
 
+  _getSourceText: function SA__getSourceText() {
+    let sourceContent = null;
+    if (this._sourceMap) {
+      sourceContent = this._sourceMap.sourceContentFor(this._url);
+    }
+
+    if (sourceContent) {
+      return resolve({
+        content: sourceContent
+      });
+    }
+
+    // XXX bug 865252: Don't load from the cache if this is a source mapped
+    // source because we can't guarantee that the cache has the most up to date
+    // content for this source like we can if it isn't source mapped.
+    return fetch(this._url, { loadFromCache: !this._sourceMap });
+  },
+
   /**
    * Handler for the "source" packet.
    */
   onSource: function SA_onSource(aRequest) {
-    let sourceContent = null;
-    if (this._sourceMap) {
-      sourceContent = this._sourceMap.sourceContentFor(this._url);
-    }
-
-    if (sourceContent) {
-      return {
-        from: this.actorID,
-        source: this.threadActor.createValueGrip(
-          sourceContent, this.threadActor.threadLifetimePool)
-      };
-    }
-
-    // XXX bug 865252: Don't load from the cache if this is a source mapped
-    // source because we can't guarantee that the cache has the most up to date
-    // content for this source like we can if it isn't source mapped.
-    return fetch(this._url, { loadFromCache: !this._sourceMap })
+    return this._getSourceText()
       .then(({ content, contentType }) => {
         return {
           from: this.actorID,
           source: this.threadActor.createValueGrip(
             content, this.threadActor.threadLifetimePool),
           contentType: contentType
         };
       })
@@ -2363,16 +2373,119 @@ SourceActor.prototype = {
           "error": "loadSourceError",
           "message": "Could not load the source for " + this._url + ".\n"
             + safeErrorString(aError)
         };
       });
   },
 
   /**
+   * Handler for the "prettyPrint" packet.
+   */
+  onPrettyPrint: function ({ indent }) {
+    return this._getSourceText()
+      .then(this._parseAST)
+      .then(this._generatePrettyCodeAndMap(indent))
+      .then(this._invertSourceMap)
+      .then(this._saveMap)
+      .then(this.onSource)
+      .then(null, error => ({
+        from: this.actorID,
+        error: "prettyPrintError",
+        message: DevToolsUtils.safeErrorString(error)
+      }));
+  },
+
+  /**
+   * Parse the source content into an AST.
+   */
+  _parseAST: function SA__parseAST({ content}) {
+    return Reflect.parse(content);
+  },
+
+  /**
+   * Take the number of spaces to indent and return a function that takes an AST
+   * and generates code and a source map from the ugly code to the pretty code.
+   */
+  _generatePrettyCodeAndMap: function SA__generatePrettyCodeAndMap(aNumSpaces) {
+    let indent = "";
+    for (let i = 0; i < aNumSpaces; i++) {
+      indent += " ";
+    }
+    return aAST => escodegen.generate(aAST, {
+      format: {
+        indent: {
+          style: indent
+        }
+      },
+      sourceMap: this._url,
+      sourceMapWithCode: true
+    });
+  },
+
+  /**
+   * Invert a source map. So if a source map maps from a to b, return a new
+   * source map from b to a. We need to do this because the source map we get
+   * from _generatePrettyCodeAndMap goes the opposite way we want it to for
+   * debugging.
+   */
+  _invertSourceMap: function SA__invertSourceMap({ code, map }) {
+    const smc = new SourceMapConsumer(map.toJSON());
+    const invertedMap = new SourceMapGenerator({
+      file: this._url
+    });
+
+    smc.eachMapping(m => {
+      if (!m.originalLine || !m.originalColumn) {
+        return;
+      }
+      const invertedMapping = {
+        source: m.source,
+        name: m.name,
+        original: {
+          line: m.generatedLine,
+          column: m.generatedColumn
+        },
+        generated: {
+          line: m.originalLine,
+          column: m.originalColumn
+        }
+      };
+      invertedMap.addMapping(invertedMapping);
+    });
+
+    invertedMap.setSourceContent(this._url, code);
+
+    return {
+      code: code,
+      map: new SourceMapConsumer(invertedMap.toJSON())
+    };
+  },
+
+  /**
+   * Save the source map back to our thread's ThreadSources object so that
+   * stepping, breakpoints, debugger statements, etc can use it. If we are
+   * pretty printing a source mapped source, we need to compose the existing
+   * source map with our new one.
+   */
+  _saveMap: function SA__saveMap({ map }) {
+    if (this._sourceMap) {
+      // Compose the source maps
+      this._sourceMap = SourceMapGenerator.fromSourceMap(this._sourceMap);
+      this._sourceMap.applySourceMap(map, this._url);
+      this._sourceMap = new SourceMapConsumer(this._sourceMap.toJSON());
+      this._threadActor.sources.saveSourceMap(this._sourceMap,
+                                              this._generatedSource);
+    } else {
+      this._sourceMap = map;
+      this._threadActor.sources.saveSourceMap(this._sourceMap, this._url);
+    }
+  },
+
+  /**
    * Handler for the "blackbox" packet.
    */
   onBlackBox: function SA_onBlackBox(aRequest) {
     this.threadActor.sources.blackBox(this.url);
     let packet = {
       from: this.actorID
     };
     if (this.threadActor.state == "paused"
@@ -2392,17 +2505,18 @@ SourceActor.prototype = {
       from: this.actorID
     };
   }
 };
 
 SourceActor.prototype.requestTypes = {
   "source": SourceActor.prototype.onSource,
   "blackbox": SourceActor.prototype.onBlackBox,
-  "unblackbox": SourceActor.prototype.onUnblackBox
+  "unblackbox": SourceActor.prototype.onUnblackBox,
+  "prettyPrint": SourceActor.prototype.onPrettyPrint
 };
 
 
 /**
  * Creates an actor for the specified object.
  *
  * @param aObj Debugger.Object
  *        The debuggee object.
@@ -3481,28 +3595,31 @@ ThreadSources.prototype = {
    *
    * Right now this takes a URL, but in the future it should
    * take a Debugger.Source. See bug 637572.
    *
    * @param String aURL
    *        The source URL.
    * @param optional SourceMapConsumer aSourceMap
    *        The source map that introduced this source, if any.
+   * @param optional String aGeneratedSource
+   *        The generated source url that introduced this source via source map,
+   *        if any.
    * @returns a SourceActor representing the source at aURL or null.
    */
-  source: function TS_source(aURL, aSourceMap=null) {
+  source: function TS_source(aURL, aSourceMap=null, aGeneratedSource=null) {
     if (!this._allow(aURL)) {
       return null;
     }
 
     if (aURL in this._sourceActors) {
       return this._sourceActors[aURL];
     }
 
-    let actor = new SourceActor(aURL, this._thread, aSourceMap);
+    let actor = new SourceActor(aURL, this._thread, aSourceMap, aGeneratedSource);
     this._thread.threadLifetimePool.addActor(actor);
     this._sourceActors[aURL] = actor;
     try {
       this._onNewSource(actor);
     } catch (e) {
       reportError(e);
     }
     return actor;
@@ -3519,17 +3636,17 @@ ThreadSources.prototype = {
   sourcesForScript: function TS_sourcesForScript(aScript) {
     if (!this._useSourceMaps || !aScript.sourceMapURL) {
       return resolve([this.source(aScript.url)].filter(isNotNull));
     }
 
     return this.sourceMap(aScript)
       .then((aSourceMap) => {
         return [
-          this.source(s, aSourceMap) for (s of aSourceMap.sources)
+          this.source(s, aSourceMap, aScript.url) for (s of aSourceMap.sources)
         ];
       })
       .then(null, (e) => {
         reportError(e);
         delete this._sourceMapsByGeneratedSource[aScript.url];
         return [this.source(aScript.url)];
       })
       .then(function (aSources) {
@@ -3541,28 +3658,35 @@ ThreadSources.prototype = {
    * Return a promise of a SourceMapConsumer for the source map for
    * |aScript|; if we already have such a promise extant, return that.
    * |aScript| must have a non-null sourceMapURL.
    */
   sourceMap: function TS_sourceMap(aScript) {
     dbg_assert(aScript.sourceMapURL, "Script should have a sourceMapURL");
     let sourceMapURL = this._normalize(aScript.sourceMapURL, aScript.url);
     let map = this._fetchSourceMap(sourceMapURL, aScript.url)
-      .then((aSourceMap) => {
-        for (let s of aSourceMap.sources) {
-          this._generatedUrlsByOriginalUrl[s] = aScript.url;
-          this._sourceMapsByOriginalSource[s] = resolve(aSourceMap);
-        }
-        return aSourceMap;
-      });
+      .then(aSourceMap => this.saveSourceMap(aSourceMap, aScript.url));
     this._sourceMapsByGeneratedSource[aScript.url] = map;
     return map;
   },
 
   /**
+   * Save the given source map so that we can use it to query source locations
+   * down the line.
+   */
+  saveSourceMap: function TS_saveSourceMap(aSourceMap, aGeneratedSource) {
+    this._sourceMapsByGeneratedSource[aGeneratedSource] = resolve(aSourceMap);
+    for (let s of aSourceMap.sources) {
+      this._generatedUrlsByOriginalUrl[s] = aGeneratedSource;
+      this._sourceMapsByOriginalSource[s] = resolve(aSourceMap);
+    }
+    return aSourceMap;
+  },
+
+  /**
    * Return a promise of a SourceMapConsumer for the source map located at
    * |aAbsSourceMapURL|, which must be absolute. If there is already such a
    * promise extant, return it.
    *
    * @param string aAbsSourceMapURL
    *        The source map URL, in absolute form, not relative.
    * @param string aScriptURL
    *        When the source map URL is a data URI, there is no sourceRoot on the
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -39,16 +39,18 @@ if (this.require) {
 } else {
   let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
   localRequire = id => devtools.require(id);
 }
 
 const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties";
 
 const nsFile = CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath");
+Cu.import("resource://gre/modules/reflect.jsm");
+Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
 const promptConnections = Services.prefs.getBoolPref("devtools.debugger.prompt-connection");
 
 Cu.import("resource://gre/modules/jsdebugger.jsm");
 addDebuggerToGlobal(this);
 
@@ -67,16 +69,17 @@ function loadSubScript(aURL)
 }
 
 let loaderRequire = this.require;
 this.require = null;
 loadSubScript.call(this, "resource://gre/modules/commonjs/sdk/core/promise.js");
 this.require = loaderRequire;
 
 Cu.import("resource://gre/modules/devtools/SourceMap.jsm");
+const escodegen = localRequire("escodegen/escodegen");
 
 loadSubScript.call(this, "resource://gre/modules/devtools/DevToolsUtils.js");
 
 function dumpn(str) {
   if (wantLogging) {
     dump("DBG-SERVER: " + str + "\n");
   }
 }
--- a/toolkit/devtools/server/tests/unit/head_dbg.js
+++ b/toolkit/devtools/server/tests/unit/head_dbg.js
@@ -10,21 +10,32 @@ const Cr = Components.results;
 Cu.import("resource://gre/modules/Services.jsm");
 
 // Always log packets when running tests. runxpcshelltests.py will throw
 // the output away anyway, unless you give it the --verbose flag.
 Services.prefs.setBoolPref("devtools.debugger.log", true);
 // Enable remote debugging for the relevant tests.
 Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
 
-Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
-Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
-Cu.import("resource://gre/modules/devtools/Loader.jsm");
 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
 
+function tryImport(url) {
+  try {
+    Cu.import(url);
+  } catch (e) {
+    dump("Error importing " + url + "\n");
+    dump(DevToolsUtils.safeErrorString(e) + "\n");
+    throw e;
+  }
+}
+
+tryImport("resource://gre/modules/devtools/dbg-server.jsm");
+tryImport("resource://gre/modules/devtools/dbg-client.jsm");
+tryImport("resource://gre/modules/devtools/Loader.jsm");
+
 function testExceptionHook(ex) {
   try {
     do_report_unexpected_exception(ex);
   } catch(ex) {
     return {throw: ex}
   }
 }
 
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_pretty_print-01.js
@@ -0,0 +1,42 @@
+/* -*- Mode: javascript; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+// Test basic pretty printing functionality
+
+function run_test() {
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-pretty-print");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTabAndResume(gClient, "test-pretty-print", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      evalCode();
+    });
+  });
+  do_test_pending();
+}
+
+function evalCode() {
+  gClient.addOneTimeListener("newSource", prettyPrintSource);
+  const code = "" + function main() { let a = 1 + 3; let b = a++; return b + a; };
+  Cu.evalInSandbox(
+    code,
+    gDebuggee,
+    "1.8",
+    "data:text/javascript," + code);
+}
+
+function prettyPrintSource(event, { source }) {
+  gThreadClient.source(source).prettyPrint(4, testPrettyPrinted);
+}
+
+function testPrettyPrinted({ error, source}) {
+  do_check_true(!error);
+  do_check_true(source.contains("\n    "));
+  finishClient(gClient);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_pretty_print-02.js
@@ -0,0 +1,67 @@
+/* -*- Mode: javascript; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+var gSource;
+
+// Test stepping through pretty printed sources.
+
+function run_test() {
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-pretty-print");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTabAndResume(gClient, "test-pretty-print", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      evalCode();
+    });
+  });
+  do_test_pending();
+}
+
+const CODE = "" + function main() { debugger; return 10; };
+const CODE_URL = "data:text/javascript," + CODE;
+
+function evalCode() {
+  gClient.addOneTimeListener("newSource", prettyPrintSource);
+  Cu.evalInSandbox(
+    CODE,
+    gDebuggee,
+    "1.8",
+    CODE_URL,
+    1
+  );
+}
+
+function prettyPrintSource(event, { source }) {
+  gSource = source;
+  gThreadClient.source(gSource).prettyPrint(2, runCode);
+}
+
+function runCode({ error }) {
+  do_check_true(!error);
+  gClient.addOneTimeListener("paused", testDbgStatement);
+  gDebuggee.main();
+}
+
+function testDbgStatement(event, { frame }) {
+  const { url, line, column } = frame.where;
+  do_check_eq(url, CODE_URL);
+  do_check_eq(line, 2);
+  do_check_eq(column, 2);
+  testStepping();
+}
+
+function testStepping() {
+  gClient.addOneTimeListener("paused", (event, { frame }) => {
+    const { url, line, column } = frame.where;
+    do_check_eq(url, CODE_URL);
+    do_check_eq(line, 3);
+    do_check_eq(column, 2);
+    finishClient(gClient);
+  });
+  gThreadClient.stepIn();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_pretty_print-03.js
@@ -0,0 +1,74 @@
+/* -*- Mode: javascript; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test pretty printing source mapped sources.
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+var gSource;
+
+Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
+
+function run_test() {
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-pretty-print");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTabAndResume(gClient, "test-pretty-print", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      evalCode();
+    });
+  });
+  do_test_pending();
+}
+
+const dataUrl = s => "data:text/javascript," + s;
+
+const A = "function a(){b()}";
+const A_URL = dataUrl(A);
+const B = "function b(){debugger}";
+const B_URL = dataUrl(B);
+
+function evalCode() {
+  let { code, map } = (new SourceNode(null, null, null, [
+    new SourceNode(1, 0, A_URL, A),
+    B.split("").map((ch, i) => new SourceNode(1, i, B_URL, ch))
+  ])).toStringWithSourceMap({
+    file: "abc.js"
+  });
+
+  code += "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString());
+
+  gClient.addListener("newSource", waitForB);
+  Components.utils.evalInSandbox(code, gDebuggee, "1.8",
+                                 "http://example.com/foo.js", 1);
+}
+
+function waitForB(event, { source }) {
+  if (source.url !== B_URL) {
+    return;
+  }
+  gClient.removeListener("newSource", waitForB);
+  prettyPrint(source);
+}
+
+function prettyPrint(source) {
+  gThreadClient.source(source).prettyPrint(2, runCode);
+}
+
+function runCode({ error }) {
+  do_check_true(!error);
+  gClient.addOneTimeListener("paused", testDbgStatement);
+  gDebuggee.a();
+}
+
+function testDbgStatement(event, { frame, why }) {
+  do_check_eq(why.type, "debuggerStatement");
+  const { url, line, column } = frame.where;
+  do_check_eq(url, B_URL);
+  do_check_eq(line, 2);
+  do_check_eq(column, 2);
+  finishClient(gClient);
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -148,16 +148,19 @@ reason = bug 820380
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_pause_exceptions-02.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_longstringactor.js]
 [test_longstringgrips-01.js]
 [test_longstringgrips-02.js]
+[test_pretty_print-01.js]
+[test_pretty_print-02.js]
+[test_pretty_print-03.js]
 [test_source-01.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_breakpointstore.js]
 [test_profiler_actor.js]
 [test_profiler_activation.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
--- a/toolkit/devtools/sourcemap/SourceMap.jsm
+++ b/toolkit/devtools/sourcemap/SourceMap.jsm
@@ -485,16 +485,17 @@ define('source-map/util', ['require', 'e
       return aDefaultValue;
     } else {
       throw new Error('"' + aName + '" is a required argument.');
     }
   }
   exports.getArg = getArg;
 
   var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/;
+  var dataUrlRegexp = /^data:.+\,.+/;
 
   function urlParse(aUrl) {
     var match = aUrl.match(urlRegexp);
     if (!match) {
       return null;
     }
     return {
       scheme: match[1],
@@ -522,17 +523,17 @@ define('source-map/util', ['require', 'e
     }
     return url;
   }
   exports.urlGenerate = urlGenerate;
 
   function join(aRoot, aPath) {
     var url;
 
-    if (aPath.match(urlRegexp)) {
+    if (aPath.match(urlRegexp) || aPath.match(dataUrlRegexp)) {
       return aPath;
     }
 
     if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) {
       url.path = aPath;
       return urlGenerate(url);
     }
 
@@ -1077,34 +1078,37 @@ define('source-map/source-map-generator'
    *        If omitted, SourceMapConsumer's file property will be used.
    */
   SourceMapGenerator.prototype.applySourceMap =
     function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile) {
       // If aSourceFile is omitted, we will use the file property of the SourceMap
       if (!aSourceFile) {
         aSourceFile = aSourceMapConsumer.file;
       }
+
       var sourceRoot = this._sourceRoot;
       // Make "aSourceFile" relative if an absolute Url is passed.
       if (sourceRoot) {
         aSourceFile = util.relative(sourceRoot, aSourceFile);
       }
+
       // Applying the SourceMap can add and remove items from the sources and
       // the names array.
       var newSources = new ArraySet();
       var newNames = new ArraySet();
 
       // Find mappings for the "aSourceFile"
       this._mappings.forEach(function (mapping) {
         if (mapping.source === aSourceFile && mapping.original) {
           // Check if it can be mapped by the source map, then update the mapping.
           var original = aSourceMapConsumer.originalPositionFor({
             line: mapping.original.line,
             column: mapping.original.column
           });
+
           if (original.source !== null) {
             // Copy mapping
             if (sourceRoot) {
               mapping.source = util.relative(sourceRoot, original.source);
             } else {
               mapping.source = original.source;
             }
             mapping.original.line = original.line;
--- a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
+++ b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
@@ -254,16 +254,17 @@ define('lib/source-map/util', ['require'
       return aDefaultValue;
     } else {
       throw new Error('"' + aName + '" is a required argument.');
     }
   }
   exports.getArg = getArg;
 
   var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/;
+  var dataUrlRegexp = /^data:.+\,.+/;
 
   function urlParse(aUrl) {
     var match = aUrl.match(urlRegexp);
     if (!match) {
       return null;
     }
     return {
       scheme: match[1],
@@ -291,17 +292,17 @@ define('lib/source-map/util', ['require'
     }
     return url;
   }
   exports.urlGenerate = urlGenerate;
 
   function join(aRoot, aPath) {
     var url;
 
-    if (aPath.match(urlRegexp)) {
+    if (aPath.match(urlRegexp) || aPath.match(dataUrlRegexp)) {
       return aPath;
     }
 
     if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) {
       url.path = aPath;
       return urlGenerate(url);
     }