@@ -62,7 +62,7 @@ public func withHeavyCacheGlobalState<T>(isolated: Bool = true, _ body: () throw
6262/// A cache designed for holding few, relatively heavy-weight objects.
6363///
6464/// This cache is specifically designed for holding a limited number of objects (usually less than 100) which are expensive enough to merit particular attention in terms of being purgeable under memory pressure, evictable in-mass, or cached with more complex parameters like time-to-live (TTL).
65- public final class HeavyCache < Key: Hashable & Sendable , Value> : _HeavyCacheBase , KeyValueStorage , @unchecked Sendable {
65+ public final class HeavyCache < Key: Hashable & Sendable , Value: Sendable > : _HeavyCacheBase , KeyValueStorage , @unchecked Sendable {
6666 public typealias HeavyCacheClock = SuspendingClock
6767
6868 /// Controls the non-deterministic eviction policy of the cache. Note that this is distinct from deterministic _pruning_ (due to TTL or size limits).
@@ -74,21 +74,32 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
7474 case `default`( totalCostLimit: Int ? , willEvictCallback: ( @Sendable ( Value ) -> Void ) ? = nil )
7575 }
7676
77- fileprivate final class Entry {
77+ fileprivate final class Entry : Sendable {
78+ /// Empty helper type to prove exclusive access to `accessTime` without storing a mutex for each instance.
79+ struct Witness : ~ Copyable { }
80+
7881 /// The actual value.
7982 let value : Value
8083
8184 /// The last access timestamp.
82- var accessTime : HeavyCacheClock . Instant
85+ private nonisolated ( unsafe ) var accessTime: HeavyCacheClock . Instant
8386
8487 init ( _ value: Value , _ accessTime: HeavyCacheClock . Instant ) {
8588 self . value = value
8689 self . accessTime = accessTime
8790 }
91+
92+ func accessTime( _ Witness: borrowing Witness ) -> HeavyCacheClock . Instant {
93+ self . accessTime
94+ }
95+
96+ func updateAccessTime( _ accessTime: HeavyCacheClock . Instant , _ Witness: borrowing Witness ) {
97+ self . accessTime = accessTime
98+ }
8899 }
89100
90101 /// The lock to protect shared instance state.
91- private let stateLock = SWBMutex ( ( ) )
102+ private let stateLock = SWBMutex ( Entry . Witness ( ) )
92103
93104 /// The underlying cache.
94105 private let _cache : any HeavyCacheImpl < Key , Entry >
@@ -132,7 +143,7 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
132143
133144 deinit {
134145 _timer? . cancel ( )
135- stateLock. withLock {
146+ stateLock. withLock { _ in
136147 for waiter in _expirationWaiters {
137148 waiter. resume ( )
138149 }
@@ -146,12 +157,12 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
146157 ///
147158 /// Due to the implementation details, this may overestimate the number of active items, if some items have been recently evicted.
148159 public var count : Int {
149- return stateLock. withLock { _keys. count }
160+ return stateLock. withLock { _ in _keys. count }
150161 }
151162
152163 /// Clear all items in the cache.
153164 public func removeAll( ) {
154- stateLock. withLock {
165+ stateLock. withLock { _ in
155166 _cache. removeAll ( )
156167 _keys. removeAll ( )
157168 }
@@ -161,35 +172,35 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
161172 ///
162173 /// This function is thread-safe, but may allow computing the value multiple times in case of a race.
163174 public func getOrInsert( _ key: Key , _ body: ( ) throws -> Value ) rethrows -> Value {
164- return try stateLock. withLock {
175+ return try stateLock. withLock { witness in
165176 let entry = try _cache. getOrInsert ( key) {
166177 return Entry ( try body ( ) , currentTime ( ) )
167178 }
168179 _keys. insert ( key)
169- entry. accessTime = currentTime ( )
170- _pruneCache ( )
180+ entry. updateAccessTime ( currentTime ( ) , witness )
181+ _pruneCache ( witness )
171182 return entry. value
172183 }
173184 }
174185
175186 /// Subscript access to the cache.
176187 public subscript( _ key: Key ) -> Value ? {
177188 get {
178- return stateLock. withLock {
189+ return stateLock. withLock { witness in
179190 if let entry = _cache [ key] {
180- entry. accessTime = currentTime ( )
191+ entry. updateAccessTime ( currentTime ( ) , witness )
181192 return entry. value
182193 }
183194 return nil
184195 }
185196 }
186197 set {
187- stateLock. withLock {
198+ stateLock. withLock { witness in
188199 if let newValue {
189200 let entry = Entry ( newValue, currentTime ( ) )
190201 _cache [ key] = entry
191202 _keys. insert ( key)
192- _pruneCache ( )
203+ _pruneCache ( witness )
193204 } else {
194205 _cache. remove ( key)
195206 _keys. remove ( key)
@@ -206,7 +217,7 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
206217 /// Prune the cache following an insert.
207218 ///
208219 /// This method is expected to be called on `queue`.
209- private func _pruneCache( ) {
220+ private func _pruneCache( _ witness : borrowing Entry . Witness ) {
210221 // Enforce the cache maximum size.
211222 guard let max = maximumSize else { return }
212223
@@ -223,7 +234,7 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
223234 _cache. remove ( key)
224235 continue whileLoop
225236 }
226- if oldest == nil || oldest!. entry. accessTime > entry. accessTime {
237+ if oldest == nil || oldest!. entry. accessTime ( witness ) > entry. accessTime ( witness ) {
227238 oldest = ( key, entry)
228239 }
229240 }
@@ -237,7 +248,7 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
237248 /// Prune the cache based on the TTL value.
238249 ///
239250 /// This method is expected to be called on `queue`.
240- private func _pruneForTTL( ) {
251+ private func _pruneForTTL( _ witness : borrowing Entry . Witness ) {
241252 guard let ttl = _timeToLive else { return }
242253
243254 let time = currentTime ( )
@@ -247,7 +258,7 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
247258 keysToRemove. append ( key)
248259 continue
249260 }
250- if time - entry. accessTime > ttl {
261+ if time - entry. accessTime ( witness ) > ttl {
251262 keysToRemove. append ( key)
252263 }
253264 }
@@ -272,9 +283,9 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
272283 return _maximumSize
273284 }
274285 set {
275- stateLock. withLock {
286+ stateLock. withLock { witness in
276287 _maximumSize = newValue
277- _pruneCache ( )
288+ _pruneCache ( witness )
278289 }
279290 }
280291 }
@@ -287,7 +298,7 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
287298 return _timeToLive
288299 }
289300 set {
290- stateLock. withLock {
301+ stateLock. withLock { _ in
291302 _timeToLive = newValue
292303
293304 // Install the TTL timer.
@@ -298,8 +309,8 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
298309 while !Task. isCancelled {
299310 if let self = self {
300311 self . preventExpiration {
301- self . stateLock. withLock {
302- self . _pruneForTTL ( )
312+ self . stateLock. withLock { witness in
313+ self . _pruneForTTL ( witness )
303314 }
304315 }
305316 }
@@ -326,7 +337,7 @@ public final class HeavyCache<Key: Hashable & Sendable, Value>: _HeavyCacheBase,
326337extension HeavyCache {
327338 /// Allows freezing the current time as seen by the object, for TTL pruning testing purposes.
328339 @_spi ( Testing) @discardableResult public func setTime( instant: HeavyCacheClock . Instant ? ) -> HeavyCacheClock . Instant {
329- stateLock. withLock {
340+ stateLock. withLock { _ in
330341 _currentTimeTestingOverride = instant
331342 return instant ?? . now
332343 }
@@ -335,7 +346,7 @@ extension HeavyCache {
335346 /// Waits until the next time pruning for TTL occurs.
336347 @_spi ( Testing) public func waitForExpiration( ) async {
337348 await withCheckedContinuation { continuation in
338- stateLock. withLock {
349+ stateLock. withLock { _ in
339350 _expirationWaiters. append ( continuation)
340351 }
341352 }
0 commit comments