diff --git a/Sources/DataCacheKit/Utils/LRUCache.swift b/Sources/DataCacheKit/Utils/LRUCache.swift index 8168cdd..e324d7b 100644 --- a/Sources/DataCacheKit/Utils/LRUCache.swift +++ b/Sources/DataCacheKit/Utils/LRUCache.swift @@ -32,7 +32,7 @@ struct LRUCache: ~Copyable, Sendable func removeValue(forKey key: Key) { entries.withLock { entries in - if let entry = entries.values[key] { + if let entry = entries.values.removeValue(forKey: key) { entries.totalCost -= entry.cost entries.remove(entry) } @@ -46,20 +46,35 @@ struct LRUCache: ~Copyable, Sendable } } +extension LRUCache { + subscript (_ key: Key, cost cost: Int = 0) -> Value? { + get { + value(forKey: key) + } + nonmutating set { + if let newValue { + setValue(newValue, forKey: key, cost: cost) + } else { + removeValue(forKey: key) + } + } + } +} + private extension LRUCache { final class CacheEntry: @unchecked Sendable { let key: Key var value: Value var cost: Int - var prevByCost: CacheEntry? - var nextByCost: CacheEntry? + weak var prev: CacheEntry? + weak var next: CacheEntry? init(key: Key, value: Value, cost: Int) { self.key = key self.value = value self.cost = cost - self.prevByCost = nil - self.nextByCost = nil + self.prev = nil + self.next = nil } } @@ -68,7 +83,8 @@ private extension LRUCache { var totalCost = 0 var totalCostLimit = 0 var countLimit = 0 - var head: CacheEntry? + weak var head: CacheEntry? + weak var tail: CacheEntry? mutating func set(_ value: Value, forKey key: Key, cost g: Int) { let g = max(g, 0) @@ -80,10 +96,8 @@ private extension LRUCache { entry.cost = g entry.value = value - if costDiff != 0 { - remove(entry) - insert(entry) - } + remove(entry) + insert(entry) } else { let entry = CacheEntry(key: key, value: value, cost: g) values[key] = entry @@ -114,62 +128,40 @@ private extension LRUCache { } mutating func insert(_ entry: CacheEntry) { - guard var curr = head else { - entry.prevByCost = nil - entry.nextByCost = nil - - head = entry - return - } - - guard entry.cost > curr.cost else { - entry.prevByCost = nil - entry.nextByCost = curr - curr.prevByCost = entry + guard head != nil else { + entry.prev = nil + entry.next = nil head = entry + tail = entry return } - while let next = curr.nextByCost, next.cost < entry.cost { - curr = next - } - - let next = curr.nextByCost - - curr.nextByCost = entry - entry.prevByCost = curr - - entry.nextByCost = next - next?.prevByCost = entry + tail?.next = entry + entry.prev = tail + tail = entry } mutating func remove(_ entry: CacheEntry) { - let oldPrev = entry.prevByCost - let oldNext = entry.nextByCost + let oldPrev = entry.prev + let oldNext = entry.next - oldPrev?.nextByCost = oldNext - oldNext?.prevByCost = oldPrev + oldPrev?.next = oldNext + oldNext?.prev = oldPrev if entry === head { head = oldNext } + if entry === tail { + tail = oldPrev + } } mutating func removeAll() { values.removeAll() - - while let curr = head { - let next = curr.nextByCost - - curr.prevByCost = nil - curr.nextByCost = nil - - head = next - } - totalCost = 0 + + assert(head == nil && tail == nil) } } } - diff --git a/Tests/DataCacheKitTests/LRUCacheTests.swift b/Tests/DataCacheKitTests/LRUCacheTests.swift new file mode 100644 index 0000000..0c8f9df --- /dev/null +++ b/Tests/DataCacheKitTests/LRUCacheTests.swift @@ -0,0 +1,417 @@ +import Testing +@testable import DataCacheKit +import Foundation + +struct LRUCacheTests { + @Test + func testCostLimit() { + let cache = LRUCache() + + // Given + cache.totalCostLimit = 10 + + // When + cache[1, cost: 4] = 1 + cache[2, cost: 5] = 2 + cache[3, cost: 5] = 3 + + // Then + #expect(cache[1] == nil) + #expect(cache[2] == 2) + #expect(cache[3] == 3) + } + + @Test + func testCostLimitNSCache() { + let cache = NSCacheWrapper() + + // Given + cache.totalCostLimit = 10 + + // When + cache[1, cost: 4] = 1 + cache[2, cost: 5] = 2 + cache[3, cost: 5] = 3 + + // Then + #expect(cache[1] == nil) + #expect(cache[2] == 2) + #expect(cache[3] == 3) + } + + @Test + func testCountLimit() { + let cache = LRUCache() + + // Given + cache.countLimit = 2 + + // When + cache[1] = 1 + cache[2] = 2 + cache[3] = 3 + + // Then + #expect(cache[1] == nil) + #expect(cache[2] == 2) + #expect(cache[3] == 3) + } + + @Test + func testCountLimitNSCache() { + let cache = NSCacheWrapper() + + // Given + cache.countLimit = 2 + + // When + cache[1] = 1 + cache[2] = 2 + cache[3] = 3 + + // Then + #expect(cache[1] == nil) + #expect(cache[2] == 2) + #expect(cache[3] == 3) + } + + @Test + func testCountLimitWithCost1() { + let cache = LRUCache() + + // Given + cache.countLimit = 2 + cache.totalCostLimit = 5 + + // When + cache[1, cost: 3] = 1 + cache[2, cost: 3] = 2 + cache[3, cost: 3] = 3 + + // Then + #expect(cache[1] == nil) + #expect(cache[2] == nil) + #expect(cache[3] == 3) + } + + @Test + func testCountLimitWithCost1NSCache() { + let cache = NSCacheWrapper() + + // Given + cache.countLimit = 2 + cache.totalCostLimit = 5 + + // When + cache[1, cost: 3] = 1 + cache[2, cost: 3] = 2 + cache[3, cost: 3] = 3 + + // Then + #expect(cache[1] == nil) + #expect(cache[2] == nil) + #expect(cache[3] == 3) + } + + @Test + func testCountLimitWithCost2() { + let cache = LRUCache() + + // Given + cache.countLimit = 2 + cache.totalCostLimit = 3 + + // When + cache[1, cost: 3] = 1 + cache[2, cost: 2] = 2 + cache[3, cost: 1] = 3 + + // Then + #expect(cache[1] == nil) + #expect(cache[2] == 2) + #expect(cache[3] == 3) + } + + @Test + func testCountLimitWithCost2NSCache() { + let cache = NSCacheWrapper() + + // Given + cache.countLimit = 2 + cache.totalCostLimit = 3 + + // When + cache[1, cost: 3] = 1 + cache[2, cost: 2] = 2 + cache[3, cost: 1] = 3 + + // Then + #expect(cache[1] == nil) + #expect(cache[2] == 2) + #expect(cache[3] == 3) + } + + @Test + func testCountLimitWithCost3() { + let cache = LRUCache() + + // Given + cache.countLimit = 2 + cache.totalCostLimit = 3 + + // When + cache[1, cost: 3] = 1 + cache[2, cost: 2] = 2 + cache[3, cost: 1] = 3 + cache[1, cost: 3] = 1 + + // Then + #expect(cache[1] == 1) + #expect(cache[2] == nil) + #expect(cache[3] == nil) + } + + @Test + func testCountLimitWithCost3NSCache() { + let cache = NSCacheWrapper() + + // Given + cache.countLimit = 2 + cache.totalCostLimit = 3 + + // When + cache[1, cost: 3] = 1 + cache[2, cost: 2] = 2 + cache[3, cost: 1] = 3 + cache[1, cost: 3] = 1 + + // Then + #expect(cache[1] == 1) + #expect(cache[2] == nil) + #expect(cache[3] == nil) + } + + @Test + func testCountLimitWithCost4() { + let cache = LRUCache() + + // Given + cache.totalCostLimit = 10 + + // When + cache[1, cost: 3] = 1 + cache[2, cost: 2] = 2 + cache[3, cost: 1] = 3 + cache[1, cost: 3] = 1 + cache[3, cost: 7] = 3 + + // Then + #expect(cache[1] == 1) + #expect(cache[2] == nil) + #expect(cache[3] == 3) + } + + @Test + func testCountLimitWithCost4NSCache() { + let cache = NSCacheWrapper() + + // Given + cache.totalCostLimit = 10 + + // When + cache[1, cost: 3] = 1 + cache[2, cost: 2] = 2 + cache[3, cost: 1] = 3 + cache[1, cost: 3] = 1 + cache[3, cost: 7] = 3 + + // Then + #expect(cache[1] == 1) + #expect(cache[2] == nil) + #expect(cache[3] == 3) + } + + @Test + func testRemoveHeadValue() { + let cache = LRUCache() + + // Given + // - + + // When + cache[1] = 1 + cache[2] = 2 + cache[3] = 3 + + cache[1] = nil + + // Then + #expect(cache[1] == nil) + #expect(cache[2] == 2) + #expect(cache[3] == 3) + } + + @Test + func testRemoveMiddleValue() { + let cache = LRUCache() + + // Given + // - + + // When + cache[1] = 1 + cache[2] = 2 + cache[3] = 3 + + cache[2] = nil + + // Then + #expect(cache[1] == 1) + #expect(cache[2] == nil) + #expect(cache[3] == 3) + } + + @Test + func testRemoveTailValue() { + let cache = LRUCache() + + // Given + // - + + // When + cache[1] = 1 + cache[2] = 2 + cache[3] = 3 + + cache[3] = nil + + // Then + #expect(cache[1] == 1) + #expect(cache[2] == 2) + #expect(cache[3] == nil) + } + + @Test + func testRemoveAll() { + let cache = LRUCache() + + // Given + // - + + // When + cache[1] = 1 + cache[2] = 2 + cache[3] = 3 + + cache.removeAllValues() + + // Then + #expect(cache[1] == nil) + #expect(cache[2] == nil) + #expect(cache[3] == nil) + } + + @Test + func testReferenceCount() { + final class MyClass: @unchecked Sendable {} + + let cache = LRUCache() + + // Given + var ref: MyClass? = MyClass() + weak var weakRef = ref + + // When + autoreleasepool { + cache[1] = ref + + #expect(cache[1] === ref) + + ref = nil + + #expect(weakRef != nil) + + cache.removeAllValues() + } + + // Then + #expect(weakRef == nil) + } +} + +// MARK: - private +private final class NSCacheWrapper { + func object(forKey key: Key) -> Object? { + inner.object(forKey: .init(key))?.object + } + + func setObject(_ obj: Object, forKey key: Key, cost: Int = 0) { + inner.setObject(.init(obj), forKey: .init(key), cost: cost) + } + + func removeObject(forKey key: Key) { + inner.removeObject(forKey: .init(key)) + } + + func removeAllObjects() { + inner.removeAllObjects() + } + + func removeAllValues() { + removeAllObjects() + } + + var totalCostLimit: Int { + get { inner.totalCostLimit } + set { inner.totalCostLimit = newValue } + } + + var countLimit: Int { + get { inner.countLimit } + set { inner.countLimit = newValue } + } + + // MARK: - + + private let inner = NSCache() + + private final class KeyWrapper: NSObject { + let key: Key + + init(_ key: Key) { + self.key = key + } + + override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? KeyWrapper else { return false } + return key == object.key + } + + override var hash: Int { + return key.hashValue + } + } + + private final class ObjectWrapper: NSObject { + let object: Object + + init(_ object: Object) { + self.object = object + } + } +} + +extension NSCacheWrapper { + subscript (_ key: Key, cost cost: Int = 0) -> Object? { + get { + object(forKey: key) + } + set { + if let newValue { + setObject(newValue, forKey: key, cost: cost) + } else { + removeObject(forKey: key) + } + } + } +}