diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..29e01b8 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-28 - Main Thread Blocking in async @MainActor methods +**Learning:** In Swift Concurrency, `async` methods on a `@MainActor` class execute on the main thread. If they perform synchronous blocking operations (like `DiskInfo.current()` which involves `URLResourceValues` I/O, or `Foundation.Process.waitUntilExit()`), they will block the UI and prevent other main actor tasks from running, even though they are inside an `async` function. +**Action:** Always offload synchronous blocking operations from the main thread using `await Task.detached { ... }.value` when executing within a `@MainActor` context. diff --git a/Sources/Cacheout/ViewModels/CacheoutViewModel.swift b/Sources/Cacheout/ViewModels/CacheoutViewModel.swift index 13a9811..a8782f3 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) @@ -241,21 +241,24 @@ class CacheoutViewModel: ObservableObject { ] do { - try process.run() - process.waitUntilExit() - let data = pipe.fileHandleForReading.readDataToEndOfFile() - let output = String(data: data, encoding: .utf8) ?? "" - - if process.terminationStatus == 0 { + let result = try await Task.detached { () -> (Int32, String) in + try process.run() + process.waitUntilExit() + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) ?? "" + return (process.terminationStatus, output) + }.value + + if result.0 == 0 { // Extract "Total reclaimed space:" line - if let line = output.components(separatedBy: "\n") + if let line = result.1.components(separatedBy: "\n") .first(where: { $0.contains("reclaimed") }) { lastDockerPruneResult = line.trimmingCharacters(in: .whitespaces) } else { lastDockerPruneResult = "Docker pruned successfully" } } else { - let lowerOutput = output.lowercased() + let lowerOutput = result.1.lowercased() if lowerOutput.contains("cannot connect") || lowerOutput.contains("is the docker daemon running") || lowerOutput.contains("connection refused") || @@ -270,7 +273,7 @@ class CacheoutViewModel: ObservableObject { } // Refresh disk info after prune - diskInfo = DiskInfo.current() + diskInfo = await Task.detached { DiskInfo.current() }.value } func clean() async {