feat: added ANE utilization to the GPU module (#2897)

This commit is contained in:
Serhiy Mytrovtsiy
2026-04-09 17:46:26 +02:00
parent cf9c858bda
commit abbdfc1e75
6 changed files with 154 additions and 2 deletions
+30
View File
@@ -0,0 +1,30 @@
//
// bridge.h
// GPU
//
// Created by Serhiy Mytrovtsiy on 02/04/2026
// Using Swift 6.0
// Running on macOS 15.1
//
// Copyright © 2026 Serhiy Mytrovtsiy. All rights reserved.
//
#ifndef bridge_h
#define bridge_h
#include <CoreFoundation/CoreFoundation.h>
typedef struct IOReportSubscriptionRef* IOReportSubscriptionRef;
CFDictionaryRef IOReportCopyChannelsInGroup(CFStringRef a, CFStringRef b, uint64_t c, uint64_t d, uint64_t e);
void IOReportMergeChannels(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef null);
IOReportSubscriptionRef IOReportCreateSubscription(void* a, CFMutableDictionaryRef b, CFMutableDictionaryRef* c, uint64_t d, CFTypeRef e);
CFDictionaryRef IOReportCreateSamples(IOReportSubscriptionRef a, CFMutableDictionaryRef b, CFTypeRef c);
CFStringRef IOReportChannelGetGroup(CFDictionaryRef a);
CFStringRef IOReportChannelGetSubGroup(CFDictionaryRef a);
CFStringRef IOReportChannelGetChannelName(CFDictionaryRef a);
int32_t IOReportStateGetCount(CFDictionaryRef a);
CFStringRef IOReportStateGetNameForIndex(CFDictionaryRef a, int32_t b);
int64_t IOReportStateGetResidency(CFDictionaryRef a, int32_t b);
#endif /* bridge_h */
+1
View File
@@ -40,6 +40,7 @@ public struct GPU_Info: Codable {
public var utilization: Double? = nil
public var renderUtilization: Double? = nil
public var tilerUtilization: Double? = nil
public var aneUtilization: Double? = nil
init(id: String, type: GPU_type, IOClass: String, vendor: String? = nil, model: String, cores: Int?, utilization: Double? = nil, render: Double? = nil, tiler: Double? = nil) {
self.id = id
+24
View File
@@ -82,6 +82,7 @@ private class GPUView: NSStackView {
private var utilizationChart: LineChartView? = nil
private var renderUtilizationChart: LineChartView? = nil
private var tilerUtilizationChart: LineChartView? = nil
private var aneUtilizationChart: LineChartView? = nil
public var sizeCallback: (() -> Void)
@@ -180,6 +181,7 @@ private class GPUView: NSStackView {
self.addStats(id: "GPU utilization", self.value.utilization)
self.addStats(id: "Render utilization", self.value.renderUtilization)
self.addStats(id: "Tiler utilization", self.value.tilerUtilization)
self.addStats(id: "ANE utilization", self.value.aneUtilization)
container.addArrangedSubview(circles)
container.addArrangedSubview(charts)
@@ -272,6 +274,14 @@ private class GPUView: NSStackView {
if self.tilerUtilizationChart == nil {
self.tilerUtilizationChart = chart
}
} else if id == "ANE utilization" {
circle.setValue(value)
circle.setText("\(Int(value*100))%")
circle.toolTip = "\(localizedString(id)): \(Int(value*100))%"
if self.aneUtilizationChart == nil {
self.aneUtilizationChart = chart
}
}
}
@@ -286,6 +296,7 @@ private class GPUView: NSStackView {
self.addStats(id: "GPU utilization", gpu.utilization)
self.addStats(id: "Render utilization", gpu.renderUtilization)
self.addStats(id: "Tiler utilization", gpu.tilerUtilization)
self.addStats(id: "ANE utilization", gpu.aneUtilization)
}
if let value = gpu.temperature {
@@ -304,6 +315,9 @@ private class GPUView: NSStackView {
if let value = gpu.tilerUtilization {
self.tilerUtilizationChart?.addValue(value)
}
if let value = gpu.aneUtilization {
self.aneUtilizationChart?.addValue(value)
}
}
@objc private func showDetails() {
@@ -330,6 +344,7 @@ private class GPUDetails: NSView {
private var utilization: NSTextField? = nil
private var renderUtilization: NSTextField? = nil
private var tilerUtilization: NSTextField? = nil
private var aneUtilization: NSTextField? = nil
open override var intrinsicContentSize: CGSize {
return CGSize(width: self.bounds.width, height: self.bounds.height)
@@ -410,6 +425,12 @@ private class GPUDetails: NSView {
grid.addRow(with: arr)
num += 1
}
if let value = value.aneUtilization {
let arr = keyValueRow("\(localizedString("ANE utilization")):", "\(Int(value*100))%")
self.aneUtilization = arr.last
grid.addRow(with: arr)
num += 1
}
self.setFrameSize(NSSize(width: self.frame.width, height: (16 * num) + Constants.Popup.margins))
grid.setFrameSize(NSSize(width: grid.frame.width, height: self.frame.height - Constants.Popup.margins))
@@ -452,5 +473,8 @@ private class GPUDetails: NSView {
if let value = gpu.tilerUtilization {
self.tilerUtilization?.stringValue = "\(Int(value*100))%"
}
if let value = gpu.aneUtilization {
self.aneUtilization?.stringValue = "\(Int(value*100))%"
}
}
}
+7 -2
View File
@@ -13,11 +13,12 @@ import Cocoa
import Kit
public class Portal: PortalWrapper {
private var circle: HalfCircleGraphView? = nil
private var circle: PieChartView? = nil
private var usageField: NSTextField? = nil
private var renderField: NSTextField? = nil
private var tilerField: NSTextField? = nil
private var aneField: NSTextField? = nil
private var initialized: Bool = false
@@ -56,7 +57,7 @@ public class Portal: PortalWrapper {
right: Constants.Popup.spacing*4
)
let chart = HalfCircleGraphView()
let chart = PieChartView(openCircle: true)
chart.toolTip = localizedString("GPU usage")
view.addArrangedSubview(chart)
self.circle = chart
@@ -73,6 +74,7 @@ public class Portal: PortalWrapper {
self.usageField = portalRow(view, title: "\(localizedString("Usage")):").1
self.renderField = portalRow(view, title: "\(localizedString("Render")):").1
self.tilerField = portalRow(view, title: "\(localizedString("Tiler")):").1
self.aneField = portalRow(view, title: "\(localizedString("ANE")):").1
return view
}
@@ -89,6 +91,9 @@ public class Portal: PortalWrapper {
if let value = value.tilerUtilization {
self.tilerField?.stringValue = "\(Int(value*100))%"
}
if let value = value.aneUtilization {
self.aneField?.stringValue = "\(Int(value*100))%"
}
self.circle?.toolTip = "\(localizedString("GPU usage")): \(Int(value.utilization!*100))%"
self.circle?.setValue(value.utilization!)
+84
View File
@@ -28,6 +28,10 @@ internal class InfoReader: Reader<GPUs> {
private var gpus: GPUs = GPUs()
private var displays: [gpu_s] = []
private var devices: [device] = []
private var aneChannels: CFMutableDictionary? = nil
private var aneSubscription: IOReportSubscriptionRef? = nil
private var previousANEResidencies: [(on: Int64, total: Int64)] = []
public override func setup() {
if let list = SystemKit.shared.device.info.gpu {
@@ -39,6 +43,10 @@ internal class InfoReader: Reader<GPUs> {
}
let devices = PCIdevices.filter{ $0.object(forKey: "IOName") as? String == "display" }
#if arch(arm64)
self.setupANE()
#endif
devices.forEach { (dict: NSDictionary) in
guard let deviceID = dict["device-id"] as? Data, let vendorID = dict["vendor-id"] as? Data else {
error("device-id or vendor-id not found", log: self.log)
@@ -205,7 +213,83 @@ internal class InfoReader: Reader<GPUs> {
}
}
#if arch(arm64)
let aneValue = self.readANEUtilization()
for i in self.gpus.list.indices where self.gpus.list[i].IOClass.lowercased().contains("agx") {
self.gpus.list[i].aneUtilization = aneValue ?? 0
}
#endif
self.gpus.list.sort{ !$0.state && $1.state }
self.callback(self.gpus)
}
// MARK: - ANE utilization
private func setupANE() {
guard let channel = IOReportCopyChannelsInGroup("SoC Stats" as CFString, "Cluster Power States" as CFString, 0, 0, 0)?.takeRetainedValue() else { return }
let size = CFDictionaryGetCount(channel)
guard let mutable = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, size, channel),
let dict = mutable as? [String: Any], dict["IOReportChannels"] != nil else { return }
self.aneChannels = mutable
var sub: Unmanaged<CFMutableDictionary>?
self.aneSubscription = IOReportCreateSubscription(nil, mutable, &sub, 0, nil)
sub?.release()
}
private func readANEUtilization() -> Double? {
guard let subscription = self.aneSubscription,
let channels = self.aneChannels,
let reportSample = IOReportCreateSamples(subscription, channels, nil)?.takeRetainedValue(),
let dict = reportSample as? [String: Any] else {
return nil
}
let items = dict["IOReportChannels"] as! CFArray
var currentResidencies: [(on: Int64, total: Int64)] = []
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",
let channel = IOReportChannelGetChannelName(item)?.takeUnretainedValue() as? String,
channel.hasPrefix("ANE") else { continue }
let stateCount = IOReportStateGetCount(item)
guard stateCount == 2 else { continue }
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
}
}
currentResidencies.append((on: on, total: total))
}
guard !currentResidencies.isEmpty 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
}
guard totalDeltaAll > 0 else { return 0 }
return Double(totalDeltaOn) / Double(totalDeltaAll)
}
}
+8
View File
@@ -65,6 +65,7 @@
5CAA50722C8E417700B13E13 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CAA50712C8E417700B13E13 /* Text.swift */; };
5CB3878A2C35A7110030459D /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB387892C35A7110030459D /* widget.swift */; };
5CC3B4E52F5A033000775E2C /* reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC3B4E42F5A032E00775E2C /* reader.swift */; };
5CC8042A2F7EDECA00B78DC7 /* bridge.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CC804292F7EDECA00B78DC7 /* bridge.h */; };
5CCA5CD52D4E8DB3002917F0 /* libIOReport.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1E45552D11D66200525864 /* libIOReport.tbd */; };
5CD342F42B2F2FB700225631 /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD342F32B2F2FB700225631 /* notifications.swift */; };
5CE7E78C2C318512006BC92C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE7E78B2C318512006BC92C /* WidgetKit.framework */; };
@@ -204,6 +205,7 @@
9AF9EE0A24648751005D2270 /* Disk.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF9EE0224648751005D2270 /* Disk.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9AF9EE0F2464875F005D2270 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AF9EE0E2464875F005D2270 /* main.swift */; };
9AF9EE1124648ADC005D2270 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AF9EE1024648ADC005D2270 /* readers.swift */; };
AA00000000000000000000A1 /* libIOReport.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1E45552D11D66200525864 /* libIOReport.tbd */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -552,6 +554,7 @@
5CAA50712C8E417700B13E13 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = "<group>"; };
5CB387892C35A7110030459D /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = "<group>"; };
5CC3B4E42F5A032E00775E2C /* reader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = reader.swift; sourceTree = "<group>"; };
5CC804292F7EDECA00B78DC7 /* bridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bridge.h; sourceTree = "<group>"; };
5CD342F32B2F2FB700225631 /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = "<group>"; };
5CE7E78A2C318512006BC92C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
5CE7E78B2C318512006BC92C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
@@ -811,6 +814,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
AA00000000000000000000A1 /* libIOReport.tbd in Frameworks */,
9A2847C72666AA8C00EC1F6D /* Kit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1160,6 +1164,7 @@
5C0A9CA32C467F7A00EE6A89 /* widget.swift */,
9A90E18C24EAD2BB00471E9A /* Info.plist */,
9A90E19724EAD3B000471E9A /* config.plist */,
5CC804292F7EDECA00B78DC7 /* bridge.h */,
);
path = GPU;
sourceTree = "<group>";
@@ -1355,6 +1360,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
5CC8042A2F7EDECA00B78DC7 /* bridge.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -3215,6 +3221,7 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = Modules/GPU/bridge.h;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -3250,6 +3257,7 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = Modules/GPU/bridge.h;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";