Skip to content

Commit 8e2a6fc

Browse files
authored
Implement hot reload using the DDC library bundle format (flutter#162498)
dart-lang/webdev#2516 - Updates restart/reload code to accept a resetCompiler boolean to disambiguate between whether this is a full restart and whether to reset the resident compiler. - Adds code to call reloadSources in DWDS and handle the response (including any errors). - Adds code to invoke reassemble. - Adds code to emit a script that DWDS can later consume that contains the changed sources and their associated libraries. This is used to hot reload. The bootstrapper puts this in the global window. DWDS should be updated to accept it in the provider itself. See dart-lang/webdev#2584. - Adds code to parse module metadata from the frontend server. This is identical to the implementation in DWDS % addressing type-related lints. - Adds tests that run the existing hot reload tests but with web. Some modifications are mode, including waiting for Flutter runs to finish executing, and skipping a test that's not possible on the web. Needs DWDS 24.3.4 to be published first and used before we can land. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing.
1 parent 4d08217 commit 8e2a6fc

21 files changed

+866
-360
lines changed

packages/flutter_tools/lib/src/devfs.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,10 @@ class DevFS {
551551
/// Updates files on the device.
552552
///
553553
/// Returns the number of bytes synced.
554+
///
555+
/// If [fullRestart] is true, assumes this is a hot restart instead of a hot
556+
/// reload. If [resetCompiler] is true, sends a `reset` instruction to the
557+
/// frontend server.
554558
Future<UpdateFSReport> update({
555559
required Uri mainUri,
556560
required ResidentCompiler generator,
@@ -566,6 +570,7 @@ class DevFS {
566570
AssetBundle? bundle,
567571
bool bundleFirstUpload = false,
568572
bool fullRestart = false,
573+
bool resetCompiler = false,
569574
File? dartPluginRegistrant,
570575
}) async {
571576
final DateTime candidateCompileTime = DateTime.now();
@@ -577,7 +582,7 @@ class DevFS {
577582
final List<Future<void>> pendingAssetBuilds = <Future<void>>[];
578583
bool assetBuildFailed = false;
579584
int syncedBytes = 0;
580-
if (fullRestart) {
585+
if (resetCompiler) {
581586
generator.reset();
582587
}
583588
// On a full restart, or on an initial compile for the attach based workflow,

packages/flutter_tools/lib/src/isolated/devfs_web.dart

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import '../web/bootstrap.dart';
4040
import '../web/chrome.dart';
4141
import '../web/compile.dart';
4242
import '../web/memory_fs.dart';
43+
import '../web/module_metadata.dart';
4344
import '../web_template.dart';
4445

4546
typedef DwdsLauncher =
@@ -158,6 +159,16 @@ class WebAssetServer implements AssetReader {
158159
/// If [writeRestartScripts] is true, writes a list of sources mapped to their
159160
/// ids to the file system that can then be consumed by the hot restart
160161
/// callback.
162+
///
163+
/// For example:
164+
/// ```json
165+
/// [
166+
/// {
167+
/// "src": "<file_name>",
168+
/// "id": "<id>",
169+
/// },
170+
/// ]
171+
/// ```
161172
void performRestart(List<String> modules, {required bool writeRestartScripts}) {
162173
for (final String module in modules) {
163174
// We skip computing the digest by using the hashCode of the underlying buffer.
@@ -174,11 +185,44 @@ class WebAssetServer implements AssetReader {
174185
for (final String src in modules) {
175186
srcIdsList.add(<String, String>{'src': '$src?gen=$_hotRestartGeneration', 'id': src});
176187
}
177-
writeFile('main.dart.js.restartScripts', json.encode(srcIdsList));
188+
writeFile('restart_scripts.json', json.encode(srcIdsList));
178189
}
179190
_hotRestartGeneration++;
180191
}
181192

193+
/// Given a list of [modules] that need to be reloaded, writes a file that
194+
/// contains a list of objects each with two fields:
195+
///
196+
/// `src`: A string that corresponds to the file path containing a DDC library
197+
/// bundle.
198+
/// `libraries`: An array of strings containing the libraries that were
199+
/// compiled in `src`.
200+
///
201+
/// For example:
202+
/// ```json
203+
/// [
204+
/// {
205+
/// "src": "<file_name>",
206+
/// "libraries": ["<lib1>", "<lib2>"],
207+
/// },
208+
/// ]
209+
/// ```
210+
///
211+
/// The path of the output file should stay consistent across the lifetime of
212+
/// the app.
213+
void performReload(List<String> modules) {
214+
final List<Map<String, Object>> moduleToLibrary = <Map<String, Object>>[];
215+
for (final String module in modules) {
216+
final ModuleMetadata metadata = ModuleMetadata.fromJson(
217+
json.decode(utf8.decode(_webMemoryFS.metadataFiles['$module.metadata']!.toList()))
218+
as Map<String, dynamic>,
219+
);
220+
final List<String> libraries = metadata.libraries.keys.toList();
221+
moduleToLibrary.add(<String, Object>{'src': module, 'libraries': libraries});
222+
}
223+
writeFile('reload_scripts.json', json.encode(moduleToLibrary));
224+
}
225+
182226
@visibleForTesting
183227
List<String> write(File codeFile, File manifestFile, File sourcemapFile, File metadataFile) {
184228
return _webMemoryFS.write(codeFile, manifestFile, sourcemapFile, metadataFile);
@@ -1001,6 +1045,7 @@ class WebDevFS implements DevFS {
10011045
AssetBundle? bundle,
10021046
bool bundleFirstUpload = false,
10031047
bool fullRestart = false,
1048+
bool resetCompiler = false,
10041049
String? projectRootPath,
10051050
File? dartPluginRegistrant,
10061051
}) async {
@@ -1077,7 +1122,7 @@ class WebDevFS implements DevFS {
10771122
await _validateTemplateFile('index.html');
10781123
await _validateTemplateFile('flutter_bootstrap.js');
10791124
final DateTime candidateCompileTime = DateTime.now();
1080-
if (fullRestart) {
1125+
if (resetCompiler) {
10811126
generator.reset();
10821127
}
10831128

@@ -1122,8 +1167,11 @@ class WebDevFS implements DevFS {
11221167
} on FileSystemException catch (err) {
11231168
throwToolExit('Failed to load recompiled sources:\n$err');
11241169
}
1125-
1126-
webAssetServer.performRestart(modules, writeRestartScripts: ddcModuleSystem);
1170+
if (fullRestart) {
1171+
webAssetServer.performRestart(modules, writeRestartScripts: ddcModuleSystem);
1172+
} else {
1173+
webAssetServer.performReload(modules);
1174+
}
11271175
return UpdateFSReport(
11281176
success: true,
11291177
syncedBytes: codeFile.lengthSync(),

packages/flutter_tools/lib/src/isolated/resident_web_runner.dart

Lines changed: 112 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
322322
}
323323
if (debuggingOptions.buildInfo.isDebug && !debuggingOptions.webUseWasm) {
324324
await runSourceGenerators();
325-
final UpdateFSReport report = await _updateDevFS(fullRestart: true);
325+
final UpdateFSReport report = await _updateDevFS(fullRestart: true, resetCompiler: true);
326326
if (!report.success) {
327327
_logger.printError('Failed to compile application.');
328328
appFailedToStart();
@@ -406,15 +406,28 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
406406
bool benchmarkMode = false,
407407
}) async {
408408
final DateTime start = _systemClock.now();
409-
final Status status = _logger.startProgress(
410-
'Performing hot restart...',
411-
progressId: 'hot.restart',
412-
);
409+
final Status status;
410+
if (debuggingOptions.buildInfo.ddcModuleFormat != DdcModuleFormat.ddc ||
411+
debuggingOptions.buildInfo.canaryFeatures == false) {
412+
// Triggering hot reload performed hot restart for the old module formats
413+
// historically. Keep that behavior and only perform hot reload when the
414+
// new module format is used.
415+
fullRestart = true;
416+
}
417+
if (fullRestart) {
418+
status = _logger.startProgress('Performing hot restart...', progressId: 'hot.restart');
419+
} else {
420+
status = _logger.startProgress('Performing hot reload...', progressId: 'hot.reload');
421+
}
413422

414423
if (debuggingOptions.buildInfo.isDebug && !debuggingOptions.webUseWasm) {
415424
await runSourceGenerators();
416-
// Full restart is always false for web, since the extra recompile is wasteful.
417-
final UpdateFSReport report = await _updateDevFS();
425+
// Don't reset the resident compiler for web, since the extra recompile is
426+
// wasteful.
427+
final UpdateFSReport report = await _updateDevFS(
428+
fullRestart: fullRestart,
429+
resetCompiler: false,
430+
);
418431
if (report.success) {
419432
device!.generator!.accept();
420433
} else {
@@ -448,10 +461,32 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
448461
if (!deviceIsDebuggable) {
449462
_logger.printStatus('Recompile complete. Page requires refresh.');
450463
} else if (isRunningDebug) {
451-
// If the hot-restart service extension method is registered, then use
452-
// it. Otherwise, default to calling "hotRestart" without a namespace.
453-
final String hotRestartMethod = _registeredMethodsForService['hotRestart'] ?? 'hotRestart';
454-
await _vmService.service.callMethod(hotRestartMethod);
464+
if (fullRestart) {
465+
// If the hot-restart service extension method is registered, then use
466+
// it. Otherwise, default to calling "hotRestart" without a namespace.
467+
final String hotRestartMethod =
468+
_registeredMethodsForService['hotRestart'] ?? 'hotRestart';
469+
await _vmService.service.callMethod(hotRestartMethod);
470+
} else {
471+
// Isolates don't work on web. For lack of a better value, pass an
472+
// empty string for the isolate id.
473+
final vmservice.ReloadReport report = await _vmService.service.reloadSources('');
474+
final ReloadReportContents contents = ReloadReportContents.fromReloadReport(report);
475+
final bool success = contents.success ?? false;
476+
if (!success) {
477+
// Rejections happen at compile-time for the web, so in theory,
478+
// nothing should go wrong here. However, if DWDS or the DDC runtime
479+
// has some internal error, we should still surface it to make
480+
// debugging easier.
481+
String reloadFailedMessage = 'Hot reload failed:';
482+
globals.printError(reloadFailedMessage);
483+
for (final ReasonForCancelling reason in contents.notices) {
484+
reloadFailedMessage += reason.toString();
485+
globals.printError(reason.toString());
486+
}
487+
return OperationResult(1, reloadFailedMessage);
488+
}
489+
}
455490
} else {
456491
// On non-debug builds, a hard refresh is required to ensure the
457492
// up to date sources are loaded.
@@ -467,40 +502,76 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
467502

468503
final Duration elapsed = _systemClock.now().difference(start);
469504
final String elapsedMS = getElapsedAsMilliseconds(elapsed);
470-
_logger.printStatus('Restarted application in $elapsedMS.');
505+
_logger.printStatus('${fullRestart ? 'Restarted' : 'Reloaded'} application in $elapsedMS.');
471506

472-
unawaited(residentDevtoolsHandler!.hotRestart(flutterDevices));
507+
if (fullRestart) {
508+
unawaited(residentDevtoolsHandler!.hotRestart(flutterDevices));
509+
}
473510

474511
// Don't track restart times for dart2js builds or web-server devices.
475512
if (debuggingOptions.buildInfo.isDebug && deviceIsDebuggable) {
476-
_analytics.send(
477-
Event.timing(
478-
workflow: 'hot',
479-
variableName: 'web-incremental-restart',
480-
elapsedMilliseconds: elapsed.inMilliseconds,
481-
),
482-
);
513+
// TODO(srujzs): There are a number of fields that the VM tracks in the
514+
// analytics that we do not for both hot restart and reload. We should
515+
// unify that.
516+
final String targetPlatform = getNameForTargetPlatform(TargetPlatform.web_javascript);
483517
final String sdkName = await device!.device!.sdkNameAndVersion;
484-
HotEvent(
485-
'restart',
486-
targetPlatform: getNameForTargetPlatform(TargetPlatform.web_javascript),
487-
sdkName: sdkName,
488-
emulator: false,
489-
fullRestart: true,
490-
reason: reason,
491-
overallTimeInMs: elapsed.inMilliseconds,
492-
).send();
493-
_analytics.send(
494-
Event.hotRunnerInfo(
495-
label: 'restart',
496-
targetPlatform: getNameForTargetPlatform(TargetPlatform.web_javascript),
518+
if (fullRestart) {
519+
_analytics.send(
520+
Event.timing(
521+
workflow: 'hot',
522+
variableName: 'web-incremental-restart',
523+
elapsedMilliseconds: elapsed.inMilliseconds,
524+
),
525+
);
526+
HotEvent(
527+
'restart',
528+
targetPlatform: targetPlatform,
497529
sdkName: sdkName,
498530
emulator: false,
499531
fullRestart: true,
500532
reason: reason,
501533
overallTimeInMs: elapsed.inMilliseconds,
502-
),
503-
);
534+
).send();
535+
_analytics.send(
536+
Event.hotRunnerInfo(
537+
label: 'restart',
538+
targetPlatform: targetPlatform,
539+
sdkName: sdkName,
540+
emulator: false,
541+
fullRestart: true,
542+
reason: reason,
543+
overallTimeInMs: elapsed.inMilliseconds,
544+
),
545+
);
546+
} else {
547+
_analytics.send(
548+
Event.timing(
549+
workflow: 'hot',
550+
variableName: 'reload',
551+
elapsedMilliseconds: elapsed.inMilliseconds,
552+
),
553+
);
554+
HotEvent(
555+
'reload',
556+
targetPlatform: targetPlatform,
557+
sdkName: sdkName,
558+
emulator: false,
559+
fullRestart: false,
560+
reason: reason,
561+
overallTimeInMs: elapsed.inMilliseconds,
562+
).send();
563+
_analytics.send(
564+
Event.hotRunnerInfo(
565+
label: 'reload',
566+
targetPlatform: targetPlatform,
567+
sdkName: sdkName,
568+
emulator: false,
569+
fullRestart: false,
570+
reason: reason,
571+
overallTimeInMs: elapsed.inMilliseconds,
572+
),
573+
);
574+
}
504575
}
505576
return OperationResult.ok;
506577
}
@@ -551,7 +622,10 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
551622
return result!.absolute.uri;
552623
}
553624

554-
Future<UpdateFSReport> _updateDevFS({bool fullRestart = false}) async {
625+
Future<UpdateFSReport> _updateDevFS({
626+
required bool fullRestart,
627+
required bool resetCompiler,
628+
}) async {
555629
final bool isFirstUpload = !assetBundle.wasBuiltOnce();
556630
final bool rebuildBundle = assetBundle.needsBuild();
557631
if (rebuildBundle) {
@@ -584,8 +658,9 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
584658
bundleFirstUpload: isFirstUpload,
585659
generator: device!.generator!,
586660
fullRestart: fullRestart,
661+
resetCompiler: resetCompiler,
587662
dillOutputPath: dillOutputPath,
588-
pathToReload: getReloadPath(fullRestart: fullRestart, swap: false),
663+
pathToReload: getReloadPath(resetCompiler: resetCompiler, swap: false),
589664
invalidatedFiles: invalidationResult.uris!,
590665
packageConfig: invalidationResult.packageConfig!,
591666
trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation,

packages/flutter_tools/lib/src/resident_runner.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ class FlutterDevice {
570570
bundleFirstUpload: bundleFirstUpload,
571571
generator: generator!,
572572
fullRestart: fullRestart,
573+
resetCompiler: fullRestart,
573574
dillOutputPath: dillOutputPath,
574575
trackWidgetCreation: buildInfo.trackWidgetCreation,
575576
pathToReload: pathToReload,
@@ -1112,8 +1113,8 @@ abstract class ResidentRunner extends ResidentHandlers {
11121113

11131114
String get dillOutputPath =>
11141115
_dillOutputPath ?? globals.fs.path.join(artifactDirectory.path, 'app.dill');
1115-
String getReloadPath({bool fullRestart = false, required bool swap}) {
1116-
if (!fullRestart) {
1116+
String getReloadPath({bool resetCompiler = false, required bool swap}) {
1117+
if (!resetCompiler) {
11171118
return 'main.dart.incremental.dill';
11181119
}
11191120
return 'main.dart${swap ? '.swap' : ''}.dill';

packages/flutter_tools/lib/src/run_hot.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ class HotRunner extends ResidentRunner {
536536
bundleFirstUpload: isFirstUpload,
537537
bundleDirty: !isFirstUpload && rebuildBundle,
538538
fullRestart: fullRestart,
539-
pathToReload: getReloadPath(fullRestart: fullRestart, swap: _swap),
539+
pathToReload: getReloadPath(resetCompiler: fullRestart, swap: _swap),
540540
invalidatedFiles: invalidationResult.uris!,
541541
packageConfig: invalidationResult.packageConfig!,
542542
dillOutputPath: dillOutputPath,

packages/flutter_tools/lib/src/web/bootstrap.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,15 @@ $_simpleLoaderScript
252252
// We should have written a file containing all the scripts that need to be
253253
// reloaded into the page. This is then read when a hot restart is triggered
254254
// in DDC via the `\$dartReloadModifiedModules` callback.
255-
let restartScripts = currentUri + '.restartScripts';
255+
let restartScripts = _currentDirectory + 'restart_scripts.json';
256+
// Flutter tools should write a file containing the scripts and libraries
257+
// that need to be hot reloaded. This is read in DWDS when a hot reload is
258+
// triggered.
259+
// TODO(srujzs): Ideally, this should be passed to the
260+
// `FrontendServerDdcLibraryBundleStrategyProvider` instead. See
261+
// https://github.com/dart-lang/webdev/issues/2584 for more details.
262+
let reloadScripts = _currentDirectory + 'reload_scripts.json';
263+
window.\$reloadScriptsPath = reloadScripts;
256264
257265
if (!window.\$dartReloadModifiedModules) {
258266
window.\$dartReloadModifiedModules = (function(appName, callback) {

0 commit comments

Comments
 (0)