Initial support of getStack (#435)
* Initial support of getStack
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index e277380..a69df8c 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.3.3
+
+- Add support for `getScript` for paused isolates.
+
## 0.3.2
- Add support for `scope` in `evaluate` calls.
diff --git a/dwds/example/hello_world/main.dart b/dwds/example/hello_world/main.dart
index 6583235..9c7a2b7 100644
--- a/dwds/example/hello_world/main.dart
+++ b/dwds/example/hello_world/main.dart
@@ -41,6 +41,10 @@
});
};
+ Timer.periodic(Duration(seconds: 1), (_) {
+ printCount();
+ });
+
// Register one up front before the proxy connects, the isolate should still
// recognize this as an available extension.
registerExtension('ext.hello_world.existing', (_, __) => null);
@@ -48,6 +52,13 @@
window.console.debug('Page Ready');
}
+var count = 0;
+
+// An easy location to add a breakpoint.
+void printCount() {
+ print('The count is ${++count}');
+}
+
String helloString(String response) => response;
bool helloBool(bool response) => response;
num helloNum(num response) => response;
diff --git a/dwds/lib/src/chrome_proxy_service.dart b/dwds/lib/src/chrome_proxy_service.dart
index 6b3d1bd..b61e198 100644
--- a/dwds/lib/src/chrome_proxy_service.dart
+++ b/dwds/lib/src/chrome_proxy_service.dart
@@ -14,6 +14,7 @@
import 'dart_uri.dart';
import 'debugger.dart';
import 'helpers.dart';
+import 'location.dart';
/// A proxy from the chrome debug protocol to the dart vm service protocol.
class ChromeProxyService implements VmServiceInterface {
@@ -43,11 +44,19 @@
/// Provides debugger-related functionality.
Debugger debugger;
+ /// The current Dart stack for the paused [_isolate].
+ ///
+ /// This is null if the [_isolate] is not paused.
+ Stack _pausedStack;
+
/// Fields that are specific to the current [_isolate].
///
/// These need to get cleared whenever [destroyIsolate] is called.
+ // TODO(grouma) - Subclass Isolate to group these together. This will ensure
+ // that they are properly cleaned up and documented.
final _classes = <String, Class>{};
final _scriptRefs = <String, ScriptRef>{};
+ final _serverPathToScriptRef = <String, ScriptRef>{};
final _libraries = <String, Library>{};
final _libraryRefs = <String, LibraryRef>{};
StreamSubscription<ConsoleAPIEvent> _consoleSubscription;
@@ -183,8 +192,10 @@
_vm.isolates.removeWhere((ref) => ref.id == _isolate.id);
_isolate = null;
+ _pausedStack = null;
_classes.clear();
_scriptRefs.clear();
+ _serverPathToScriptRef.clear();
_libraries.clear();
_libraryRefs.clear();
_consoleSubscription.cancel();
@@ -474,6 +485,8 @@
..id = createId();
_scriptRefs[scriptRef.id] = scriptRef;
+ _serverPathToScriptRef[DartUri(libraryRef.id, scriptRef.uri).serverPath] =
+ scriptRef;
return Library()
..id = libraryRef.id
@@ -523,6 +536,9 @@
return ScriptList()..scripts = scripts;
}
+ /// Returns the [ScriptRef] for the provided Dart server path [uri].
+ ScriptRef scriptRefFor(String uri) => _serverPathToScriptRef[uri];
+
Future<Script> _getScript(String isolateId, ScriptRef scriptRef) async {
var libraryId = scriptRef.uri;
// TODO(401): Remove uri parameter.
@@ -560,10 +576,12 @@
throw UnimplementedError();
}
+ /// Returns the current stack.
+ ///
+ /// Returns null if the corresponding isolate is not paused.
@override
- Future<Stack> getStack(String isolateId) {
- throw UnimplementedError();
- }
+ Future<Stack> getStack(String isolateId) async =>
+ isolateId == _isolate?.id ? _pausedStack : null;
@override
Future<VM> getVM() => Future.value(_vm);
@@ -771,6 +789,28 @@
return controller;
}
+ List<Frame> _dartFramesFor(DebuggerPausedEvent e) {
+ var dartFrames = <Frame>[];
+ var index = 0;
+ for (var frame in e.params['callFrames']) {
+ var location = frame['location'];
+ // TODO(grouma) - This function name is JS based. Add logic to
+ // translate this to a Dart function name.
+ var functionName = frame['functionName'] as String ?? '';
+ // Chrome is 0 based. Account for this.
+ var jsLocation = JsLocation.fromZeroBased(location['scriptId'] as String,
+ location['lineNumber'] as int, location['columnNumber'] as int);
+ var dartFrame = debugger.frameFor(jsLocation);
+ if (dartFrame != null) {
+ dartFrame.code.name =
+ functionName.isEmpty ? '(anonymous)' : functionName;
+ dartFrame.index = index++;
+ dartFrames.add(dartFrame);
+ }
+ }
+ return dartFrames;
+ }
+
/// Listens to the `debugger` events from chrome and translates those to
/// the `Debug` stream events for the vm service protocol.
///
@@ -792,10 +832,14 @@
} else {
event.kind = EventKind.kPauseInterrupted;
}
+ _pausedStack = Stack()
+ ..frames = _dartFramesFor(e)
+ ..messages = [];
streamNotify('Debug', event);
});
resumeSubscription = tabConnection.debugger.onResumed.listen((e) {
if (_isolate == null) return;
+ _pausedStack = null;
streamNotify(
'Debug',
Event()
diff --git a/dwds/lib/src/debugger.dart b/dwds/lib/src/debugger.dart
index a14342e..6f0bf9f 100644
--- a/dwds/lib/src/debugger.dart
+++ b/dwds/lib/src/debugger.dart
@@ -67,7 +67,7 @@
// TODO(401): Remove the additional parameter.
var dartUri = DartUri(
dartScript.uri, '${Uri.parse(mainProxy.uri).path}/garbage.dart');
- var location = locationFor(dartUri, line);
+ var location = locationForDart(dartUri, line);
// TODO: Handle cases where a breakpoint can't be set exactly at that line.
if (location == null) return null;
var jsBreakpointId = await _setBreakpoint(location);
@@ -109,8 +109,8 @@
var response =
await chromeDebugger.sendCommand('Debugger.setBreakpoint', params: {
'location': {
- 'scriptId': location.jsScriptId,
- 'lineNumber': location.jsLine - 1,
+ 'scriptId': location.jsLocation.scriptId,
+ 'lineNumber': location.jsLocation.line - 1,
}
});
handleErrorIfPresent(response);
@@ -127,9 +127,43 @@
/// Find the [Location] for the given Dart source position.
///
/// The [line] number is 1-based.
- Location locationFor(DartUri uri, int line) => sources
- .locationsFor(uri.serverPath)
- .firstWhere((location) => location.dartLine == line, orElse: () => null);
+ Location locationForDart(DartUri uri, int line) => sources
+ .locationsForDart(uri.serverPath)
+ .firstWhere((location) => location.dartLocation.line == line,
+ orElse: () => null);
+
+ /// Returns the closest [Location] for the given Dart source position.
+ ///
+ /// The [line] and [column] are 1-based.
+ ///
+ /// Can return null if no suitable [Location] is found.
+ Location _bestLocationForJs(String scriptId, int line, int column) {
+ Location result;
+ for (var location in sources.locationsForJs(scriptId)) {
+ if (location.jsLocation.line == line) {
+ result ??= location;
+ if ((location.jsLocation.column - column).abs() <
+ (result.jsLocation.column - column).abs()) {
+ result = location;
+ }
+ }
+ }
+ return result;
+ }
+
+ /// Returns a Dart [Frame] for a [JsLocation].
+ Frame frameFor(JsLocation jsLocation) {
+ var location = _bestLocationForJs(
+ jsLocation.scriptId, jsLocation.line, jsLocation.column);
+ if (location == null) return null;
+ var script = mainProxy.scriptRefFor(location.dartLocation.uri.serverPath);
+ return Frame()
+ ..code = (CodeRef()..kind = CodeKind.kDart)
+ ..location = (SourceLocation()
+ ..tokenPos = location.tokenPos
+ ..script = script)
+ ..kind = FrameKind.kRegular;
+ }
}
/// Keeps track of the Dart and JS breakpoint Ids that correspond.
diff --git a/dwds/lib/src/location.dart b/dwds/lib/src/location.dart
index 9b97676..2dc2615 100644
--- a/dwds/lib/src/location.dart
+++ b/dwds/lib/src/location.dart
@@ -4,47 +4,29 @@
import 'package:source_maps/parser.dart';
+import 'dart_uri.dart';
+
var _startTokenId = 1337;
/// A source location, with both Dart and JS information.
-///
-/// Note that line and column numbers here are always 1-based. The Dart VM
-/// Service protocol line/column numbers are one-based, but in JS source maps and
-/// the Chrome protocol are zero-based, so they require translation.
class Location {
- final String jsScriptId;
+ final JsLocation jsLocation;
- /// 1 based row offset within the JS source code.
- final int jsLine;
-
- /// 1 based column offset within the JS source code.
- final int jsColumn;
-
- final String dartUri;
-
- /// 1 based row offset within the Dart source code.
- final int dartLine;
-
- /// 1 based column offset within the Dart source code.
- final int dartColumn;
+ final DartLocation dartLocation;
/// An arbitrary integer value used to represent this location.
final int tokenPos;
Location._(
- this.jsScriptId,
- this.jsLine,
- this.jsColumn,
- this.dartUri,
- this.dartLine,
- this.dartColumn,
+ this.jsLocation,
+ this.dartLocation,
) : tokenPos = _startTokenId++;
static Location from(
String scriptId,
TargetLineEntry lineEntry,
TargetEntry entry,
- String dartUrl,
+ DartUri dartUri,
) {
var dartLine = entry.sourceLine;
var dartColumn = entry.sourceColumn;
@@ -52,7 +34,54 @@
var jsColumn = entry.column;
// lineEntry data is 0 based according to:
// https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k
- return Location._(scriptId, jsLine + 1, jsColumn + 1, dartUrl, dartLine + 1,
- dartColumn + 1);
+ return Location._(JsLocation.fromZeroBased(scriptId, jsLine, jsColumn),
+ DartLocation.fromZeroBased(dartUri, dartLine, dartColumn));
}
}
+
+/// Location information for a Dart source.
+class DartLocation {
+ final DartUri uri;
+
+ /// 1 based row offset within the Dart source code.
+ final int line;
+
+ /// 1 based column offset within the Dart source code.
+ final int column;
+
+ DartLocation._(
+ this.uri,
+ this.line,
+ this.column,
+ );
+
+ static DartLocation fromZeroBased(DartUri uri, int line, int column) =>
+ DartLocation._(uri, line + 1, column + 1);
+
+ static DartLocation fromOneBased(DartUri uri, int line, int column) =>
+ DartLocation._(uri, line, column);
+}
+
+/// Location information for a JS source.
+class JsLocation {
+ /// The script ID as provided by Chrome.
+ final String scriptId;
+
+ /// 1 based row offset within the JS source code.
+ final int line;
+
+ /// 1 based column offset within the JS source code.
+ final int column;
+
+ JsLocation._(
+ this.scriptId,
+ this.line,
+ this.column,
+ );
+
+ static JsLocation fromZeroBased(String scriptId, int line, int column) =>
+ JsLocation._(scriptId, line + 1, column + 1);
+
+ static JsLocation fromOneBased(String scriptId, int line, int column) =>
+ JsLocation._(scriptId, line, column);
+}
diff --git a/dwds/lib/src/sources.dart b/dwds/lib/src/sources.dart
index 481b0e2..2a402a5 100644
--- a/dwds/lib/src/sources.dart
+++ b/dwds/lib/src/sources.dart
@@ -30,15 +30,19 @@
/// Map from Dart server path to all corresponding [Location] data.
final _sourceToLocation = <String, Set<Location>>{};
- /// Map from Dart URL to server path.
- final _canonicalPaths = <String, String>{};
+ /// Map from JS scriptId to all corresponding [Location] data.
+ final _scriptIdToLocation = <String, Set<Location>>{};
Sources(this._mainProxy);
/// Returns all [Location] data for a provided Dart source.
- Set<Location> locationsFor(String serverPath) =>
+ 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 {
@@ -53,19 +57,18 @@
for (var entry in lineEntry.entries) {
var index = entry.sourceUrlId;
if (index == null) continue;
- var dartUrl = mapping.urls[index];
+ var dartUri = DartUri(mapping.urls[index], script.url);
var location = Location.from(
script.scriptId,
lineEntry,
entry,
- dartUrl,
+ dartUri,
);
- _canonicalPaths.putIfAbsent(
- dartUrl,
- // TODO(401): Remove the additional parameter after D24 is stable.
- () => DartUri(dartUrl, script.url).serverPath);
_sourceToLocation
- .putIfAbsent(_canonicalPaths[dartUrl], () => Set())
+ .putIfAbsent(dartUri.serverPath, () => Set())
+ .add(location);
+ _scriptIdToLocation
+ .putIfAbsent(script.scriptId, () => Set())
.add(location);
}
}
@@ -90,7 +93,7 @@
var lineNumberToLocation = <int, Set<Location>>{};
for (var location in locations) {
lineNumberToLocation
- .putIfAbsent(location.dartLine, () => Set())
+ .putIfAbsent(location.dartLocation.line, () => Set())
.add(location);
}
for (var lineNumber in lineNumberToLocation.keys) {
@@ -98,7 +101,7 @@
lineNumber,
for (var location in lineNumberToLocation[lineNumber]) ...[
location.tokenPos,
- location.dartColumn
+ location.dartLocation.column
]
]);
}
diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml
index 2c51183..cd10a32 100644
--- a/dwds/pubspec.yaml
+++ b/dwds/pubspec.yaml
@@ -1,5 +1,5 @@
name: dwds
-version: 0.3.2
+version: 0.3.3-dev
author: Dart Team <misc@dartlang.org>
homepage: https://github.com/dart-lang/webdev/tree/master/dwds
description: >-
diff --git a/dwds/test/chrome_proxy_service_test.dart b/dwds/test/chrome_proxy_service_test.dart
index cbdee3e..749f596 100644
--- a/dwds/test/chrome_proxy_service_test.dart
+++ b/dwds/test/chrome_proxy_service_test.dart
@@ -35,7 +35,6 @@
Isolate isolate;
ScriptList scripts;
ScriptRef mainScript;
- var breakpoints = <Breakpoint>[];
setUp(() async {
vm = await service.getVM();
@@ -46,19 +45,12 @@
await service.debugger.sources.waitForSourceMap('hello_world/main.dart');
});
- tearDown(() async {
- for (var breakpoint in breakpoints) {
- await service.removeBreakpointInternal(breakpoint.id);
- }
- breakpoints = [];
- });
-
test('addBreakpoint', () async {
// TODO: Much more testing.
var bp = await service.addBreakpoint(isolate.id, mainScript.id, 21);
- breakpoints = isolate.breakpoints;
- breakpoints = [bp];
- expect(breakpoints.any((b) => b.location.tokenPos == 42), isNotNull);
+ // Remove breakpoint so it doesn't impact other tests.
+ await service.removeBreakpointInternal(bp.id);
+ expect(bp.id, '1');
});
test('addBreakpointAtEntry', () {
@@ -351,8 +343,55 @@
expect(() => service.getSourceReport(null, null), throwsUnimplementedError);
});
- test('getStack', () {
- expect(() => service.getStack(null), throwsUnimplementedError);
+ group('getStack', () {
+ String isolateId;
+ Stream<Event> stream;
+ ScriptList scripts;
+ ScriptRef mainScript;
+
+ setUp(() async {
+ var vm = await service.getVM();
+ isolateId = vm.isolates.first.id;
+ scripts = await service.getScripts(isolateId);
+ await service.streamListen('Debug');
+ stream = service.onEvent('Debug');
+ mainScript =
+ scripts.scripts.firstWhere((each) => each.uri.contains('main.dart'));
+ await service.debugger.sources.waitForSourceMap('hello_world/main.dart');
+ });
+
+ test('returns null if not paused', () async {
+ expect(await service.getStack(isolateId), isNull);
+ });
+
+ test('returns stack when broken', () async {
+ var bp = await service.addBreakpoint(isolateId, mainScript.id, 59);
+ // Wait for breakpoint to trigger.
+ await stream
+ .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
+ // Remove breakpoint so it doesn't impact other tests.
+ await service.removeBreakpointInternal(bp.id);
+ var stack = await service.getStack(isolateId);
+ // Resume as to not impact other tests.
+ await service.resume(isolateId);
+ expect(stack, isNotNull);
+ expect(stack.frames.length, 2);
+ var first = stack.frames.first;
+ expect(first.kind, 'Regular');
+ expect(first.code.kind, 'Dart');
+ // TODO(grouma) - Expect a Dart name.
+ expect(first.code.name, 'main.printCount');
+ });
+
+ test('returns non-empty stack when paused', () async {
+ await service.pause(isolateId);
+ // Wait for pausing to actually propagate.
+ await stream
+ .firstWhere((event) => event.kind == EventKind.kPauseInterrupted);
+ expect(await service.getStack(isolateId), isNotNull);
+ // Resume the isolate to not impact other tests.
+ await service.resume(isolateId);
+ });
});
test('getVM', () async {