diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index eeac6d4871..811cfd0d7e 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -927,7 +927,7 @@ class AppDomain extends Domain { final Map? result = await device.vmService!.invokeFlutterExtensionRpcRaw( methodName, args: params, - isolateId: views.first.uiIsolate!.id!, + isolateId: views.first.uiIsolate!.id, ); if (result == null) { throw DaemonException('method not available: $methodName'); diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index ed6c83f9da..b779d18877 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -380,13 +380,15 @@ class UpdateFSReport { Duration compileDuration = Duration.zero, Duration transferDuration = Duration.zero, Duration findInvalidatedDuration = Duration.zero, + bool hotReloadRejected = false, }) : _success = success, _invalidatedSourcesCount = invalidatedSourcesCount, _syncedBytes = syncedBytes, _scannedSourcesCount = scannedSourcesCount, _compileDuration = compileDuration, _transferDuration = transferDuration, - _findInvalidatedDuration = findInvalidatedDuration; + _findInvalidatedDuration = findInvalidatedDuration, + _hotReloadRejected = hotReloadRejected; bool get success => _success; int get invalidatedSourcesCount => _invalidatedSourcesCount; @@ -396,6 +398,12 @@ class UpdateFSReport { Duration get transferDuration => _transferDuration; Duration get findInvalidatedDuration => _findInvalidatedDuration; + /// Whether there was a hot reload rejection in this compile. + /// + /// On the web, hot reload can be rejected during compile time instead of at + /// runtime. + bool get hotReloadRejected => _hotReloadRejected; + bool _success; int _invalidatedSourcesCount; int _syncedBytes; @@ -403,6 +411,7 @@ class UpdateFSReport { Duration _compileDuration; Duration _transferDuration; Duration _findInvalidatedDuration; + bool _hotReloadRejected; void incorporateResults(UpdateFSReport report) { if (!report._success) { @@ -414,6 +423,9 @@ class UpdateFSReport { _compileDuration += report._compileDuration; _transferDuration += report._transferDuration; _findInvalidatedDuration += report._findInvalidatedDuration; + if (report._hotReloadRejected) { + _hotReloadRejected = true; + } } } diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart index 3d03483639..dd1cb3fd84 100644 --- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart +++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart @@ -1139,7 +1139,14 @@ class WebDevFS implements DevFS { recompileRestart: fullRestart, ); if (compilerOutput == null || compilerOutput.errorCount > 0) { - return UpdateFSReport(); + return UpdateFSReport( + // TODO(srujzs): We're currently reliant on compile error string parsing + // as hot reload rejections are sent to stderr just like other + // compilation errors. Ideally, we should have some shared parsing + // functionality, but that would require a shared package. + // See https://github.com/dart-lang/sdk/issues/60275. + hotReloadRejected: compilerOutput?.errorMessage?.contains('Hot reload rejected') ?? false, + ); } // Only update the last compiled time if we successfully compiled. diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index 48075324ca..a4ef4e4df7 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -432,19 +432,40 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). status = _logger.startProgress('Performing hot reload...', progressId: 'hot.reload'); } + final String targetPlatform = getNameForTargetPlatform(TargetPlatform.web_javascript); + final String sdkName = await device!.device!.sdkNameAndVersion; + + late UpdateFSReport report; if (debuggingOptions.buildInfo.isDebug && !debuggingOptions.webUseWasm) { await runSourceGenerators(); // Don't reset the resident compiler for web, since the extra recompile is // wasteful. - final UpdateFSReport report = await _updateDevFS( - fullRestart: fullRestart, - resetCompiler: false, - ); + report = await _updateDevFS(fullRestart: fullRestart, resetCompiler: false); if (report.success) { device!.generator!.accept(); } else { status.stop(); await device!.generator!.reject(); + if (report.hotReloadRejected) { + // We cannot capture the reason why the reload was rejected as it may + // contain user information. + HotEvent( + 'reload-reject', + targetPlatform: targetPlatform, + sdkName: sdkName, + emulator: false, + fullRestart: fullRestart, + ).send(); + _analytics.send( + Event.hotRunnerInfo( + label: 'reload-reject', + targetPlatform: targetPlatform, + sdkName: sdkName, + emulator: false, + fullRestart: fullRestart, + ), + ); + } return OperationResult(1, 'Failed to recompile application.'); } } else { @@ -469,6 +490,8 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). } } + late Duration reloadDuration; + late Duration reassembleDuration; try { if (!deviceIsDebuggable) { _logger.printStatus('Recompile complete. Page requires refresh.'); @@ -482,7 +505,9 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). } else { // Isolates don't work on web. For lack of a better value, pass an // empty string for the isolate id. + final DateTime reloadStart = _systemClock.now(); final vmservice.ReloadReport report = await _vmService.service.reloadSources(''); + reloadDuration = _systemClock.now().difference(reloadStart); final ReloadReportContents contents = ReloadReportContents.fromReloadReport(report); final bool success = contents.success ?? false; if (!success) { @@ -498,6 +523,21 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). } return OperationResult(1, reloadFailedMessage); } + String? failedReassemble; + final DateTime reassembleStart = _systemClock.now(); + await _vmService + .flutterReassemble(isolateId: null) + .then( + (Object? o) => o, + onError: (Object error, StackTrace stackTrace) { + failedReassemble = 'Reassembling failed: $error\n$stackTrace'; + _logger.printError(failedReassemble!); + }, + ); + reassembleDuration = _systemClock.now().difference(reassembleStart); + if (failedReassemble != null) { + return OperationResult(1, failedReassemble!); + } } } else { // On non-debug builds, a hard refresh is required to ensure the @@ -522,11 +562,6 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). // Don't track restart times for dart2js builds or web-server devices. if (debuggingOptions.buildInfo.isDebug && deviceIsDebuggable) { - // TODO(srujzs): There are a number of fields that the VM tracks in the - // analytics that we do not for both hot restart and reload. We should - // unify that. - final String targetPlatform = getNameForTargetPlatform(TargetPlatform.web_javascript); - final String sdkName = await device!.device!.sdkNameAndVersion; if (fullRestart) { _analytics.send( Event.timing( @@ -543,6 +578,12 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). fullRestart: true, reason: reason, overallTimeInMs: elapsed.inMilliseconds, + syncedBytes: report.syncedBytes, + invalidatedSourcesCount: report.invalidatedSourcesCount, + transferTimeInMs: report.transferDuration.inMilliseconds, + compileTimeInMs: report.compileDuration.inMilliseconds, + findInvalidatedTimeInMs: report.findInvalidatedDuration.inMilliseconds, + scannedSourcesCount: report.scannedSourcesCount, ).send(); _analytics.send( Event.hotRunnerInfo( @@ -553,6 +594,12 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). fullRestart: true, reason: reason, overallTimeInMs: elapsed.inMilliseconds, + syncedBytes: report.syncedBytes, + invalidatedSourcesCount: report.invalidatedSourcesCount, + transferTimeInMs: report.transferDuration.inMilliseconds, + compileTimeInMs: report.compileDuration.inMilliseconds, + findInvalidatedTimeInMs: report.findInvalidatedDuration.inMilliseconds, + scannedSourcesCount: report.scannedSourcesCount, ), ); } else { @@ -571,6 +618,14 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). fullRestart: false, reason: reason, overallTimeInMs: elapsed.inMilliseconds, + syncedBytes: report.syncedBytes, + invalidatedSourcesCount: report.invalidatedSourcesCount, + transferTimeInMs: report.transferDuration.inMilliseconds, + compileTimeInMs: report.compileDuration.inMilliseconds, + findInvalidatedTimeInMs: report.findInvalidatedDuration.inMilliseconds, + scannedSourcesCount: report.scannedSourcesCount, + reassembleTimeInMs: reassembleDuration.inMilliseconds, + reloadVMTimeInMs: reloadDuration.inMilliseconds, ).send(); _analytics.send( Event.hotRunnerInfo( @@ -581,6 +636,14 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). fullRestart: false, reason: reason, overallTimeInMs: elapsed.inMilliseconds, + syncedBytes: report.syncedBytes, + invalidatedSourcesCount: report.invalidatedSourcesCount, + transferTimeInMs: report.transferDuration.inMilliseconds, + compileTimeInMs: report.compileDuration.inMilliseconds, + findInvalidatedTimeInMs: report.findInvalidatedDuration.inMilliseconds, + scannedSourcesCount: report.scannedSourcesCount, + reassembleTimeInMs: reassembleDuration.inMilliseconds, + reloadVMTimeInMs: reloadDuration.inMilliseconds, ), ); } diff --git a/packages/flutter_tools/lib/src/resident_devtools_handler.dart b/packages/flutter_tools/lib/src/resident_devtools_handler.dart index a48fcd9768..19140deb77 100644 --- a/packages/flutter_tools/lib/src/resident_devtools_handler.dart +++ b/packages/flutter_tools/lib/src/resident_devtools_handler.dart @@ -298,7 +298,7 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler { await device.vmService!.invokeFlutterExtensionRpcRaw( method, args: params, - isolateId: views.first.uiIsolate!.id!, + isolateId: views.first.uiIsolate!.id, ); } diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 8e26c31087..efda6de35f 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -1416,7 +1416,7 @@ Future _defaultReassembleHelper( // If the tool identified a change in a single widget, do a fast instead // of a full reassemble. final Future reassembleWork = device.vmService!.flutterReassemble( - isolateId: view.uiIsolate!.id!, + isolateId: view.uiIsolate!.id, ); reassembleFutures.add( reassembleWork.then( diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 29f80086f4..9d23256a43 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -684,7 +684,7 @@ class FlutterVmService { ); } - Future?> flutterReassemble({required String isolateId}) { + Future?> flutterReassemble({required String? isolateId}) { return invokeFlutterExtensionRpcRaw('ext.flutter.reassemble', isolateId: isolateId); } @@ -803,12 +803,12 @@ class FlutterVmService { /// available, returns null. Future?> invokeFlutterExtensionRpcRaw( String method, { - required String isolateId, + required String? isolateId, Map? args, }) async { final vm_service.Response? response = await _checkedCallServiceExtension( method, - args: {'isolateId': isolateId, ...?args}, + args: {if (isolateId != null) 'isolateId': isolateId, ...?args}, ); return response?.json; } diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 21df2fbaa3..fe0ae13483 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: archive: 3.6.1 args: 2.6.0 dds: 5.0.0 - dwds: 24.3.6 + dwds: 24.3.7 code_builder: 4.10.1 completion: 1.0.1 coverage: 1.11.1 @@ -122,4 +122,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: 5a6c +# PUBSPEC CHECKSUM: 776d diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_cold_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_cold_test.dart index 360c02d044..9d196bf558 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_cold_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_cold_test.dart @@ -260,6 +260,11 @@ class FakeWebDevice extends Fake implements Device { return true; } + @override + Future get sdkNameAndVersion async { + return 'Flutter Tools'; + } + @override Future startApp( ApplicationPackage? package, { diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index cf87025692..93fc2c05ae 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -75,6 +75,11 @@ const List kAttachExpectations = [ ...kAttachIsolateExpectations, ]; +const List kDdcLibraryBundleFlags = [ + '--dartdevc-module-format=ddc', + '--dartdevc-canary', +]; + void main() { late FakeDebugConnection debugConnection; late FakeChromeDevice chromeDevice; @@ -725,13 +730,29 @@ name: my_app flutterDevice, logger: logger, systemClock: SystemClock.fixed(DateTime(2001)), + debuggingOptions: DebuggingOptions.enabled( + const BuildInfo( + BuildMode.debug, + null, + trackWidgetCreation: true, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', + // Hot reload only supported with these flags for now. + extraFrontEndOptions: kDdcLibraryBundleFlags, + ), + ), ); fakeVmServiceHost = FakeVmServiceHost( requests: [ ...kAttachExpectations, const FakeVmServiceRequest( - method: kHotRestartServiceName, - jsonResponse: {'type': 'Success'}, + method: kReloadSourcesServiceName, + args: {'isolateId': ''}, + jsonResponse: {'type': 'ReloadReport', 'success': true}, + ), + const FakeVmServiceRequest( + method: 'ext.flutter.reassemble', + jsonResponse: {'type': 'ReloadReport', 'success': true}, ), const FakeVmServiceRequest( method: 'streamListen', @@ -769,7 +790,7 @@ name: my_app final OperationResult result = await residentWebRunner.restart(); - expect(logger.statusText, contains('Restarted application in')); + expect(logger.statusText, contains('Reloaded application in')); expect(result.code, 0); expect(webDevFS.mainUri.toString(), contains('entrypoint.dart')); @@ -777,24 +798,26 @@ name: my_app fakeAnalytics.sentEvents, contains( Event.hotRunnerInfo( - label: 'restart', + label: 'reload', targetPlatform: 'web-javascript', sdkName: '', emulator: false, - fullRestart: true, + fullRestart: false, overallTimeInMs: 0, + syncedBytes: 0, + invalidatedSourcesCount: 0, + transferTimeInMs: 0, + compileTimeInMs: 0, + findInvalidatedTimeInMs: 0, + scannedSourcesCount: 0, + reassembleTimeInMs: 0, + reloadVMTimeInMs: 0, ), ), ); expect( fakeAnalytics.sentEvents, - contains( - Event.timing( - workflow: 'hot', - variableName: 'web-incremental-restart', - elapsedMilliseconds: 0, - ), - ), + contains(Event.timing(workflow: 'hot', variableName: 'reload', elapsedMilliseconds: 0)), ); }, overrides: { @@ -807,20 +830,31 @@ name: my_app ); testUsingContext( - 'Can hot restart after attaching', + 'Hot reload reject reports correct analytics', () async { final BufferLogger logger = BufferLogger.test(); final ResidentRunner residentWebRunner = setUpResidentRunner( flutterDevice, logger: logger, systemClock: SystemClock.fixed(DateTime(2001)), + debuggingOptions: DebuggingOptions.enabled( + const BuildInfo( + BuildMode.debug, + null, + trackWidgetCreation: true, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', + // Hot reload only supported with these flags for now. + extraFrontEndOptions: kDdcLibraryBundleFlags, + ), + ), ); fakeVmServiceHost = FakeVmServiceHost( requests: [ ...kAttachExpectations, const FakeVmServiceRequest( - method: kHotRestartServiceName, - jsonResponse: {'type': 'Success'}, + method: 'streamListen', + args: {'streamId': 'Isolate'}, ), ], ); @@ -848,39 +882,25 @@ name: my_app final Completer connectionInfoCompleter = Completer(); unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter)); - await connectionInfoCompleter.future; - final OperationResult result = await residentWebRunner.restart(fullRestart: true); + final DebugConnectionInfo debugConnectionInfo = await connectionInfoCompleter.future; - // Ensure that generated entrypoint is generated correctly. - expect(webDevFS.mainUri, isNotNull); - final String entrypointContents = fileSystem.file(webDevFS.mainUri).readAsStringSync(); - expect(entrypointContents, contains('// Flutter web bootstrap script')); - expect(entrypointContents, contains("import 'dart:ui_web' as ui_web;")); - expect(entrypointContents, contains('await ui_web.bootstrapEngine(')); + expect(debugConnectionInfo, isNotNull); - expect(logger.statusText, contains('Restarted application in')); - expect(result.code, 0); + webDevFS.report = UpdateFSReport(hotReloadRejected: true); + final OperationResult result = await residentWebRunner.restart(); + + expect(result.code, 1); + expect(webDevFS.mainUri.toString(), contains('entrypoint.dart')); expect( fakeAnalytics.sentEvents, contains( Event.hotRunnerInfo( - label: 'restart', + label: 'reload-reject', targetPlatform: 'web-javascript', sdkName: '', emulator: false, - fullRestart: true, - overallTimeInMs: 0, - ), - ), - ); - expect( - fakeAnalytics.sentEvents, - contains( - Event.timing( - workflow: 'hot', - variableName: 'web-incremental-restart', - elapsedMilliseconds: 0, + fullRestart: false, ), ), ); @@ -894,6 +914,120 @@ name: my_app }, ); + // Hot restart is available with and without the DDC library bundle format. + // Test one extra config where `fullRestart` is false without the DDC library + // bundle format - we should do a hot restart in this case because hot reload + // is not available. + for (final (List flags, bool fullRestart) in <(List, bool)>[ + (kDdcLibraryBundleFlags, true), + ([], true), + ([], false), + ]) { + testUsingContext( + 'Can hot restart after attaching with flags: $flags fullRestart: $fullRestart', + () async { + final BufferLogger logger = BufferLogger.test(); + final ResidentRunner residentWebRunner = setUpResidentRunner( + flutterDevice, + logger: logger, + systemClock: SystemClock.fixed(DateTime(2001)), + debuggingOptions: DebuggingOptions.enabled( + BuildInfo( + BuildMode.debug, + null, + trackWidgetCreation: true, + treeShakeIcons: false, + packageConfigPath: '.dart_tool/package_config.json', + extraFrontEndOptions: flags, + ), + ), + ); + fakeVmServiceHost = FakeVmServiceHost( + requests: [ + ...kAttachExpectations, + const FakeVmServiceRequest( + method: kHotRestartServiceName, + jsonResponse: {'type': 'Success'}, + ), + ], + ); + setupMocks(); + final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher(); + final FakeProcess process = FakeProcess(); + final Chromium chrome = Chromium( + 1, + chromeConnection, + chromiumLauncher: chromiumLauncher, + process: process, + logger: logger, + ); + chromiumLauncher.setInstance(chrome); + + flutterDevice.device = GoogleChromeDevice( + fileSystem: fileSystem, + chromiumLauncher: chromiumLauncher, + logger: BufferLogger.test(), + platform: FakePlatform(), + processManager: FakeProcessManager.any(), + ); + webDevFS.report = UpdateFSReport(success: true); + + final Completer connectionInfoCompleter = + Completer(); + unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter)); + await connectionInfoCompleter.future; + final OperationResult result = await residentWebRunner.restart(fullRestart: fullRestart); + + // Ensure that generated entrypoint is generated correctly. + expect(webDevFS.mainUri, isNotNull); + final String entrypointContents = fileSystem.file(webDevFS.mainUri).readAsStringSync(); + expect(entrypointContents, contains('// Flutter web bootstrap script')); + expect(entrypointContents, contains("import 'dart:ui_web' as ui_web;")); + expect(entrypointContents, contains('await ui_web.bootstrapEngine(')); + + expect(logger.statusText, contains('Restarted application in')); + expect(result.code, 0); + + expect( + fakeAnalytics.sentEvents, + contains( + Event.hotRunnerInfo( + label: 'restart', + targetPlatform: 'web-javascript', + sdkName: '', + emulator: false, + fullRestart: true, + overallTimeInMs: 0, + syncedBytes: 0, + invalidatedSourcesCount: 0, + transferTimeInMs: 0, + compileTimeInMs: 0, + findInvalidatedTimeInMs: 0, + scannedSourcesCount: 0, + ), + ), + ); + expect( + fakeAnalytics.sentEvents, + contains( + Event.timing( + workflow: 'hot', + variableName: 'web-incremental-restart', + elapsedMilliseconds: 0, + ), + ), + ); + }, + overrides: { + Analytics: () => fakeAnalytics, + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + FeatureFlags: enableExplicitPackageDependencies, + Pub: FakePubWithPrimedDeps.new, + }, + ); + } + testUsingContext( 'Can hot restart after attaching with web-server device', () async { @@ -1130,7 +1264,8 @@ name: my_app trackWidgetCreation: true, treeShakeIcons: false, packageConfigPath: '.dart_tool/package_config.json', - extraFrontEndOptions: ['--dartdevc-module-format=ddc', '--dartdevc-canary'], + // Hot reload only supported with these flags for now. + extraFrontEndOptions: kDdcLibraryBundleFlags, ), ), );