Skip to content

Commit c29e5f8

Browse files
authored
Do not persist breakpoints across hot restarts or page refreshes (#2371)
1 parent 51b5484 commit c29e5f8

File tree

10 files changed

+128
-291
lines changed

10 files changed

+128
-291
lines changed

dwds/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Rename `dart_library.js` to `ddc_module_loader.js` to match SDK naming changes. - [#2360](https://github.com/dart-lang/webdev/pull/2360)
44
- Implement `setFlag` when it is called with `pause_isolates_on_start`. - [#2373](https://github.com/dart-lang/webdev/pull/2373)
5+
- Do not persist breakpoints across hot restarts or page reloads. - [#2371](https://github.com/dart-lang/webdev/pull/2371)
56

67
## 23.3.0
78

dwds/lib/src/debugging/debugger.dart

Lines changed: 57 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'package:dwds/src/utilities/server.dart';
1818
import 'package:dwds/src/utilities/shared.dart';
1919
import 'package:dwds/src/utilities/synchronized.dart';
2020
import 'package:logging/logging.dart';
21+
import 'package:path/path.dart' as p;
2122
import 'package:vm_service/vm_service.dart';
2223
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
2324
hide StackTrace;
@@ -36,6 +37,14 @@ const _pauseModePauseStates = {
3637
'unhandled': PauseState.uncaught,
3738
};
3839

40+
/// Mapping from the path of a script in Chrome to the Runtime.ScriptId Chrome
41+
/// uses to reference it.
42+
///
43+
/// See https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ScriptId
44+
///
45+
/// e.g. 'packages/myapp/main.dart.lib.js' -> '12'
46+
final chromePathToRuntimeScriptId = <String, String>{};
47+
3948
class Debugger extends Domain {
4049
static final logger = Logger('Debugger');
4150

@@ -44,18 +53,17 @@ class Debugger extends Domain {
4453
final StreamNotify _streamNotify;
4554
final Locations _locations;
4655
final SkipLists _skipLists;
47-
final String _root;
4856

4957
Debugger._(
5058
this._remoteDebugger,
5159
this._streamNotify,
5260
this._locations,
5361
this._skipLists,
54-
this._root,
62+
root,
5563
) : _breakpoints = _Breakpoints(
5664
locations: _locations,
5765
remoteDebugger: _remoteDebugger,
58-
root: _root,
66+
root: root,
5967
);
6068

6169
/// The breakpoints we have set so far, indexable by either
@@ -207,6 +215,7 @@ class Debugger extends Domain {
207215
// miss events.
208216
// Allow a null debugger/connection for unit tests.
209217
runZonedGuarded(() {
218+
_remoteDebugger.onScriptParsed.listen(_scriptParsedHandler);
210219
_remoteDebugger.onPaused.listen(_pauseHandler);
211220
_remoteDebugger.onResumed.listen(_resumeHandler);
212221
_remoteDebugger.onTargetCrashed.listen(_crashHandler);
@@ -253,67 +262,6 @@ class Debugger extends Domain {
253262
return breakpoint;
254263
}
255264

256-
Future<ScriptRef?> _updatedScriptRefFor(Breakpoint breakpoint) async {
257-
final oldRef = (breakpoint.location as SourceLocation).script;
258-
final uri = oldRef?.uri;
259-
if (uri == null) return null;
260-
final dartUri = DartUri(uri, _root);
261-
return await inspector.scriptRefFor(dartUri.serverPath);
262-
}
263-
264-
Future<void> reestablishBreakpoints(
265-
Set<Breakpoint> previousBreakpoints,
266-
Set<Breakpoint> disabledBreakpoints,
267-
) async {
268-
// Previous breakpoints were never removed from Chrome since we use
269-
// `setBreakpointByUrl`. We simply need to update the references.
270-
for (var breakpoint in previousBreakpoints) {
271-
final dartBpId = breakpoint.id!;
272-
final scriptRef = await _updatedScriptRefFor(breakpoint);
273-
final scriptUri = scriptRef?.uri;
274-
if (scriptRef != null && scriptUri != null) {
275-
final jsBpId = _breakpoints.jsIdFor(dartBpId)!;
276-
final updatedLocation = await _locations.locationForDart(
277-
DartUri(scriptUri, _root),
278-
_lineNumberFor(breakpoint),
279-
_columnNumberFor(breakpoint),
280-
);
281-
if (updatedLocation != null) {
282-
final updatedBreakpoint = _breakpoints._dartBreakpoint(
283-
scriptRef,
284-
updatedLocation,
285-
dartBpId,
286-
);
287-
_breakpoints._note(bp: updatedBreakpoint, jsId: jsBpId);
288-
_notifyBreakpoint(updatedBreakpoint);
289-
} else {
290-
logger.warning('Cannot update breakpoint $dartBpId:'
291-
' cannot update location.');
292-
}
293-
} else {
294-
logger.warning('Cannot update breakpoint $dartBpId:'
295-
' cannot find script ref.');
296-
}
297-
}
298-
299-
// Disabled breakpoints were actually removed from Chrome so simply add
300-
// them back.
301-
for (var breakpoint in disabledBreakpoints) {
302-
final scriptRef = await _updatedScriptRefFor(breakpoint);
303-
final scriptId = scriptRef?.id;
304-
if (scriptId != null) {
305-
await addBreakpoint(
306-
scriptId,
307-
_lineNumberFor(breakpoint),
308-
column: _columnNumberFor(breakpoint),
309-
);
310-
} else {
311-
logger.warning('Cannot update disabled breakpoint ${breakpoint.id}:'
312-
' cannot find script ref.');
313-
}
314-
}
315-
}
316-
317265
void _notifyBreakpoint(Breakpoint breakpoint) {
318266
final event = Event(
319267
kind: EventKind.kBreakpointAdded,
@@ -520,6 +468,31 @@ class Debugger extends Domain {
520468
return dartFrame;
521469
}
522470

471+
void _scriptParsedHandler(ScriptParsedEvent e) {
472+
final scriptPath = _pathForChromeScript(e.script.url);
473+
if (scriptPath != null) {
474+
chromePathToRuntimeScriptId[scriptPath] = e.script.scriptId;
475+
}
476+
}
477+
478+
String? _pathForChromeScript(String scriptUrl) {
479+
final scriptPathSegments = Uri.parse(scriptUrl).pathSegments;
480+
if (scriptPathSegments.isEmpty) {
481+
return null;
482+
}
483+
484+
final isInternal = globalToolConfiguration.appMetadata.isInternalBuild;
485+
const packagesDir = 'packages';
486+
if (isInternal && scriptUrl.contains(packagesDir)) {
487+
final packagesIdx = scriptPathSegments.indexOf(packagesDir);
488+
return p.joinAll(scriptPathSegments.sublist(packagesIdx));
489+
}
490+
491+
// Note: Replacing "\" with "/" is necessary because `joinAll` uses "\" if
492+
// the platform is Windows. However, only "/" is expected by the browser.
493+
return p.joinAll(scriptPathSegments).replaceAll('\\', '/');
494+
}
495+
523496
/// Handles pause events coming from the Chrome connection.
524497
Future<void> _pauseHandler(DebuggerPausedEvent e) async {
525498
final isolate = inspector.isolate;
@@ -738,14 +711,6 @@ Future<T> sendCommandAndValidateResult<T>(
738711
return result;
739712
}
740713

741-
/// Returns the Dart line number for the provided breakpoint.
742-
int _lineNumberFor(Breakpoint breakpoint) =>
743-
int.parse(breakpoint.id!.split('#').last.split(':').first);
744-
745-
/// Returns the Dart column number for the provided breakpoint.
746-
int _columnNumberFor(Breakpoint breakpoint) =>
747-
int.parse(breakpoint.id!.split('#').last.split(':').last);
748-
749714
/// Returns the breakpoint ID for the provided Dart script ID and Dart line
750715
/// number.
751716
String breakpointIdFor(String scriptId, int line, int column) =>
@@ -779,8 +744,10 @@ class _Breakpoints extends Domain {
779744
int line,
780745
int column,
781746
) async {
747+
print('creating breakpoint at $scriptId:$line:$column)');
782748
final dartScript = inspector.scriptWithId(scriptId);
783749
final dartScriptUri = dartScript?.uri;
750+
print('dart script uri is $dartScriptUri');
784751
Location? location;
785752
if (dartScriptUri != null) {
786753
final dartUri = DartUri(dartScriptUri, root);
@@ -853,22 +820,27 @@ class _Breakpoints extends Domain {
853820

854821
/// Calls the Chrome protocol setBreakpoint and returns the remote ID.
855822
Future<String?> _setJsBreakpoint(Location location) {
856-
// The module can be loaded from a nested path and contain an ETAG suffix.
857-
final urlRegex = '.*${location.jsLocation.module}.*';
858823
// Prevent `Aww, snap!` errors when setting multiple breakpoints
859824
// simultaneously by serializing the requests.
860825
return _queue.run(() async {
861-
final breakPointId = await sendCommandAndValidateResult<String>(
862-
remoteDebugger,
863-
method: 'Debugger.setBreakpointByUrl',
864-
resultField: 'breakpointId',
865-
params: {
866-
'urlRegex': urlRegex,
867-
'lineNumber': location.jsLocation.line,
868-
'columnNumber': location.jsLocation.column,
869-
},
870-
);
871-
return breakPointId;
826+
final scriptId = location.jsLocation.runtimeScriptId;
827+
if (scriptId != null) {
828+
return sendCommandAndValidateResult<String>(
829+
remoteDebugger,
830+
method: 'Debugger.setBreakpoint',
831+
resultField: 'breakpointId',
832+
params: {
833+
'location': {
834+
'lineNumber': location.jsLocation.line,
835+
'columnNumber': location.jsLocation.column,
836+
'scriptId': scriptId,
837+
},
838+
},
839+
);
840+
} else {
841+
_logger.fine('No runtime script ID for location $location');
842+
return null;
843+
}
872844
});
873845
}
874846

dwds/lib/src/debugging/location.dart

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class Location {
3333
TargetLineEntry lineEntry,
3434
TargetEntry entry,
3535
DartUri dartUri,
36+
String? runtimeScriptId,
3637
) {
3738
final dartLine = entry.sourceLine;
3839
final dartColumn = entry.sourceColumn;
@@ -42,7 +43,7 @@ class Location {
4243
// lineEntry data is 0 based according to:
4344
// https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k
4445
return Location._(
45-
JsLocation.fromZeroBased(module, jsLine, jsColumn),
46+
JsLocation.fromZeroBased(module, jsLine, jsColumn, runtimeScriptId),
4647
DartLocation.fromZeroBased(dartUri, dartLine ?? 0, dartColumn ?? 0),
4748
);
4849
}
@@ -104,10 +105,16 @@ class JsLocation {
104105
/// 0 based column offset within the JS source code.
105106
final int column;
106107

108+
/// The Runtime.ScriptId of a script in Chrome.
109+
///
110+
/// See https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-ScriptId
111+
String? runtimeScriptId;
112+
107113
JsLocation._(
108114
this.module,
109115
this.line,
110116
this.column,
117+
this.runtimeScriptId,
111118
);
112119

113120
int compareTo(JsLocation other) => compareToLine(other.line, other.column);
@@ -122,8 +129,13 @@ class JsLocation {
122129

123130
// JS Location is 0 based according to:
124131
// https://chromedevtools.github.io/devtools-protocol/tot/Debugger#type-Location
125-
factory JsLocation.fromZeroBased(String module, int line, int column) =>
126-
JsLocation._(module, line, column);
132+
factory JsLocation.fromZeroBased(
133+
String module,
134+
int line,
135+
int column,
136+
String? runtimeScriptId,
137+
) =>
138+
JsLocation._(module, line, column, runtimeScriptId);
127139
}
128140

129141
/// Contains meta data for known [Location]s.
@@ -321,6 +333,10 @@ class Locations {
321333
p.url.dirname('/${stripLeadingSlashes(modulePath)}');
322334

323335
if (sourceMapContents == null) return result;
336+
337+
final runtimeScriptId =
338+
await _modules.getRuntimeScriptIdForModule(_entrypoint, module);
339+
324340
// This happens to be a [SingleMapping] today in DDC.
325341
final mapping = parse(sourceMapContents);
326342
if (mapping is SingleMapping) {
@@ -339,12 +355,14 @@ class Locations {
339355
);
340356

341357
final dartUri = DartUri(path, _root);
358+
342359
result.add(
343360
Location.from(
344361
modulePath,
345362
lineEntry,
346363
entry,
347364
dartUri,
365+
runtimeScriptId,
348366
),
349367
);
350368
}

dwds/lib/src/debugging/modules.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:async/async.dart';
66
import 'package:dwds/src/config/tool_configuration.dart';
7+
import 'package:dwds/src/debugging/debugger.dart';
78
import 'package:dwds/src/utilities/dart_uri.dart';
89
import 'package:logging/logging.dart';
910

@@ -62,6 +63,15 @@ class Modules {
6263
return _sourceToModule;
6364
}
6465

66+
Future<String?> getRuntimeScriptIdForModule(
67+
String entrypoint,
68+
String module,
69+
) async {
70+
final serverPath = await globalToolConfiguration.loadStrategy
71+
.serverPathForModule(entrypoint, module);
72+
return chromePathToRuntimeScriptId[serverPath];
73+
}
74+
6575
/// Initializes [_sourceToModule] and [_sourceToLibrary].
6676
Future<void> _initializeMapping() async {
6777
final provider =

dwds/lib/src/services/chrome_proxy_service.dart

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,6 @@ class ChromeProxyService implements VmServiceInterface {
9999
Stream<bool> get pauseIsolatesOnStartStream =>
100100
_pauseIsolatesOnStartController.stream;
101101

102-
final _disabledBreakpoints = <Breakpoint>{};
103-
final _previousBreakpoints = <Breakpoint>{};
104-
105102
final _logger = Logger('ChromeProxyService');
106103

107104
final ExpressionCompiler? _compiler;
@@ -295,12 +292,6 @@ class ChromeProxyService implements VmServiceInterface {
295292

296293
safeUnawaited(_prewarmExpressionCompilerCache());
297294

298-
await debugger.reestablishBreakpoints(
299-
_previousBreakpoints,
300-
_disabledBreakpoints,
301-
);
302-
_disabledBreakpoints.clear();
303-
304295
safeUnawaited(
305296
appConnection.onStart.then((_) {
306297
debugger.resumeFromStart();
@@ -376,19 +367,15 @@ class ChromeProxyService implements VmServiceInterface {
376367
);
377368
_vm.isolates?.removeWhere((ref) => ref.id == isolate.id);
378369
_inspector = null;
379-
_previousBreakpoints.clear();
380-
_previousBreakpoints.addAll(isolate.breakpoints ?? []);
381370
_expressionEvaluator?.close();
382371
_consoleSubscription?.cancel();
383372
_consoleSubscription = null;
384373
}
385374

386375
Future<void> disableBreakpoints() async {
387-
_disabledBreakpoints.clear();
388376
if (!_isIsolateRunning) return;
389377
final isolate = inspector.isolate;
390378

391-
_disabledBreakpoints.addAll(isolate.breakpoints ?? []);
392379
for (var breakpoint in isolate.breakpoints?.toList() ?? []) {
393380
await (await debuggerFuture).removeBreakpoint(breakpoint.id);
394381
}
@@ -1121,8 +1108,6 @@ ${globalToolConfiguration.loadStrategy.loadModuleSnippet}("dart_sdk").developer.
11211108
) async {
11221109
await isInitialized;
11231110
_checkIsolate('removeBreakpoint', isolateId);
1124-
_disabledBreakpoints
1125-
.removeWhere((breakpoint) => breakpoint.id == breakpointId);
11261111
return (await debuggerFuture).removeBreakpoint(breakpointId);
11271112
}
11281113

dwds/test/fixtures/fakes.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,13 @@ class FakeModules implements Modules {
169169

170170
@override
171171
Future<String> moduleForLibrary(String libraryUri) async => _module;
172+
173+
@override
174+
Future<String?> getRuntimeScriptIdForModule(
175+
String entrypoint,
176+
String module,
177+
) async =>
178+
null;
172179
}
173180

174181
class FakeWebkitDebugger implements WebkitDebugger {

0 commit comments

Comments
 (0)