diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..d33e643 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2026-04-04 - Actor Isolation Blocking Parallelism in TaskGroups +**Learning:** Using an `actor` to manage a `withTaskGroup` where child tasks invoke synchronous, blocking operations directly on the actor (`await self.method()`) inadvertently serializes the tasks on the actor's executor, preventing parallelism. +**Action:** For stateless components interacting with thread-safe dependencies (like `FileManager`), use `struct`s or `nonisolated` methods instead of `actor`s to allow tasks to execute concurrently across threads. diff --git a/Sources/Cacheout/Scanner/CacheScanner.swift b/Sources/Cacheout/Scanner/CacheScanner.swift index 3ce3e9c..a4e72bb 100644 --- a/Sources/Cacheout/Scanner/CacheScanner.swift +++ b/Sources/Cacheout/Scanner/CacheScanner.swift @@ -26,13 +26,13 @@ import Foundation -actor CacheScanner { +struct CacheScanner { private let fileManager = FileManager.default func scanAll(_ categories: [CacheCategory]) async -> [ScanResult] { await withTaskGroup(of: ScanResult.self) { group in for category in categories { - group.addTask { await self.scanCategory(category) } + group.addTask { self.scanCategory(category) } } var results: [ScanResult] = [] for await result in group { @@ -42,7 +42,7 @@ actor CacheScanner { } } - func scanCategory(_ category: CacheCategory) async -> ScanResult { + func scanCategory(_ category: CacheCategory) -> ScanResult { let resolvedPaths = category.resolvedPaths guard !resolvedPaths.isEmpty else { return ScanResult(category: category, sizeBytes: 0, itemCount: 0, exists: false) diff --git a/Sources/Cacheout/Scanner/NodeModulesScanner.swift b/Sources/Cacheout/Scanner/NodeModulesScanner.swift index 3ed4d8c..24d9211 100644 --- a/Sources/Cacheout/Scanner/NodeModulesScanner.swift +++ b/Sources/Cacheout/Scanner/NodeModulesScanner.swift @@ -28,7 +28,7 @@ import Foundation -actor NodeModulesScanner { +struct NodeModulesScanner { private let fileManager = FileManager.default /// Common directories where developers keep projects @@ -62,7 +62,7 @@ actor NodeModulesScanner { let rootURL = home.appendingPathComponent(root) guard fileManager.fileExists(atPath: rootURL.path) else { continue } group.addTask { - await self.findNodeModules(in: rootURL, maxDepth: maxDepth) + self.findNodeModules(in: rootURL, maxDepth: maxDepth) } } for await items in group { @@ -77,7 +77,7 @@ actor NodeModulesScanner { .sorted { $0.sizeBytes > $1.sizeBytes } } - private func findNodeModules(in directory: URL, maxDepth: Int, currentDepth: Int = 0) async -> [NodeModulesItem] { + private func findNodeModules(in directory: URL, maxDepth: Int, currentDepth: Int = 0) -> [NodeModulesItem] { guard currentDepth < maxDepth else { return [] } var results: [NodeModulesItem] = [] @@ -113,7 +113,7 @@ actor NodeModulesScanner { let name = item.lastPathComponent guard !Self.skipDirs.contains(name) else { continue } guard (try? item.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true else { continue } - let subResults = await findNodeModules(in: item, maxDepth: maxDepth, currentDepth: currentDepth + 1) + let subResults = findNodeModules(in: item, maxDepth: maxDepth, currentDepth: currentDepth + 1) results.append(contentsOf: subResults) }