Skip to content

Commit c1caa24

Browse files
authored
Optimize file transfer when using proxied devices. (#139968)
List of changes: 1. Optimizations in FileTransfer. a. Use `stream.forEach` instead of `await for`. b. Type cast `List<int>` to `Uint8List` instead of using `Uint8List.fromList` results in (presumably) fewer copy and faster execution. c. Iterate through `Uint8List` with regular for loop instead of for-in loop. 2. Precache the block hashes of a file, and reuse it on subsequent runs.
1 parent a9c40a2 commit c1caa24

File tree

5 files changed

+257
-30
lines changed

5 files changed

+257
-30
lines changed

packages/flutter_tools/lib/src/commands/daemon.dart

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,15 @@ class Daemon {
156156
this.connection, {
157157
this.notifyingLogger,
158158
this.logToStdout = false,
159+
FileTransfer fileTransfer = const FileTransfer(),
159160
}) {
160161
// Set up domains.
161162
registerDomain(daemonDomain = DaemonDomain(this));
162163
registerDomain(appDomain = AppDomain(this));
163164
registerDomain(deviceDomain = DeviceDomain(this));
164165
registerDomain(emulatorDomain = EmulatorDomain(this));
165166
registerDomain(devToolsDomain = DevToolsDomain(this));
166-
registerDomain(proxyDomain = ProxyDomain(this));
167+
registerDomain(proxyDomain = ProxyDomain(this, fileTransfer: fileTransfer));
167168

168169
// Start listening.
169170
_commandSubscription = connection.incomingCommands.listen(
@@ -1412,7 +1413,10 @@ class EmulatorDomain extends Domain {
14121413
}
14131414

14141415
class ProxyDomain extends Domain {
1415-
ProxyDomain(Daemon daemon) : super(daemon, 'proxy') {
1416+
ProxyDomain(Daemon daemon, {
1417+
required FileTransfer fileTransfer,
1418+
}) : _fileTransfer = fileTransfer,
1419+
super(daemon, 'proxy') {
14161420
registerHandlerWithBinary('writeTempFile', writeTempFile);
14171421
registerHandler('calculateFileHashes', calculateFileHashes);
14181422
registerHandlerWithBinary('updateFile', updateFile);
@@ -1421,6 +1425,8 @@ class ProxyDomain extends Domain {
14211425
registerHandlerWithBinary('write', write);
14221426
}
14231427

1428+
final FileTransfer _fileTransfer;
1429+
14241430
final Map<String, Socket> _forwardedConnections = <String, Socket>{};
14251431
int _id = 0;
14261432

@@ -1435,12 +1441,26 @@ class ProxyDomain extends Domain {
14351441
/// Calculate rolling hashes for a file in the local temporary directory.
14361442
Future<Map<String, Object?>?> calculateFileHashes(Map<String, Object?> args) async {
14371443
final String path = _getStringArg(args, 'path', required: true)!;
1444+
final bool cacheResult = _getBoolArg(args, 'cacheResult') ?? false;
14381445
final File file = tempDirectory.childFile(path);
14391446
if (!await file.exists()) {
14401447
return null;
14411448
}
1442-
final BlockHashes result = await FileTransfer().calculateBlockHashesOfFile(file);
1443-
return result.toJson();
1449+
final File hashFile = file.parent.childFile('${file.basename}.hashes');
1450+
if (hashFile.existsSync() && hashFile.statSync().modified.isAfter(file.statSync().modified)) {
1451+
// If the cached hash file is newer than the file, assume that the cached
1452+
// is up to date. Return the cached result directly.
1453+
final String cachedJson = await hashFile.readAsString();
1454+
return json.decode(cachedJson) as Map<String, Object?>;
1455+
}
1456+
final BlockHashes result = await _fileTransfer.calculateBlockHashesOfFile(file);
1457+
final Map<String, Object?> resultObject = result.toJson();
1458+
1459+
if (cacheResult) {
1460+
await hashFile.writeAsString(json.encode(resultObject));
1461+
}
1462+
1463+
return resultObject;
14441464
}
14451465

14461466
Future<bool?> updateFile(Map<String, Object?> args, Stream<List<int>>? binary) async {
@@ -1451,7 +1471,7 @@ class ProxyDomain extends Domain {
14511471
}
14521472
final List<Map<String, Object?>> deltaJson = (args['delta']! as List<Object?>).cast<Map<String, Object?>>();
14531473
final List<FileDeltaBlock> delta = FileDeltaBlock.fromJsonList(deltaJson);
1454-
final bool result = await FileTransfer().rebuildFile(file, delta, binary!);
1474+
final bool result = await _fileTransfer.rebuildFile(file, delta, binary!);
14551475
return result;
14561476
}
14571477

packages/flutter_tools/lib/src/proxied_devices/devices.dart

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ class ProxiedDevices extends PollingDeviceDiscovery {
4141
bool deltaFileTransfer = true,
4242
bool enableDdsProxy = false,
4343
required Logger logger,
44+
FileTransfer fileTransfer = const FileTransfer(),
4445
}) : _deltaFileTransfer = deltaFileTransfer,
4546
_enableDdsProxy = enableDdsProxy,
4647
_logger = logger,
48+
_fileTransfer = fileTransfer,
4749
super('Proxied devices');
4850

4951
/// [DaemonConnection] used to communicate with the daemon.
@@ -55,6 +57,8 @@ class ProxiedDevices extends PollingDeviceDiscovery {
5557

5658
final bool _enableDdsProxy;
5759

60+
final FileTransfer _fileTransfer;
61+
5862
@override
5963
bool get supportsPlatform => true;
6064

@@ -117,6 +121,7 @@ class ProxiedDevices extends PollingDeviceDiscovery {
117121
supportsFastStart: _cast<bool>(capabilities['fastStart']),
118122
supportsHardwareRendering: _cast<bool>(capabilities['hardwareRendering']),
119123
logger: _logger,
124+
fileTransfer: _fileTransfer,
120125
);
121126
}
122127
}
@@ -149,6 +154,7 @@ class ProxiedDevice extends Device {
149154
required this.supportsFastStart,
150155
required bool supportsHardwareRendering,
151156
required Logger logger,
157+
FileTransfer fileTransfer = const FileTransfer(),
152158
}): _deltaFileTransfer = deltaFileTransfer,
153159
_enableDdsProxy = enableDdsProxy,
154160
_isLocalEmulator = isLocalEmulator,
@@ -157,6 +163,7 @@ class ProxiedDevice extends Device {
157163
_supportsHardwareRendering = supportsHardwareRendering,
158164
_targetPlatform = targetPlatform,
159165
_logger = logger,
166+
_fileTransfer = fileTransfer,
160167
super(id,
161168
category: category,
162169
platformType: platformType,
@@ -171,6 +178,8 @@ class ProxiedDevice extends Device {
171178

172179
final bool _enableDdsProxy;
173180

181+
final FileTransfer _fileTransfer;
182+
174183
@override
175184
final String name;
176185

@@ -359,7 +368,7 @@ class ProxiedDevice extends Device {
359368

360369
Map<String, Object?>? rollingHashResultJson;
361370
if (_deltaFileTransfer) {
362-
rollingHashResultJson = _cast<Map<String, Object?>?>(await connection.sendRequest('proxy.calculateFileHashes', args));
371+
rollingHashResultJson = _cast<Map<String, Object?>?>(await connection.sendRequest('proxy.calculateFileHashes', args));
363372
}
364373

365374
if (rollingHashResultJson == null) {
@@ -371,12 +380,12 @@ class ProxiedDevice extends Device {
371380
await connection.sendRequest('proxy.writeTempFile', args, await binary.readAsBytes());
372381
} else {
373382
final BlockHashes rollingHashResult = BlockHashes.fromJson(rollingHashResultJson);
374-
final List<FileDeltaBlock> delta = await FileTransfer().computeDelta(binary, rollingHashResult);
383+
final List<FileDeltaBlock> delta = await _fileTransfer.computeDelta(binary, rollingHashResult);
375384

376385
// Delta is empty if the file does not need to be updated
377386
if (delta.isNotEmpty) {
378387
final List<Map<String, Object>> deltaJson = delta.map((FileDeltaBlock block) => block.toJson()).toList();
379-
final Uint8List buffer = await FileTransfer().binaryForRebuilding(binary, delta);
388+
final Uint8List buffer = await _fileTransfer.binaryForRebuilding(binary, delta);
380389

381390
await connection.sendRequest('proxy.updateFile', <String, Object>{
382391
'path': fileName,
@@ -385,6 +394,19 @@ class ProxiedDevice extends Device {
385394
}
386395
}
387396

397+
if (_deltaFileTransfer) {
398+
// Ask the daemon to precache the hash content for subsequent runs.
399+
// Wait for several seconds for the app to be launched, to not interfere
400+
// with whatever the daemon is doing.
401+
unawaited(() async {
402+
await Future<void>.delayed(const Duration(seconds: 60));
403+
await connection.sendRequest('proxy.calculateFileHashes', <String, Object>{
404+
'path': fileName,
405+
'cacheResult': true,
406+
});
407+
}());
408+
}
409+
388410
final String id = _cast<String>(await connection.sendRequest('device.uploadApplicationPackage', <String, Object>{
389411
'targetPlatform': getNameForTargetPlatform(_targetPlatform),
390412
'applicationBinary': fileName,

packages/flutter_tools/lib/src/proxied_devices/file_transfer.dart

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const int _adler32Prime = 65521;
104104

105105
/// Helper function to calculate Adler32 hash of a binary.
106106
@visibleForTesting
107-
int adler32Hash(List<int> binary) {
107+
int adler32Hash(Uint8List binary) {
108108
// The maximum integer that can be stored in the `int` data type.
109109
const int maxInt = 0x1fffffffffffff;
110110
// maxChunkSize is the maximum number of bytes we can sum without
@@ -119,8 +119,8 @@ int adler32Hash(List<int> binary) {
119119
final int length = binary.length;
120120
for (int i = 0; i < length; i += maxChunkSize) {
121121
final int end = i + maxChunkSize < length ? i + maxChunkSize : length;
122-
for (final int c in binary.getRange(i, end)) {
123-
a += c;
122+
for (int j = i; j < end; j++) {
123+
a += binary[j];
124124
b += a;
125125
}
126126
a %= _adler32Prime;
@@ -220,19 +220,22 @@ class RollingAdler32 {
220220
/// On the receiving end, it will build a copy of the source file from the
221221
/// given instructions.
222222
class FileTransfer {
223+
const FileTransfer();
224+
223225
/// Calculate hashes of blocks in the file.
224226
Future<BlockHashes> calculateBlockHashesOfFile(File file, { int? blockSize }) async {
225227
final int totalSize = await file.length();
226228
blockSize ??= max(sqrt(totalSize).ceil(), 2560);
227229

228-
final Stream<Uint8List> fileContentStream = file.openRead().map((List<int> chunk) => Uint8List.fromList(chunk));
230+
final Stream<Uint8List> fileContentStream = file.openRead().map((List<int> chunk) => chunk is Uint8List ? chunk : Uint8List.fromList(chunk));
229231

230232
final List<int> adler32Results = <int>[];
231233
final List<String> md5Results = <String>[];
232-
await for (final Uint8List chunk in convertToChunks(fileContentStream, blockSize)) {
234+
235+
await convertToChunks(fileContentStream, blockSize).forEach((Uint8List chunk) {
233236
adler32Results.add(adler32Hash(chunk));
234237
md5Results.add(base64.encode(md5.convert(chunk).bytes));
235-
}
238+
});
236239

237240
// Handle whole file md5 separately. Md5Hash requires the chunk size to be a multiple of 64.
238241
final String fileMd5 = await _md5OfFile(file);
@@ -276,8 +279,9 @@ class FileTransfer {
276279

277280
final List<FileDeltaBlock> blocks = <FileDeltaBlock>[];
278281

279-
await for (final List<int> chunk in fileContentStream) {
280-
for (final int c in chunk) {
282+
await fileContentStream.forEach((List<int> chunk) {
283+
for (int i = 0; i < chunk.length; i++) {
284+
final int c = chunk[i];
281285
final int hash = adler32.push(c);
282286
size++;
283287

@@ -326,7 +330,7 @@ class FileTransfer {
326330
break;
327331
}
328332
}
329-
}
333+
});
330334

331335
// For the remaining content that is not matched, copy from the source.
332336
if (start < size) {
@@ -401,7 +405,7 @@ class FileTransfer {
401405

402406
Future<String> _md5OfFile(File file) async {
403407
final Md5Hash fileMd5Hash = Md5Hash();
404-
await file.openRead().forEach((List<int> chunk) => fileMd5Hash.addChunk(Uint8List.fromList(chunk)));
408+
await file.openRead().forEach((List<int> chunk) => fileMd5Hash.addChunk(chunk is Uint8List ? chunk : Uint8List.fromList(chunk)));
405409
return base64.encode(fileMd5Hash.finalize().buffer.asUint8List());
406410
}
407411
}

packages/flutter_tools/test/general.shard/proxied_devices/file_transfer_test.dart

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ void main() {
6363

6464
group('adler32Hash', () {
6565
test('works correctly', () {
66-
final int hash = adler32Hash(utf8.encode('abcdefg'));
66+
final int hash = adler32Hash(Uint8List.fromList(utf8.encode('abcdefg')));
6767
expect(hash, 0x0adb02bd);
6868
});
6969
});
@@ -72,27 +72,27 @@ void main() {
7272
test('works correctly without rolling', () {
7373
final RollingAdler32 adler32 = RollingAdler32(7);
7474
utf8.encode('abcdefg').forEach(adler32.push);
75-
expect(adler32.hash, adler32Hash(utf8.encode('abcdefg')));
75+
expect(adler32.hash, adler32Hash(Uint8List.fromList(utf8.encode('abcdefg'))));
7676
});
7777

7878
test('works correctly after rolling once', () {
7979
final RollingAdler32 adler32 = RollingAdler32(7);
8080
utf8.encode('12abcdefg').forEach(adler32.push);
81-
expect(adler32.hash, adler32Hash(utf8.encode('abcdefg')));
81+
expect(adler32.hash, adler32Hash(Uint8List.fromList(utf8.encode('abcdefg'))));
8282
});
8383

8484
test('works correctly after rolling multiple cycles', () {
8585
final RollingAdler32 adler32 = RollingAdler32(7);
8686
utf8.encode('1234567890123456789abcdefg').forEach(adler32.push);
87-
expect(adler32.hash, adler32Hash(utf8.encode('abcdefg')));
87+
expect(adler32.hash, adler32Hash(Uint8List.fromList(utf8.encode('abcdefg'))));
8888
});
8989

9090
test('works correctly after reset', () {
9191
final RollingAdler32 adler32 = RollingAdler32(7);
9292
utf8.encode('1234567890123456789abcdefg').forEach(adler32.push);
9393
adler32.reset();
9494
utf8.encode('abcdefg').forEach(adler32.push);
95-
expect(adler32.hash, adler32Hash(utf8.encode('abcdefg')));
95+
expect(adler32.hash, adler32Hash(Uint8List.fromList(utf8.encode('abcdefg'))));
9696
});
9797

9898
test('currentBlock returns the correct entry when read less than one block', () {
@@ -133,7 +133,7 @@ void main() {
133133
test('calculateBlockHashesOfFile works normally', () async {
134134
final File file = fileSystem.file('test')..writeAsStringSync(content1);
135135

136-
final BlockHashes hashes = await FileTransfer().calculateBlockHashesOfFile(file, blockSize: 4);
136+
final BlockHashes hashes = await const FileTransfer().calculateBlockHashesOfFile(file, blockSize: 4);
137137
expect(hashes.blockSize, 4);
138138
expect(hashes.totalSize, content1.length);
139139
expect(hashes.adler32, hasLength(5));
@@ -159,8 +159,8 @@ void main() {
159159
final File file1 = fileSystem.file('file1')..writeAsStringSync(content1);
160160
final File file2 = fileSystem.file('file1')..writeAsStringSync(content1);
161161

162-
final BlockHashes hashes = await FileTransfer().calculateBlockHashesOfFile(file1, blockSize: 4);
163-
final List<FileDeltaBlock> delta = await FileTransfer().computeDelta(file2, hashes);
162+
final BlockHashes hashes = await const FileTransfer().calculateBlockHashesOfFile(file1, blockSize: 4);
163+
final List<FileDeltaBlock> delta = await const FileTransfer().computeDelta(file2, hashes);
164164

165165
expect(delta, isEmpty);
166166
});
@@ -169,21 +169,21 @@ void main() {
169169
final File file1 = fileSystem.file('file1')..writeAsStringSync(content1);
170170
final File file2 = fileSystem.file('file2')..writeAsStringSync(content2);
171171

172-
final BlockHashes hashes = await FileTransfer().calculateBlockHashesOfFile(file1, blockSize: 4);
173-
final List<FileDeltaBlock> delta = await FileTransfer().computeDelta(file2, hashes);
172+
final BlockHashes hashes = await const FileTransfer().calculateBlockHashesOfFile(file1, blockSize: 4);
173+
final List<FileDeltaBlock> delta = await const FileTransfer().computeDelta(file2, hashes);
174174

175175
expect(delta, expectedDelta);
176176
});
177177

178178
test('binaryForRebuilding returns the correct binary', () async {
179179
final File file = fileSystem.file('file')..writeAsStringSync(content2);
180-
final List<int> binaryForRebuilding = await FileTransfer().binaryForRebuilding(file, expectedDelta);
180+
final List<int> binaryForRebuilding = await const FileTransfer().binaryForRebuilding(file, expectedDelta);
181181
expect(binaryForRebuilding, utf8.encode(expectedBinaryForRebuilding));
182182
});
183183

184184
test('rebuildFile can rebuild the correct file', () async {
185185
final File file = fileSystem.file('file')..writeAsStringSync(content1);
186-
await FileTransfer().rebuildFile(file, expectedDelta, Stream<List<int>>.fromIterable(<List<int>>[utf8.encode(expectedBinaryForRebuilding)]));
186+
await const FileTransfer().rebuildFile(file, expectedDelta, Stream<List<int>>.fromIterable(<List<int>>[utf8.encode(expectedBinaryForRebuilding)]));
187187
expect(file.readAsStringSync(), content2);
188188
});
189189
});

0 commit comments

Comments
 (0)