- Add AppDiscovery provider for running app enumeration - Implement AppDropdownView with auto-launch functionality - Create SignalAction models for 40+ yabai commands - Build ActionBuilderView with nested parameter controls - Add LiveShellPreview for real-time shell command generation - Implement ActionValidator for conflict detection - Add migration parser for existing raw action strings - Include feature flag for safe rollout - Maintain full backward compatibility
328 lines
10 KiB
Swift
328 lines
10 KiB
Swift
//
|
|
// PerformanceMonitor.swift
|
|
// YabaiPro
|
|
//
|
|
// Created by Jake Shore
|
|
// Copyright © 2024 Jake Shore. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
import IOKit.ps
|
|
|
|
class PerformanceMonitor: ObservableObject {
|
|
static let shared = PerformanceMonitor()
|
|
|
|
private var monitoringTimer: Timer?
|
|
private var isMonitoring = false
|
|
private var samples: [PerformanceSample] = []
|
|
private var maxSamples = 100 // Keep last 100 samples (about 10 seconds at 10Hz)
|
|
|
|
struct PerformanceSample {
|
|
let timestamp: Date
|
|
let cpuUsage: Double
|
|
let gpuUsage: Double
|
|
let memoryUsage: Double
|
|
let thermalState: ProcessInfo.ThermalState
|
|
let batteryLevel: Int
|
|
let frameRate: Double
|
|
}
|
|
|
|
func startMonitoring(updateInterval: TimeInterval = 0.1) {
|
|
guard !isMonitoring else { return }
|
|
|
|
isMonitoring = true
|
|
samples.removeAll()
|
|
|
|
monitoringTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { [weak self] _ in
|
|
self?.captureSample()
|
|
}
|
|
}
|
|
|
|
func stopMonitoring() {
|
|
isMonitoring = false
|
|
monitoringTimer?.invalidate()
|
|
monitoringTimer = nil
|
|
}
|
|
|
|
private func captureSample() {
|
|
let sample = PerformanceSample(
|
|
timestamp: Date(),
|
|
cpuUsage: getCPUUsage(),
|
|
gpuUsage: getGPUUsage(),
|
|
memoryUsage: getMemoryUsage(),
|
|
thermalState: ProcessInfo.processInfo.thermalState,
|
|
batteryLevel: getBatteryLevel(),
|
|
frameRate: 60.0 // Placeholder - would need actual frame timing
|
|
)
|
|
|
|
samples.append(sample)
|
|
|
|
// Keep only recent samples
|
|
if samples.count > maxSamples {
|
|
samples.removeFirst(samples.count - maxSamples)
|
|
}
|
|
}
|
|
|
|
func getRecentAverages(seconds: TimeInterval = 5.0) -> PerformanceMetrics {
|
|
let cutoff = Date().addingTimeInterval(-seconds)
|
|
let recentSamples = samples.filter { $0.timestamp > cutoff }
|
|
|
|
guard !recentSamples.isEmpty else {
|
|
return PerformanceMetrics(
|
|
cpuUsage: 0,
|
|
gpuUsage: 0,
|
|
frameRate: 60,
|
|
thermalState: .nominal,
|
|
batteryLevel: 100
|
|
)
|
|
}
|
|
|
|
let avgCPU = recentSamples.map { $0.cpuUsage }.reduce(0, +) / Double(recentSamples.count)
|
|
let avgGPU = recentSamples.map { $0.gpuUsage }.reduce(0, +) / Double(recentSamples.count)
|
|
let avgFrameRate = recentSamples.map { $0.frameRate }.reduce(0, +) / Double(recentSamples.count)
|
|
let worstThermalState = recentSamples.map { $0.thermalState.rawValue }.max() ?? 0
|
|
let avgBatteryLevel = Int(recentSamples.map { Double($0.batteryLevel) }.reduce(0, +) / Double(recentSamples.count))
|
|
|
|
return PerformanceMetrics(
|
|
cpuUsage: avgCPU,
|
|
gpuUsage: avgGPU,
|
|
frameRate: avgFrameRate,
|
|
thermalState: ProcessInfo.ThermalState(rawValue: worstThermalState) ?? .nominal,
|
|
batteryLevel: avgBatteryLevel
|
|
)
|
|
}
|
|
|
|
func getPerformanceReport() -> String {
|
|
let metrics = getRecentAverages()
|
|
|
|
return """
|
|
Performance Report (last 5 seconds):
|
|
CPU Usage: \(String(format: "%.1f", metrics.cpuUsage))%
|
|
GPU Usage: \(String(format: "%.1f", metrics.gpuUsage))%
|
|
Frame Rate: \(String(format: "%.1f", metrics.frameRate)) FPS
|
|
Thermal State: \(metrics.thermalState.description)
|
|
Battery Level: \(metrics.batteryLevel)%
|
|
|
|
Samples collected: \(samples.count)
|
|
Monitoring active: \(isMonitoring)
|
|
"""
|
|
}
|
|
|
|
// MARK: - System Metrics (Simplified implementations)
|
|
|
|
private func getCPUUsage() -> Double {
|
|
// Simplified CPU usage - in production, use host_statistics
|
|
// This returns a mock value based on system load
|
|
return Double.random(in: 5...35)
|
|
}
|
|
|
|
private func getGPUUsage() -> Double {
|
|
// GPU usage would require Metal system integration
|
|
// Mock implementation
|
|
return Double.random(in: 2...25)
|
|
}
|
|
|
|
private func getMemoryUsage() -> Double {
|
|
// Simplified memory usage - in production, use more accurate system calls
|
|
return 45.0 // Placeholder percentage
|
|
}
|
|
|
|
private func getBatteryLevel() -> Int {
|
|
let service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSmartBattery"))
|
|
guard service != 0 else { return 100 }
|
|
|
|
defer { IOObjectRelease(service) }
|
|
|
|
if let currentCapacity = IORegistryEntryCreateCFProperty(service, "CurrentCapacity" as CFString, kCFAllocatorDefault, 0)?.takeUnretainedValue(),
|
|
let maxCapacity = IORegistryEntryCreateCFProperty(service, "MaxCapacity" as CFString, kCFAllocatorDefault, 0)?.takeUnretainedValue() {
|
|
|
|
let current = (currentCapacity as? Int) ?? 0
|
|
let max = (maxCapacity as? Int) ?? 100
|
|
|
|
if max > 0 {
|
|
return (current * 100) / max
|
|
}
|
|
}
|
|
|
|
return 100
|
|
}
|
|
|
|
func shouldReduceAnimations() -> Bool {
|
|
let metrics = getRecentAverages()
|
|
|
|
return metrics.cpuUsage > 70 ||
|
|
metrics.gpuUsage > 60 ||
|
|
metrics.thermalState == .serious ||
|
|
metrics.thermalState == .critical ||
|
|
metrics.batteryLevel < 15
|
|
}
|
|
|
|
func getOptimizationSuggestions() -> [String] {
|
|
var suggestions: [String] = []
|
|
let metrics = getRecentAverages()
|
|
|
|
if metrics.cpuUsage > 60 {
|
|
suggestions.append("High CPU usage detected - consider reducing particle count")
|
|
}
|
|
|
|
if metrics.gpuUsage > 50 {
|
|
suggestions.append("High GPU usage detected - consider disabling Metal effects")
|
|
}
|
|
|
|
if metrics.thermalState == .serious || metrics.thermalState == .critical {
|
|
suggestions.append("System running hot - animations automatically reduced")
|
|
}
|
|
|
|
if metrics.batteryLevel < 20 {
|
|
suggestions.append("Low battery - switch to minimal animation preset")
|
|
}
|
|
|
|
if metrics.frameRate < 50 {
|
|
suggestions.append("Low frame rate detected - reduce animation quality")
|
|
}
|
|
|
|
if suggestions.isEmpty {
|
|
suggestions.append("Performance looks good - no optimization needed")
|
|
}
|
|
|
|
return suggestions
|
|
}
|
|
}
|
|
|
|
// MARK: - Performance Dashboard View
|
|
|
|
struct PerformanceDashboardView: View {
|
|
@StateObject private var monitor = PerformanceMonitor.shared
|
|
@State private var isMonitoring = false
|
|
@State private var performanceReport = ""
|
|
|
|
var body: some View {
|
|
VStack(spacing: 20) {
|
|
HStack {
|
|
Text("Performance Monitor")
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
|
|
Spacer()
|
|
|
|
Button(action: {
|
|
if isMonitoring {
|
|
monitor.stopMonitoring()
|
|
} else {
|
|
monitor.startMonitoring()
|
|
}
|
|
isMonitoring.toggle()
|
|
}) {
|
|
Label(isMonitoring ? "Stop Monitoring" : "Start Monitoring",
|
|
systemImage: isMonitoring ? "stop.fill" : "play.fill")
|
|
}
|
|
|
|
Button(action: {
|
|
performanceReport = monitor.getPerformanceReport()
|
|
}) {
|
|
Label("Generate Report", systemImage: "doc.text")
|
|
}
|
|
}
|
|
|
|
if isMonitoring {
|
|
LiveMetricsView()
|
|
}
|
|
|
|
if !performanceReport.isEmpty {
|
|
ScrollView {
|
|
Text(performanceReport)
|
|
.font(.system(.body, design: .monospaced))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding()
|
|
.background(Color(.textBackgroundColor))
|
|
.cornerRadius(8)
|
|
}
|
|
.frame(height: 200)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Optimization Suggestions")
|
|
.font(.headline)
|
|
|
|
ForEach(monitor.getOptimizationSuggestions(), id: \.self) { suggestion in
|
|
Text("• \(suggestion)")
|
|
.font(.body)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.controlBackgroundColor))
|
|
.cornerRadius(8)
|
|
}
|
|
.padding()
|
|
.frame(minWidth: 500, minHeight: 400)
|
|
.onDisappear {
|
|
monitor.stopMonitoring()
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LiveMetricsView: View {
|
|
@StateObject private var monitor = PerformanceMonitor.shared
|
|
@State private var metrics = PerformanceMetrics(
|
|
cpuUsage: 0, gpuUsage: 0, frameRate: 60,
|
|
thermalState: .nominal, batteryLevel: 100
|
|
)
|
|
|
|
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
|
|
|
|
var body: some View {
|
|
VStack(spacing: 16) {
|
|
HStack(spacing: 20) {
|
|
MetricView(label: "CPU", value: String(format: "%.1f%%", metrics.cpuUsage), color: .blue)
|
|
MetricView(label: "GPU", value: String(format: "%.1f%%", metrics.gpuUsage), color: .green)
|
|
MetricView(label: "FPS", value: String(format: "%.0f", metrics.frameRate), color: .orange)
|
|
MetricView(label: "Battery", value: "\(metrics.batteryLevel)%", color: .purple)
|
|
}
|
|
|
|
HStack {
|
|
Text("Thermal State:")
|
|
Text(metrics.thermalState.description)
|
|
.foregroundColor(thermalStateColor(metrics.thermalState))
|
|
.fontWeight(.semibold)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.controlBackgroundColor))
|
|
.cornerRadius(8)
|
|
.onReceive(timer) { _ in
|
|
metrics = monitor.getRecentAverages(seconds: 1.0)
|
|
}
|
|
}
|
|
|
|
private func thermalStateColor(_ state: ProcessInfo.ThermalState) -> Color {
|
|
switch state {
|
|
case .nominal: return .green
|
|
case .fair: return .yellow
|
|
case .serious: return .orange
|
|
case .critical: return .red
|
|
@unknown default: return .gray
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MetricView: View {
|
|
let label: String
|
|
let value: String
|
|
let color: Color
|
|
|
|
var body: some View {
|
|
VStack {
|
|
Text(label)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text(value)
|
|
.font(.title2)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(color)
|
|
}
|
|
.frame(width: 80)
|
|
}
|
|
}
|