1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:meta/meta.dart';
8import 'package:package_config/package_config.dart';
9import 'package:pool/pool.dart';
10import 'package:unified_analytics/unified_analytics.dart';
11import 'package:vm_service/vm_service.dart' as vm_service;
12
13import 'base/context.dart';
14import 'base/file_system.dart';
15import 'base/logger.dart';
16import 'base/platform.dart';
17import 'base/utils.dart';
18import 'build_info.dart';
19import 'compile.dart';
20import 'convert.dart';
21import 'devfs.dart';
22import 'device.dart';
23import 'globals.dart' as globals;
24import 'project.dart';
25import 'reporting/reporting.dart';
26import 'resident_runner.dart';
27import 'vmservice.dart';
28
29ProjectFileInvalidator get projectFileInvalidator =>
30 context.get<ProjectFileInvalidator>() ??
31 ProjectFileInvalidator(
32 fileSystem: globals.fs,
33 platform: globals.platform,
34 logger: globals.logger,
35 );
36
37HotRunnerConfig? get hotRunnerConfig => context.get<HotRunnerConfig>();
38
39class HotRunnerConfig {
40 /// Should the hot runner assume that the minimal Dart dependencies do not change?
41 bool stableDartDependencies = false;
42
43 /// Whether the hot runner should scan for modified files asynchronously.
44 bool asyncScanning = false;
45
46 /// A hook for implementations to perform any necessary initialization prior
47 /// to a hot restart. Should return true if the hot restart should continue.
48 Future<bool?> setupHotRestart() async {
49 return true;
50 }
51
52 /// A hook for implementations to perform any necessary initialization prior
53 /// to a hot reload. Should return true if the hot restart should continue.
54 Future<bool?> setupHotReload() async {
55 return true;
56 }
57
58 /// A hook for implementations to perform any necessary cleanup after the
59 /// devfs sync is complete. At this point the flutter_tools no longer needs to
60 /// access the source files and assets.
61 void updateDevFSComplete() {}
62
63 /// A hook for implementations to perform any necessary operations right
64 /// before the runner is about to be shut down.
65 Future<void> runPreShutdownOperations() async {
66 return;
67 }
68}
69
70const bool kHotReloadDefault = true;
71
72class DeviceReloadReport {
73 DeviceReloadReport(this.device, this.reports);
74
75 FlutterDevice? device;
76 List<vm_service.ReloadReport> reports; // List has one report per Flutter view.
77}
78
79class HotRunner extends ResidentRunner {
80 HotRunner(
81 super.flutterDevices, {
82 required super.target,
83 required super.debuggingOptions,
84 this.benchmarkMode = false,
85 this.applicationBinary,
86 this.hostIsIde = false,
87 super.projectRootPath,
88 super.dillOutputPath,
89 super.stayResident,
90 super.machine,
91 super.devtoolsHandler,
92 StopwatchFactory stopwatchFactory = const StopwatchFactory(),
93 ReloadSourcesHelper reloadSourcesHelper = defaultReloadSourcesHelper,
94 ReassembleHelper reassembleHelper = _defaultReassembleHelper,
95 String? nativeAssetsYamlFile,
96 required Analytics analytics,
97 }) : _stopwatchFactory = stopwatchFactory,
98 _reloadSourcesHelper = reloadSourcesHelper,
99 _reassembleHelper = reassembleHelper,
100 _nativeAssetsYamlFile = nativeAssetsYamlFile,
101 _analytics = analytics,
102 super(hotMode: true);
103
104 final StopwatchFactory _stopwatchFactory;
105 final ReloadSourcesHelper _reloadSourcesHelper;
106 final ReassembleHelper _reassembleHelper;
107 final Analytics _analytics;
108
109 final bool benchmarkMode;
110 final File? applicationBinary;
111 final bool hostIsIde;
112
113 /// When performing a hot restart, the tool needs to upload a new main.dart.dill to
114 /// each attached device's devfs. Replacing the existing file is not safe and does
115 /// not work at all on the windows embedder, because the old dill file will still be
116 /// memory-mapped by the embedder. To work around this issue, the tool will alternate
117 /// names for the uploaded dill, sometimes inserting `.swap`. Since the active dill will
118 /// never be replaced, there is no risk of writing the file while the embedder is attempting
119 /// to read from it. This also avoids filling up the devfs, if a incrementing counter was
120 /// used instead.
121 ///
122 /// This is only used for hot restart, incremental dills uploaded as part of the hot
123 /// reload process do not have this issue.
124 bool _swap = false;
125
126 final Map<String, List<int>> benchmarkData = <String, List<int>>{};
127
128 String? _targetPlatform;
129 String? _sdkName;
130 bool? _emulator;
131
132 final String? _nativeAssetsYamlFile;
133
134 String? flavor;
135
136 @override
137 bool get supportsDetach => stopAppDuringCleanup;
138
139 Future<void> _calculateTargetPlatform() async {
140 if (_targetPlatform != null) {
141 return;
142 }
143
144 switch (flutterDevices.length) {
145 case 1:
146 final Device device = flutterDevices.first.device!;
147 _targetPlatform = getNameForTargetPlatform(await device.targetPlatform);
148 _sdkName = await device.sdkNameAndVersion;
149 _emulator = await device.isLocalEmulator;
150 case > 1:
151 _targetPlatform = 'multiple';
152 _sdkName = 'multiple';
153 _emulator = false;
154 default:
155 _targetPlatform = 'unknown';
156 _sdkName = 'unknown';
157 _emulator = false;
158 }
159 }
160
161 void _addBenchmarkData(String name, int value) {
162 benchmarkData[name] ??= <int>[];
163 benchmarkData[name]!.add(value);
164 }
165
166 Future<void> _reloadSourcesService(
167 String isolateId, {
168 bool force = false,
169 bool pause = false,
170 }) async {
171 final OperationResult result = await restart(pause: pause);
172 if (!result.isOk) {
173 throw vm_service.RPCError(
174 'Unable to reload sources',
175 vm_service.RPCErrorKind.kInternalError.code,
176 '',
177 );
178 }
179 }
180
181 Future<void> _restartService({bool pause = false}) async {
182 final OperationResult result = await restart(fullRestart: true, pause: pause);
183 if (!result.isOk) {
184 throw vm_service.RPCError(
185 'Unable to restart',
186 vm_service.RPCErrorKind.kInternalError.code,
187 '',
188 );
189 }
190 }
191
192 Future<String> _compileExpressionService(
193 String isolateId,
194 String expression,
195 List<String> definitions,
196 List<String> definitionTypes,
197 List<String> typeDefinitions,
198 List<String> typeBounds,
199 List<String> typeDefaults,
200 String libraryUri,
201 String? klass,
202 String? method,
203 bool isStatic,
204 ) async {
205 for (final FlutterDevice? device in flutterDevices) {
206 if (device!.generator != null) {
207 final CompilerOutput? compilerOutput = await device.generator!.compileExpression(
208 expression,
209 definitions,
210 definitionTypes,
211 typeDefinitions,
212 typeBounds,
213 typeDefaults,
214 libraryUri,
215 klass,
216 method,
217 isStatic,
218 );
219 if (compilerOutput != null) {
220 if (compilerOutput.errorCount == 0 && compilerOutput.expressionData != null) {
221 return base64.encode(compilerOutput.expressionData!);
222 } else if (compilerOutput.errorCount > 0 && compilerOutput.errorMessage != null) {
223 throw VmServiceExpressionCompilationException(compilerOutput.errorMessage!);
224 }
225 }
226 }
227 }
228 throw Exception('Failed to compile $expression');
229 }
230
231 @override
232 @nonVirtual
233 Future<int> attach({
234 Completer<DebugConnectionInfo>? connectionInfoCompleter,
235 Completer<void>? appStartedCompleter,
236 bool allowExistingDdsInstance = false,
237 bool needsFullRestart = true,
238 }) async {
239 stopAppDuringCleanup = false;
240 return _attach(
241 connectionInfoCompleter: connectionInfoCompleter,
242 appStartedCompleter: appStartedCompleter,
243 allowExistingDdsInstance: allowExistingDdsInstance,
244 needsFullRestart: needsFullRestart,
245 );
246 }
247
248 Future<int> _attach({
249 Completer<DebugConnectionInfo>? connectionInfoCompleter,
250 Completer<void>? appStartedCompleter,
251 bool allowExistingDdsInstance = false,
252 bool needsFullRestart = true,
253 }) async {
254 try {
255 await connectToServiceProtocol(
256 reloadSources: _reloadSourcesService,
257 restart: _restartService,
258 compileExpression: _compileExpressionService,
259 allowExistingDdsInstance: allowExistingDdsInstance,
260 );
261 // Catches all exceptions, non-Exception objects are rethrown.
262 } catch (error) {
263 if (error is! Exception && error is! String) {
264 rethrow;
265 }
266 globals.printError('Error connecting to the service protocol: $error');
267 return 2;
268 }
269
270 if (debuggingOptions.serveObservatory) {
271 await enableObservatory();
272 }
273
274 // TODO(bkonyi): remove when ready to serve DevTools from DDS.
275 if (debuggingOptions.enableDevTools) {
276 // The method below is guaranteed never to return a failing future.
277 unawaited(
278 residentDevtoolsHandler!.serveAndAnnounceDevTools(
279 devToolsServerAddress: debuggingOptions.devToolsServerAddress,
280 flutterDevices: flutterDevices,
281 isStartPaused: debuggingOptions.startPaused,
282 ),
283 );
284 }
285
286 for (final FlutterDevice? device in flutterDevices) {
287 device!.developmentShaderCompiler.configureCompiler(device.targetPlatform);
288 }
289 try {
290 final List<Uri?> baseUris = await _initDevFS();
291 if (connectionInfoCompleter != null) {
292 // Only handle one debugger connection.
293 connectionInfoCompleter.complete(
294 DebugConnectionInfo(
295 httpUri: flutterDevices.first.vmService!.httpAddress,
296 wsUri: flutterDevices.first.vmService!.wsAddress,
297 baseUri: baseUris.first.toString(),
298 ),
299 );
300 }
301 } on DevFSException catch (error) {
302 globals.printError('Error initializing DevFS: $error');
303 return 3;
304 }
305
306 final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
307 final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: needsFullRestart);
308 _addBenchmarkData(
309 'hotReloadInitialDevFSSyncMilliseconds',
310 initialUpdateDevFSsTimer.elapsed.inMilliseconds,
311 );
312 if (!devfsResult.success) {
313 return 3;
314 }
315
316 for (final FlutterDevice? device in flutterDevices) {
317 // VM must have accepted the kernel binary, there will be no reload
318 // report, so we let incremental compiler know that source code was accepted.
319 if (device!.generator != null) {
320 device.generator!.accept();
321 }
322 final List<FlutterView> views = await device.vmService!.getFlutterViews();
323 for (final FlutterView view in views) {
324 globals.printTrace('Connected to $view.');
325 }
326 }
327
328 // In fast-start mode, apps are initialized from a placeholder splashscreen
329 // app. We must do a restart here to load the program and assets for the
330 // real app.
331 if (debuggingOptions.fastStart) {
332 await restart(fullRestart: true, reason: 'restart', silent: true);
333 }
334
335 appStartedCompleter?.complete();
336
337 if (benchmarkMode) {
338 // Wait multiple seconds for the isolate to have fully started.
339 await Future<void>.delayed(const Duration(seconds: 10));
340 // We are running in benchmark mode.
341 globals.printStatus('Running in benchmark mode.');
342 // Measure time to perform a hot restart.
343 globals.printStatus('Benchmarking hot restart');
344 await restart(fullRestart: true);
345 // Wait multiple seconds to stabilize benchmark on slower device lab hardware.
346 // Hot restart finishes when the new isolate is started, not when the new isolate
347 // is ready. This process can actually take multiple seconds.
348 await Future<void>.delayed(const Duration(seconds: 10));
349
350 globals.printStatus('Benchmarking hot reload');
351 // Measure time to perform a hot reload.
352 await restart();
353 if (stayResident) {
354 await waitForAppToFinish();
355 } else {
356 globals.printStatus('Benchmark completed. Exiting application.');
357 await _cleanupDevFS();
358 await stopEchoingDeviceLog();
359 await exitApp();
360 }
361 final File benchmarkOutput = globals.fs.file('hot_benchmark.json');
362 benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
363 return 0;
364 }
365 writeVmServiceFile();
366
367 int result = 0;
368 if (stayResident) {
369 result = await waitForAppToFinish();
370 }
371 await cleanupAtFinish();
372 return result;
373 }
374
375 @override
376 Future<int> run({
377 Completer<DebugConnectionInfo>? connectionInfoCompleter,
378 Completer<void>? appStartedCompleter,
379 String? route,
380 }) async {
381 await _calculateTargetPlatform();
382
383 final Uri? nativeAssetsYaml =
384 _nativeAssetsYamlFile != null ? globals.fs.path.toUri(_nativeAssetsYamlFile) : null;
385
386 final Stopwatch appStartedTimer = Stopwatch()..start();
387 final File mainFile = globals.fs.file(mainPath);
388
389 Duration totalCompileTime = Duration.zero;
390 Duration totalLaunchAppTime = Duration.zero;
391
392 final List<Future<bool>> startupTasks = <Future<bool>>[];
393 for (final FlutterDevice? device in flutterDevices) {
394 // Here we initialize the frontend_server concurrently with the platform
395 // build, reducing overall initialization time. This is safe because the first
396 // invocation of the frontend server produces a full dill file that the
397 // subsequent invocation in devfs will not overwrite.
398 await runSourceGenerators();
399 if (device!.generator != null) {
400 final Stopwatch compileTimer = Stopwatch()..start();
401 startupTasks.add(
402 device.generator!
403 .recompile(
404 mainFile.uri,
405 <Uri>[],
406 // When running without a provided applicationBinary, the tool will
407 // simultaneously run the initial frontend_server compilation and
408 // the native build step. If there is a Dart compilation error, it
409 // should only be displayed once.
410 suppressErrors: applicationBinary == null,
411 checkDartPluginRegistry: true,
412 dartPluginRegistrant: FlutterProject.current().dartPluginRegistrant,
413 outputPath: dillOutputPath,
414 packageConfig: debuggingOptions.buildInfo.packageConfig,
415 projectRootPath: FlutterProject.current().directory.absolute.path,
416 fs: globals.fs,
417 nativeAssetsYaml: nativeAssetsYaml,
418 )
419 .then((CompilerOutput? output) {
420 compileTimer.stop();
421 totalCompileTime += compileTimer.elapsed;
422 return output?.errorCount == 0;
423 }),
424 );
425 }
426
427 final Stopwatch launchAppTimer = Stopwatch()..start();
428 startupTasks.add(
429 device.runHot(hotRunner: this, route: route).then((int result) {
430 totalLaunchAppTime += launchAppTimer.elapsed;
431 return result == 0;
432 }),
433 );
434 }
435
436 unawaited(
437 appStartedCompleter?.future.then((_) {
438 HotEvent(
439 'reload-ready',
440 targetPlatform: _targetPlatform!,
441 sdkName: _sdkName!,
442 emulator: _emulator!,
443 fullRestart: false,
444 overallTimeInMs: appStartedTimer.elapsed.inMilliseconds,
445 compileTimeInMs: totalCompileTime.inMilliseconds,
446 transferTimeInMs: totalLaunchAppTime.inMilliseconds,
447 ).send();
448
449 _analytics.send(
450 Event.hotRunnerInfo(
451 label: 'reload-ready',
452 targetPlatform: _targetPlatform!,
453 sdkName: _sdkName!,
454 emulator: _emulator!,
455 fullRestart: false,
456 overallTimeInMs: appStartedTimer.elapsed.inMilliseconds,
457 compileTimeInMs: totalCompileTime.inMilliseconds,
458 transferTimeInMs: totalLaunchAppTime.inMilliseconds,
459 ),
460 );
461 }),
462 );
463
464 try {
465 final List<bool> results = await Future.wait(startupTasks);
466 if (!results.every((bool passed) => passed)) {
467 appFailedToStart();
468 return 1;
469 }
470 cacheInitialDillCompilation();
471 } on Exception catch (err) {
472 globals.printError(err.toString());
473 appFailedToStart();
474 return 1;
475 }
476
477 return _attach(
478 connectionInfoCompleter: connectionInfoCompleter,
479 appStartedCompleter: appStartedCompleter,
480 needsFullRestart: false,
481 );
482 }
483
484 Future<List<Uri?>> _initDevFS() async {
485 final String fsName = globals.fs.path.basename(projectRootPath);
486 return <Uri?>[
487 for (final FlutterDevice? device in flutterDevices)
488 await device!.setupDevFS(fsName, globals.fs.directory(projectRootPath)),
489 ];
490 }
491
492 Future<UpdateFSReport> _updateDevFS({bool fullRestart = false}) async {
493 final bool isFirstUpload = !assetBundle.wasBuiltOnce();
494 final bool rebuildBundle = assetBundle.needsBuild();
495 if (rebuildBundle) {
496 globals.printTrace('Updating assets');
497 final int result = await assetBundle.build(
498 packageConfigPath: debuggingOptions.buildInfo.packageConfigPath,
499 flavor: debuggingOptions.buildInfo.flavor,
500 );
501 if (result != 0) {
502 return UpdateFSReport();
503 }
504 }
505
506 final Stopwatch findInvalidationTimer = _stopwatchFactory.createStopwatch('updateDevFS')
507 ..start();
508 final DevFS devFS = flutterDevices[0].devFS!;
509 final InvalidationResult invalidationResult = await projectFileInvalidator.findInvalidated(
510 lastCompiled: devFS.lastCompiled,
511 urisToMonitor: devFS.sources,
512 packagesPath: packagesFilePath,
513 asyncScanning: hotRunnerConfig!.asyncScanning,
514 packageConfig: devFS.lastPackageConfig ?? debuggingOptions.buildInfo.packageConfig,
515 );
516 findInvalidationTimer.stop();
517 final File entrypointFile = globals.fs.file(mainPath);
518 if (!entrypointFile.existsSync()) {
519 globals.printError(
520 'The entrypoint file (i.e. the file with main()) ${entrypointFile.path} '
521 'cannot be found. Moving or renaming this file will prevent changes to '
522 'its contents from being discovered during hot reload/restart until '
523 'flutter is restarted or the file is restored.',
524 );
525 }
526 final UpdateFSReport results = UpdateFSReport(
527 success: true,
528 scannedSourcesCount: devFS.sources.length,
529 findInvalidatedDuration: findInvalidationTimer.elapsed,
530 );
531 for (final FlutterDevice? device in flutterDevices) {
532 results.incorporateResults(
533 await device!.updateDevFS(
534 mainUri: entrypointFile.absolute.uri,
535 target: target,
536 bundle: assetBundle,
537 bundleFirstUpload: isFirstUpload,
538 bundleDirty: !isFirstUpload && rebuildBundle,
539 fullRestart: fullRestart,
540 pathToReload: getReloadPath(resetCompiler: fullRestart, swap: _swap),
541 invalidatedFiles: invalidationResult.uris!,
542 packageConfig: invalidationResult.packageConfig!,
543 dillOutputPath: dillOutputPath,
544 ),
545 );
546 }
547 return results;
548 }
549
550 void _resetDirtyAssets() {
551 for (final FlutterDevice device in flutterDevices) {
552 final DevFS? devFS = device.devFS;
553 if (devFS == null) {
554 // This is sometimes null, however we don't know why and have not been
555 // able to reproduce, https://github.com/flutter/flutter/issues/108653
556 continue;
557 }
558 devFS.assetPathsToEvict.clear();
559 devFS.shaderPathsToEvict.clear();
560 }
561 }
562
563 Future<void> _cleanupDevFS() async {
564 final List<Future<void>> futures = <Future<void>>[];
565 for (final FlutterDevice device in flutterDevices) {
566 if (device.devFS != null) {
567 // Cleanup the devFS, but don't wait indefinitely.
568 // We ignore any errors, because it's not clear what we would do anyway.
569 futures.add(
570 device.devFS!
571 .destroy()
572 .timeout(const Duration(milliseconds: 250))
573 .then<void>(
574 (Object? _) {},
575 onError: (Object? error, StackTrace stackTrace) {
576 globals.printTrace('Ignored error while cleaning up DevFS: $error\n$stackTrace');
577 },
578 ),
579 );
580 }
581 device.devFS = null;
582 }
583 await Future.wait(futures);
584 }
585
586 Future<void> _launchInView(FlutterDevice device, Uri main, Uri assetsDirectory) async {
587 final List<FlutterView> views = await device.vmService!.getFlutterViews();
588 await Future.wait(<Future<void>>[
589 for (final FlutterView view in views)
590 device.vmService!.runInView(viewId: view.id, main: main, assetsDirectory: assetsDirectory),
591 ]);
592 }
593
594 Future<void> _launchFromDevFS() async {
595 final List<Future<void>> futures = <Future<void>>[];
596 for (final FlutterDevice? device in flutterDevices) {
597 final Uri deviceEntryUri = device!.devFS!.baseUri!.resolve(
598 _swap ? 'main.dart.swap.dill' : 'main.dart.dill',
599 );
600 final Uri deviceAssetsDirectoryUri = device.devFS!.baseUri!.resolveUri(
601 globals.fs.path.toUri(getAssetBuildDirectory()),
602 );
603 futures.add(_launchInView(device, deviceEntryUri, deviceAssetsDirectoryUri));
604 }
605 await Future.wait(futures);
606 }
607
608 Future<OperationResult> _restartFromSources({String? reason}) async {
609 final Stopwatch restartTimer = Stopwatch()..start();
610 UpdateFSReport updatedDevFS;
611 try {
612 updatedDevFS = await _updateDevFS(fullRestart: true);
613 } finally {
614 hotRunnerConfig!.updateDevFSComplete();
615 }
616 if (!updatedDevFS.success) {
617 for (final FlutterDevice? device in flutterDevices) {
618 if (device!.generator != null) {
619 await device.generator!.reject();
620 }
621 }
622 return OperationResult(1, 'DevFS synchronization failed');
623 }
624 _resetDirtyAssets();
625 for (final FlutterDevice? device in flutterDevices) {
626 // VM must have accepted the kernel binary, there will be no reload
627 // report, so we let incremental compiler know that source code was accepted.
628 if (device!.generator != null) {
629 device.generator!.accept();
630 }
631 }
632 // Check if the isolate is paused and resume it.
633 final List<Future<void>> operations = <Future<void>>[];
634 for (final FlutterDevice? device in flutterDevices) {
635 final Set<String?> uiIsolatesIds = <String?>{};
636 final List<FlutterView> views = await device!.vmService!.getFlutterViews();
637 for (final FlutterView view in views) {
638 if (view.uiIsolate == null) {
639 continue;
640 }
641 uiIsolatesIds.add(view.uiIsolate!.id);
642 // Reload the isolate.
643 final Future<vm_service.Isolate?> reloadIsolate = device.vmService!.getIsolateOrNull(
644 view.uiIsolate!.id!,
645 );
646 operations.add(
647 reloadIsolate.then((vm_service.Isolate? isolate) async {
648 if (isolate != null) {
649 // The embedder requires that the isolate is unpaused, because the
650 // runInView method requires interaction with dart engine APIs that
651 // are not thread-safe, and thus must be run on the same thread that
652 // would be blocked by the pause. Simply un-pausing is not sufficient,
653 // because this does not prevent the isolate from immediately hitting
654 // a breakpoint (for example if the breakpoint was placed in a loop
655 // or in a frequently called method) or an exception. Instead, all
656 // breakpoints are first disabled and exception pause mode set to
657 // None, and then the isolate resumed.
658 // These settings do not need restoring as Hot Restart results in
659 // new isolates, which will be configured by the editor as they are
660 // started.
661 final List<Future<void>> breakpointAndExceptionRemoval = <Future<void>>[
662 device.vmService!.service.setIsolatePauseMode(
663 isolate.id!,
664 exceptionPauseMode: vm_service.ExceptionPauseMode.kNone,
665 ),
666 for (final vm_service.Breakpoint breakpoint in isolate.breakpoints!)
667 device.vmService!.service.removeBreakpoint(isolate.id!, breakpoint.id!),
668 ];
669 await Future.wait(breakpointAndExceptionRemoval);
670 if (isPauseEvent(isolate.pauseEvent!.kind!)) {
671 await device.vmService!.service.resume(view.uiIsolate!.id!);
672 }
673 }
674 }),
675 );
676 }
677
678 // Wait for the UI isolates to have their breakpoints removed and exception pause mode
679 // cleared while also ensuring the isolate's are no longer paused. If we don't clear
680 // the exception pause mode before we start killing child isolates, it's possible that
681 // any UI isolate waiting on a result from a child isolate could throw an unhandled
682 // exception and re-pause the isolate, causing hot restart to hang.
683 await Future.wait(operations);
684 operations.clear();
685
686 // The engine handles killing and recreating isolates that it has spawned
687 // ("uiIsolates"). The isolates that were spawned from these uiIsolates
688 // will not be restarted, and so they must be manually killed.
689 final vm_service.VM vm = await device.vmService!.service.getVM();
690 for (final vm_service.IsolateRef isolateRef in vm.isolates!) {
691 if (uiIsolatesIds.contains(isolateRef.id)) {
692 continue;
693 }
694 operations.add(
695 device.vmService!.service
696 .kill(isolateRef.id!)
697 // Since we never check the value of this Future, only await its
698 // completion, make its type nullable so we can return null when
699 // catching errors.
700 .then<vm_service.Success?>(
701 (vm_service.Success success) => success,
702 onError: (Object error, StackTrace stackTrace) {
703 if (error is vm_service.SentinelException ||
704 (error is vm_service.RPCError && error.code == 105)) {
705 // Do nothing on a SentinelException since it means the isolate
706 // has already been killed.
707 // Error code 105 indicates the isolate is not yet runnable, and might
708 // be triggered if the tool is attempting to kill the asset parsing
709 // isolate before it has finished starting up.
710 return null;
711 }
712 return Future<vm_service.Success?>.error(error, stackTrace);
713 },
714 ),
715 );
716 }
717 }
718 await Future.wait(operations);
719 globals.printTrace('Finished waiting on operations.');
720 await _launchFromDevFS();
721 restartTimer.stop();
722 globals.printTrace(
723 'Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.',
724 );
725 _addBenchmarkData('hotRestartMillisecondsToFrame', restartTimer.elapsed.inMilliseconds);
726
727 // Send timing analytics.
728 final Duration elapsedDuration = restartTimer.elapsed;
729 _analytics.send(
730 Event.timing(
731 workflow: 'hot',
732 variableName: 'restart',
733 elapsedMilliseconds: elapsedDuration.inMilliseconds,
734 ),
735 );
736
737 // Toggle the main dill name after successfully uploading.
738 _swap = !_swap;
739
740 return OperationResult(
741 OperationResult.ok.code,
742 OperationResult.ok.message,
743 updateFSReport: updatedDevFS,
744 );
745 }
746
747 /// Returns [true] if the reload was successful.
748 /// Prints errors if [printErrors] is [true].
749 static bool validateReloadReport(
750 vm_service.ReloadReport? reloadReport, {
751 bool printErrors = true,
752 }) {
753 if (reloadReport == null) {
754 if (printErrors) {
755 globals.printError('Hot reload did not receive reload report.');
756 }
757 return false;
758 }
759 final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport);
760 if (!reloadReport.success!) {
761 if (printErrors) {
762 globals.printError('Hot reload was rejected:');
763 for (final ReasonForCancelling reason in contents.notices) {
764 globals.printError(reason.toString());
765 }
766 }
767 return false;
768 }
769 return true;
770 }
771
772 @override
773 Future<OperationResult> restart({
774 bool fullRestart = false,
775 String? reason,
776 bool silent = false,
777 bool pause = false,
778 }) async {
779 if (flutterDevices.any((FlutterDevice? device) => device!.devFS == null)) {
780 return OperationResult(1, 'Device initialization has not completed.');
781 }
782 await _calculateTargetPlatform();
783 final Stopwatch timer = Stopwatch()..start();
784
785 // Run source generation if needed.
786 await runSourceGenerators();
787
788 if (fullRestart) {
789 final OperationResult result = await _fullRestartHelper(
790 targetPlatform: _targetPlatform,
791 sdkName: _sdkName,
792 emulator: _emulator,
793 reason: reason,
794 silent: silent,
795 );
796 if (!silent) {
797 globals.printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
798 }
799 // TODO(bkonyi): remove when ready to serve DevTools from DDS.
800 unawaited(residentDevtoolsHandler!.hotRestart(flutterDevices));
801 // for (final FlutterDevice? device in flutterDevices) {
802 // unawaited(device?.handleHotRestart());
803 // }
804 return result;
805 }
806 final OperationResult result = await _hotReloadHelper(
807 targetPlatform: _targetPlatform,
808 sdkName: _sdkName,
809 emulator: _emulator,
810 reason: reason,
811 pause: pause,
812 );
813 if (result.isOk) {
814 final String elapsed = getElapsedAsMilliseconds(timer.elapsed);
815 if (!silent) {
816 if (result.extraTimings.isNotEmpty) {
817 final String extraTimingsString = result.extraTimings
818 .map((OperationResultExtraTiming e) => '${e.description}: ${e.timeInMs} ms')
819 .join(', ');
820 globals.printStatus('${result.message} in $elapsed ($extraTimingsString).');
821 } else {
822 globals.printStatus('${result.message} in $elapsed.');
823 }
824 }
825 }
826 return result;
827 }
828
829 Future<OperationResult> _fullRestartHelper({
830 String? targetPlatform,
831 String? sdkName,
832 bool? emulator,
833 String? reason,
834 bool? silent,
835 }) async {
836 if (!supportsRestart) {
837 return OperationResult(1, 'hotRestart not supported');
838 }
839 Status? status;
840 if (!silent!) {
841 status = globals.logger.startProgress('Performing hot restart...', progressId: 'hot.restart');
842 }
843 OperationResult result;
844 String? restartEvent;
845 try {
846 final Stopwatch restartTimer = _stopwatchFactory.createStopwatch('fullRestartHelper')
847 ..start();
848 if ((await hotRunnerConfig!.setupHotRestart()) != true) {
849 return OperationResult(1, 'setupHotRestart failed');
850 }
851 result = await _restartFromSources(reason: reason);
852 restartTimer.stop();
853 if (!result.isOk) {
854 restartEvent = 'restart-failed';
855 } else {
856 HotEvent(
857 'restart',
858 targetPlatform: targetPlatform!,
859 sdkName: sdkName!,
860 emulator: emulator!,
861 fullRestart: true,
862 reason: reason,
863 overallTimeInMs: restartTimer.elapsed.inMilliseconds,
864 syncedBytes: result.updateFSReport?.syncedBytes,
865 invalidatedSourcesCount: result.updateFSReport?.invalidatedSourcesCount,
866 transferTimeInMs: result.updateFSReport?.transferDuration.inMilliseconds,
867 compileTimeInMs: result.updateFSReport?.compileDuration.inMilliseconds,
868 findInvalidatedTimeInMs: result.updateFSReport?.findInvalidatedDuration.inMilliseconds,
869 scannedSourcesCount: result.updateFSReport?.scannedSourcesCount,
870 ).send();
871 _analytics.send(
872 Event.hotRunnerInfo(
873 label: 'restart',
874 targetPlatform: targetPlatform,
875 sdkName: sdkName,
876 emulator: emulator,
877 fullRestart: true,
878 reason: reason,
879 overallTimeInMs: restartTimer.elapsed.inMilliseconds,
880 syncedBytes: result.updateFSReport?.syncedBytes,
881 invalidatedSourcesCount: result.updateFSReport?.invalidatedSourcesCount,
882 transferTimeInMs: result.updateFSReport?.transferDuration.inMilliseconds,
883 compileTimeInMs: result.updateFSReport?.compileDuration.inMilliseconds,
884 findInvalidatedTimeInMs: result.updateFSReport?.findInvalidatedDuration.inMilliseconds,
885 scannedSourcesCount: result.updateFSReport?.scannedSourcesCount,
886 ),
887 );
888 }
889 } on vm_service.SentinelException catch (err, st) {
890 restartEvent = 'exception';
891 return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true);
892 } on vm_service.RPCError catch (err, st) {
893 restartEvent = 'exception';
894 return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true);
895 } finally {
896 // The `restartEvent` variable will be null if restart succeeded. We will
897 // only handle the case when it failed here.
898 if (restartEvent != null) {
899 HotEvent(
900 restartEvent,
901 targetPlatform: targetPlatform!,
902 sdkName: sdkName!,
903 emulator: emulator!,
904 fullRestart: true,
905 reason: reason,
906 ).send();
907 _analytics.send(
908 Event.hotRunnerInfo(
909 label: restartEvent,
910 targetPlatform: targetPlatform,
911 sdkName: sdkName,
912 emulator: emulator,
913 fullRestart: true,
914 reason: reason,
915 ),
916 );
917 }
918 status?.cancel();
919 }
920 return result;
921 }
922
923 Future<OperationResult> _hotReloadHelper({
924 String? targetPlatform,
925 String? sdkName,
926 bool? emulator,
927 String? reason,
928 bool? pause,
929 }) async {
930 Status status = globals.logger.startProgress(
931 'Performing hot reload...',
932 progressId: 'hot.reload',
933 );
934 OperationResult result;
935 try {
936 result = await _reloadSources(
937 targetPlatform: targetPlatform,
938 sdkName: sdkName,
939 emulator: emulator,
940 reason: reason,
941 pause: pause,
942 onSlow: (String message) {
943 status.cancel();
944 status = globals.logger.startProgress(message, progressId: 'hot.reload');
945 },
946 );
947 } on vm_service.RPCError catch (error) {
948 String errorMessage = 'hot reload failed to complete';
949 int errorCode = 1;
950 if (error.code == kIsolateReloadBarred) {
951 errorCode = error.code;
952 errorMessage =
953 'Unable to hot reload application due to an unrecoverable error in '
954 'the source code. Please address the error and then use "R" to '
955 'restart the app.\n'
956 '${error.message} (error code: ${error.code})';
957 HotEvent(
958 'reload-barred',
959 targetPlatform: targetPlatform!,
960 sdkName: sdkName!,
961 emulator: emulator!,
962 fullRestart: false,
963 reason: reason,
964 ).send();
965 _analytics.send(
966 Event.hotRunnerInfo(
967 label: 'reload-barred',
968 targetPlatform: targetPlatform,
969 sdkName: sdkName,
970 emulator: emulator,
971 fullRestart: false,
972 reason: reason,
973 ),
974 );
975 } else {
976 HotEvent(
977 'exception',
978 targetPlatform: targetPlatform!,
979 sdkName: sdkName!,
980 emulator: emulator!,
981 fullRestart: false,
982 reason: reason,
983 ).send();
984 _analytics.send(
985 Event.hotRunnerInfo(
986 label: 'exception',
987 targetPlatform: targetPlatform,
988 sdkName: sdkName,
989 emulator: emulator,
990 fullRestart: false,
991 reason: reason,
992 ),
993 );
994 }
995 return OperationResult(errorCode, errorMessage, fatal: true);
996 } finally {
997 status.cancel();
998 }
999 return result;
1000 }
1001
1002 Future<OperationResult> _reloadSources({
1003 String? targetPlatform,
1004 String? sdkName,
1005 bool? emulator,
1006 bool? pause = false,
1007 String? reason,
1008 void Function(String message)? onSlow,
1009 }) async {
1010 final Map<FlutterDevice?, List<FlutterView>> viewCache = <FlutterDevice?, List<FlutterView>>{};
1011 for (final FlutterDevice? device in flutterDevices) {
1012 final List<FlutterView> views = await device!.vmService!.getFlutterViews();
1013 viewCache[device] = views;
1014 for (final FlutterView view in views) {
1015 if (view.uiIsolate == null) {
1016 return OperationResult(2, 'Application isolate not found', fatal: true);
1017 }
1018 }
1019 }
1020
1021 final Stopwatch reloadTimer = _stopwatchFactory.createStopwatch('reloadSources:reload')
1022 ..start();
1023 if ((await hotRunnerConfig!.setupHotReload()) != true) {
1024 return OperationResult(1, 'setupHotReload failed');
1025 }
1026 final Stopwatch devFSTimer = Stopwatch()..start();
1027 UpdateFSReport updatedDevFS;
1028 try {
1029 updatedDevFS = await _updateDevFS();
1030 } finally {
1031 hotRunnerConfig!.updateDevFSComplete();
1032 }
1033 // Record time it took to synchronize to DevFS.
1034 bool shouldReportReloadTime = true;
1035 _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
1036 if (!updatedDevFS.success) {
1037 return OperationResult(1, 'DevFS synchronization failed');
1038 }
1039
1040 final List<OperationResultExtraTiming> extraTimings = <OperationResultExtraTiming>[];
1041 extraTimings.add(
1042 OperationResultExtraTiming('compile', updatedDevFS.compileDuration.inMilliseconds),
1043 );
1044
1045 String reloadMessage = 'Reloaded 0 libraries';
1046 final Stopwatch reloadVMTimer = _stopwatchFactory.createStopwatch('reloadSources:vm')..start();
1047 final Map<String, Object?> firstReloadDetails = <String, Object?>{};
1048 if (updatedDevFS.invalidatedSourcesCount > 0) {
1049 final OperationResult result = await _reloadSourcesHelper(
1050 this,
1051 flutterDevices,
1052 pause,
1053 firstReloadDetails,
1054 targetPlatform,
1055 sdkName,
1056 emulator,
1057 reason,
1058 globals.analytics,
1059 );
1060 if (result.code != 0) {
1061 return result;
1062 }
1063 reloadMessage = result.message;
1064 } else {
1065 _addBenchmarkData('hotReloadVMReloadMilliseconds', 0);
1066 }
1067 reloadVMTimer.stop();
1068 extraTimings.add(OperationResultExtraTiming('reload', reloadVMTimer.elapsedMilliseconds));
1069
1070 await evictDirtyAssets();
1071
1072 final Stopwatch reassembleTimer = _stopwatchFactory.createStopwatch('reloadSources:reassemble')
1073 ..start();
1074
1075 final ReassembleResult reassembleResult = await _reassembleHelper(
1076 flutterDevices,
1077 viewCache,
1078 onSlow,
1079 reloadMessage,
1080 );
1081 shouldReportReloadTime = reassembleResult.shouldReportReloadTime;
1082 if (reassembleResult.reassembleViews.isEmpty) {
1083 return OperationResult(OperationResult.ok.code, reloadMessage);
1084 }
1085 // Record time it took for Flutter to reassemble the application.
1086 reassembleTimer.stop();
1087 _addBenchmarkData(
1088 'hotReloadFlutterReassembleMilliseconds',
1089 reassembleTimer.elapsed.inMilliseconds,
1090 );
1091 extraTimings.add(OperationResultExtraTiming('reassemble', reassembleTimer.elapsedMilliseconds));
1092
1093 reloadTimer.stop();
1094 final Duration reloadDuration = reloadTimer.elapsed;
1095 final int reloadInMs = reloadDuration.inMilliseconds;
1096
1097 // Collect stats that help understand scale of update for this hot reload request.
1098 // For example, [syncedLibraryCount]/[finalLibraryCount] indicates how
1099 // many libraries were affected by the hot reload request.
1100 // Relation of [invalidatedSourcesCount] to [syncedLibraryCount] should help
1101 // understand sync/transfer "overhead" of updating this number of source files.
1102 HotEvent(
1103 'reload',
1104 targetPlatform: targetPlatform!,
1105 sdkName: sdkName!,
1106 emulator: emulator!,
1107 fullRestart: false,
1108 reason: reason,
1109 overallTimeInMs: reloadInMs,
1110 finalLibraryCount: firstReloadDetails['finalLibraryCount'] as int? ?? 0,
1111 syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int? ?? 0,
1112 syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int? ?? 0,
1113 syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int? ?? 0,
1114 syncedBytes: updatedDevFS.syncedBytes,
1115 invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
1116 transferTimeInMs: updatedDevFS.transferDuration.inMilliseconds,
1117 compileTimeInMs: updatedDevFS.compileDuration.inMilliseconds,
1118 findInvalidatedTimeInMs: updatedDevFS.findInvalidatedDuration.inMilliseconds,
1119 scannedSourcesCount: updatedDevFS.scannedSourcesCount,
1120 reassembleTimeInMs: reassembleTimer.elapsed.inMilliseconds,
1121 reloadVMTimeInMs: reloadVMTimer.elapsed.inMilliseconds,
1122 ).send();
1123 _analytics.send(
1124 Event.hotRunnerInfo(
1125 label: 'reload',
1126 targetPlatform: targetPlatform,
1127 sdkName: sdkName,
1128 emulator: emulator,
1129 fullRestart: false,
1130 reason: reason,
1131 overallTimeInMs: reloadInMs,
1132 finalLibraryCount: firstReloadDetails['finalLibraryCount'] as int? ?? 0,
1133 syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int? ?? 0,
1134 syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int? ?? 0,
1135 syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int? ?? 0,
1136 syncedBytes: updatedDevFS.syncedBytes,
1137 invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
1138 transferTimeInMs: updatedDevFS.transferDuration.inMilliseconds,
1139 compileTimeInMs: updatedDevFS.compileDuration.inMilliseconds,
1140 findInvalidatedTimeInMs: updatedDevFS.findInvalidatedDuration.inMilliseconds,
1141 scannedSourcesCount: updatedDevFS.scannedSourcesCount,
1142 reassembleTimeInMs: reassembleTimer.elapsed.inMilliseconds,
1143 reloadVMTimeInMs: reloadVMTimer.elapsed.inMilliseconds,
1144 ),
1145 );
1146
1147 if (shouldReportReloadTime) {
1148 globals.printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.');
1149 // Record complete time it took for the reload.
1150 _addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
1151 }
1152 // Only report timings if we reloaded a single view without any errors.
1153 if ((reassembleResult.reassembleViews.length == 1) &&
1154 !reassembleResult.failedReassemble &&
1155 shouldReportReloadTime) {
1156 _analytics.send(
1157 Event.timing(
1158 workflow: 'hot',
1159 variableName: 'reload',
1160 elapsedMilliseconds: reloadDuration.inMilliseconds,
1161 ),
1162 );
1163 }
1164 return OperationResult(
1165 reassembleResult.failedReassemble ? 1 : OperationResult.ok.code,
1166 reloadMessage,
1167 extraTimings: extraTimings,
1168 );
1169 }
1170
1171 @visibleForTesting
1172 Future<void> evictDirtyAssets() async {
1173 final List<Future<void>> futures = <Future<void>>[];
1174 for (final FlutterDevice? device in flutterDevices) {
1175 if (device!.devFS!.assetPathsToEvict.isEmpty && device.devFS!.shaderPathsToEvict.isEmpty) {
1176 continue;
1177 }
1178 final List<FlutterView> views = await device.vmService!.getFlutterViews();
1179
1180 // If this is the first time we update the assets, make sure to call the setAssetDirectory
1181 if (!device.devFS!.hasSetAssetDirectory) {
1182 final Uri deviceAssetsDirectoryUri = device.devFS!.baseUri!.resolveUri(
1183 globals.fs.path.toUri(getAssetBuildDirectory()),
1184 );
1185 await Future.wait<void>(
1186 views.map<Future<void>>(
1187 (FlutterView view) => device.vmService!.setAssetDirectory(
1188 assetsDirectory: deviceAssetsDirectoryUri,
1189 uiIsolateId: view.uiIsolate!.id,
1190 viewId: view.id,
1191 windows: device.targetPlatform == TargetPlatform.windows_x64,
1192 ),
1193 ),
1194 );
1195 for (final FlutterView view in views) {
1196 globals.printTrace('Set asset directory in $view.');
1197 }
1198 device.devFS!.hasSetAssetDirectory = true;
1199 }
1200
1201 if (views.first.uiIsolate == null) {
1202 globals.printError('Application isolate not found for $device');
1203 continue;
1204 }
1205
1206 if (device.devFS!.didUpdateFontManifest) {
1207 futures.add(
1208 device.vmService!.reloadAssetFonts(
1209 isolateId: views.first.uiIsolate!.id!,
1210 viewId: views.first.id,
1211 ),
1212 );
1213 }
1214
1215 for (final String assetPath in device.devFS!.assetPathsToEvict) {
1216 futures.add(
1217 device.vmService!.flutterEvictAsset(assetPath, isolateId: views.first.uiIsolate!.id!),
1218 );
1219 }
1220 for (final String assetPath in device.devFS!.shaderPathsToEvict) {
1221 futures.add(
1222 device.vmService!.flutterEvictShader(assetPath, isolateId: views.first.uiIsolate!.id!),
1223 );
1224 }
1225 device.devFS!.assetPathsToEvict.clear();
1226 device.devFS!.shaderPathsToEvict.clear();
1227 }
1228 await Future.wait<void>(futures);
1229 }
1230
1231 @override
1232 Future<void> cleanupAfterSignal() async {
1233 await residentDevtoolsHandler!.shutdown();
1234 await stopEchoingDeviceLog();
1235 await hotRunnerConfig!.runPreShutdownOperations();
1236 shutdownDartDevelopmentService();
1237 if (stopAppDuringCleanup) {
1238 return exitApp();
1239 }
1240 appFinished();
1241 }
1242
1243 @override
1244 Future<void> preExit() async {
1245 await _cleanupDevFS();
1246 await hotRunnerConfig!.runPreShutdownOperations();
1247 await super.preExit();
1248 }
1249
1250 @override
1251 Future<void> cleanupAtFinish() async {
1252 for (final FlutterDevice? flutterDevice in flutterDevices) {
1253 await flutterDevice!.device!.dispose();
1254 }
1255 await _cleanupDevFS();
1256 await residentDevtoolsHandler!.shutdown();
1257 await stopEchoingDeviceLog();
1258 }
1259}
1260
1261typedef ReloadSourcesHelper =
1262 Future<OperationResult> Function(
1263 HotRunner hotRunner,
1264 List<FlutterDevice?> flutterDevices,
1265 bool? pause,
1266 Map<String, dynamic> firstReloadDetails,
1267 String? targetPlatform,
1268 String? sdkName,
1269 bool? emulator,
1270 String? reason,
1271 Analytics analytics,
1272 );
1273
1274@visibleForTesting
1275Future<OperationResult> defaultReloadSourcesHelper(
1276 HotRunner hotRunner,
1277 List<FlutterDevice?> flutterDevices,
1278 bool? pause,
1279 Map<String, dynamic> firstReloadDetails,
1280 String? targetPlatform,
1281 String? sdkName,
1282 bool? emulator,
1283 String? reason,
1284 Analytics analytics,
1285) async {
1286 final Stopwatch vmReloadTimer = Stopwatch()..start();
1287 const String entryPath = 'main.dart.incremental.dill';
1288 final List<Future<DeviceReloadReport?>> allReportsFutures = <Future<DeviceReloadReport?>>[];
1289
1290 for (final FlutterDevice? device in flutterDevices) {
1291 final List<Future<vm_service.ReloadReport>> reportFutures = await _reloadDeviceSources(
1292 device!,
1293 entryPath,
1294 pause: pause,
1295 );
1296 allReportsFutures.add(
1297 Future.wait(reportFutures).then<DeviceReloadReport?>((
1298 List<vm_service.ReloadReport> reports,
1299 ) async {
1300 // TODO(aam): Investigate why we are validating only first reload report,
1301 // which seems to be current behavior
1302 if (reports.isEmpty) {
1303 return null;
1304 }
1305 final vm_service.ReloadReport firstReport = reports.first;
1306 // Don't print errors because they will be printed further down when
1307 // `validateReloadReport` is called again.
1308 await device.updateReloadStatus(
1309 HotRunner.validateReloadReport(firstReport, printErrors: false),
1310 );
1311 return DeviceReloadReport(device, reports);
1312 }),
1313 );
1314 }
1315 final Iterable<DeviceReloadReport> reports =
1316 (await Future.wait(allReportsFutures)).whereType<DeviceReloadReport>();
1317 final vm_service.ReloadReport? reloadReport = reports.isEmpty ? null : reports.first.reports[0];
1318 if (reloadReport == null || !HotRunner.validateReloadReport(reloadReport)) {
1319 analytics.send(
1320 Event.hotRunnerInfo(
1321 label: 'reload-reject',
1322 targetPlatform: targetPlatform!,
1323 sdkName: sdkName!,
1324 emulator: emulator!,
1325 fullRestart: false,
1326 reason: reason,
1327 ),
1328 );
1329 // Reset devFS lastCompileTime to ensure the file will still be marked
1330 // as dirty on subsequent reloads.
1331 _resetDevFSCompileTime(flutterDevices);
1332 if (reloadReport == null) {
1333 return OperationResult(1, 'No Dart isolates found');
1334 }
1335 final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport);
1336 return OperationResult(1, 'Reload rejected: ${contents.notices.join("\n")}');
1337 }
1338 // Collect stats only from the first device. If/when run -d all is
1339 // refactored, we'll probably need to send one hot reload/restart event
1340 // per device to analytics.
1341 firstReloadDetails.addAll(castStringKeyedMap(reloadReport.json!['details'])!);
1342 final Map<String, dynamic> details = reloadReport.json!['details'] as Map<String, dynamic>;
1343 final int? loadedLibraryCount = details['loadedLibraryCount'] as int?;
1344 final int? finalLibraryCount = details['finalLibraryCount'] as int?;
1345 globals.printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
1346 // reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
1347 // Record time it took for the VM to reload the sources.
1348 hotRunner._addBenchmarkData(
1349 'hotReloadVMReloadMilliseconds',
1350 vmReloadTimer.elapsed.inMilliseconds,
1351 );
1352 return OperationResult(0, 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries');
1353}
1354
1355Future<List<Future<vm_service.ReloadReport>>> _reloadDeviceSources(
1356 FlutterDevice device,
1357 String entryPath, {
1358 bool? pause = false,
1359}) async {
1360 final String deviceEntryUri = device.devFS!.baseUri!.resolve(entryPath).toString();
1361 final vm_service.VM vm = await device.vmService!.service.getVM();
1362 return <Future<vm_service.ReloadReport>>[
1363 for (final vm_service.IsolateRef isolateRef in vm.isolates!)
1364 device.vmService!.service.reloadSources(
1365 isolateRef.id!,
1366 pause: pause,
1367 rootLibUri: deviceEntryUri,
1368 ),
1369 ];
1370}
1371
1372void _resetDevFSCompileTime(List<FlutterDevice?> flutterDevices) {
1373 for (final FlutterDevice? device in flutterDevices) {
1374 device!.devFS!.resetLastCompiled();
1375 }
1376}
1377
1378@visibleForTesting
1379class ReassembleResult {
1380 ReassembleResult(this.reassembleViews, this.failedReassemble, this.shouldReportReloadTime);
1381 final Map<FlutterView?, FlutterVmService?> reassembleViews;
1382 final bool failedReassemble;
1383 final bool shouldReportReloadTime;
1384}
1385
1386typedef ReassembleHelper =
1387 Future<ReassembleResult> Function(
1388 List<FlutterDevice?> flutterDevices,
1389 Map<FlutterDevice?, List<FlutterView>> viewCache,
1390 void Function(String message)? onSlow,
1391 String reloadMessage,
1392 );
1393
1394Future<ReassembleResult> _defaultReassembleHelper(
1395 List<FlutterDevice?> flutterDevices,
1396 Map<FlutterDevice?, List<FlutterView>> viewCache,
1397 void Function(String message)? onSlow,
1398 String reloadMessage,
1399) async {
1400 // Check if any isolates are paused and reassemble those that aren't.
1401 final Map<FlutterView, FlutterVmService?> reassembleViews = <FlutterView, FlutterVmService?>{};
1402 final List<Future<void>> reassembleFutures = <Future<void>>[];
1403 String? serviceEventKind;
1404 int pausedIsolatesFound = 0;
1405 bool failedReassemble = false;
1406 bool shouldReportReloadTime = true;
1407 for (final FlutterDevice? device in flutterDevices) {
1408 final List<FlutterView> views = viewCache[device]!;
1409 for (final FlutterView view in views) {
1410 // Check if the isolate is paused, and if so, don't reassemble. Ignore the
1411 // PostPauseEvent event - the client requesting the pause will resume the app.
1412 final vm_service.Event? pauseEvent = await device!.vmService!.getIsolatePauseEventOrNull(
1413 view.uiIsolate!.id!,
1414 );
1415 if (pauseEvent != null &&
1416 isPauseEvent(pauseEvent.kind!) &&
1417 pauseEvent.kind != vm_service.EventKind.kPausePostRequest) {
1418 pausedIsolatesFound += 1;
1419 if (serviceEventKind == null) {
1420 serviceEventKind = pauseEvent.kind;
1421 } else if (serviceEventKind != pauseEvent.kind) {
1422 serviceEventKind = ''; // many kinds
1423 }
1424 } else {
1425 reassembleViews[view] = device.vmService;
1426 // If the tool identified a change in a single widget, do a fast instead
1427 // of a full reassemble.
1428 final Future<void> reassembleWork = device.vmService!.flutterReassemble(
1429 isolateId: view.uiIsolate!.id,
1430 );
1431 reassembleFutures.add(
1432 reassembleWork.then(
1433 (Object? obj) => obj,
1434 onError: (Object error, StackTrace stackTrace) {
1435 if (error is! Exception) {
1436 return Future<Object?>.error(error, stackTrace);
1437 }
1438 failedReassemble = true;
1439 globals.printError(
1440 'Reassembling ${view.uiIsolate!.name} failed: $error\n$stackTrace',
1441 );
1442 },
1443 ),
1444 );
1445 }
1446 }
1447 }
1448 if (pausedIsolatesFound > 0) {
1449 if (onSlow != null) {
1450 onSlow(
1451 '${_describePausedIsolates(pausedIsolatesFound, serviceEventKind!)}; interface might not update.',
1452 );
1453 }
1454 if (reassembleViews.isEmpty) {
1455 globals.printTrace('Skipping reassemble because all isolates are paused.');
1456 return ReassembleResult(reassembleViews, failedReassemble, shouldReportReloadTime);
1457 }
1458 }
1459 assert(reassembleViews.isNotEmpty);
1460
1461 globals.printTrace('Reassembling application');
1462
1463 final Future<void> reassembleFuture = Future.wait<void>(reassembleFutures).then((void _) => null);
1464 await reassembleFuture.timeout(
1465 const Duration(seconds: 2),
1466 onTimeout: () async {
1467 if (pausedIsolatesFound > 0) {
1468 shouldReportReloadTime = false;
1469 return; // probably no point waiting, they're probably deadlocked and we've already warned.
1470 }
1471 // Check if any isolate is newly paused.
1472 globals.printTrace('This is taking a long time; will now check for paused isolates.');
1473 int postReloadPausedIsolatesFound = 0;
1474 String? serviceEventKind;
1475 for (final FlutterView view in reassembleViews.keys) {
1476 final vm_service.Event? pauseEvent = await reassembleViews[view]!
1477 .getIsolatePauseEventOrNull(view.uiIsolate!.id!);
1478 if (pauseEvent != null && isPauseEvent(pauseEvent.kind!)) {
1479 postReloadPausedIsolatesFound += 1;
1480 if (serviceEventKind == null) {
1481 serviceEventKind = pauseEvent.kind;
1482 } else if (serviceEventKind != pauseEvent.kind) {
1483 serviceEventKind = ''; // many kinds
1484 }
1485 }
1486 }
1487 globals.printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).');
1488 if (postReloadPausedIsolatesFound == 0) {
1489 await reassembleFuture; // must just be taking a long time... keep waiting!
1490 return;
1491 }
1492 shouldReportReloadTime = false;
1493 if (onSlow != null) {
1494 onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind!)}.');
1495 }
1496 return;
1497 },
1498 );
1499 return ReassembleResult(reassembleViews, failedReassemble, shouldReportReloadTime);
1500}
1501
1502String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) {
1503 assert(pausedIsolatesFound > 0);
1504 final StringBuffer message = StringBuffer();
1505 bool plural;
1506 if (pausedIsolatesFound == 1) {
1507 message.write('The application is ');
1508 plural = false;
1509 } else {
1510 message.write('$pausedIsolatesFound isolates are ');
1511 plural = true;
1512 }
1513 message.write(switch (serviceEventKind) {
1514 vm_service.EventKind.kPauseStart => 'paused (probably due to --start-paused)',
1515 vm_service.EventKind.kPauseExit =>
1516 'paused because ${plural ? 'they have' : 'it has'} terminated',
1517 vm_service.EventKind.kPauseBreakpoint => 'paused in the debugger on a breakpoint',
1518 vm_service.EventKind.kPauseInterrupted => 'paused due in the debugger',
1519 vm_service.EventKind.kPauseException => 'paused in the debugger after an exception was thrown',
1520 vm_service.EventKind.kPausePostRequest => 'paused',
1521 '' => 'paused for various reasons',
1522 _ => 'paused',
1523 });
1524 return message.toString();
1525}
1526
1527/// The result of an invalidation check from [ProjectFileInvalidator].
1528class InvalidationResult {
1529 const InvalidationResult({this.uris, this.packageConfig});
1530
1531 final List<Uri>? uris;
1532 final PackageConfig? packageConfig;
1533}
1534
1535/// The [ProjectFileInvalidator] track the dependencies for a running
1536/// application to determine when they are dirty.
1537class ProjectFileInvalidator {
1538 ProjectFileInvalidator({
1539 required FileSystem fileSystem,
1540 required Platform platform,
1541 required Logger logger,
1542 }) : _fileSystem = fileSystem,
1543 _platform = platform,
1544 _logger = logger;
1545
1546 final FileSystem _fileSystem;
1547 final Platform _platform;
1548 final Logger _logger;
1549
1550 static const String _pubCachePathLinuxAndMac = '.pub-cache';
1551 static const String _pubCachePathWindows = 'Pub/Cache';
1552
1553 // As of writing, Dart supports up to 32 asynchronous I/O threads per
1554 // isolate. We also want to avoid hitting platform limits on open file
1555 // handles/descriptors.
1556 //
1557 // This value was chosen based on empirical tests scanning a set of
1558 // ~2000 files.
1559 static const int _kMaxPendingStats = 8;
1560
1561 Future<InvalidationResult> findInvalidated({
1562 required DateTime? lastCompiled,
1563 required List<Uri> urisToMonitor,
1564 required String packagesPath,
1565 required PackageConfig packageConfig,
1566 bool asyncScanning = false,
1567 }) async {
1568 if (lastCompiled == null) {
1569 // Initial load.
1570 assert(urisToMonitor.isEmpty);
1571 return InvalidationResult(packageConfig: packageConfig, uris: <Uri>[]);
1572 }
1573
1574 final Stopwatch stopwatch = Stopwatch()..start();
1575 final List<Uri> urisToScan = <Uri>[
1576 // Don't watch pub cache directories to speed things up a little.
1577 for (final Uri uri in urisToMonitor)
1578 if (_isNotInPubCache(uri)) uri,
1579 ];
1580 final List<Uri> invalidatedFiles = <Uri>[];
1581 if (asyncScanning) {
1582 final Pool pool = Pool(_kMaxPendingStats);
1583 final List<Future<void>> waitList = <Future<void>>[];
1584 for (final Uri uri in urisToScan) {
1585 waitList.add(
1586 pool.withResource<void>(
1587 // Calling fs.stat() is more performant than fs.file().stat(), but
1588 // uri.toFilePath() does not work with MultiRootFileSystem.
1589 () => (uri.hasScheme && uri.scheme != 'file'
1590 ? _fileSystem.file(uri).stat()
1591 : _fileSystem.stat(uri.toFilePath(windows: _platform.isWindows)))
1592 .then((FileStat stat) {
1593 final DateTime updatedAt = stat.modified;
1594 if (updatedAt.isAfter(lastCompiled)) {
1595 invalidatedFiles.add(uri);
1596 }
1597 }),
1598 ),
1599 );
1600 }
1601 await Future.wait<void>(waitList);
1602 } else {
1603 for (final Uri uri in urisToScan) {
1604 // Calling fs.statSync() is more performant than fs.file().statSync(), but
1605 // uri.toFilePath() does not work with MultiRootFileSystem.
1606 final DateTime updatedAt =
1607 uri.hasScheme && uri.scheme != 'file'
1608 ? _fileSystem.file(uri).statSync().modified
1609 : _fileSystem.statSync(uri.toFilePath(windows: _platform.isWindows)).modified;
1610 if (updatedAt.isAfter(lastCompiled)) {
1611 invalidatedFiles.add(uri);
1612 }
1613 }
1614 }
1615 // We need to check the .dart_tool/package_config.json file too since it is
1616 // not used in compilation.
1617 final File packageFile = _fileSystem.file(packagesPath);
1618 final Uri packageUri = packageFile.uri;
1619 final DateTime updatedAt = packageFile.statSync().modified;
1620 if (updatedAt.isAfter(lastCompiled)) {
1621 invalidatedFiles.add(packageUri);
1622 }
1623
1624 _logger.printTrace(
1625 'Scanned through ${urisToScan.length} files in '
1626 '${stopwatch.elapsedMilliseconds}ms'
1627 '${asyncScanning ? " (async)" : ""}',
1628 );
1629 return InvalidationResult(packageConfig: packageConfig, uris: invalidatedFiles);
1630 }
1631
1632 bool _isNotInPubCache(Uri uri) {
1633 return !(_platform.isWindows && uri.path.contains(_pubCachePathWindows)) &&
1634 !uri.path.contains(_pubCachePathLinuxAndMac);
1635 }
1636}
1637
1638/// Additional serialization logic for a hot reload response.
1639class ReloadReportContents {
1640 factory ReloadReportContents.fromReloadReport(vm_service.ReloadReport report) {
1641 final List<ReasonForCancelling> reasons = <ReasonForCancelling>[];
1642 final Object? notices = report.json!['notices'];
1643 if (notices is! List<dynamic>) {
1644 return ReloadReportContents._(report.success, reasons, report);
1645 }
1646 for (final Object? obj in notices) {
1647 if (obj is! Map<String, dynamic>) {
1648 continue;
1649 }
1650 final Map<String, dynamic> notice = obj;
1651 reasons.add(
1652 ReasonForCancelling(
1653 message: notice['message'] is String ? notice['message'] as String? : 'Unknown Error',
1654 ),
1655 );
1656 }
1657
1658 return ReloadReportContents._(report.success, reasons, report);
1659 }
1660
1661 ReloadReportContents._(this.success, this.notices, this.report);
1662
1663 final bool? success;
1664 final List<ReasonForCancelling> notices;
1665 final vm_service.ReloadReport report;
1666}
1667
1668/// A serialization class for hot reload rejection reasons.
1669///
1670/// Injects an additional error message that a hot restart will
1671/// resolve the issue.
1672class ReasonForCancelling {
1673 ReasonForCancelling({this.message});
1674
1675 final String? message;
1676
1677 @override
1678 String toString() {
1679 return '$message.\nTry performing a hot restart instead.';
1680 }
1681}
1682

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com