mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
feat: added CPU, GPU, ANE, RAM, and PCI powers from IOReport (#2346)
This commit is contained in:
@@ -202,6 +202,19 @@ public extension Double {
|
||||
|
||||
return "\(h)h \(minutes)min"
|
||||
}
|
||||
|
||||
func power(_ unit: String) -> Double {
|
||||
switch unit {
|
||||
case "mJ":
|
||||
return self / 1e3
|
||||
case "uJ":
|
||||
return self / 1e6
|
||||
case "nJ":
|
||||
return self / 1e9
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSView {
|
||||
|
||||
@@ -1395,7 +1395,7 @@ public class PreferencesRow: NSStackView {
|
||||
self.addArrangedSubview(view)
|
||||
}
|
||||
|
||||
private func text(_ title: String? = nil, _ description: String? = nil) -> NSView {
|
||||
fileprivate func text(_ title: String? = nil, _ description: String? = nil) -> NSView {
|
||||
let view: NSStackView = NSStackView()
|
||||
view.orientation = .vertical
|
||||
view.spacing = 0
|
||||
|
||||
@@ -46,17 +46,16 @@ open class Settings: NSStackView, Settings_p {
|
||||
Store.shared.bool(key: "OneView", defaultValue: false)
|
||||
}
|
||||
private var oneViewState: Bool {
|
||||
get {
|
||||
return Store.shared.bool(key: "\(self.config.pointee.name)_oneView", defaultValue: false)
|
||||
}
|
||||
set {
|
||||
Store.shared.set(key: "\(self.config.pointee.name)_oneView", value: newValue)
|
||||
}
|
||||
get { Store.shared.bool(key: "\(self.config.pointee.name)_oneView", defaultValue: false) }
|
||||
set { Store.shared.set(key: "\(self.config.pointee.name)_oneView", value: newValue) }
|
||||
}
|
||||
|
||||
private var isPopupSettingsAvailable: Bool
|
||||
private var isNotificationsSettingsAvailable: Bool
|
||||
|
||||
private var previewView: NSView? = nil
|
||||
private var settingsView: NSView? = nil
|
||||
|
||||
init(config: UnsafePointer<module_c>, widgets: UnsafeMutablePointer<[SWidget]>, moduleSettings: Settings_v?, popupSettings: Popup_p?, notificationsSettings: NotificationsWrapper?) {
|
||||
self.config = config
|
||||
self.widgets = widgets.pointee
|
||||
@@ -80,11 +79,15 @@ open class Settings: NSStackView, Settings_p {
|
||||
right: Constants.Settings.margin
|
||||
)
|
||||
|
||||
let widgetSelector = WidgetSelectorView(module: self.config.pointee.name, widgets: self.widgets, stateCallback: self.loadWidget)
|
||||
let tabView = self.settingsView()
|
||||
let header = self.header()
|
||||
let settingsView = self.settings()
|
||||
self.settingsView = settingsView
|
||||
let previewView = self.preview()
|
||||
self.previewView = previewView
|
||||
|
||||
self.addArrangedSubview(widgetSelector)
|
||||
self.addArrangedSubview(tabView)
|
||||
self.addArrangedSubview(header)
|
||||
self.addArrangedSubview(settingsView)
|
||||
self.addArrangedSubview(previewView)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(listenForOneView), name: .toggleOneView, object: nil)
|
||||
self.segmentedControl?.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -(Constants.Settings.margin*2)).isActive = true
|
||||
@@ -102,7 +105,31 @@ open class Settings: NSStackView, Settings_p {
|
||||
toggleNSControlState(self.enableControl, state: newState ? .on : .off)
|
||||
}
|
||||
|
||||
private func settingsView() -> NSView {
|
||||
private func header() -> NSView {
|
||||
let view = NSStackView()
|
||||
view.orientation = .horizontal
|
||||
view.spacing = Constants.Settings.margin
|
||||
|
||||
let widgetSelector = WidgetSelectorView(module: self.config.pointee.name, widgets: self.widgets, stateCallback: self.loadWidget)
|
||||
// let button = ButtonSelectorView { [weak self] in
|
||||
// self?.toggleView()
|
||||
// }
|
||||
|
||||
view.addArrangedSubview(widgetSelector)
|
||||
// view.addArrangedSubview(button)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private func preview() -> NSView {
|
||||
let view = NSStackView()
|
||||
view.isHidden = true
|
||||
view.orientation = .vertical
|
||||
view.addArrangedSubview(EmptyView(height: 0, msg: localizedString("Preview is not available for that module")))
|
||||
return view
|
||||
}
|
||||
|
||||
private func settings() -> NSView {
|
||||
let view = NSStackView()
|
||||
view.orientation = .vertical
|
||||
view.spacing = Constants.Settings.margin
|
||||
@@ -268,6 +295,13 @@ open class Settings: NSStackView, Settings_p {
|
||||
self.oneViewBtn?.state = self.oneViewState ? .on : .off
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func toggleView() {
|
||||
guard let preview = self.previewView, let settings = self.settingsView else { return }
|
||||
|
||||
preview.isHidden = !preview.isHidden
|
||||
settings.isHidden = !settings.isHidden
|
||||
}
|
||||
}
|
||||
|
||||
private class WidgetSelectorView: NSStackView {
|
||||
@@ -622,3 +656,90 @@ private class WidgetSettings: NSStackView {
|
||||
return container
|
||||
}
|
||||
}
|
||||
|
||||
private class ButtonSelectorView: NSStackView {
|
||||
private var callback: () -> Void
|
||||
|
||||
private var background: NSVisualEffectView = {
|
||||
let view = NSVisualEffectView(frame: NSRect.zero)
|
||||
view.blendingMode = .withinWindow
|
||||
view.material = .contentBackground
|
||||
view.state = .active
|
||||
view.wantsLayer = true
|
||||
view.layer?.cornerRadius = 5
|
||||
return view
|
||||
}()
|
||||
|
||||
private var settingsIcon: NSImage {
|
||||
if #available(macOS 12.0, *), let icon = iconFromSymbol(name: "gear", scale: .large) {
|
||||
return icon
|
||||
}
|
||||
return NSImage(named: NSImage.Name("settings"))!
|
||||
}
|
||||
private var previewIcon: NSImage {
|
||||
if #available(macOS 12.0, *), let icon = iconFromSymbol(name: "command", scale: .large) {
|
||||
return icon
|
||||
}
|
||||
return NSImage(named: NSImage.Name("chart"))!
|
||||
}
|
||||
|
||||
private var button: NSButton? = nil
|
||||
private var isSettingsEnabled: Bool = false
|
||||
|
||||
fileprivate init(callback: @escaping () -> Void) {
|
||||
self.callback = callback
|
||||
|
||||
super.init(frame: NSRect.zero)
|
||||
|
||||
self.heightAnchor.constraint(equalToConstant: Constants.Widget.height + (Constants.Settings.margin*2)).isActive = true
|
||||
self.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.edgeInsets = NSEdgeInsets(
|
||||
top: Constants.Settings.margin,
|
||||
left: Constants.Settings.margin,
|
||||
bottom: Constants.Settings.margin,
|
||||
right: Constants.Settings.margin
|
||||
)
|
||||
self.spacing = Constants.Settings.margin
|
||||
|
||||
self.addSubview(self.background, positioned: .below, relativeTo: .none)
|
||||
|
||||
let button = NSButton()
|
||||
button.toolTip = localizedString("Open module settings")
|
||||
button.bezelStyle = .regularSquare
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.imageScaling = .scaleNone
|
||||
button.image = self.settingsIcon
|
||||
button.contentTintColor = .secondaryLabelColor
|
||||
button.isBordered = false
|
||||
button.action = #selector(self.action)
|
||||
button.target = self
|
||||
button.focusRingType = .none
|
||||
button.widthAnchor.constraint(equalToConstant: Constants.Widget.height).isActive = true
|
||||
self.button = button
|
||||
|
||||
self.addArrangedSubview(button)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func updateLayer() {
|
||||
self.background.setFrameSize(self.frame.size)
|
||||
}
|
||||
|
||||
@objc private func action() {
|
||||
guard let button = self.button else { return }
|
||||
self.callback()
|
||||
|
||||
self.isSettingsEnabled = !self.isSettingsEnabled
|
||||
|
||||
if self.isSettingsEnabled {
|
||||
button.image = self.previewIcon
|
||||
button.toolTip = localizedString("Close module settings")
|
||||
} else {
|
||||
button.image = self.settingsIcon
|
||||
button.toolTip = localizedString("Open module settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,8 +415,8 @@ public class FrequencyReader: Reader<[Double]> {
|
||||
private func getSamples() async -> [([IOSample], TimeInterval)] {
|
||||
let duration = 500
|
||||
let step = UInt64(duration / self.measurementCount)
|
||||
var prev = self.prev ?? self.getSample() ?? self.prev!
|
||||
var samples = [([IOSample], TimeInterval)]()
|
||||
guard var prev = self.prev ?? self.getSample() else { return samples }
|
||||
|
||||
for _ in 0..<self.measurementCount {
|
||||
let milliseconds = UInt64(step) * 1_000_000
|
||||
|
||||
@@ -12,9 +12,11 @@
|
||||
//
|
||||
|
||||
#include <IOKit/hidsystem/IOHIDEventSystemClient.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
typedef struct __IOHIDEvent *IOHIDEventRef;
|
||||
typedef struct __IOHIDServiceClient *IOHIDServiceClientRef;
|
||||
typedef struct IOReportSubscriptionRef* IOReportSubscriptionRef;
|
||||
#ifdef __LP64__
|
||||
typedef double IOHIDFloat;
|
||||
#else
|
||||
@@ -32,3 +34,17 @@ CFTypeRef IOHIDServiceClientCopyProperty(IOHIDServiceClientRef service, CFString
|
||||
IOHIDFloat IOHIDEventGetFloatValue(IOHIDEventRef event, int32_t field);
|
||||
|
||||
NSDictionary*AppleSiliconSensors(int page, int usage, int32_t type);
|
||||
|
||||
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);
|
||||
CFDictionaryRef IOReportCreateSamplesDelta(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef c);
|
||||
CFStringRef IOReportChannelGetGroup(CFDictionaryRef a);
|
||||
CFStringRef IOReportChannelGetSubGroup(CFDictionaryRef a);
|
||||
CFStringRef IOReportChannelGetChannelName(CFDictionaryRef a);
|
||||
CFStringRef IOReportChannelGetUnitLabel(CFDictionaryRef a);
|
||||
int32_t IOReportStateGetCount(CFDictionaryRef a);
|
||||
CFStringRef IOReportStateGetNameForIndex(CFDictionaryRef a, int32_t b);
|
||||
int64_t IOReportStateGetResidency(CFDictionaryRef a, int32_t b);
|
||||
int64_t IOReportSimpleGetIntegerValue(CFDictionaryRef a, int32_t b);
|
||||
|
||||
@@ -25,9 +25,19 @@ internal class SensorsReader: Reader<Sensors_List> {
|
||||
}
|
||||
private var unknownSensorsState: Bool
|
||||
|
||||
private var channels: CFMutableDictionary? = nil
|
||||
private var subscription: IOReportSubscriptionRef? = nil
|
||||
private var powers: (CPU: Double, GPU: Double, ANE: Double, RAM: Double, PCI: Double) = (0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
init(callback: @escaping (T?) -> Void = {_ in }) {
|
||||
self.unknownSensorsState = Store.shared.bool(key: "Sensors_unknown", defaultValue: false)
|
||||
super.init(.sensors, callback: callback)
|
||||
|
||||
self.channels = self.getChannels()
|
||||
var dict: Unmanaged<CFMutableDictionary>?
|
||||
self.subscription = IOReportCreateSubscription(nil, self.channels, &dict, 0, nil)
|
||||
dict?.release()
|
||||
|
||||
self.list.sensors = self.sensors()
|
||||
}
|
||||
|
||||
@@ -108,6 +118,7 @@ internal class SensorsReader: Reader<Sensors_List> {
|
||||
if self.HIDState {
|
||||
results += self.initHIDSensors()
|
||||
}
|
||||
results += self.initIOSensors()
|
||||
#endif
|
||||
results += self.initCalculatedSensors(results)
|
||||
|
||||
@@ -161,6 +172,24 @@ internal class SensorsReader: Reader<Sensors_List> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (cpu, gpu, ane, ram, pci) = self.IOSensors() {
|
||||
if let idx = self.list.sensors.firstIndex(where: { $0.key == "CPU Power" }) {
|
||||
self.list.sensors[idx].value = cpu
|
||||
}
|
||||
if let idx = self.list.sensors.firstIndex(where: { $0.key == "GPU Power" }) {
|
||||
self.list.sensors[idx].value = gpu
|
||||
}
|
||||
if let idx = self.list.sensors.firstIndex(where: { $0.key == "ANE Power" }) {
|
||||
self.list.sensors[idx].value = ane
|
||||
}
|
||||
if let idx = self.list.sensors.firstIndex(where: { $0.key == "RAM Power" }) {
|
||||
self.list.sensors[idx].value = ram
|
||||
}
|
||||
if let idx = self.list.sensors.firstIndex(where: { $0.key == "PCI Power" }) {
|
||||
self.list.sensors[idx].value = pci
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if !cpuSensors.isEmpty {
|
||||
@@ -448,3 +477,90 @@ extension SensorsReader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Apple Silicon power sensors
|
||||
|
||||
extension SensorsReader {
|
||||
private func getChannels() -> CFMutableDictionary? {
|
||||
let channelNames: [(String, String?)] = [("Energy Model", nil)]
|
||||
|
||||
var channels: [CFDictionary] = []
|
||||
for (gname, sname) in channelNames {
|
||||
let channel = IOReportCopyChannelsInGroup(gname as CFString?, sname as CFString?, 0, 0, 0)
|
||||
guard let channel = channel?.takeRetainedValue() else { continue }
|
||||
channels.append(channel)
|
||||
}
|
||||
|
||||
let chan = channels[0]
|
||||
for i in 1..<channels.count {
|
||||
IOReportMergeChannels(chan, channels[i], nil)
|
||||
}
|
||||
|
||||
let size = CFDictionaryGetCount(chan)
|
||||
guard let channel = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, size, chan),
|
||||
let chan = channel as? [String: Any], chan["IOReportChannels"] != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
private func initIOSensors() -> [Sensor] {
|
||||
guard let (cpu, gpu, ane, ram, pci) = self.IOSensors() else { return [] }
|
||||
return [
|
||||
Sensor(key: "CPU Power", name: "CPU Power", value: cpu, group: .CPU, type: .power, platforms: Platform.apple, isComputed: true),
|
||||
Sensor(key: "GPU Power", name: "GPU Power", value: gpu, group: .GPU, type: .power, platforms: Platform.apple, isComputed: true),
|
||||
Sensor(key: "ANE Power", name: "ANE Power", value: ane, group: .system, type: .power, platforms: Platform.apple, isComputed: true),
|
||||
Sensor(key: "RAM Power", name: "RAM Power", value: ram, group: .system, type: .power, platforms: Platform.apple, isComputed: true),
|
||||
Sensor(key: "PCI Power", name: "PCI Power", value: pci, group: .system, type: .power, platforms: Platform.apple, isComputed: true)
|
||||
]
|
||||
}
|
||||
|
||||
private func IOSensors() -> (Double, Double, Double, Double, Double)? {
|
||||
guard let sample = IOReportCreateSamples(self.subscription, self.channels, nil)?.takeRetainedValue(),
|
||||
let dict = sample as? [String: Any] else {
|
||||
return nil
|
||||
}
|
||||
let items = dict["IOReportChannels"] as! CFArray
|
||||
|
||||
let prevCPU = self.powers.CPU
|
||||
let prevGPU = self.powers.GPU
|
||||
let prevANE = self.powers.ANE
|
||||
let prevRAM = self.powers.RAM
|
||||
let prevPCI = self.powers.PCI
|
||||
|
||||
for i in 0..<CFArrayGetCount(items) {
|
||||
let dict = CFArrayGetValueAtIndex(items, i)
|
||||
let item = unsafeBitCast(dict, to: CFDictionary.self)
|
||||
|
||||
guard let group = IOReportChannelGetGroup(item)?.takeUnretainedValue() as? String,
|
||||
group == "Energy Model",
|
||||
let channel = IOReportChannelGetChannelName(item)?.takeUnretainedValue() as? String,
|
||||
let unit = IOReportChannelGetUnitLabel(item)?.takeUnretainedValue() as? String else { continue }
|
||||
|
||||
let value = Double(IOReportSimpleGetIntegerValue(item, 0))
|
||||
|
||||
if channel.hasSuffix("CPU Energy") {
|
||||
self.powers.CPU = value.power(unit)
|
||||
} else if channel.hasSuffix("GPU Energy") {
|
||||
self.powers.GPU = value.power(unit)
|
||||
} else if channel.starts(with: "ANE") {
|
||||
self.powers.ANE = value.power(unit)
|
||||
} else if channel.starts(with: "DRAM") {
|
||||
self.powers.RAM = value.power(unit)
|
||||
} else if channel.starts(with: "PCI") && channel.hasSuffix("Energy") {
|
||||
self.powers.PCI = value.power(unit)
|
||||
}
|
||||
}
|
||||
|
||||
guard prevCPU != 0 else { return (0, 0, 0, 0, 0) } // omit first read
|
||||
|
||||
return (
|
||||
self.powers.CPU - prevCPU,
|
||||
self.powers.GPU - prevGPU,
|
||||
self.powers.ANE - prevANE,
|
||||
self.powers.RAM - prevRAM,
|
||||
self.powers.PCI - prevPCI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,8 @@ internal class Settings: NSStackView, Settings_v {
|
||||
if let row = self.sensorsPrefs?.findRow("active_sensor") {
|
||||
if !widgets.isEmpty {
|
||||
self.sensorsPrefs?.setRowVisibility(row, newState: widgets.contains(where: { $0 == .mini }))
|
||||
} else {
|
||||
self.sensorsPrefs?.setRowVisibility(row, newState: false)
|
||||
}
|
||||
row.replaceComponent(with: selectView(
|
||||
action: #selector(self.handleSelection),
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
5CA518382B543FE600EBCCC4 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA518372B543FE600EBCCC4 /* portal.swift */; };
|
||||
5CAA50722C8E417700B13E13 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CAA50712C8E417700B13E13 /* Text.swift */; };
|
||||
5CB3878A2C35A7110030459D /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB387892C35A7110030459D /* widget.swift */; };
|
||||
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 */; };
|
||||
5CE7E78E2C318512006BC92C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE7E78D2C318512006BC92C /* SwiftUI.framework */; };
|
||||
@@ -844,6 +845,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CCA5CD52D4E8DB3002917F0 /* libIOReport.tbd in Frameworks */,
|
||||
9A2847E02666AAA400EC1F6D /* Kit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
@@ -72,7 +72,7 @@ class SettingsWindow: NSWindow, NSWindowDelegate, NSToolbarDelegate {
|
||||
self.contentViewController = sidebarViewController
|
||||
self.titlebarAppearsTransparent = true
|
||||
self.backgroundColor = .clear
|
||||
self.positionCenter()
|
||||
// self.positionCenter()
|
||||
self.setIsVisible(false)
|
||||
|
||||
let windowController = NSWindowController()
|
||||
|
||||
Reference in New Issue
Block a user