Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 9 additions & 10 deletions Sources/Cacheout/Scanner/NodeModulesScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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,
Expand Down