diff --git a/.jules/bolt.md b/.jules/bolt.md index dd1b466..506fd2d 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -21,3 +21,7 @@ ## 2026-03-19 - FileManager Enumerator Pre-fetching **Learning:** Any property requested via `resourceValues(forKeys:)` inside a `FileManager.enumerator` loop must also be in `includingPropertiesForKeys` — otherwise `URL.resourceValues` falls back to a synchronous `stat()` per file, turning bulk reads into O(N) disk I/O. **Action:** Keep the keys array passed to `resourceValues(forKeys:)` a subset of the prefetch list passed to `FileManager.enumerator(at:includingPropertiesForKeys:)`. + +## 2024-05-18 - Dictionary overhead vs O(n) array lookups +**Learning:** Avoid replacing O(n) `firstIndex(where:)` searches with O(1) dictionary-based index maps for SwiftUI `@Published` arrays if the collection is small, the interaction is infrequent, or the map requires frequent reconstruction (e.g., on every scan). The O(n) map construction overhead (allocating memory and hashing every UUID) will outweigh the lookup benefits. +**Action:** When evaluating lookup optimizations, explicitly weigh the frequency of the lookup against the frequency of the dataset being rebuilt. Use index maps only for large, long-lived datasets with extremely frequent, hot-path lookups. diff --git a/Sources/Cacheout/ViewModels/CacheoutViewModel.swift b/Sources/Cacheout/ViewModels/CacheoutViewModel.swift index 0e8464b..6531f71 100644 --- a/Sources/Cacheout/ViewModels/CacheoutViewModel.swift +++ b/Sources/Cacheout/ViewModels/CacheoutViewModel.swift @@ -54,6 +54,9 @@ class CacheoutViewModel: ObservableObject { @Published var nodeModulesItems: [NodeModulesItem] = [] @Published var isNodeModulesScanning = false + private var scanResultsIndexMap: [UUID: Int] = [:] + private var nodeModulesIndexMap: [UUID: Int] = [:] + /// Increments on every completed scan — views can use .task(id:) to react @Published var scanGeneration: Int = 0 @@ -154,9 +157,11 @@ class CacheoutViewModel: ObservableObject { async let nmResults = nodeModulesScanner.scan() scanResults = await cacheResults + scanResultsIndexMap = scanResults.enumerated().reduce(into: [:]) { $0[$1.element.id] = $1.offset } isScanning = false nodeModulesItems = await nmResults + nodeModulesIndexMap = nodeModulesItems.enumerated().reduce(into: [:]) { $0[$1.element.id] = $1.offset } isNodeModulesScanning = false // Track scan completion for reactive UI updates @@ -166,7 +171,11 @@ class CacheoutViewModel: ObservableObject { } func toggleSelection(for id: UUID) { - if let index = scanResults.firstIndex(where: { $0.id == id }) { + // Try O(1) map lookup first + if let index = scanResultsIndexMap[id], index < scanResults.count, scanResults[index].id == id { + scanResults[index].isSelected.toggle() + } else if let index = scanResults.firstIndex(where: { $0.id == id }) { + // Fallback to O(n) search if map is desynced scanResults[index].isSelected.toggle() } } @@ -191,7 +200,11 @@ class CacheoutViewModel: ObservableObject { // MARK: - Node Modules selection func toggleNodeModulesSelection(for id: UUID) { - if let i = nodeModulesItems.firstIndex(where: { $0.id == id }) { + // Try O(1) map lookup first + if let i = nodeModulesIndexMap[id], i < nodeModulesItems.count, nodeModulesItems[i].id == id { + nodeModulesItems[i].isSelected.toggle() + } else if let i = nodeModulesItems.firstIndex(where: { $0.id == id }) { + // Fallback to O(n) search if map is desynced nodeModulesItems[i].isSelected.toggle() } }