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 | |
5 | import 'dart:async'; |
6 | |
7 | import 'package:meta/meta.dart'; |
8 | import 'package:package_config/package_config.dart'; |
9 | import 'package:pool/pool.dart'; |
10 | import 'package:unified_analytics/unified_analytics.dart'; |
11 | import 'package:vm_service/vm_service.dart'as vm_service; |
12 | |
13 | import 'base/context.dart'; |
14 | import 'base/file_system.dart'; |
15 | import 'base/logger.dart'; |
16 | import 'base/platform.dart'; |
17 | import 'base/utils.dart'; |
18 | import 'build_info.dart'; |
19 | import 'compile.dart'; |
20 | import 'convert.dart'; |
21 | import 'devfs.dart'; |
22 | import 'device.dart'; |
23 | import 'globals.dart' as globals; |
24 | import 'project.dart'; |
25 | import 'reporting/reporting.dart'; |
26 | import 'resident_runner.dart'; |
27 | import 'vmservice.dart'; |
28 | |
29 | ProjectFileInvalidator get projectFileInvalidator => |
30 | context.get<ProjectFileInvalidator>() ?? |
31 | ProjectFileInvalidator( |
32 | fileSystem: globals.fs, |
33 | platform: globals.platform, |
34 | logger: globals.logger, |
35 | ); |
36 | |
37 | HotRunnerConfig? get hotRunnerConfig => context.get<HotRunnerConfig>(); |
38 | |
39 | class 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 | |
70 | const bool kHotReloadDefault = true; |
71 | |
72 | class 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 | |
79 | class 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 | |
1261 | typedef 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 |
1275 | Future<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 | |
1355 | Future<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 | |
1372 | void _resetDevFSCompileTime(List<FlutterDevice?> flutterDevices) { |
1373 | for (final FlutterDevice? device in flutterDevices) { |
1374 | device!.devFS!.resetLastCompiled(); |
1375 | } |
1376 | } |
1377 | |
1378 | @visibleForTesting |
1379 | class 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 | |
1386 | typedef 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 | |
1394 | Future<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 | |
1502 | String _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]. |
1528 | class 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. |
1537 | class 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. |
1639 | class 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. |
1672 | class 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 |
Definitions
- projectFileInvalidator
- hotRunnerConfig
- HotRunnerConfig
- setupHotRestart
- setupHotReload
- updateDevFSComplete
- runPreShutdownOperations
- kHotReloadDefault
- DeviceReloadReport
- DeviceReloadReport
- HotRunner
- HotRunner
- supportsDetach
- _calculateTargetPlatform
- _addBenchmarkData
- _reloadSourcesService
- _restartService
- _compileExpressionService
- attach
- _attach
- run
- _initDevFS
- _updateDevFS
- _resetDirtyAssets
- _cleanupDevFS
- _launchInView
- _launchFromDevFS
- _restartFromSources
- validateReloadReport
- restart
- _fullRestartHelper
- _hotReloadHelper
- _reloadSources
- evictDirtyAssets
- cleanupAfterSignal
- preExit
- cleanupAtFinish
- defaultReloadSourcesHelper
- _reloadDeviceSources
- _resetDevFSCompileTime
- ReassembleResult
- ReassembleResult
- _defaultReassembleHelper
- _describePausedIsolates
- InvalidationResult
- InvalidationResult
- ProjectFileInvalidator
- ProjectFileInvalidator
- findInvalidated
- _isNotInPubCache
- ReloadReportContents
- fromReloadReport
- _
- ReasonForCancelling
- ReasonForCancelling
Learn more about Flutter for embedded and desktop on industrialflutter.com