From ab72d3069b25080add256fae32001a88792caec8 Mon Sep 17 00:00:00 2001 From: swiftty <62803132+swiftty@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:25:16 +0900 Subject: [PATCH 1/5] Add LRUCacheTests --- Tests/DataCacheKitTests/LRUCacheTests.swift | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Tests/DataCacheKitTests/LRUCacheTests.swift diff --git a/Tests/DataCacheKitTests/LRUCacheTests.swift b/Tests/DataCacheKitTests/LRUCacheTests.swift new file mode 100644 index 0000000..2b3d42a --- /dev/null +++ b/Tests/DataCacheKitTests/LRUCacheTests.swift @@ -0,0 +1,41 @@ +import Testing +@testable import DataCacheKit +import Foundation + +struct LRUCacheTests { + @Test + func testCountLimit() { + let cache = LRUCache() + + // Given + cache.countLimit = 2 + + // When + cache.setValue(1, forKey: 1) + cache.setValue(2, forKey: 2) + cache.setValue(3, forKey: 3) + + // Then + #expect(cache.value(forKey: 1) == nil) + #expect(cache.value(forKey: 2) == 2) + #expect(cache.value(forKey: 3) == 3) + } + + @Test + func testCountLimitNSCache() { + let cache = NSCache() + + // Given + cache.countLimit = 2 + + // When + cache.setObject(1, forKey: 1) + cache.setObject(2, forKey: 2) + cache.setObject(3, forKey: 3) + + // Then + #expect(cache.object(forKey: 1) == nil) + #expect(cache.object(forKey: 2) == 2) + #expect(cache.object(forKey: 3) == 3) + } +} From abfbf1718e1f75d4db1e3baff72d9549e2241f8a Mon Sep 17 00:00:00 2001 From: swiftty <62803132+swiftty@users.noreply.github.com> Date: Sat, 4 Jan 2025 00:26:08 +0900 Subject: [PATCH 2/5] Fix cache item ordering --- Sources/DataCacheKit/Utils/LRUCache.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/DataCacheKit/Utils/LRUCache.swift b/Sources/DataCacheKit/Utils/LRUCache.swift index 8168cdd..b458a95 100644 --- a/Sources/DataCacheKit/Utils/LRUCache.swift +++ b/Sources/DataCacheKit/Utils/LRUCache.swift @@ -122,7 +122,7 @@ private extension LRUCache { return } - guard entry.cost > curr.cost else { + guard entry.cost >= curr.cost else { entry.prevByCost = nil entry.nextByCost = curr curr.prevByCost = entry From 28648cd776f9fba15c12dedd5aaaf95d72e285df Mon Sep 17 00:00:00 2001 From: swiftty <62803132+swiftty@users.noreply.github.com> Date: Sat, 4 Jan 2025 03:30:48 +0900 Subject: [PATCH 3/5] Add test cases --- Tests/DataCacheKitTests/LRUCacheTests.swift | 295 +++++++++++++++++++- 1 file changed, 282 insertions(+), 13 deletions(-) diff --git a/Tests/DataCacheKitTests/LRUCacheTests.swift b/Tests/DataCacheKitTests/LRUCacheTests.swift index 2b3d42a..88b8682 100644 --- a/Tests/DataCacheKitTests/LRUCacheTests.swift +++ b/Tests/DataCacheKitTests/LRUCacheTests.swift @@ -3,6 +3,42 @@ import Testing 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() @@ -11,31 +47,264 @@ struct LRUCacheTests { cache.countLimit = 2 // When - cache.setValue(1, forKey: 1) - cache.setValue(2, forKey: 2) - cache.setValue(3, forKey: 3) + cache[1] = 1 + cache[2] = 2 + cache[3] = 3 // Then - #expect(cache.value(forKey: 1) == nil) - #expect(cache.value(forKey: 2) == 2) - #expect(cache.value(forKey: 3) == 3) + #expect(cache[1] == nil) + #expect(cache[2] == 2) + #expect(cache[3] == 3) } @Test func testCountLimitNSCache() { - let cache = NSCache() + 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.setObject(1, forKey: 1) - cache.setObject(2, forKey: 2) - cache.setObject(3, forKey: 3) + cache[1, cost: 3] = 1 + cache[2, cost: 3] = 2 + cache[3, cost: 3] = 3 // Then - #expect(cache.object(forKey: 1) == nil) - #expect(cache.object(forKey: 2) == 2) - #expect(cache.object(forKey: 3) == 3) + #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) + } +} + +// 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) + } + } } } From e9ada9d68acbb1d88ab173c14d7c2e337e40b58d Mon Sep 17 00:00:00 2001 From: swiftty <62803132+swiftty@users.noreply.github.com> Date: Sat, 4 Jan 2025 04:37:48 +0900 Subject: [PATCH 4/5] Fix LRUCache implementation --- Sources/DataCacheKit/Utils/LRUCache.swift | 86 ++++++++++------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/Sources/DataCacheKit/Utils/LRUCache.swift b/Sources/DataCacheKit/Utils/LRUCache.swift index b458a95..8b1c056 100644 --- a/Sources/DataCacheKit/Utils/LRUCache.swift +++ b/Sources/DataCacheKit/Utils/LRUCache.swift @@ -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) } } } - From 6a7f74fef34078bca89037f9ad7a4e0cc4ad4e95 Mon Sep 17 00:00:00 2001 From: swiftty <62803132+swiftty@users.noreply.github.com> Date: Sat, 4 Jan 2025 05:02:10 +0900 Subject: [PATCH 5/5] Add test cases for removing value --- Sources/DataCacheKit/Utils/LRUCache.swift | 2 +- Tests/DataCacheKitTests/LRUCacheTests.swift | 107 ++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/Sources/DataCacheKit/Utils/LRUCache.swift b/Sources/DataCacheKit/Utils/LRUCache.swift index 8b1c056..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) } diff --git a/Tests/DataCacheKitTests/LRUCacheTests.swift b/Tests/DataCacheKitTests/LRUCacheTests.swift index 88b8682..0c8f9df 100644 --- a/Tests/DataCacheKitTests/LRUCacheTests.swift +++ b/Tests/DataCacheKitTests/LRUCacheTests.swift @@ -230,6 +230,113 @@ struct LRUCacheTests { #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