Skip to content

Commit 16dc2c1

Browse files
author
Anna Gringauze
authored
Fix slow stepping into with deferred loaded modules (#1593)
* Fix slow stepping into with deferred loaded modules * Only consider previous JS location for sourcemap matching * Updated tests
1 parent 049a8b8 commit 16dc2c1

File tree

16 files changed

+689
-246
lines changed

16 files changed

+689
-246
lines changed

dwds/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,19 @@
1111
- Fix breakpoints not hitting after changing a base in index.html.
1212
- Find best locations for call frames, breakpoints, or expression evaluation.
1313
- Close the SSE connection when a DebugExtension.detached event is received.
14+
- Fix issues discovered when using legacy module system, debug extension,
15+
and JIT modules:
16+
- Improve step-into times by not stepping into library loading code.
17+
- Fix incorrect skip lists due to unsorted locations.
18+
- Fix memory leak in extension debugger by removing stale script IDs.
19+
- Allow mapping JS locations to Dart locations matching other JS lines,
20+
to match the behavior of Chrome DevTools.
21+
- Fix expression evaluation failure if debugger is stopped in the middle
22+
of a variable definition.
1423

1524
**Breaking changes:**
1625
- Add `basePath` parameter to `FrontendServerRequireStrategy`.
26+
- Add `loadLibrariesModule` getter to `LoadStrategy` interface.
1727

1828
## 13.1.0
1929
- Update _fe_analyzer_shared to version ^38.0.0.

dwds/lib/src/debugging/debugger.dart

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,16 @@ class Debugger extends Domain {
332332
var column = location['columnNumber'] as int;
333333

334334
var url = urlForScriptId(scriptId);
335+
if (url == null) return null;
335336
return _locations.locationForJs(url, line, column);
336337
}
337338

339+
/// Returns script ID for the paused event.
340+
String _frameScriptId(DebuggerPausedEvent e) {
341+
var frame = e.params['callFrames'][0];
342+
return frame['location']['scriptId'] as String;
343+
}
344+
338345
/// The variables visible in a frame in Dart protocol [BoundVariable] form.
339346
Future<List<BoundVariable>> variablesFor(WipCallFrame frame) async {
340347
// TODO(alanknight): Can these be moved to dart_scope.dart?
@@ -470,7 +477,7 @@ class Debugger extends Domain {
470477
var url = urlForScriptId(location.scriptId);
471478
if (url == null) {
472479
logger.severe('Failed to create dart frame for ${frame.functionName}: '
473-
'cannot find location for script ${location.scriptId}');
480+
'cannot find url for script ${location.scriptId}');
474481
return null;
475482
}
476483

@@ -567,26 +574,32 @@ class Debugger extends Domain {
567574
exception: exception,
568575
);
569576
} else {
570-
// If we don't have source location continue stepping.
571-
if (_isStepping && (await _sourceLocation(e)) == null) {
572-
var frame = e.params['callFrames'][0];
573-
var scriptId = '${frame["location"]["scriptId"]}';
574-
577+
// Continue stepping until we hit a dart location,
578+
// avoiding stepping through library loading code.
579+
if (_isStepping) {
580+
var scriptId = _frameScriptId(e);
575581
var url = urlForScriptId(scriptId);
582+
576583
if (url == null) {
577584
logger.severe('Stepping failed: '
578-
'cannot find location for script $scriptId');
585+
'cannot find url for script $scriptId');
586+
throw StateError('Stepping failed in script $scriptId');
579587
}
580588

581-
// TODO(grouma) - In the future we should send all previously computed
582-
// skipLists.
583-
await _remoteDebugger.stepInto(params: {
584-
'skipList': await _skipLists.compute(
585-
scriptId,
586-
await _locations.locationsForUrl(url),
587-
)
588-
});
589-
return;
589+
if (url.contains(globalLoadStrategy.loadLibrariesModule)) {
590+
await _remoteDebugger.stepOut();
591+
return;
592+
} else if ((await _sourceLocation(e)) == null) {
593+
// TODO(grouma) - In the future we should send all previously computed
594+
// skipLists.
595+
await _remoteDebugger.stepInto(params: {
596+
'skipList': await _skipLists.compute(
597+
scriptId,
598+
await _locations.locationsForUrl(url),
599+
)
600+
});
601+
return;
602+
}
590603
}
591604
event = Event(
592605
kind: EventKind.kPauseInterrupted,

dwds/lib/src/debugging/location.dart

Lines changed: 24 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ class DartLocation {
6969
this.column,
7070
);
7171

72+
int compareTo(DartLocation other) => compareToLine(other.line, other.column);
73+
74+
int compareToLine(int otherLine, int otherColumn) {
75+
var result = line.compareTo(otherLine);
76+
return result == 0 ? column.compareTo(otherColumn) : result;
77+
}
78+
7279
@override
7380
String toString() => '[${uri.serverPath}:$line:$column]';
7481

@@ -92,6 +99,13 @@ class JsLocation {
9299
this.column,
93100
);
94101

102+
int compareTo(JsLocation other) => compareToLine(other.line, other.column);
103+
104+
int compareToLine(int otherLine, int otherColumn) {
105+
var result = line.compareTo(otherLine);
106+
return result == 0 ? column.compareTo(otherColumn) : result;
107+
}
108+
95109
@override
96110
String toString() => '[$module:$line:$column]';
97111

@@ -177,7 +191,7 @@ class Locations {
177191
if (location.dartLocation.line == line &&
178192
location.dartLocation.column >= column) {
179193
bestLocation ??= location;
180-
if (location.dartLocation.column < bestLocation.dartLocation.column) {
194+
if (location.dartLocation.compareTo(bestLocation.dartLocation) < 0) {
181195
bestLocation = location;
182196
}
183197
}
@@ -188,76 +202,23 @@ class Locations {
188202
/// Find closest existing JavaScript location for the line and column.
189203
///
190204
/// 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.
205+
/// closest existing location coming before the given column.
194206
///
195207
/// This is a known problem that other code bases solve using by finding
196208
/// the closest location to the current one:
197209
///
198210
/// 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`.
215211
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;
212+
Location bestLocation;
213+
for (var location in locations) {
214+
if (location.jsLocation.compareToLine(line, column) <= 0) {
215+
bestLocation ??= location;
216+
if (location.jsLocation.compareTo(bestLocation.jsLocation) > 0) {
217+
bestLocation = location;
218+
}
236219
}
237220
}
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;
221+
return bestLocation;
261222
}
262223

263224
/// Returns the tokenPosTable for the provided Dart script path as defined

dwds/lib/src/debugging/skip_list.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ class SkipLists {
2626
) async {
2727
if (_idToList.containsKey(scriptId)) return _idToList[scriptId];
2828

29+
var sortedLocations = locations.toList()
30+
..sort((a, b) => a.jsLocation.compareTo(b.jsLocation));
31+
2932
var ranges = <Map<String, dynamic>>[];
3033
var startLine = 0;
3134
var startColumn = 0;
32-
for (var location in locations) {
33-
// Account for 1 based.
35+
for (var location in sortedLocations) {
3436
var endLine = location.jsLocation.line;
3537
var endColumn = location.jsLocation.column;
3638
// Stop before the known location.
@@ -40,8 +42,10 @@ class SkipLists {
4042
endColumn = maxValue;
4143
}
4244
if (endLine > startLine || endColumn > startColumn) {
43-
ranges.add(
44-
_rangeFor(scriptId, startLine, startColumn, endLine, endColumn));
45+
if (endLine >= startLine) {
46+
ranges.add(
47+
_rangeFor(scriptId, startLine, startColumn, endLine, endColumn));
48+
}
4549
startLine = location.jsLocation.line;
4650
startColumn = location.jsLocation.column + 1;
4751
}

dwds/lib/src/loaders/legacy.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ class LegacyStrategy extends LoadStrategy {
7979
@override
8080
String get moduleFormat => 'ddc';
8181

82+
@override
83+
String get loadLibrariesModule => 'dart_library.ddk.js';
84+
8285
@override
8386
String get loadLibrariesSnippet =>
8487
'for(let module of dart_library.libraries()) {\n'

dwds/lib/src/loaders/require.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ class RequireStrategy extends LoadStrategy {
169169
@override
170170
String get moduleFormat => 'amd';
171171

172+
@override
173+
String get loadLibrariesModule => 'require.js';
174+
172175
@override
173176
String get loadLibrariesSnippet =>
174177
'let libs = $loadModuleSnippet("dart_sdk").dart.getLibraries();\n';

dwds/lib/src/loaders/strategy.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ abstract class LoadStrategy {
3636
/// expression evaluation.
3737
String get moduleFormat;
3838

39+
/// Module containing code for loading libraries.
40+
///
41+
/// Used for preventing stepping into the library loading code.
42+
String get loadLibrariesModule;
43+
3944
/// Returns a snippet of JS code that loads all Dart libraries into a `libs`
4045
/// variable.
4146
String get loadLibrariesSnippet;

dwds/lib/src/servers/extension_debugger.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class ExtensionDebugger implements RemoteDebugger {
6464
(WipEvent event) => ExceptionThrownEvent(event.json));
6565

6666
final _scripts = <String, WipScript>{};
67+
final _scriptIds = <String, String>{};
6768

6869
ExtensionDebugger(this.sseConnection) {
6970
sseConnection.stream.listen((data) {
@@ -113,7 +114,13 @@ class ExtensionDebugger implements RemoteDebugger {
113114
close();
114115
}, onDone: close);
115116
onScriptParsed.listen((event) {
117+
// Remove stale scripts from cache.
118+
if (event.script.url.isNotEmpty &&
119+
_scriptIds.containsKey(event.script.url)) {
120+
_scripts.remove(_scriptIds[event.script.url]);
121+
}
116122
_scripts[event.script.scriptId] = event.script;
123+
_scriptIds[event.script.url] = event.script.scriptId;
117124
});
118125
// Listens for a page reload.
119126
onGlobalObjectCleared.listen((_) {

dwds/lib/src/services/expression_evaluator.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ class ExpressionEvaluator {
229229
return ret;
230230
}
231231

232+
bool _isUndefined(RemoteObject value) => value.type == 'undefined';
233+
232234
RemoteObject _formatCompilationError(String error) {
233235
// Frontend currently gives a text message including library name
234236
// and function name on compilation error. Strip this information
@@ -284,6 +286,7 @@ class ExpressionEvaluator {
284286
for (var p in variables) {
285287
var name = p.name;
286288
var value = p.value;
289+
if (_isUndefined(value)) continue;
287290

288291
if (scopeType == 'closure') {
289292
// Substitute potentially unavailable captures with their values from

0 commit comments

Comments
 (0)