diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..4abbeaf --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-06-18 - Unnecessary blocking operations on @MainActor +**Learning:** In `CacheoutViewModel.swift`, synchronous blocking operations like `DiskInfo.current()`, `process.waitUntilExit()`, and `pipe.fileHandleForReading.readDataToEndOfFile()` are run directly on the `@MainActor` (since the class is marked `@MainActor`). This causes the main thread to block, potentially causing UI hitching during long operations. +**Action:** Use `Task.detached` to offload these blocking operations to a background thread to maintain UI responsiveness. diff --git a/Sources/Cacheout/ViewModels/CacheoutViewModel.swift b/Sources/Cacheout/ViewModels/CacheoutViewModel.swift index 13a9811..702c9ae 100644 --- a/Sources/Cacheout/ViewModels/CacheoutViewModel.swift +++ b/Sources/Cacheout/ViewModels/CacheoutViewModel.swift @@ -147,7 +147,7 @@ class CacheoutViewModel: ObservableObject { func scan() async { isScanning = true isNodeModulesScanning = true - diskInfo = DiskInfo.current() + diskInfo = await Task.detached { DiskInfo.current() }.value // Scan caches and node_modules in parallel async let cacheResults = scanner.scanAll(CacheCategory.allCategories) @@ -229,48 +229,50 @@ class CacheoutViewModel: ObservableObject { isDockerPruning = true defer { isDockerPruning = false } - let process = Process() - let pipe = Pipe() - process.executableURL = URL(fileURLWithPath: "/bin/bash") - process.arguments = ["-c", "docker system prune -f 2>&1"] - process.standardOutput = pipe - process.standardError = pipe - process.environment = [ - "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin", - "HOME": FileManager.default.homeDirectoryForCurrentUser.path - ] - - do { - try process.run() - process.waitUntilExit() - let data = pipe.fileHandleForReading.readDataToEndOfFile() - let output = String(data: data, encoding: .utf8) ?? "" - - if process.terminationStatus == 0 { - // Extract "Total reclaimed space:" line - if let line = output.components(separatedBy: "\n") - .first(where: { $0.contains("reclaimed") }) { - lastDockerPruneResult = line.trimmingCharacters(in: .whitespaces) + lastDockerPruneResult = await Task.detached { + let process = Process() + let pipe = Pipe() + process.executableURL = URL(fileURLWithPath: "/bin/bash") + process.arguments = ["-c", "docker system prune -f 2>&1"] + process.standardOutput = pipe + process.standardError = pipe + process.environment = [ + "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin", + "HOME": FileManager.default.homeDirectoryForCurrentUser.path + ] + + do { + try process.run() + process.waitUntilExit() + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) ?? "" + + if process.terminationStatus == 0 { + // Extract "Total reclaimed space:" line + if let line = output.components(separatedBy: "\n") + .first(where: { $0.contains("reclaimed") }) { + return line.trimmingCharacters(in: .whitespaces) + } else { + return "Docker pruned successfully" + } } else { - lastDockerPruneResult = "Docker pruned successfully" - } - } else { - let lowerOutput = output.lowercased() - if lowerOutput.contains("cannot connect") || - lowerOutput.contains("is the docker daemon running") || - lowerOutput.contains("connection refused") || - lowerOutput.contains("no such file or directory") { - lastDockerPruneResult = "Docker must be running to prune" - } else { - lastDockerPruneResult = "Docker prune failed — is Docker running?" + let lowerOutput = output.lowercased() + if lowerOutput.contains("cannot connect") || + lowerOutput.contains("is the docker daemon running") || + lowerOutput.contains("connection refused") || + lowerOutput.contains("no such file or directory") { + return "Docker must be running to prune" + } else { + return "Docker prune failed — is Docker running?" + } } + } catch { + return "Docker not found" } - } catch { - lastDockerPruneResult = "Docker not found" - } + }.value // Refresh disk info after prune - diskInfo = DiskInfo.current() + diskInfo = await Task.detached { DiskInfo.current() }.value } func clean() async {