Skip to content

Commit 27a6705

Browse files
[flutter_tools] chunk the hashing of large files (flutter#57506)
For files larger than 250 KB (roughly the size of framework.dart), chunk the conversion. This may be important for large assets like images.
1 parent b075168 commit 27a6705

File tree

2 files changed

+45
-2
lines changed

2 files changed

+45
-2
lines changed

packages/flutter_tools/lib/src/build_system/file_store.dart

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import '../base/utils.dart';
1616
import '../convert.dart';
1717
import 'build_system.dart';
1818

19+
/// The default threshold for file chunking is 250 KB, or about the size of `framework.dart`.
20+
const int kDefaultFileChunkThresholdBytes = 250000;
21+
1922
/// An encoded representation of all file hashes.
2023
class FileStorage {
2124
FileStorage(this.version, this.files);
@@ -91,13 +94,16 @@ class FileStore {
9194
@required File cacheFile,
9295
@required Logger logger,
9396
FileStoreStrategy strategy = FileStoreStrategy.hash,
97+
int fileChunkThreshold = kDefaultFileChunkThresholdBytes,
9498
}) : _logger = logger,
9599
_strategy = strategy,
96-
_cacheFile = cacheFile;
100+
_cacheFile = cacheFile,
101+
_fileChunkThreshold = fileChunkThreshold;
97102

98103
final File _cacheFile;
99104
final Logger _logger;
100105
final FileStoreStrategy _strategy;
106+
final int _fileChunkThreshold;
101107

102108
final HashMap<String, String> previousAssetKeys = HashMap<String, String>();
103109
final HashMap<String, String> currentAssetKeys = HashMap<String, String>();
@@ -229,7 +235,18 @@ class FileStore {
229235
dirty.add(file);
230236
return;
231237
}
232-
final Digest digest = md5.convert(await file.readAsBytes());
238+
Digest digest;
239+
final int fileBytes = file.lengthSync();
240+
// For files larger than a given threshold, chunk the conversion.
241+
if (fileBytes > _fileChunkThreshold) {
242+
final StreamController<Digest> digests = StreamController<Digest>();
243+
final ByteConversionSink inputSink = md5.startChunkedConversion(digests);
244+
await file.openRead().forEach(inputSink.add);
245+
inputSink.close();
246+
digest = await digests.stream.last;
247+
} else {
248+
digest = md5.convert(await file.readAsBytes());
249+
}
233250
final String currentHash = digest.toString();
234251
if (currentHash != previousHash) {
235252
dirty.add(file);

packages/flutter_tools/test/general.shard/build_system/file_store_test.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:typed_data';
66

7+
import 'package:crypto/crypto.dart';
78
import 'package:file/memory.dart';
89
import 'package:file_testing/file_testing.dart';
910
import 'package:flutter_tools/src/artifacts.dart';
@@ -173,6 +174,31 @@ void main() {
173174

174175
expect(logger.errorText, contains('Out of space!'));
175176
});
177+
178+
testWithoutContext('FileStore handles chunked conversion of a file', () async {
179+
final FileSystem fileSystem = MemoryFileSystem.test();
180+
final File cacheFile = fileSystem
181+
.directory('example')
182+
.childFile(FileStore.kFileCache)
183+
..createSync(recursive: true);
184+
final FileStore fileCache = FileStore(
185+
cacheFile: cacheFile,
186+
logger: BufferLogger.test(),
187+
fileChunkThreshold: 1, // Chunk files larger than 1 byte.
188+
);
189+
final File file = fileSystem.file('foo.dart')
190+
..createSync()
191+
..writeAsStringSync('hello');
192+
fileCache.initialize();
193+
194+
cacheFile.parent.deleteSync(recursive: true);
195+
196+
await fileCache.diffFileList(<File>[file]);
197+
198+
// Validate that chunked hash is the same as non-chunked.
199+
expect(fileCache.currentAssetKeys['foo.dart'],
200+
md5.convert(file.readAsBytesSync()).toString());
201+
});
176202
}
177203

178204
class MockFile extends Mock implements File {}

0 commit comments

Comments
 (0)