Skip to content

Commit 016caae

Browse files
committed
Add back write caching.
1 parent c0f78f3 commit 016caae

File tree

16 files changed

+461
-309
lines changed

16 files changed

+461
-309
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
}

build/lib/src/state/filesystem_cache.dart

Lines changed: 145 additions & 2 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 write. 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,6 +185,24 @@ class InMemoryFilesystemCache implements FilesystemCache {
106185
return result;
107186
}
108187

188+
@override
189+
@override
190+
void writeAsBytes(
191+
AssetId id,
192+
List<int> contents, {
193+
required void Function() writer,
194+
}) {
195+
_stringContentCache.remove(id);
196+
final uint8ListContents =
197+
contents is Uint8List ? contents : Uint8List.fromList(contents);
198+
_bytesContentCache[id] = uint8ListContents;
199+
_existsCache[id] = true;
200+
_pendingWrites[id] = _PendingWrite(
201+
writer: writer,
202+
bytes: uint8ListContents,
203+
);
204+
}
205+
109206
@override
110207
String readAsString(
111208
AssetId id, {
@@ -117,9 +214,19 @@ class InMemoryFilesystemCache implements FilesystemCache {
117214
return encoding.decode(bytes);
118215
}
119216

217+
// Check _stringContentCache first to use it as a cache for conversion of
218+
// bytes from _pendingWrites.
120219
final maybeResult = _stringContentCache[id];
121220
if (maybeResult != null) return maybeResult;
122221

222+
final maybePendingWrite = _pendingWrites[id];
223+
if (maybePendingWrite != null) {
224+
// Throws if it's a delete; callers should check [exists] before reading.
225+
final converted = utf8.decode(maybePendingWrite.bytes!);
226+
_stringContentCache[id] = converted;
227+
return converted;
228+
}
229+
123230
var bytes = _bytesContentCache[id];
124231
if (bytes == null) {
125232
bytes = ifAbsent();
@@ -129,4 +236,40 @@ class InMemoryFilesystemCache implements FilesystemCache {
129236
_stringContentCache[id] = result;
130237
return result;
131238
}
239+
240+
@override
241+
void writeAsString(
242+
AssetId id,
243+
String contents, {
244+
Encoding encoding = utf8,
245+
required void Function() writer,
246+
}) {
247+
_stringContentCache[id] = contents;
248+
_bytesContentCache.remove(id);
249+
_existsCache[id] = true;
250+
251+
final encoded = encoding.encode(contents);
252+
_pendingWrites[id] = _PendingWrite(
253+
writer: writer,
254+
bytes: encoded is Uint8List ? encoded : Uint8List.fromList(encoded),
255+
);
256+
}
257+
258+
@override
259+
void delete(AssetId id, {required void Function() deleter}) {
260+
_stringContentCache.remove(id);
261+
_bytesContentCache.remove(id);
262+
_existsCache[id] = false;
263+
_pendingWrites[id] = _PendingWrite(writer: deleter);
264+
}
265+
}
266+
267+
/// The data that will be written on flush; used for reads before flush.
268+
class _PendingWrite {
269+
final void Function() writer;
270+
final Uint8List? bytes;
271+
272+
_PendingWrite({required this.writer, this.bytes});
273+
274+
bool get isDelete => bytes == null;
132275
}

build/test/state/filesystem_cache_test.dart

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,43 @@ void main() {
4545
isFalse /* updated value */,
4646
);
4747
});
48+
49+
test('reads from latest writeAsBytes without calling ifAbsent', () async {
50+
cache.writeAsBytes(
51+
txt1,
52+
txt1Bytes,
53+
writer: () => throw UnimplementedError(),
54+
);
55+
expect(
56+
cache.exists(txt1, ifAbsent: () => throw UnimplementedError()),
57+
true,
58+
);
59+
});
60+
61+
test('reads from writeAsString without calling ifAbsent', () async {
62+
cache.writeAsString(
63+
txt1,
64+
txt1String,
65+
writer: () => throw UnimplementedError(),
66+
);
67+
expect(
68+
cache.exists(txt1, ifAbsent: () => throw UnimplementedError()),
69+
true,
70+
);
71+
});
72+
73+
test('reads from delete without calling ifAbsent', () async {
74+
cache.writeAsString(
75+
txt1,
76+
txt1String,
77+
writer: () => throw UnimplementedError(),
78+
);
79+
cache.delete(txt1, deleter: () => throw UnimplementedError());
80+
expect(
81+
cache.exists(txt1, ifAbsent: () => throw UnimplementedError()),
82+
false,
83+
);
84+
});
4885
});
4986

5087
group('readAsBytes', () {
@@ -68,6 +105,30 @@ void main() {
68105
txt2Bytes /* updated value */,
69106
);
70107
});
108+
109+
test('reads from latest writeAsBytes without calling ifAbsent', () async {
110+
cache.writeAsBytes(
111+
txt1,
112+
txt1Bytes,
113+
writer: () => throw UnimplementedError(),
114+
);
115+
expect(
116+
cache.readAsBytes(txt1, ifAbsent: () => throw UnimplementedError()),
117+
txt1Bytes,
118+
);
119+
});
120+
121+
test('reads from writeAsString without calling ifAbsent', () async {
122+
cache.writeAsString(
123+
txt1,
124+
txt1String,
125+
writer: () => throw UnimplementedError(),
126+
);
127+
expect(
128+
cache.readAsBytes(txt1, ifAbsent: () => throw UnimplementedError()),
129+
txt1Bytes,
130+
);
131+
});
71132
});
72133

73134
group('readAsString', () {
@@ -92,4 +153,75 @@ void main() {
92153
);
93154
});
94155
});
156+
157+
test('reads from latest writeAsBytes without calling ifAbsent', () async {
158+
cache.writeAsBytes(
159+
txt1,
160+
txt1Bytes,
161+
writer: () => throw UnimplementedError(),
162+
);
163+
expect(
164+
cache.readAsString(txt1, ifAbsent: () => throw UnimplementedError()),
165+
txt1String,
166+
);
167+
});
168+
169+
test('reads from writeAsString without calling ifAbsent', () async {
170+
cache.writeAsString(
171+
txt1,
172+
txt1String,
173+
writer: () => throw UnimplementedError(),
174+
);
175+
expect(
176+
cache.readAsString(txt1, ifAbsent: () => throw UnimplementedError()),
177+
txt1String,
178+
);
179+
});
180+
181+
group('writeAsBytes', () {
182+
test('writes on flush', () async {
183+
var written = false;
184+
cache.writeAsBytes(
185+
txt1,
186+
txt1Bytes,
187+
writer: () {
188+
written = true;
189+
},
190+
);
191+
expect(written, isFalse);
192+
cache.flush();
193+
expect(written, isTrue);
194+
});
195+
});
196+
197+
group('writeAsString', () {
198+
test('writes on flush', () async {
199+
var written = false;
200+
cache.writeAsString(
201+
txt1,
202+
txt1String,
203+
writer: () {
204+
written = true;
205+
},
206+
);
207+
expect(written, isFalse);
208+
cache.flush();
209+
expect(written, isTrue);
210+
});
211+
});
212+
213+
group('delete', () {
214+
test('deletes on flush', () async {
215+
var deleted = false;
216+
cache.delete(
217+
txt1,
218+
deleter: () {
219+
deleted = true;
220+
},
221+
);
222+
expect(deleted, isFalse);
223+
cache.flush();
224+
expect(deleted, isTrue);
225+
});
226+
});
95227
}

0 commit comments

Comments
 (0)