-
Notifications
You must be signed in to change notification settings - Fork 214
Refactor CachingAssetReader
to FilesystemCache
.
#3869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | ||
// for details. All rights reserved. Use of this source code is governed by a | ||
// BSD-style license that can be found in the LICENSE file. | ||
|
||
import 'dart:async'; | ||
import 'dart:convert'; | ||
import 'dart:typed_data'; | ||
|
||
import '../asset/id.dart'; | ||
import 'lru_cache.dart'; | ||
|
||
/// Cache for file existence and contents. | ||
/// | ||
/// 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<void> invalidate(Iterable<AssetId> ids); | ||
|
||
/// Whether [id] exists. | ||
/// | ||
/// Returns a cached result if available, or caches and returns `ifAbsent()`. | ||
Future<bool> exists(AssetId id, {required Future<bool> Function() ifAbsent}); | ||
|
||
/// Reads [id] as bytes. | ||
/// | ||
/// Returns a cached result if available, or caches and returns `ifAbsent()`. | ||
Future<Uint8List> readAsBytes( | ||
AssetId id, { | ||
required Future<Uint8List> Function() ifAbsent, | ||
}); | ||
|
||
/// Reads [id] as a `String`. | ||
/// | ||
/// Returns a cached result if available, or caches and returns `ifAbsent()`. | ||
/// | ||
/// The encoding used is always `utf8`. For other encodings, use | ||
/// [readAsBytes]. | ||
Future<String> readAsString( | ||
AssetId id, { | ||
required Future<String> Function() ifAbsent, | ||
}); | ||
} | ||
|
||
/// [FilesystemCache] that always reads from the underlying source. | ||
class PassthroughFilesystemCache implements FilesystemCache { | ||
const PassthroughFilesystemCache(); | ||
|
||
@override | ||
Future<void> invalidate(Iterable<AssetId> ids) async {} | ||
|
||
@override | ||
Future<bool> exists( | ||
AssetId id, { | ||
required Future<bool> Function() ifAbsent, | ||
}) => ifAbsent(); | ||
|
||
@override | ||
Future<Uint8List> readAsBytes( | ||
AssetId id, { | ||
required Future<Uint8List> Function() ifAbsent, | ||
}) => ifAbsent(); | ||
|
||
@override | ||
Future<String> readAsString( | ||
AssetId id, { | ||
required Future<String> Function() ifAbsent, | ||
}) => ifAbsent(); | ||
} | ||
|
||
/// [FilesystemCache] that stores data in memory. | ||
class InMemoryFilesystemCache implements FilesystemCache { | ||
/// Cached results of [readAsBytes]. | ||
final _bytesContentCache = LruCache<AssetId, Uint8List>( | ||
1024 * 1024, | ||
1024 * 1024 * 512, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these numbers copied from somewhere? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it's copied. Added a TODO to look into the caching strategy; I suspect we can do something simpler. It would be surprising if we can't fit one build's worth of input in memory ... particularly given that when running with the analyzer, there is exactly another copy of all the inputs in the in-memory filesystem we give to the analyzer :) Thanks. |
||
(value) => value.lengthInBytes, | ||
); | ||
|
||
/// Pending [readAsBytes] operations. | ||
final _pendingBytesContentCache = <AssetId, Future<Uint8List>>{}; | ||
|
||
/// Cached results of [exists]. | ||
/// | ||
/// Don't bother using an LRU cache for this since it's just booleans. | ||
final _canReadCache = <AssetId, Future<bool>>{}; | ||
|
||
/// Cached results of [readAsString]. | ||
/// | ||
/// These are computed and stored lazily using [readAsBytes]. | ||
/// | ||
/// Only files read with [utf8] encoding (the default) will ever be cached. | ||
final _stringContentCache = LruCache<AssetId, String>( | ||
1024 * 1024, | ||
1024 * 1024 * 512, | ||
(value) => value.length, | ||
); | ||
|
||
/// Pending `readAsString` operations. | ||
final _pendingStringContentCache = <AssetId, Future<String>>{}; | ||
|
||
@override | ||
Future<void> invalidate(Iterable<AssetId> 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) { | ||
_bytesContentCache.remove(id); | ||
_stringContentCache.remove(id); | ||
} | ||
} | ||
|
||
@override | ||
Future<bool> exists( | ||
AssetId id, { | ||
required Future<bool> Function() ifAbsent, | ||
}) => _canReadCache.putIfAbsent(id, ifAbsent); | ||
|
||
@override | ||
Future<Uint8List> readAsBytes( | ||
AssetId id, { | ||
required Future<Uint8List> 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; | ||
}); | ||
} | ||
|
||
@override | ||
Future<String> readAsString( | ||
AssetId id, { | ||
required Future<String> Function() ifAbsent, | ||
}) async { | ||
var cached = _stringContentCache[id]; | ||
if (cached != null) return cached; | ||
|
||
return _pendingStringContentCache.putIfAbsent(id, () async { | ||
final result = await ifAbsent(); | ||
_stringContentCache[id] = result; | ||
unawaited(_pendingStringContentCache.remove(id)); | ||
return result; | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here - and below - it talks about a specific implementation. Isn't that weird to have as documentation for the interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit awkward :) while writing the next PR I realized it makes more sense to leave
Filesystem
always uncached and do the caching one level up in the reader. So that's the plan.As the code is I do think it makes sense on the interface, because exactly how the cache is used has visible side effects. Which is also why it makes sense to move it out, it makes this type do too much :)