diff --git a/.jules/bolt.md b/.jules/bolt.md index dd1b466..2b16977 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -21,3 +21,7 @@ ## 2026-03-19 - FileManager Enumerator Pre-fetching **Learning:** Any property requested via `resourceValues(forKeys:)` inside a `FileManager.enumerator` loop must also be in `includingPropertiesForKeys` — otherwise `URL.resourceValues` falls back to a synchronous `stat()` per file, turning bulk reads into O(N) disk I/O. **Action:** Keep the keys array passed to `resourceValues(forKeys:)` a subset of the prefetch list passed to `FileManager.enumerator(at:includingPropertiesForKeys:)`. + +## 2024-05-30 - DispatchQueue.concurrentPerform Thread Starvation +**Learning:** Do not use `DispatchQueue.concurrentPerform` inside an `actor` or `async` context in Swift. It is a synchronous, blocking call that will lock a thread in the limited cooperative thread pool until all iterations finish, causing system-wide thread starvation. Furthermore, do not wrap large numbers of blocking I/O calls in `Task.detached` within a loop, as this allocates excessive tasks and causes overhead. +**Action:** To parallelize bulk I/O operations (e.g., `FileManager.removeItem` loops) inside an actor, mark the method as `nonisolated async throws` and use a `withThrowingTaskGroup`. Implement a sliding window iterator (e.g., `maxConcurrency` of 8) and inject `FileManager` to safely parallelize the blocking calls without overloading the thread pool. diff --git a/Sources/Cacheout/Cleaner/CacheCleaner.swift b/Sources/Cacheout/Cleaner/CacheCleaner.swift index 5e1c18f..1f3e214 100644 --- a/Sources/Cacheout/Cleaner/CacheCleaner.swift +++ b/Sources/Cacheout/Cleaner/CacheCleaner.swift @@ -64,7 +64,7 @@ actor CacheCleaner { if moveToTrash { try await trashDirectory(url) } else { - try removeContents(of: url) + try await removeContents(of: url, fileManager: fileManager) } categoryFreed += result.sizeBytes } catch { @@ -133,12 +133,33 @@ actor CacheCleaner { } } - private func removeContents(of url: URL) throws { + nonisolated private func removeContents(of url: URL, fileManager: FileManager) async throws { let contents = try fileManager.contentsOfDirectory( at: url, includingPropertiesForKeys: nil ) - for item in contents { - try fileManager.removeItem(at: item) + + try await withThrowingTaskGroup(of: Void.self) { group in + // Limit concurrency to avoid overloading the system and cooperative thread pool + let maxConcurrency = 8 + var iterator = contents.makeIterator() + + for _ in 0..