381 lines
16 KiB
Swift
381 lines
16 KiB
Swift
import Foundation
|
|
|
|
public nonisolated struct MetricsSelection: Codable, Hashable, Identifiable, Sendable {
|
|
public var id: UUID
|
|
public var enabled = true
|
|
/// nil selects all default devices; an empty set deliberately selects none.
|
|
public var disks: Set<String>?
|
|
public var interfaces: Set<String>?
|
|
public init(id: UUID, enabled: Bool = true, disks: Set<String>? = nil, interfaces: Set<String>? = nil) {
|
|
self.id = id
|
|
self.enabled = enabled
|
|
self.disks = disks
|
|
self.interfaces = interfaces
|
|
}
|
|
|
|
}
|
|
|
|
public nonisolated struct MetricsCounters: Equatable, Sendable {
|
|
public var read: UInt64
|
|
public var write: UInt64
|
|
public var readOperations: UInt64?
|
|
public var writeOperations: UInt64?
|
|
public var busyMilliseconds: UInt64?
|
|
public var readMilliseconds: UInt64?
|
|
public var writeMilliseconds: UInt64?
|
|
public init(
|
|
read: UInt64, write: UInt64, readOperations: UInt64? = nil, writeOperations: UInt64? = nil,
|
|
busyMilliseconds: UInt64? = nil, readMilliseconds: UInt64? = nil, writeMilliseconds: UInt64? = nil
|
|
) {
|
|
self.read = read
|
|
self.write = write
|
|
self.readOperations = readOperations
|
|
self.writeOperations = writeOperations
|
|
self.busyMilliseconds = busyMilliseconds
|
|
self.readMilliseconds = readMilliseconds
|
|
self.writeMilliseconds = writeMilliseconds
|
|
}
|
|
|
|
}
|
|
|
|
public nonisolated struct MetricsSample: Sendable {
|
|
public var uptime: Double?
|
|
public var cpu: [UInt64]?
|
|
public var coreCount: Int?
|
|
public var load: [Double] = []
|
|
public var memoryFree: Double?
|
|
public var memoryCached: Double?
|
|
public var memoryUsed: Double?
|
|
public var memoryTotal: Double?
|
|
public var swapFree: Double?
|
|
public var swapTotal: Double?
|
|
public var disks: [String: MetricsCounters]?
|
|
public var interfaces: [String: MetricsCounters]?
|
|
public var blocks: [String: MetricsCounters]?
|
|
public var interfaceKinds: [String: MetricsInterfaceKind] = [:]
|
|
public var interfaceAddresses: [String: [String]] = [:]
|
|
public var tcp: MetricsTCP?
|
|
public init(
|
|
uptime: Double? = nil, cpu: [UInt64]? = nil, coreCount: Int? = nil, load: [Double] = [],
|
|
memoryFree: Double? = nil, memoryCached: Double? = nil, memoryUsed: Double? = nil, memoryTotal: Double? = nil,
|
|
swapFree: Double? = nil, swapTotal: Double? = nil,
|
|
disks: [String: MetricsCounters]? = nil, interfaces: [String: MetricsCounters]? = nil,
|
|
blocks: [String: MetricsCounters]? = nil, interfaceKinds: [String: MetricsInterfaceKind] = [:],
|
|
interfaceAddresses: [String: [String]] = [:], tcp: MetricsTCP? = nil
|
|
) {
|
|
self.uptime = uptime
|
|
self.cpu = cpu
|
|
self.coreCount = coreCount
|
|
self.load = load
|
|
self.memoryFree = memoryFree
|
|
self.memoryCached = memoryCached
|
|
self.memoryUsed = memoryUsed
|
|
self.memoryTotal = memoryTotal
|
|
self.swapFree = swapFree
|
|
self.swapTotal = swapTotal
|
|
self.disks = disks
|
|
self.interfaces = interfaces
|
|
self.blocks = blocks
|
|
self.interfaceKinds = interfaceKinds
|
|
self.interfaceAddresses = interfaceAddresses
|
|
self.tcp = tcp
|
|
}
|
|
|
|
}
|
|
|
|
public nonisolated struct NvidiaGPU: Identifiable, Equatable, Sendable {
|
|
public var id: String
|
|
public var name: String
|
|
public var utilization: Double?
|
|
public var memoryUsed: Double?
|
|
public var memoryTotal: Double?
|
|
public var memoryFree: Double?
|
|
public var memoryUtilization: Double?
|
|
public var encoder: Double?
|
|
public var decoder: Double?
|
|
public var fan: Double?
|
|
public var vbios: String?
|
|
public var memoryPercent: Double? {
|
|
guard let memoryUsed, let memoryTotal, memoryTotal > 0 else { return nil }
|
|
return min(100, max(0, memoryUsed / memoryTotal * 100))
|
|
}
|
|
public init(
|
|
id: String, name: String, utilization: Double? = nil, memoryUsed: Double? = nil, memoryTotal: Double? = nil,
|
|
memoryFree: Double? = nil, memoryUtilization: Double? = nil, encoder: Double? = nil, decoder: Double? = nil,
|
|
fan: Double? = nil, vbios: String? = nil
|
|
) {
|
|
self.id = id
|
|
self.name = name
|
|
self.utilization = utilization
|
|
self.memoryUsed = memoryUsed
|
|
self.memoryTotal = memoryTotal
|
|
self.memoryFree = memoryFree
|
|
self.memoryUtilization = memoryUtilization
|
|
self.encoder = encoder
|
|
self.decoder = decoder
|
|
self.fan = fan
|
|
self.vbios = vbios
|
|
}
|
|
|
|
}
|
|
|
|
public nonisolated struct NvidiaSnapshot: Sendable {
|
|
public var cudaVersion: String?
|
|
public var devices: [NvidiaGPU]
|
|
public init(cudaVersion: String? = nil, devices: [NvidiaGPU]) {
|
|
self.cudaVersion = cudaVersion
|
|
self.devices = devices
|
|
}
|
|
|
|
}
|
|
|
|
public nonisolated enum MetricsGPUState: Equatable, Sendable {
|
|
case probing, available, unavailable, failed
|
|
}
|
|
|
|
public nonisolated struct MetricsDevicePoint: Identifiable, Sendable {
|
|
public var id: String
|
|
public var read: Double?
|
|
public var write: Double?
|
|
public var readTotal: Double
|
|
public var writeTotal: Double
|
|
public var readIOPS: Double?
|
|
public var writeIOPS: Double?
|
|
public var busy: Double?
|
|
public var readWait: Double?
|
|
public var writeWait: Double?
|
|
public var kind: MetricsInterfaceKind = .unknown
|
|
public var addresses: [String] = []
|
|
public init(
|
|
id: String, read: Double? = nil, write: Double? = nil, readTotal: Double, writeTotal: Double,
|
|
readIOPS: Double? = nil, writeIOPS: Double? = nil, busy: Double? = nil, readWait: Double? = nil,
|
|
writeWait: Double? = nil, kind: MetricsInterfaceKind = .unknown, addresses: [String] = []
|
|
) {
|
|
self.id = id
|
|
self.read = read
|
|
self.write = write
|
|
self.readTotal = readTotal
|
|
self.writeTotal = writeTotal
|
|
self.readIOPS = readIOPS
|
|
self.writeIOPS = writeIOPS
|
|
self.busy = busy
|
|
self.readWait = readWait
|
|
self.writeWait = writeWait
|
|
self.kind = kind
|
|
self.addresses = addresses
|
|
}
|
|
|
|
}
|
|
|
|
public nonisolated struct MetricsPoint: Identifiable, Sendable {
|
|
public var id: Date { date }
|
|
public var date: Date
|
|
public var cpu: Double?
|
|
public var memory: Double?
|
|
public var memoryUsed: Double?
|
|
public var memoryTotal: Double?
|
|
public var swapPercent: Double?
|
|
public var swapUsed: Double?
|
|
public var swapTotal: Double?
|
|
public var swapFree: Double?
|
|
public var diskRead: Double?
|
|
public var diskWrite: Double?
|
|
public var networkReceive: Double?
|
|
public var networkSend: Double?
|
|
public var gpus: [NvidiaGPU] = []
|
|
public var uptime: Double?
|
|
public var coreCount: Int?
|
|
public var load: [Double] = []
|
|
public var cpuUser: Double?
|
|
public var cpuSystem: Double?
|
|
public var cpuWait: Double?
|
|
public var cpuSteal: Double?
|
|
public var cpuIdle: Double?
|
|
public var memoryFree: Double?
|
|
public var memoryCached: Double?
|
|
public var diskDevices: [MetricsDevicePoint] = []
|
|
public var networkDevices: [MetricsDevicePoint] = []
|
|
public var blockDevices: [String: MetricsDevicePoint] = [:]
|
|
public var networkSentTotal: Double?
|
|
public var networkReceivedTotal: Double?
|
|
public var tcp: MetricsTCP?
|
|
public var tcpRetransmissionPercent: Double?
|
|
public init(
|
|
date: Date, cpu: Double? = nil, memory: Double? = nil, memoryUsed: Double? = nil, memoryTotal: Double? = nil,
|
|
swapPercent: Double? = nil, swapUsed: Double? = nil, swapTotal: Double? = nil, swapFree: Double? = nil,
|
|
diskRead: Double? = nil, diskWrite: Double? = nil, networkReceive: Double? = nil, networkSend: Double? = nil,
|
|
gpus: [NvidiaGPU] = [], uptime: Double? = nil, coreCount: Int? = nil, load: [Double] = [],
|
|
cpuUser: Double? = nil, cpuSystem: Double? = nil, cpuWait: Double? = nil, cpuSteal: Double? = nil,
|
|
cpuIdle: Double? = nil, memoryFree: Double? = nil, memoryCached: Double? = nil,
|
|
diskDevices: [MetricsDevicePoint] = [], networkDevices: [MetricsDevicePoint] = [],
|
|
blockDevices: [String: MetricsDevicePoint] = [:], networkSentTotal: Double? = nil,
|
|
networkReceivedTotal: Double? = nil, tcp: MetricsTCP? = nil, tcpRetransmissionPercent: Double? = nil
|
|
) {
|
|
self.date = date
|
|
self.cpu = cpu
|
|
self.memory = memory
|
|
self.memoryUsed = memoryUsed
|
|
self.memoryTotal = memoryTotal
|
|
self.swapPercent = swapPercent
|
|
self.swapUsed = swapUsed
|
|
self.swapTotal = swapTotal
|
|
self.swapFree = swapFree
|
|
self.diskRead = diskRead
|
|
self.diskWrite = diskWrite
|
|
self.networkReceive = networkReceive
|
|
self.networkSend = networkSend
|
|
self.gpus = gpus
|
|
self.uptime = uptime
|
|
self.coreCount = coreCount
|
|
self.load = load
|
|
self.cpuUser = cpuUser
|
|
self.cpuSystem = cpuSystem
|
|
self.cpuWait = cpuWait
|
|
self.cpuSteal = cpuSteal
|
|
self.cpuIdle = cpuIdle
|
|
self.memoryFree = memoryFree
|
|
self.memoryCached = memoryCached
|
|
self.diskDevices = diskDevices
|
|
self.networkDevices = networkDevices
|
|
self.blockDevices = blockDevices
|
|
self.networkSentTotal = networkSentTotal
|
|
self.networkReceivedTotal = networkReceivedTotal
|
|
self.tcp = tcp
|
|
self.tcpRetransmissionPercent = tcpRetransmissionPercent
|
|
}
|
|
|
|
}
|
|
|
|
public nonisolated enum MetricsCalculation {
|
|
public static func point(
|
|
previous: MetricsSample?, current: MetricsSample, selection: MetricsSelection, date: Date
|
|
) -> MetricsPoint {
|
|
var point = MetricsPoint(date: date)
|
|
point.uptime = current.uptime
|
|
point.coreCount = current.coreCount
|
|
point.load = current.load
|
|
point.memoryFree = current.memoryFree
|
|
point.memoryCached = current.memoryCached
|
|
point.diskDevices = devices(nil, current.disks, selection.disks, elapsed: nil)
|
|
point.networkDevices = networkDevices(nil, current, elapsed: nil)
|
|
point.tcp = current.tcp
|
|
let hardware = Set(current.interfaceKinds.filter { $0.value == .hardware }.keys)
|
|
let aggregateInterfaces = selection.interfaces ?? (hardware.isEmpty ? nil : hardware)
|
|
if let interfaces = current.interfaces {
|
|
let names = aggregateInterfaces ?? Set(interfaces.keys)
|
|
point.networkSentTotal = names.reduce(0) { $0 + Double(interfaces[$1]?.write ?? 0) }
|
|
point.networkReceivedTotal = names.reduce(0) { $0 + Double(interfaces[$1]?.read ?? 0) }
|
|
}
|
|
point.blockDevices = Dictionary(
|
|
uniqueKeysWithValues: devices(nil, current.blocks, nil, elapsed: nil).map { ($0.id, $0) })
|
|
point.memoryUsed = current.memoryUsed
|
|
point.memoryTotal = current.memoryTotal
|
|
if let used = current.memoryUsed, let total = current.memoryTotal, total > 0 {
|
|
point.memory = min(100, max(0, used / total * 100))
|
|
}
|
|
point.swapFree = current.swapFree
|
|
point.swapTotal = current.swapTotal
|
|
if let free = current.swapFree, let total = current.swapTotal {
|
|
let used = min(total, max(0, total - free))
|
|
point.swapUsed = used
|
|
if total > 0 { point.swapPercent = min(100, max(0, used / total * 100)) }
|
|
}
|
|
guard let previous, let before = previous.uptime, let after = current.uptime,
|
|
after > before
|
|
else { return point }
|
|
let elapsed = after - before
|
|
if let old = previous.cpu, let new = current.cpu, old.count >= 8, new.count >= 8,
|
|
(0..<8).allSatisfy({ new[$0] >= old[$0] })
|
|
{
|
|
let delta = (0..<8).map { Double(new[$0] - old[$0]) }
|
|
let total = delta.reduce(0, +)
|
|
if total > 0 {
|
|
point.cpu = max(0, min(100, (total - delta[3] - delta[4]) / total * 100))
|
|
point.cpuUser = (delta[0] + delta[1]) / total * 100
|
|
point.cpuSystem = (delta[2] + delta[5] + delta[6]) / total * 100
|
|
point.cpuWait = delta[4] / total * 100
|
|
point.cpuSteal = delta[7] / total * 100
|
|
point.cpuIdle = delta[3] / total * 100
|
|
}
|
|
}
|
|
point.diskDevices = devices(previous.disks, current.disks, selection.disks, elapsed: elapsed)
|
|
point.networkDevices = networkDevices(previous, current, elapsed: elapsed)
|
|
point.blockDevices = Dictionary(
|
|
uniqueKeysWithValues: devices(previous.blocks, current.blocks, nil, elapsed: elapsed).map { ($0.id, $0) })
|
|
if let oldSent = previous.tcp?.sent, let sent = current.tcp?.sent, sent > oldSent,
|
|
let oldRetransmitted = previous.tcp?.retransmitted, let retransmitted = current.tcp?.retransmitted,
|
|
retransmitted >= oldRetransmitted
|
|
{
|
|
point.tcpRetransmissionPercent = min(
|
|
100, Double(retransmitted - oldRetransmitted) / Double(sent - oldSent) * 100)
|
|
}
|
|
let disk = rate(previous.disks, current.disks, selection.disks, elapsed)
|
|
point.diskRead = disk?.0
|
|
point.diskWrite = disk?.1
|
|
let net = rate(previous.interfaces, current.interfaces, aggregateInterfaces, elapsed)
|
|
point.networkReceive = net?.0
|
|
point.networkSend = net?.1
|
|
return point
|
|
}
|
|
|
|
private static func networkDevices(_ old: MetricsSample?, _ current: MetricsSample, elapsed: Double?)
|
|
-> [MetricsDevicePoint]
|
|
{
|
|
devices(old?.interfaces, current.interfaces, nil, elapsed: elapsed).map { value in
|
|
var result = value
|
|
result.kind = current.interfaceKinds[value.id] ?? .unknown
|
|
result.addresses = current.interfaceAddresses[value.id] ?? []
|
|
return result
|
|
}
|
|
}
|
|
|
|
private static func devices(
|
|
_ old: [String: MetricsCounters]?, _ current: [String: MetricsCounters]?,
|
|
_ selected: Set<String>?, elapsed: Double?
|
|
) -> [MetricsDevicePoint] {
|
|
(current ?? [:]).keys.sorted().filter { selected?.contains($0) ?? true }.compactMap { name in
|
|
guard let value = current?[name] else { return nil }
|
|
let before = old?[name]
|
|
func delta(_ oldValue: UInt64?, _ newValue: UInt64?) -> Double? {
|
|
guard let oldValue, let newValue, newValue >= oldValue, let elapsed, elapsed > 0 else { return nil }
|
|
return Double(newValue - oldValue) / elapsed
|
|
}
|
|
func wait(_ oldTime: UInt64?, _ newTime: UInt64?, _ oldCount: UInt64?, _ newCount: UInt64?) -> Double? {
|
|
guard let oldTime, let newTime, newTime >= oldTime, let oldCount, let newCount, newCount > oldCount
|
|
else { return nil }
|
|
return Double(newTime - oldTime) / Double(newCount - oldCount)
|
|
}
|
|
return MetricsDevicePoint(
|
|
id: name, read: delta(before?.read, value.read), write: delta(before?.write, value.write),
|
|
readTotal: Double(value.read), writeTotal: Double(value.write),
|
|
readIOPS: delta(before?.readOperations, value.readOperations),
|
|
writeIOPS: delta(before?.writeOperations, value.writeOperations),
|
|
busy: delta(before?.busyMilliseconds, value.busyMilliseconds).map { min(100, $0 / 10) },
|
|
readWait: wait(
|
|
before?.readMilliseconds, value.readMilliseconds, before?.readOperations, value.readOperations),
|
|
writeWait: wait(
|
|
before?.writeMilliseconds, value.writeMilliseconds, before?.writeOperations, value.writeOperations)
|
|
)
|
|
}
|
|
}
|
|
|
|
private static func rate(
|
|
_ old: [String: MetricsCounters]?, _ new: [String: MetricsCounters]?,
|
|
_ selected: Set<String>?, _ elapsed: Double
|
|
) -> (Double, Double)? {
|
|
guard let old, let new else { return nil }
|
|
let names = selected ?? Set(new.keys)
|
|
guard !names.isEmpty else { return nil }
|
|
var read = 0.0
|
|
var write = 0.0
|
|
for name in names {
|
|
guard let before = old[name], let after = new[name],
|
|
after.read >= before.read, after.write >= before.write
|
|
else { return nil }
|
|
read += Double(after.read - before.read) / elapsed
|
|
write += Double(after.write - before.write) / elapsed
|
|
}
|
|
return (read, write)
|
|
}
|
|
}
|