Lazily parse source maps (#768)

- Trim down `sources.dart` such that it is only concerned with reading JS / Dart sources
- Create new `ModuleMetaData` abstraction which contains helpful methods for getting modules and their corresponding Dart sources and vice versa
- Create new `LocationMetaData` abstraction that contains helpful methods for getting location data fro Dart / JS scripts
- Update logic to lazily process source maps, thus corresponding calls are now asynchronous
- Remove `Debugger` from `Inspector` abstraction and replace it with `LocationMetaData`
- Clean up tests

Closes https://github.com/dart-lang/webdev/issues/749
diff --git a/dwds/lib/src/debugging/debugger.dart b/dwds/lib/src/debugging/debugger.dart
index f4a13c7..6711027 100644
--- a/dwds/lib/src/debugging/debugger.dart
+++ b/dwds/lib/src/debugging/debugger.dart
@@ -8,8 +8,6 @@
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
     hide StackTrace;
 
-import '../../asset_handler.dart';
-import '../../dwds.dart' show LogWriter;
 import '../services/chrome_proxy_service.dart';
 import '../utilities/dart_uri.dart';
 import '../utilities/domain.dart';
@@ -18,6 +16,7 @@
 import '../utilities/wrapped_service.dart';
 import 'dart_scope.dart';
 import 'location.dart';
+import 'modules.dart';
 import 'remote_debugger.dart';
 import 'sources.dart';
 
@@ -31,31 +30,32 @@
   'unhandled': PauseState.uncaught,
 };
 
+/// Paths to black box in the Chrome debugger.
+const _pathsToBlackBox = {'/packages/stack_trace/'};
+
 class Debugger extends Domain {
   static final logger = Logger('Debugger');
 
-  final AssetHandler _assetHandler;
-  final LogWriter _logWriter;
   final RemoteDebugger _remoteDebugger;
 
   /// The root URI from which the application is served.
   final String _root;
   final StreamNotify _streamNotify;
+  final Sources _sources;
+  final Modules _modules;
+  final Locations _locations;
 
   Debugger._(
-    this._assetHandler,
     this._remoteDebugger,
     this._streamNotify,
     AppInspectorProvider provider,
-    // TODO(401) - Remove.
+    this._sources,
+    this._modules,
+    this._locations,
     this._root,
-    this._logWriter,
   )   : _breakpoints = _Breakpoints(provider),
         super(provider);
 
-  /// The scripts and sourcemaps for the application, both JS and Dart.
-  Sources sources;
-
   /// The breakpoints we have set so far, indexable by either
   /// Dart or JS ID.
   final _Breakpoints _breakpoints;
@@ -149,33 +149,36 @@
   }
 
   static Future<Debugger> create(
-    AssetHandler assetHandler,
     RemoteDebugger remoteDebugger,
     StreamNotify streamNotify,
     AppInspectorProvider appInspectorProvider,
+    Sources sources,
+    Modules modules,
+    Locations locations,
     String root,
-    LogWriter logWriter,
   ) async {
     var debugger = Debugger._(
-      assetHandler,
       remoteDebugger,
       streamNotify,
       appInspectorProvider,
-      // TODO(401) - Remove.
+      sources,
+      modules,
+      locations,
       root,
-      logWriter,
     );
     await debugger._initialize();
     return debugger;
   }
 
   Future<Null> _initialize() async {
-    sources = Sources(_assetHandler, _remoteDebugger, _logWriter, _root);
     // We must add a listener before enabling the debugger otherwise we will
     // miss events.
     // Allow a null debugger/connection for unit tests.
     runZoned(() {
-      _remoteDebugger?.onScriptParsed?.listen(sources.scriptParsed);
+      _remoteDebugger?.onScriptParsed?.listen((e) {
+        _blackBoxIfNecessary(e.script);
+        _modules.noteModule(e.script.url, e.script.scriptId);
+      });
       _remoteDebugger?.onPaused?.listen(_pauseHandler);
       _remoteDebugger?.onResumed?.listen(_resumeHandler);
     }, onError: (e, StackTrace s) {
@@ -186,6 +189,50 @@
     handleErrorIfPresent(await _remoteDebugger?.enable() as WipResponse);
   }
 
+  /// Black boxes the Dart SDK and paths in [_pathsToBlackBox].
+  Future<void> _blackBoxIfNecessary(WipScript script) async {
+    if (script.url.endsWith('dart_sdk.js')) {
+      await _blackBoxSdk(script);
+    } else if (_pathsToBlackBox.any(script.url.contains)) {
+      var content =
+          await _sources.readAssetOrNull(DartUri(script.url).serverPath);
+      if (content == null) return;
+      var lines = content.split('\n');
+      await _blackBoxRanges(script.scriptId, [lines.length]);
+    }
+  }
+
+  /// Black boxes the SDK excluding the range which includes exception logic.
+  Future<void> _blackBoxSdk(WipScript script) async {
+    var content =
+        await _sources.readAssetOrNull(DartUri(script.url).serverPath);
+    if (content == null) return;
+    var sdkSourceLines = content.split('\n');
+    // TODO(grouma) - Find a more robust way to identify this location.
+    var throwIndex = sdkSourceLines.indexWhere(
+        (line) => line.contains('dart.throw = function throw_(exception) {'));
+    if (throwIndex != -1) {
+      await _blackBoxRanges(script.scriptId, [throwIndex, throwIndex + 6]);
+    }
+  }
+
+  Future<void> _blackBoxRanges(String scriptId, List<int> lineNumbers) async {
+    try {
+      await _remoteDebugger
+          .sendCommand('Debugger.setBlackboxedRanges', params: {
+        'scriptId': scriptId,
+        'positions': [
+          {'lineNumber': 0, 'columnNumber': 0},
+          for (var line in lineNumbers) {'lineNumber': line, 'columnNumber': 0},
+        ]
+      });
+    } catch (_) {
+      // Attempting to set ranges immediately after a refresh can cause issues
+      // as the corresponding script will no longer exist. Silently ignore
+      // these failures.
+    }
+  }
+
   /// Resumes the Isolate from start.
   ///
   /// The JS VM is technically not paused at the start of the Isolate so there
@@ -205,7 +252,7 @@
     checkIsolate(isolateId);
     var dartScript = await inspector.scriptWithId(scriptId);
     var dartUri = DartUri(dartScript.uri, _root);
-    var location = _locationForDart(dartUri, line);
+    var location = await _locations.locationForDart(dartUri, line);
     // TODO: Handle cases where a breakpoint can't be set exactly at that line.
     if (location == null) {
       // ignore: only_throw_errors
@@ -287,32 +334,16 @@
     handleErrorIfPresent(response);
   }
 
-  /// Find the [Location] for the given Dart source position.
-  ///
-  /// The [line] number is 1-based.
-  Location _locationForDart(DartUri uri, int line) => sources
-      .locationsForDart(uri.serverPath)
-      .firstWhere((location) => location.dartLocation.line == line,
-          orElse: () => null);
-
-  /// Find the [Location] for the given JS source position.
-  ///
-  /// The [line] number is 1-based.
-  Location _locationForJs(String scriptId, int line) => sources
-      .locationsForJs(scriptId)
-      .firstWhere((location) => location.jsLocation.line == line,
-          orElse: () => null);
-
   /// Returns source [Location] for the paused event.
   ///
   /// If we do not have [Location] data for the embedded JS location, null is
   /// returned.
-  Location _sourceLocation(DebuggerPausedEvent e) {
+  Future<Location> _sourceLocation(DebuggerPausedEvent e) {
     var frame = e.params['callFrames'][0];
     var location = frame['location'];
     var jsLocation = JsLocation.fromZeroBased(location['scriptId'] as String,
         location['lineNumber'] as int, location['columnNumber'] as int);
-    return _locationForJs(jsLocation.scriptId, jsLocation.line);
+    return _locations.locationForJs(jsLocation.scriptId, jsLocation.line);
   }
 
   /// Translates Chrome callFrames contained in [DebuggerPausedEvent] into Dart
@@ -393,7 +424,7 @@
     // TODO(sdk/issues/37240) - ideally we look for an exact location instead
     // of the closest location on a given line.
     Location bestLocation;
-    for (var location in sources.locationsForJs(jsLocation.scriptId)) {
+    for (var location in await _locations.locationsForJs(jsLocation.scriptId)) {
       if (location.jsLocation.line == jsLocation.line) {
         bestLocation ??= location;
         if ((location.jsLocation.column - jsLocation.column).abs() <
@@ -448,7 +479,7 @@
           isolate: inspector.isolateRef);
     } else {
       // If we don't have source location continue stepping.
-      if (_isStepping && _sourceLocation(e) == null) {
+      if (_isStepping && (await _sourceLocation(e)) == null) {
         await _remoteDebugger.sendCommand('Debugger.stepInto');
         return;
       }
diff --git a/dwds/lib/src/debugging/inspector.dart b/dwds/lib/src/debugging/inspector.dart
index de8bad5..255c31c 100644
--- a/dwds/lib/src/debugging/inspector.dart
+++ b/dwds/lib/src/debugging/inspector.dart
@@ -7,6 +7,7 @@
 import 'dart:io';
 
 import 'package:dwds/src/connections/app_connection.dart';
+import 'package:dwds/src/debugging/location.dart';
 import 'package:dwds/src/debugging/remote_debugger.dart';
 import 'package:path/path.dart' as p;
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
@@ -18,7 +19,6 @@
 import '../utilities/domain.dart';
 import '../utilities/shared.dart';
 import '../utilities/wrapped_service.dart';
-import 'debugger.dart';
 import 'exceptions.dart';
 import 'instance.dart';
 import 'metadata.dart';
@@ -54,7 +54,7 @@
 
   final RemoteDebugger _remoteDebugger;
   final AssetHandler _assetHandler;
-  final Debugger debugger;
+  final Locations _locations;
   final Isolate isolate;
   final IsolateRef isolateRef;
   final InstanceHelper instanceHelper;
@@ -67,7 +67,7 @@
     this.appConnection,
     this.isolate,
     this._assetHandler,
-    this.debugger,
+    this._locations,
     this._root,
     this._remoteDebugger,
     this.instanceHelper,
@@ -98,9 +98,10 @@
       AppConnection appConnection,
       RemoteDebugger remoteDebugger,
       AssetHandler assetHandler,
-      Debugger debugger,
+      Locations locations,
       String root,
-      InstanceHelper instanceHelper) async {
+      InstanceHelper instanceHelper,
+      String pauseMode) async {
     var id = createId();
     var time = DateTime.now().millisecondsSinceEpoch;
     var name = '$root:main()';
@@ -118,14 +119,13 @@
         livePorts: 0,
         libraries: [],
         breakpoints: [],
-        exceptionPauseMode: debugger.pauseState)
+        exceptionPauseMode: pauseMode)
       ..extensionRPCs = [];
-    debugger.notifyPausedAtStart();
     var inspector = AppInspector._(
       appConnection,
       isolate,
       assetHandler,
-      debugger,
+      locations,
       root,
       remoteDebugger,
       instanceHelper,
@@ -452,7 +452,7 @@
       library: _libraryRefs[libraryId],
       id: scriptRef.id,
     )
-      ..tokenPosTable = debugger.sources.tokenPosTableFor(serverPath)
+      ..tokenPosTable = await _locations.tokenPosTableFor(serverPath)
       ..source = script;
   }
 
diff --git a/dwds/lib/src/debugging/location.dart b/dwds/lib/src/debugging/location.dart
index 0b7778c..9432e73 100644
--- a/dwds/lib/src/debugging/location.dart
+++ b/dwds/lib/src/debugging/location.dart
@@ -2,9 +2,15 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'package:source_maps/parser.dart';
+import 'dart:async';
 
+import 'package:path/path.dart' as p;
+import 'package:source_maps/parser.dart';
+import 'package:source_maps/source_maps.dart';
+
+import '../debugging/sources.dart';
 import '../utilities/dart_uri.dart';
+import 'modules.dart';
 
 var _startTokenId = 1337;
 
@@ -85,3 +91,161 @@
   static JsLocation fromOneBased(String scriptId, int line, int column) =>
       JsLocation._(scriptId, line, column);
 }
+
+/// Contains meta data for known [Location]s.
+class Locations {
+  /// Map from Dart server path to all corresponding [Location] data.
+  final _sourceToLocation = <String, Set<Location>>{};
+
+  /// Map from JS scriptId to all corresponding [Location] data.
+  final _scriptIdToLocation = <String, Set<Location>>{};
+
+  /// Map from Dart server path to tokenPosTable as defined in the
+  /// Dart VM Service Protocol:
+  /// https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#script
+  final _sourceToTokenPosTable = <String, List<List<int>>>{};
+
+  /// The set of all known [Location]s for a module.
+  final _moduleToLocations = <String, Set<Location>>{};
+
+  /// Set of all modules for which the corresponding source map has been
+  /// processed.
+  final _processedModules = <String>{};
+
+  final Sources _sources;
+  final Modules _modules;
+  final String _root;
+
+  Locations(this._sources, this._modules, this._root);
+
+  /// Clears all location meta data.
+  void clearCache() {
+    _sourceToTokenPosTable.clear();
+    _scriptIdToLocation.clear();
+    _sourceToLocation.clear();
+    _moduleToLocations.clear();
+    _processedModules.clear();
+  }
+
+  /// Returns all [Location] data for a provided Dart source.
+  Future<Set<Location>> locationsForDart(String serverPath) async {
+    var module = await _modules.moduleForSource(serverPath);
+    var cache = _sourceToLocation[serverPath];
+    if (cache != null) return cache;
+
+    for (var location in await _locationsForModule(module)) {
+      noteLocation(location.dartLocation.uri.serverPath, location,
+          location.jsLocation.scriptId);
+    }
+
+    return _sourceToLocation[serverPath] ?? {};
+  }
+
+  /// Returns all [Location] data for a provided JS scriptId.
+  Future<Set<Location>> locationsForJs(String scriptId) async {
+    var module = await _modules.moduleForScriptId(scriptId);
+
+    var cache = _scriptIdToLocation[scriptId];
+    if (cache != null) return cache;
+
+    for (var location in await _locationsForModule(module)) {
+      noteLocation(location.dartLocation.uri.serverPath, location,
+          location.jsLocation.scriptId);
+    }
+
+    return _scriptIdToLocation[scriptId] ?? {};
+  }
+
+  /// Find the [Location] for the given Dart source position.
+  ///
+  /// The [line] number is 1-based.
+  Future<Location> locationForDart(DartUri uri, int line) async =>
+      (await locationsForDart(uri.serverPath)).firstWhere(
+          (location) => location.dartLocation.line == line,
+          orElse: () => null);
+
+  /// Find the [Location] for the given JS source position.
+  ///
+  /// The [line] number is 1-based.
+  Future<Location> locationForJs(String scriptId, int line) async =>
+      (await locationsForJs(scriptId)).firstWhere(
+          (location) => location.jsLocation.line == line,
+          orElse: () => null);
+
+  /// Note [location] meta data.
+  void noteLocation(
+      String dartServerPath, Location location, String wipScriptId) {
+    _sourceToLocation.putIfAbsent(dartServerPath, () => Set()).add(location);
+    _scriptIdToLocation.putIfAbsent(wipScriptId, () => Set()).add(location);
+  }
+
+  /// Returns the tokenPosTable for the provided Dart script path as defined
+  /// in:
+  /// https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#script
+  Future<List<List<int>>> tokenPosTableFor(String serverPath) async {
+    var tokenPosTable = _sourceToTokenPosTable[serverPath];
+    if (tokenPosTable != null) return tokenPosTable;
+    // Construct the tokenPosTable which is of the form:
+    // [lineNumber, (tokenId, columnNumber)*]
+    tokenPosTable = <List<int>>[];
+    var locations = await locationsForDart(serverPath);
+    var lineNumberToLocation = <int, Set<Location>>{};
+    for (var location in locations) {
+      lineNumberToLocation
+          .putIfAbsent(location.dartLocation.line, () => Set())
+          .add(location);
+    }
+    for (var lineNumber in lineNumberToLocation.keys) {
+      tokenPosTable.add([
+        lineNumber,
+        for (var location in lineNumberToLocation[lineNumber]) ...[
+          location.tokenPos,
+          location.dartLocation.column
+        ]
+      ]);
+    }
+    _sourceToTokenPosTable[serverPath] = tokenPosTable;
+    return tokenPosTable;
+  }
+
+  /// Returns all known [Location]s for the provided [module].
+  ///
+  /// [module] refers to the JS path of a DDC module without the extension.
+  Future<Set<Location>> _locationsForModule(String module) async {
+    if (_moduleToLocations[module] != null) return _moduleToLocations[module];
+    var result = <Location>{};
+    if (module?.isEmpty ?? true) return _moduleToLocations[module] = result;
+    var moduleExtension = await _modules.moduleExtension;
+    var modulePath = '$module$moduleExtension';
+    var sourceMapContents = await _sources.readAssetOrNull('$modulePath.map');
+    var scriptLocation = p.url.dirname('/$modulePath');
+    if (sourceMapContents == null) return result;
+    var scriptId = await _modules.scriptIdForModule(module);
+    if (scriptId == null) return result;
+    // This happens to be a [SingleMapping] today in DDC.
+    var mapping = parse(sourceMapContents);
+    if (mapping is SingleMapping) {
+      // Create TokenPos for each entry in the source map.
+      for (var lineEntry in mapping.lines) {
+        for (var entry in lineEntry.entries) {
+          var index = entry.sourceUrlId;
+          if (index == null) continue;
+          // Source map URLS are relative to the script. They may have platform separators
+          // or they may use URL semantics. To be sure, we split and re-join them.
+          // This works on Windows because path treats both / and \ as separators.
+          // It will fail if the path has both separators in it.
+          var relativeSegments = p.split(mapping.urls[index]);
+          var path = p.url.joinAll([scriptLocation, ...relativeSegments]);
+          var dartUri = DartUri(path, _root);
+          result.add(Location.from(
+            scriptId,
+            lineEntry,
+            entry,
+            dartUri,
+          ));
+        }
+      }
+    }
+    return _moduleToLocations[module] = result;
+  }
+}
diff --git a/dwds/lib/src/debugging/modules.dart b/dwds/lib/src/debugging/modules.dart
new file mode 100644
index 0000000..f43f9c5
--- /dev/null
+++ b/dwds/lib/src/debugging/modules.dart
@@ -0,0 +1,122 @@
+// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:path/path.dart' as p;
+
+import '../services/chrome_proxy_service.dart';
+import '../utilities/dart_uri.dart';
+import 'remote_debugger.dart';
+
+/// Contains meta data and helpful methods for DDC modules.
+class Modules {
+  final String _root;
+  final RemoteDebugger _remoteDebugger;
+  // The Dart server path to containing module.
+  final _sourceToModule = <String, String>{};
+
+  // The Chrome script ID to corresponding module.
+  final _scriptIdToModule = <String, String>{};
+
+  // The module to corresponding Chrome script ID.
+  final _moduleToScriptId = <String, String>{};
+
+  final _moduleExtensionCompleter = Completer<String>();
+
+  var _initializedCompleter = Completer();
+
+  Modules(this._remoteDebugger, this._root);
+
+  /// Completes with the module extension i.e. `.ddc.js` or `.ddk.js`.
+  ///
+  /// We use the script parsed events from Chrome to determine this information.
+  // TODO(grouma) - Do something better here.
+  Future<String> get moduleExtension => _moduleExtensionCompleter.future;
+
+  /// Initializes the mapping from source to module.
+  ///
+  /// Intended to be called multiple times throughout the development workflow,
+  /// e.g. after a hot-reload.
+  void initialize() {
+    _initializedCompleter = Completer();
+    _initializeMapping();
+  }
+
+  /// Returns the module for the Chrome script ID.
+  Future<String> moduleForScriptId(String scriptId) async =>
+      _scriptIdToModule[scriptId];
+
+  /// Returns the Chrome script ID for the provided module.
+  Future<String> scriptIdForModule(String module) async =>
+      _moduleToScriptId[module];
+
+  /// Returns the containing module for the provided Dart server path.
+  Future<String> moduleForSource(String serverPath) async {
+    await _initializedCompleter.future;
+    return _sourceToModule[serverPath];
+  }
+
+  /// Checks if the [url] correspond to a module and stores meta data.
+  Future<Null> noteModule(String url, String scriptId) async {
+    if (url == null || !(url.endsWith('.ddc.js') || url.endsWith('.ddk.js'))) {
+      return;
+    }
+
+    // TODO(grouma) - This is wonky. Find a better way.
+    if (!_moduleExtensionCompleter.isCompleted) {
+      if (url.endsWith('.ddc.js')) {
+        _moduleExtensionCompleter.complete('.ddc.js');
+      } else {
+        _moduleExtensionCompleter.complete('.ddk.js');
+      }
+    }
+
+    // Remove the DDC extension (e.g. .ddc.js) from the path.
+    var module = p
+        .withoutExtension(p.withoutExtension(Uri.parse(url).path))
+        .substring(1);
+
+    _scriptIdToModule[scriptId] = module;
+    _moduleToScriptId[module] = scriptId;
+  }
+
+  /// Initializes [_sourceToModule].
+  Future<void> _initializeMapping() async {
+    var expression = '''
+    (function() {
+          var dart = require('dart_sdk').dart;
+          var result = {};
+          dart.getModuleNames().forEach(function(module){
+            Object.keys(dart.getModuleLibraries(module)).forEach(
+              function(script){
+                result[script] =  '/' + module;
+            });
+          });
+          return result;
+      })();
+      ''';
+    var response = await _remoteDebugger.sendCommand('Runtime.evaluate',
+        params: {'expression': expression, 'returnByValue': true});
+    handleErrorIfPresent(response);
+    var value = response.result['result']['value'] as Map<String, dynamic>;
+    for (var dartScript in value.keys) {
+      if (!dartScript.endsWith('.dart')) continue;
+      var scriptUri = Uri.parse(dartScript);
+      var moduleUri = Uri.parse(value[dartScript] as String);
+      // The module uris returned by the expression contain the root. Rewrite
+      // the uris so that DartUri properly accounts for this fact.
+      if (scriptUri.scheme == 'org-dartlang-app') {
+        moduleUri = moduleUri.replace(scheme: 'org-dartlang-app');
+      } else if (scriptUri.scheme == 'package') {
+        moduleUri = moduleUri.replace(
+            scheme: 'package', path: moduleUri.path.split('/packages/').last);
+      }
+      // TODO(grouma) - handle G3 scheme.
+      _sourceToModule[DartUri(dartScript, _root).serverPath] =
+          DartUri(moduleUri.toString()).serverPath;
+    }
+    _initializedCompleter.complete();
+  }
+}
diff --git a/dwds/lib/src/debugging/sources.dart b/dwds/lib/src/debugging/sources.dart
index d4d8d66..a5eabc9 100644
--- a/dwds/lib/src/debugging/sources.dart
+++ b/dwds/lib/src/debugging/sources.dart
@@ -6,157 +6,25 @@
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:dwds/dwds.dart';
 import 'package:http/http.dart';
 import 'package:logging/logging.dart';
-import 'package:path/path.dart' as p;
-import 'package:source_maps/source_maps.dart';
-import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
 import '../../asset_handler.dart';
 import '../../dwds.dart' show LogWriter;
-import '../utilities/dart_uri.dart';
-import 'location.dart';
-import 'remote_debugger.dart';
 
-/// The scripts and sourcemaps for the application, both JS and Dart.
+/// Handles reading both Dart and JS sources for the running application.
 class Sources {
-  /// Map from Dart server path to tokenPosTable as defined in the
-  /// Dart VM Service Protocol:
-  /// https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#script
-  final _sourceToTokenPosTable = <String, List<List<int>>>{};
-
-  /// Map from Dart server path to all corresponding [Location] data.
-  final _sourceToLocation = <String, Set<Location>>{};
-
-  /// Map from JS scriptId to all corresponding [Location] data.
-  final _scriptIdToLocation = <String, Set<Location>>{};
-
-  /// Map from JS source url to corresponding Dart server paths.
-  final _sourceToServerPaths = <String, Set<String>>{};
-
-  /// Map from JS source url to Chrome script ID.
-  final _sourceToScriptId = <String, String>{};
-
-  /// Paths to black box in the Chrome debugger.
-  final _pathsToBlackBox = {'/packages/stack_trace/'};
-
-  /// Use `_readAssetOrNull` instead of using this directly, as it handles
-  /// logging unsuccessful responses.
   final AssetHandler _assetHandler;
-
   final LogWriter _logWriter;
 
-  final RemoteDebugger _remoteDebugger;
-
-  final String _root;
-
-  Sources(
-      this._assetHandler, this._remoteDebugger, this._logWriter, this._root);
-
-  /// Returns all [Location] data for a provided Dart source.
-  Set<Location> locationsForDart(String serverPath) =>
-      _sourceToLocation[serverPath] ?? {};
-
-  /// Returns all [Location] data for a provided JS scriptId.
-  Set<Location> locationsForJs(String scriptId) =>
-      _scriptIdToLocation[scriptId] ?? {};
-
-  /// Called to handle the event that a script has been parsed
-  /// and add its sourcemap information.
-  Future<Null> scriptParsed(ScriptParsedEvent e) async {
-    var script = e.script;
-    // TODO(grouma) - This should be configurable.
-    await _blackBoxIfNecessary(script);
-    _clearCacheFor(script);
-    _sourceToScriptId[script.url] = script.scriptId;
-    var sourceMapContents = await _sourceMapOrNull(script);
-    if (sourceMapContents == null) return;
-    var scriptLocation = p.url.dirname(Uri.parse(script.url).path);
-    // This happens to be a [SingleMapping] today in DDC.
-    var mapping = parse(sourceMapContents);
-    if (mapping is SingleMapping) {
-      var serverPaths =
-          _sourceToServerPaths.putIfAbsent(script.url, () => Set());
-      // Create TokenPos for each entry in the source map.
-      for (var lineEntry in mapping.lines) {
-        for (var entry in lineEntry.entries) {
-          var index = entry.sourceUrlId;
-          if (index == null) continue;
-          // Source map URLS are relative to the script. They may have platform separators
-          // or they may use URL semantics. To be sure, we split and re-join them.
-          // This works on Windows because path treats both / and \ as separators.
-          // It will fail if the path has both separators in it.
-          var relativeSegments = p.split(mapping.urls[index]);
-          var path = p.url.joinAll([scriptLocation, ...relativeSegments]);
-          var dartUri = DartUri(path, _root);
-          var location = Location.from(
-            script.scriptId,
-            lineEntry,
-            entry,
-            dartUri,
-          );
-          serverPaths.add(dartUri.serverPath);
-          noteLocation(dartUri.serverPath, location, script.scriptId);
-        }
-      }
-    }
-  }
-
-  /// Add [location] to our lookups for both the Dart and JS scripts.
-  void noteLocation(
-      String dartServerPath, Location location, String wipScriptId) {
-    _sourceToLocation.putIfAbsent(dartServerPath, () => Set()).add(location);
-    _scriptIdToLocation.putIfAbsent(wipScriptId, () => Set()).add(location);
-  }
-
-  /// Returns the tokenPosTable for the provided Dart script path as defined
-  /// in:
-  /// https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#script
-  List<List<int>> tokenPosTableFor(String serverPath) {
-    var tokenPosTable = _sourceToTokenPosTable[serverPath];
-    if (tokenPosTable != null) return tokenPosTable;
-    // Construct the tokenPosTable which is of the form:
-    // [lineNumber, (tokenId, columnNumber)*]
-    tokenPosTable = <List<int>>[];
-    var locations = _sourceToLocation[serverPath] ?? {};
-    var lineNumberToLocation = <int, Set<Location>>{};
-    for (var location in locations) {
-      lineNumberToLocation
-          .putIfAbsent(location.dartLocation.line, () => Set())
-          .add(location);
-    }
-    for (var lineNumber in lineNumberToLocation.keys) {
-      tokenPosTable.add([
-        lineNumber,
-        for (var location in lineNumberToLocation[lineNumber]) ...[
-          location.tokenPos,
-          location.dartLocation.column
-        ]
-      ]);
-    }
-    _sourceToTokenPosTable[serverPath] = tokenPosTable;
-    return tokenPosTable;
-  }
-
-  void _clearCacheFor(WipScript script) {
-    var serverPaths = _sourceToServerPaths[script.url] ?? {};
-    for (var serverPath in serverPaths) {
-      _sourceToLocation.remove(serverPath);
-      _sourceToTokenPosTable.remove(serverPath);
-    }
-    // This is the previous script ID for the file with the same URL.
-    var scriptId = _sourceToScriptId[script.url];
-    _scriptIdToLocation.remove(scriptId);
-
-    _sourceToServerPaths.remove(script.url);
-    _sourceToScriptId.remove(script.url);
-  }
+  Sources(this._assetHandler, this._logWriter);
 
   /// Reads an asset at [path] relative to the server root.
   ///
   /// Returns `null` and logs the response if the status is anything other than
   /// [HttpStatus.ok].
-  Future<String> _readAssetOrNull(String path) async {
+  Future<String> readAssetOrNull(String path) async {
     var response = await _assetHandler.getRelativeAsset(path);
     var responseText = '';
     var hasError = false;
@@ -170,74 +38,17 @@
       return responseText;
     } else {
       _logWriter(Level.WARNING, '''
-Failed to load asset at path: $path.
+      Failed to load asset at path: $path.
 
-Status code: ${response.statusCode}
+      Status code: ${response.statusCode}
 
-Headers:
-${const JsonEncoder.withIndent('  ').convert(response.headers)}
+      Headers:
+      ${const JsonEncoder.withIndent('  ').convert(response.headers)}
 
-Content:
-$responseText}
-''');
+      Content:
+      $responseText}
+      ''');
       return null;
     }
   }
-
-  /// The source map for a DDC-compiled JS [script].
-  ///
-  /// Returns `null` and logs if it can't be read.
-  Future<String> _sourceMapOrNull(WipScript script) {
-    var sourceMapUrl = script.sourceMapURL;
-    if (sourceMapUrl == null ||
-        !(sourceMapUrl.endsWith('.ddc.js.map') ||
-            sourceMapUrl.endsWith('.ddk.js.map'))) {
-      return null;
-    }
-    var scriptPath = DartUri(script.url).serverPath;
-    var sourcemapPath = p.url.join(p.url.dirname(scriptPath), sourceMapUrl);
-    return _readAssetOrNull(sourcemapPath);
-  }
-
-  /// Black boxes the Dart SDK and paths in [_pathsToBlackBox].
-  Future<void> _blackBoxIfNecessary(WipScript script) async {
-    if (script.url.endsWith('dart_sdk.js')) {
-      await _blackBoxSdk(script);
-    } else if (_pathsToBlackBox.any((path) => script.url.contains(path))) {
-      var content = await _readAssetOrNull(DartUri(script.url).serverPath);
-      if (content == null) return;
-      var lines = content.split('\n');
-      await _blackBoxRanges(script.scriptId, [lines.length]);
-    }
-  }
-
-  /// Black boxes the SDK excluding the range which includes exception logic.
-  Future<void> _blackBoxSdk(WipScript script) async {
-    var content = await _readAssetOrNull(DartUri(script.url).serverPath);
-    if (content == null) return;
-    var sdkSourceLines = content.split('\n');
-    // TODO(grouma) - Find a more robust way to identify this location.
-    var throwIndex = sdkSourceLines.indexWhere(
-        (line) => line.contains('dart.throw = function throw_(exception) {'));
-    if (throwIndex != -1) {
-      await _blackBoxRanges(script.scriptId, [throwIndex, throwIndex + 6]);
-    }
-  }
-
-  Future<void> _blackBoxRanges(String scriptId, List<int> lineNumbers) async {
-    try {
-      await _remoteDebugger
-          .sendCommand('Debugger.setBlackboxedRanges', params: {
-        'scriptId': scriptId,
-        'positions': [
-          {'lineNumber': 0, 'columnNumber': 0},
-          for (var line in lineNumbers) {'lineNumber': line, 'columnNumber': 0},
-        ]
-      });
-    } catch (_) {
-      // Attempting to set ranges immediately after a refresh can cause issues
-      // as the corresponding script will no longer exist. Silently ignore
-      // these failures.
-    }
-  }
 }
diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart
index 5ffe410..1796714 100644
--- a/dwds/lib/src/services/chrome_proxy_service.dart
+++ b/dwds/lib/src/services/chrome_proxy_service.dart
@@ -6,7 +6,6 @@
 import 'dart:convert';
 import 'dart:io';
 
-import 'package:dwds/src/debugging/instance.dart';
 import 'package:pedantic/pedantic.dart';
 import 'package:pub_semver/pub_semver.dart' as semver;
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
@@ -16,7 +15,11 @@
 import '../connections/app_connection.dart';
 import '../debugging/debugger.dart';
 import '../debugging/inspector.dart';
+import '../debugging/instance.dart';
+import '../debugging/location.dart';
+import '../debugging/modules.dart';
 import '../debugging/remote_debugger.dart';
+import '../debugging/sources.dart';
 import '../utilities/dart_uri.dart';
 import '../utilities/shared.dart';
 import '../utilities/wrapped_service.dart';
@@ -32,8 +35,6 @@
 
 /// A proxy from the chrome debug protocol to the dart vm service protocol.
 class ChromeProxyService implements VmServiceInterface {
-  final LogWriter _logWriter;
-
   /// Cache of all existing StreamControllers.
   ///
   /// These are all created through [onEvent].
@@ -52,12 +53,16 @@
 
   final RemoteDebugger remoteDebugger;
 
+  /// Provides debugger-related functionality.
+  Future<Debugger> get debugger => _debuggerCompleter.future;
+
   final AssetHandler _assetHandler;
 
-  final _debuggerCompleter = Completer<Debugger>();
+  final Locations _locations;
 
-  /// Provides debugger-related functionality.
-  Future<Debugger> get _debugger => _debuggerCompleter.future;
+  final Modules _modules;
+
+  final _debuggerCompleter = Completer<Debugger>();
 
   AppInspector _inspector;
 
@@ -73,15 +78,18 @@
     this.uri,
     this._assetHandler,
     this.remoteDebugger,
-    this._logWriter,
+    Sources sources,
+    this._modules,
+    this._locations,
   ) {
     _debuggerCompleter.complete(Debugger.create(
-      _assetHandler,
       remoteDebugger,
       _streamNotify,
       appInspectorProvider,
+      sources,
+      _modules,
+      _locations,
       uri,
-      _logWriter,
     ));
   }
 
@@ -98,8 +106,11 @@
       ..name = 'ChromeDebugProxy'
       ..startTime = DateTime.now().millisecondsSinceEpoch
       ..version = Platform.version;
+    var modules = Modules(remoteDebugger, tabUrl);
+    var sources = Sources(assetHandler, logWriter);
+    var locations = Locations(sources, modules, tabUrl);
     var service = ChromeProxyService._(
-        vm, tabUrl, assetHandler, remoteDebugger, logWriter);
+        vm, tabUrl, assetHandler, remoteDebugger, sources, modules, locations);
     unawaited(service.createIsolate(appConnection));
     return service;
   }
@@ -115,20 +126,25 @@
           'Cannot create multiple isolates for the same app');
     }
 
+    _locations.clearCache();
+    _modules.initialize();
+    (await debugger).notifyPausedAtStart();
+
     var instanceHelper =
-        InstanceHelper(await _debugger, remoteDebugger, appInspectorProvider);
+        InstanceHelper(await debugger, remoteDebugger, appInspectorProvider);
 
     _inspector = await AppInspector.initialize(
       appConnection,
       remoteDebugger,
       _assetHandler,
-      await _debugger,
+      _locations,
       uri,
       instanceHelper,
+      (await debugger).pauseState,
     );
 
     unawaited(appConnection.onStart.then((_) async {
-      await (await _debugger).resumeFromStart();
+      await (await debugger).resumeFromStart();
     }));
 
     var isolateRef = _inspector.isolateRef;
@@ -190,8 +206,7 @@
   @override
   Future<Breakpoint> addBreakpoint(String isolateId, String scriptId, int line,
           {int column}) async =>
-      (await _debugger)
-          .addBreakpoint(isolateId, scriptId, line, column: column);
+      (await debugger).addBreakpoint(isolateId, scriptId, line, column: column);
 
   @override
   Future<Breakpoint> addBreakpointAtEntry(String isolateId, String functionId) {
@@ -204,7 +219,7 @@
       {int column}) async {
     var dartUri = DartUri(scriptUri, uri);
     var ref = await _inspector.scriptRefFor(dartUri.serverPath);
-    return (await _debugger)
+    return (await debugger)
         .addBreakpoint(isolateId, ref.id, line, column: column);
   }
 
@@ -314,7 +329,7 @@
   /// Returns null if the corresponding isolate is not paused.
   @override
   Future<Stack> getStack(String isolateId) async =>
-      (await _debugger).getStack(isolateId);
+      (await debugger).getStack(isolateId);
 
   @override
   Future<VM> getVM() async {
@@ -394,7 +409,7 @@
   }
 
   @override
-  Future<Success> pause(String isolateId) async => (await _debugger).pause();
+  Future<Success> pause(String isolateId) async => (await debugger).pause();
 
   @override
   Future<Success> registerService(String service, String alias) async {
@@ -410,13 +425,13 @@
   @override
   Future<Success> removeBreakpoint(
           String isolateId, String breakpointId) async =>
-      (await _debugger).removeBreakpoint(isolateId, breakpointId);
+      (await debugger).removeBreakpoint(isolateId, breakpointId);
 
   @override
   Future<Success> resume(String isolateId,
       {String step, int frameIndex}) async {
     if (_inspector.appConnection.isStarted) {
-      return await (await _debugger)
+      return await (await debugger)
           .resume(isolateId, step: step, frameIndex: frameIndex);
     } else {
       _inspector.appConnection.runMain();
@@ -426,7 +441,7 @@
 
   @override
   Future<Success> setExceptionPauseMode(String isolateId, String mode) async =>
-      (await _debugger).setExceptionPauseMode(isolateId, mode);
+      (await debugger).setExceptionPauseMode(isolateId, mode);
 
   @override
   Future<Success> setFlag(String name, String value) {
diff --git a/dwds/test/debugger_test.dart b/dwds/test/debugger_test.dart
index b0ac91e..51e8018 100644
--- a/dwds/test/debugger_test.dart
+++ b/dwds/test/debugger_test.dart
@@ -4,10 +4,12 @@
 
 @TestOn('vm')
 import 'dart:async';
+
 import 'package:dwds/dwds.dart' show ModuleStrategy;
 import 'package:dwds/src/debugging/debugger.dart';
 import 'package:dwds/src/debugging/inspector.dart';
 import 'package:dwds/src/debugging/location.dart';
+import 'package:dwds/src/debugging/modules.dart';
 import 'package:dwds/src/utilities/dart_uri.dart';
 import 'package:dwds/src/utilities/shared.dart';
 import 'package:source_maps/parser.dart';
@@ -24,6 +26,7 @@
 Debugger debugger;
 FakeWebkitDebugger webkitDebugger;
 StreamController<DebuggerPausedEvent> pausedController;
+Locations locations;
 
 void main() async {
   setUpAll(() async {
@@ -31,15 +34,17 @@
     webkitDebugger = FakeWebkitDebugger();
     pausedController = StreamController<DebuggerPausedEvent>();
     webkitDebugger.onPaused = pausedController.stream;
+    var root = 'fakeRoot';
+    var modules = Modules(webkitDebugger, root);
+    locations = Locations(null, modules, root);
     debugger = await Debugger.create(
-      null,
       webkitDebugger,
       null,
       () => inspector,
-      'fakeRoot',
-      (level, message) {
-        printOnFailure('[$level]: $message');
-      },
+      null,
+      null,
+      locations,
+      root,
     );
     inspector = FakeInspector();
   });
@@ -63,7 +68,7 @@
     );
     // Create a single location in the JS script the location in our hard-coded
     // frame.
-    debugger.sources.noteLocation('dart', location, '69');
+    locations.noteLocation('dart', location, '69');
 
     var frames = await debugger.dartFramesFor(frames1);
     expect(frames, isNotNull);
diff --git a/dwds/test/debugging/sources_test.dart b/dwds/test/debugging/sources_test.dart
index 41806c7..c3de98d 100644
--- a/dwds/test/debugging/sources_test.dart
+++ b/dwds/test/debugging/sources_test.dart
@@ -7,7 +7,6 @@
 import 'package:logging/logging.dart';
 import 'package:shelf/shelf.dart';
 import 'package:test/test.dart';
-import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
 void main() {
   test('Gracefully handles missing source maps', () async {
@@ -17,15 +16,9 @@
       sourcePath: '',
     };
     var logs = <LogRecord>[];
-    var sources = Sources(TestingAssetHandler(assets), null,
-        (level, message) => logs.add(LogRecord(level, message, '')), '');
-    var serverUri = 'http://localhost:1234/';
-    await sources.scriptParsed(ScriptParsedEvent(WipEvent({
-      'params': {
-        'url': '$serverUri$sourcePath',
-        'sourceMapURL': '$serverUri$sourceMapPath'
-      }
-    })));
+    var sources = Sources(TestingAssetHandler(assets),
+        (level, message) => logs.add(LogRecord(level, message, '')));
+    await sources.readAssetOrNull(sourceMapPath);
     expect(
         logs,
         contains(predicate((LogRecord log) =>
diff --git a/dwds/test/inspector_test.dart b/dwds/test/inspector_test.dart
index 71b1492..512c998 100644
--- a/dwds/test/inspector_test.dart
+++ b/dwds/test/inspector_test.dart
@@ -4,8 +4,9 @@
 
 @TestOn('vm')
 import 'package:dwds/src/connections/debug_connection.dart';
-import 'package:dwds/src/utilities/conversions.dart';
+import 'package:dwds/src/debugging/debugger.dart';
 import 'package:dwds/src/debugging/inspector.dart';
+import 'package:dwds/src/utilities/conversions.dart';
 import 'package:dwds/src/utilities/shared.dart';
 import 'package:test/test.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
@@ -19,12 +20,13 @@
 
 void main() {
   AppInspector inspector;
+  Debugger debugger;
 
   setUpAll(() async {
     await context.setUp();
-    // TODO(alanknight): A nicer way of getting the inspector.
-    inspector =
-        fetchChromeProxyService(context.debugConnection).appInspectorProvider();
+    var service = fetchChromeProxyService(context.debugConnection);
+    debugger = await service.debugger;
+    inspector = service.appInspectorProvider();
   });
 
   tearDownAll(() async {
@@ -62,8 +64,7 @@
 
   test('properties', () async {
     var remoteObject = await libraryPublicFinal();
-    var properties =
-        await inspector.debugger.getProperties(remoteObject.objectId);
+    var properties = await debugger.getProperties(remoteObject.objectId);
     var names =
         properties.map((p) => p.name).where((x) => x != '__proto__').toList();
     var expected = [
diff --git a/dwds/test/instance_test.dart b/dwds/test/instance_test.dart
index e0bbe6c..9e01f26 100644
--- a/dwds/test/instance_test.dart
+++ b/dwds/test/instance_test.dart
@@ -30,7 +30,7 @@
     var chromeProxyService = fetchChromeProxyService(context.debugConnection);
     inspector = chromeProxyService.appInspectorProvider();
     remoteDebugger = chromeProxyService.remoteDebugger;
-    debugger = inspector.debugger;
+    debugger = await chromeProxyService.debugger;
     instanceHelper = InstanceHelper(
         debugger, remoteDebugger, chromeProxyService.appInspectorProvider);
   });
diff --git a/dwds/test/variable_scope_test.dart b/dwds/test/variable_scope_test.dart
index 3cdffde..1b65bc3 100644
--- a/dwds/test/variable_scope_test.dart
+++ b/dwds/test/variable_scope_test.dart
@@ -101,7 +101,7 @@
 
     test('evaluateJsOnCallFrame', () async {
       stack = await breakAt('nestedFunction', mainScript);
-      var debugger = service.appInspectorProvider().debugger;
+      var debugger = await service.debugger;
       var parameter = await debugger.evaluateJsOnCallFrameIndex(0, 'parameter');
       expect(parameter.value, matches(RegExp(r'\d+ world')));
       var ticks = await debugger.evaluateJsOnCallFrameIndex(1, 'ticks');