From c7da84190bf2bf26320f625e9cd173f494ae22a6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 03:42:32 +0000 Subject: [PATCH] Fix sequential scanning by removing unnecessary actor isolation Changed CacheScanner and NodeModulesScanner from `actor` to `struct` to prevent TaskGroup serialization. Previously, Tasks spawned via `withTaskGroup` were inadvertently being serialized due to actor isolation when calling their internal instance methods. Converting them to structs solves this issue and allows scanning to process concurrently, greatly reducing the time necessary to execute cache scanning operations. Co-authored-by: acebytes <2820910+acebytes@users.noreply.github.com> --- .jules/bolt.md | 3 +++ Sources/Cacheout/Scanner/CacheScanner.swift | 6 +++--- Sources/Cacheout/Scanner/NodeModulesScanner.swift | 8 ++++---- 3 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 .jules/bolt.md 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) }