[ddc] Split hot restart into two phases

Exposes two new methods `hotRestartBegin()` and `hotRestartEnd()` in
the `DartDevEmbedder`. This provides a more customizable loading
of sources across the variety of environments we are supporting.

Change-Id: Id7a35695234f0625fe4de1d67c9d5a8055bad461
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/500240
Commit-Queue: Nicholas Shahan <nshahan@google.com>
Reviewed-by: Srujan Gaddam <srujzs@google.com>
This commit is contained in:
Nicholas Shahan
2026-05-08 11:43:35 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 377c02e4d0
commit de18b40933
3 changed files with 187 additions and 47 deletions
@@ -1694,8 +1694,53 @@ if (!self.deferred_loader) {
this.hotReloadGeneration += 1;
}
// See docs on `DartDevEmbedder.hotRestartBegin`.
async hotRestartBegin(filesToRequest) {
if (!this.savedEntryPointLibraryName) {
throw 'Error: Hot restart requested before application started.';
}
this.hotRestartInProgress = true;
let reloadedFiles = await self.$dartReloadModifiedModules(
filesToRequest, this.savedEntryPointLibraryName);
return reloadedFiles;
}
// See docs on `DartDevEmbedder.hotRestartEnd`.
hotRestartEnd() {
if (!this.hotRestartInProgress) {
throw 'Error: Hot restart end called without a corresponding start.';
}
// Clear all libraries.
this.libraries = Object.create(null);
this.triggeredSDKLibrariesWithSideEffects = false;
this.setDartSDKRuntimeOptions(this.savedDartSdkRuntimeOptions);
// Update initializers. They'll be invoked later at some point after we
// call main.
for (let name in this.pendingHotRestartLibraryInitializers) {
let initializer = this.pendingHotRestartLibraryInitializers[name];
this.libraryInitializers[name] = initializer;
}
let entryPointLibrary =
this.initializeAndLinkLibrary(this.savedEntryPointLibraryName);
// TODO(nshahan): Start sharing a single source of truth for the restart
// generation between the dart:_runtime and this module system.
this.hotRestartGeneration += 1;
console.log(
'Hot restarting application from main method in: ' +
this.savedEntryPointLibraryName +
' (generation: ' + this.hotRestartGeneration + ').');
// Cleanup.
this.hotRestartInProgress = false;
this.pendingHotRestartLibraryInitializers = Object.create(null);
this._runMain(entryPointLibrary);
}
/**
* Completes a hot restart operation.
*
* @deprecated Use `hotRestartBegin` and `hotRestartEnd` instead.
*/
hotRestart() {
if (!this.savedEntryPointLibraryName) {
@@ -2076,6 +2121,7 @@ if (!self.deferred_loader) {
* that invokes 'main' when called.
* @type {?function(function())}
*/
// TODO(nshahan): Remove this once the migration is complete.
capturedMainHandler = null;
/**
@@ -2194,9 +2240,63 @@ if (!self.deferred_loader) {
await libraryManager.hotReloadStart(filesToLoad, librariesToReload);
}
/**
* Immediately triggers the start of a hot restart of the application.
*
* Assumes that a function named `$dartReloadModifiedModules` is available
* in the global scope and relies on it to actually request the files
* that are being reloaded and add them to the document.
*
* @param {!Array<!Object>} filesToRequest The descriptor Objects
* describing the JavaScript files that could be part of this hot
* restart. This must include all the files that changed and need
* to be reloaded on the page.
*
* Each descriptor Object must have the following properties:
* - src: The url of the Javascript file to request.
* - id: The unique and stable identifier for this file, typically
* the "module name" is used.
*
* Additional properties may be present and will simply be passed
* through to the `$dartReloadModifiedModules` function.
*
* @return {!Promise<!Array<!Object>>} Descriptor Objects for the
* JavaScript files that were actually requested by
* `$dartReloadModifiedModules`.
*
* This is expected to be the same set of files described by
* `filesToRequest` or a subset. Naming additional files could cause
* undefined behavior.
*
* Note: when a Dart debugger is attached, the hot restart operation
* will block until a script parse event is received from Chrome
* DevTools for each of the JavaScript files described here.
*
* Each descriptor Object returned from `$dartReloadModifiedModules`
* must contain the following properties:
* - src: The url of the Javascript file that was requested.
*
* Additional properties may be present.
*/
async hotRestartBegin(filesToRequest) {
return await libraryManager.hotRestartBegin(filesToRequest);
}
/**
* Finishes the hot restart operation that must have been previously
* started by [hotRestartBegin] losing all application state and running
* the main method again.
*/
hotRestartEnd() {
libraryManager.hotRestartEnd();
}
/**
* Immediately triggers a hot restart of the application losing all state
* and running the main method again.
*
* @deprecated Use `hotRestartBegin` and `hotRestartEnd` instead.
*/
async hotRestart() {
libraryManager.hotRestartInProgress = true;
+73 -45
View File
@@ -136,16 +136,8 @@ self.\$dartLoader.loader = loader;
let modifiedFilesPerGeneration = ${_encoder.convert(modifiedFilesPerGeneration)};
let previousGenerations = new Set();
// Append a helper function for hot restart.
self.\$dartReloadModifiedModules = async function(subAppName, callback) {
let expectedName = "$entrypointModuleName";
if (subAppName !== expectedName) {
throw Error("Unexpected app name " + subAppName
+ " (expected: " + expectedName + "). "
+ "Hot Reload Runner does not support multiple subapps, so only "
+ "one app name should be provided across reloads/restarts.");
}
// Append a helper function to provide reloaded sources information.
self.\$injectedReloadedSourcesHelper = function() {
// Resolve the next generation's directory and load all modified files.
let nextGeneration = self.\$dartLoader.loader.intendedHotRestartGeneration;
if (previousGenerations.has(nextGeneration)) {
@@ -156,18 +148,37 @@ self.\$dartReloadModifiedModules = async function(subAppName, callback) {
let modifiedFilePaths = modifiedFilesPerGeneration[nextGeneration];
// Stop if the next generation does not exist.
if (modifiedFilePaths == void 0) {
return;
return null;
}
// Load all modified files.
// Collect reload generation resources.
let fileDescriptors = [];
for (let i = 0; i < modifiedFilePaths.length; i++) {
let modifiedFilePath = modifiedFilePaths[i][1];
self.\$dartLoader.forceLoadScript(modifiedFilePath);
fileDescriptors.push({src: modifiedFilePath});
}
return fileDescriptors;
}
// Run main in an async callback. D8 performs synchronous loads, so we need
// to insert an async task to match its semantics to that of Chrome.
await Promise.resolve().then(() => { callback(); });
// Append a helper function for hot restart.
self.\$dartReloadModifiedModules = function(filesToReload, appName) {
let expectedName = "$entrypointModuleName";
if (appName !== expectedName) {
throw Error("Unexpected app name " + appName
+ " (expected: " + expectedName + "). "
+ "Hot Reload Runner does not support multiple subapps, so only "
+ "one app name should be provided across reloads/restarts.");
}
return new Promise(function(resolve) {
// Load all modified files.
for (let i = 0; i < filesToReload.length; i++) {
let modifiedFilePath = filesToReload[i].src;
self.\$dartLoader.forceLoadScript(modifiedFilePath);
}
// D8 performs synchronous loads, but we return a Promise to match the
// semantics in Chrome.
resolve(filesToReload);
});
}
// Append a helper function for hot reload.
@@ -505,15 +516,8 @@ let _scriptUrls = {
}
let previousGenerations = new Set();
self.\$dartReloadModifiedModules = async function(subAppName, callback) {
let expectedName = "$entrypointModuleName";
if (subAppName !== expectedName) {
throw Error("Unexpected app name " + subAppName
+ " (expected: " + expectedName + "). "
+ "Hot Reload Runner does not support multiple subapps, so only "
+ "one app name should be provided across reloads/restarts.");
}
// Append a helper function to provide reloaded sources information.
self.\$injectedReloadedSourcesHelper = function() {
// Resolve the next generation's directory and load all modified files.
let nextGeneration = self.\$dartLoader.loader.intendedHotRestartGeneration;
if (previousGenerations.has(nextGeneration)) {
@@ -524,33 +528,57 @@ let _scriptUrls = {
let modifiedFilePaths = modifiedFilesPerGeneration[nextGeneration];
// Stop if the next generation does not exist.
if (modifiedFilePaths == void 0) {
return;
return null;
}
// Load all modified files.
var numToLoad = 0;
var numLoaded = 0;
// Collect reload generation resources.
let fileDescriptors = [];
for (let i = 0; i < modifiedFilePaths.length; i++) {
numToLoad++
let modifiedFileId = modifiedFilePaths[i][0];
let modifiedFilePath = modifiedFilePaths[i][1];
// Invalidate DDC state for hot restart.
self.\$dartLoader.moduleIdToUrl.set(modifiedFileId, modifiedFilePath);
self.\$dartLoader.urlToModuleId.set(modifiedFilePath, modifiedFileId);
// Remove the old script.
var el = document.getElementById(modifiedFileId);
if (el) el.remove();
loadHotRestartScript(modifiedFileId, modifiedFilePath, function() {
numLoaded++;
if (numToLoad == numLoaded) callback();
});
fileDescriptors.push({src: modifiedFilePath, id: modifiedFileId});
}
return fileDescriptors;
}
// Call the callback immediately if we found no updated scripts.
if (numToLoad == 0) callback();
// Append a helper function for hot restart.
self.\$dartReloadModifiedModules = async function(filesToReload, subAppName) {
let expectedName = "$entrypointModuleName";
if (subAppName !== expectedName) {
throw Error("Unexpected app name " + subAppName
+ " (expected: " + expectedName + "). "
+ "Hot Reload Runner does not support multiple subapps, so only "
+ "one app name should be provided across reloads/restarts.");
}
// Load all modified files.
return new Promise(function(resolve) {
function callback() {
resolve(filesToReload);
}
var numToLoad = 0;
var numLoaded = 0;
for (let i = 0; i < filesToReload.length; i++) {
numToLoad++
let modifiedFileId = filesToReload[i].id;
let modifiedFilePath = filesToReload[i].src;
// Invalidate DDC state for hot restart.
self.\$dartLoader.moduleIdToUrl.set(modifiedFileId, modifiedFilePath);
self.\$dartLoader.urlToModuleId.set(modifiedFilePath, modifiedFileId);
// Remove the old script.
var el = document.getElementById(modifiedFileId);
if (el) el.remove();
loadHotRestartScript(modifiedFileId, modifiedFilePath, function() {
numLoaded++;
if (numToLoad == numLoaded) callback();
});
}
// Call the callback immediately if we found no updated scripts.
if (numToLoad == 0) callback();
});
}
// Begin loading libraries
+14 -2
View File
@@ -26,7 +26,10 @@ extension type _DDCLoader(JSObject _) implements JSObject {
extension type _DartDevEmbedder(JSObject _) implements JSObject {
external JSPromise hotReload(JSArray<JSString> files, JSArray<JSString> ids);
external JSPromise hotRestart();
external JSPromise<JSArray<JSObject>> hotRestartBegin(
JSArray<JSObject> filesToRequest,
);
external void hotRestartEnd();
external JSNumber get hotReloadGeneration;
external JSNumber get hotRestartGeneration;
}
@@ -39,6 +42,9 @@ external JSArray<JSArray<JSString>>? injectedFilesAndLibrariesToReload(
JSNumber requestedFileGeneration,
);
@JS('\$injectedReloadedSourcesHelper')
external JSArray<JSObject>? injectedReloadedSourcesHelper();
@JS('\$dartLoader')
external _DartLoader get _dartLoader;
@@ -48,6 +54,12 @@ int get hotRestartGeneration => _dartDevEmbedder.hotRestartGeneration.toDartInt;
Future<void> hotRestart() async {
_ddcLoader.intendedHotRestartGeneration++;
final filesToRequest = injectedReloadedSourcesHelper();
if (filesToRequest == null) {
throw Exception('Restart requested but no remaining generations found.');
}
await _dartDevEmbedder.hotRestartBegin(filesToRequest).toDart;
// Must return the receipt before re-running the main method.
final restartReceipt = HotReloadReceipt(
generation: _ddcLoader.intendedHotRestartGeneration,
status: Status.restarted,
@@ -56,7 +68,7 @@ Future<void> hotRestart() async {
'${HotReloadReceipt.hotReloadReceiptTag}'
'${jsonEncode(restartReceipt.toJson())}',
);
await _dartDevEmbedder.hotRestart().toDart;
_dartDevEmbedder.hotRestartEnd();
}
/// The reload generation of the currently running application.