diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..3a650a3 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,7 @@ +## 2024-05-24 - Swift Actor TaskGroup Serialization +**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. +**Action:** Mark computationally intensive or state-independent methods as `nonisolated` (and pass injected dependencies like `FileManager`) to ensure they run on the global concurrent pool. + +## 2024-05-24 - Single Syscall File Attributes +**Learning:** Using separate `FileManager.fileExists` and `FileManager.attributesOfItem` calls is inefficient. +**Action:** Use `URL.resourceValues(forKeys:)` to perform a single optimized syscall for retrieving directory existence and modification dates simultaneously. \ No newline at end of file diff --git a/Sources/Cacheout/Scanner/NodeModulesScanner.swift b/Sources/Cacheout/Scanner/NodeModulesScanner.swift index 3ed4d8c..63d7654 100644 --- a/Sources/Cacheout/Scanner/NodeModulesScanner.swift +++ b/Sources/Cacheout/Scanner/NodeModulesScanner.swift @@ -61,8 +61,9 @@ actor NodeModulesScanner { for root in Self.searchRoots { let rootURL = home.appendingPathComponent(root) guard fileManager.fileExists(atPath: rootURL.path) else { continue } + let fm = fileManager group.addTask { - await self.findNodeModules(in: rootURL, maxDepth: maxDepth) + await self.findNodeModules(in: rootURL, maxDepth: maxDepth, fileManager: fm) } } 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 nonisolated func findNodeModules(in directory: URL, maxDepth: Int, currentDepth: Int = 0, fileManager: FileManager) 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) + // Check if this directory contains node_modules using a single syscall + 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 @@ -113,14 +112,14 @@ 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 = await findNodeModules(in: item, maxDepth: maxDepth, currentDepth: currentDepth + 1, fileManager: fileManager) results.append(contentsOf: subResults) } return results } - private func directorySize(at url: URL) -> Int64 { + private nonisolated func directorySize(at url: URL, fileManager: FileManager) -> Int64 { var total: Int64 = 0 guard let enumerator = fileManager.enumerator( at: url,