// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'package:meta/meta.dart'; import 'package:package_config/package_config.dart'; import 'package:pool/pool.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import 'base/context.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; import 'base/platform.dart'; import 'base/utils.dart'; import 'build_info.dart'; import 'compile.dart'; import 'convert.dart'; import 'dart/package_map.dart'; import 'devfs.dart'; import 'device.dart'; import 'features.dart'; import 'globals.dart' as globals; import 'project.dart'; import 'reporting/reporting.dart'; import 'resident_runner.dart'; import 'vmservice.dart'; ProjectFileInvalidator get projectFileInvalidator => context.get() ?? ProjectFileInvalidator( fileSystem: globals.fs, platform: globals.platform, logger: globals.logger, ); HotRunnerConfig? get hotRunnerConfig => context.get(); class HotRunnerConfig { /// Should the hot runner assume that the minimal Dart dependencies do not change? bool stableDartDependencies = false; /// Whether the hot runner should scan for modified files asynchronously. bool asyncScanning = false; /// A hook for implementations to perform any necessary initialization prior /// to a hot restart. Should return true if the hot restart should continue. Future setupHotRestart() async { return true; } /// A hook for implementations to perform any necessary initialization prior /// to a hot reload. Should return true if the hot restart should continue. Future setupHotReload() async { return true; } /// A hook for implementations to perform any necessary cleanup after the /// devfs sync is complete. At this point the flutter_tools no longer needs to /// access the source files and assets. void updateDevFSComplete() {} /// A hook for implementations to perform any necessary operations right /// before the runner is about to be shut down. Future runPreShutdownOperations() async { return; } } const bool kHotReloadDefault = true; class DeviceReloadReport { DeviceReloadReport(this.device, this.reports); FlutterDevice? device; List reports; // List has one report per Flutter view. } class HotRunner extends ResidentRunner { HotRunner( super.devices, { required super.target, required super.debuggingOptions, this.benchmarkMode = false, this.applicationBinary, this.hostIsIde = false, super.projectRootPath, super.dillOutputPath, super.stayResident, bool super.ipv6 = false, super.machine, this.multidexEnabled = false, super.devtoolsHandler, StopwatchFactory stopwatchFactory = const StopwatchFactory(), ReloadSourcesHelper reloadSourcesHelper = _defaultReloadSourcesHelper, ReassembleHelper reassembleHelper = _defaultReassembleHelper, }) : _stopwatchFactory = stopwatchFactory, _reloadSourcesHelper = reloadSourcesHelper, _reassembleHelper = reassembleHelper, super( hotMode: true, ); final StopwatchFactory _stopwatchFactory; final ReloadSourcesHelper _reloadSourcesHelper; final ReassembleHelper _reassembleHelper; final bool benchmarkMode; final File? applicationBinary; final bool hostIsIde; final bool multidexEnabled; /// When performing a hot restart, the tool needs to upload a new main.dart.dill to /// each attached device's devfs. Replacing the existing file is not safe and does /// not work at all on the windows embedder, because the old dill file will still be /// memory-mapped by the embedder. To work around this issue, the tool will alternate /// names for the uploaded dill, sometimes inserting `.swap`. Since the active dill will /// never be replaced, there is no risk of writing the file while the embedder is attempting /// to read from it. This also avoids filling up the devfs, if a incrementing counter was /// used instead. /// /// This is only used for hot restart, incremental dills uploaded as part of the hot /// reload process do not have this issue. bool _swap = false; /// Whether the resident runner has correctly attached to the running application. bool _didAttach = false; final Map> benchmarkData = >{}; DateTime? firstBuildTime; String? _targetPlatform; String? _sdkName; bool? _emulator; Future _calculateTargetPlatform() async { if (_targetPlatform != null) { return; } if (flutterDevices.length == 1) { final Device device = flutterDevices.first.device!; _targetPlatform = getNameForTargetPlatform(await device.targetPlatform); _sdkName = await device.sdkNameAndVersion; _emulator = await device.isLocalEmulator; } else if (flutterDevices.length > 1) { _targetPlatform = 'multiple'; _sdkName = 'multiple'; _emulator = false; } else { _targetPlatform = 'unknown'; _sdkName = 'unknown'; _emulator = false; } } void _addBenchmarkData(String name, int value) { benchmarkData[name] ??= []; benchmarkData[name]!.add(value); } Future _reloadSourcesService( String isolateId, { bool force = false, bool pause = false, }) async { final OperationResult result = await restart(pause: pause); if (!result.isOk) { throw vm_service.RPCError( 'Unable to reload sources', RPCErrorCodes.kInternalError, '', ); } } Future _restartService({ bool pause = false }) async { final OperationResult result = await restart(fullRestart: true, pause: pause); if (!result.isOk) { throw vm_service.RPCError( 'Unable to restart', RPCErrorCodes.kInternalError, '', ); } } Future _compileExpressionService( String isolateId, String expression, List definitions, List typeDefinitions, String libraryUri, String? klass, bool isStatic, ) async { for (final FlutterDevice? device in flutterDevices) { if (device!.generator != null) { final CompilerOutput? compilerOutput = await device.generator!.compileExpression(expression, definitions, typeDefinitions, libraryUri, klass, isStatic); if (compilerOutput != null && compilerOutput.expressionData != null) { return base64.encode(compilerOutput.expressionData!); } } } throw Exception('Failed to compile $expression'); } // Returns the exit code of the flutter tool process, like [run]. @override Future attach({ Completer? connectionInfoCompleter, Completer? appStartedCompleter, bool allowExistingDdsInstance = false, bool enableDevTools = false, bool needsFullRestart = true, }) async { _didAttach = true; try { await connectToServiceProtocol( reloadSources: _reloadSourcesService, restart: _restartService, compileExpression: _compileExpressionService, getSkSLMethod: writeSkSL, allowExistingDdsInstance: allowExistingDdsInstance, ); // Catches all exceptions, non-Exception objects are rethrown. } catch (error) { // ignore: avoid_catches_without_on_clauses if (error is! Exception && error is! String) { rethrow; } globals.printError('Error connecting to the service protocol: $error'); return 2; } if (enableDevTools) { // The method below is guaranteed never to return a failing future. unawaited(residentDevtoolsHandler!.serveAndAnnounceDevTools( devToolsServerAddress: debuggingOptions.devToolsServerAddress, flutterDevices: flutterDevices, )); } for (final FlutterDevice? device in flutterDevices) { await device!.initLogReader(); device .developmentShaderCompiler .configureCompiler(device.targetPlatform, enableImpeller: debuggingOptions.enableImpeller); } try { final List baseUris = await _initDevFS(); if (connectionInfoCompleter != null) { // Only handle one debugger connection. connectionInfoCompleter.complete( DebugConnectionInfo( httpUri: flutterDevices.first.vmService!.httpAddress, wsUri: flutterDevices.first.vmService!.wsAddress, baseUri: baseUris.first.toString(), ), ); } } on DevFSException catch (error) { globals.printError('Error initializing DevFS: $error'); return 3; } final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start(); final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: needsFullRestart); _addBenchmarkData( 'hotReloadInitialDevFSSyncMilliseconds', initialUpdateDevFSsTimer.elapsed.inMilliseconds, ); if (!devfsResult.success) { return 3; } for (final FlutterDevice? device in flutterDevices) { // VM must have accepted the kernel binary, there will be no reload // report, so we let incremental compiler know that source code was accepted. if (device!.generator != null) { device.generator!.accept(); } final List views = await device.vmService!.getFlutterViews(); for (final FlutterView view in views) { globals.printTrace('Connected to $view.'); } } // In fast-start mode, apps are initialized from a placeholder splashscreen // app. We must do a restart here to load the program and assets for the // real app. if (debuggingOptions.fastStart) { await restart( fullRestart: true, reason: 'restart', silent: true, ); } appStartedCompleter?.complete(); if (benchmarkMode) { // Wait multiple seconds for the isolate to have fully started. await Future.delayed(const Duration(seconds: 10)); // We are running in benchmark mode. globals.printStatus('Running in benchmark mode.'); // Measure time to perform a hot restart. globals.printStatus('Benchmarking hot restart'); await restart(fullRestart: true); // Wait multiple seconds to stabilize benchmark on slower device lab hardware. // Hot restart finishes when the new isolate is started, not when the new isolate // is ready. This process can actually take multiple seconds. await Future.delayed(const Duration(seconds: 10)); globals.printStatus('Benchmarking hot reload'); // Measure time to perform a hot reload. await restart(); if (stayResident) { await waitForAppToFinish(); } else { globals.printStatus('Benchmark completed. Exiting application.'); await _cleanupDevFS(); await stopEchoingDeviceLog(); await exitApp(); } final File benchmarkOutput = globals.fs.file('hot_benchmark.json'); benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData)); return 0; } writeVmServiceFile(); int result = 0; if (stayResident) { result = await waitForAppToFinish(); } await cleanupAtFinish(); return result; } @override Future run({ Completer? connectionInfoCompleter, Completer? appStartedCompleter, bool enableDevTools = false, String? route, }) async { await _calculateTargetPlatform(); final Stopwatch appStartedTimer = Stopwatch()..start(); final File mainFile = globals.fs.file(mainPath); firstBuildTime = DateTime.now(); Duration totalCompileTime = Duration.zero; Duration totalLaunchAppTime = Duration.zero; final List> startupTasks = >[]; for (final FlutterDevice? device in flutterDevices) { // Here we initialize the frontend_server concurrently with the platform // build, reducing overall initialization time. This is safe because the first // invocation of the frontend server produces a full dill file that the // subsequent invocation in devfs will not overwrite. await runSourceGenerators(); if (device!.generator != null) { final Stopwatch compileTimer = Stopwatch()..start(); startupTasks.add( device.generator!.recompile( mainFile.uri, [], // When running without a provided applicationBinary, the tool will // simultaneously run the initial frontend_server compilation and // the native build step. If there is a Dart compilation error, it // should only be displayed once. suppressErrors: applicationBinary == null, checkDartPluginRegistry: true, dartPluginRegistrant: FlutterProject.current().dartPluginRegistrant, outputPath: dillOutputPath, packageConfig: debuggingOptions.buildInfo.packageConfig, projectRootPath: FlutterProject.current().directory.absolute.path, fs: globals.fs, ).then((CompilerOutput? output) { compileTimer.stop(); totalCompileTime += compileTimer.elapsed; return output?.errorCount == 0; }) ); } final Stopwatch launchAppTimer = Stopwatch()..start(); startupTasks.add(device.runHot( hotRunner: this, route: route, ).then((int result) { totalLaunchAppTime += launchAppTimer.elapsed; return result == 0; })); } unawaited(appStartedCompleter?.future.then((_) => HotEvent('reload-ready', targetPlatform: _targetPlatform!, sdkName: _sdkName!, emulator: _emulator!, fullRestart: false, fastReassemble: false, overallTimeInMs: appStartedTimer.elapsed.inMilliseconds, compileTimeInMs: totalCompileTime.inMilliseconds, transferTimeInMs: totalLaunchAppTime.inMilliseconds, ).send())); try { final List results = await Future.wait(startupTasks); if (!results.every((bool passed) => passed)) { appFailedToStart(); return 1; } cacheInitialDillCompilation(); } on Exception catch (err) { globals.printError(err.toString()); appFailedToStart(); return 1; } return attach( connectionInfoCompleter: connectionInfoCompleter, appStartedCompleter: appStartedCompleter, enableDevTools: enableDevTools, needsFullRestart: false, ); } Future> _initDevFS() async { final String fsName = globals.fs.path.basename(projectRootPath); return [ for (final FlutterDevice? device in flutterDevices) await device!.setupDevFS( fsName, globals.fs.directory(projectRootPath), ), ]; } Future _updateDevFS({ bool fullRestart = false }) async { final bool isFirstUpload = !assetBundle.wasBuiltOnce(); final bool rebuildBundle = assetBundle.needsBuild(); if (rebuildBundle) { globals.printTrace('Updating assets'); final int result = await assetBundle.build(packagesPath: '.packages'); if (result != 0) { return UpdateFSReport(); } } final Stopwatch findInvalidationTimer = _stopwatchFactory.createStopwatch('updateDevFS')..start(); final DevFS devFS = flutterDevices[0].devFS!; final InvalidationResult invalidationResult = await projectFileInvalidator.findInvalidated( lastCompiled: devFS.lastCompiled, urisToMonitor: devFS.sources, packagesPath: packagesFilePath, asyncScanning: hotRunnerConfig!.asyncScanning, packageConfig: devFS.lastPackageConfig ?? debuggingOptions.buildInfo.packageConfig, ); findInvalidationTimer.stop(); final File entrypointFile = globals.fs.file(mainPath); if (!entrypointFile.existsSync()) { globals.printError( 'The entrypoint file (i.e. the file with main()) ${entrypointFile.path} ' 'cannot be found. Moving or renaming this file will prevent changes to ' 'its contents from being discovered during hot reload/restart until ' 'flutter is restarted or the file is restored.' ); } final UpdateFSReport results = UpdateFSReport( success: true, scannedSourcesCount: devFS.sources.length, findInvalidatedDuration: findInvalidationTimer.elapsed, ); for (final FlutterDevice? device in flutterDevices) { results.incorporateResults(await device!.updateDevFS( mainUri: entrypointFile.absolute.uri, target: target, bundle: assetBundle, firstBuildTime: firstBuildTime, bundleFirstUpload: isFirstUpload, bundleDirty: !isFirstUpload && rebuildBundle, fullRestart: fullRestart, projectRootPath: projectRootPath, pathToReload: getReloadPath(fullRestart: fullRestart, swap: _swap), invalidatedFiles: invalidationResult.uris!, packageConfig: invalidationResult.packageConfig!, dillOutputPath: dillOutputPath, )); } return results; } void _resetDirtyAssets() { for (final FlutterDevice device in flutterDevices) { final DevFS? devFS = device.devFS; if (devFS == null) { // This is sometimes null, however we don't know why and have not been // able to reproduce, https://github.com/flutter/flutter/issues/108653 continue; } devFS.assetPathsToEvict.clear(); devFS.shaderPathsToEvict.clear(); } } Future _cleanupDevFS() async { final List> futures = >[]; for (final FlutterDevice device in flutterDevices) { if (device.devFS != null) { // Cleanup the devFS, but don't wait indefinitely. // We ignore any errors, because it's not clear what we would do anyway. futures.add(device.devFS!.destroy() .timeout(const Duration(milliseconds: 250)) .catchError((Object? error) { globals.printTrace('Ignored error while cleaning up DevFS: $error'); })); } device.devFS = null; } await Future.wait(futures); } Future _launchInView( FlutterDevice device, Uri main, Uri assetsDirectory, ) async { final List views = await device.vmService!.getFlutterViews(); await Future.wait(>[ for (final FlutterView view in views) device.vmService!.runInView( viewId: view.id, main: main, assetsDirectory: assetsDirectory, ), ]); } Future _launchFromDevFS() async { final List> futures = >[]; for (final FlutterDevice? device in flutterDevices) { final Uri deviceEntryUri = device!.devFS!.baseUri!.resolve(_swap ? 'main.dart.swap.dill' : 'main.dart.dill'); final Uri deviceAssetsDirectoryUri = device.devFS!.baseUri!.resolveUri( globals.fs.path.toUri(getAssetBuildDirectory())); futures.add(_launchInView(device, deviceEntryUri, deviceAssetsDirectoryUri)); } await Future.wait(futures); } Future _restartFromSources({ String? reason, }) async { final Stopwatch restartTimer = Stopwatch()..start(); UpdateFSReport updatedDevFS; try { updatedDevFS = await _updateDevFS(fullRestart: true); } finally { hotRunnerConfig!.updateDevFSComplete(); } if (!updatedDevFS.success) { for (final FlutterDevice? device in flutterDevices) { if (device!.generator != null) { await device.generator!.reject(); } } return OperationResult(1, 'DevFS synchronization failed'); } _resetDirtyAssets(); for (final FlutterDevice? device in flutterDevices) { // VM must have accepted the kernel binary, there will be no reload // report, so we let incremental compiler know that source code was accepted. if (device!.generator != null) { device.generator!.accept(); } } // Check if the isolate is paused and resume it. final List> operations = >[]; for (final FlutterDevice? device in flutterDevices) { final Set uiIsolatesIds = {}; final List views = await device!.vmService!.getFlutterViews(); for (final FlutterView view in views) { if (view.uiIsolate == null) { continue; } uiIsolatesIds.add(view.uiIsolate!.id); // Reload the isolate. final Future reloadIsolate = device.vmService! .getIsolateOrNull(view.uiIsolate!.id!); operations.add(reloadIsolate.then((vm_service.Isolate? isolate) async { if ((isolate != null) && isPauseEvent(isolate.pauseEvent!.kind!)) { // The embedder requires that the isolate is unpaused, because the // runInView method requires interaction with dart engine APIs that // are not thread-safe, and thus must be run on the same thread that // would be blocked by the pause. Simply un-pausing is not sufficient, // because this does not prevent the isolate from immediately hitting // a breakpoint (for example if the breakpoint was placed in a loop // or in a frequently called method) or an exception. Instead, all // breakpoints are first disabled and exception pause mode set to // None, and then the isolate resumed. // These settings to not need restoring as Hot Restart results in // new isolates, which will be configured by the editor as they are // started. final List> breakpointAndExceptionRemoval = >[ device.vmService!.service.setIsolatePauseMode(isolate.id!, exceptionPauseMode: vm_service.ExceptionPauseMode.kNone), for (final vm_service.Breakpoint breakpoint in isolate.breakpoints!) device.vmService!.service.removeBreakpoint(isolate.id!, breakpoint.id!), ]; await Future.wait(breakpointAndExceptionRemoval); await device.vmService!.service.resume(view.uiIsolate!.id!); } })); } // The engine handles killing and recreating isolates that it has spawned // ("uiIsolates"). The isolates that were spawned from these uiIsolates // will not be restarted, and so they must be manually killed. final vm_service.VM vm = await device.vmService!.service.getVM(); for (final vm_service.IsolateRef isolateRef in vm.isolates!) { if (uiIsolatesIds.contains(isolateRef.id)) { continue; } operations.add( device.vmService!.service.kill(isolateRef.id!) // Since we never check the value of this Future, only await its // completion, make its type nullable so we can return null when // catching errors. .then((vm_service.Success success) => success) .catchError((dynamic error, StackTrace stackTrace) { // Do nothing on a SentinelException since it means the isolate // has already been killed. // Error code 105 indicates the isolate is not yet runnable, and might // be triggered if the tool is attempting to kill the asset parsing // isolate before it has finished starting up. return null; }, test: (dynamic error) => error is vm_service.SentinelException || (error is vm_service.RPCError && error.code == 105))); } } await Future.wait(operations); await _launchFromDevFS(); restartTimer.stop(); globals.printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.'); _addBenchmarkData('hotRestartMillisecondsToFrame', restartTimer.elapsed.inMilliseconds); // Send timing analytics. globals.flutterUsage.sendTiming('hot', 'restart', restartTimer.elapsed); // Toggle the main dill name after successfully uploading. _swap =! _swap; return OperationResult( OperationResult.ok.code, OperationResult.ok.message, updateFSReport: updatedDevFS, ); } /// Returns [true] if the reload was successful. /// Prints errors if [printErrors] is [true]. static bool validateReloadReport( vm_service.ReloadReport? reloadReport, { bool printErrors = true, }) { if (reloadReport == null) { if (printErrors) { globals.printError('Hot reload did not receive reload report.'); } return false; } final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport); if (!reloadReport.success!) { if (printErrors) { globals.printError('Hot reload was rejected:'); for (final ReasonForCancelling reason in contents.notices) { globals.printError(reason.toString()); } } return false; } return true; } @override Future restart({ bool fullRestart = false, String? reason, bool silent = false, bool pause = false, }) async { if (flutterDevices.any((FlutterDevice? device) => device!.devFS == null)) { return OperationResult(1, 'Device initialization has not completed.'); } await _calculateTargetPlatform(); final Stopwatch timer = Stopwatch()..start(); // Run source generation if needed. await runSourceGenerators(); if (fullRestart) { final OperationResult result = await _fullRestartHelper( targetPlatform: _targetPlatform, sdkName: _sdkName, emulator: _emulator, reason: reason, silent: silent, ); if (!silent) { globals.printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.'); } unawaited(residentDevtoolsHandler!.hotRestart(flutterDevices)); return result; } final OperationResult result = await _hotReloadHelper( targetPlatform: _targetPlatform, sdkName: _sdkName, emulator: _emulator, reason: reason, pause: pause, ); if (result.isOk) { final String elapsed = getElapsedAsMilliseconds(timer.elapsed); if (!silent) { if (result.extraTimings.isNotEmpty) { final String extraTimingsString = result.extraTimings .map((OperationResultExtraTiming e) => '${e.description}: ${e.timeInMs} ms') .join(', '); globals.printStatus('${result.message} in $elapsed ($extraTimingsString).'); } else { globals.printStatus('${result.message} in $elapsed.'); } } } return result; } Future _fullRestartHelper({ String? targetPlatform, String? sdkName, bool? emulator, String? reason, bool? silent, }) async { if (!supportsRestart) { return OperationResult(1, 'hotRestart not supported'); } Status? status; if (!silent!) { status = globals.logger.startProgress( 'Performing hot restart...', progressId: 'hot.restart', ); } OperationResult result; String? restartEvent; try { final Stopwatch restartTimer = _stopwatchFactory.createStopwatch('fullRestartHelper')..start(); if ((await hotRunnerConfig!.setupHotRestart()) != true) { return OperationResult(1, 'setupHotRestart failed'); } result = await _restartFromSources(reason: reason); restartTimer.stop(); if (!result.isOk) { restartEvent = 'restart-failed'; } else { HotEvent('restart', targetPlatform: targetPlatform!, sdkName: sdkName!, emulator: emulator!, fullRestart: true, reason: reason, fastReassemble: false, overallTimeInMs: restartTimer.elapsed.inMilliseconds, syncedBytes: result.updateFSReport?.syncedBytes, invalidatedSourcesCount: result.updateFSReport?.invalidatedSourcesCount, transferTimeInMs: result.updateFSReport?.transferDuration.inMilliseconds, compileTimeInMs: result.updateFSReport?.compileDuration.inMilliseconds, findInvalidatedTimeInMs: result.updateFSReport?.findInvalidatedDuration.inMilliseconds, scannedSourcesCount: result.updateFSReport?.scannedSourcesCount, ).send(); } } on vm_service.SentinelException catch (err, st) { restartEvent = 'exception'; return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true); } on vm_service.RPCError catch (err, st) { restartEvent = 'exception'; return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true); } finally { // The `restartEvent` variable will be null if restart succeeded. We will // only handle the case when it failed here. if (restartEvent != null) { HotEvent(restartEvent, targetPlatform: targetPlatform!, sdkName: sdkName!, emulator: emulator!, fullRestart: true, reason: reason, fastReassemble: false, ).send(); } status?.cancel(); } return result; } Future _hotReloadHelper({ String? targetPlatform, String? sdkName, bool? emulator, String? reason, bool? pause, }) async { Status status = globals.logger.startProgress( 'Performing hot reload...', progressId: 'hot.reload', ); OperationResult result; try { result = await _reloadSources( targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, reason: reason, pause: pause, onSlow: (String message) { status.cancel(); status = globals.logger.startProgress( message, progressId: 'hot.reload', ); }, ); } on vm_service.RPCError catch (error) { String errorMessage = 'hot reload failed to complete'; int errorCode = 1; if (error.code == kIsolateReloadBarred) { errorCode = error.code; errorMessage = 'Unable to hot reload application due to an unrecoverable error in ' 'the source code. Please address the error and then use "R" to ' 'restart the app.\n' '${error.message} (error code: ${error.code})'; HotEvent('reload-barred', targetPlatform: targetPlatform!, sdkName: sdkName!, emulator: emulator!, fullRestart: false, reason: reason, fastReassemble: false, ).send(); } else { HotEvent('exception', targetPlatform: targetPlatform!, sdkName: sdkName!, emulator: emulator!, fullRestart: false, reason: reason, fastReassemble: false, ).send(); } return OperationResult(errorCode, errorMessage, fatal: true); } finally { status.cancel(); } return result; } Future _reloadSources({ String? targetPlatform, String? sdkName, bool? emulator, bool? pause = false, String? reason, void Function(String message)? onSlow, }) async { final Map> viewCache = >{}; for (final FlutterDevice? device in flutterDevices) { final List views = await device!.vmService!.getFlutterViews(); viewCache[device] = views; for (final FlutterView view in views) { if (view.uiIsolate == null) { return OperationResult(2, 'Application isolate not found', fatal: true); } } } final Stopwatch reloadTimer = _stopwatchFactory.createStopwatch('reloadSources:reload')..start(); if ((await hotRunnerConfig!.setupHotReload()) != true) { return OperationResult(1, 'setupHotReload failed'); } final Stopwatch devFSTimer = Stopwatch()..start(); UpdateFSReport updatedDevFS; try { updatedDevFS= await _updateDevFS(); } finally { hotRunnerConfig!.updateDevFSComplete(); } // Record time it took to synchronize to DevFS. bool shouldReportReloadTime = true; _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds); if (!updatedDevFS.success) { return OperationResult(1, 'DevFS synchronization failed'); } final List extraTimings = []; extraTimings.add(OperationResultExtraTiming('compile', updatedDevFS.compileDuration.inMilliseconds)); String reloadMessage = 'Reloaded 0 libraries'; final Stopwatch reloadVMTimer = _stopwatchFactory.createStopwatch('reloadSources:vm')..start(); final Map firstReloadDetails = {}; if (updatedDevFS.invalidatedSourcesCount > 0) { final OperationResult result = await _reloadSourcesHelper( this, flutterDevices, pause, firstReloadDetails, targetPlatform, sdkName, emulator, reason, ); if (result.code != 0) { return result; } reloadMessage = result.message; } else { _addBenchmarkData('hotReloadVMReloadMilliseconds', 0); } reloadVMTimer.stop(); extraTimings.add(OperationResultExtraTiming('reload', reloadVMTimer.elapsedMilliseconds)); await evictDirtyAssets(); final Stopwatch reassembleTimer = _stopwatchFactory.createStopwatch('reloadSources:reassemble')..start(); final ReassembleResult reassembleResult = await _reassembleHelper( flutterDevices, viewCache, onSlow, reloadMessage, updatedDevFS.fastReassembleClassName, ); shouldReportReloadTime = reassembleResult.shouldReportReloadTime; if (reassembleResult.reassembleViews.isEmpty) { return OperationResult(OperationResult.ok.code, reloadMessage); } // Record time it took for Flutter to reassemble the application. reassembleTimer.stop(); _addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds); extraTimings.add(OperationResultExtraTiming('reassemble', reassembleTimer.elapsedMilliseconds)); reloadTimer.stop(); final Duration reloadDuration = reloadTimer.elapsed; final int reloadInMs = reloadDuration.inMilliseconds; // Collect stats that help understand scale of update for this hot reload request. // For example, [syncedLibraryCount]/[finalLibraryCount] indicates how // many libraries were affected by the hot reload request. // Relation of [invalidatedSourcesCount] to [syncedLibraryCount] should help // understand sync/transfer "overhead" of updating this number of source files. HotEvent('reload', targetPlatform: targetPlatform!, sdkName: sdkName!, emulator: emulator!, fullRestart: false, reason: reason, overallTimeInMs: reloadInMs, finalLibraryCount: firstReloadDetails['finalLibraryCount'] as int? ?? 0, syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int? ?? 0, syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int? ?? 0, syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int? ?? 0, syncedBytes: updatedDevFS.syncedBytes, invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount, transferTimeInMs: updatedDevFS.transferDuration.inMilliseconds, fastReassemble: featureFlags.isSingleWidgetReloadEnabled && updatedDevFS.fastReassembleClassName != null, compileTimeInMs: updatedDevFS.compileDuration.inMilliseconds, findInvalidatedTimeInMs: updatedDevFS.findInvalidatedDuration.inMilliseconds, scannedSourcesCount: updatedDevFS.scannedSourcesCount, reassembleTimeInMs: reassembleTimer.elapsed.inMilliseconds, reloadVMTimeInMs: reloadVMTimer.elapsed.inMilliseconds, ).send(); if (shouldReportReloadTime) { globals.printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.'); // Record complete time it took for the reload. _addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs); } // Only report timings if we reloaded a single view without any errors. if ((reassembleResult.reassembleViews.length == 1) && !reassembleResult.failedReassemble && shouldReportReloadTime) { globals.flutterUsage.sendTiming('hot', 'reload', reloadDuration); } return OperationResult( reassembleResult.failedReassemble ? 1 : OperationResult.ok.code, reloadMessage, extraTimings: extraTimings ); } @override void printHelp({ required bool details }) { globals.printStatus('Flutter run key commands.'); commandHelp.r.print(); if (supportsRestart) { commandHelp.R.print(); } if (details) { printHelpDetails(); commandHelp.hWithDetails.print(); } else { commandHelp.hWithoutDetails.print(); } if (_didAttach) { commandHelp.d.print(); } commandHelp.c.print(); commandHelp.q.print(); globals.printStatus(''); if (debuggingOptions.buildInfo.nullSafetyMode == NullSafetyMode.sound) { globals.printStatus('💪 Running with sound null safety 💪', emphasis: true); } else { globals.printStatus( 'Running without sound null safety ⚠️', emphasis: true, ); globals.printStatus( 'Dart 3 will only support sound null safety, see https://dart.dev/null-safety', ); } globals.printStatus(''); printDebuggerList(); } @visibleForTesting Future evictDirtyAssets() async { final List> futures = >[]; for (final FlutterDevice? device in flutterDevices) { if (device!.devFS!.assetPathsToEvict.isEmpty && device.devFS!.shaderPathsToEvict.isEmpty) { continue; } final List views = await device.vmService!.getFlutterViews(); // If this is the first time we update the assets, make sure to call the setAssetDirectory if (!device.devFS!.hasSetAssetDirectory) { final Uri deviceAssetsDirectoryUri = device.devFS!.baseUri!.resolveUri(globals.fs.path.toUri(getAssetBuildDirectory())); await Future.wait(views.map>( (FlutterView view) => device.vmService!.setAssetDirectory( assetsDirectory: deviceAssetsDirectoryUri, uiIsolateId: view.uiIsolate!.id, viewId: view.id, windows: device.targetPlatform == TargetPlatform.windows_x64, ) )); for (final FlutterView view in views) { globals.printTrace('Set asset directory in $view.'); } device.devFS!.hasSetAssetDirectory = true; } if (views.first.uiIsolate == null) { globals.printError('Application isolate not found for $device'); continue; } if (device.devFS!.didUpdateFontManifest) { futures.add(device.vmService!.reloadAssetFonts( isolateId: views.first.uiIsolate!.id!, viewId: views.first.id, )); } for (final String assetPath in device.devFS!.assetPathsToEvict) { futures.add( device.vmService! .flutterEvictAsset( assetPath, isolateId: views.first.uiIsolate!.id!, ) ); } for (final String assetPath in device.devFS!.shaderPathsToEvict) { futures.add( device.vmService! .flutterEvictShader( assetPath, isolateId: views.first.uiIsolate!.id!, ) ); } device.devFS!.assetPathsToEvict.clear(); device.devFS!.shaderPathsToEvict.clear(); } await Future.wait(futures); } @override Future cleanupAfterSignal() async { await stopEchoingDeviceLog(); await hotRunnerConfig!.runPreShutdownOperations(); if (_didAttach) { appFinished(); } else { await exitApp(); } } @override Future preExit() async { await _cleanupDevFS(); await hotRunnerConfig!.runPreShutdownOperations(); await super.preExit(); } @override Future cleanupAtFinish() async { for (final FlutterDevice? flutterDevice in flutterDevices) { await flutterDevice!.device!.dispose(); } await _cleanupDevFS(); await residentDevtoolsHandler!.shutdown(); await stopEchoingDeviceLog(); } } typedef ReloadSourcesHelper = Future Function( HotRunner hotRunner, List flutterDevices, bool? pause, Map firstReloadDetails, String? targetPlatform, String? sdkName, bool? emulator, String? reason, ); Future _defaultReloadSourcesHelper( HotRunner hotRunner, List flutterDevices, bool? pause, Map firstReloadDetails, String? targetPlatform, String? sdkName, bool? emulator, String? reason, ) async { final Stopwatch vmReloadTimer = Stopwatch()..start(); const String entryPath = 'main.dart.incremental.dill'; final List> allReportsFutures = >[]; for (final FlutterDevice? device in flutterDevices) { final List> reportFutures = await _reloadDeviceSources( device!, entryPath, pause: pause, ); allReportsFutures.add(Future.wait(reportFutures).then( (List reports) async { // TODO(aam): Investigate why we are validating only first reload report, // which seems to be current behavior final vm_service.ReloadReport firstReport = reports.first; // Don't print errors because they will be printed further down when // `validateReloadReport` is called again. await device.updateReloadStatus( HotRunner.validateReloadReport(firstReport, printErrors: false), ); return DeviceReloadReport(device, reports); }, )); } final List reports = await Future.wait(allReportsFutures); final vm_service.ReloadReport reloadReport = reports.first.reports[0]; if (!HotRunner.validateReloadReport(reloadReport)) { // Reload failed. HotEvent('reload-reject', targetPlatform: targetPlatform!, sdkName: sdkName!, emulator: emulator!, fullRestart: false, reason: reason, fastReassemble: false, ).send(); // Reset devFS lastCompileTime to ensure the file will still be marked // as dirty on subsequent reloads. _resetDevFSCompileTime(flutterDevices); final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport); return OperationResult(1, 'Reload rejected: ${contents.notices.join("\n")}'); } // Collect stats only from the first device. If/when run -d all is // refactored, we'll probably need to send one hot reload/restart event // per device to analytics. firstReloadDetails.addAll(castStringKeyedMap(reloadReport.json!['details'])!); final Map details = reloadReport.json!['details'] as Map; final int? loadedLibraryCount = details['loadedLibraryCount'] as int?; final int? finalLibraryCount = details['finalLibraryCount'] as int?; globals.printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries'); // reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries'; // Record time it took for the VM to reload the sources. hotRunner._addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds); return OperationResult(0, 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries'); } Future>> _reloadDeviceSources( FlutterDevice device, String entryPath, { bool? pause = false, }) async { final String deviceEntryUri = device.devFS!.baseUri! .resolve(entryPath).toString(); final vm_service.VM vm = await device.vmService!.service.getVM(); return >[ for (final vm_service.IsolateRef isolateRef in vm.isolates!) device.vmService!.service.reloadSources( isolateRef.id!, pause: pause, rootLibUri: deviceEntryUri, ), ]; } void _resetDevFSCompileTime(List flutterDevices) { for (final FlutterDevice? device in flutterDevices) { device!.devFS!.resetLastCompiled(); } } @visibleForTesting class ReassembleResult { ReassembleResult(this.reassembleViews, this.failedReassemble, this.shouldReportReloadTime); final Map reassembleViews; final bool failedReassemble; final bool shouldReportReloadTime; } typedef ReassembleHelper = Future Function( List flutterDevices, Map> viewCache, void Function(String message)? onSlow, String reloadMessage, String? fastReassembleClassName, ); Future _defaultReassembleHelper( List flutterDevices, Map> viewCache, void Function(String message)? onSlow, String reloadMessage, String? fastReassembleClassName, ) async { // Check if any isolates are paused and reassemble those that aren't. final Map reassembleViews = {}; final List> reassembleFutures = >[]; String? serviceEventKind; int pausedIsolatesFound = 0; bool failedReassemble = false; bool shouldReportReloadTime = true; for (final FlutterDevice? device in flutterDevices) { final List views = viewCache[device]!; for (final FlutterView view in views) { // Check if the isolate is paused, and if so, don't reassemble. Ignore the // PostPauseEvent event - the client requesting the pause will resume the app. final vm_service.Isolate? isolate = await device!.vmService! .getIsolateOrNull(view.uiIsolate!.id!); final vm_service.Event? pauseEvent = isolate?.pauseEvent; if (pauseEvent != null && isPauseEvent(pauseEvent.kind!) && pauseEvent.kind != vm_service.EventKind.kPausePostRequest) { pausedIsolatesFound += 1; if (serviceEventKind == null) { serviceEventKind = pauseEvent.kind; } else if (serviceEventKind != pauseEvent.kind) { serviceEventKind = ''; // many kinds } } else { reassembleViews[view] = device.vmService; // If the tool identified a change in a single widget, do a fast instead // of a full reassemble. Future reassembleWork; if (fastReassembleClassName != null) { reassembleWork = device.vmService!.flutterFastReassemble( isolateId: view.uiIsolate!.id!, className: fastReassembleClassName, ); } else { reassembleWork = device.vmService!.flutterReassemble( isolateId: view.uiIsolate!.id!, ); } reassembleFutures.add(reassembleWork.catchError((dynamic error) { failedReassemble = true; globals.printError('Reassembling ${view.uiIsolate!.name} failed: $error'); }, test: (dynamic error) => error is Exception)); } } } if (pausedIsolatesFound > 0) { if (onSlow != null) { onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind!)}; interface might not update.'); } if (reassembleViews.isEmpty) { globals.printTrace('Skipping reassemble because all isolates are paused.'); return ReassembleResult(reassembleViews, failedReassemble, shouldReportReloadTime); } } assert(reassembleViews.isNotEmpty); globals.printTrace('Reassembling application'); final Future reassembleFuture = Future.wait(reassembleFutures).then((void _) => null); await reassembleFuture.timeout( const Duration(seconds: 2), onTimeout: () async { if (pausedIsolatesFound > 0) { shouldReportReloadTime = false; return ; // probably no point waiting, they're probably deadlocked and we've already warned. } // Check if any isolate is newly paused. globals.printTrace('This is taking a long time; will now check for paused isolates.'); int postReloadPausedIsolatesFound = 0; String? serviceEventKind; for (final FlutterView view in reassembleViews.keys) { final vm_service.Isolate? isolate = await reassembleViews[view]! .getIsolateOrNull(view.uiIsolate!.id!); if (isolate == null) { continue; } if (isolate.pauseEvent != null && isPauseEvent(isolate.pauseEvent!.kind!)) { postReloadPausedIsolatesFound += 1; if (serviceEventKind == null) { serviceEventKind = isolate.pauseEvent!.kind; } else if (serviceEventKind != isolate.pauseEvent!.kind) { serviceEventKind = ''; // many kinds } } } globals.printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).'); if (postReloadPausedIsolatesFound == 0) { await reassembleFuture; // must just be taking a long time... keep waiting! return; } shouldReportReloadTime = false; if (onSlow != null) { onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind!)}.'); } return; }, ); return ReassembleResult(reassembleViews, failedReassemble, shouldReportReloadTime); } String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) { assert(pausedIsolatesFound > 0); final StringBuffer message = StringBuffer(); bool plural; if (pausedIsolatesFound == 1) { message.write('The application is '); plural = false; } else { message.write('$pausedIsolatesFound isolates are '); plural = true; } assert(serviceEventKind != null); switch (serviceEventKind) { case vm_service.EventKind.kPauseStart: message.write('paused (probably due to --start-paused)'); break; case vm_service.EventKind.kPauseExit: message.write('paused because ${ plural ? 'they have' : 'it has' } terminated'); break; case vm_service.EventKind.kPauseBreakpoint: message.write('paused in the debugger on a breakpoint'); break; case vm_service.EventKind.kPauseInterrupted: message.write('paused due in the debugger'); break; case vm_service.EventKind.kPauseException: message.write('paused in the debugger after an exception was thrown'); break; case vm_service.EventKind.kPausePostRequest: message.write('paused'); break; case '': message.write('paused for various reasons'); break; default: message.write('paused'); } return message.toString(); } /// The result of an invalidation check from [ProjectFileInvalidator]. class InvalidationResult { const InvalidationResult({ this.uris, this.packageConfig, }); final List? uris; final PackageConfig? packageConfig; } /// The [ProjectFileInvalidator] track the dependencies for a running /// application to determine when they are dirty. class ProjectFileInvalidator { ProjectFileInvalidator({ required FileSystem fileSystem, required Platform platform, required Logger logger, }): _fileSystem = fileSystem, _platform = platform, _logger = logger; final FileSystem _fileSystem; final Platform _platform; final Logger _logger; static const String _pubCachePathLinuxAndMac = '.pub-cache'; static const String _pubCachePathWindows = 'Pub/Cache'; // As of writing, Dart supports up to 32 asynchronous I/O threads per // isolate. We also want to avoid hitting platform limits on open file // handles/descriptors. // // This value was chosen based on empirical tests scanning a set of // ~2000 files. static const int _kMaxPendingStats = 8; Future findInvalidated({ required DateTime? lastCompiled, required List urisToMonitor, required String packagesPath, required PackageConfig packageConfig, bool asyncScanning = false, }) async { assert(urisToMonitor != null); assert(packagesPath != null); if (lastCompiled == null) { // Initial load. assert(urisToMonitor.isEmpty); return InvalidationResult( packageConfig: packageConfig, uris: [], ); } final Stopwatch stopwatch = Stopwatch()..start(); final List urisToScan = [ // Don't watch pub cache directories to speed things up a little. for (final Uri uri in urisToMonitor) if (_isNotInPubCache(uri)) uri, ]; final List invalidatedFiles = []; if (asyncScanning) { final Pool pool = Pool(_kMaxPendingStats); final List> waitList = >[]; for (final Uri uri in urisToScan) { waitList.add(pool.withResource( // Calling fs.stat() is more performant than fs.file().stat(), but // uri.toFilePath() does not work with MultiRootFileSystem. () => (uri.hasScheme && uri.scheme != 'file' ? _fileSystem.file(uri).stat() : _fileSystem.stat(uri.toFilePath(windows: _platform.isWindows))) .then((FileStat stat) { final DateTime updatedAt = stat.modified; if (updatedAt != null && updatedAt.isAfter(lastCompiled)) { invalidatedFiles.add(uri); } }) )); } await Future.wait(waitList); } else { for (final Uri uri in urisToScan) { // Calling fs.statSync() is more performant than fs.file().statSync(), but // uri.toFilePath() does not work with MultiRootFileSystem. final DateTime updatedAt = uri.hasScheme && uri.scheme != 'file' ? _fileSystem.file(uri).statSync().modified : _fileSystem.statSync(uri.toFilePath(windows: _platform.isWindows)).modified; if (updatedAt != null && updatedAt.isAfter(lastCompiled)) { invalidatedFiles.add(uri); } } } // We need to check the .packages file too since it is not used in compilation. final File packageFile = _fileSystem.file(packagesPath); final Uri packageUri = packageFile.uri; final DateTime updatedAt = packageFile.statSync().modified; if (updatedAt != null && updatedAt.isAfter(lastCompiled)) { invalidatedFiles.add(packageUri); packageConfig = await _createPackageConfig(packagesPath); // The frontend_server might be monitoring the package_config.json file, // Pub should always produce both files. // TODO(zanderso): remove after https://github.com/flutter/flutter/issues/55249 if (_fileSystem.path.basename(packagesPath) == '.packages') { final File packageConfigFile = _fileSystem.file(packagesPath) .parent.childDirectory('.dart_tool') .childFile('package_config.json'); if (packageConfigFile.existsSync()) { invalidatedFiles.add(packageConfigFile.uri); } } } _logger.printTrace( 'Scanned through ${urisToScan.length} files in ' '${stopwatch.elapsedMilliseconds}ms' '${asyncScanning ? " (async)" : ""}', ); return InvalidationResult( packageConfig: packageConfig, uris: invalidatedFiles, ); } bool _isNotInPubCache(Uri uri) { return !(_platform.isWindows && uri.path.contains(_pubCachePathWindows)) && !uri.path.contains(_pubCachePathLinuxAndMac); } Future _createPackageConfig(String packagesPath) { return loadPackageConfigWithLogging( _fileSystem.file(packagesPath), logger: _logger, ); } } /// Additional serialization logic for a hot reload response. class ReloadReportContents { factory ReloadReportContents.fromReloadReport(vm_service.ReloadReport report) { final List reasons = []; final Object? notices = report.json!['notices']; if (notices is! List) { return ReloadReportContents._(report.success, reasons, report); } for (final Object? obj in notices) { if (obj is! Map) { continue; } final Map notice = obj; reasons.add(ReasonForCancelling( message: notice['message'] is String ? notice['message'] as String? : 'Unknown Error', )); } return ReloadReportContents._(report.success, reasons, report); } ReloadReportContents._( this.success, this.notices, this.report, ); final bool? success; final List notices; final vm_service.ReloadReport report; } /// A serialization class for hot reload rejection reasons. /// /// Injects an additional error message that a hot restart will /// resolve the issue. class ReasonForCancelling { ReasonForCancelling({ this.message, }); final String? message; @override String toString() { return '$message.\nTry performing a hot restart instead.'; } }