From de18b40933b79b67fb72760284b4341feb51f965 Mon Sep 17 00:00:00 2001 From: Nicholas Shahan Date: Fri, 8 May 2026 11:43:35 -0700 Subject: [PATCH] [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 Reviewed-by: Srujan Gaddam --- .../lib/js/ddc/ddc_module_loader.js | 100 +++++++++++++++ pkg/reload_test/lib/ddc_helpers.dart | 118 +++++++++++------- .../lib/src/_ddc_reload_utils.dart | 16 ++- 3 files changed, 187 insertions(+), 47 deletions(-) diff --git a/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js b/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js index e5de482d7bb..d22732b0bf0 100644 --- a/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js +++ b/pkg/dev_compiler/lib/js/ddc/ddc_module_loader.js @@ -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} 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>} 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; diff --git a/pkg/reload_test/lib/ddc_helpers.dart b/pkg/reload_test/lib/ddc_helpers.dart index f34bd76de63..2f17435fbb3 100644 --- a/pkg/reload_test/lib/ddc_helpers.dart +++ b/pkg/reload_test/lib/ddc_helpers.dart @@ -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 diff --git a/pkg/reload_test/lib/src/_ddc_reload_utils.dart b/pkg/reload_test/lib/src/_ddc_reload_utils.dart index 902bd70259e..ba925d19274 100644 --- a/pkg/reload_test/lib/src/_ddc_reload_utils.dart +++ b/pkg/reload_test/lib/src/_ddc_reload_utils.dart @@ -26,7 +26,10 @@ extension type _DDCLoader(JSObject _) implements JSObject { extension type _DartDevEmbedder(JSObject _) implements JSObject { external JSPromise hotReload(JSArray files, JSArray ids); - external JSPromise hotRestart(); + external JSPromise> hotRestartBegin( + JSArray filesToRequest, + ); + external void hotRestartEnd(); external JSNumber get hotReloadGeneration; external JSNumber get hotRestartGeneration; } @@ -39,6 +42,9 @@ external JSArray>? injectedFilesAndLibrariesToReload( JSNumber requestedFileGeneration, ); +@JS('\$injectedReloadedSourcesHelper') +external JSArray? injectedReloadedSourcesHelper(); + @JS('\$dartLoader') external _DartLoader get _dartLoader; @@ -48,6 +54,12 @@ int get hotRestartGeneration => _dartDevEmbedder.hotRestartGeneration.toDartInt; Future 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 hotRestart() async { '${HotReloadReceipt.hotReloadReceiptTag}' '${jsonEncode(restartReceipt.toJson())}', ); - await _dartDevEmbedder.hotRestart().toDart; + _dartDevEmbedder.hotRestartEnd(); } /// The reload generation of the currently running application.