Unverified Commit 89a3c353 authored by Lau Ching Jun's avatar Lau Ching Jun Committed by GitHub

Add more analytics for hot reload in flutter_tools. (#83972)

parent 1e328e2b
......@@ -19,7 +19,10 @@ class StopwatchFactory {
const StopwatchFactory();
/// Create a new [Stopwatch] instance.
Stopwatch createStopwatch() => Stopwatch();
///
/// The optional [name] parameter is useful in tests when there are multiple
/// instances being created.
Stopwatch createStopwatch([String name = '']) => Stopwatch();
}
typedef VoidCallback = void Function();
......
......@@ -399,18 +399,34 @@ class UpdateFSReport {
int invalidatedSourcesCount = 0,
int syncedBytes = 0,
this.fastReassembleClassName,
int scannedSourcesCount = 0,
Duration compileDuration = Duration.zero,
Duration transferDuration = Duration.zero,
Duration findInvalidatedDuration = Duration.zero,
}) : _success = success,
_invalidatedSourcesCount = invalidatedSourcesCount,
_syncedBytes = syncedBytes;
_syncedBytes = syncedBytes,
_scannedSourcesCount = scannedSourcesCount,
_compileDuration = compileDuration,
_transferDuration = transferDuration,
_findInvalidatedDuration = findInvalidatedDuration;
bool get success => _success;
int get invalidatedSourcesCount => _invalidatedSourcesCount;
int get syncedBytes => _syncedBytes;
int get scannedSourcesCount => _scannedSourcesCount;
Duration get compileDuration => _compileDuration;
Duration get transferDuration => _transferDuration;
Duration get findInvalidatedDuration => _findInvalidatedDuration;
bool _success;
String fastReassembleClassName;
int _invalidatedSourcesCount;
int _syncedBytes;
int _scannedSourcesCount;
Duration _compileDuration;
Duration _transferDuration;
Duration _findInvalidatedDuration;
void incorporateResults(UpdateFSReport report) {
if (!report._success) {
......@@ -419,6 +435,10 @@ class UpdateFSReport {
fastReassembleClassName ??= report.fastReassembleClassName;
_invalidatedSourcesCount += report._invalidatedSourcesCount;
_syncedBytes += report._syncedBytes;
_scannedSourcesCount += report._scannedSourcesCount;
_compileDuration += report._compileDuration;
_transferDuration += report._transferDuration;
_findInvalidatedDuration += report._findInvalidatedDuration;
}
}
......@@ -435,6 +455,7 @@ class DevFS {
@required FileSystem fileSystem,
HttpClient httpClient,
Duration uploadRetryThrottle,
StopwatchFactory stopwatchFactory = const StopwatchFactory(),
}) : _vmService = serviceProtocol,
_logger = logger,
_fileSystem = fileSystem,
......@@ -446,13 +467,14 @@ class DevFS {
uploadRetryThrottle: uploadRetryThrottle,
httpClient: httpClient ?? ((context.get<HttpClientFactory>() == null)
? HttpClient()
: context.get<HttpClientFactory>()())
);
: context.get<HttpClientFactory>()())),
_stopwatchFactory = stopwatchFactory;
final FlutterVmService _vmService;
final _DevFSHttpWriter _httpWriter;
final Logger _logger;
final FileSystem _fileSystem;
final StopwatchFactory _stopwatchFactory;
final String fsName;
final Directory rootDirectory;
......@@ -575,6 +597,7 @@ class DevFS {
// Await the compiler response after checking if the bundle is updated. This allows the file
// stating to be done while waiting for the frontend_server response.
final Stopwatch compileTimer = _stopwatchFactory.createStopwatch('compile')..start();
final Future<CompilerOutput> pendingCompilerOutput = generator.recompile(
mainUri,
invalidatedFiles,
......@@ -582,7 +605,11 @@ class DevFS {
fs: _fileSystem,
projectRootPath: projectRootPath,
packageConfig: packageConfig,
);
).then((CompilerOutput result) {
compileTimer.stop();
return result;
});
if (bundle != null) {
// The tool writes the assets into the AssetBundle working dir so that they
// are in the same location in DevFS and the iOS simulator.
......@@ -627,15 +654,19 @@ class DevFS {
}
}
_logger.printTrace('Updating files.');
final Stopwatch transferTimer = _stopwatchFactory.createStopwatch('transfer')..start();
if (dirtyEntries.isNotEmpty) {
await (devFSWriter ?? _httpWriter).write(dirtyEntries, _baseUri, _httpWriter);
}
transferTimer.stop();
_logger.printTrace('DevFS: Sync finished');
return UpdateFSReport(
success: true,
syncedBytes: syncedBytes,
invalidatedSourcesCount: invalidatedFiles.length,
fastReassembleClassName: _checkIfSingleWidgetReloadApplied(),
compileDuration: compileTimer.elapsed,
transferDuration: transferTimer.elapsed,
);
}
......
......@@ -61,6 +61,11 @@ class CustomDimensions {
this.fastReassemble,
this.nullSafeMigratedLibraries,
this.nullSafeTotalLibraries,
this.hotEventCompileTimeInMs,
this.hotEventFindInvalidatedTimeInMs,
this.hotEventScannedSourcesCount,
this.hotEventReassembleTimeInMs,
this.hotEventReloadVMTimeInMs,
});
final String? sessionHostOsDetails; // cd1
......@@ -113,6 +118,11 @@ class CustomDimensions {
final bool? fastReassemble; // cd48
final int? nullSafeMigratedLibraries; // cd49
final int? nullSafeTotalLibraries; // cd50
final int? hotEventCompileTimeInMs; // cd 51
final int? hotEventFindInvalidatedTimeInMs; // cd 52
final int? hotEventScannedSourcesCount; // cd 53
final int? hotEventReassembleTimeInMs; // cd 54
final int? hotEventReloadVMTimeInMs; // cd 55
/// Convert to a map that will be used to upload to the analytics backend.
Map<String, String> toMap() => <String, String>{
......@@ -166,6 +176,11 @@ class CustomDimensions {
if (fastReassemble != null) cdKey(CustomDimensionsEnum.fastReassemble): fastReassemble.toString(),
if (nullSafeMigratedLibraries != null) cdKey(CustomDimensionsEnum.nullSafeMigratedLibraries): nullSafeMigratedLibraries.toString(),
if (nullSafeTotalLibraries != null) cdKey(CustomDimensionsEnum.nullSafeTotalLibraries): nullSafeTotalLibraries.toString(),
if (hotEventCompileTimeInMs != null) cdKey(CustomDimensionsEnum.hotEventCompileTimeInMs): hotEventCompileTimeInMs.toString(),
if (hotEventFindInvalidatedTimeInMs != null) cdKey(CustomDimensionsEnum.hotEventFindInvalidatedTimeInMs): hotEventFindInvalidatedTimeInMs.toString(),
if (hotEventScannedSourcesCount != null) cdKey(CustomDimensionsEnum.hotEventScannedSourcesCount): hotEventScannedSourcesCount.toString(),
if (hotEventReassembleTimeInMs != null) cdKey(CustomDimensionsEnum.hotEventReassembleTimeInMs): hotEventReassembleTimeInMs.toString(),
if (hotEventReloadVMTimeInMs != null) cdKey(CustomDimensionsEnum.hotEventReloadVMTimeInMs): hotEventReloadVMTimeInMs.toString(),
};
/// Merge the values of two [CustomDimensions] into one. If a value is defined
......@@ -226,6 +241,11 @@ class CustomDimensions {
fastReassemble: other.fastReassemble ?? fastReassemble,
nullSafeMigratedLibraries: other.nullSafeMigratedLibraries ?? nullSafeMigratedLibraries,
nullSafeTotalLibraries: other.nullSafeTotalLibraries ?? nullSafeTotalLibraries,
hotEventCompileTimeInMs: other.hotEventCompileTimeInMs ?? hotEventCompileTimeInMs,
hotEventFindInvalidatedTimeInMs: other.hotEventFindInvalidatedTimeInMs ?? hotEventFindInvalidatedTimeInMs,
hotEventScannedSourcesCount: other.hotEventScannedSourcesCount ?? hotEventScannedSourcesCount,
hotEventReassembleTimeInMs: other.hotEventReassembleTimeInMs ?? hotEventReassembleTimeInMs,
hotEventReloadVMTimeInMs: other.hotEventReloadVMTimeInMs ?? hotEventReloadVMTimeInMs,
);
}
......@@ -280,6 +300,11 @@ class CustomDimensions {
fastReassemble: _extractBool(map, CustomDimensionsEnum.fastReassemble),
nullSafeMigratedLibraries: _extractInt(map, CustomDimensionsEnum.nullSafeMigratedLibraries),
nullSafeTotalLibraries: _extractInt(map, CustomDimensionsEnum.nullSafeTotalLibraries),
hotEventCompileTimeInMs: _extractInt(map, CustomDimensionsEnum.hotEventCompileTimeInMs),
hotEventFindInvalidatedTimeInMs: _extractInt(map, CustomDimensionsEnum.hotEventFindInvalidatedTimeInMs),
hotEventScannedSourcesCount: _extractInt(map, CustomDimensionsEnum.hotEventScannedSourcesCount),
hotEventReassembleTimeInMs: _extractInt(map, CustomDimensionsEnum.hotEventReassembleTimeInMs),
hotEventReloadVMTimeInMs: _extractInt(map, CustomDimensionsEnum.hotEventReloadVMTimeInMs),
);
static bool? _extractBool(Map<String, String> map, CustomDimensionsEnum field) =>
......@@ -365,6 +390,11 @@ enum CustomDimensionsEnum {
fastReassemble, // cd48
nullSafeMigratedLibraries, // cd49
nullSafeTotalLibraries, // cd50
hotEventCompileTimeInMs, // cd51
hotEventFindInvalidatedTimeInMs, // cd52
hotEventScannedSourcesCount, // cd53
hotEventReassembleTimeInMs, // cd54
hotEventReloadVMTimeInMs, // cd55
}
String cdKey(CustomDimensionsEnum cd) => 'cd${cd.index + 1}';
......@@ -49,6 +49,11 @@ class HotEvent extends UsageEvent {
this.invalidatedSourcesCount,
this.transferTimeInMs,
this.overallTimeInMs,
this.compileTimeInMs,
this.findInvalidatedTimeInMs,
this.scannedSourcesCount,
this.reassembleTimeInMs,
this.reloadVMTimeInMs,
}) : super('hot', parameter, flutterUsage: globals.flutterUsage);
final String? reason;
......@@ -65,6 +70,11 @@ class HotEvent extends UsageEvent {
final int? invalidatedSourcesCount;
final int? transferTimeInMs;
final int? overallTimeInMs;
final int? compileTimeInMs;
final int? findInvalidatedTimeInMs;
final int? scannedSourcesCount;
final int? reassembleTimeInMs;
final int? reloadVMTimeInMs;
@override
void send() {
......@@ -83,6 +93,11 @@ class HotEvent extends UsageEvent {
hotEventTransferTimeInMs: transferTimeInMs,
hotEventOverallTimeInMs: overallTimeInMs,
fastReassemble: fastReassemble,
hotEventCompileTimeInMs: compileTimeInMs,
hotEventFindInvalidatedTimeInMs: findInvalidatedTimeInMs,
hotEventScannedSourcesCount: scannedSourcesCount,
hotEventReassembleTimeInMs: reassembleTimeInMs,
hotEventReloadVMTimeInMs: reloadVMTimeInMs,
);
flutterUsage.sendEvent(category, parameter, parameters: parameters);
}
......
......@@ -1460,7 +1460,7 @@ abstract class ResidentRunner extends ResidentHandlers {
}
class OperationResult {
OperationResult(this.code, this.message, { this.fatal = false });
OperationResult(this.code, this.message, { this.fatal = false, this.updateFSReport });
/// The result of the operation; a non-zero code indicates a failure.
final int code;
......@@ -1471,6 +1471,8 @@ class OperationResult {
/// Whether this error should cause the runner to exit.
final bool fatal;
final UpdateFSReport updateFSReport;
bool get isOk => code == 0;
static final OperationResult ok = OperationResult(0, '');
......
......@@ -82,7 +82,13 @@ class HotRunner extends ResidentRunner {
bool ipv6 = false,
bool machine = false,
ResidentDevtoolsHandlerFactory devtoolsHandler = createDefaultHandler,
}) : super(
StopwatchFactory stopwatchFactory = const StopwatchFactory(),
ReloadSourcesHelper reloadSourcesHelper = _defaultReloadSourcesHelper,
ReassembleHelper reassembleHelper = _defaultReassembleHelper,
}) : _stopwatchFactory = stopwatchFactory,
_reloadSourcesHelper = reloadSourcesHelper,
_reassembleHelper = reassembleHelper,
super(
devices,
target: target,
debuggingOptions: debuggingOptions,
......@@ -95,6 +101,10 @@ class HotRunner extends ResidentRunner {
devtoolsHandler: devtoolsHandler,
);
final StopwatchFactory _stopwatchFactory;
final ReloadSourcesHelper _reloadSourcesHelper;
final ReassembleHelper _reassembleHelper;
final bool benchmarkMode;
final File applicationBinary;
final bool hostIsIde;
......@@ -119,6 +129,31 @@ class HotRunner extends ResidentRunner {
DateTime firstBuildTime;
String _targetPlatform;
String _sdkName;
bool _emulator;
Future<void> _calculateTargetPlatform() async {
if (_targetPlatform != null) {
return;
}
if (flutterDevices.length == 1) {
final Device device = flutterDevices.first.device;
_targetPlatform = getNameForTargetPlatform(await device.targetPlatform);
_sdkName = await device.sdkNameAndVersion;
_emulator = await device.isLocalEmulator;
} else if (flutterDevices.length > 1) {
_targetPlatform = 'multiple';
_sdkName = 'multiple';
_emulator = false;
} else {
_targetPlatform = 'unknown';
_sdkName = 'unknown';
_emulator = false;
}
}
void _addBenchmarkData(String name, int value) {
benchmarkData[name] ??= <int>[];
benchmarkData[name].add(value);
......@@ -315,9 +350,15 @@ class HotRunner extends ResidentRunner {
bool enableDevTools = false,
String route,
}) async {
await _calculateTargetPlatform();
final Stopwatch appStartedTimer = Stopwatch()..start();
final File mainFile = globals.fs.file(mainPath);
firstBuildTime = DateTime.now();
Duration totalCompileTime = Duration.zero;
Duration totalLaunchAppTime = Duration.zero;
final List<Future<bool>> startupTasks = <Future<bool>>[];
for (final FlutterDevice device in flutterDevices) {
// Here we initialize the frontend_server concurrently with the platform
......@@ -326,6 +367,7 @@ class HotRunner extends ResidentRunner {
// subsequent invocation in devfs will not overwrite.
await runSourceGenerators();
if (device.generator != null) {
final Stopwatch compileTimer = Stopwatch()..start();
startupTasks.add(
device.generator.recompile(
mainFile.uri,
......@@ -342,14 +384,35 @@ class HotRunner extends ResidentRunner {
packageConfig: debuggingOptions.buildInfo.packageConfig,
projectRootPath: FlutterProject.current().directory.absolute.path,
fs: globals.fs,
).then((CompilerOutput output) => output?.errorCount == 0)
).then((CompilerOutput output) {
compileTimer.stop();
totalCompileTime += compileTimer.elapsed;
return output?.errorCount == 0;
})
);
}
final Stopwatch launchAppTimer = Stopwatch()..start();
startupTasks.add(device.runHot(
hotRunner: this,
route: route,
).then((int result) => result == 0));
).then((int result) {
totalLaunchAppTime += launchAppTimer.elapsed;
return result == 0;
}));
}
unawaited(appStartedCompleter?.future?.then((_) => HotEvent('reload-ready',
targetPlatform: _targetPlatform,
sdkName: _sdkName,
emulator: _emulator,
fullRestart: null,
fastReassemble: null,
overallTimeInMs: appStartedTimer.elapsed.inMilliseconds,
compileTimeInMs: totalCompileTime.inMilliseconds,
transferTimeInMs: totalLaunchAppTime.inMilliseconds,
)?.send()));
try {
final List<bool> results = await Future.wait(startupTasks);
if (!results.every((bool passed) => passed)) {
......@@ -392,6 +455,7 @@ class HotRunner extends ResidentRunner {
}
}
final Stopwatch findInvalidationTimer = _stopwatchFactory.createStopwatch('updateDevFS')..start();
final InvalidationResult invalidationResult = await projectFileInvalidator.findInvalidated(
lastCompiled: flutterDevices[0].devFS.lastCompiled,
urisToMonitor: flutterDevices[0].devFS.sources,
......@@ -400,6 +464,7 @@ class HotRunner extends ResidentRunner {
packageConfig: flutterDevices[0].devFS.lastPackageConfig
?? debuggingOptions.buildInfo.packageConfig,
);
findInvalidationTimer.stop();
final File entrypointFile = globals.fs.file(mainPath);
if (!entrypointFile.existsSync()) {
globals.printError(
......@@ -409,7 +474,11 @@ class HotRunner extends ResidentRunner {
'flutter is restarted or the file is restored.'
);
}
final UpdateFSReport results = UpdateFSReport(success: true);
final UpdateFSReport results = UpdateFSReport(
success: true,
scannedSourcesCount: flutterDevices[0].devFS.sources.length,
findInvalidatedDuration: findInvalidationTimer.elapsed,
);
for (final FlutterDevice device in flutterDevices) {
results.incorporateResults(await device.updateDevFS(
mainUri: entrypointFile.absolute.uri,
......@@ -429,12 +498,6 @@ class HotRunner extends ResidentRunner {
return results;
}
void _resetDevFSCompileTime() {
for (final FlutterDevice device in flutterDevices) {
device.devFS.resetLastCompiled();
}
}
void _resetDirtyAssets() {
for (final FlutterDevice device in flutterDevices) {
device.devFS.assetPathsToEvict.clear();
......@@ -574,7 +637,11 @@ class HotRunner extends ResidentRunner {
// Toggle the main dill name after successfully uploading.
_swap =! _swap;
return OperationResult.ok;
return OperationResult(
OperationResult.ok.code,
OperationResult.ok.message,
updateFSReport: updatedDevFS,
);
}
/// Returns [true] if the reload was successful.
......@@ -612,23 +679,7 @@ class HotRunner extends ResidentRunner {
if (flutterDevices.any((FlutterDevice device) => device.devFS == null)) {
return OperationResult(1, 'Device initialization has not completed.');
}
String targetPlatform;
String sdkName;
bool emulator;
if (flutterDevices.length == 1) {
final Device device = flutterDevices.first.device;
targetPlatform = getNameForTargetPlatform(await device.targetPlatform);
sdkName = await device.sdkNameAndVersion;
emulator = await device.isLocalEmulator;
} else if (flutterDevices.length > 1) {
targetPlatform = 'multiple';
sdkName = 'multiple';
emulator = false;
} else {
targetPlatform = 'unknown';
sdkName = 'unknown';
emulator = false;
}
await _calculateTargetPlatform();
final Stopwatch timer = Stopwatch()..start();
// Run source generation if needed.
......@@ -636,9 +687,9 @@ class HotRunner extends ResidentRunner {
if (fullRestart) {
final OperationResult result = await _fullRestartHelper(
targetPlatform: targetPlatform,
sdkName: sdkName,
emulator: emulator,
targetPlatform: _targetPlatform,
sdkName: _sdkName,
emulator: _emulator,
reason: reason,
silent: silent,
);
......@@ -649,9 +700,9 @@ class HotRunner extends ResidentRunner {
return result;
}
final OperationResult result = await _hotReloadHelper(
targetPlatform: targetPlatform,
sdkName: sdkName,
emulator: emulator,
targetPlatform: _targetPlatform,
sdkName: _sdkName,
emulator: _emulator,
reason: reason,
pause: pause,
);
......@@ -682,14 +733,32 @@ class HotRunner extends ResidentRunner {
);
}
OperationResult result;
String restartEvent = 'restart';
String restartEvent;
try {
final Stopwatch restartTimer = _stopwatchFactory.createStopwatch('fullRestartHelper')..start();
if (!(await hotRunnerConfig.setupHotRestart())) {
return OperationResult(1, 'setupHotRestart failed');
}
result = await _restartFromSources(reason: reason,);
result = await _restartFromSources(reason: reason);
restartTimer.stop();
if (!result.isOk) {
restartEvent = 'restart-failed';
} else {
HotEvent('restart',
targetPlatform: targetPlatform,
sdkName: sdkName,
emulator: emulator,
fullRestart: true,
reason: reason,
fastReassemble: null,
overallTimeInMs: restartTimer.elapsed.inMilliseconds,
syncedBytes: result.updateFSReport?.syncedBytes,
invalidatedSourcesCount: result.updateFSReport?.invalidatedSourcesCount,
transferTimeInMs: result.updateFSReport?.transferDuration?.inMilliseconds,
compileTimeInMs: result.updateFSReport?.compileDuration?.inMilliseconds,
findInvalidatedTimeInMs: result.updateFSReport?.findInvalidatedDuration?.inMilliseconds,
scannedSourcesCount: result.updateFSReport?.scannedSourcesCount,
).send();
}
} on vm_service.SentinelException catch (err, st) {
restartEvent = 'exception';
......@@ -698,6 +767,9 @@ class HotRunner extends ResidentRunner {
restartEvent = 'exception';
return OperationResult(1, 'hot restart failed to complete: $err\n$st', fatal: true);
} finally {
// The `restartEvent` variable will be null if restart succeeded. We will
// only handle the case when it failed here.
if (restartEvent != null) {
HotEvent(restartEvent,
targetPlatform: targetPlatform,
sdkName: sdkName,
......@@ -706,6 +778,7 @@ class HotRunner extends ResidentRunner {
reason: reason,
fastReassemble: null,
).send();
}
status?.cancel();
}
return result;
......@@ -772,24 +845,6 @@ class HotRunner extends ResidentRunner {
return result;
}
Future<List<Future<vm_service.ReloadReport>>> _reloadDeviceSources(
FlutterDevice device,
String entryPath, {
bool pause = false,
}) async {
final String deviceEntryUri = device.devFS.baseUri
.resolve(entryPath).toString();
final vm_service.VM vm = await device.vmService.service.getVM();
return <Future<vm_service.ReloadReport>>[
for (final vm_service.IsolateRef isolateRef in vm.isolates)
device.vmService.service.reloadSources(
isolateRef.id,
pause: pause,
rootLibUri: deviceEntryUri,
)
];
}
Future<OperationResult> _reloadSources({
String targetPlatform,
String sdkName,
......@@ -809,7 +864,7 @@ class HotRunner extends ResidentRunner {
}
}
final Stopwatch reloadTimer = Stopwatch()..start();
final Stopwatch reloadTimer = _stopwatchFactory.createStopwatch('reloadSources:reload')..start();
final Stopwatch devFSTimer = Stopwatch()..start();
final UpdateFSReport updatedDevFS = await _updateDevFS();
// Record time it took to synchronize to DevFS.
......@@ -819,9 +874,12 @@ class HotRunner extends ResidentRunner {
return OperationResult(1, 'DevFS synchronization failed');
}
String reloadMessage = 'Reloaded 0 libraries';
final Stopwatch reloadVMTimer = _stopwatchFactory.createStopwatch('reloadSources:vm')..start();
final Map<String, Object> firstReloadDetails = <String, Object>{};
if (updatedDevFS.invalidatedSourcesCount > 0) {
final OperationResult result = await _reloadSourcesHelper(
this,
flutterDevices,
pause,
firstReloadDetails,
targetPlatform,
......@@ -836,107 +894,25 @@ class HotRunner extends ResidentRunner {
} else {
_addBenchmarkData('hotReloadVMReloadMilliseconds', 0);
}
reloadVMTimer.stop();
final Stopwatch reassembleTimer = Stopwatch()..start();
await _evictDirtyAssets();
// Check if any isolates are paused and reassemble those that aren't.
final Map<FlutterView, FlutterVmService> reassembleViews = <FlutterView, FlutterVmService>{};
final List<Future<void>> reassembleFutures = <Future<void>>[];
String serviceEventKind;
int pausedIsolatesFound = 0;
bool failedReassemble = false;
for (final FlutterDevice device in flutterDevices) {
final List<FlutterView> views = viewCache[device];
for (final FlutterView view in views) {
// Check if the isolate is paused, and if so, don't reassemble. Ignore the
// PostPauseEvent event - the client requesting the pause will resume the app.
final vm_service.Isolate isolate = await device.vmService
.getIsolateOrNull(view.uiIsolate.id);
final vm_service.Event pauseEvent = isolate?.pauseEvent;
if (pauseEvent != null
&& isPauseEvent(pauseEvent.kind)
&& pauseEvent.kind != vm_service.EventKind.kPausePostRequest) {
pausedIsolatesFound += 1;
if (serviceEventKind == null) {
serviceEventKind = pauseEvent.kind;
} else if (serviceEventKind != pauseEvent.kind) {
serviceEventKind = ''; // many kinds
}
} else {
reassembleViews[view] = device.vmService;
// If the tool identified a change in a single widget, do a fast instead
// of a full reassemble.
Future<void> reassembleWork;
if (updatedDevFS.fastReassembleClassName != null) {
reassembleWork = device.vmService.flutterFastReassemble(
isolateId: view.uiIsolate.id,
className: updatedDevFS.fastReassembleClassName,
);
} else {
reassembleWork = device.vmService.flutterReassemble(
isolateId: view.uiIsolate.id,
final Stopwatch reassembleTimer = _stopwatchFactory.createStopwatch('reloadSources:reassemble')..start();
final ReassembleResult reassembleResult = await _reassembleHelper(
flutterDevices,
viewCache,
onSlow,
reloadMessage,
updatedDevFS.fastReassembleClassName,
);
}
reassembleFutures.add(reassembleWork.catchError((dynamic error) {
failedReassemble = true;
globals.printError('Reassembling ${view.uiIsolate.name} failed: $error');
}, test: (dynamic error) => error is Exception));
}
}
}
if (pausedIsolatesFound > 0) {
if (onSlow != null) {
onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.');
}
if (reassembleViews.isEmpty) {
globals.printTrace('Skipping reassemble because all isolates are paused.');
shouldReportReloadTime = reassembleResult.shouldReportReloadTime;
if (reassembleResult.reassembleViews.isEmpty) {
return OperationResult(OperationResult.ok.code, reloadMessage);
}
}
assert(reassembleViews.isNotEmpty);
globals.printTrace('Reassembling application');
final Future<void> reassembleFuture = Future.wait<void>(reassembleFutures);
await reassembleFuture.timeout(
const Duration(seconds: 2),
onTimeout: () async {
if (pausedIsolatesFound > 0) {
shouldReportReloadTime = false;
return; // probably no point waiting, they're probably deadlocked and we've already warned.
}
// Check if any isolate is newly paused.
globals.printTrace('This is taking a long time; will now check for paused isolates.');
int postReloadPausedIsolatesFound = 0;
String serviceEventKind;
for (final FlutterView view in reassembleViews.keys) {
final vm_service.Isolate isolate = await reassembleViews[view]
.getIsolateOrNull(view.uiIsolate.id);
if (isolate == null) {
continue;
}
if (isolate.pauseEvent != null && isPauseEvent(isolate.pauseEvent.kind)) {
postReloadPausedIsolatesFound += 1;
if (serviceEventKind == null) {
serviceEventKind = isolate.pauseEvent.kind;
} else if (serviceEventKind != isolate.pauseEvent.kind) {
serviceEventKind = ''; // many kinds
}
}
}
globals.printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).');
if (postReloadPausedIsolatesFound == 0) {
await reassembleFuture; // must just be taking a long time... keep waiting!
return;
}
shouldReportReloadTime = false;
if (onSlow != null) {
onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind)}.');
}
},
);
// Record time it took for Flutter to reassemble the application.
reassembleTimer.stop();
_addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds);
reloadTimer.stop();
......@@ -961,10 +937,15 @@ class HotRunner extends ResidentRunner {
syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int ?? 0,
syncedBytes: updatedDevFS.syncedBytes,
invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
transferTimeInMs: updatedDevFS.transferDuration.inMilliseconds,
fastReassemble: featureFlags.isSingleWidgetReloadEnabled
? updatedDevFS.fastReassembleClassName != null
: null,
compileTimeInMs: updatedDevFS.compileDuration.inMilliseconds,
findInvalidatedTimeInMs: updatedDevFS.findInvalidatedDuration.inMilliseconds,
scannedSourcesCount: updatedDevFS.scannedSourcesCount,
reassembleTimeInMs: reassembleTimer.elapsed.inMilliseconds,
reloadVMTimeInMs: reloadVMTimer.elapsed.inMilliseconds,
).send();
if (shouldReportReloadTime) {
......@@ -973,23 +954,124 @@ class HotRunner extends ResidentRunner {
_addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
}
// Only report timings if we reloaded a single view without any errors.
if ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime) {
if ((reassembleResult.reassembleViews.length == 1) && !reassembleResult.failedReassemble && shouldReportReloadTime) {
globals.flutterUsage.sendTiming('hot', 'reload', reloadDuration);
}
return OperationResult(
failedReassemble ? 1 : OperationResult.ok.code,
reassembleResult.failedReassemble ? 1 : OperationResult.ok.code,
reloadMessage,
);
}
Future<OperationResult> _reloadSourcesHelper(
@override
void printHelp({ @required bool details }) {
globals.printStatus('Flutter run key commands.');
commandHelp.r.print();
if (supportsRestart) {
commandHelp.R.print();
}
if (details) {
printHelpDetails();
commandHelp.hWithDetails.print();
} else {
commandHelp.hWithoutDetails.print();
}
if (_didAttach) {
commandHelp.d.print();
}
commandHelp.c.print();
commandHelp.q.print();
globals.printStatus('');
if (debuggingOptions.buildInfo.nullSafetyMode == NullSafetyMode.sound) {
globals.printStatus('💪 Running with sound null safety 💪', emphasis: true);
} else {
globals.printStatus(
'Running with unsound null safety',
emphasis: true,
);
globals.printStatus(
'For more information see https://dart.dev/null-safety/unsound-null-safety',
);
}
globals.printStatus('');
printDebuggerList();
}
Future<void> _evictDirtyAssets() async {
final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
for (final FlutterDevice device in flutterDevices) {
if (device.devFS.assetPathsToEvict.isEmpty) {
continue;
}
final List<FlutterView> views = await device.vmService.getFlutterViews();
if (views.first.uiIsolate == null) {
globals.printError('Application isolate not found for $device');
continue;
}
for (final String assetPath in device.devFS.assetPathsToEvict) {
futures.add(
device.vmService
.flutterEvictAsset(
assetPath,
isolateId: views.first.uiIsolate.id,
)
);
}
device.devFS.assetPathsToEvict.clear();
}
return Future.wait<Map<String, dynamic>>(futures);
}
@override
Future<void> cleanupAfterSignal() async {
await stopEchoingDeviceLog();
await hotRunnerConfig.runPreShutdownOperations();
if (_didAttach) {
appFinished();
} else {
await exitApp();
}
}
@override
Future<void> preExit() async {
await _cleanupDevFS();
await hotRunnerConfig.runPreShutdownOperations();
await super.preExit();
}
@override
Future<void> cleanupAtFinish() async {
for (final FlutterDevice flutterDevice in flutterDevices) {
await flutterDevice.device.dispose();
}
await _cleanupDevFS();
await residentDevtoolsHandler.shutdown();
await stopEchoingDeviceLog();
}
}
typedef ReloadSourcesHelper = Future<OperationResult> Function(
HotRunner hotRunner,
List<FlutterDevice> flutterDevices,
bool pause,
Map<String, dynamic> firstReloadDetails,
String targetPlatform,
String sdkName,
bool emulator,
String reason,
) async {
);
Future<OperationResult> _defaultReloadSourcesHelper(
HotRunner hotRunner,
List<FlutterDevice> flutterDevices,
bool pause,
Map<String, dynamic> firstReloadDetails,
String targetPlatform,
String sdkName,
bool emulator,
String reason,
) async {
final Stopwatch vmReloadTimer = Stopwatch()..start();
const String entryPath = 'main.dart.incremental.dill';
final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[];
......@@ -1008,7 +1090,7 @@ class HotRunner extends ResidentRunner {
// Don't print errors because they will be printed further down when
// `validateReloadReport` is called again.
await device.updateReloadStatus(
validateReloadReport(firstReport, printErrors: false),
HotRunner.validateReloadReport(firstReport, printErrors: false),
);
return DeviceReloadReport(device, reports);
},
......@@ -1016,7 +1098,7 @@ class HotRunner extends ResidentRunner {
}
final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
final vm_service.ReloadReport reloadReport = reports.first.reports[0];
if (!validateReloadReport(reloadReport)) {
if (!HotRunner.validateReloadReport(reloadReport)) {
// Reload failed.
HotEvent('reload-reject',
targetPlatform: targetPlatform,
......@@ -1028,7 +1110,7 @@ class HotRunner extends ResidentRunner {
).send();
// Reset devFS lastCompileTime to ensure the file will still be marked
// as dirty on subsequent reloads.
_resetDevFSCompileTime();
_resetDevFSCompileTime(flutterDevices);
final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport);
return OperationResult(1, 'Reload rejected: ${contents.notices.join("\n")}');
}
......@@ -1041,11 +1123,158 @@ class HotRunner extends ResidentRunner {
globals.printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
// reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
// Record time it took for the VM to reload the sources.
_addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
hotRunner._addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
return OperationResult(0, 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries');
}
Future<List<Future<vm_service.ReloadReport>>> _reloadDeviceSources(
FlutterDevice device,
String entryPath, {
bool pause = false,
}) async {
final String deviceEntryUri = device.devFS.baseUri
.resolve(entryPath).toString();
final vm_service.VM vm = await device.vmService.service.getVM();
return <Future<vm_service.ReloadReport>>[
for (final vm_service.IsolateRef isolateRef in vm.isolates)
device.vmService.service.reloadSources(
isolateRef.id,
pause: pause,
rootLibUri: deviceEntryUri,
)
];
}
void _resetDevFSCompileTime(List<FlutterDevice> flutterDevices) {
for (final FlutterDevice device in flutterDevices) {
device.devFS.resetLastCompiled();
}
}
@visibleForTesting
class ReassembleResult {
ReassembleResult(this.reassembleViews, this.failedReassemble, this.shouldReportReloadTime);
final Map<FlutterView, FlutterVmService> reassembleViews;
final bool failedReassemble;
final bool shouldReportReloadTime;
}
String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) {
typedef ReassembleHelper = Future<ReassembleResult> Function(
List<FlutterDevice> flutterDevices,
Map<FlutterDevice, List<FlutterView>> viewCache,
void Function(String message) onSlow,
String reloadMessage,
String fastReassembleClassName,
);
Future<ReassembleResult> _defaultReassembleHelper(
List<FlutterDevice> flutterDevices,
Map<FlutterDevice, List<FlutterView>> viewCache,
void Function(String message) onSlow,
String reloadMessage,
String fastReassembleClassName,
) async {
// Check if any isolates are paused and reassemble those that aren't.
final Map<FlutterView, FlutterVmService> reassembleViews = <FlutterView, FlutterVmService>{};
final List<Future<void>> reassembleFutures = <Future<void>>[];
String serviceEventKind;
int pausedIsolatesFound = 0;
bool failedReassemble = false;
bool shouldReportReloadTime = true;
for (final FlutterDevice device in flutterDevices) {
final List<FlutterView> views = viewCache[device];
for (final FlutterView view in views) {
// Check if the isolate is paused, and if so, don't reassemble. Ignore the
// PostPauseEvent event - the client requesting the pause will resume the app.
final vm_service.Isolate isolate = await device.vmService
.getIsolateOrNull(view.uiIsolate.id);
final vm_service.Event pauseEvent = isolate?.pauseEvent;
if (pauseEvent != null
&& isPauseEvent(pauseEvent.kind)
&& pauseEvent.kind != vm_service.EventKind.kPausePostRequest) {
pausedIsolatesFound += 1;
if (serviceEventKind == null) {
serviceEventKind = pauseEvent.kind;
} else if (serviceEventKind != pauseEvent.kind) {
serviceEventKind = ''; // many kinds
}
} else {
reassembleViews[view] = device.vmService;
// If the tool identified a change in a single widget, do a fast instead
// of a full reassemble.
Future<void> reassembleWork;
if (fastReassembleClassName != null) {
reassembleWork = device.vmService.flutterFastReassemble(
isolateId: view.uiIsolate.id,
className: fastReassembleClassName,
);
} else {
reassembleWork = device.vmService.flutterReassemble(
isolateId: view.uiIsolate.id,
);
}
reassembleFutures.add(reassembleWork.catchError((dynamic error) {
failedReassemble = true;
globals.printError('Reassembling ${view.uiIsolate.name} failed: $error');
}, test: (dynamic error) => error is Exception));
}
}
}
if (pausedIsolatesFound > 0) {
if (onSlow != null) {
onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.');
}
if (reassembleViews.isEmpty) {
globals.printTrace('Skipping reassemble because all isolates are paused.');
return ReassembleResult(reassembleViews, failedReassemble, shouldReportReloadTime);
}
}
assert(reassembleViews.isNotEmpty);
globals.printTrace('Reassembling application');
final Future<void> reassembleFuture = Future.wait<void>(reassembleFutures);
await reassembleFuture.timeout(
const Duration(seconds: 2),
onTimeout: () async {
if (pausedIsolatesFound > 0) {
shouldReportReloadTime = false;
return; // probably no point waiting, they're probably deadlocked and we've already warned.
}
// Check if any isolate is newly paused.
globals.printTrace('This is taking a long time; will now check for paused isolates.');
int postReloadPausedIsolatesFound = 0;
String serviceEventKind;
for (final FlutterView view in reassembleViews.keys) {
final vm_service.Isolate isolate = await reassembleViews[view]
.getIsolateOrNull(view.uiIsolate.id);
if (isolate == null) {
continue;
}
if (isolate.pauseEvent != null && isPauseEvent(isolate.pauseEvent.kind)) {
postReloadPausedIsolatesFound += 1;
if (serviceEventKind == null) {
serviceEventKind = isolate.pauseEvent.kind;
} else if (serviceEventKind != isolate.pauseEvent.kind) {
serviceEventKind = ''; // many kinds
}
}
}
globals.printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).');
if (postReloadPausedIsolatesFound == 0) {
await reassembleFuture; // must just be taking a long time... keep waiting!
return;
}
shouldReportReloadTime = false;
if (onSlow != null) {
onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind)}.');
}
},
);
return ReassembleResult(reassembleViews, failedReassemble, shouldReportReloadTime);
}
String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) {
assert(pausedIsolatesFound > 0);
final StringBuffer message = StringBuffer();
bool plural;
......@@ -1083,94 +1312,6 @@ class HotRunner extends ResidentRunner {
message.write('paused');
}
return message.toString();
}
@override
void printHelp({ @required bool details }) {
globals.printStatus('Flutter run key commands.');
commandHelp.r.print();
if (supportsRestart) {
commandHelp.R.print();
}
if (details) {
printHelpDetails();
commandHelp.hWithDetails.print();
} else {
commandHelp.hWithoutDetails.print();
}
if (_didAttach) {
commandHelp.d.print();
}
commandHelp.c.print();
commandHelp.q.print();
globals.printStatus('');
if (debuggingOptions.buildInfo.nullSafetyMode == NullSafetyMode.sound) {
globals.printStatus('💪 Running with sound null safety 💪', emphasis: true);
} else {
globals.printStatus(
'Running with unsound null safety',
emphasis: true,
);
globals.printStatus(
'For more information see https://dart.dev/null-safety/unsound-null-safety',
);
}
globals.printStatus('');
printDebuggerList();
}
Future<void> _evictDirtyAssets() async {
final List<Future<Map<String, dynamic>>> futures = <Future<Map<String, dynamic>>>[];
for (final FlutterDevice device in flutterDevices) {
if (device.devFS.assetPathsToEvict.isEmpty) {
continue;
}
final List<FlutterView> views = await device.vmService.getFlutterViews();
if (views.first.uiIsolate == null) {
globals.printError('Application isolate not found for $device');
continue;
}
for (final String assetPath in device.devFS.assetPathsToEvict) {
futures.add(
device.vmService
.flutterEvictAsset(
assetPath,
isolateId: views.first.uiIsolate.id,
)
);
}
device.devFS.assetPathsToEvict.clear();
}
return Future.wait<Map<String, dynamic>>(futures);
}
@override
Future<void> cleanupAfterSignal() async {
await stopEchoingDeviceLog();
await hotRunnerConfig.runPreShutdownOperations();
if (_didAttach) {
appFinished();
} else {
await exitApp();
}
}
@override
Future<void> preExit() async {
await _cleanupDevFS();
await hotRunnerConfig.runPreShutdownOperations();
await super.preExit();
}
@override
Future<void> cleanupAtFinish() async {
for (final FlutterDevice flutterDevice in flutterDevices) {
await flutterDevice.device.dispose();
}
await _cleanupDevFS();
await residentDevtoolsHandler.shutdown();
await stopEchoingDeviceLog();
}
}
/// The result of an invalidation check from [ProjectFileInvalidator].
......@@ -1225,7 +1366,7 @@ class ProjectFileInvalidator {
assert(urisToMonitor.isEmpty);
return InvalidationResult(
packageConfig: packageConfig,
uris: <Uri>[]
uris: <Uri>[],
);
}
......
......@@ -244,7 +244,7 @@ void main() {
);
final VerboseLogger verboseLogger = VerboseLogger(
mockLogger,
stopwatchFactory: FakeStopwatchFactory(fakeStopWatch),
stopwatchFactory: FakeStopwatchFactory(stopwatch: fakeStopWatch),
);
verboseLogger.printStatus('Hey Hey Hey Hey');
......@@ -266,7 +266,7 @@ void main() {
outputPreferences: OutputPreferences.test(showColor: true),
);
final VerboseLogger verboseLogger = VerboseLogger(
mockLogger, stopwatchFactory: FakeStopwatchFactory(fakeStopWatch),
mockLogger, stopwatchFactory: FakeStopwatchFactory(stopwatch: fakeStopWatch),
);
verboseLogger.printStatus('Hey Hey Hey Hey');
......@@ -377,7 +377,7 @@ void main() {
mockStopwatch = FakeStopwatch();
mockStdio = FakeStdio();
called = 0;
stopwatchFactory = FakeStopwatchFactory(mockStopwatch);
stopwatchFactory = FakeStopwatchFactory(stopwatch: mockStopwatch);
});
List<String> outputStdout() => mockStdio.writtenToStdout.join('').split('\n');
......@@ -938,7 +938,7 @@ void main() {
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: false),
stopwatchFactory: FakeStopwatchFactory(fakeStopwatch),
stopwatchFactory: FakeStopwatchFactory(stopwatch: fakeStopwatch),
);
final Status status = logger.startProgress(
'Hello',
......@@ -1062,53 +1062,6 @@ void main() {
});
}
class FakeStopwatch implements Stopwatch {
@override
bool get isRunning => _isRunning;
bool _isRunning = false;
@override
void start() => _isRunning = true;
@override
void stop() => _isRunning = false;
@override
Duration elapsed = Duration.zero;
@override
int get elapsedMicroseconds => elapsed.inMicroseconds;
@override
int get elapsedMilliseconds => elapsed.inMilliseconds;
@override
int get elapsedTicks => elapsed.inMilliseconds;
@override
int get frequency => 1000;
@override
void reset() {
_isRunning = false;
elapsed = Duration.zero;
}
@override
String toString() => '$runtimeType $elapsed $isRunning';
}
class FakeStopwatchFactory implements StopwatchFactory {
FakeStopwatchFactory([this.stopwatch]);
Stopwatch stopwatch;
@override
Stopwatch createStopwatch() {
return stopwatch ?? FakeStopwatch();
}
}
/// A fake [Logger] that throws the [Invocation] for any method call.
class FakeLogger implements Logger {
@override
......
......@@ -425,6 +425,51 @@ void main() {
Uri.parse('goodbye'): DevFSFileContent(file),
}, Uri.parse('/foo/bar/devfs/')), throwsA(isA<DevFSException>()));
});
testWithoutContext('DevFS correctly records the elapsed time', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
// final FakeDevFSWriter writer = FakeDevFSWriter();
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[createDevFSRequest],
httpAddress: Uri.parse('http://localhost'),
);
final DevFS devFS = DevFS(
fakeVmServiceHost.vmService,
'test',
fileSystem.currentDirectory,
fileSystem: fileSystem,
logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(),
httpClient: FakeHttpClient.any(),
stopwatchFactory: FakeStopwatchFactory(stopwatches: <String, Stopwatch>{
'compile': FakeStopwatch()..elapsed = const Duration(seconds: 3),
'transfer': FakeStopwatch()..elapsed = const Duration(seconds: 5),
}),
);
await devFS.create();
final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
residentCompiler.onRecompile = (Uri mainUri, List<Uri> invalidatedFiles) async {
fileSystem.file('lib/foo.txt.dill').createSync(recursive: true);
return const CompilerOutput('lib/foo.txt.dill', 0, <Uri>[]);
};
final UpdateFSReport report = await devFS.update(
mainUri: Uri.parse('lib/main.dart'),
generator: residentCompiler,
dillOutputPath: 'lib/foo.dill',
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
invalidatedFiles: <Uri>[],
packageConfig: PackageConfig.empty,
);
expect(report.success, true);
expect(report.compileDuration, const Duration(seconds: 3));
expect(report.transferDuration, const Duration(seconds: 5));
});
}
class FakeResidentCompiler extends Fake implements ResidentCompiler {
......
......@@ -7,6 +7,7 @@
import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/asset.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/platform.dart';
......@@ -14,17 +15,20 @@ import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/resident_devtools_handler.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import '../src/common.dart';
import '../src/context.dart';
import '../src/fake_vm_services.dart';
import '../src/fakes.dart';
final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate(
id: '1',
......@@ -146,9 +150,11 @@ void main() {
group('hotRestart', () {
final FakeResidentCompiler residentCompiler = FakeResidentCompiler();
FileSystem fileSystem;
TestUsage testUsage;
setUp(() {
fileSystem = MemoryFileSystem.test();
testUsage = TestUsage();
});
testUsingContext('setup function fails', () async {
......@@ -228,6 +234,158 @@ void main() {
ProcessManager: () => FakeProcessManager.any(),
});
});
testUsingContext('correctly tracks time spent for analytics for hot restart', () async {
final FakeDevice device = FakeDevice();
final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device);
final List<FlutterDevice> devices = <FlutterDevice>[
fakeFlutterDevice,
];
fakeFlutterDevice.updateDevFSReport = UpdateFSReport(
success: true,
invalidatedSourcesCount: 2,
syncedBytes: 4,
scannedSourcesCount: 8,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
final FakeStopwatchFactory fakeStopwatchFactory = FakeStopwatchFactory(
stopwatches: <String, Stopwatch>{
'fullRestartHelper': FakeStopwatch()..elapsed = const Duration(seconds: 64),
'updateDevFS': FakeStopwatch()..elapsed = const Duration(seconds: 128),
},
);
(fakeFlutterDevice.devFS as FakeDevFs).baseUri = Uri.parse('file:///base_uri');
final OperationResult result = await HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
stopwatchFactory: fakeStopwatchFactory,
).restart(fullRestart: true);
expect(result.isOk, true);
expect(testUsage.events, <TestUsageEvent>[
const TestUsageEvent('hot', 'restart', parameters: CustomDimensions(
hotEventTargetPlatform: 'flutter-tester',
hotEventSdkName: 'Tester',
hotEventEmulator: false,
hotEventFullRestart: true,
hotEventOverallTimeInMs: 64000,
hotEventSyncedBytes: 4,
hotEventInvalidatedSourcesCount: 2,
hotEventTransferTimeInMs: 32000,
hotEventCompileTimeInMs: 16000,
hotEventFindInvalidatedTimeInMs: 128000,
hotEventScannedSourcesCount: 8,
)),
]);
}, overrides: <Type, Generator>{
HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(operatingSystem: 'linux'),
ProcessManager: () => FakeProcessManager.any(),
Usage: () => testUsage,
});
testUsingContext('correctly tracks time spent for analytics for hot reload', () async {
final FakeDevice device = FakeDevice();
final FakeFlutterDevice fakeFlutterDevice = FakeFlutterDevice(device);
final List<FlutterDevice> devices = <FlutterDevice>[
fakeFlutterDevice,
];
fakeFlutterDevice.updateDevFSReport = UpdateFSReport(
success: true,
invalidatedSourcesCount: 6,
syncedBytes: 8,
scannedSourcesCount: 16,
compileDuration: const Duration(seconds: 16),
transferDuration: const Duration(seconds: 32),
);
final FakeStopwatchFactory fakeStopwatchFactory = FakeStopwatchFactory(
stopwatches: <String, Stopwatch>{
'updateDevFS': FakeStopwatch()..elapsed = const Duration(seconds: 64),
'reloadSources:reload': FakeStopwatch()..elapsed = const Duration(seconds: 128),
'reloadSources:reassemble': FakeStopwatch()..elapsed = const Duration(seconds: 256),
'reloadSources:vm': FakeStopwatch()..elapsed = const Duration(seconds: 512),
},
);
(fakeFlutterDevice.devFS as FakeDevFs).baseUri = Uri.parse('file:///base_uri');
final OperationResult result = await HotRunner(
devices,
debuggingOptions: DebuggingOptions.disabled(BuildInfo.debug),
target: 'main.dart',
devtoolsHandler: createNoOpHandler,
stopwatchFactory: fakeStopwatchFactory,
reloadSourcesHelper: (
HotRunner hotRunner,
List<FlutterDevice> flutterDevices,
bool pause,
Map<String, dynamic> firstReloadDetails,
String targetPlatform,
String sdkName,
bool emulator,
String reason,
) async {
firstReloadDetails['finalLibraryCount'] = 2;
firstReloadDetails['receivedLibraryCount'] = 3;
firstReloadDetails['receivedClassesCount'] = 4;
firstReloadDetails['receivedProceduresCount'] = 5;
return OperationResult.ok;
},
reassembleHelper: (
List<FlutterDevice> flutterDevices,
Map<FlutterDevice, List<FlutterView>> viewCache,
void Function(String message) onSlow,
String reloadMessage,
String fastReassembleClassName,
) async => ReassembleResult(
<FlutterView, FlutterVmService>{null: null},
false,
true,
),
).restart(fullRestart: false);
expect(result.isOk, true);
expect(testUsage.events, <TestUsageEvent>[
const TestUsageEvent('hot', 'reload', parameters: CustomDimensions(
hotEventFinalLibraryCount: 2,
hotEventSyncedLibraryCount: 3,
hotEventSyncedClassesCount: 4,
hotEventSyncedProceduresCount: 5,
hotEventSyncedBytes: 8,
hotEventInvalidatedSourcesCount: 6,
hotEventTransferTimeInMs: 32000,
hotEventOverallTimeInMs: 128000,
hotEventTargetPlatform: 'flutter-tester',
hotEventSdkName: 'Tester',
hotEventEmulator: false,
hotEventFullRestart: false,
fastReassemble: false,
hotEventCompileTimeInMs: 16000,
hotEventFindInvalidatedTimeInMs: 64000,
hotEventScannedSourcesCount: 16,
hotEventReassembleTimeInMs: 256000,
hotEventReloadVMTimeInMs: 512000,
)),
]);
}, overrides: <Type, Generator>{
HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
Artifacts: () => Artifacts.test(),
FileSystem: () => fileSystem,
Platform: () => FakePlatform(operatingSystem: 'linux'),
ProcessManager: () => FakeProcessManager.any(),
Usage: () => testUsage,
});
});
group('hot attach', () {
......@@ -299,6 +457,21 @@ void main() {
class FakeDevFs extends Fake implements DevFS {
@override
Future<void> destroy() async { }
@override
List<Uri> sources = <Uri>[];
@override
DateTime lastCompiled;
@override
PackageConfig lastPackageConfig;
@override
Set<String> assetPathsToEvict = <String>{};
@override
Uri baseUri;
}
class FakeDevice extends Fake implements Device {
......@@ -325,6 +498,9 @@ class FakeDevice extends Fake implements Device {
@override
Future<bool> get isLocalEmulator async => false;
@override
String get name => 'Fake Device';
@override
Future<bool> stopApp(
covariant ApplicationPackage app, {
......@@ -343,6 +519,7 @@ class FakeFlutterDevice extends Fake implements FlutterDevice {
FakeFlutterDevice(this.device);
bool stoppedEchoingDeviceLog = false;
UpdateFSReport updateDevFSReport;
@override
final FakeDevice device;
......@@ -354,6 +531,28 @@ class FakeFlutterDevice extends Fake implements FlutterDevice {
@override
DevFS devFS = FakeDevFs();
@override
FlutterVmService get vmService => FakeFlutterVmService();
@override
ResidentCompiler generator;
@override
Future<UpdateFSReport> updateDevFS({
Uri mainUri,
String target,
AssetBundle bundle,
DateTime firstBuildTime,
bool bundleFirstUpload = false,
bool bundleDirty = false,
bool fullRestart = false,
String projectRootPath,
String pathToReload,
@required String dillOutputPath,
@required List<Uri> invalidatedFiles,
@required PackageConfig packageConfig,
}) async => updateDevFSReport;
}
class TestFlutterDevice extends FlutterDevice {
......@@ -405,3 +604,23 @@ class FakeResidentCompiler extends Fake implements ResidentCompiler {
@override
void accept() {}
}
class FakeFlutterVmService extends Fake implements FlutterVmService {
@override
vm_service.VmService get service => FakeVmService();
@override
Future<List<FlutterView>> getFlutterViews({bool returnEarly = false, Duration delay = const Duration(milliseconds: 50)}) async {
return <FlutterView>[];
}
}
class FakeVmService extends Fake implements vm_service.VmService {
@override
Future<vm_service.VM> getVM() async => FakeVm();
}
class FakeVm extends Fake implements vm_service.VM {
@override
List<vm_service.IsolateRef> get isolates => <vm_service.IsolateRef>[];
}
......@@ -535,3 +535,56 @@ class FakeOperatingSystemUtils extends Fake implements OperatingSystemUtils {
@override
Future<int> findFreePort({bool ipv6 = false}) async => 12345;
}
class FakeStopwatch implements Stopwatch {
@override
bool get isRunning => _isRunning;
bool _isRunning = false;
@override
void start() => _isRunning = true;
@override
void stop() => _isRunning = false;
@override
Duration elapsed = Duration.zero;
@override
int get elapsedMicroseconds => elapsed.inMicroseconds;
@override
int get elapsedMilliseconds => elapsed.inMilliseconds;
@override
int get elapsedTicks => elapsed.inMilliseconds;
@override
int get frequency => 1000;
@override
void reset() {
_isRunning = false;
elapsed = Duration.zero;
}
@override
String toString() => '$runtimeType $elapsed $isRunning';
}
class FakeStopwatchFactory implements StopwatchFactory {
FakeStopwatchFactory({
Stopwatch? stopwatch,
Map<String, Stopwatch>? stopwatches
}) : stopwatches = <String, Stopwatch>{
if (stopwatches != null) ...stopwatches,
if (stopwatch != null) '': stopwatch,
};
Map<String, Stopwatch> stopwatches;
@override
Stopwatch createStopwatch([String name = '']) {
return stopwatches[name] ?? FakeStopwatch();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment