Skip to content

Commit ed8075f

Browse files
authored
Add back write caching. (#4001)
* Add back write caching. * Address review comments. * Address review comments.
1 parent 6f4c629 commit ed8075f

20 files changed

+577
-319
lines changed

_test_common/lib/runner_asset_writer_spy.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,4 @@ class RunnerAssetWriterSpy extends AssetWriterSpy implements RunnerAssetWriter {
2323

2424
@override
2525
Future<void> deleteDirectory(AssetId id) => _delegate.deleteDirectory(id);
26-
27-
@override
28-
Future<void> completeBuild() async {}
2926
}

_test_common/lib/test_environment.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,12 @@ class TestBuildEnvironment implements BuildEnvironment {
7070
BuildEnvironment copyWith({
7171
void Function(LogRecord)? onLogOverride,
7272
RunnerAssetWriter? writer,
73+
AssetReader? reader,
7374
}) => TestBuildEnvironment(
74-
readerWriter: (writer as TestReaderWriter?) ?? _readerWriter,
75+
readerWriter:
76+
(writer as TestReaderWriter?) ??
77+
(reader as TestReaderWriter?) ??
78+
_readerWriter,
7579
throwOnPrompt: throwOnPrompt,
7680
);
7781

build/lib/src/state/filesystem_cache.dart

Lines changed: 146 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66
import 'dart:convert';
77
import 'dart:typed_data';
88

9+
import '../../build.dart';
910
import '../asset/id.dart';
1011
import 'lru_cache.dart';
1112

@@ -16,6 +17,9 @@ abstract interface class FilesystemCache {
1617
/// Clears all [ids] from all caches.
1718
void invalidate(Iterable<AssetId> ids);
1819

20+
/// Flushes pending writes and deletes.
21+
void flush();
22+
1923
/// Whether [id] exists.
2024
///
2125
/// Returns a cached result if available, or caches and returns `ifAbsent()`.
@@ -26,6 +30,17 @@ abstract interface class FilesystemCache {
2630
/// Returns a cached result if available, or caches and returns `ifAbsent()`.
2731
Uint8List readAsBytes(AssetId id, {required Uint8List Function() ifAbsent});
2832

33+
/// Writes [contents] to [id].
34+
///
35+
/// [writer] is a function that does the actual write. If this cache does
36+
/// write caching, it is not called until [flush], and might not be called at
37+
/// all if another write to the same asset happens first.
38+
void writeAsBytes(
39+
AssetId id,
40+
List<int> contents, {
41+
required void Function() writer,
42+
});
43+
2944
/// Reads [id] as a `String`.
3045
///
3146
/// Returns a cached result if available, or caches and returns `ifAbsent()`.
@@ -34,6 +49,25 @@ abstract interface class FilesystemCache {
3449
Encoding encoding = utf8,
3550
required Uint8List Function() ifAbsent,
3651
});
52+
53+
/// Writes [contents] to [id].
54+
///
55+
/// [writer] is a function that does the actual write. If this cache does
56+
/// write caching, it is not called until [flush], and might not be called at
57+
/// all if another write to the same asset happens first.
58+
void writeAsString(
59+
AssetId id,
60+
String contents, {
61+
Encoding encoding = utf8,
62+
required void Function() writer,
63+
});
64+
65+
/// Deletes [id].
66+
///
67+
/// [deleter] is a function that does the actual delete. If this cache does
68+
/// write caching, it is not called until [flush], and might not be called at
69+
/// all if another write to the same asset happens first.
70+
void delete(AssetId id, {required void Function() deleter});
3771
}
3872

3973
/// [FilesystemCache] that always reads from the underlying source.
@@ -43,19 +77,40 @@ class PassthroughFilesystemCache implements FilesystemCache {
4377
@override
4478
Future<void> invalidate(Iterable<AssetId> ids) async {}
4579

80+
@override
81+
void flush() {}
82+
4683
@override
4784
bool exists(AssetId id, {required bool Function() ifAbsent}) => ifAbsent();
4885

4986
@override
5087
Uint8List readAsBytes(AssetId id, {required Uint8List Function() ifAbsent}) =>
5188
ifAbsent();
5289

90+
@override
91+
void writeAsBytes(
92+
AssetId id,
93+
List<int> contents, {
94+
required void Function() writer,
95+
}) => writer();
96+
5397
@override
5498
String readAsString(
5599
AssetId id, {
56100
Encoding encoding = utf8,
57101
required Uint8List Function() ifAbsent,
58102
}) => encoding.decode(ifAbsent());
103+
104+
@override
105+
void writeAsString(
106+
AssetId id,
107+
String contents, {
108+
Encoding encoding = utf8,
109+
required void Function() writer,
110+
}) => writer();
111+
112+
@override
113+
void delete(AssetId id, {required void Function() deleter}) => deleter();
59114
}
60115

61116
/// [FilesystemCache] that stores data in memory.
@@ -83,8 +138,13 @@ class InMemoryFilesystemCache implements FilesystemCache {
83138
(value) => value.length,
84139
);
85140

141+
final _pendingWrites = <AssetId, _PendingWrite>{};
142+
86143
@override
87144
Future<void> invalidate(Iterable<AssetId> ids) async {
145+
if (_pendingWrites.isNotEmpty) {
146+
throw StateError("Can't invalidate while there are pending writes.");
147+
}
88148
for (var id in ids) {
89149
_existsCache.remove(id);
90150
_bytesContentCache.remove(id);
@@ -93,11 +153,30 @@ class InMemoryFilesystemCache implements FilesystemCache {
93153
}
94154

95155
@override
96-
bool exists(AssetId id, {required bool Function() ifAbsent}) =>
97-
_existsCache.putIfAbsent(id, ifAbsent);
156+
void flush() {
157+
for (final write in _pendingWrites.values) {
158+
write.writer();
159+
}
160+
_pendingWrites.clear();
161+
}
162+
163+
@override
164+
bool exists(AssetId id, {required bool Function() ifAbsent}) {
165+
final maybePendingWrite = _pendingWrites[id];
166+
if (maybePendingWrite != null) {
167+
return !maybePendingWrite.isDelete;
168+
}
169+
return _existsCache.putIfAbsent(id, ifAbsent);
170+
}
98171

99172
@override
100173
Uint8List readAsBytes(AssetId id, {required Uint8List Function() ifAbsent}) {
174+
final maybePendingWrite = _pendingWrites[id];
175+
if (maybePendingWrite != null) {
176+
// Throws if it's a delete; callers should check [exists] before reading.
177+
return maybePendingWrite.bytes!;
178+
}
179+
101180
final maybeResult = _bytesContentCache[id];
102181
if (maybeResult != null) return maybeResult;
103182

@@ -106,27 +185,87 @@ class InMemoryFilesystemCache implements FilesystemCache {
106185
return result;
107186
}
108187

188+
@override
189+
void writeAsBytes(
190+
AssetId id,
191+
List<int> contents, {
192+
required void Function() writer,
193+
}) {
194+
_stringContentCache.remove(id);
195+
_writeBytes(id, contents, writer: writer);
196+
}
197+
198+
void _writeBytes(
199+
AssetId id,
200+
List<int> contents, {
201+
required void Function() writer,
202+
}) {
203+
final uint8ListContents =
204+
contents is Uint8List ? contents : Uint8List.fromList(contents);
205+
_bytesContentCache[id] = uint8ListContents;
206+
_existsCache[id] = true;
207+
_pendingWrites[id] = _PendingWrite(
208+
writer: writer,
209+
bytes: uint8ListContents,
210+
);
211+
}
212+
109213
@override
110214
String readAsString(
111215
AssetId id, {
112216
Encoding encoding = utf8,
113217
required Uint8List Function() ifAbsent,
114218
}) {
219+
// Encodings other than utf8 do not use `_stringContentCache`. Read as
220+
// bytes then convert, instead.
115221
if (encoding != utf8) {
116222
final bytes = readAsBytes(id, ifAbsent: ifAbsent);
117223
return encoding.decode(bytes);
118224
}
119225

226+
// Check `_stringContentCache` first to use it as a cache for conversion of
227+
// bytes from _pendingWrites.
120228
final maybeResult = _stringContentCache[id];
121229
if (maybeResult != null) return maybeResult;
122230

123-
var bytes = _bytesContentCache[id];
124-
if (bytes == null) {
125-
bytes = ifAbsent();
126-
_bytesContentCache[id] = bytes;
127-
}
231+
final bytes = readAsBytes(id, ifAbsent: ifAbsent);
128232
final result = utf8.decode(bytes);
129233
_stringContentCache[id] = result;
130234
return result;
131235
}
236+
237+
@override
238+
void writeAsString(
239+
AssetId id,
240+
String contents, {
241+
Encoding encoding = utf8,
242+
required void Function() writer,
243+
}) {
244+
// Encodings other than utf8 do not use `_stringContentCache`.
245+
if (encoding == utf8) {
246+
_stringContentCache[id] = contents;
247+
} else {
248+
_stringContentCache.remove(id);
249+
}
250+
final bytes = encoding.encode(contents);
251+
_writeBytes(id, bytes, writer: writer);
252+
}
253+
254+
@override
255+
void delete(AssetId id, {required void Function() deleter}) {
256+
_stringContentCache.remove(id);
257+
_bytesContentCache.remove(id);
258+
_existsCache[id] = false;
259+
_pendingWrites[id] = _PendingWrite(writer: deleter);
260+
}
261+
}
262+
263+
/// The data that will be written on flush; used for reads before flush.
264+
class _PendingWrite {
265+
final void Function() writer;
266+
final Uint8List? bytes;
267+
268+
_PendingWrite({required this.writer, this.bytes});
269+
270+
bool get isDelete => bytes == null;
132271
}

0 commit comments

Comments
 (0)