From c343ac7c610c969432521b4642702910e237db5c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 05:15:15 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Offload=20blocking=20I/O=20?= =?UTF-8?q?off=20the=20@MainActor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wrapped `DiskInfo.current()`, `Process` execution, `waitUntilExit()`, and `Pipe` reads inside `Task.detached { ... }` in `CacheoutViewModel`. - This ensures these synchronous blocking I/O calls run on the background concurrent pool rather than blocking the main UI thread. - Results are safely awaited and assigned back to the `@MainActor` variables (`diskInfo` and `lastDockerPruneResult`). Co-authored-by: acebytes <2820910+acebytes@users.noreply.github.com> --- .jules/bolt.md | 3 + .../ViewModels/CacheoutViewModel.swift | 78 ++++++++++--------- 2 files changed, 43 insertions(+), 38 deletions(-) create mode 100644 .jules/bolt.md 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 {