22// ObjectCache.swift
33// OpenSwiftUICore
44//
5- // Audited for 6.0.87
5+ // Audited for 6.5.4
66// Status: Complete
7- // ID: FCB2944DC319042A861E82C8B244E212
7+ // ID: FCB2944DC319042A861E82C8B244E212 (SwiftUICore)
88
9+ /// A thread-safe cache that stores key-value pairs with automatic eviction.
10+ ///
11+ /// `ObjectCache` implements a set-associative cache with LRU (Least Recently Used)
12+ /// eviction policy. When a bucket is full and a new item needs to be inserted, the least
13+ /// recently used item in that bucket is evicted.
14+ ///
15+ /// For example:
16+ ///
17+ /// let cache = ObjectCache<String, ExpensiveObject> { key in
18+ /// ExpensiveObject(key: key)
19+ /// }
20+ ///
21+ /// let value = cache["myKey"]
22+ ///
923final package class ObjectCache < Key, Value> where Key: Hashable {
24+
25+ /// The constructor function used to create new values for cache misses.
1026 let constructor : ( Key ) -> Value
11-
27+
28+ /// The internal cache data structure, protected by atomic access.
1229 @AtomicBox
1330 private var data : Data
14-
31+
32+ /// Creates a new cache with the specified constructor function.
33+ ///
34+ /// - Parameter constructor: A closure that creates a value for a given key.
35+ /// This closure is called when a key is accessed but not found in the cache.
1536 @inlinable
1637 package init ( constructor: @escaping ( Key ) -> Value ) {
1738 self . constructor = constructor
1839 self . data = Data ( )
1940 }
20-
41+
42+ /// Accesses the value associated with the given key.
43+ ///
44+ /// If the key exists in the cache, returns the cached value and updates its
45+ /// access time. If the key doesn't exist, calls the constructor to create a
46+ /// new value, stores it in the cache (potentially evicting the least recently
47+ /// used item in the same bucket), and returns the new value.
48+ ///
49+ /// - Parameter key: The key to look up.
50+ /// - Returns: The value associated with the key, either from cache or newly constructed.
2151 final package subscript( key: Key ) -> Value {
2252 let hash = key. hashValue
23- let bucket = ( hash & ( ( 1 << 3 ) - 1 ) ) << 2
53+ let bucket = ( hash & ( Data . bucketCount - 1 ) ) * Data . waysPerBucket
2454 var targetOffset : Int = 0
2555 var diff : Int32 = Int32 . min
2656 let value = $data. access { data -> Value ? in
27- for offset in 0 ..< 3 {
57+ for offset in 0 ..< Data . waysPerBucket {
2858 let index = bucket + offset
2959 if let itemData = data. table [ index] . data {
3060 if itemData. hash == hash, itemData. key == key {
3161 data. clock &+= 1
3262 data. table [ index] . used = data. clock
3363 return itemData. value
3464 } else {
35- if diff < Int32 ( bitPattern: data. clock &- data. table [ index] . used) {
65+ let dist = Int32 ( bitPattern: data. clock &- data. table [ index] . used)
66+ if diff < dist {
3667 targetOffset = offset
37- diff = Int32 . max
68+ diff = dist
3869 }
3970 }
4071 } else {
@@ -57,24 +88,112 @@ final package class ObjectCache<Key, Value> where Key: Hashable {
5788 return value
5889 }
5990 }
60-
91+
92+ /// A cache slot that can hold an item or be empty.
93+ ///
94+ /// Each slot tracks when it was last used via the `used` timestamp, which is
95+ /// compared against the global `clock` to determine the least recently used item.
6196 private struct Item {
97+
98+ /// The cached data tuple containing the key, hash, and value, or nil if empty.
6299 var data : ( key: Key , hash: Int , value: Value ) ?
100+
101+ /// The clock value when this item was last accessed or inserted.
102+ ///
103+ /// This timestamp is used for LRU eviction. When a bucket is full, the item
104+ /// with the smallest `used` value (i.e., the one with the largest time distance
105+ /// from the current clock) is evicted.
63106 var used : UInt32
64-
107+
65108 init ( data: ( key: Key , hash: Int , value: Value ) ? , used: UInt32 ) {
66109 self . data = data
67110 self . used = used
68111 }
69112 }
70-
113+
114+ /// The internal data structure holding the cache table and global clock.
71115 private struct Data {
116+
117+ /// The number of buckets in the cache.
118+ ///
119+ /// The cache uses 8 buckets to distribute keys based on their hash values.
120+ /// Each bucket can hold multiple items (ways) for collision resolution.
121+ static var bucketCount : Int { 8 }
122+
123+ /// The number of ways (slots) per bucket.
124+ ///
125+ /// Each bucket contains 4 ways, implementing a 4-way set-associative cache.
126+ /// When all ways in a bucket are full, the least recently used item is evicted.
127+ static var waysPerBucket : Int { 4 }
128+
129+ /// The total number of slots in the cache table.
130+ ///
131+ /// Computed as `bucketCount × waysPerBucket`, resulting in 32 total cache slots.
132+ static var tableSize : Int { bucketCount * waysPerBucket }
133+
134+ /// The hash table with 32 slots (8 buckets × 4 ways per bucket).
72135 var table : [ Item ]
136+
137+ /// A monotonically increasing counter used for LRU tracking.
138+ ///
139+ /// The `clock` is incremented on every cache access (hit or miss). Each item's
140+ /// `used` field stores the clock value at its last access. When eviction is needed,
141+ /// the item with the oldest `used` value (largest difference from current clock)
142+ /// is selected for replacement.
143+ ///
144+ /// This implements a pseudo-LRU policy that efficiently approximates true LRU
145+ /// without maintaining a global ordering of all items.
73146 var clock : UInt32
74-
147+
75148 init ( ) {
76- self . table = Array ( repeating: Item ( data: nil , used: 0 ) , count: 32 )
149+ self . table = Array ( repeating: Item ( data: nil , used: 0 ) , count: Self . tableSize )
77150 self . clock = 0
78151 }
79152 }
80153}
154+
155+ #if DEBUG
156+ extension ObjectCache : CustomDebugStringConvertible {
157+ package var debugDescription : String {
158+ $data. access { data in
159+ var description = " ObjectCache(clock: \( data. clock) , items: \( data. table. filter { $0. data != nil } . count) / \( Data . tableSize) ) \n "
160+ for (index, item) in data. table. enumerated ( ) {
161+ if let itemData = item. data {
162+ let bucket = index / Data. waysPerBucket
163+ let offset = index % Data. waysPerBucket
164+ let age = data. clock &- item. used
165+ description += " [ \( bucket) : \( offset) ] hash= \( itemData. hash) , used= \( item. used) , age= \( age) \n "
166+ }
167+ }
168+ return description
169+ }
170+ }
171+ }
172+
173+ extension ObjectCache {
174+ package var count : Int {
175+ $data. access { data in
176+ data. table. filter { $0. data != nil } . count
177+ }
178+ }
179+
180+ package var currentClock : UInt32 {
181+ $data. access { data in
182+ data. clock
183+ }
184+ }
185+
186+ package var keys : [ Key ] {
187+ $data. access { data in
188+ data. table. compactMap { $0. data? . key }
189+ }
190+ }
191+
192+ package func reset( ) {
193+ $data. access { data in
194+ data. table = Array ( repeating: Item ( data: nil , used: 0 ) , count: Data . tableSize)
195+ data. clock = 0
196+ }
197+ }
198+ }
199+ #endif
0 commit comments