Skip to content

Commit abbdfc1

Browse files
committed
feat: added ANE utilization to the GPU module (#2897)
1 parent cf9c858 commit abbdfc1

File tree

6 files changed

+154
-2
lines changed

6 files changed

+154
-2
lines changed

Modules/GPU/bridge.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// bridge.h
3+
// GPU
4+
//
5+
// Created by Serhiy Mytrovtsiy on 02/04/2026
6+
// Using Swift 6.0
7+
// Running on macOS 15.1
8+
//
9+
// Copyright © 2026 Serhiy Mytrovtsiy. All rights reserved.
10+
//
11+
12+
#ifndef bridge_h
13+
#define bridge_h
14+
15+
#include <CoreFoundation/CoreFoundation.h>
16+
17+
typedef struct IOReportSubscriptionRef* IOReportSubscriptionRef;
18+
19+
CFDictionaryRef IOReportCopyChannelsInGroup(CFStringRef a, CFStringRef b, uint64_t c, uint64_t d, uint64_t e);
20+
void IOReportMergeChannels(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef null);
21+
IOReportSubscriptionRef IOReportCreateSubscription(void* a, CFMutableDictionaryRef b, CFMutableDictionaryRef* c, uint64_t d, CFTypeRef e);
22+
CFDictionaryRef IOReportCreateSamples(IOReportSubscriptionRef a, CFMutableDictionaryRef b, CFTypeRef c);
23+
CFStringRef IOReportChannelGetGroup(CFDictionaryRef a);
24+
CFStringRef IOReportChannelGetSubGroup(CFDictionaryRef a);
25+
CFStringRef IOReportChannelGetChannelName(CFDictionaryRef a);
26+
int32_t IOReportStateGetCount(CFDictionaryRef a);
27+
CFStringRef IOReportStateGetNameForIndex(CFDictionaryRef a, int32_t b);
28+
int64_t IOReportStateGetResidency(CFDictionaryRef a, int32_t b);
29+
30+
#endif /* bridge_h */

Modules/GPU/main.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public struct GPU_Info: Codable {
4040
public var utilization: Double? = nil
4141
public var renderUtilization: Double? = nil
4242
public var tilerUtilization: Double? = nil
43+
public var aneUtilization: Double? = nil
4344

4445
init(id: String, type: GPU_type, IOClass: String, vendor: String? = nil, model: String, cores: Int?, utilization: Double? = nil, render: Double? = nil, tiler: Double? = nil) {
4546
self.id = id

Modules/GPU/popup.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ private class GPUView: NSStackView {
8282
private var utilizationChart: LineChartView? = nil
8383
private var renderUtilizationChart: LineChartView? = nil
8484
private var tilerUtilizationChart: LineChartView? = nil
85+
private var aneUtilizationChart: LineChartView? = nil
8586

8687
public var sizeCallback: (() -> Void)
8788

@@ -180,6 +181,7 @@ private class GPUView: NSStackView {
180181
self.addStats(id: "GPU utilization", self.value.utilization)
181182
self.addStats(id: "Render utilization", self.value.renderUtilization)
182183
self.addStats(id: "Tiler utilization", self.value.tilerUtilization)
184+
self.addStats(id: "ANE utilization", self.value.aneUtilization)
183185

184186
container.addArrangedSubview(circles)
185187
container.addArrangedSubview(charts)
@@ -272,6 +274,14 @@ private class GPUView: NSStackView {
272274
if self.tilerUtilizationChart == nil {
273275
self.tilerUtilizationChart = chart
274276
}
277+
} else if id == "ANE utilization" {
278+
circle.setValue(value)
279+
circle.setText("\(Int(value*100))%")
280+
circle.toolTip = "\(localizedString(id)): \(Int(value*100))%"
281+
282+
if self.aneUtilizationChart == nil {
283+
self.aneUtilizationChart = chart
284+
}
275285
}
276286
}
277287

@@ -286,6 +296,7 @@ private class GPUView: NSStackView {
286296
self.addStats(id: "GPU utilization", gpu.utilization)
287297
self.addStats(id: "Render utilization", gpu.renderUtilization)
288298
self.addStats(id: "Tiler utilization", gpu.tilerUtilization)
299+
self.addStats(id: "ANE utilization", gpu.aneUtilization)
289300
}
290301

291302
if let value = gpu.temperature {
@@ -304,6 +315,9 @@ private class GPUView: NSStackView {
304315
if let value = gpu.tilerUtilization {
305316
self.tilerUtilizationChart?.addValue(value)
306317
}
318+
if let value = gpu.aneUtilization {
319+
self.aneUtilizationChart?.addValue(value)
320+
}
307321
}
308322

309323
@objc private func showDetails() {
@@ -330,6 +344,7 @@ private class GPUDetails: NSView {
330344
private var utilization: NSTextField? = nil
331345
private var renderUtilization: NSTextField? = nil
332346
private var tilerUtilization: NSTextField? = nil
347+
private var aneUtilization: NSTextField? = nil
333348

334349
open override var intrinsicContentSize: CGSize {
335350
return CGSize(width: self.bounds.width, height: self.bounds.height)
@@ -410,6 +425,12 @@ private class GPUDetails: NSView {
410425
grid.addRow(with: arr)
411426
num += 1
412427
}
428+
if let value = value.aneUtilization {
429+
let arr = keyValueRow("\(localizedString("ANE utilization")):", "\(Int(value*100))%")
430+
self.aneUtilization = arr.last
431+
grid.addRow(with: arr)
432+
num += 1
433+
}
413434

414435
self.setFrameSize(NSSize(width: self.frame.width, height: (16 * num) + Constants.Popup.margins))
415436
grid.setFrameSize(NSSize(width: grid.frame.width, height: self.frame.height - Constants.Popup.margins))
@@ -452,5 +473,8 @@ private class GPUDetails: NSView {
452473
if let value = gpu.tilerUtilization {
453474
self.tilerUtilization?.stringValue = "\(Int(value*100))%"
454475
}
476+
if let value = gpu.aneUtilization {
477+
self.aneUtilization?.stringValue = "\(Int(value*100))%"
478+
}
455479
}
456480
}

Modules/GPU/portal.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ import Cocoa
1313
import Kit
1414

1515
public class Portal: PortalWrapper {
16-
private var circle: HalfCircleGraphView? = nil
16+
private var circle: PieChartView? = nil
1717

1818
private var usageField: NSTextField? = nil
1919
private var renderField: NSTextField? = nil
2020
private var tilerField: NSTextField? = nil
21+
private var aneField: NSTextField? = nil
2122

2223
private var initialized: Bool = false
2324

@@ -56,7 +57,7 @@ public class Portal: PortalWrapper {
5657
right: Constants.Popup.spacing*4
5758
)
5859

59-
let chart = HalfCircleGraphView()
60+
let chart = PieChartView(openCircle: true)
6061
chart.toolTip = localizedString("GPU usage")
6162
view.addArrangedSubview(chart)
6263
self.circle = chart
@@ -73,6 +74,7 @@ public class Portal: PortalWrapper {
7374
self.usageField = portalRow(view, title: "\(localizedString("Usage")):").1
7475
self.renderField = portalRow(view, title: "\(localizedString("Render")):").1
7576
self.tilerField = portalRow(view, title: "\(localizedString("Tiler")):").1
77+
self.aneField = portalRow(view, title: "\(localizedString("ANE")):").1
7678

7779
return view
7880
}
@@ -89,6 +91,9 @@ public class Portal: PortalWrapper {
8991
if let value = value.tilerUtilization {
9092
self.tilerField?.stringValue = "\(Int(value*100))%"
9193
}
94+
if let value = value.aneUtilization {
95+
self.aneField?.stringValue = "\(Int(value*100))%"
96+
}
9297

9398
self.circle?.toolTip = "\(localizedString("GPU usage")): \(Int(value.utilization!*100))%"
9499
self.circle?.setValue(value.utilization!)

Modules/GPU/reader.swift

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ internal class InfoReader: Reader<GPUs> {
2828
private var gpus: GPUs = GPUs()
2929
private var displays: [gpu_s] = []
3030
private var devices: [device] = []
31+
32+
private var aneChannels: CFMutableDictionary? = nil
33+
private var aneSubscription: IOReportSubscriptionRef? = nil
34+
private var previousANEResidencies: [(on: Int64, total: Int64)] = []
3135

3236
public override func setup() {
3337
if let list = SystemKit.shared.device.info.gpu {
@@ -39,6 +43,10 @@ internal class InfoReader: Reader<GPUs> {
3943
}
4044
let devices = PCIdevices.filter{ $0.object(forKey: "IOName") as? String == "display" }
4145

46+
#if arch(arm64)
47+
self.setupANE()
48+
#endif
49+
4250
devices.forEach { (dict: NSDictionary) in
4351
guard let deviceID = dict["device-id"] as? Data, let vendorID = dict["vendor-id"] as? Data else {
4452
error("device-id or vendor-id not found", log: self.log)
@@ -205,7 +213,83 @@ internal class InfoReader: Reader<GPUs> {
205213
}
206214
}
207215

216+
#if arch(arm64)
217+
let aneValue = self.readANEUtilization()
218+
for i in self.gpus.list.indices where self.gpus.list[i].IOClass.lowercased().contains("agx") {
219+
self.gpus.list[i].aneUtilization = aneValue ?? 0
220+
}
221+
#endif
222+
208223
self.gpus.list.sort{ !$0.state && $1.state }
209224
self.callback(self.gpus)
210225
}
226+
227+
// MARK: - ANE utilization
228+
229+
private func setupANE() {
230+
guard let channel = IOReportCopyChannelsInGroup("SoC Stats" as CFString, "Cluster Power States" as CFString, 0, 0, 0)?.takeRetainedValue() else { return }
231+
232+
let size = CFDictionaryGetCount(channel)
233+
guard let mutable = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, size, channel),
234+
let dict = mutable as? [String: Any], dict["IOReportChannels"] != nil else { return }
235+
236+
self.aneChannels = mutable
237+
var sub: Unmanaged<CFMutableDictionary>?
238+
self.aneSubscription = IOReportCreateSubscription(nil, mutable, &sub, 0, nil)
239+
sub?.release()
240+
}
241+
242+
private func readANEUtilization() -> Double? {
243+
guard let subscription = self.aneSubscription,
244+
let channels = self.aneChannels,
245+
let reportSample = IOReportCreateSamples(subscription, channels, nil)?.takeRetainedValue(),
246+
let dict = reportSample as? [String: Any] else {
247+
return nil
248+
}
249+
let items = dict["IOReportChannels"] as! CFArray
250+
251+
var currentResidencies: [(on: Int64, total: Int64)] = []
252+
253+
for i in 0..<CFArrayGetCount(items) {
254+
let item = unsafeBitCast(CFArrayGetValueAtIndex(items, i), to: CFDictionary.self)
255+
256+
guard let group = IOReportChannelGetGroup(item)?.takeUnretainedValue() as? String,
257+
group == "SoC Stats",
258+
let subgroup = IOReportChannelGetSubGroup(item)?.takeUnretainedValue() as? String,
259+
subgroup == "Cluster Power States",
260+
let channel = IOReportChannelGetChannelName(item)?.takeUnretainedValue() as? String,
261+
channel.hasPrefix("ANE") else { continue }
262+
263+
let stateCount = IOReportStateGetCount(item)
264+
guard stateCount == 2 else { continue }
265+
266+
var on: Int64 = 0
267+
var total: Int64 = 0
268+
for s in 0..<stateCount {
269+
let residency = IOReportStateGetResidency(item, s)
270+
let name = IOReportStateGetNameForIndex(item, s)?.takeUnretainedValue() as? String ?? ""
271+
total += residency
272+
if name != "INACT" {
273+
on += residency
274+
}
275+
}
276+
277+
currentResidencies.append((on: on, total: total))
278+
}
279+
280+
guard !currentResidencies.isEmpty else { return nil }
281+
282+
defer { self.previousANEResidencies = currentResidencies }
283+
guard self.previousANEResidencies.count == currentResidencies.count else { return nil }
284+
285+
var totalDeltaOn: Int64 = 0
286+
var totalDeltaAll: Int64 = 0
287+
for i in 0..<currentResidencies.count {
288+
totalDeltaOn += currentResidencies[i].on - self.previousANEResidencies[i].on
289+
totalDeltaAll += currentResidencies[i].total - self.previousANEResidencies[i].total
290+
}
291+
292+
guard totalDeltaAll > 0 else { return 0 }
293+
return Double(totalDeltaOn) / Double(totalDeltaAll)
294+
}
211295
}

Stats.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
5CAA50722C8E417700B13E13 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CAA50712C8E417700B13E13 /* Text.swift */; };
6666
5CB3878A2C35A7110030459D /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB387892C35A7110030459D /* widget.swift */; };
6767
5CC3B4E52F5A033000775E2C /* reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC3B4E42F5A032E00775E2C /* reader.swift */; };
68+
5CC8042A2F7EDECA00B78DC7 /* bridge.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CC804292F7EDECA00B78DC7 /* bridge.h */; };
6869
5CCA5CD52D4E8DB3002917F0 /* libIOReport.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1E45552D11D66200525864 /* libIOReport.tbd */; };
6970
5CD342F42B2F2FB700225631 /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD342F32B2F2FB700225631 /* notifications.swift */; };
7071
5CE7E78C2C318512006BC92C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE7E78B2C318512006BC92C /* WidgetKit.framework */; };
@@ -204,6 +205,7 @@
204205
9AF9EE0A24648751005D2270 /* Disk.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF9EE0224648751005D2270 /* Disk.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
205206
9AF9EE0F2464875F005D2270 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AF9EE0E2464875F005D2270 /* main.swift */; };
206207
9AF9EE1124648ADC005D2270 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AF9EE1024648ADC005D2270 /* readers.swift */; };
208+
AA00000000000000000000A1 /* libIOReport.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1E45552D11D66200525864 /* libIOReport.tbd */; };
207209
/* End PBXBuildFile section */
208210

209211
/* Begin PBXContainerItemProxy section */
@@ -552,6 +554,7 @@
552554
5CAA50712C8E417700B13E13 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = "<group>"; };
553555
5CB387892C35A7110030459D /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = "<group>"; };
554556
5CC3B4E42F5A032E00775E2C /* reader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = reader.swift; sourceTree = "<group>"; };
557+
5CC804292F7EDECA00B78DC7 /* bridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bridge.h; sourceTree = "<group>"; };
555558
5CD342F32B2F2FB700225631 /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = "<group>"; };
556559
5CE7E78A2C318512006BC92C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
557560
5CE7E78B2C318512006BC92C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
@@ -811,6 +814,7 @@
811814
isa = PBXFrameworksBuildPhase;
812815
buildActionMask = 2147483647;
813816
files = (
817+
AA00000000000000000000A1 /* libIOReport.tbd in Frameworks */,
814818
9A2847C72666AA8C00EC1F6D /* Kit.framework in Frameworks */,
815819
);
816820
runOnlyForDeploymentPostprocessing = 0;
@@ -1160,6 +1164,7 @@
11601164
5C0A9CA32C467F7A00EE6A89 /* widget.swift */,
11611165
9A90E18C24EAD2BB00471E9A /* Info.plist */,
11621166
9A90E19724EAD3B000471E9A /* config.plist */,
1167+
5CC804292F7EDECA00B78DC7 /* bridge.h */,
11631168
);
11641169
path = GPU;
11651170
sourceTree = "<group>";
@@ -1355,6 +1360,7 @@
13551360
isa = PBXHeadersBuildPhase;
13561361
buildActionMask = 2147483647;
13571362
files = (
1363+
5CC8042A2F7EDECA00B78DC7 /* bridge.h in Headers */,
13581364
);
13591365
runOnlyForDeploymentPostprocessing = 0;
13601366
};
@@ -3215,6 +3221,7 @@
32153221
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
32163222
PROVISIONING_PROFILE_SPECIFIER = "";
32173223
SKIP_INSTALL = YES;
3224+
SWIFT_OBJC_BRIDGING_HEADER = Modules/GPU/bridge.h;
32183225
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
32193226
SWIFT_VERSION = 5.0;
32203227
VERSIONING_SYSTEM = "apple-generic";
@@ -3250,6 +3257,7 @@
32503257
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
32513258
PROVISIONING_PROFILE_SPECIFIER = "";
32523259
SKIP_INSTALL = YES;
3260+
SWIFT_OBJC_BRIDGING_HEADER = Modules/GPU/bridge.h;
32533261
SWIFT_VERSION = 5.0;
32543262
VERSIONING_SYSTEM = "apple-generic";
32553263
VERSION_INFO_PREFIX = "";

0 commit comments

Comments
 (0)