Skip to content

Commit eaa0303

Browse files
committed
Refactor CachingAssetReader to FilesystemCache.
1 parent 36936be commit eaa0303

31 files changed

+487
-349
lines changed

build/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Refactor `MultiPackageAssetReader` to internal `AssetFinder`.
99
- Add internal `Filesystem` that backs `AssetReader` and `AssetWriter`
1010
implementations.
11+
- Refactor `CachingAssetReader` to `FilesystemCache`.
1112

1213
## 2.4.2
1314

build/lib/src/builder/build_step_impl.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import '../resource/resource.dart';
2121
import '../state/asset_finder.dart';
2222
import '../state/asset_path_provider.dart';
2323
import '../state/filesystem.dart';
24+
import '../state/filesystem_cache.dart';
2425
import '../state/input_tracker.dart';
2526
import '../state/reader_state.dart';
2627
import 'build_step.dart';
@@ -83,6 +84,19 @@ class BuildStepImpl implements BuildStep, AssetReaderState {
8384
_stageTracker = stageTracker ?? NoOpStageTracker.instance,
8485
_reportUnusedAssets = reportUnusedAssets;
8586

87+
@override
88+
BuildStepImpl copyWith({FilesystemCache? cache}) => BuildStepImpl(
89+
inputId,
90+
allowedOutputs,
91+
_reader.copyWith(cache: cache),
92+
_writer,
93+
_resolvers,
94+
_resourceManager,
95+
_resolvePackageConfig,
96+
stageTracker: _stageTracker,
97+
reportUnusedAssets: _reportUnusedAssets,
98+
);
99+
86100
@override
87101
Filesystem get filesystem => _reader.filesystem;
88102

build/lib/src/internal.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ library;
99
export 'state/asset_finder.dart';
1010
export 'state/asset_path_provider.dart';
1111
export 'state/filesystem.dart';
12+
export 'state/filesystem_cache.dart';
1213
export 'state/input_tracker.dart';
1314
export 'state/reader_state.dart';

build/lib/src/state/filesystem.dart

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,39 @@ import 'package:pool/pool.dart';
1010

1111
import '../asset/id.dart';
1212
import 'asset_path_provider.dart';
13+
import 'filesystem_cache.dart';
1314

1415
/// The filesystem the build is running on.
1516
///
1617
/// Methods behave as the `dart:io` methods with the same names, with some
17-
/// exceptions noted.
18+
/// exceptions noted in the docs.
19+
///
20+
/// Some methods cache, all uses of the cache are noted in the docs.
21+
///
22+
/// The cache might be a [PassthroughFilesystemCache] in which case it has no
23+
/// effect.
24+
///
25+
/// TODO(davidmorgan): extend caching to sync methods, deletes, writes.
1826
abstract interface class Filesystem {
27+
FilesystemCache get cache;
28+
29+
/// Returns a new instance with optionally updated [cache].
30+
Filesystem copyWith({FilesystemCache? cache});
31+
32+
/// Whether the file exists.
33+
///
34+
/// Uses [cache].
1935
Future<bool> exists(AssetId id);
2036

37+
/// Reads a file as a string.
38+
///
39+
/// Uses [cache]. For `utf8`, the `String` is cached; for any other encoding
40+
/// the bytes are cached but the conversion runs on every read.
2141
Future<String> readAsString(AssetId id, {Encoding encoding = utf8});
42+
43+
/// Reads a file as bytes.
44+
///
45+
/// Uses [cache].
2246
Future<Uint8List> readAsBytes(AssetId id);
2347

2448
/// Deletes a file.
@@ -62,23 +86,48 @@ abstract interface class Filesystem {
6286

6387
/// A filesystem using [assetPathProvider] to map to the `dart:io` filesystem.
6488
class IoFilesystem implements Filesystem {
89+
@override
90+
final FilesystemCache cache;
91+
6592
final AssetPathProvider assetPathProvider;
6693

6794
/// Pool for async file operations.
6895
final _pool = Pool(32);
6996

70-
IoFilesystem({required this.assetPathProvider});
97+
IoFilesystem({
98+
required this.assetPathProvider,
99+
this.cache = const PassthroughFilesystemCache(),
100+
});
101+
102+
@override
103+
IoFilesystem copyWith({FilesystemCache? cache}) => IoFilesystem(
104+
assetPathProvider: assetPathProvider,
105+
cache: cache ?? this.cache,
106+
);
71107

72108
@override
73-
Future<bool> exists(AssetId id) => _pool.withResource(_fileFor(id).exists);
109+
Future<bool> exists(AssetId id) =>
110+
cache.exists(id, ifAbsent: () => _pool.withResource(_fileFor(id).exists));
74111

75112
@override
76-
Future<Uint8List> readAsBytes(AssetId id) =>
77-
_pool.withResource(_fileFor(id).readAsBytes);
113+
Future<Uint8List> readAsBytes(AssetId id) => cache.readAsBytes(
114+
id,
115+
ifAbsent: () => _pool.withResource(_fileFor(id).readAsBytes),
116+
);
78117

79118
@override
80-
Future<String> readAsString(AssetId id, {Encoding encoding = utf8}) =>
81-
_pool.withResource(_fileFor(id).readAsString);
119+
Future<String> readAsString(AssetId id, {Encoding encoding = utf8}) async {
120+
// The cache only directly supports utf8, for other encodings get the
121+
// bytes via the cache then convert.
122+
if (encoding == utf8) {
123+
return cache.readAsString(
124+
id,
125+
ifAbsent: () => _pool.withResource(_fileFor(id).readAsString),
126+
);
127+
} else {
128+
return encoding.decode(await readAsBytes(id));
129+
}
130+
}
82131

83132
@override
84133
void deleteSync(AssetId id) {
@@ -146,17 +195,42 @@ class IoFilesystem implements Filesystem {
146195

147196
/// An in-memory [Filesystem].
148197
class InMemoryFilesystem implements Filesystem {
149-
final Map<AssetId, List<int>> assets = {};
198+
@override
199+
FilesystemCache cache;
200+
201+
final Map<AssetId, List<int>> assets;
202+
203+
InMemoryFilesystem({FilesystemCache? cache})
204+
: cache = cache ?? const PassthroughFilesystemCache(),
205+
assets = {};
206+
207+
InMemoryFilesystem._({required this.cache, required this.assets});
150208

151209
@override
152-
Future<bool> exists(AssetId id) async => assets.containsKey(id);
210+
InMemoryFilesystem copyWith({FilesystemCache? cache}) =>
211+
InMemoryFilesystem._(assets: assets, cache: cache ?? this.cache);
153212

154213
@override
155-
Future<Uint8List> readAsBytes(AssetId id) async => assets[id] as Uint8List;
214+
Future<bool> exists(AssetId id) async =>
215+
cache.exists(id, ifAbsent: () async => assets.containsKey(id));
156216

157217
@override
158-
Future<String> readAsString(AssetId id, {Encoding encoding = utf8}) async =>
159-
encoding.decode(assets[id] as Uint8List);
218+
Future<Uint8List> readAsBytes(AssetId id) async =>
219+
cache.readAsBytes(id, ifAbsent: () async => assets[id] as Uint8List);
220+
221+
@override
222+
Future<String> readAsString(AssetId id, {Encoding encoding = utf8}) async {
223+
// The cache only directly supports utf8, for other encodings get the
224+
// bytes via the cache then convert.
225+
if (encoding == utf8) {
226+
return cache.readAsString(
227+
id,
228+
ifAbsent: () async => encoding.decode(assets[id]!),
229+
);
230+
} else {
231+
return encoding.decode(await readAsBytes(id));
232+
}
233+
}
160234

161235
@override
162236
Future<void> delete(AssetId id) async {
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:convert';
7+
import 'dart:typed_data';
8+
9+
import '../asset/id.dart';
10+
import 'lru_cache.dart';
11+
12+
/// Cache for file existence and contents.
13+
abstract interface class FilesystemCache {
14+
/// Clears all [ids] from all caches.
15+
///
16+
/// Waits for any pending reads to complete first.
17+
Future<void> invalidate(Iterable<AssetId> ids);
18+
19+
/// Whether [id] exists.
20+
///
21+
/// Returns a cached result if available, or caches and returns `ifAbsent()`.
22+
Future<bool> exists(AssetId id, {required Future<bool> Function() ifAbsent});
23+
24+
/// Reads [id] as bytes.
25+
///
26+
/// Returns a cached result if available, or caches and returns `ifAbsent()`.
27+
Future<Uint8List> readAsBytes(
28+
AssetId id, {
29+
required Future<Uint8List> Function() ifAbsent,
30+
});
31+
32+
/// Reads [id] as a `String`.
33+
///
34+
/// Returns a cached result if available, or caches and returns `ifAbsent()`.
35+
///
36+
/// The encoding used is always `utf8`. For other encodings, use
37+
/// [readAsBytes].
38+
Future<String> readAsString(
39+
AssetId id, {
40+
required Future<String> Function() ifAbsent,
41+
});
42+
}
43+
44+
/// [FilesystemCache] that always reads from the underlying source.
45+
class PassthroughFilesystemCache implements FilesystemCache {
46+
const PassthroughFilesystemCache();
47+
48+
@override
49+
Future<void> invalidate(Iterable<AssetId> ids) async {}
50+
51+
@override
52+
Future<bool> exists(
53+
AssetId id, {
54+
required Future<bool> Function() ifAbsent,
55+
}) => ifAbsent();
56+
57+
@override
58+
Future<Uint8List> readAsBytes(
59+
AssetId id, {
60+
required Future<Uint8List> Function() ifAbsent,
61+
}) => ifAbsent();
62+
63+
@override
64+
Future<String> readAsString(
65+
AssetId id, {
66+
required Future<String> Function() ifAbsent,
67+
}) => ifAbsent();
68+
}
69+
70+
/// [FilesystemCache] that stores data in memory.
71+
class InMemoryFilesystemCache implements FilesystemCache {
72+
/// Cached results of [readAsBytes].
73+
final _bytesContentCache = LruCache<AssetId, Uint8List>(
74+
1024 * 1024,
75+
1024 * 1024 * 512,
76+
(value) => value.lengthInBytes,
77+
);
78+
79+
/// Pending [readAsBytes] operations.
80+
final _pendingBytesContentCache = <AssetId, Future<Uint8List>>{};
81+
82+
/// Cached results of [exists].
83+
///
84+
/// Don't bother using an LRU cache for this since it's just booleans.
85+
final _canReadCache = <AssetId, Future<bool>>{};
86+
87+
/// Cached results of [readAsString].
88+
///
89+
/// These are computed and stored lazily using [readAsBytes].
90+
///
91+
/// Only files read with [utf8] encoding (the default) will ever be cached.
92+
final _stringContentCache = LruCache<AssetId, String>(
93+
1024 * 1024,
94+
1024 * 1024 * 512,
95+
(value) => value.length,
96+
);
97+
98+
/// Pending `readAsString` operations.
99+
final _pendingStringContentCache = <AssetId, Future<String>>{};
100+
101+
@override
102+
Future<void> invalidate(Iterable<AssetId> ids) async {
103+
// First finish all pending operations, as they will write to the cache.
104+
for (var id in ids) {
105+
await _canReadCache.remove(id);
106+
await _pendingBytesContentCache.remove(id);
107+
await _pendingStringContentCache.remove(id);
108+
}
109+
for (var id in ids) {
110+
_bytesContentCache.remove(id);
111+
_stringContentCache.remove(id);
112+
}
113+
}
114+
115+
@override
116+
Future<bool> exists(
117+
AssetId id, {
118+
required Future<bool> Function() ifAbsent,
119+
}) => _canReadCache.putIfAbsent(id, ifAbsent);
120+
121+
@override
122+
Future<Uint8List> readAsBytes(
123+
AssetId id, {
124+
required Future<Uint8List> Function() ifAbsent,
125+
}) {
126+
var cached = _bytesContentCache[id];
127+
if (cached != null) return Future.value(cached);
128+
129+
return _pendingBytesContentCache.putIfAbsent(id, () async {
130+
final result = await ifAbsent();
131+
_bytesContentCache[id] = result;
132+
unawaited(_pendingBytesContentCache.remove(id));
133+
return result;
134+
});
135+
}
136+
137+
@override
138+
Future<String> readAsString(
139+
AssetId id, {
140+
required Future<String> Function() ifAbsent,
141+
}) async {
142+
var cached = _stringContentCache[id];
143+
if (cached != null) return cached;
144+
145+
return _pendingStringContentCache.putIfAbsent(id, () async {
146+
final result = await ifAbsent();
147+
_stringContentCache[id] = result;
148+
unawaited(_pendingStringContentCache.remove(id));
149+
return result;
150+
});
151+
}
152+
}

build/lib/src/state/reader_state.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@ import '../asset/reader.dart';
66
import 'asset_finder.dart';
77
import 'asset_path_provider.dart';
88
import 'filesystem.dart';
9+
import 'filesystem_cache.dart';
910
import 'input_tracker.dart';
1011

1112
/// Provides access to the state backing an [AssetReader].
1213
extension AssetReaderStateExtension on AssetReader {
14+
/// Returns a new instance with optionally updated [cache].
15+
AssetReader copyWith({FilesystemCache? cache}) {
16+
_requireIsAssetReaderState();
17+
return (this as AssetReaderState).copyWith(cache: cache);
18+
}
19+
1320
Filesystem get filesystem {
1421
_requireIsAssetReaderState();
1522
return (this as AssetReaderState).filesystem;
@@ -52,6 +59,9 @@ extension AssetReaderStateExtension on AssetReader {
5259

5360
/// The state backing an [AssetReader].
5461
abstract interface class AssetReaderState {
62+
/// Returns a new instance with optionally updated [cache].
63+
AssetReader copyWith({FilesystemCache? cache});
64+
5565
/// The [Filesystem] that this reader reads from.
5666
///
5767
/// Warning: this access to the filesystem bypasses reader functionality

0 commit comments

Comments
 (0)