diff --git a/.jules/bolt.md b/.jules/bolt.md index 2a6279f..8424e68 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -25,3 +25,9 @@ ## 2024-05-25 - Efficient Dictionary Initialization **Learning:** Building a dictionary via a standard `for` loop by inserting elements one by one can cause unnecessary overhead due to repeated mutations. `reduce(into: [:])` is highly optimized in Swift to build collections without creating intermediate copies. **Action:** Use `reduce(into: [:])` when constructing a dictionary from an array, particularly when uniqueness checks or transformations are required. +## 2024-05-18 - Cooperative Thread Pool Exhaustion in Bulk I/O +**Learning:** In Swift Concurrency, executing synchronous blocking operations like `FileManager.removeItem` inside a `TaskGroup` without isolation can quickly exhaust the limited cooperative thread pool (which has threads equal to CPU cores), leading to system-wide deadlock or starvation. +**Action:** When parallelizing synchronous blocking I/O calls, wrap them in `Task.detached { ... }.value` to escape the cooperative pool and execute on a background queue, and control concurrency with a sliding window iterator. +## 2024-05-18 - Escaping Cooperative Thread Pool +**Learning:** `Task.detached` executes on the global cooperative thread pool and does *not* safely offload synchronous blocking I/O. Using it with concurrent groups can still exhaust the thread pool. +**Action:** To truly move blocking work (like `FileManager.removeItem`) off the cooperative thread pool in Swift Concurrency, wrap the execution in `withCheckedThrowingContinuation` and dispatch to a background GCD queue (e.g., `DispatchQueue.global(qos: .userInitiated).async`). diff --git a/Sources/Cacheout/Cleaner/CacheCleaner.swift b/Sources/Cacheout/Cleaner/CacheCleaner.swift index 5e1c18f..bfaf261 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 { @@ -86,7 +86,18 @@ actor CacheCleaner { if moveToTrash { try await trashItem(item.nodeModulesPath) } else { - try fileManager.removeItem(at: item.nodeModulesPath) + let path = item.nodeModulesPath + let fm = fileManager + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + DispatchQueue.global(qos: .userInitiated).async { + do { + try fm.removeItem(at: path) + continuation.resume() + } catch { + continuation.resume(throwing: error) + } + } + } } cleaned.append(("node_modules: \(item.projectName)", item.sizeBytes)) logCleanup(category: "node_modules/\(item.projectName)", bytesFreed: item.sizeBytes) @@ -133,12 +144,48 @@ 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 + var iterator = contents.makeIterator() + let maxConcurrency = 8 + + for _ in 0..) in + DispatchQueue.global(qos: .userInitiated).async { + do { + try fileManager.removeItem(at: item) + continuation.resume() + } catch { + continuation.resume(throwing: error) + } + } + } + } + } + } + + for try await _ in group { + if let item = iterator.next() { + group.addTask { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + DispatchQueue.global(qos: .userInitiated).async { + do { + try fileManager.removeItem(at: item) + continuation.resume() + } catch { + continuation.resume(throwing: error) + } + } + } + } + } + } } }