From 88ce102644d62d71b6658dd1f2986efc509d9289 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 05:12:10 +0000 Subject: [PATCH] perf: Convert Scanner actors to structs to prevent TaskGroup serialization Converting CacheScanner and NodeModulesScanner from `actor` to `struct` since they are stateless. Using an actor with `withTaskGroup` inadvertently serializes all child tasks onto the actor's single-threaded executor, destroying parallelism. This change allows true parallel filesystem traversal. Co-authored-by: acebytes <2820910+acebytes@users.noreply.github.com> --- .jules/bolt.md | 3 +++ Sources/Cacheout/Scanner/CacheScanner.swift | 4 +++- Sources/Cacheout/Scanner/NodeModulesScanner.swift | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..7cb4036 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-24 - Actor Serialization in TaskGroup +**Learning:** In Swift structured concurrency, using an `actor` to manage a `withTaskGroup` where tasks invoke synchronous, blocking I/O (like `FileManager` operations) directly on the actor inadvertently serializes the tasks, preventing parallelism. +**Action:** For stateless components interacting with thread-safe dependencies (like `FileManager.default`), use `struct`s or `nonisolated` methods to allow tasks to execute concurrently across threads. diff --git a/Sources/Cacheout/Scanner/CacheScanner.swift b/Sources/Cacheout/Scanner/CacheScanner.swift index 3ce3e9c..1ded582 100644 --- a/Sources/Cacheout/Scanner/CacheScanner.swift +++ b/Sources/Cacheout/Scanner/CacheScanner.swift @@ -26,7 +26,9 @@ import Foundation -actor CacheScanner { +// Using struct instead of actor to prevent serialization of parallel tasks. +// Since this component is stateless, struct allows true concurrency across threads. +struct CacheScanner { private let fileManager = FileManager.default func scanAll(_ categories: [CacheCategory]) async -> [ScanResult] { diff --git a/Sources/Cacheout/Scanner/NodeModulesScanner.swift b/Sources/Cacheout/Scanner/NodeModulesScanner.swift index 3ed4d8c..2a9a5a6 100644 --- a/Sources/Cacheout/Scanner/NodeModulesScanner.swift +++ b/Sources/Cacheout/Scanner/NodeModulesScanner.swift @@ -28,7 +28,9 @@ import Foundation -actor NodeModulesScanner { +// Using struct instead of actor to prevent serialization of parallel tasks. +// Since this component is stateless, struct allows true concurrency across threads. +struct NodeModulesScanner { private let fileManager = FileManager.default /// Common directories where developers keep projects