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');