Files

129 lines
5.6 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import SwiftUI
import TermindApplication
import TermindDomain
public struct MetricsGPUSection: View {
public init(server: ServerMetric, initiallyExpanded: Bool = false) {
self.server = server
self.initiallyExpanded = initiallyExpanded
}
let server: ServerMetric
var initiallyExpanded = false
public var body: some View {
VStack(alignment: .leading, spacing: 12) {
if server.gpuState == .failed, server.gpuSnapshot?.devices.isEmpty == false {
Text("GPU metrics unavailable; showing the last sample.")
.font(.caption).foregroundStyle(Color.orange)
}
ForEach(server.gpuSnapshot?.devices ?? []) { gpu in
MetricsGPUCard(
gpu: gpu, cudaVersion: server.gpuSnapshot?.cudaVersion, initiallyExpanded: initiallyExpanded)
}
}
}
}
private struct MetricsGPUCard: View {
let gpu: NvidiaGPU
let cudaVersion: String?
@State private var informationExpanded: Bool
init(gpu: NvidiaGPU, cudaVersion: String?, initiallyExpanded: Bool = false) {
self.gpu = gpu
self.cudaVersion = cudaVersion
_informationExpanded = State(initialValue: initiallyExpanded)
}
public var body: some View {
VStack(alignment: .leading, spacing: 12) {
Label(gpu.name, systemImage: "cpu")
.font(.caption.bold()).lineLimit(1).help(gpu.name)
HStack(spacing: 14) {
MetricsRing(title: "GPU", value: gpu.utilization)
.help(Text("GPU Utilization"))
MetricsRing(title: "VRAM", value: gpu.memoryPercent)
.help(Text("VRAM Occupancy"))
VStack(alignment: .trailing, spacing: 6) {
memoryRow("Used", value: gpu.memoryUsed)
memoryRow("VRAM Total", value: gpu.memoryTotal)
memoryRow("Free Memory", value: gpu.memoryFree)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
GeometryReader { geometry in
let width = max(0, (geometry.size.width - 3 * 12) / 4)
HStack(alignment: .top, spacing: 12) {
meter("GPU Memory Activity", value: gpu.memoryUtilization, color: .purple)
.frame(width: width).help(Text("Memory Utilization"))
meter("GPU Encoder", value: gpu.encoder, color: Color.green)
.frame(width: width).help(Text("Encoder Utilization"))
meter("GPU Decoder", value: gpu.decoder, color: Color.green)
.frame(width: width).help(Text("Decoder Utilization"))
meter("GPU Fan", value: gpu.fan, color: Color.orange)
.frame(width: width).help(Text("Fan Speed"))
}
}
.frame(height: 36)
DisclosureGroup("Device Information", isExpanded: $informationExpanded) {
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading, spacing: 3) {
Text("UUID").foregroundStyle(.secondary)
Text(gpu.id).font(.system(size: 9, design: .monospaced))
.fixedSize(horizontal: false, vertical: true)
}
HStack {
Text("VBIOS").foregroundStyle(.secondary)
Spacer()
Text(gpu.vbios ?? "—").monospacedDigit()
}
HStack {
Text("Driver CUDA Version").foregroundStyle(.secondary)
Spacer()
Text(cudaVersion ?? "—").monospacedDigit()
}
}
.font(.caption2).textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading).padding(.top, 8)
}
.font(.caption).foregroundStyle(.secondary)
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay {
RoundedRectangle(cornerRadius: 12).strokeBorder(.quaternary, lineWidth: 1)
}
}
private func memoryRow(_ title: LocalizedStringResource, value: Double?) -> some View {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(title).font(.system(size: 9)).foregroundStyle(.secondary)
Text(MetricsFormat.bytes(value, style: .memory)).font(
.system(size: 11, weight: .semibold, design: .monospaced)
)
.frame(minWidth: 72, alignment: .trailing)
}
.lineLimit(1).minimumScaleFactor(0.7)
}
private func meter(_ title: LocalizedStringResource, value: Double?, color: Color) -> some View {
VStack(alignment: .leading, spacing: 3) {
Text(title).font(.system(size: 9)).foregroundStyle(.secondary).lineLimit(1)
Text(MetricsFormat.percent(value)).font(.system(size: 11, weight: .semibold, design: .monospaced))
.lineLimit(1).minimumScaleFactor(0.7)
GeometryReader { geometry in
Capsule().fill(.quaternary)
.overlay(alignment: .leading) {
if let value {
Capsule().fill(color).frame(width: geometry.size.width * min(1, max(0, value / 100)))
}
}
}.frame(height: 3).padding(.top, 3).accessibilityHidden(true)
}
}
}