Skip to content

Commit 66bba1d

Browse files
author
Anna Gringauze
authored
Find best locations for breakpoints, expression evaluation, and call frames. (#1590)
* Fix finding locations on breakpoints, call frames, and expression evaluation * Update JS and Dart location search heuristic * Fix typos in comments * Updated heuristic for finding dart locations to work for more cases, added tests * Addressed CR comments * Enable accidentally disabled tests * Format * Fix failing tests
1 parent 881a4f8 commit 66bba1d

File tree

17 files changed

+775
-102
lines changed

17 files changed

+775
-102
lines changed

dwds/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Fix crash when using flutter tools with web server device.
1010
- Remove clearing all scripts on page load for extension debugger.
1111
- Fix breakpoints not hitting after changing a base in index.html.
12+
- Find best locations for call frames, breakpoints, or expression evaluation.
1213

1314
**Breaking changes:**
1415
- Add `basePath` parameter to `FrontendServerRequireStrategy`.

dwds/lib/src/debugging/debugger.dart

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ class Debugger extends Domain {
317317
}
318318

319319
/// Returns Chrome script uri for Chrome script ID.
320-
String _urlForScriptId(String scriptId) =>
320+
String urlForScriptId(String scriptId) =>
321321
_remoteDebugger.scripts[scriptId]?.url;
322322

323323
/// Returns source [Location] for the paused event.
@@ -331,7 +331,7 @@ class Debugger extends Domain {
331331
var line = location['lineNumber'] as int;
332332
var column = location['columnNumber'] as int;
333333

334-
var url = _urlForScriptId(scriptId);
334+
var url = urlForScriptId(scriptId);
335335
return _locations.locationForJs(url, line, column);
336336
}
337337

@@ -467,25 +467,14 @@ class Debugger extends Domain {
467467
var line = location.lineNumber;
468468
var column = location.columnNumber;
469469

470-
var url = _urlForScriptId(location.scriptId);
470+
var url = urlForScriptId(location.scriptId);
471471
if (url == null) {
472472
logger.severe('Failed to create dart frame for ${frame.functionName}: '
473473
'cannot find location for script ${location.scriptId}');
474474
return null;
475475
}
476476

477-
// TODO(sdk/issues/37240) - ideally we look for an exact location instead
478-
// of the closest location on a given line.
479-
Location bestLocation;
480-
for (var location in await _locations.locationsForUrl(url)) {
481-
if (location.jsLocation.line == line) {
482-
bestLocation ??= location;
483-
if ((location.jsLocation.column - column).abs() <
484-
(bestLocation.jsLocation.column - column).abs()) {
485-
bestLocation = location;
486-
}
487-
}
488-
}
477+
var bestLocation = await _locations.locationForJs(url, line, column);
489478
if (bestLocation == null) return null;
490479

491480
var script =
@@ -506,7 +495,11 @@ class Debugger extends Domain {
506495
kind: CodeKind.kDart,
507496
id: createId(),
508497
),
509-
location: SourceLocation(tokenPos: bestLocation.tokenPos, script: script),
498+
location: SourceLocation(
499+
line: bestLocation.dartLocation.line,
500+
column: bestLocation.dartLocation.column,
501+
tokenPos: bestLocation.tokenPos,
502+
script: script),
510503
kind: FrameKind.kRegular,
511504
);
512505

@@ -579,7 +572,7 @@ class Debugger extends Domain {
579572
var frame = e.params['callFrames'][0];
580573
var scriptId = '${frame["location"]["scriptId"]}';
581574

582-
var url = _urlForScriptId(scriptId);
575+
var url = urlForScriptId(scriptId);
583576
if (url == null) {
584577
logger.severe('Stepping failed: '
585578
'cannot find location for script $scriptId');

dwds/lib/src/debugging/frame_computer.dart

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,24 @@
44

55
// @dart = 2.9
66

7+
import 'package:logging/logging.dart';
78
import 'package:pool/pool.dart';
89
import 'package:vm_service/vm_service.dart';
910
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
1011

11-
import 'dart_scope.dart';
1212
import 'debugger.dart';
1313

1414
class FrameComputer {
1515
final Debugger debugger;
1616

17+
final _logger = Logger('FrameComputer');
18+
1719
// To ensure that the frames are computed only once, we use a pool to guard
1820
// the work. Frames are computed sequentially.
1921
final _pool = Pool(1);
2022

2123
final List<WipCallFrame> _callFrames;
22-
final _computedFrames = <Frame>[];
24+
final List<Frame> _computedFrames = [];
2325

2426
var _frameIndex = 0;
2527

@@ -36,12 +38,6 @@ class FrameComputer {
3638
return frameIndex < _callFrames.length ? _callFrames[frameIndex] : null;
3739
}
3840

39-
/// Return the WipScopes for the given JavaScript frame index that are
40-
/// pertinent for Dart debugging.
41-
List<WipScope> getWipScopesForFrameIndex(int frameIndex) {
42-
return filterScopes(jsFrameForIndex(frameIndex));
43-
}
44-
4541
/// Translates Chrome callFrames contained in [DebuggerPausedEvent] into Dart
4642
/// [Frame]s.
4743
Future<List<Frame>> calculateFrames({int limit}) async {
@@ -101,6 +97,14 @@ class FrameComputer {
10197
var location = WipLocation.fromValues(
10298
callFrame.scriptId, callFrame.lineNumber,
10399
columnNumber: callFrame.columnNumber);
100+
101+
var url = callFrame.url ?? debugger.urlForScriptId(location.scriptId);
102+
if (url == null) {
103+
_logger.severe(
104+
'Failed to create dart frame for ${callFrame.functionName}: '
105+
'cannot find location for script ${callFrame.scriptId}');
106+
}
107+
104108
var tempWipFrame = WipCallFrame({
105109
'url': callFrame.url,
106110
'functionName': callFrame.functionName,

dwds/lib/src/debugging/location.dart

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -152,22 +152,113 @@ class Locations {
152152
/// Find the [Location] for the given Dart source position.
153153
///
154154
/// The [line] number is 1-based.
155-
Future<Location> locationForDart(DartUri uri, int line, int column) async =>
156-
(await locationsForDart(uri.serverPath)).firstWhere(
157-
(location) =>
158-
location.dartLocation.line == line &&
159-
location.dartLocation.column >= column,
160-
orElse: () => null);
155+
Future<Location> locationForDart(DartUri uri, int line, int column) async {
156+
var locations = await locationsForDart(uri.serverPath);
157+
return _bestDartLocation(locations, line, column);
158+
}
161159

162160
/// Find the [Location] for the given JS source position.
163161
///
164162
/// The [line] number is 0-based.
165-
Future<Location> locationForJs(String url, int line, int column) async =>
166-
(await locationsForUrl(url)).firstWhere(
167-
(location) =>
168-
location.jsLocation.line == line &&
169-
location.jsLocation.column >= column,
170-
orElse: () => null);
163+
Future<Location> locationForJs(String url, int line, int column) async {
164+
var locations = await locationsForUrl(url);
165+
return _bestJsLocation(locations, line, column);
166+
}
167+
168+
/// Find closest existing Dart location for the line and column.
169+
///
170+
/// Dart columns for breakpoints are either exact or start at the
171+
/// beginning of the line - return the first existing location
172+
/// that comes after the given column.
173+
Location _bestDartLocation(
174+
Iterable<Location> locations, int line, int column) {
175+
Location bestLocation;
176+
for (var location in locations) {
177+
if (location.dartLocation.line == line &&
178+
location.dartLocation.column >= column) {
179+
bestLocation ??= location;
180+
if (location.dartLocation.column < bestLocation.dartLocation.column) {
181+
bestLocation = location;
182+
}
183+
}
184+
}
185+
return bestLocation;
186+
}
187+
188+
/// Find closest existing JavaScript location for the line and column.
189+
///
190+
/// Some JS locations are not stored in the source maps, so we find the
191+
/// closest existing location, preferring the one coming after the given
192+
/// column, if the current break is at an expression statement, or the
193+
/// one coming before if the current break is at a function call.
194+
///
195+
/// This is a known problem that other code bases solve using by finding
196+
/// the closest location to the current one:
197+
///
198+
/// https://github.com/microsoft/vscode-js-debug/blob/536f96bae61a3d87546b61bc7916097904c81429/src/common/sourceUtils.ts#L286
199+
///
200+
/// Unfortunately, this approach fails for Flutter code too often, as it
201+
/// frequently contains multi-line statements with nested objects.
202+
///
203+
/// For example:
204+
///
205+
/// - `t33 = main.doSomething()` in top frame:
206+
/// Current column is at `t33`. Return existing location starting
207+
/// at `main`.
208+
/// - `main.doSomething()` in top frame:
209+
/// Current column is at `main`. Return existing location starting
210+
/// at `main`.
211+
/// - `main.doSomething()` in a frame down the stack:
212+
/// Current column is at `doSomething`. Source map does not have a
213+
/// location stored that starts at `doSomething()`. Return existing
214+
/// location starting at `main`.
215+
Location _bestJsLocation(Iterable<Location> locations, int line, int column) {
216+
Location bestLocationBefore;
217+
Location bestLocationAfter;
218+
219+
var locationsAfter = locations.where((location) =>
220+
location.jsLocation.line == line &&
221+
location.jsLocation.column >= column);
222+
var locationsBefore = locations.where((location) =>
223+
location.jsLocation.line == line &&
224+
location.jsLocation.column < column);
225+
226+
for (var location in locationsAfter) {
227+
bestLocationAfter ??= location;
228+
if (location.jsLocation.column < bestLocationAfter.jsLocation.column) {
229+
bestLocationAfter = location;
230+
}
231+
}
232+
for (var location in locationsBefore) {
233+
bestLocationBefore ??= location;
234+
if (location.jsLocation.column > bestLocationBefore.jsLocation.column) {
235+
bestLocationBefore = location;
236+
}
237+
}
238+
if (bestLocationAfter == null) return bestLocationBefore;
239+
if (bestLocationBefore == null) return bestLocationAfter;
240+
241+
if (bestLocationAfter.jsLocation.line == line &&
242+
bestLocationAfter.jsLocation.column == column) {
243+
// Prefer exact match.
244+
return bestLocationAfter;
245+
}
246+
247+
// Return the closest location after the current if the current location
248+
// is at the beginning of the line (i.e. on expression statement).
249+
// Return the closest location before the current if the current location
250+
// is in the middle of the line (i.e. on function call).
251+
if (locationsBefore.length == 1 &&
252+
locationsBefore.first.jsLocation.column == 0) {
253+
// Best guess on whether the the current location is at the beginning of
254+
// the line (i.e. expression statement):
255+
// The only location on the left has column 0, so only spaces are on the
256+
// left.
257+
return bestLocationAfter;
258+
}
259+
// Current column is in the middle of the line (i.e .function call).
260+
return bestLocationBefore;
261+
}
171262

172263
/// Returns the tokenPosTable for the provided Dart script path as defined
173264
/// in:

dwds/lib/src/debugging/metadata/provider.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ class MetadataProvider {
216216
_soundNullSafety = hasSoundNullSafety;
217217
}
218218
_logger.info('Loaded debug metadata '
219-
'(${_soundNullSafety ? "" : "no "}sound null safety)');
219+
'(${_soundNullSafety ? "sound" : "weak"} null safety)');
220220
}
221221
});
222222
}

dwds/lib/src/services/expression_evaluator.dart

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ class ExpressionEvaluator {
155155
var jsFrame = _inspector.debugger.jsFrameForIndex(frameIndex);
156156
if (jsFrame == null) {
157157
return _createError(
158-
ErrorKind.internal, 'No frame with index $frameIndex');
158+
ErrorKind.internal,
159+
'Expression evaluation in async frames '
160+
'is not supported. No frame with index $frameIndex.');
159161
}
160162

161163
var functionName = jsFrame.functionName;
@@ -165,12 +167,6 @@ class ExpressionEvaluator {
165167
var jsScope = await _collectLocalJsScope(jsFrame);
166168

167169
// Find corresponding dart location and scope.
168-
//
169-
// TODO(annagrin): handle unknown dart locations
170-
// Debugger does not map every js location to a dart location,
171-
// so this will result in expressions not evaluated in some
172-
// cases. Invent location matching strategy for those cases.
173-
// [issue 890](https://github.com/dart-lang/webdev/issues/890)
174170
var url = _urlForScriptId(jsScriptId);
175171
var locationMap = await _locations.locationForJs(url, jsLine, jsColumn);
176172
if (locationMap == null) {

dwds/test/build_daemon_breakpoint_test.dart

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -74,28 +74,6 @@ void main() {
7474
await service.removeBreakpoint(isolate.id, bp.id);
7575
});
7676

77-
test('set breakpoint inside a JavaScript line succeeds on', () async {
78-
var line = await context.findBreakpointLine(
79-
'printNestedObjectMultiLine', isolate.id, mainScript);
80-
var column = 0;
81-
var bp = await service.addBreakpointWithScriptUri(
82-
isolate.id, mainScript.uri, line,
83-
column: column);
84-
await stream.firstWhere(
85-
(Event event) => event.kind == EventKind.kPauseBreakpoint);
86-
87-
expect(bp, isNotNull);
88-
expect(
89-
bp.location,
90-
isA<SourceLocation>()
91-
.having((loc) => loc.script, 'script', equals(mainScript))
92-
.having((loc) => loc.line, 'line', equals(line))
93-
.having((loc) => loc.column, 'column', greaterThan(column)));
94-
95-
// Remove breakpoint so it doesn't impact other tests.
96-
await service.removeBreakpoint(isolate.id, bp.id);
97-
});
98-
9977
test('set breakpoint again', () async {
10078
var line = await context.findBreakpointLine(
10179
'printLocal', isolate.id, mainScript);

0 commit comments

Comments
 (0)