feat: moved ANE utilization from read to calculation from power usage

This commit is contained in:
Serhiy Mytrovtsiy
2026-04-24 20:45:45 +02:00
parent 0c721a24c5
commit 685c7ab9fc
+51 -34
View File
@@ -24,6 +24,22 @@ let vendors: [Data: String] = [
Data.init([0x02, 0x10, 0x00, 0x00]): "AMD"
]
private func maxANEPower(for platform: Platform?) -> Double {
switch platform {
case .m1, .m1Pro, .m1Max: return 2.0
case .m1Ultra: return 4.0
case .m2, .m2Pro, .m2Max: return 2.5
case .m2Ultra: return 5.0
case .m3, .m3Pro, .m3Max: return 3.0
case .m3Ultra: return 6.0
case .m4, .m4Pro, .m4Max: return 6.0
case .m4Ultra: return 12.0
case .m5, .m5Pro, .m5Max: return 8.0
case .m5Ultra: return 16.0
default: return 8.0
}
}
internal class InfoReader: Reader<GPUs> {
private var gpus: GPUs = GPUs()
private var displays: [gpu_s] = []
@@ -31,7 +47,9 @@ internal class InfoReader: Reader<GPUs> {
private var aneChannels: CFMutableDictionary? = nil
private var aneSubscription: IOReportSubscriptionRef? = nil
private var previousANEResidencies: [(on: Int64, total: Int64)] = []
private var previousANEEnergy: Double = 0
private var previousANERead: Date? = nil
private var aneMaxPower: Double = 8.0
private var framesChannels: CFMutableDictionary? = nil
private var framesSubscription: IOReportSubscriptionRef? = nil
@@ -49,6 +67,7 @@ internal class InfoReader: Reader<GPUs> {
let devices = PCIdevices.filter{ $0.object(forKey: "IOName") as? String == "display" }
#if arch(arm64)
self.aneMaxPower = maxANEPower(for: SystemKit.shared.device.platform)
self.setupANE()
self.setupFrames()
#endif
@@ -220,10 +239,11 @@ internal class InfoReader: Reader<GPUs> {
}
#if arch(arm64)
let aneValue = self.readANEUtilization()
let anePower = self.readANEPower()
let aneUtil = anePower.map { min(1.0, max(0.0, $0 / self.aneMaxPower)) }
let fpsValue = self.readFrames()
for i in self.gpus.list.indices where self.gpus.list[i].IOClass.lowercased().contains("agx") {
self.gpus.list[i].aneUtilization = aneValue ?? 0
self.gpus.list[i].aneUtilization = aneUtil ?? 0
self.gpus.list[i].fps = fpsValue
}
#endif
@@ -288,10 +308,10 @@ internal class InfoReader: Reader<GPUs> {
return Double(delta) / elapsed
}
// MARK: - ANE utilization
// MARK: - ANE power
private func setupANE() {
guard let channel = IOReportCopyChannelsInGroup("SoC Stats" as CFString, "Cluster Power States" as CFString, 0, 0, 0)?.takeRetainedValue() else { return }
guard let channel = IOReportCopyChannelsInGroup("Energy Model" as CFString, nil, 0, 0, 0)?.takeRetainedValue() else { return }
let size = CFDictionaryGetCount(channel)
guard let mutable = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, size, channel),
@@ -303,7 +323,7 @@ internal class InfoReader: Reader<GPUs> {
sub?.release()
}
private func readANEUtilization() -> Double? {
private func readANEPower() -> Double? {
guard let subscription = self.aneSubscription,
let channels = self.aneChannels,
let reportSample = IOReportCreateSamples(subscription, channels, nil)?.takeRetainedValue(),
@@ -312,48 +332,45 @@ internal class InfoReader: Reader<GPUs> {
}
let items = dict["IOReportChannels"] as! CFArray
var currentResidencies: [(on: Int64, total: Int64)] = []
var currentEnergy: Double = 0
var found = false
for i in 0..<CFArrayGetCount(items) {
let item = unsafeBitCast(CFArrayGetValueAtIndex(items, i), to: CFDictionary.self)
guard let group = IOReportChannelGetGroup(item)?.takeUnretainedValue() as? String,
group == "SoC Stats",
let subgroup = IOReportChannelGetSubGroup(item)?.takeUnretainedValue() as? String,
subgroup == "Cluster Power States",
group == "Energy Model",
let channel = IOReportChannelGetChannelName(item)?.takeUnretainedValue() as? String,
channel.hasPrefix("ANE") else { continue }
channel.starts(with: "ANE") else { continue }
let stateCount = IOReportStateGetCount(item)
guard stateCount == 2 else { continue }
let raw = Double(IOReportSimpleGetIntegerValue(item, 0))
let unit = (IOReportChannelGetUnitLabel(item)?.takeUnretainedValue() as? String)?
.trimmingCharacters(in: .whitespaces) ?? ""
var on: Int64 = 0
var total: Int64 = 0
for s in 0..<stateCount {
let residency = IOReportStateGetResidency(item, s)
let name = IOReportStateGetNameForIndex(item, s)?.takeUnretainedValue() as? String ?? ""
total += residency
if name != "INACT" {
on += residency
}
let joules: Double
switch unit.lowercased() {
case "mj": joules = raw / 1e3
case "uj", "µj": joules = raw / 1e6
case "nj": joules = raw / 1e9
case "pj": joules = raw / 1e12
default: joules = raw / 1e9
}
currentResidencies.append((on: on, total: total))
currentEnergy += joules
found = true
}
guard !currentResidencies.isEmpty else { return nil }
guard found else { return nil }
defer { self.previousANEResidencies = currentResidencies }
guard self.previousANEResidencies.count == currentResidencies.count else { return nil }
var totalDeltaOn: Int64 = 0
var totalDeltaAll: Int64 = 0
for i in 0..<currentResidencies.count {
totalDeltaOn += currentResidencies[i].on - self.previousANEResidencies[i].on
totalDeltaAll += currentResidencies[i].total - self.previousANEResidencies[i].total
let now = Date()
defer {
self.previousANEEnergy = currentEnergy
self.previousANERead = now
}
guard totalDeltaAll > 0 else { return 0 }
return Double(totalDeltaOn) / Double(totalDeltaAll)
guard let previousRead = self.previousANERead else { return 0 }
let elapsed = now.timeIntervalSince(previousRead)
guard elapsed > 0 else { return 0 }
return (currentEnergy - self.previousANEEnergy) / elapsed
}
}