From bc25af6426fce8c384596a7111020ec2d028a470 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 05:28:24 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Parallelize=20bulk=20cache?= =?UTF-8?q?=20deletion=20using=20TaskGroup=20sliding=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: acebytes <2820910+acebytes@users.noreply.github.com> --- .jules/bolt.md | 4 +++ Sources/Cacheout/Cleaner/CacheCleaner.swift | 29 ++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) 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..