diff --git a/build/lib/src/state/filesystem.dart b/build/lib/src/state/filesystem.dart index 6e7a1b8435..71a7d99510 100644 --- a/build/lib/src/state/filesystem.dart +++ b/build/lib/src/state/filesystem.dart @@ -6,36 +6,20 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:pool/pool.dart'; - /// The filesystem the build is running on. /// /// Methods behave as the `dart:io` methods with the same names, with some /// exceptions noted in the docs. abstract interface class Filesystem { - /// Whether the file exists. - Future exists(String path); - /// Whether the file exists. bool existsSync(String path); - /// Reads a file as a string. - Future readAsString(String path, {Encoding encoding = utf8}); - /// Reads a file as a string. String readAsStringSync(String path, {Encoding encoding = utf8}); - /// Reads a file as bytes. - Future readAsBytes(String path); - /// Reads a file as bytes. Uint8List readAsBytesSync(String path); - /// Deletes a file. - /// - /// If the file does not exist, does nothing. - Future delete(String path); - /// Deletes a file. /// /// If the file does not exist, does nothing. @@ -44,7 +28,7 @@ abstract interface class Filesystem { /// Deletes a directory recursively. /// /// If the directory does not exist, does nothing. - Future deleteDirectory(String path); + void deleteDirectorySync(String path); /// Writes a file. /// @@ -55,48 +39,20 @@ abstract interface class Filesystem { Encoding encoding = utf8, }); - /// Writes a file. - /// - /// Creates enclosing directories as needed if they don't exist. - Future writeAsString( - String path, - String contents, { - Encoding encoding = utf8, - }); - /// Writes a file. /// /// Creates enclosing directories as needed if they don't exist. void writeAsBytesSync(String path, List contents); - - /// Writes a file. - /// - /// Creates enclosing directories as needed if they don't exist. - Future writeAsBytes(String path, List contents); } /// The `dart:io` filesystem. class IoFilesystem implements Filesystem { - /// Pool for async file operations. - final _pool = Pool(32); - - @override - Future exists(String path) => _pool.withResource(File(path).exists); - @override bool existsSync(String path) => File(path).existsSync(); - @override - Future readAsBytes(String path) => - _pool.withResource(File(path).readAsBytes); - @override Uint8List readAsBytesSync(String path) => File(path).readAsBytesSync(); - @override - Future readAsString(String path, {Encoding encoding = utf8}) => - _pool.withResource(() => File(path).readAsString(encoding: encoding)); - @override String readAsStringSync(String path, {Encoding encoding = utf8}) => File(path).readAsStringSync(encoding: encoding); @@ -108,19 +64,9 @@ class IoFilesystem implements Filesystem { } @override - Future delete(String path) { - return _pool.withResource(() async { - final file = File(path); - if (await file.exists()) await file.delete(); - }); - } - - @override - Future deleteDirectory(String path) { - return _pool.withResource(() async { - final directory = Directory(path); - if (await directory.exists()) await directory.delete(recursive: true); - }); + void deleteDirectorySync(String path) { + final directory = Directory(path); + if (directory.existsSync()) directory.deleteSync(recursive: true); } @override @@ -134,15 +80,6 @@ class IoFilesystem implements Filesystem { file.writeAsBytesSync(contents); } - @override - Future writeAsBytes(String path, List contents) { - return _pool.withResource(() async { - final file = File(path); - await file.parent.create(recursive: true); - await file.writeAsBytes(contents); - }); - } - @override void writeAsStringSync( String path, @@ -153,19 +90,6 @@ class IoFilesystem implements Filesystem { file.parent.createSync(recursive: true); file.writeAsStringSync(contents, encoding: encoding); } - - @override - Future writeAsString( - String path, - String contents, { - Encoding encoding = utf8, - }) { - return _pool.withResource(() async { - final file = File(path); - await file.parent.create(recursive: true); - await file.writeAsString(contents, encoding: encoding); - }); - } } /// An in-memory [Filesystem]. @@ -175,40 +99,23 @@ class InMemoryFilesystem implements Filesystem { /// The paths to all files present on the filesystem. Iterable get filePaths => _files.keys; - @override - Future exists(String path) => Future.value(_files.containsKey(path)); - @override bool existsSync(String path) => _files.containsKey(path); - @override - Future readAsBytes(String path) => Future.value(_files[path]!); - @override Uint8List readAsBytesSync(String path) => _files[path]!; - @override - Future readAsString(String path, {Encoding encoding = utf8}) => - Future.value(encoding.decode(_files[path]!)); - @override String readAsStringSync(String path, {Encoding encoding = utf8}) => encoding.decode(_files[path]!); - @override - Future delete(String path) { - _files.remove(path); - return Future.value(); - } - @override void deleteSync(String path) => _files.remove(path); @override - Future deleteDirectory(String path) { + void deleteDirectorySync(String path) { final prefix = '$path/'; _files.removeWhere((filePath, _) => filePath.startsWith(prefix)); - return Future.value(); } @override @@ -216,12 +123,6 @@ class InMemoryFilesystem implements Filesystem { _files[path] = Uint8List.fromList(contents); } - @override - Future writeAsBytes(String path, List contents) { - _files[path] = Uint8List.fromList(contents); - return Future.value(); - } - @override void writeAsStringSync( String path, @@ -230,14 +131,4 @@ class InMemoryFilesystem implements Filesystem { }) { _files[path] = Uint8List.fromList(encoding.encode(contents)); } - - @override - Future writeAsString( - String path, - String contents, { - Encoding encoding = utf8, - }) { - _files[path] = Uint8List.fromList(encoding.encode(contents)); - return Future.value(); - } } diff --git a/build/lib/src/state/filesystem_cache.dart b/build/lib/src/state/filesystem_cache.dart index febb7625e9..374fa21174 100644 --- a/build/lib/src/state/filesystem_cache.dart +++ b/build/lib/src/state/filesystem_cache.dart @@ -14,30 +14,25 @@ import 'lru_cache.dart'; /// TODO(davidmorgan): benchmark, optimize the caching strategy. abstract interface class FilesystemCache { /// Clears all [ids] from all caches. - /// - /// Waits for any pending reads to complete first. - Future invalidate(Iterable ids); + void invalidate(Iterable ids); /// Whether [id] exists. /// /// Returns a cached result if available, or caches and returns `ifAbsent()`. - Future exists(AssetId id, {required Future Function() ifAbsent}); + bool exists(AssetId id, {required bool Function() ifAbsent}); /// Reads [id] as bytes. /// /// Returns a cached result if available, or caches and returns `ifAbsent()`. - Future readAsBytes( - AssetId id, { - required Future Function() ifAbsent, - }); + Uint8List readAsBytes(AssetId id, {required Uint8List Function() ifAbsent}); /// Reads [id] as a `String`. /// /// Returns a cached result if available, or caches and returns `ifAbsent()`. - Future readAsString( + String readAsString( AssetId id, { Encoding encoding = utf8, - required Future Function() ifAbsent, + required Uint8List Function() ifAbsent, }); } @@ -49,23 +44,18 @@ class PassthroughFilesystemCache implements FilesystemCache { Future invalidate(Iterable ids) async {} @override - Future exists( - AssetId id, { - required Future Function() ifAbsent, - }) => ifAbsent(); + bool exists(AssetId id, {required bool Function() ifAbsent}) => ifAbsent(); @override - Future readAsBytes( - AssetId id, { - required Future Function() ifAbsent, - }) => ifAbsent(); + Uint8List readAsBytes(AssetId id, {required Uint8List Function() ifAbsent}) => + ifAbsent(); @override - Future readAsString( + String readAsString( AssetId id, { Encoding encoding = utf8, - required Future Function() ifAbsent, - }) async => encoding.decode(await ifAbsent()); + required Uint8List Function() ifAbsent, + }) => encoding.decode(ifAbsent()); } /// [FilesystemCache] that stores data in memory. @@ -77,13 +67,10 @@ class InMemoryFilesystemCache implements FilesystemCache { (value) => value.lengthInBytes, ); - /// Pending [readAsBytes] operations. - final _pendingBytesContentCache = >{}; - /// Cached results of [exists]. /// /// Don't bother using an LRU cache for this since it's just booleans. - final _canReadCache = >{}; + final _existsCache = {}; /// Cached results of [readAsString]. /// @@ -96,64 +83,50 @@ class InMemoryFilesystemCache implements FilesystemCache { (value) => value.length, ); - /// Pending `readAsString` operations. - final _pendingStringContentCache = >{}; - @override Future invalidate(Iterable ids) async { - // First finish all pending operations, as they will write to the cache. - for (var id in ids) { - await _canReadCache.remove(id); - await _pendingBytesContentCache.remove(id); - await _pendingStringContentCache.remove(id); - } for (var id in ids) { + _existsCache.remove(id); _bytesContentCache.remove(id); _stringContentCache.remove(id); } } @override - Future exists( - AssetId id, { - required Future Function() ifAbsent, - }) => _canReadCache.putIfAbsent(id, ifAbsent); + bool exists(AssetId id, {required bool Function() ifAbsent}) => + _existsCache.putIfAbsent(id, ifAbsent); @override - Future readAsBytes( - AssetId id, { - required Future Function() ifAbsent, - }) { - var cached = _bytesContentCache[id]; - if (cached != null) return Future.value(cached); - - return _pendingBytesContentCache.putIfAbsent(id, () async { - final result = await ifAbsent(); - _bytesContentCache[id] = result; - unawaited(_pendingBytesContentCache.remove(id)); - return result; - }); + Uint8List readAsBytes(AssetId id, {required Uint8List Function() ifAbsent}) { + final maybeResult = _bytesContentCache[id]; + if (maybeResult != null) return maybeResult; + + final result = ifAbsent(); + _bytesContentCache[id] = result; + return result; } @override - Future readAsString( + String readAsString( AssetId id, { Encoding encoding = utf8, - required Future Function() ifAbsent, - }) async { + required Uint8List Function() ifAbsent, + }) { if (encoding != utf8) { - final bytes = await readAsBytes(id, ifAbsent: ifAbsent); + final bytes = readAsBytes(id, ifAbsent: ifAbsent); return encoding.decode(bytes); } - var cached = _stringContentCache[id]; - if (cached != null) return cached; + final maybeResult = _stringContentCache[id]; + if (maybeResult != null) return maybeResult; - return _pendingStringContentCache.putIfAbsent(id, () async { - final bytes = await ifAbsent(); - final result = _stringContentCache[id] = utf8.decode(bytes); - unawaited(_pendingStringContentCache.remove(id)); - return result; - }); + var bytes = _bytesContentCache[id]; + if (bytes == null) { + bytes = ifAbsent(); + _bytesContentCache[id] = bytes; + } + final result = utf8.decode(bytes); + _stringContentCache[id] = result; + return result; } } diff --git a/build/test/state/filesystem_cache_test.dart b/build/test/state/filesystem_cache_test.dart index 1e5943cf8b..3c23cf1939 100644 --- a/build/test/state/filesystem_cache_test.dart +++ b/build/test/state/filesystem_cache_test.dart @@ -24,88 +24,70 @@ void main() { }); group('exists', () { - test('reads from ifAbsent', () async { - expect(await cache.exists(txt1, ifAbsent: () async => true), isTrue); - expect(await cache.exists(txt2, ifAbsent: () async => false), isFalse); + test('reads from ifAbsent', () { + expect(cache.exists(txt1, ifAbsent: () => true), isTrue); + expect(cache.exists(txt2, ifAbsent: () => false), isFalse); }); - test('does not re-read from ifAbsent', () async { - expect(await cache.exists(txt1, ifAbsent: () async => true), isTrue); + test('does not re-read from ifAbsent', () { + expect(cache.exists(txt1, ifAbsent: () => true), isTrue); expect( - await cache.exists(txt1, ifAbsent: () async => false), + cache.exists(txt1, ifAbsent: () => false), isTrue /* cached value */, ); }); - test('can be invalidated with invalidate', () async { - expect(await cache.exists(txt1, ifAbsent: () async => true), isTrue); - await cache.invalidate([txt1]); + test('can be invalidated with invalidate', () { + expect(cache.exists(txt1, ifAbsent: () => true), isTrue); + cache.invalidate([txt1]); expect( - await cache.exists(txt1, ifAbsent: () async => false), + cache.exists(txt1, ifAbsent: () => false), isFalse /* updated value */, ); }); }); group('readAsBytes', () { - test('reads from ifAbsent', () async { - expect( - await cache.readAsBytes(txt1, ifAbsent: () async => txt1Bytes), - txt1Bytes, - ); + test('reads from ifAbsent', () { + expect(cache.readAsBytes(txt1, ifAbsent: () => txt1Bytes), txt1Bytes); }); - test('does not re-read from ifAbsent', () async { - expect( - await cache.readAsBytes(txt1, ifAbsent: () async => txt1Bytes), - txt1Bytes, - ); + test('does not re-read from ifAbsent', () { + expect(cache.readAsBytes(txt1, ifAbsent: () => txt1Bytes), txt1Bytes); expect( - await cache.readAsBytes(txt1, ifAbsent: () async => txt2Bytes), + cache.readAsBytes(txt1, ifAbsent: () => txt2Bytes), txt1Bytes /* cached value */, ); }); - test('can be invalidated with invalidate', () async { - expect( - await cache.readAsBytes(txt1, ifAbsent: () async => txt1Bytes), - txt1Bytes, - ); - await cache.invalidate([txt1]); + test('can be invalidated with invalidate', () { + expect(cache.readAsBytes(txt1, ifAbsent: () => txt1Bytes), txt1Bytes); + cache.invalidate([txt1]); expect( - await cache.readAsBytes(txt1, ifAbsent: () async => txt2Bytes), + cache.readAsBytes(txt1, ifAbsent: () => txt2Bytes), txt2Bytes /* updated value */, ); }); }); group('readAsString', () { - test('reads from isAbsent', () async { - expect( - await cache.readAsString(txt1, ifAbsent: () async => txt1Bytes), - txt1String, - ); + test('reads from isAbsent', () { + expect(cache.readAsString(txt1, ifAbsent: () => txt1Bytes), txt1String); }); - test('does not re-read from isAbsent', () async { - expect( - await cache.readAsString(txt1, ifAbsent: () async => txt1Bytes), - txt1String, - ); + test('does not re-read from isAbsent', () { + expect(cache.readAsString(txt1, ifAbsent: () => txt1Bytes), txt1String); expect( - await cache.readAsString(txt1, ifAbsent: () async => txt2Bytes), + cache.readAsString(txt1, ifAbsent: () => txt2Bytes), txt1String /* cached value */, ); }); - test('can be invalidated with invalidate', () async { - expect( - await cache.readAsString(txt1, ifAbsent: () async => txt1Bytes), - txt1String, - ); - await cache.invalidate([txt1]); + test('can be invalidated with invalidate', () { + expect(cache.readAsString(txt1, ifAbsent: () => txt1Bytes), txt1String); + cache.invalidate([txt1]); expect( - await cache.readAsString(txt1, ifAbsent: () async => txt2Bytes), + cache.readAsString(txt1, ifAbsent: () => txt2Bytes), txt2String /* updated value */, ); }); diff --git a/build_runner_core/lib/src/asset/reader_writer.dart b/build_runner_core/lib/src/asset/reader_writer.dart index 9f6a5906a2..3a53deb66c 100644 --- a/build_runner_core/lib/src/asset/reader_writer.dart +++ b/build_runner_core/lib/src/asset/reader_writer.dart @@ -83,41 +83,47 @@ class ReaderWriter extends AssetReader @override Future canRead(AssetId id) { - return cache.exists( - id, - ifAbsent: () async { - final path = _pathFor(id); - return filesystem.exists(path); - }, + return Future.value( + cache.exists( + id, + ifAbsent: () { + final path = _pathFor(id); + return filesystem.existsSync(path); + }, + ), ); } @override - Future> readAsBytes(AssetId id) async { - return cache.readAsBytes( - id, - ifAbsent: () async { - final path = _pathFor(id); - if (!await filesystem.exists(path)) { - throw AssetNotFoundException(id, path: path); - } - return filesystem.readAsBytes(path); - }, + Future> readAsBytes(AssetId id) { + return Future.value( + cache.readAsBytes( + id, + ifAbsent: () { + final path = _pathFor(id); + if (!filesystem.existsSync(path)) { + throw AssetNotFoundException(id, path: path); + } + return filesystem.readAsBytesSync(path); + }, + ), ); } @override - Future readAsString(AssetId id, {Encoding encoding = utf8}) async { - return cache.readAsString( - id, - encoding: encoding, - ifAbsent: () async { - final path = _pathFor(id); - if (!await filesystem.exists(path)) { - throw AssetNotFoundException(id, path: path); - } - return filesystem.readAsBytes(path); - }, + Future readAsString(AssetId id, {Encoding encoding = utf8}) { + return Future.value( + cache.readAsString( + id, + encoding: encoding, + ifAbsent: () { + final path = _pathFor(id); + if (!filesystem.existsSync(path)) { + throw AssetNotFoundException(id, path: path); + } + return filesystem.readAsBytesSync(path); + }, + ), ); } @@ -128,9 +134,10 @@ class ReaderWriter extends AssetReader // [AssetWriter] methods. @override - Future writeAsBytes(AssetId id, List bytes) async { + Future writeAsBytes(AssetId id, List bytes) { final path = _pathFor(id); - await filesystem.writeAsBytes(path, bytes); + filesystem.writeAsBytesSync(path, bytes); + return Future.value(); } @override @@ -138,13 +145,14 @@ class ReaderWriter extends AssetReader AssetId id, String contents, { Encoding encoding = utf8, - }) async { + }) { final path = _pathFor(id); - await filesystem.writeAsString(path, contents, encoding: encoding); + filesystem.writeAsStringSync(path, contents, encoding: encoding); + return Future.value(); } @override - Future delete(AssetId id) async { + Future delete(AssetId id) { onDelete?.call(id); final path = _pathFor(id); // Hidden generated files are moved by `assetPathProvider` under the root @@ -160,13 +168,15 @@ class ReaderWriter extends AssetReader 'Should not delete assets outside of $rootPackage', ); } - await filesystem.delete(path); + filesystem.deleteSync(path); + return Future.value(); } @override - Future deleteDirectory(AssetId id) async { + Future deleteDirectory(AssetId id) { final path = _pathFor(id); - await filesystem.deleteDirectory(path); + filesystem.deleteDirectorySync(path); + return Future.value(); } @override diff --git a/build_runner_core/lib/src/asset_graph/graph.dart b/build_runner_core/lib/src/asset_graph/graph.dart index faa012f2c4..7393c2dec6 100644 --- a/build_runner_core/lib/src/asset_graph/graph.dart +++ b/build_runner_core/lib/src/asset_graph/graph.dart @@ -252,7 +252,7 @@ class AssetGraph implements GeneratedAssetHider { Iterable ids, AssetReader digestReader, ) async { - await digestReader.cache.invalidate(ids); + digestReader.cache.invalidate(ids); await Future.wait( ids.map((id) async { final digest = await digestReader.digest(id); diff --git a/build_runner_core/lib/src/generate/asset_tracker.dart b/build_runner_core/lib/src/generate/asset_tracker.dart index 18d35b0a91..a00735ca2b 100644 --- a/build_runner_core/lib/src/generate/asset_tracker.dart +++ b/build_runner_core/lib/src/generate/asset_tracker.dart @@ -112,7 +112,7 @@ class AssetTracker { var node = assetGraph.get(id)!; var originalDigest = node.digest; if (originalDigest == null) return; - await _reader.cache.invalidate([id]); + _reader.cache.invalidate([id]); var currentDigest = await _reader.digest(id); if (currentDigest != originalDigest) { updates[id] = ChangeType.MODIFY; diff --git a/build_runner_core/lib/src/generate/build.dart b/build_runner_core/lib/src/generate/build.dart index 5d3f87cf06..044b28aed8 100644 --- a/build_runner_core/lib/src/generate/build.dart +++ b/build_runner_core/lib/src/generate/build.dart @@ -231,7 +231,7 @@ class Build { deletedAssets.addAll(deleted); // TODO(davidmorgan): there are a few places that invalidate, check that // it's once per file, add test coverage. - await readerWriter.cache.invalidate(changedInputs); + readerWriter.cache.invalidate(changedInputs); }); } @@ -1209,7 +1209,7 @@ class Build { final result = errors.isEmpty; - await readerWriter.cache.invalidate(outputs); + readerWriter.cache.invalidate(outputs); for (final output in outputs) { final wasOutput = readerWriter.assetsWritten.contains(output); final digest = wasOutput ? await this.readerWriter.digest(output) : null; diff --git a/build_runner_core/test/asset/file_based_test.dart b/build_runner_core/test/asset/file_based_test.dart index b9d542e96b..494c892d64 100644 --- a/build_runner_core/test/asset/file_based_test.dart +++ b/build_runner_core/test/asset/file_based_test.dart @@ -76,15 +76,15 @@ void main() async { test('throws when attempting to read a non-existent file', () async { expect( - readerWriter.readAsString(makeAssetId('basic_pkg|foo.txt')), + () => readerWriter.readAsString(makeAssetId('basic_pkg|foo.txt')), throwsA(assetNotFoundException), ); expect( - readerWriter.readAsString(makeAssetId('a|lib/b.txt')), + () => readerWriter.readAsString(makeAssetId('a|lib/b.txt')), throwsA(assetNotFoundException), ); expect( - readerWriter.readAsString(makeAssetId('foo|lib/bar.txt')), + () => readerWriter.readAsString(makeAssetId('foo|lib/bar.txt')), throwsA(packageNotFoundException), ); });