Skip to content

Commit 1525eb1

Browse files
committed
Wait until all scripts are parsed before recomputing metadata on a hot reload
Fixes #2640 On a hot reload, we wait until all the scripts are downloaded, but we don't wait until all of them are parsed and a script ID is created for them to refer to. This then results in incorrect metadata being computed. Instead, return the srcs that are being loaded during a hot reload, use a controller to determine when scripts are parsed, and only compute metadata once we have parsed all the reloaded scripts. In order to compare the parsed scripts' URLs with the reloaded scripts' URLs, we now require full URLs in the hot reload sources metadata. This is already true in Flutter tools, so this just canonicalizes that and modifies the tests to do the same. To be consistent, hot restart also provides the full URL in the DDC library bundle format and the bootstrap is modified to reflect that (and to clean up some unused code around module tracking).
1 parent 1b8586d commit 1525eb1

File tree

15 files changed

+239
-128
lines changed

15 files changed

+239
-128
lines changed

dwds/lib/src/debugging/debugger.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ class Debugger extends Domain {
5656

5757
int _frameErrorCount = 0;
5858

59+
final StreamController<String> parsedScriptsController =
60+
StreamController.broadcast();
61+
5962
Debugger._(
6063
this._remoteDebugger,
6164
this._streamNotify,
@@ -489,6 +492,9 @@ class Debugger extends Domain {
489492
final scriptPath = _pathForChromeScript(e.script.url);
490493
if (scriptPath != null) {
491494
chromePathToRuntimeScriptId[scriptPath] = e.script.scriptId;
495+
if (parsedScriptsController.hasListener) {
496+
parsedScriptsController.sink.add(e.script.url);
497+
}
492498
}
493499
}
494500

dwds/lib/src/debugging/modules.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,6 @@ class Modules {
9898
) async {
9999
final serverPath = await globalToolConfiguration.loadStrategy
100100
.serverPathForModule(entrypoint, module);
101-
// TODO(srujzs): We should wait until all scripts are parsed before
102-
// accessing after a hot reload. See
103-
// https://github.com/dart-lang/webdev/issues/2640.
104101
return chromePathToRuntimeScriptId[serverPath];
105102
}
106103

dwds/lib/src/injected/client.js

Lines changed: 11 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dwds/lib/src/loaders/ddc_library_bundle.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,16 @@ class DdcLibraryBundleStrategy extends LoadStrategy {
110110
/// ```json
111111
/// [
112112
/// {
113-
/// "src": "<file_name>",
113+
/// "src": "<base_uri>/<file_name>",
114+
/// "module": "<module_name>",
114115
/// "libraries": ["<lib1>", "<lib2>"],
115116
/// },
116117
/// ]
117118
/// ```
118119
///
119120
/// `src`: A string that corresponds to the file path containing a DDC library
120121
/// bundle.
122+
/// `module`: The name of the library bundle in `src`.
121123
/// `libraries`: An array of strings containing the libraries that were
122124
/// compiled in `src`.
123125
///

dwds/lib/src/services/chrome_proxy_service.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,15 +1221,42 @@ class ChromeProxyService implements VmServiceInterface {
12211221
Future<void> _performClientSideHotReload(String isolateId) async {
12221222
_logger.info('Attempting a hot reload');
12231223

1224+
final debugger = await debuggerFuture;
1225+
final reloadedSrcs = <String>{};
1226+
final computedReloadedSrcs = Completer<void>();
1227+
final parsedAllReloadedSrcs = Completer<void>();
1228+
// Wait until all the reloaded scripts are parsed before we reinitialize
1229+
// metadata below.
1230+
final parsedScriptsSubscription = debugger.parsedScriptsController.stream
1231+
.listen((url) {
1232+
computedReloadedSrcs.future.then((_) {
1233+
reloadedSrcs.remove(Uri.parse(url).normalizePath().path);
1234+
if (reloadedSrcs.isEmpty) {
1235+
parsedAllReloadedSrcs.complete();
1236+
}
1237+
});
1238+
});
1239+
12241240
// Initiate a hot reload.
12251241
_logger.info('Issuing \$dartHotReloadStartDwds request');
12261242
final remoteObject = await inspector.jsEvaluate(
12271243
'\$dartHotReloadStartDwds();',
12281244
awaitPromise: true,
12291245
returnByValue: true,
12301246
);
1231-
final reloadedModulesToLibraries =
1232-
(remoteObject.value as Map).cast<String, List>();
1247+
final reloadedSrcModuleLibraries = (remoteObject.value as List).cast<Map>();
1248+
final reloadedModulesToLibraries = <String, List<String>>{};
1249+
for (final srcModuleLibrary in reloadedSrcModuleLibraries) {
1250+
final srcModuleLibraryCast = srcModuleLibrary.cast<String, Object>();
1251+
reloadedSrcs.add(
1252+
Uri.parse(srcModuleLibraryCast['src'] as String).normalizePath().path,
1253+
);
1254+
reloadedModulesToLibraries[srcModuleLibraryCast['module'] as String] =
1255+
(srcModuleLibraryCast['libraries'] as List).cast<String>();
1256+
}
1257+
computedReloadedSrcs.complete();
1258+
if (reloadedSrcs.isNotEmpty) await parsedAllReloadedSrcs.future;
1259+
await parsedScriptsSubscription.cancel();
12331260

12341261
if (!pauseIsolatesOnStart) {
12351262
// Finish hot reload immediately.

dwds/test/fixtures/context.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -339,11 +339,12 @@ class TestContext {
339339
_hostname = appMetadata.hostname;
340340
await webRunner.run(
341341
frontendServerFileSystem,
342-
_hostname,
343-
assetServerPort,
344-
filePathToServe,
342+
hostname: _hostname,
343+
port: assetServerPort,
344+
index: filePathToServe,
345345
initialCompile: true,
346346
fullRestart: false,
347+
fileServerUri: null,
347348
);
348349

349350
if (testSettings.enableExpressionEvaluation) {
@@ -414,7 +415,7 @@ class TestContext {
414415
'remote-debugging-port=$debugPort',
415416
if (enableDebugExtension)
416417
'--load-extension=debug_extension/prod_build',
417-
if (headless) '--headless',
418+
// if (headless) '--headless',
418419
],
419420
},
420421
});
@@ -433,6 +434,10 @@ class TestContext {
433434
final appConnectionCompleter = Completer();
434435
final connection = ChromeConnection('localhost', debugPort);
435436

437+
// TODO(srujzs): In the case of the frontend server, it doesn't make sense
438+
// that we initialize a new HTTP server instead of reusing the one in
439+
// `TestAssetServer`. Why not just use that one? That seems to match with
440+
// what Flutter tools is doing.
436441
_testServer = await TestServer.start(
437442
debugSettings: debugSettings.copyWith(
438443
expressionCompiler: expressionCompiler,
@@ -592,11 +597,9 @@ class TestContext {
592597
Future<void> recompile({required bool fullRestart}) async {
593598
await webRunner.run(
594599
frontendServerFileSystem,
595-
_hostname,
596-
await findUnusedPort(),
597-
webCompatiblePath([project.directoryToServe, project.filePathToServe]),
598600
initialCompile: false,
599601
fullRestart: fullRestart,
602+
fileServerUri: Uri.parse('http://${testServer.host}:${testServer.port}'),
600603
);
601604
return;
602605
}

dwds/test/hot_reload_breakpoints_test.dart

Lines changed: 126 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
library;
99

1010
import 'dart:async';
11-
import 'dart:io';
1211

1312
import 'package:dwds/expression_compiler.dart';
1413
import 'package:test/test.dart';
@@ -207,6 +206,37 @@ void main() {
207206
Future<void> waitForBreakpoint() =>
208207
expectLater(stream, emitsThrough(_hasKind(EventKind.kPauseBreakpoint)));
209208

209+
test('empty hot reload keeps breakpoints', () async {
210+
final genString = 'main gen0';
211+
212+
final bp = await addBreakpoint(
213+
file: mainFile,
214+
breakpointMarker: callLogMarker,
215+
);
216+
217+
var breakpointFuture = waitForBreakpoint();
218+
219+
await callEvaluate();
220+
221+
// Should break at `callLog`.
222+
await breakpointFuture;
223+
await resumeAndExpectLog(genString);
224+
225+
await context.recompile(fullRestart: false);
226+
227+
await hotReloadAndHandlePausePost([
228+
(file: mainFile, breakpointMarker: callLogMarker, bp: bp),
229+
]);
230+
231+
breakpointFuture = waitForBreakpoint();
232+
233+
await callEvaluate();
234+
235+
// Should break at `callLog`.
236+
await breakpointFuture;
237+
await resumeAndExpectLog(genString);
238+
});
239+
210240
test('after edit and hot reload, breakpoint is in new file', () async {
211241
final oldString = 'main gen0';
212242
final newString = 'main gen1';
@@ -359,52 +389,109 @@ void main() {
359389
},
360390
);
361391

362-
test(
363-
'breakpoint in captured code is deleted',
364-
() async {
365-
var bp = await addBreakpoint(
366-
file: mainFile,
367-
breakpointMarker: capturedStringMarker,
368-
);
392+
// Test that we wait for all scripts to be parsed first before computing
393+
// location metadata.
394+
test('after adding many files and putting breakpoint in the last one,'
395+
'breakpoint is correctly registered', () async {
396+
final genLog = 'main gen0';
369397

370-
final oldLog = "log('\$mainValue');";
371-
final newLog = "log('\${closure()}');";
372-
await makeEditAndRecompile(mainFile, oldLog, newLog);
398+
final bp = await addBreakpoint(
399+
file: mainFile,
400+
breakpointMarker: callLogMarker,
401+
);
373402

374-
bp =
375-
(await hotReloadAndHandlePausePost([
376-
(file: mainFile, breakpointMarker: capturedStringMarker, bp: bp),
377-
])).first;
403+
var breakpointFuture = waitForBreakpoint();
378404

379-
final breakpointFuture = waitForBreakpoint();
405+
await callEvaluate();
380406

381-
await callEvaluate();
407+
// Should break at `callLog`.
408+
await breakpointFuture;
409+
await resumeAndExpectLog(genLog);
382410

383-
// Should break at `capturedString`.
384-
await breakpointFuture;
385-
final oldCapturedString = 'captured closure gen0';
386-
// Closure gets evaluated for the first time.
387-
await resumeAndExpectLog(oldCapturedString);
388-
389-
final newCapturedString = 'captured closure gen1';
390-
await makeEditAndRecompile(
391-
mainFile,
392-
oldCapturedString,
393-
newCapturedString,
411+
// Add library files, import them, but only refer to the last one in main.
412+
final numFiles = 50;
413+
for (var i = 1; i <= numFiles; i++) {
414+
final libFile = 'library$i.dart';
415+
context.addLibraryFile(
416+
libFileName: libFile,
417+
contents: '''String get libraryValue$i {
418+
return 'lib gen$i'; // Breakpoint: libValue$i
419+
}''',
394420
);
421+
final oldImports = "import 'dart:js_interop';";
422+
final newImports =
423+
'$oldImports\n'
424+
"import 'package:_test_hot_reload_breakpoints/$libFile';";
425+
makeEdit(mainFile, oldImports, newImports);
426+
}
427+
final oldLog = "log('\$mainValue');";
428+
final newLog = "log('\$libraryValue$numFiles');";
429+
await makeEditAndRecompile(mainFile, oldLog, newLog);
395430

396-
await hotReloadAndHandlePausePost([
397-
(file: mainFile, breakpointMarker: capturedStringMarker, bp: bp),
398-
]);
431+
await hotReloadAndHandlePausePost([
432+
(file: mainFile, breakpointMarker: callLogMarker, bp: bp),
433+
(
434+
file: 'library$numFiles.dart',
435+
breakpointMarker: 'libValue$numFiles',
436+
bp: null,
437+
),
438+
]);
399439

400-
// Breakpoint should not be hit as it's now deleted. We should also see
401-
// the old string still as the closure has not been reevaluated.
402-
await callEvaluateAndExpectLog(oldCapturedString);
403-
},
404-
// TODO(srujzs): Re-enable after
405-
// https://github.com/dart-lang/webdev/issues/2640.
406-
skip: Platform.isWindows,
407-
);
440+
breakpointFuture = waitForBreakpoint();
441+
442+
await callEvaluate();
443+
444+
// Should break at `callLog`.
445+
await breakpointFuture;
446+
447+
breakpointFuture = waitForBreakpoint();
448+
449+
await resume();
450+
// Should break at the breakpoint in the last file.
451+
await breakpointFuture;
452+
await resumeAndExpectLog('lib gen$numFiles');
453+
});
454+
455+
test('breakpoint in captured code is deleted', () async {
456+
var bp = await addBreakpoint(
457+
file: mainFile,
458+
breakpointMarker: capturedStringMarker,
459+
);
460+
461+
final oldLog = "log('\$mainValue');";
462+
final newLog = "log('\${closure()}');";
463+
await makeEditAndRecompile(mainFile, oldLog, newLog);
464+
465+
bp =
466+
(await hotReloadAndHandlePausePost([
467+
(file: mainFile, breakpointMarker: capturedStringMarker, bp: bp),
468+
])).first;
469+
470+
final breakpointFuture = waitForBreakpoint();
471+
472+
await callEvaluate();
473+
474+
// Should break at `capturedString`.
475+
await breakpointFuture;
476+
final oldCapturedString = 'captured closure gen0';
477+
// Closure gets evaluated for the first time.
478+
await resumeAndExpectLog(oldCapturedString);
479+
480+
final newCapturedString = 'captured closure gen1';
481+
await makeEditAndRecompile(
482+
mainFile,
483+
oldCapturedString,
484+
newCapturedString,
485+
);
486+
487+
await hotReloadAndHandlePausePost([
488+
(file: mainFile, breakpointMarker: capturedStringMarker, bp: bp),
489+
]);
490+
491+
// Breakpoint should not be hit as it's now deleted. We should also see
492+
// the old string still as the closure has not been reevaluated.
493+
await callEvaluateAndExpectLog(oldCapturedString);
494+
});
408495
}, timeout: Timeout.factor(2));
409496

410497
group('when pause_isolates_on_start is false', () {

0 commit comments

Comments
 (0)