Skip to content

Commit 8cb2c91

Browse files
committed
fix: 2.1.2
1 parent 603e5b6 commit 8cb2c91

6 files changed

Lines changed: 133 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 2.1.2
2+
3+
- Optimize cache entry removal performance.
4+
> Previously `removeWhere` was used which caused iteration of whole cache set
5+
> on every removal.
6+
> Now additional `Expando` is used to map Weak references to their keys.
7+
18
## 2.1.1
29

310
- Fix web release target compilation.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Weak cache is a `Map` implementation that uses `WeakReference`s for holding
44
values and `Finalizer` to manage it's storage.
55

6-
You can use it to cache data for a small amount of time until next garbage
6+
You can use this to cache data for a small amount of time until next garbage
77
collection cycle.
88

99
> Note: Values cannot be numbers, strings, booleans, records, `null`,
@@ -18,13 +18,13 @@ collection cycle.
1818
> references, to prevent concurrent edit of storage, while iterating over it.
1919
* Optimized `containsValue` via internal managed `Expando`.
2020
* Implements full `Map<K, V>` interface.
21-
* WeakCache itself can be safely garbage collected and doesn't hold unto any
22-
stored.
21+
* `WeakCache` itself can be safely garbage collected and doesn't hold unto any
22+
stored data.
2323

2424
## Usage
2525

26-
Create cache, add values, and they'll be removed when all other strong
27-
references to them are lost.
26+
Create cache, add values, and they'll be removed once there no more strong
27+
references to them.
2828

2929
```dart
3030
// ID - Object cache

lib/src/cache_mutex.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ class CacheMutex {
2424
/// Lock mutex.
2525
void lock() {
2626
debugPrint('CacheMutex.lock ($_locksCount + 1)');
27-
_locksCount++;
28-
if (_locksCount == 1)
27+
if (++_locksCount == 1)
2928
onLock?.call();
3029
}
3130

@@ -35,8 +34,7 @@ class CacheMutex {
3534
if (!isLocked)
3635
return;
3736

38-
_locksCount--;
39-
if (_locksCount == 0)
37+
if (--_locksCount == 0)
4038
onUnlock?.call();
4139
}
4240
}

lib/src/cache_state.dart

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ typedef CacheStateSnapshot<K, V extends Object> = ({
88
Map<K, WeakReference<V>> cache,
99
ListQueue<WeakReference<V>> removeQueue,
1010
Expando<bool> containsExpando,
11+
Expando<(K,)> referenceKeys,
1112
});
1213

1314
/// Aggregation of [CacheState] main properties with details
@@ -33,9 +34,11 @@ class CacheState<K, V extends Object> {
3334
final cache = <K, WeakReference<V>>{};
3435
/// Queue for delayed concurrent modifications.
3536
final removeQueue = ListQueue<WeakReference<V>>();
36-
/// [Expando] used to optimize `containsValue` calls and bypass using
37+
/// [Expando] used to optimize `containsValue` calls and bypass usage
3738
/// of iteration.
3839
final containsExpando = Expando<bool>();
40+
/// [Expando] used to optimize `remove` calls and bypass usage of iteration.
41+
final referenceKeys = Expando<(K,)>();
3942

4043
/// [Finalizer] that manages [cache].
4144
static final cacheFinalizer = Finalizer<CacheStateSnapshotWithReference<Object?, Object>>(
@@ -54,28 +57,31 @@ class CacheState<K, V extends Object> {
5457
) = argument;
5558
if (mutex?.isLocked != true)
5659
return removeCacheEntryStatic(argument);
57-
removeQueue.addLast(reference);
60+
return removeQueue.addLast(reference);
5861
}
5962

6063
/// Removes cache entry from [cache] table and detach finalizer form it.
6164
static void removeCacheEntryStatic<K, V extends Object>(
6265
CacheStateSnapshotWithReference<K, V> argument,
6366
) {
6467
final CacheStateSnapshotWithReference(
65-
snapshot: CacheStateSnapshot(:cache, :containsExpando),
68+
snapshot: CacheStateSnapshot(:cache, :containsExpando, :referenceKeys),
6669
:reference,
6770
) = argument;
6871
cacheFinalizer.detach(reference);
72+
if (referenceKeys[reference] case (final key,) when cache[key] == reference)
73+
cache.remove(key);
74+
referenceKeys[reference] = null;
6975
if (reference.target case final target?)
7076
containsExpando[target] = null;
71-
cache.removeWhere((key, value) => value == reference);
7277
}
7378

7479
/// Get aggregation of [CacheState] main properties.
7580
CacheStateSnapshot<K, V> get snapshot => (
7681
cache: cache,
7782
removeQueue: removeQueue,
7883
containsExpando: containsExpando,
84+
referenceKeys: referenceKeys,
7985
);
8086

8187
/// Create aggregation of [CacheState] properties required for value deletion.
@@ -99,6 +105,7 @@ class CacheState<K, V extends Object> {
99105
}
100106
containsExpando[value] = true;
101107
final reference = cache[key] = WeakReference<V>(value);
108+
referenceKeys[reference] = (key,);
102109
cacheFinalizer.attach(
103110
value,
104111
makeSnapshotWithReference(reference),
@@ -113,6 +120,7 @@ class CacheState<K, V extends Object> {
113120
return null;
114121
}
115122
cacheFinalizer.detach(reference);
123+
referenceKeys[reference] = null;
116124
if (reference.target case final target?)
117125
containsExpando[target] = null;
118126
return reference.target;

pubspec.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: weak_cache
22
description: >
33
Weak cache that uses weak references for holding values.
44
Implements full Map interface including keys and values iteration.
5-
version: 2.1.1
5+
version: 2.1.2
66
homepage: https://github.com/Zekfad/weak_cache
77
repository: https://github.com/Zekfad/weak_cache
88
issue_tracker: https://github.com/Zekfad/weak_cache/issues
@@ -18,5 +18,5 @@ dependencies:
1818
meta: ^1.9.1
1919

2020
dev_dependencies:
21-
test: ^1.25.2
22-
zekfad_lints: ^2.0.0
21+
test: ^1.25.7
22+
zekfad_lints: ^2.1.0

test/weak_cache_test.dart

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ Future<void> parallelLoops(
1111
}
1212
) => Future.wait([
1313
for (final function in functions)
14-
Future.doWhile(() async {
15-
await Future<void>.delayed(delay);
16-
return function();
17-
}),
14+
Future.doWhile(() async => Future.delayed(delay, function)),
1815
]);
1916

2017
class _Int {
@@ -29,6 +26,109 @@ void main() {
2926
kDebug = true;
3027

3128
group('Test Weak Cache', () {
29+
test('updating key should not interfere removal', () async {
30+
final events = <String>[];
31+
32+
final cache = WeakCache<int, Object>();
33+
34+
// Create some objects
35+
Object? someObject = Object();
36+
Object? someObject2 = Object();
37+
Object? someObject3 = Object();
38+
39+
final _someObjectRef = WeakReference(someObject);
40+
final _someObject2Ref = WeakReference(someObject2);
41+
final _someObject3Ref = WeakReference(someObject3);
42+
43+
// Add them to cache
44+
cache[0] = someObject;
45+
cache[1] = someObject2;
46+
47+
final _someObjectRefDone = expectAsync0<bool>(() {
48+
events.add('some object');
49+
return false;
50+
});
51+
52+
final _someObject2RefDone = expectAsync0<bool>(() {
53+
events.add('some object 2');
54+
return false;
55+
});
56+
57+
final _someObject3RefDone = expectAsync0<bool>(() {
58+
events.add('some object 3');
59+
return false;
60+
});
61+
62+
expect(identical(cache[0], someObject), isTrue);
63+
expect(identical(cache[0], _someObjectRef.target), isTrue);
64+
expect(identical(cache[1], someObject2), isTrue);
65+
expect(identical(cache[1], _someObject2Ref.target), isTrue);
66+
67+
someObject = null;
68+
someObject2 = null;
69+
70+
cache[1] = someObject3;
71+
72+
expect(identical(cache[1], someObject3), isTrue);
73+
expect(identical(cache[1], _someObject3Ref.target), isTrue);
74+
75+
await parallelLoops([
76+
() {
77+
// ignore: unused_local_variable
78+
final bigChunkOfData = '0' * 1024 * 1024 * 50; // about 50mb of data.
79+
if (_someObjectRef.target != null)
80+
return true;
81+
return _someObjectRefDone();
82+
},
83+
() {
84+
// ignore: unused_local_variable
85+
final bigChunkOfData = '0' * 1024 * 1024 * 50; // about 50mb of data.
86+
if (_someObject2Ref.target != null)
87+
return true;
88+
return _someObject2RefDone();
89+
},
90+
]);
91+
92+
// 0 is null
93+
expect(cache[0], isNull);
94+
expect(_someObjectRef.target, isNull);
95+
// 1 is obj3
96+
expect(cache[1], isNotNull);
97+
expect(identical(cache[1], _someObject3Ref.target), isTrue);
98+
// obj2 is deleted
99+
expect(_someObject2Ref.target, isNull);
100+
expect(
101+
events,
102+
unorderedEquals([
103+
'some object',
104+
'some object 2',
105+
]),
106+
);
107+
108+
someObject3 = null;
109+
110+
await parallelLoops([
111+
() {
112+
// ignore: unused_local_variable
113+
final bigChunkOfData = '0' * 1024 * 1024 * 50; // about 50mb of data.
114+
if (_someObject2Ref.target != null)
115+
return true;
116+
return _someObject3RefDone();
117+
},
118+
]);
119+
120+
expect(cache[1], isNull);
121+
expect(_someObject3Ref.target, isNull);
122+
expect(
123+
events,
124+
unorderedEquals([
125+
'some object',
126+
'some object 2',
127+
'some object 3',
128+
]),
129+
);
130+
});
131+
32132
test('held objects should be garbage collected and removed from cache', () async {
33133
final events = <String>[];
34134

0 commit comments

Comments
 (0)