From 946739b570130356d6f8afb2a0893959d8faadaa Mon Sep 17 00:00:00 2001 From: swiftty <62803132+swiftty@users.noreply.github.com> Date: Wed, 1 Jan 2025 20:03:35 +0900 Subject: [PATCH 1/5] Upgrade library to use actor --- Sources/DataCacheKit/Cache.swift | 55 ++++++--------- Sources/DataCacheKit/Caching.swift | 2 +- Sources/DataCacheKit/DiskCache+Options.swift | 4 +- Sources/DataCacheKit/DiskCache.swift | 70 ++++++------------- .../DataCacheKit/MemoryCache+Options.swift | 2 +- Sources/DataCacheKit/MemoryCache.swift | 13 +--- Sources/DataCacheKit/Utils/LRUCache.swift | 0 Sources/DataCacheKit/Utils/Task.swift | 13 ++++ Tests/DataCacheKitTests/DiskCacheTests.swift | 26 +++---- 9 files changed, 77 insertions(+), 108 deletions(-) create mode 100644 Sources/DataCacheKit/Utils/LRUCache.swift create mode 100644 Sources/DataCacheKit/Utils/Task.swift diff --git a/Sources/DataCacheKit/Cache.swift b/Sources/DataCacheKit/Cache.swift index 34dfec8..4ee0cbd 100644 --- a/Sources/DataCacheKit/Cache.swift +++ b/Sources/DataCacheKit/Cache.swift @@ -1,11 +1,19 @@ import Foundation import OSLog -public final class Cache: Caching, @unchecked Sendable { - public typealias Options = (forMemory: MemoryCache.Options, forDisk: DiskCache.Options) +public actor Cache: Caching { + public struct Options: Sendable { + public let forMemory: MemoryCache.Options + public let forDisk: DiskCache.Options + + public init(forMemory: MemoryCache.Options, forDisk: DiskCache.Options) { + self.forMemory = forMemory + self.forDisk = forDisk + } + } - public let options: Options - public let logger: Logger + public nonisolated let options: Options + public nonisolated let logger: Logger public subscript (key: Key) -> Value? { get async throws { @@ -16,7 +24,6 @@ public final class Cache: C private let onMemery: MemoryCache private let onDisk: DiskCache - private let queueingLock = NSLock() private var queueingTask: Task? public init(options: Options, logger: Logger = .init(.disabled)) { @@ -26,8 +33,8 @@ public final class Cache: C self.logger = logger } - public func prepare() throws { - try onDisk.prepare() + public func prepare() async throws { + try await onDisk.prepare() } public func value(for key: Key) async throws -> Value? { @@ -46,64 +53,48 @@ public final class Cache: C return try decoder.decode(Value.self, from: data) }() - onMemery.store(value, for: key) + await onMemery.store(value, for: key) return value } @discardableResult public func store(_ value: Value, for key: Key) -> Task { - queueingLock.lock() - defer { queueingLock.unlock() } - let oldTask = queueingTask - let task = Task { - await oldTask?.value + queueingTask.enqueueAndReplacing { [weak self] in + guard let self else { return } async let memory: Void = await onMemery.store(value, for: key).value async let disk: Void = await _storeToDisk(value, for: key) await memory await disk } - queueingTask = task - return task } @discardableResult public func remove(for key: Key) -> Task { - queueingLock.lock() - defer { queueingLock.unlock() } - let oldTask = queueingTask - let task = Task { - await oldTask?.value + queueingTask.enqueueAndReplacing { [weak self] in + guard let self else { return } async let memory: Void = await onMemery.remove(for: key).value async let disk: Void = await onDisk.remove(for: key).value await memory await disk } - queueingTask = task - return task } @discardableResult public func removeAll() -> Task { - queueingLock.lock() - defer { queueingLock.unlock() } - let oldTask = queueingTask - let task = Task { - await oldTask?.value - + queueingTask.enqueueAndReplacing { [weak self] in + guard let self else { return } async let memory: Void = await onMemery.removeAll().value async let disk: Void = await onDisk.removeAll().value await memory await disk } - queueingTask = task - return task } - public func url(for key: Key) -> URL? { - onDisk.url(for: key) + public func url(for key: Key) async -> URL? { + await onDisk.url(for: key) } } diff --git a/Sources/DataCacheKit/Caching.swift b/Sources/DataCacheKit/Caching.swift index 695647e..f6b9ea4 100644 --- a/Sources/DataCacheKit/Caching.swift +++ b/Sources/DataCacheKit/Caching.swift @@ -1,6 +1,6 @@ import Foundation -public protocol Caching: Sendable { +public protocol Caching: Actor { associatedtype Key: Hashable & Sendable associatedtype Value diff --git a/Sources/DataCacheKit/DiskCache+Options.swift b/Sources/DataCacheKit/DiskCache+Options.swift index 5a2eb7d..317c737 100644 --- a/Sources/DataCacheKit/DiskCache+Options.swift +++ b/Sources/DataCacheKit/DiskCache+Options.swift @@ -2,13 +2,13 @@ import Foundation import CommonCrypto extension DiskCache { - public struct Options { + public struct Options: Sendable { public var sizeLimit: Int public var filename: @Sendable (Key) -> String? public var path: Path public var expirationTimeout: TimeInterval? - public enum Path { + public enum Path: Sendable { case `default`(name: String) case custom(URL) } diff --git a/Sources/DataCacheKit/DiskCache.swift b/Sources/DataCacheKit/DiskCache.swift index 5e6fd62..96a37c8 100644 --- a/Sources/DataCacheKit/DiskCache.swift +++ b/Sources/DataCacheKit/DiskCache.swift @@ -6,20 +6,13 @@ import Foundation import OSLog -@globalActor -public struct DiskCacheActor { - public actor Actor {} - - public static let shared = Actor() -} - // MARK: - DiskCache -public final class DiskCache: Caching, @unchecked Sendable { +public actor DiskCache: Caching, @unchecked Sendable { public typealias Key = Key public typealias Value = Data - public let options: Options - public let logger: Logger + public nonisolated let options: Options + public nonisolated let logger: Logger public subscript (key: Key) -> Data? { get async throws { @@ -48,25 +41,18 @@ public final class DiskCache: Caching, @unchecked Send private let queueingLock = NSLock() private var queueingTask: Task? - @DiskCacheActor private(set) lazy var staging = Staging() - @DiskCacheActor private var runningTasks: [Key: Task] = [:] - @DiskCacheActor private(set) var flushingTask: Task? - @DiskCacheActor private(set) var sweepingTask: Task? - @DiskCacheActor private(set) var isFlushNeeded = false - @DiskCacheActor private(set) var isFlushScheduled = false - @DiskCacheActor private var logKey: String { guard let path = try? path else { return "" } return "[\(path.lastPathComponent)] " @@ -80,7 +66,9 @@ public final class DiskCache: Caching, @unchecked Send self.clock = _Clock() } self.logger = logger - try? _prepare() + Task { + try await _prepare() + } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -88,7 +76,9 @@ public final class DiskCache: Caching, @unchecked Send self.options = options self.clock = NewClock(clock) self.logger = logger - try? _prepare() + Task { + try await _prepare() + } } public func prepare() throws { @@ -102,7 +92,7 @@ public final class DiskCache: Caching, @unchecked Send func value(for key: Key, with now: Date) async throws -> Data? { await Task.yield() - let task = Task { @DiskCacheActor in + let task = Task { _ = await queueingTask?.result for stage in staging.stages.reversed() { @@ -191,7 +181,7 @@ public final class DiskCache: Caching, @unchecked Send if prepared { return } - defer { prepared = true } + prepared = true let dir: URL? switch options.path { @@ -202,37 +192,31 @@ public final class DiskCache: Caching, @unchecked Send dir = url } _path = dir - Task { - await scheduleSweep(after: 10) - } + scheduleSweep(after: 10) if dir == nil { throw CocoaError(.fileNoSuchFile) } } - @DiskCacheActor private func _storeData(_ data: Data, for key: Key) async { logger.debug("\(self.logKey)store data: \(data) for \(String(describing: key))") staging.add(data: data, for: key) setNeedsFlushChanges() } - @DiskCacheActor private func _removeData(for key: Key) async { logger.debug("\(self.logKey)remove data for \(String(describing: key))") staging.remove(for: key) setNeedsFlushChanges() } - @DiskCacheActor private func _removeDataAll() async { logger.debug("\(self.logKey)remove data all") staging.removeAll() setNeedsFlushChanges() } - @DiskCacheActor private func waitForTask(for key: Key) async { guard let task = runningTasks[key] else { return } _ = await task.result @@ -240,7 +224,6 @@ public final class DiskCache: Caching, @unchecked Send } extension DiskCache { - @DiskCacheActor private func setNeedsFlushChanges() { guard !isFlushNeeded else { return } isFlushNeeded = true @@ -253,7 +236,6 @@ extension DiskCache { } } - @DiskCacheActor private func flushIfNeeded(_ oldTask: Task?) async throws { guard !isFlushScheduled else { return } isFlushScheduled = true @@ -269,7 +251,6 @@ extension DiskCache { await _flushIfNeeded(numberOfAttempts: staging.stages.count) } - @DiskCacheActor private func _flushIfNeeded(numberOfAttempts: Int) async { guard numberOfAttempts > 0, let stage = staging.stages.first else { return } let changes = await _flush(on: stage) @@ -281,7 +262,7 @@ extension DiskCache { } private func _flush(on stage: Staging.Stage) async -> [Staging.Change] { - await withTaskGroup(of: [Staging.Change].self) { @DiskCacheActor group in + await withTaskGroup(of: [Staging.Change].self) { group in if stage.removeAll { let changes = Array(stage.changes.values) let task = performChangeRemoveAll(for: changes) @@ -319,7 +300,6 @@ extension DiskCache { } } - @DiskCacheActor private func peformChange(_ change: Staging.Change, with url: URL) -> Task { let task = Task { do { @@ -352,7 +332,6 @@ extension DiskCache { return task } - @DiskCacheActor private func performChangeRemoveAll(for changes: some Collection.Change>) -> Task { let task = Task { do { @@ -377,7 +356,6 @@ extension DiskCache { } extension DiskCache { - @DiskCacheActor private func scheduleSweep(after seconds: Int) { logger.debug("\(self.logKey)sweep scheduled") let oldTask = sweepingTask @@ -395,7 +373,6 @@ extension DiskCache { } } - @DiskCacheActor private func performSweep() throws { var items = try contents(keys: [.contentAccessDateKey, .totalFileAllocatedSizeKey]) guard !items.isEmpty else { return } @@ -452,7 +429,6 @@ extension DiskCache { } } - @DiskCacheActor private func contents(keys: [URLResourceKey] = []) throws -> [Entry] { let urls: [URL] do { @@ -470,8 +446,8 @@ extension DiskCache { /// The total number of items in the cache. public var totalCount: Int { - get async throws { - try await contents().count + get throws { + try contents().count } } @@ -481,8 +457,8 @@ extension DiskCache { /// The total allocated size (see `totalAllocatedSize`. on disk might /// actually be bigger. public var totalSize: Int { - get async throws { - try await contents(keys: [.fileSizeKey]).reduce(0) { + get throws { + try contents(keys: [.fileSizeKey]).reduce(0) { $0 + ($1.meta.fileSize ?? 0) } } @@ -492,8 +468,8 @@ extension DiskCache { /// /// Uses `URLResourceKey.totalFileAllocatedSizeKey`. public var totalAllocatedSize: Int { - get async throws { - try await contents(keys: [.totalFileAllocatedSizeKey]).reduce(0) { + get throws { + try contents(keys: [.totalFileAllocatedSizeKey]).reduce(0) { $0 + ($1.meta.totalFileAllocatedSize ?? 0) } } @@ -501,14 +477,14 @@ extension DiskCache { } extension DiskCache { - class _Clock { + class _Clock: @unchecked Sendable { func sleep(until seconds: Int) async throws { try await Task.sleep(nanoseconds: UInt64(seconds) * 1_000_000_000) } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - final class NewClock: _Clock where C.Instant.Duration == Duration { + final class NewClock: _Clock, @unchecked Sendable where C.Instant.Duration == Duration { let clock: C init(_ clock: C) { @@ -521,20 +497,16 @@ extension DiskCache { } } -@DiskCacheActor struct Staging { - @DiskCacheActor enum Operation: Equatable { case add(Data) case remove } - @DiskCacheActor struct Change { let key: Key let id: Int let operation: Operation } - @DiskCacheActor struct Stage { let id: Int var changes: [Key: Change] diff --git a/Sources/DataCacheKit/MemoryCache+Options.swift b/Sources/DataCacheKit/MemoryCache+Options.swift index 40486f6..c015a85 100644 --- a/Sources/DataCacheKit/MemoryCache+Options.swift +++ b/Sources/DataCacheKit/MemoryCache+Options.swift @@ -1,7 +1,7 @@ import Foundation extension MemoryCache { - public struct Options { + public struct Options: Sendable { public var countLimit: Int var sizeLimit: Int? diff --git a/Sources/DataCacheKit/MemoryCache.swift b/Sources/DataCacheKit/MemoryCache.swift index 31e5134..b981b51 100644 --- a/Sources/DataCacheKit/MemoryCache.swift +++ b/Sources/DataCacheKit/MemoryCache.swift @@ -1,12 +1,11 @@ import Foundation import OSLog -public final class MemoryCache: Caching, @unchecked Sendable { - public let options: Options - public let logger: Logger +public actor MemoryCache: Caching { + public nonisolated let options: Options + public nonisolated let logger: Logger private let nsCache = NSCache, ValueWrapper>() - private let queueingLock = NSLock() private var queueingTask: Task? public subscript (key: Key) -> Value? { @@ -31,8 +30,6 @@ public final class MemoryCache: Cachi @discardableResult public func store(_ value: Value, for key: Key) -> Task { - queueingLock.lock() - defer { queueingLock.unlock() } let oldTask = queueingTask let task = Task { await oldTask?.value @@ -44,8 +41,6 @@ public final class MemoryCache: Cachi @discardableResult public func remove(for key: Key) -> Task { - queueingLock.lock() - defer { queueingLock.unlock() } let oldTask = queueingTask let task = Task { await oldTask?.value @@ -57,8 +52,6 @@ public final class MemoryCache: Cachi @discardableResult public func removeAll() -> Task { - queueingLock.lock() - defer { queueingLock.unlock() } let oldTask = queueingTask let task = Task { await oldTask?.value diff --git a/Sources/DataCacheKit/Utils/LRUCache.swift b/Sources/DataCacheKit/Utils/LRUCache.swift new file mode 100644 index 0000000..e69de29 diff --git a/Sources/DataCacheKit/Utils/Task.swift b/Sources/DataCacheKit/Utils/Task.swift new file mode 100644 index 0000000..5a247a4 --- /dev/null +++ b/Sources/DataCacheKit/Utils/Task.swift @@ -0,0 +1,13 @@ +extension Optional> { + mutating func enqueueAndReplacing( + _ operation: sending @escaping @isolated(any) () async -> Void + ) -> Task { + let oldTask = self + let newTask = Task { + await oldTask?.value + await operation() + } + self = newTask + return newTask + } +} diff --git a/Tests/DataCacheKitTests/DiskCacheTests.swift b/Tests/DataCacheKitTests/DiskCacheTests.swift index 08b844e..9abb006 100644 --- a/Tests/DataCacheKitTests/DiskCacheTests.swift +++ b/Tests/DataCacheKitTests/DiskCacheTests.swift @@ -44,7 +44,7 @@ final class DiskCacheTests { let clock = ManualClock() let cache = DiskCache(options: cacheOptions(), clock: clock, logger: .init(.default)) - cache.store(Data(), for: "empty") + await cache.store(Data(), for: "empty") try await yield(until: await cache.isFlushScheduled) @@ -53,7 +53,7 @@ final class DiskCacheTests { let data = try await cache.value(for: "empty") #expect(data != nil) - let url = try #require(cache.url(for: "empty")) + let url = try #require(await cache.url(for: "empty")) #expect(!FileManager.default.fileExists(atPath: url.path)) } @@ -72,7 +72,7 @@ final class DiskCacheTests { let data = try await cache.value(for: "empty") #expect(data != nil) - let url = try #require(cache.url(for: "empty")) + let url = try #require(await cache.url(for: "empty")) #expect(FileManager.default.fileExists(atPath: url.path)) } } @@ -83,8 +83,8 @@ final class DiskCacheTests { let clock = ManualClock() let cache = DiskCache(options: cacheOptions(), clock: clock, logger: .init(.default)) - cache.store(Data([1]), for: "item0") - cache.store(Data([1, 2]), for: "item1") + await cache.store(Data([1]), for: "item0") + await cache.store(Data([1, 2]), for: "item1") try await yield(until: await cache.isFlushScheduled) @@ -107,9 +107,9 @@ final class DiskCacheTests { let clock = ManualClock() let cache = DiskCache(options: cacheOptions(), clock: clock, logger: .init(.default)) - cache.store(Data([1]), for: "item0") - cache.store(Data([1, 2]), for: "item1") - cache.remove(for: "item0") + await cache.store(Data([1]), for: "item0") + await cache.store(Data([1, 2]), for: "item1") + await cache.remove(for: "item0") try await yield(until: await cache.isFlushScheduled) @@ -142,7 +142,7 @@ final class DiskCacheTests { let clock = ManualClock() let cache = DiskCache(options: cacheOptions(), clock: clock, logger: .init(.default)) - cache.store(Data([1]), for: "item0") + await cache.store(Data([1]), for: "item0") try await yield(until: await cache.isFlushScheduled) clock.advance(by: .milliseconds(1000)) @@ -157,7 +157,7 @@ final class DiskCacheTests { #expect(isEmpty) } - cache.removeAll() + await cache.removeAll() try await yield(until: await cache.isFlushScheduled) clock.advance(by: .milliseconds(1000)) @@ -186,9 +186,9 @@ final class DiskCacheTests { let clock = ManualClock() let cache = DiskCache(options: options, clock: clock) - cache.store(Data([1]), for: "item0") - cache.store(Data([1, 2]), for: "item1") - cache.store(Data([1, 2, 3]), for: "item2") + await cache.store(Data([1]), for: "item0") + await cache.store(Data([1, 2]), for: "item1") + await cache.store(Data([1, 2, 3]), for: "item2") try await yield(until: await cache.isFlushScheduled) From c953998d34f0fa75c57725fc7a076960867a291b Mon Sep 17 00:00:00 2001 From: swiftty <62803132+swiftty@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:54:55 +0900 Subject: [PATCH 2/5] Update MemoryCache implementation --- Package.swift | 6 +- Sources/DataCacheKit/MemoryCache.swift | 57 ++----- Sources/DataCacheKit/Utils/LRUCache.swift | 175 ++++++++++++++++++++++ 3 files changed, 191 insertions(+), 47 deletions(-) diff --git a/Package.swift b/Package.swift index a8f236e..165ee64 100644 --- a/Package.swift +++ b/Package.swift @@ -6,10 +6,10 @@ import PackageDescription let package = Package( name: "DataCacheKit", platforms: [ - .iOS(.v14), - .tvOS(.v14), + .iOS(.v16), + .tvOS(.v16), .watchOS(.v8), - .macOS(.v12) + .macOS(.v13) ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. diff --git a/Sources/DataCacheKit/MemoryCache.swift b/Sources/DataCacheKit/MemoryCache.swift index b981b51..bb8d9af 100644 --- a/Sources/DataCacheKit/MemoryCache.swift +++ b/Sources/DataCacheKit/MemoryCache.swift @@ -5,7 +5,7 @@ public actor MemoryCache: Caching { public nonisolated let options: Options public nonisolated let logger: Logger - private let nsCache = NSCache, ValueWrapper>() + private let lruCache = LRUCache() private var queueingTask: Task? public subscript (key: Key) -> Value? { @@ -17,69 +17,38 @@ public actor MemoryCache: Caching { public init(options: Options, logger: Logger = .init(.disabled)) { self.options = options self.logger = logger - nsCache.countLimit = options.countLimit + lruCache.countLimit = options.countLimit if let costLimit = options.sizeLimit { - nsCache.totalCostLimit = costLimit + lruCache.totalCostLimit = costLimit } } public func value(for key: Key) async -> Value? { _ = await queueingTask?.result - return nsCache.object(forKey: .init(key))?.value + return lruCache.value(forKey: key) } @discardableResult public func store(_ value: Value, for key: Key) -> Task { - let oldTask = queueingTask - let task = Task { - await oldTask?.value - nsCache.setObject(.init(value), forKey: .init(key), cost: (value as? Data)?.count ?? 0) + queueingTask.enqueueAndReplacing { [weak self] in + guard let self else { return } + lruCache.setValue(value, forKey: key, cost: (value as? Data)?.count ?? 0) } - queueingTask = task - return task } @discardableResult public func remove(for key: Key) -> Task { - let oldTask = queueingTask - let task = Task { - await oldTask?.value - nsCache.removeObject(forKey: .init(key)) + queueingTask.enqueueAndReplacing { [weak self] in + guard let self else { return } + lruCache.removeValue(forKey: key) } - queueingTask = task - return task } @discardableResult public func removeAll() -> Task { - let oldTask = queueingTask - let task = Task { - await oldTask?.value - nsCache.removeAllObjects() + queueingTask.enqueueAndReplacing { [weak self] in + guard let self else { return } + lruCache.removeAllValues() } - queueingTask = task - return task } } - -// MARK: - -private final class KeyWrapper: NSObject { - let key: Key - - init(_ key: Key) { self.key = key } - - override func isEqual(_ object: Any?) -> Bool { - guard let other = object as? KeyWrapper else { return false } - return key == other.key - } - - override var hash: Int { - key.hashValue - } -} - -private final class ValueWrapper { - let value: Value - - init(_ value: Value) { self.value = value } -} diff --git a/Sources/DataCacheKit/Utils/LRUCache.swift b/Sources/DataCacheKit/Utils/LRUCache.swift index e69de29..8168cdd 100644 --- a/Sources/DataCacheKit/Utils/LRUCache.swift +++ b/Sources/DataCacheKit/Utils/LRUCache.swift @@ -0,0 +1,175 @@ +// https://github.com/swiftlang/swift-corelibs-foundation/blob/25d044f2c4ceb635d9f714f588673fd7a29790c1/Sources/Foundation/NSCache.swift +import os + +struct LRUCache: ~Copyable, Sendable { + var totalCostLimit: Int { + get { entries.withLock { $0.totalCostLimit } } + nonmutating set { entries.withLock { $0.totalCostLimit = newValue } } + } + + var countLimit: Int { + get { entries.withLock { $0.countLimit } } + nonmutating set { entries.withLock { $0.countLimit = newValue } } + } + + private let entries = OSAllocatedUnfairLock(initialState: .init()) + + func value(forKey key: Key) -> Value? { + entries.withLock { entries in + entries.values[key]?.value + } + } + + func setValue(_ value: Value, forKey key: Key) { + setValue(value, forKey: key, cost: 0) + } + + func setValue(_ value: Value, forKey key: Key, cost: Int) { + entries.withLock { entries in + entries.set(value, forKey: key, cost: cost) + } + } + + func removeValue(forKey key: Key) { + entries.withLock { entries in + if let entry = entries.values[key] { + entries.totalCost -= entry.cost + entries.remove(entry) + } + } + } + + func removeAllValues() { + entries.withLock { entiries in + entiries.removeAll() + } + } +} + +private extension LRUCache { + final class CacheEntry: @unchecked Sendable { + let key: Key + var value: Value + var cost: Int + var prevByCost: CacheEntry? + var nextByCost: CacheEntry? + + init(key: Key, value: Value, cost: Int) { + self.key = key + self.value = value + self.cost = cost + self.prevByCost = nil + self.nextByCost = nil + } + } + + struct Entiries: Sendable { + var values: [Key: CacheEntry] = [:] + var totalCost = 0 + var totalCostLimit = 0 + var countLimit = 0 + var head: CacheEntry? + + mutating func set(_ value: Value, forKey key: Key, cost g: Int) { + let g = max(g, 0) + + let costDiff: Int + + if let entry = values[key] { + costDiff = g - entry.cost + entry.cost = g + entry.value = value + + if costDiff != 0 { + remove(entry) + insert(entry) + } + } else { + let entry = CacheEntry(key: key, value: value, cost: g) + values[key] = entry + insert(entry) + + costDiff = g + } + + totalCost += costDiff + + var purgeAmount = totalCostLimit > 0 ? totalCost - totalCostLimit : 0 + while purgeAmount > 0, let entry = head { + totalCost -= entry.cost + purgeAmount -= entry.cost + + remove(entry) + values[entry.key] = nil + } + + var purgeCount = countLimit > 0 ? values.count - countLimit : 0 + while purgeCount > 0, let entry = head { + totalCost -= entry.cost + purgeCount -= 1 + + remove(entry) + values[entry.key] = nil + } + } + + 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 + + head = 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 + } + + mutating func remove(_ entry: CacheEntry) { + let oldPrev = entry.prevByCost + let oldNext = entry.nextByCost + + oldPrev?.nextByCost = oldNext + oldNext?.prevByCost = oldPrev + + if entry === head { + head = oldNext + } + } + + mutating func removeAll() { + values.removeAll() + + while let curr = head { + let next = curr.nextByCost + + curr.prevByCost = nil + curr.nextByCost = nil + + head = next + } + + totalCost = 0 + } + } +} + From 464133d2ccd555a07175c0bfd8e721b90ace282b Mon Sep 17 00:00:00 2001 From: swiftty <62803132+swiftty@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:57:32 +0900 Subject: [PATCH 3/5] Modify DiskCache implementation --- Sources/DataCacheKit/DiskCache.swift | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/Sources/DataCacheKit/DiskCache.swift b/Sources/DataCacheKit/DiskCache.swift index 96a37c8..d6c33f1 100644 --- a/Sources/DataCacheKit/DiskCache.swift +++ b/Sources/DataCacheKit/DiskCache.swift @@ -38,7 +38,6 @@ public actor DiskCache: Caching, @unchecked Sendable { private var _path: URL! private var prepared = false - private let queueingLock = NSLock() private var queueingTask: Task? private(set) lazy var staging = Staging() @@ -134,41 +133,26 @@ public actor DiskCache: Caching, @unchecked Sendable { @discardableResult public func store(_ data: Data, for key: Key) -> Task { - queueingLock.lock() - defer { queueingLock.unlock() } - let oldTask = queueingTask - let task = Task { - await oldTask?.value + queueingTask.enqueueAndReplacing { [weak self] in + guard let self else { return } await _storeData(data, for: key) } - queueingTask = task - return task } @discardableResult public func remove(for key: Key) -> Task { - queueingLock.lock() - defer { queueingLock.unlock() } - let oldTask = queueingTask - let task = Task { - await oldTask?.value + queueingTask.enqueueAndReplacing { [weak self] in + guard let self else { return } await _removeData(for: key) } - queueingTask = task - return task } @discardableResult public func removeAll() -> Task { - queueingLock.lock() - defer { queueingLock.unlock() } - let oldTask = queueingTask - let task = Task { - await oldTask?.value + queueingTask.enqueueAndReplacing { [weak self] in + guard let self else { return } await _removeDataAll() } - queueingTask = task - return task } public func url(for key: Key) -> URL? { From 176f1445e392f93f262753d3f0b1f677a8f246f2 Mon Sep 17 00:00:00 2001 From: swiftty <62803132+swiftty@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:39:13 +0900 Subject: [PATCH 4/5] Add visionOS support --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 165ee64..302b327 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,8 @@ let package = Package( platforms: [ .iOS(.v16), .tvOS(.v16), - .watchOS(.v8), + .watchOS(.v9), + .visionOS(.v2), .macOS(.v13) ], products: [ From ab8d8eb59a2b81561a784a5d587c311060929961 Mon Sep 17 00:00:00 2001 From: swiftty <62803132+swiftty@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:23:22 +0900 Subject: [PATCH 5/5] Use Clock directly --- Sources/DataCacheKit/DiskCache.swift | 45 +++----------------- Tests/DataCacheKitTests/DiskCacheTests.swift | 5 --- 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/Sources/DataCacheKit/DiskCache.swift b/Sources/DataCacheKit/DiskCache.swift index d6c33f1..6eaad88 100644 --- a/Sources/DataCacheKit/DiskCache.swift +++ b/Sources/DataCacheKit/DiskCache.swift @@ -27,7 +27,7 @@ public actor DiskCache: Caching, @unchecked Sendable { // } } - private let clock: _Clock + private let clock: any Clock private var path: URL { get throws { @@ -57,23 +57,9 @@ public actor DiskCache: Caching, @unchecked Sendable { return "[\(path.lastPathComponent)] " } - public init(options: Options, logger: Logger = .init(.disabled)) { + public init(options: Options, clock: some Clock = .suspending, logger: Logger = .init(.disabled)) { self.options = options - if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { - self.clock = NewClock(.suspending) - } else { - self.clock = _Clock() - } - self.logger = logger - Task { - try await _prepare() - } - } - - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - init(options: Options, clock: C, logger: Logger = .init(.disabled)) where C.Instant.Duration == Duration { - self.options = options - self.clock = NewClock(clock) + self.clock = clock self.logger = logger Task { try await _prepare() @@ -225,7 +211,7 @@ extension DiskCache { isFlushScheduled = true defer { isFlushScheduled = false } - try await clock.sleep(until: 1) + try await clock.sleep(for: .seconds(1)) try? await oldTask?.value guard isFlushNeeded else { return } @@ -344,7 +330,7 @@ extension DiskCache { logger.debug("\(self.logKey)sweep scheduled") let oldTask = sweepingTask sweepingTask = Task { - try await clock.sleep(until: seconds) + try await clock.sleep(for: .seconds(seconds)) _ = await oldTask?.result do { logger.debug("\(self.logKey)sweep starting") @@ -460,27 +446,6 @@ extension DiskCache { } } -extension DiskCache { - class _Clock: @unchecked Sendable { - func sleep(until seconds: Int) async throws { - try await Task.sleep(nanoseconds: UInt64(seconds) * 1_000_000_000) - } - } - - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - final class NewClock: _Clock, @unchecked Sendable where C.Instant.Duration == Duration { - let clock: C - - init(_ clock: C) { - self.clock = clock - } - - override func sleep(until seconds: Int) async throws { - try await clock.sleep(until: clock.now.advanced(by: .seconds(seconds)), tolerance: nil) - } - } -} - struct Staging { enum Operation: Equatable { case add(Data) diff --git a/Tests/DataCacheKitTests/DiskCacheTests.swift b/Tests/DataCacheKitTests/DiskCacheTests.swift index 9abb006..7b7b825 100644 --- a/Tests/DataCacheKitTests/DiskCacheTests.swift +++ b/Tests/DataCacheKitTests/DiskCacheTests.swift @@ -39,7 +39,6 @@ final class DiskCacheTests { } @Test - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func testStoreData() async throws { let clock = ManualClock() let cache = DiskCache(options: cacheOptions(), clock: clock, logger: .init(.default)) @@ -78,7 +77,6 @@ final class DiskCacheTests { } @Test - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func testStoreDataMultiple() async throws { let clock = ManualClock() let cache = DiskCache(options: cacheOptions(), clock: clock, logger: .init(.default)) @@ -102,7 +100,6 @@ final class DiskCacheTests { } @Test - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func testRemoveData() async throws { let clock = ManualClock() let cache = DiskCache(options: cacheOptions(), clock: clock, logger: .init(.default)) @@ -137,7 +134,6 @@ final class DiskCacheTests { } @Test - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func testRemoveDataAll() async throws { let clock = ManualClock() let cache = DiskCache(options: cacheOptions(), clock: clock, logger: .init(.default)) @@ -177,7 +173,6 @@ final class DiskCacheTests { } @Test - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func testSweep() async throws { let allocationUnit = 4096