Bug 670002 - Use source maps in the web console w/ performance issues. r=jsantell
☠☠ backed out by c6e4b6a74469 ☠ ☠
authorJaideep Bhoosreddy <jaideepb@buffalo.edu>
Wed, 20 Jul 2016 00:40:00 -0400
changeset 305859 aab8baf2c5f54cf16b69b197094719fc0f0eae23
parent 305799 ed8e23b5e0c7b739e61173bb180cf3410a306679
child 305860 c6a1177a17e4cb0a3b0fc81a7d1e82de6d04e42a
push id30473
push usercbook@mozilla.com
push dateThu, 21 Jul 2016 14:23:24 +0000
treeherdermozilla-central@e28e856b9873 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell
bugs670002
milestone50.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
Bug 670002 - Use source maps in the web console w/ performance issues. r=jsantell
devtools/client/framework/location-store.js
devtools/client/framework/moz.build
devtools/client/framework/source-location.js
devtools/client/framework/source-map-service.js
devtools/client/framework/toolbox.js
devtools/client/preferences/devtools.js
devtools/client/shared/components/frame.js
devtools/client/webconsole/webconsole.js
devtools/server/actors/utils/TabSources.js
devtools/server/actors/webbrowser.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/location-store.js
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const SOURCE_TOKEN = "<:>";
+
+function LocationStore (store) {
+  this._store = store || new Map();
+}
+
+/**
+ * Method to get a promised location from the Store.
+ * @param location
+ * @returns Promise<Object>
+ */
+LocationStore.prototype.get = function (location) {
+  this._safeAccessInit(location.url);
+  return this._store.get(location.url).get(location);
+};
+
+/**
+ * Method to set a promised location to the Store
+ * @param location
+ * @param promisedLocation
+ */
+LocationStore.prototype.set = function (location, promisedLocation = null) {
+  this._safeAccessInit(location.url);
+  this._store.get(location.url).set(serialize(location), promisedLocation);
+};
+
+/**
+ * Utility method to verify if key exists in Store before accessing it.
+ * If not, initializing it.
+ * @param url
+ * @private
+ */
+LocationStore.prototype._safeAccessInit = function (url) {
+  if (!this._store.has(url)) {
+    this._store.set(url, new Map());
+  }
+};
+
+/**
+ * Utility proxy method to Map.clear() method
+ */
+LocationStore.prototype.clear = function () {
+  this._store.clear();
+};
+
+/**
+ * Retrieves an object containing all locations to be resolved when `source-updated`
+ * event is triggered.
+ * @param url
+ * @returns {Array<String>}
+ */
+LocationStore.prototype.getByURL = function (url){
+  if (this._store.has(url)) {
+    return [...this._store.get(url).keys()];
+  }
+  return [];
+};
+
+/**
+ * Invalidates the stale location promises from the store when `source-updated`
+ * event is triggered, and when FrameView unsubscribes from a location.
+ * @param url
+ */
+LocationStore.prototype.clearByURL = function (url) {
+  this._safeAccessInit(url);
+  this._store.set(url, new Map());
+};
+
+exports.LocationStore = LocationStore;
+exports.serialize = serialize;
+exports.deserialize = deserialize;
+
+/**
+ * Utility method to serialize the source
+ * @param source
+ * @returns {string}
+ */
+function serialize(source) {
+  let { url, line, column } = source;
+  line = line || 0;
+  column = column || 0;
+  return `${url}${SOURCE_TOKEN}${line}${SOURCE_TOKEN}${column}`;
+};
+
+/**
+ * Utility method to serialize the source
+ * @param source
+ * @returns Object
+ */
+function deserialize(source) {
+  let [ url, line, column ] = source.split(SOURCE_TOKEN);
+  line = parseInt(line);
+  column = parseInt(column);
+  if (column === 0) {
+    return { url, line };
+  }
+  return { url, line, column };
+};
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -11,21 +11,22 @@ TEST_HARNESS_FILES.xpcshell.devtools.cli
 
 DevToolsModules(
     'about-devtools-toolbox.js',
     'attach-thread.js',
     'browser-menus.js',
     'devtools-browser.js',
     'devtools.js',
     'gDevTools.jsm',
+    'location-store.js',
     'menu-item.js',
     'menu.js',
     'selection.js',
     'sidebar.js',
-    'source-location.js',
+    'source-map-service.js',
     'target-from-url.js',
     'target.js',
     'toolbox-highlighter-utils.js',
     'toolbox-hosts.js',
     'toolbox-options.js',
     'toolbox.js',
     'ToolboxProcess.jsm',
 )
rename from devtools/client/framework/source-location.js
rename to devtools/client/framework/source-map-service.js
--- a/devtools/client/framework/source-location.js
+++ b/devtools/client/framework/source-map-service.js
@@ -1,98 +1,171 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { Task } = require("devtools/shared/task");
-const { assert } = require("devtools/shared/DevToolsUtils");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { LocationStore, serialize, deserialize } = require("./location-store");
 
 /**
  * A manager class that wraps a TabTarget and listens to source changes
  * from source maps and resolves non-source mapped locations to the source mapped
  * versions and back and forth, and creating smart elements with a location that
  * auto-update when the source changes (from pretty printing, source maps loading, etc)
  *
  * @param {TabTarget} target
  */
-function SourceLocationController(target) {
-  this.target = target;
-  this.locations = new Set();
+
+function SourceMapService(target) {
+  this._target = target;
+  this._locationStore = new LocationStore();
+  this._isInitialResolve = true;
+
+  EventEmitter.decorate(this);
 
   this._onSourceUpdated = this._onSourceUpdated.bind(this);
+  this._resolveLocation = this._resolveLocation.bind(this);
+  this._resolveAndUpdate = this._resolveAndUpdate.bind(this);
+  this.subscribe = this.subscribe.bind(this);
+  this.unsubscribe = this.unsubscribe.bind(this);
   this.reset = this.reset.bind(this);
   this.destroy = this.destroy.bind(this);
 
   target.on("source-updated", this._onSourceUpdated);
   target.on("navigate", this.reset);
   target.on("will-navigate", this.reset);
   target.on("close", this.destroy);
 }
 
-SourceLocationController.prototype.reset = function () {
-  this.locations.clear();
+/**
+ * Clears the store containing the cached resolved locations and promises
+ */
+SourceMapService.prototype.reset = function () {
+  this._isInitialResolve = true;
+  this._locationStore.clear();
+};
+
+SourceMapService.prototype.destroy = function () {
+  this.reset();
+  this._target.off("source-updated", this._onSourceUpdated);
+  this._target.off("navigate", this.reset);
+  this._target.off("will-navigate", this.reset);
+  this._target.off("close", this.destroy);
+  this._isInitialResolve = null;
+  this._target = this._locationStore = null;
 };
 
-SourceLocationController.prototype.destroy = function () {
-  this.locations.clear();
-  this.target.off("source-updated", this._onSourceUpdated);
-  this.target.off("navigate", this.reset);
-  this.target.off("will-navigate", this.reset);
-  this.target.off("close", this.destroy);
-  this.target = this.locations = null;
+/**
+ * Sets up listener for the callback to update the FrameView and tries to resolve location
+ * @param location
+ * @param callback
+ */
+SourceMapService.prototype.subscribe = function (location, callback) {
+  this.on(serialize(location), callback);
+  this._locationStore.set(location);
+  if (this._isInitialResolve) {
+    this._resolveAndUpdate(location);
+    this._isInitialResolve = false;
+  }
+};
+
+/**
+ * Removes the listener for the location and clears cached locations
+ * @param location
+ * @param callback
+ */
+SourceMapService.prototype.unsubscribe = function (location, callback) {
+  this.off(serialize(location), callback);
+  this._locationStore.clearByURL(location.url);
 };
 
 /**
- * Add this `location` to be observed and register a callback
- * whenever the underlying source is updated.
- *
- * @param {Object} location
- *        An object with a {String} url, {Number} line, and optionally
- *        a {Number} column.
- * @param {Function} callback
+ * Tries to resolve the location and if successful,
+ * emits the resolved location and caches it
+ * @param location
+ * @private
  */
-SourceLocationController.prototype.bindLocation = function (location, callback) {
-  assert(location.url, "Location must have a url.");
-  assert(location.line, "Location must have a line.");
-  this.locations.add({ location, callback });
+SourceMapService.prototype._resolveAndUpdate = function (location) {
+  this._resolveLocation(location).then(resolvedLocation => {
+    // We try to source map the first console log to initiate the source-updated event from
+    // target. The isSameLocation check is to make sure we don't update the frame, if the
+    // location is not source-mapped.
+    if (resolvedLocation) {
+      if (this._isInitialResolve) {
+        if (!isSameLocation(location, resolvedLocation)) {
+          this.emit(serialize(location), location, resolvedLocation);
+          return;
+        }
+      }
+      this.emit(serialize(location), location, resolvedLocation);
+    }
+  });
 };
 
 /**
- * Called when a new source occurs (a normal source, source maps) or an updated
- * source (pretty print) occurs.
- *
- * @param {String} eventName
- * @param {Object} sourceEvent
+ * Validates the location model,
+ * checks if there is existing promise to resolve location, if so returns cached promise
+ * if not promised to resolve,
+ * tries to resolve location and returns a promised location
+ * @param location
+ * @return Promise<Object>
+ * @private
  */
-SourceLocationController.prototype._onSourceUpdated = function (_, sourceEvent) {
+SourceMapService.prototype._resolveLocation = Task.async(function* (location) {
+  // Location must have a url and a line
+  if (!location.url || !location.line) {
+    return null;
+  }
+  const cachedLocation = this._locationStore.get(location);
+  if (cachedLocation) {
+    return cachedLocation;
+  } else {
+    const promisedLocation = resolveLocation(this._target, location);
+    if (promisedLocation) {
+      this._locationStore.set(location, promisedLocation);
+      return promisedLocation;
+    }
+  }
+});
+
+/**
+ * Checks if the `source-updated` event is fired from the target.
+ * Checks to see if location store has the source url in its cache,
+ * if so, tries to update each stale location in the store.
+ * @param _
+ * @param sourceEvent
+ * @private
+ */
+SourceMapService.prototype._onSourceUpdated = function (_, sourceEvent) {
   let { type, source } = sourceEvent;
   // If we get a new source, and it's not a source map, abort;
-  // we can ahve no actionable updates as this is just a new normal source.
+  // we can have no actionable updates as this is just a new normal source.
   // Also abort if there's no `url`, which means it's unsourcemappable anyway,
   // like an eval script.
   if (!source.url || type === "newSource" && !source.isSourceMapped) {
     return;
   }
-
-  for (let locationItem of this.locations) {
-    if (isSourceRelated(locationItem.location, source)) {
-      this._updateSource(locationItem);
+  let sourceUrl = null;
+  if (source.generatedUrl && source.isSourceMapped) {
+    sourceUrl = source.generatedUrl;
+  } else if (source.url && source.isPrettyPrinted) {
+    sourceUrl = source.url;
+  }
+  const locationsToResolve = this._locationStore.getByURL(sourceUrl);
+  if (locationsToResolve.length) {
+    this._locationStore.clearByURL(sourceUrl);
+    for (let location of locationsToResolve) {
+      this._resolveAndUpdate(deserialize(location));
     }
   }
 };
 
-SourceLocationController.prototype._updateSource = Task.async(function* (locationItem) {
-  let newLocation = yield resolveLocation(this.target, locationItem.location);
-  if (newLocation) {
-    let previousLocation = Object.assign({}, locationItem.location);
-    Object.assign(locationItem.location, newLocation);
-    locationItem.callback(previousLocation, newLocation);
-  }
-});
+exports.SourceMapService = SourceMapService;
 
 /**
  * Take a TabTarget and a location, containing a `url`, `line`, and `column`, resolve
  * the location to the latest location (so a source mapped location, or if pretty print
  * status has been updated)
  *
  * @param {TabTarget} target
  * @param {Object} location
@@ -100,38 +173,28 @@ SourceLocationController.prototype._upda
  */
 function resolveLocation(target, location) {
   return Task.spawn(function* () {
     let newLocation = yield target.resolveLocation({
       url: location.url,
       line: location.line,
       column: location.column || Infinity
     });
-
     // Source or mapping not found, so don't do anything
     if (newLocation.error) {
       return null;
     }
 
     return newLocation;
   });
 }
 
 /**
- * Takes a serialized SourceActor form and returns a boolean indicating
- * if this source is related to this location, like if a location is a generated source,
- * and the source map is loaded subsequently, the new source mapped SourceActor
- * will be considered related to this location. Same with pretty printing new sources.
- *
- * @param {Object} location
- * @param {Object} source
- * @return {Boolean}
+ * Returns if the original location and resolved location are the same
+ * @param location
+ * @param resolvedLocation
+ * @returns {boolean}
  */
-function isSourceRelated(location, source) {
-         // Mapping location to subsequently loaded source map
-  return source.generatedUrl === location.url ||
-         // Mapping source map loc to source map
-         source.url === location.url;
-}
-
-exports.SourceLocationController = SourceLocationController;
-exports.resolveLocation = resolveLocation;
-exports.isSourceRelated = isSourceRelated;
+function isSameLocation(location, resolvedLocation) {
+  return location.url === resolvedLocation.url &&
+    location.line === resolvedLocation.line &&
+    location.column === resolvedLocation.column;
+};
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -6,16 +6,17 @@
 
 const MAX_ORDINAL = 99;
 const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
 const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
 const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER";
 const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER";
 const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
 const HTML_NS = "http://www.w3.org/1999/xhtml";
+const { SourceMapService } = require("./source-map-service");
 
 var {Cc, Ci, Cu} = require("chrome");
 var promise = require("promise");
 var defer = require("devtools/shared/defer");
 var Services = require("Services");
 var {Task} = require("devtools/shared/task");
 var {gDevTools} = require("devtools/client/framework/devtools");
 var EventEmitter = require("devtools/shared/event-emitter");
@@ -113,16 +114,19 @@ const ToolboxButtons = exports.ToolboxBu
  *        Type of host that will host the toolbox (e.g. sidebar, window)
  * @param {object} hostOptions
  *        Options for host specifically
  */
 function Toolbox(target, selectedTool, hostType, hostOptions) {
   this._target = target;
   this._toolPanels = new Map();
   this._telemetry = new Telemetry();
+  if (Services.prefs.getBoolPref("devtools.sourcemap.locations.enabled")) {
+    this._sourceMapService = new SourceMapService(this._target);
+  }
 
   this._initInspector = null;
   this._inspector = null;
 
   // Map of frames (id => frame-info) and currently selected frame id.
   this.frameMap = new Map();
   this.selectedFrameId = null;
 
@@ -2027,16 +2031,21 @@ Toolbox.prototype = {
 
     gDevTools.off("tool-registered", this._toolRegistered);
     gDevTools.off("tool-unregistered", this._toolUnregistered);
 
     gDevTools.off("pref-changed", this._prefChanged);
 
     this._lastFocusedElement = null;
 
+    if (this._sourceMapService) {
+      this._sourceMapService.destroy();
+      this._sourceMapService = null;
+    }
+
     if (this.webconsolePanel) {
       this._saveSplitConsoleHeight();
       this.webconsolePanel.removeEventListener("resize",
         this._saveSplitConsoleHeight);
     }
     this.closeButton.removeEventListener("click", this.destroy, true);
     this.textboxContextMenuPopup.removeEventListener("popupshowing",
       this._updateTextboxMenuItems, true);
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -291,16 +291,19 @@ pref("devtools.webconsole.timestampMessa
 
 // Web Console automatic multiline mode: |true| if you want incomplete statements
 // to automatically trigger multiline editing (equivalent to shift + enter).
 pref("devtools.webconsole.autoMultiline", true);
 
 // Enable the experimental webconsole frontend (work in progress)
 pref("devtools.webconsole.new-frontend-enabled", false);
 
+// Enable the experimental support for source maps in console (work in progress)
+pref("devtools.sourcemap.locations.enabled", false);
+
 // The number of lines that are displayed in the web console.
 pref("devtools.hud.loglimit", 1000);
 
 // The number of lines that are displayed in the web console for the Net,
 // CSS, JS and Web Developer categories. These defaults should be kept in sync
 // with DEFAULT_LOG_LIMIT in the webconsole frontend.
 pref("devtools.hud.loglimit.network", 1000);
 pref("devtools.hud.loglimit.cssparser", 1000);
--- a/devtools/client/shared/components/frame.js
+++ b/devtools/client/shared/components/frame.js
@@ -29,59 +29,135 @@ module.exports = createClass({
     // Option to display a function name even if it's anonymous.
     showAnonymousFunctionName: PropTypes.bool,
     // Option to display a host name after the source link.
     showHost: PropTypes.bool,
     // Option to display a host name if the filename is empty or just '/'
     showEmptyPathAsHost: PropTypes.bool,
     // Option to display a full source instead of just the filename.
     showFullSourceUrl: PropTypes.bool,
+    // Service to enable the source map feature for console.
+    sourceMapService: PropTypes.object,
   },
 
   getDefaultProps() {
     return {
       showFunctionName: false,
       showAnonymousFunctionName: false,
       showHost: false,
       showEmptyPathAsHost: false,
       showFullSourceUrl: false,
     };
   },
 
+  componentWillMount() {
+    const sourceMapService = this.props.sourceMapService;
+    if (sourceMapService) {
+      const source = this.getSource();
+      sourceMapService.subscribe(source, this.onSourceUpdated);
+    }
+  },
+
+  componentWillUnmount() {
+    const sourceMapService = this.props.sourceMapService;
+    if (sourceMapService) {
+      const source = this.getSource();
+      sourceMapService.unsubscribe(source, this.onSourceUpdated);
+    }
+  },
+
+  /**
+   * Component method to update the FrameView when a resolved location is available
+   * @param event
+   * @param location
+   */
+  onSourceUpdated(event, location, resolvedLocation) {
+    const frame = this.getFrame(resolvedLocation);
+    this.setState({
+      frame,
+      isSourceMapped: true,
+    });
+  },
+
+  /**
+   * Utility method to convert the Frame object to the
+   * Source Object model required by SourceMapService
+   * @param frame
+   * @returns {{url: *, line: *, column: *}}
+   */
+  getSource(frame) {
+    frame = frame || this.props.frame;
+    const { source, line, column } = frame;
+    return {
+      url: source,
+      line,
+      column,
+    };
+  },
+
+  /**
+   * Utility method to convert the Source object model to the
+   * Frame object model required by FrameView class.
+   * @param source
+   * @returns {{source: *, line: *, column: *, functionDisplayName: *}}
+   */
+  getFrame(source) {
+    const { url, line, column } = source;
+    return {
+      source: url,
+      line,
+      column,
+      functionDisplayName: this.props.frame.functionDisplayName,
+    };
+  },
+
   render() {
+    let frame, isSourceMapped;
     let {
       onClick,
-      frame,
       showFunctionName,
       showAnonymousFunctionName,
       showHost,
       showEmptyPathAsHost,
       showFullSourceUrl
     } = this.props;
 
+    if (this.state && this.state.isSourceMapped) {
+      frame = this.state.frame;
+      isSourceMapped = this.state.isSourceMapped;
+    } else {
+      frame = this.props.frame;
+    }
+
     let source = frame.source ? String(frame.source) : "";
     let line = frame.line != void 0 ? Number(frame.line) : null;
     let column = frame.column != void 0 ? Number(frame.column) : null;
 
     const { short, long, host } = getSourceNames(source);
     // Reparse the URL to determine if we should link this; `getSourceNames`
     // has already cached this indirectly. We don't want to attempt to
     // link to "self-hosted" and "(unknown)". However, we do want to link
     // to Scratchpad URIs.
-    const isLinkable = !!(isScratchpadScheme(source) || parseURL(source));
+    // Source mapped sources might not necessary linkable, but they
+    // are still valid in the debugger.
+    const isLinkable = !!(isScratchpadScheme(source) || parseURL(source)) || isSourceMapped;
     const elements = [];
     const sourceElements = [];
     let sourceEl;
 
     let tooltip = long;
+
+    // If the source is linkable and line > 0
+    const shouldDisplayLine = isLinkable && line;
+
     // Exclude all falsy values, including `0`, as even
     // a number 0 for line doesn't make sense, and should not be displayed.
     // If source isn't linkable, don't attempt to append line and column
     // info, as this probably doesn't make sense.
-    if (isLinkable && line) {
+    if (shouldDisplayLine) {
       tooltip += `:${line}`;
       // Intentionally exclude 0
       if (column) {
         tooltip += `:${column}`;
       }
     }
 
     let attributes = {
@@ -99,26 +175,35 @@ module.exports = createClass({
         elements.push(
           dom.span({ className: "frame-link-function-display-name" },
             functionDisplayName)
         );
       }
     }
 
     let displaySource = showFullSourceUrl ? long : short;
-    if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) {
+    // SourceMapped locations might not be parsed properly by parseURL.
+    // Eg: sourcemapped location could be /folder/file.coffee instead of a url
+    // and so the url parser would not parse non-url locations properly
+    // Check for "/" in displaySource. If "/" is in displaySource, take everything after last "/".
+    if (isSourceMapped) {
+      displaySource = displaySource.lastIndexOf("/") < 0 ?
+        displaySource :
+        displaySource.slice(displaySource.lastIndexOf("/") + 1);
+    } else if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) {
       displaySource = host;
+
     }
 
     sourceElements.push(dom.span({
       className: "frame-link-filename",
     }, displaySource));
 
     // If source is linkable, and we have a line number > 0
-    if (isLinkable && line) {
+    if (shouldDisplayLine) {
       let lineInfo = `:${line}`;
       // Add `data-line` attribute for testing
       attributes["data-line"] = line;
 
       // Intentionally exclude 0
       if (column) {
         lineInfo += `:${column}`;
         // Add `data-column` attribute for testing
@@ -129,17 +214,17 @@ module.exports = createClass({
     }
 
     // If source is not a URL (self-hosted, eval, etc.), don't make
     // it an anchor link, as we can't link to it.
     if (isLinkable) {
       sourceEl = dom.a({
         onClick: e => {
           e.preventDefault();
-          onClick(frame);
+          onClick(this.getSource(frame));
         },
         href: source,
         className: "frame-link-source",
         draggable: false,
         title: l10n.getFormatStr("frame.viewsourceindebugger", tooltip)
       }, sourceElements);
     } else {
       sourceEl = dom.span({
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -2526,55 +2526,61 @@ WebConsoleFrame.prototype = {
     locationNode.className = "message-location devtools-monospace";
 
     if (!url) {
       url = "";
     }
 
     let fullURL = url.split(" -> ").pop();
     // Make the location clickable.
-    let onClick = () => {
+    let onClick = ({ url, line }) => {
       let category = locationNode.closest(".message").category;
       let target = null;
 
       if (/^Scratchpad\/\d+$/.test(url)) {
         target = "scratchpad";
       } else if (category === CATEGORY_CSS) {
         target = "styleeditor";
       } else if (category === CATEGORY_JS || category === CATEGORY_WEBDEV) {
         target = "jsdebugger";
-      } else if (/\.js$/.test(fullURL)) {
+      } else if (/\.js$/.test(url)) {
         // If it ends in .js, let's attempt to open in debugger
         // anyway, as this falls back to normal view-source.
         target = "jsdebugger";
+      } else {
+        // Point everything else to debugger, if source not available,
+        // it will fall back to view-source.
+        target = "jsdebugger";
       }
 
       switch (target) {
         case "scratchpad":
           this.owner.viewSourceInScratchpad(url, line);
           return;
         case "jsdebugger":
-          this.owner.viewSourceInDebugger(fullURL, line);
+          this.owner.viewSourceInDebugger(url, line);
           return;
         case "styleeditor":
-          this.owner.viewSourceInStyleEditor(fullURL, line);
+          this.owner.viewSourceInStyleEditor(url, line);
           return;
       }
       // No matching tool found; use old school view-source
-      this.owner.viewSource(fullURL, line);
+      this.owner.viewSource(url, line);
     };
 
+    const toolbox = gDevTools.getToolbox(this.owner.target);
     this.ReactDOM.render(this.FrameView({
       frame: {
         source: fullURL,
         line,
         column
       },
       showEmptyPathAsHost: true,
       onClick,
+      sourceMapService: toolbox ? toolbox._sourceMapService : null,
     }), locationNode);
 
     return locationNode;
   },
 
   /**
    * Adjusts the category and severity of the given message.
    *
--- a/devtools/server/actors/utils/TabSources.js
+++ b/devtools/server/actors/utils/TabSources.js
@@ -240,17 +240,17 @@ TabSources.prototype = {
         }
       }
 
       if (url in this._sourceMappedSourceActors) {
         return this._sourceMappedSourceActors[url];
       }
     }
 
-    throw new Error("getSourceByURL: could not find source for " + url);
+    throw new Error("getSourceActorByURL: could not find source for " + url);
     return null;
   },
 
   /**
    * Returns true if the URL likely points to a minified resource, false
    * otherwise.
    *
    * @param String aURL
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -2086,51 +2086,47 @@ TabActor.prototype = {
    * @param {String} request.url
    * @param {Number} request.line
    * @param {Number?} request.column
    * @return {Promise<Object>}
    */
   onResolveLocation(request) {
     let { url, line } = request;
     let column = request.column || 0;
-    let actor = this.sources.getSourceActorByURL(url);
-
-    if (actor) {
-      // Get the generated source actor if this is source mapped
-      let generatedActor = actor.generatedSource ?
-        this.sources.createNonSourceMappedActor(actor.generatedSource) :
-        actor;
-      let generatedLocation = new GeneratedLocation(
-        generatedActor, line, column);
+    const scripts = this.threadActor.dbg.findScripts({ url });
 
-      return this.sources.getOriginalLocation(generatedLocation).then(loc => {
-        // If no map found, return this packet
-        if (loc.originalLine == null) {
-          return {
-            from: this.actorID,
-            type: "resolveLocation",
-            error: "MAP_NOT_FOUND"
-          };
-        }
+    if (!scripts[0] || !scripts[0].source) {
+      return promise.resolve({
+        from: this.actorID,
+        type: "resolveLocation",
+        error: "SOURCE_NOT_FOUND"
+      });
+    }
+    const source = scripts[0].source;
+    const generatedActor = this.sources.createNonSourceMappedActor(source);
+    let generatedLocation = new GeneratedLocation(
+      generatedActor, line, column);
 
-        loc = loc.toJSON();
+    return this.sources.getOriginalLocation(generatedLocation).then(loc => {
+      // If no map found, return this packet
+      if (loc.originalLine == null) {
         return {
           from: this.actorID,
-          url: loc.source.url,
-          column: loc.column,
-          line: loc.line
+          type: "resolveLocation",
+          error: "MAP_NOT_FOUND"
         };
-      });
-    }
+      }
 
-    // Fall back to this packet when source is not found
-    return promise.resolve({
-      from: this.actorID,
-      type: "resolveLocation",
-      error: "SOURCE_NOT_FOUND"
+      loc = loc.toJSON();
+      return {
+        from: this.actorID,
+        url: loc.source.url,
+        column: loc.column,
+        line: loc.line
+      };
     });
   },
 };
 
 /**
  * The request types this actor can handle.
  */
 TabActor.prototype.requestTypes = {