diff --git a/dwds/lib/src/debugging/debugger.dart b/dwds/lib/src/debugging/debugger.dart index c8e313d9a..a95d4a03b 100644 --- a/dwds/lib/src/debugging/debugger.dart +++ b/dwds/lib/src/debugging/debugger.dart @@ -587,6 +587,33 @@ class Debugger extends Domain { return dartFrame; } + /// Returns a JS [Frame] from a Chrome frame. + Frame? calculateJsFrameFor( + WipCallFrame frame, + int frameIndex, { + bool isAsync = false, + }) { + final location = frame.location; + final url = urlForScriptId(location.scriptId); + + final jsFrame = Frame( + index: frameIndex, + code: CodeRef( + name: frame.functionName.isEmpty ? 'anonymous' : frame.functionName, + kind: CodeKind.kNative, + id: createId(), + ), + location: SourceLocation( + script: ScriptRef(uri: url, id: createId()), + line: location.lineNumber, + column: location.columnNumber, + ), + kind: isAsync ? FrameKind.kAsyncCausal : FrameKind.kRegular, + ); + + return jsFrame; + } + /// Handles pause events coming from the Chrome connection. Future _pauseHandler(DebuggerPausedEvent e) async { final isolate = inspector.isolate; diff --git a/dwds/lib/src/debugging/frame_computer.dart b/dwds/lib/src/debugging/frame_computer.dart index 245388050..96330c201 100644 --- a/dwds/lib/src/debugging/frame_computer.dart +++ b/dwds/lib/src/debugging/frame_computer.dart @@ -16,6 +16,7 @@ class FrameComputer { final List _callFrames; final List _computedFrames = []; + final List _jsFrames = []; var _frameIndex = 0; @@ -51,6 +52,11 @@ class FrameComputer { _computedFrames.removeLast(); } + // Only show JS frames if we were unable to calculate any Dart frames. + if (_computedFrames.isEmpty && _jsFrames.isNotEmpty) { + return _jsFrames.take(limit ?? _jsFrames.length).toList(); + } + return _computedFrames; }); } @@ -61,10 +67,16 @@ class FrameComputer { final callFrame = _callFrames[_frameIndex]; final dartFrame = - await debugger.calculateDartFrameFor(callFrame, _frameIndex++); + await debugger.calculateDartFrameFor(callFrame, _frameIndex); if (dartFrame != null) { _computedFrames.add(dartFrame); + } else { + final jsFrame = debugger.calculateJsFrameFor(callFrame, _frameIndex); + if (jsFrame != null) { + _jsFrames.add(jsFrame); + } } + _frameIndex++; } } @@ -103,13 +115,23 @@ class FrameComputer { final frame = await debugger.calculateDartFrameFor( tempWipFrame, - _frameIndex++, + _frameIndex, populateVariables: false, ); if (frame != null) { frame.kind = FrameKind.kAsyncCausal; _computedFrames.add(frame); + } else { + final jsFrame = debugger.calculateJsFrameFor( + tempWipFrame, + _frameIndex, + isAsync: true, + ); + if (jsFrame != null) { + _jsFrames.add(jsFrame); + } } + _frameIndex++; } } diff --git a/dwds/test/debugger_test.dart b/dwds/test/debugger_test.dart index c6514e4aa..a82406ad5 100644 --- a/dwds/test/debugger_test.dart +++ b/dwds/test/debugger_test.dart @@ -101,8 +101,8 @@ void main() async { /// Test that we get expected variable values from a hard-coded /// stack frame. - test('frames 1', () async { - final stackComputer = FrameComputer(debugger, frames1); + test('Only Dart frames', () async { + final stackComputer = FrameComputer(debugger, dartFrames); final frames = await stackComputer.calculateFrames(); expect(frames, isNotNull); expect(frames, isNotEmpty); @@ -112,6 +112,42 @@ void main() async { expect(frame1Variables, ['a', 'b']); }); + /// Test that we get JS frames if only JS frames are in the stack trace: + test('Only JS frames', () async { + final stackComputer = FrameComputer(debugger, jsFrames); + final frames = await stackComputer.calculateFrames(); + + expect(frames, isNotNull); + expect(frames.length, equals(3)); + + final firstFrame = frames[0]; + expect(firstFrame.code?.kind, equals('Native')); + expect(firstFrame.code?.name, equals('createTimer')); + + final secondFrame = frames[1]; + expect(secondFrame.code?.kind, equals('Native')); + expect(secondFrame.code?.name, equals('rootRun')); + + final thirdFrame = frames[2]; + expect(thirdFrame.code?.kind, equals('Native')); + expect(thirdFrame.code?.name, equals('runGuarded')); + }); + + /// Test that we get only Dart frames if both Dart and JS frames are in the stack trace: + test('Dart and JS frames', () async { + final stackComputer = FrameComputer(debugger, dartAndJsFrames); + final frames = await stackComputer.calculateFrames(); + + expect(frames, isNotNull); + expect(frames, isNotEmpty); + + final jsFrames = frames.where((frame) => frame.code?.kind == 'Native'); + final dartFrames = frames.where((frame) => frame.code?.kind == 'Dart'); + + expect(jsFrames, isEmpty); + expect(dartFrames, isNotEmpty); + }); + test('creates async frames', () async { final stackComputer = FrameComputer( debugger, diff --git a/dwds/test/extension_debugger_test.dart b/dwds/test/extension_debugger_test.dart index 4e11e1093..a307e542e 100644 --- a/dwds/test/extension_debugger_test.dart +++ b/dwds/test/extension_debugger_test.dart @@ -50,12 +50,12 @@ void main() async { test('an ExtensionEvent', () async { final extensionEvent = ExtensionEvent((b) => b ..method = jsonEncode('Debugger.paused') - ..params = jsonEncode(frames1Json[0])); + ..params = jsonEncode(dartFramesJson[0])); connection.controllerIncoming.sink .add(jsonEncode(serializers.serialize(extensionEvent))); final wipEvent = await extensionDebugger.onNotification.first; expect(wipEvent.method, 'Debugger.paused'); - expect(wipEvent.params, frames1Json[0]); + expect(wipEvent.params, dartFramesJson[0]); }); test('a BatchedEvents', () async { diff --git a/dwds/test/fixtures/debugger_data.dart b/dwds/test/fixtures/debugger_data.dart index f702cab88..2341c8b47 100644 --- a/dwds/test/fixtures/debugger_data.dart +++ b/dwds/test/fixtures/debugger_data.dart @@ -11,9 +11,14 @@ import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; /// /// This is taken from a real run, but truncated to two levels of scope and one /// level of stack. -List frames1 = frames1Json.map(WipCallFrame.new).toList(); +List dartFrames = dartFramesJson.map(WipCallFrame.new).toList(); -List> frames1Json = [ +List jsFrames = jsFramesJson.map(WipCallFrame.new).toList(); + +List dartAndJsFrames = + [...dartFramesJson, ...jsFramesJson].map(WipCallFrame.new).toList(); + +List> dartFramesJson = [ { "callFrameId": "{\"ordinal\":0,\"injectedScriptId\":2}", "functionName": "", @@ -86,6 +91,39 @@ List> frames1Json = [ } ]; +final jsFramesJson = [ + { + "url": "http://localhost:63691/dwds/src/injected/client.js", + "functionName": "createTimer", + "location": { + "scriptId": "239", + "lineNumber": 19833, + "columnNumber": 18, + }, + "scopeChain": [] + }, + { + "url": "http://localhost:63691/dwds/src/injected/client.js", + "functionName": "rootRun", + "location": { + "scriptId": "239", + "lineNumber": 12193, + "columnNumber": 2, + }, + "scopeChain": [] + }, + { + "url": "http://localhost:63691/dwds/src/injected/client.js", + "functionName": "runGuarded", + "location": { + "scriptId": "239", + "lineNumber": 13008, + "columnNumber": 13, + }, + "scopeChain": [] + }, +]; + /// Data in the form returned from getProperties called twice on successive /// elements of a scope chain. ///