Unverified Commit 6cec03c7 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] do not reload sources if no sources changed (#69450)

If no source files were changed by a hot reload, do not send the reload sources RPC. This slightly speeds up the no-op reloads and gives users a faster way to force a re-render.
parent b73cae17
...@@ -818,64 +818,23 @@ class HotRunner extends ResidentRunner { ...@@ -818,64 +818,23 @@ class HotRunner extends ResidentRunner {
if (!updatedDevFS.success) { if (!updatedDevFS.success) {
return OperationResult(1, 'DevFS synchronization failed'); return OperationResult(1, 'DevFS synchronization failed');
} }
String reloadMessage; String reloadMessage = 'Reloaded 0 libraries';
final Stopwatch vmReloadTimer = Stopwatch()..start(); final Map<String, Object> firstReloadDetails = <String, Object>{};
Map<String, Object> firstReloadDetails; if (updatedDevFS.invalidatedSourcesCount > 0) {
const String entryPath = 'main.dart.incremental.dill'; final OperationResult result = await _reloadSourcesHelper(
final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[]; pause,
firstReloadDetails,
for (final FlutterDevice device in flutterDevices) { targetPlatform,
final List<Future<vm_service.ReloadReport>> reportFutures = await _reloadDeviceSources( sdkName,
device, emulator,
entryPath, reason,
pause: pause,
); );
allReportsFutures.add(Future.wait(reportFutures).then( if (result.code != 0) {
(List<vm_service.ReloadReport> reports) async { return result;
// TODO(aam): Investigate why we are validating only first reload report,
// which seems to be current behavior
final vm_service.ReloadReport firstReport = reports.first;
// Don't print errors because they will be printed further down when
// `validateReloadReport` is called again.
await device.updateReloadStatus(
validateReloadReport(firstReport, printErrors: false),
);
return DeviceReloadReport(device, reports);
},
));
}
final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
for (final DeviceReloadReport report in reports) {
final vm_service.ReloadReport reloadReport = report.reports[0];
if (!validateReloadReport(reloadReport)) {
// Reload failed.
HotEvent('reload-reject',
targetPlatform: targetPlatform,
sdkName: sdkName,
emulator: emulator,
fullRestart: false,
reason: reason,
nullSafety: usageNullSafety,
fastReassemble: null,
).send();
// Reset devFS lastCompileTime to ensure the file will still be marked
// as dirty on subsequent reloads.
_resetDevFSCompileTime();
final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport);
return OperationResult(1, 'Reload rejected: ${contents.notices.join("\n")}');
} }
// Collect stats only from the first device. If/when run -d all is reloadMessage = result.message;
// refactored, we'll probably need to send one hot reload/restart event
// per device to analytics.
firstReloadDetails ??= castStringKeyedMap(reloadReport.json['details']);
final int loadedLibraryCount = reloadReport.json['details']['loadedLibraryCount'] as int;
final int finalLibraryCount = reloadReport.json['details']['finalLibraryCount'] as int;
globals.printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
} }
// Record time it took for the VM to reload the sources.
_addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
final Stopwatch reassembleTimer = Stopwatch()..start(); final Stopwatch reassembleTimer = Stopwatch()..start();
await _evictDirtyAssets(); await _evictDirtyAssets();
...@@ -994,10 +953,10 @@ class HotRunner extends ResidentRunner { ...@@ -994,10 +953,10 @@ class HotRunner extends ResidentRunner {
fullRestart: false, fullRestart: false,
reason: reason, reason: reason,
overallTimeInMs: reloadInMs, overallTimeInMs: reloadInMs,
finalLibraryCount: firstReloadDetails['finalLibraryCount'] as int, finalLibraryCount: firstReloadDetails['finalLibraryCount'] as int ?? 0,
syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int, syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int ?? 0,
syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int, syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int ?? 0,
syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int, syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int ?? 0,
syncedBytes: updatedDevFS.syncedBytes, syncedBytes: updatedDevFS.syncedBytes,
invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount, invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
transferTimeInMs: devFSTimer.elapsed.inMilliseconds, transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
...@@ -1022,6 +981,70 @@ class HotRunner extends ResidentRunner { ...@@ -1022,6 +981,70 @@ class HotRunner extends ResidentRunner {
); );
} }
Future<OperationResult> _reloadSourcesHelper(
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>>[];
for (final FlutterDevice device in flutterDevices) {
final List<Future<vm_service.ReloadReport>> reportFutures = await _reloadDeviceSources(
device,
entryPath,
pause: pause,
);
allReportsFutures.add(Future.wait(reportFutures).then(
(List<vm_service.ReloadReport> reports) async {
// TODO(aam): Investigate why we are validating only first reload report,
// which seems to be current behavior
final vm_service.ReloadReport firstReport = reports.first;
// Don't print errors because they will be printed further down when
// `validateReloadReport` is called again.
await device.updateReloadStatus(
validateReloadReport(firstReport, printErrors: false),
);
return DeviceReloadReport(device, reports);
},
));
}
final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
final vm_service.ReloadReport reloadReport = reports.first.reports[0];
if (!validateReloadReport(reloadReport)) {
// Reload failed.
HotEvent('reload-reject',
targetPlatform: targetPlatform,
sdkName: sdkName,
emulator: emulator,
fullRestart: false,
reason: reason,
nullSafety: usageNullSafety,
fastReassemble: null,
).send();
// Reset devFS lastCompileTime to ensure the file will still be marked
// as dirty on subsequent reloads.
_resetDevFSCompileTime();
final ReloadReportContents contents = ReloadReportContents.fromReloadReport(reloadReport);
return OperationResult(1, 'Reload rejected: ${contents.notices.join("\n")}');
}
// Collect stats only from the first device. If/when run -d all is
// refactored, we'll probably need to send one hot reload/restart event
// per device to analytics.
firstReloadDetails.addAll(castStringKeyedMap(reloadReport.json['details']));
final int loadedLibraryCount = reloadReport.json['details']['loadedLibraryCount'] as int;
final int finalLibraryCount = reloadReport.json['details']['finalLibraryCount'] as int;
globals.printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
// reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
// Record time it took for the VM to reload the sources.
_addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
return OperationResult(0, 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries');
}
String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) { String _describePausedIsolates(int pausedIsolatesFound, String serviceEventKind) {
assert(pausedIsolatesFound > 0); assert(pausedIsolatesFound > 0);
final StringBuffer message = StringBuffer(); final StringBuffer message = StringBuffer();
......
...@@ -652,6 +652,72 @@ void main() { ...@@ -652,6 +652,72 @@ void main() {
Usage: () => MockUsage(), Usage: () => MockUsage(),
})); }));
testUsingContext('ResidentRunner does not reload sources if no sources changed', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews,
listViews,
setAssetBundlePath,
listViews,
FakeVmServiceRequest(
method: 'getIsolate',
args: <String, Object>{
'isolateId': '1',
},
jsonResponse: fakeUnpausedIsolate.toJson(),
),
FakeVmServiceRequest(
method: 'ext.flutter.reassemble',
args: <String, Object>{
'isolateId': fakeUnpausedIsolate.id,
},
),
]);
residentRunner = HotRunner(
<FlutterDevice>[
mockFlutterDevice,
],
stayResident: false,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
);
when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async {
return 'Example';
});
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
return TargetPlatform.android_arm;
});
when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async {
return false;
});
final Completer<DebugConnectionInfo> onConnectionInfo = Completer<DebugConnectionInfo>.sync();
final Completer<void> onAppStart = Completer<void>.sync();
unawaited(residentRunner.attach(
appStartedCompleter: onAppStart,
connectionInfoCompleter: onConnectionInfo,
));
await onAppStart.future;
when(mockFlutterDevice.updateDevFS(
mainUri: anyNamed('mainUri'),
target: anyNamed('target'),
bundle: anyNamed('bundle'),
firstBuildTime: anyNamed('firstBuildTime'),
bundleFirstUpload: anyNamed('bundleFirstUpload'),
bundleDirty: anyNamed('bundleDirty'),
fullRestart: anyNamed('fullRestart'),
projectRootPath: anyNamed('projectRootPath'),
pathToReload: anyNamed('pathToReload'),
invalidatedFiles: anyNamed('invalidatedFiles'),
dillOutputPath: anyNamed('dillOutputPath'),
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation _) async {
return UpdateFSReport(success: true, invalidatedSourcesCount: 0);
});
final OperationResult result = await residentRunner.restart(fullRestart: false);
expect(result.code, 0);
expect(fakeVmServiceHost.hasRemainingExpectations, false);
}));
testUsingContext('ResidentRunner reports error with missing entrypoint file', () => testbed.run(() async { testUsingContext('ResidentRunner reports error with missing entrypoint file', () => testbed.run(() async {
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
listViews, listViews,
......
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