mirror of
https://github.com/exelban/stats.git
synced 2026-05-07 20:02:34 +00:00
feat: added ANE utilization to the GPU module (#2897)
This commit is contained in:
@@ -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 */
|
||||
@@ -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
|
||||
|
||||
@@ -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))%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
Reference in New Issue
Block a user