From 0e963547b7da649423c5bb734f0985953c2181f2 Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Sat, 19 Aug 2023 09:06:48 +0200 Subject: [PATCH] Improve cache clearing (#518) * Rewrite cache code to remove outdated entities and not preserve empty cache stores * Add a method to inspect cache contents per client --- lib/src/cache/cache.dart | 72 +++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/lib/src/cache/cache.dart b/lib/src/cache/cache.dart index 038d038b7..540fd2774 100644 --- a/lib/src/cache/cache.dart +++ b/lib/src/cache/cache.dart @@ -22,14 +22,20 @@ class CacheConfig { const CacheConfig({this.maxSize, this.shouldCache}); } +typedef _CacheKey = ({String identifier, Snowflake key}); + +class _CacheEntry { + Object? value; + int accessCount; + + _CacheEntry(this.value) : accessCount = 0; +} + /// A simple cache for [SnowflakeEntity]s. class Cache with MapMixin { - static final Expando>> _stores = Expando('Cache stores'); - static final Expando>> _counts = Expando('Cache counts'); + static final Expando> _stores = Expando('Cache store'); - // References to the backing store for this Cache instance to avoid looking up the client & identifier every time. - late final Map _store = ((_stores[client] ??= {})[identifier] ??= {}) as Map; - late final Map _count = (_counts[client] ??= {})[identifier] ??= {}; + Map<_CacheKey, _CacheEntry> get _store => _stores[client] ??= {}; /// The configuration for this cache. final CacheConfig config; @@ -50,15 +56,15 @@ class Cache with MapMixin { /// Items are retained based on the number of accesses they have until the [CacheConfig.maxSize] /// is respected. void filterItems() { - if (config.maxSize != null && _store.length > config.maxSize!) { - final items = List.of(_store.keys); - items.sort((a, b) => _count[a]!.compareTo(_count[b]!)); + final keys = List.of(_store.keys.where((element) => element.identifier == identifier)); - final overflow = _store.length - config.maxSize!; + if (config.maxSize != null && keys.length > config.maxSize!) { + keys.sort((a, b) => _store[a]!.accessCount.compareTo(_store[b]!.accessCount)); - for (final item in items.take(overflow)) { - _store.remove(item); - _count.remove(item); + final overflow = keys.length - config.maxSize!; + + for (final key in keys.take(overflow)) { + _store.remove(key); } } } @@ -83,11 +89,15 @@ class Cache with MapMixin { assert(value is! ManagedSnowflakeEntity || value.id == key, 'Mismatched entity key in cache'); if (config.shouldCache?.call(value) == false) { + remove(key); return; } - _store[key] = value; - _count[key] ??= 0; + _store.update( + (identifier: identifier, key: key), + (entry) => entry..value = value, + ifAbsent: () => _CacheEntry(value), + ); scheduleFilterItems(); } @@ -98,26 +108,42 @@ class Cache with MapMixin { return null; } - final value = _store[key]; - if (value != null) { - _count.update(key, (value) => value + 1); + final entry = _store[(identifier: identifier, key: key)]; + if (entry == null) { + return null; } - return value; + + entry.accessCount++; + return entry.value as T; } @override void clear() { - _store.clear(); - _count.clear(); + _store.removeWhere((key, value) => key.identifier == identifier); } @override - Iterable get keys => _store.keys; + Iterable get keys => _store.keys.where((element) => element.identifier == identifier).map((e) => e.key); @override T? remove(Object? key) { - _count.remove(key); - return _store.remove(key); + return _store.remove((identifier: identifier, key: key))?.value as T?; + } + + /// Return a mapping of identifier to cache contents for all caches associated with [client]. + static Map> cachesFor(Nyxx client) { + final store = _stores[client]; + if (store == null) { + return {}; + } + + final result = >{}; + + for (final entry in store.entries) { + (result[entry.key.identifier] ??= {})[entry.key.key] = entry.value.value; + } + + return result; } }