Stepping and evaluation analytics (#1248)
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index 96a0fee..0e3056a 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -6,6 +6,7 @@
is paused.
- Support keep-alive for debug service connections.
- Depend on the latest `package:sse`.
+- Add `DwdsEvent`s around stepping and evaluation.
**Breaking changes:**
- `LoadStrategy`s now require a `moduleInfoForEntrypoint`.
diff --git a/dwds/lib/src/events.dart b/dwds/lib/src/events.dart
index 816182e..616c94d 100644
--- a/dwds/lib/src/events.dart
+++ b/dwds/lib/src/events.dart
@@ -9,6 +9,11 @@
final Map<String, dynamic> payload;
DwdsEvent(this.type, this.payload);
+
+ @override
+ String toString() {
+ return 'TYPE: $type Payload: $payload';
+ }
}
final _eventController = StreamController<DwdsEvent>.broadcast();
diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart
index 793457c..09122b1 100644
--- a/dwds/lib/src/services/chrome_proxy_service.dart
+++ b/dwds/lib/src/services/chrome_proxy_service.dart
@@ -12,6 +12,7 @@
import 'package:vm_service/vm_service.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+import '../../dwds.dart';
import '../connections/app_connection.dart';
import '../debugging/debugger.dart';
import '../debugging/execution_context.dart';
@@ -21,6 +22,7 @@
import '../debugging/modules.dart';
import '../debugging/remote_debugger.dart';
import '../debugging/skip_list.dart';
+import '../events.dart';
import '../loaders/strategy.dart';
import '../readers/asset_reader.dart';
import '../utilities/dart_uri.dart';
@@ -175,7 +177,12 @@
await _compiler.initialize(soundNullSafety: soundNullSafety);
var dependencies =
await globalLoadStrategy.moduleInfoForEntrypoint(entrypoint);
+ var stopwatch = Stopwatch()..start();
await _compiler.updateDependencies(dependencies);
+ emitEvent(DwdsEvent('COMPILER_UPDATE_DEPENDENCIES', {
+ 'entrypoint': entrypoint,
+ 'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
+ }));
}
}
@@ -343,9 +350,23 @@
String isolateId, String targetId, String expression,
{Map<String, String> scope, bool disableBreakpoints}) async {
// TODO(798) - respect disableBreakpoints.
- var remote = await _inspector?.evaluate(isolateId, targetId, expression,
- scope: scope);
- return _inspector?.instanceHelper?.instanceRefFor(remote);
+ var stopwatch = Stopwatch()..start();
+ dynamic error;
+ try {
+ var remote = await _inspector?.evaluate(isolateId, targetId, expression,
+ scope: scope);
+ return _inspector?.instanceHelper?.instanceRefFor(remote);
+ } catch (e) {
+ error = e;
+ rethrow;
+ } finally {
+ emitEvent(DwdsEvent('EVALUATE', {
+ 'expression': expression,
+ 'success': error == null,
+ 'exception': error,
+ 'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
+ }));
+ }
}
@override
@@ -358,7 +379,8 @@
throw RPCError('evaluateInFrame', RPCError.kInvalidParams,
'Unrecognized isolate id: $isolateId. Supported isolate: ${isolate?.id}');
}
-
+ dynamic error;
+ var stopwatch = Stopwatch()..start();
try {
var result = await _expressionEvaluator.evaluateExpression(
isolateId, frameIndex, expression);
@@ -382,6 +404,7 @@
}
return _inspector?.instanceHelper?.instanceRefFor(result);
} catch (e, s) {
+ error = e;
// Handle errors that throw exceptions, such as invalid JavaScript
// generated by the expression evaluator
_logger.warning('Failed to evaluate expression \'$expression\'. ');
@@ -390,6 +413,13 @@
'to file a bug.');
_logger.info('$e:$s');
return ErrorRef(kind: 'error', message: '<unknown>', id: createId());
+ } finally {
+ emitEvent(DwdsEvent('EVALUATE_IN_FRAME', {
+ 'expression': expression,
+ 'success': error == null,
+ 'exception': error,
+ 'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
+ }));
}
}
@@ -579,8 +609,14 @@
{String step, int frameIndex}) async {
if (_inspector == null) throw StateError('No running isolate.');
if (_inspector.appConnection.isStarted) {
- return await (await _debugger)
+ var stopwatch = Stopwatch()..start();
+ var result = await (await _debugger)
.resume(isolateId, step: step, frameIndex: frameIndex);
+ emitEvent(DwdsEvent('RESUME', {
+ 'step': step,
+ 'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
+ }));
+ return result;
} else {
_inspector.appConnection.runMain();
return Success();
diff --git a/dwds/test/events_test.dart b/dwds/test/events_test.dart
index 8661d8f..f31b28f 100644
--- a/dwds/test/events_test.dart
+++ b/dwds/test/events_test.dart
@@ -2,17 +2,30 @@
// 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:dwds/src/connections/debug_connection.dart';
import 'package:dwds/src/events.dart';
+import 'package:dwds/src/services/chrome_proxy_service.dart';
import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart';
import 'package:webdriver/async_core.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
import 'fixtures/context.dart';
+ChromeProxyService get service =>
+ fetchChromeProxyService(context.debugConnection);
+
+WipConnection get tabConnection => context.tabConnection;
+
+final context = TestContext();
+
void main() {
- final context = TestContext();
setUpAll(() async {
await context.setUp(
serveDevTools: true,
+ enableExpressionEvaluation: true,
);
});
@@ -28,4 +41,81 @@
context.testServer.dwds.events.listen((_) {});
context.testServer.dwds.events.listen((_) {});
});
+
+ group('evaluate', () {
+ Isolate isolate;
+ LibraryRef bootstrap;
+
+ setUpAll(() async {
+ var vm = await service.getVM();
+ isolate = await service.getIsolate(vm.isolates.first.id);
+ bootstrap = isolate.libraries.first;
+ });
+
+ test('emits EVALUATE events with expression', () async {
+ var expression = "helloString('world')";
+ expect(
+ context.testServer.dwds.events,
+ emits(predicate((DwdsEvent event) =>
+ event.type == 'EVALUATE' &&
+ event.payload['expression'] == expression)));
+ await service.evaluate(
+ isolate.id,
+ bootstrap.id,
+ expression,
+ );
+ });
+ });
+
+ test('emits EVALUATE_IN_FRAME events', () async {
+ var vm = await service.getVM();
+ var isolate = await service.getIsolate(vm.isolates.first.id);
+ expect(
+ context.testServer.dwds.events,
+ emits(predicate((DwdsEvent event) =>
+ event.type == 'EVALUATE_IN_FRAME' &&
+ event.payload['success'] == false)));
+ try {
+ await service.evaluateInFrame(
+ isolate.id,
+ 0,
+ 'some-bad-expression',
+ );
+ } catch (_) {}
+ });
+
+ group('resume', () {
+ 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((script) => script.uri.contains('main.dart'));
+ var line = await context.findBreakpointLine(
+ 'callPrintCount', isolateId, mainScript);
+ var bp = await service.addBreakpoint(isolateId, mainScript.id, line);
+ // Wait for breakpoint to trigger.
+ await stream
+ .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
+ await service.removeBreakpoint(isolateId, bp.id);
+ });
+
+ tearDown(() async {
+ // Resume execution to not impact other tests.
+ await service.resume(isolateId);
+ });
+
+ test('emits RESUME events', () async {
+ expect(context.testServer.dwds.events,
+ emits(predicate((DwdsEvent event) => event.type == 'RESUME')));
+ await service.resume(isolateId, step: 'Into');
+ });
+ });
}