diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..e8bc0d5 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-05 - TaskGroup parallelism and URL.resourceValues +**Learning:** In Swift Concurrency, when an actor creates a `TaskGroup` and its subtasks call an `isolated` method on the same actor, execution is serialized on the actor's executor, destroying parallelism. Also, `URL.resourceValues(forKeys:)` can perform a single optimized syscall instead of separate `FileManager.fileExists` and `FileManager.attributesOfItem` calls. +**Action:** Mark computationally intensive or state-independent methods as `static` or `nonisolated` so they run on the global concurrent pool, and pass dependencies (like `FileManager` and `skipDirs`) as arguments. Use `URL.resourceValues(forKeys:)` for checking directory existence and attributes simultaneously. diff --git a/Sources/Cacheout/Scanner/NodeModulesScanner.swift b/Sources/Cacheout/Scanner/NodeModulesScanner.swift index 3ed4d8c..70dbd47 100644 --- a/Sources/Cacheout/Scanner/NodeModulesScanner.swift +++ b/Sources/Cacheout/Scanner/NodeModulesScanner.swift @@ -55,14 +55,15 @@ actor NodeModulesScanner { func scan(maxDepth: Int = 6) async -> [NodeModulesItem] { let home = fileManager.homeDirectoryForCurrentUser var allItems: [NodeModulesItem] = [] + let currentFileManager = self.fileManager // Scan each search root in parallel await withTaskGroup(of: [NodeModulesItem].self) { group in for root in Self.searchRoots { let rootURL = home.appendingPathComponent(root) - guard fileManager.fileExists(atPath: rootURL.path) else { continue } + guard currentFileManager.fileExists(atPath: rootURL.path) else { continue } group.addTask { - await self.findNodeModules(in: rootURL, maxDepth: maxDepth) + await Self.findNodeModules(in: rootURL, fileManager: currentFileManager, skipDirs: Self.skipDirs, maxDepth: maxDepth) } } for await items in group { @@ -77,25 +78,23 @@ actor NodeModulesScanner { .sorted { $0.sizeBytes > $1.sizeBytes } } - private func findNodeModules(in directory: URL, maxDepth: Int, currentDepth: Int = 0) async -> [NodeModulesItem] { + private static func findNodeModules(in directory: URL, fileManager: FileManager, skipDirs: Set, maxDepth: Int, currentDepth: Int = 0) async -> [NodeModulesItem] { guard currentDepth < maxDepth else { return [] } var results: [NodeModulesItem] = [] let nodeModulesURL = directory.appendingPathComponent("node_modules") // Check if this directory contains node_modules - var isDir: ObjCBool = false - if fileManager.fileExists(atPath: nodeModulesURL.path, isDirectory: &isDir), isDir.boolValue { - let size = directorySize(at: nodeModulesURL) + if let values = try? nodeModulesURL.resourceValues(forKeys: [.isDirectoryKey, .contentModificationDateKey]), values.isDirectory == true { + let size = directorySize(at: nodeModulesURL, fileManager: fileManager) if size > 0 { - let lastMod = try? fileManager.attributesOfItem(atPath: nodeModulesURL.path)[.modificationDate] as? Date let projectName = directory.lastPathComponent results.append(NodeModulesItem( projectName: projectName, projectPath: directory, nodeModulesPath: nodeModulesURL, sizeBytes: size, - lastModified: lastMod + lastModified: values.contentModificationDate )) } // Don't recurse into projects that have node_modules — they won't have nested projects @@ -111,16 +110,16 @@ actor NodeModulesScanner { for item in contents { let name = item.lastPathComponent - guard !Self.skipDirs.contains(name) else { continue } + guard !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 = await findNodeModules(in: item, fileManager: fileManager, skipDirs: skipDirs, maxDepth: maxDepth, currentDepth: currentDepth + 1) results.append(contentsOf: subResults) } return results } - private func directorySize(at url: URL) -> Int64 { + private static func directorySize(at url: URL, fileManager: FileManager) -> Int64 { var total: Int64 = 0 guard let enumerator = fileManager.enumerator( at: url,