mirror of
https://github.com/MobAI-App/simslim.git
synced 2026-10-05 22:35:33 +02:00
Three changes from the SwiftSimSlim report in #50, all in the Go backend and the SwiftUI front end. **The shell batching never ran.** `runBatch` spawned `/bin/sh -c` inside the simulator to walk a chunk of labels, but no iOS runtime ships a shell — `RuntimeRoot/bin` holds exactly `df` and `launchctl`, from iOS 18.3 through 27.1. Every spawn failed with LaunchdSimError 111, `parseBatchOK` returned nothing, and all 170 labels fell through to the serial per-label retry pass. Delete the script and its helpers; run the direct spawns through a pool of 8 instead. That is both less code and the actual speedup: `on --no-reboot` over 170 labels on a freshly erased iPhone 17 Pro / iOS 26.5 goes 2m14s -> 37.5s. **A booted device now takes the offline store too.** An override only takes effect at the next boot, so a booted device already owed a shutdown and a boot. `ensure` spends them up front and hands off to `ensureOffline`, which replaces every launchctl spawn for the same two state changes. Measured 16.8s to slim a booted simulator, 15.0s to restore one. It also reads the live state first and returns early when the profile already matches, so re-applying an unchanged profile costs one read and no reboot at all (0.7s). **The GUI no longer freezes on a global busy flag.** Batches reserve every device up front and run two at a time, and controls gate on the devices they would act on rather than on "anything is running", so work on one simulator leaves the others live. `BatchProgress` carries the names in flight instead of a single current name.
210 lines
4.8 KiB
Swift
210 lines
4.8 KiB
Swift
import Foundation
|
|
|
|
enum SlimmingMode: String, CaseIterable, Identifiable {
|
|
case memory = "Memory Use"
|
|
case disk = "Disk Size"
|
|
|
|
var id: String { rawValue }
|
|
}
|
|
|
|
struct SimulatorDevice: Decodable, Identifiable, Equatable {
|
|
let udid: String
|
|
let name: String
|
|
let state: String
|
|
let osVersion: String
|
|
let managedDisabled: Int?
|
|
let managedTotal: Int
|
|
let statusError: String?
|
|
let memory: SimulatorMeasurement?
|
|
let memoryError: String?
|
|
|
|
var id: String { udid }
|
|
var isBooted: Bool { state == "Booted" }
|
|
}
|
|
|
|
struct SlimCategory: Decodable, Identifiable, Equatable {
|
|
let id: String
|
|
let name: String
|
|
let description: String
|
|
let downside: String
|
|
let approxMemoryMB: Int
|
|
let labels: [String]
|
|
let serviceDescriptions: [String: String]?
|
|
let alwaysEnabled: [AlwaysEnabledService]?
|
|
|
|
var alwaysEnabledServices: [AlwaysEnabledService] {
|
|
alwaysEnabled ?? []
|
|
}
|
|
|
|
var approximateMemoryText: String {
|
|
"Uses ~\(approxMemoryMB) MB RAM"
|
|
}
|
|
|
|
func serviceDescription(for label: String) -> String {
|
|
serviceDescriptions?[label] ?? "Apple background service."
|
|
}
|
|
}
|
|
|
|
struct AlwaysEnabledService: Decodable, Identifiable, Equatable {
|
|
let label: String
|
|
let reason: String
|
|
|
|
var id: String { label }
|
|
}
|
|
|
|
struct SimulatorMeasurement: Decodable, Equatable {
|
|
let processes: Int
|
|
let bytes: Int64
|
|
|
|
var memoryText: String {
|
|
ByteCountFormatter.string(fromByteCount: bytes, countStyle: .memory)
|
|
}
|
|
}
|
|
|
|
struct SimulatorDiskMeasurement: Decodable, Equatable {
|
|
let bytes: Int64
|
|
|
|
var sizeText: String {
|
|
ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file)
|
|
}
|
|
}
|
|
|
|
struct DiskCleanupCategory: Decodable, Identifiable, Equatable {
|
|
let id: String
|
|
let name: String
|
|
let description: String
|
|
let downside: String
|
|
let recovery: String
|
|
let risk: String
|
|
let defaultSelected: Bool
|
|
let canClean: Bool
|
|
}
|
|
|
|
struct SimulatorDiskCleanupMeasurement: Decodable, Equatable {
|
|
let id: String
|
|
let bytes: Int64
|
|
let targets: Int
|
|
|
|
var sizeText: String {
|
|
ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file)
|
|
}
|
|
}
|
|
|
|
struct SimulatorDiskStorageMeasurement: Decodable, Identifiable, Equatable {
|
|
let id: String
|
|
let name: String
|
|
let description: String
|
|
let bytes: Int64
|
|
|
|
var sizeText: String {
|
|
ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file)
|
|
}
|
|
|
|
var systemImage: String {
|
|
switch id {
|
|
case "installed-apps": return "app.dashed"
|
|
case "documents": return "doc"
|
|
case "app-data": return "externaldrive"
|
|
case "user-media": return "photo.on.rectangle"
|
|
default: return "folder"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SimulatorDiskCleanupPlan: Decodable, Equatable {
|
|
let udid: String
|
|
let totalBytes: Int64
|
|
let cleanableBytes: Int64
|
|
let categories: [SimulatorDiskCleanupMeasurement]
|
|
let storage: [SimulatorDiskStorageMeasurement]
|
|
|
|
func bytes(for categoryID: String) -> Int64 {
|
|
categories.first(where: { $0.id == categoryID })?.bytes ?? 0
|
|
}
|
|
|
|
func storageBytes(for storageID: String) -> Int64 {
|
|
storage.first(where: { $0.id == storageID })?.bytes ?? 0
|
|
}
|
|
}
|
|
|
|
struct SimulatorDiskCleanupResult: Decodable, Equatable {
|
|
let udid: String
|
|
let categoryIds: [String]
|
|
let beforeBytes: Int64
|
|
let afterBytes: Int64
|
|
let reclaimedBytes: Int64
|
|
let wasBooted: Bool
|
|
let bootStateRestored: Bool
|
|
|
|
var reclaimedText: String {
|
|
ByteCountFormatter.string(fromByteCount: reclaimedBytes, countStyle: .file)
|
|
}
|
|
}
|
|
|
|
struct SimulatorMutationResult: Decodable, Equatable {
|
|
let action: String
|
|
let udid: String
|
|
let name: String?
|
|
let sourceUdid: String?
|
|
}
|
|
|
|
enum ServiceSlimState: Equatable {
|
|
case stock
|
|
case partial(disabled: Int, total: Int)
|
|
case full(total: Int)
|
|
case unknown
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .stock: return "Stock"
|
|
case .partial(let disabled, let total): return "Partial (\(disabled)/\(total))"
|
|
case .full(let total): return "Fully slim (\(total)/\(total))"
|
|
case .unknown: return "Unknown while shutdown"
|
|
}
|
|
}
|
|
|
|
var systemImage: String {
|
|
switch self {
|
|
case .stock: return "circle.fill"
|
|
case .partial: return "circle.lefthalf.filled"
|
|
case .full: return "circle"
|
|
case .unknown: return "questionmark.circle"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct BatchProgress: Equatable {
|
|
let completed: Int
|
|
let total: Int
|
|
/// Names of the simulators in flight right now; a batch runs more than one.
|
|
let running: [String]
|
|
let action: String
|
|
|
|
var fraction: Double {
|
|
guard total > 0 else { return 0 }
|
|
return Double(completed) / Double(total)
|
|
}
|
|
|
|
var runningText: String {
|
|
running.isEmpty ? "\(action)…" : "\(action) \(running.joined(separator: ", "))…"
|
|
}
|
|
}
|
|
|
|
struct ActivityEntry: Identifiable, Equatable {
|
|
enum Level {
|
|
case info
|
|
case success
|
|
case failure
|
|
}
|
|
|
|
let id = UUID()
|
|
let date = Date()
|
|
let level: Level
|
|
let message: String
|
|
}
|
|
|
|
struct PresentedError: Identifiable {
|
|
let id = UUID()
|
|
let message: String
|
|
}
|