mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
feat: added efficiency and performance cores frequency and total cores frequency
This commit is contained in:
@@ -662,6 +662,23 @@ internal func getIOName(_ entry: io_registry_entry_t) -> String? {
|
||||
return String(cString: UnsafeRawPointer(pointer).assumingMemoryBound(to: CChar.self))
|
||||
}
|
||||
|
||||
internal func convertCFDataToArr(_ data: CFData) -> [Int32] {
|
||||
let length = CFDataGetLength(data)
|
||||
var bytes = [UInt8](repeating: 0, count: length)
|
||||
CFDataGetBytes(data, CFRange(location: 0, length: length), &bytes)
|
||||
|
||||
var arr: [Int32] = []
|
||||
var chunks = stride(from: 0, to: bytes.count, by: 8).map { Array(bytes[$0..<min($0 + 8, bytes.count)])}
|
||||
for chunk in chunks {
|
||||
let v = UInt32(chunk[0]) | UInt32(chunk[1]) << 8 | UInt32(chunk[2]) << 16 | UInt32(chunk[3]) << 24
|
||||
arr.append(Int32(v / 1000 / 1000))
|
||||
}
|
||||
bytes.removeAll()
|
||||
chunks.removeAll()
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
public class ColorView: NSView {
|
||||
public var inactiveColor: NSColor = NSColor.lightGray.withAlphaComponent(0.75)
|
||||
|
||||
|
||||
@@ -109,6 +109,8 @@ public struct cpu_s {
|
||||
public var eCores: Int32? = nil
|
||||
public var pCores: Int32? = nil
|
||||
public var cores: [core_s]? = nil
|
||||
public var eCoreFrequencies: [Int32]? = nil
|
||||
public var pCoreFrequencies: [Int32]? = nil
|
||||
}
|
||||
|
||||
public struct dimm_s {
|
||||
@@ -128,6 +130,7 @@ public struct gpu_s {
|
||||
public var vendor: String? = nil
|
||||
public var vram: String? = nil
|
||||
public var cores: Int? = nil
|
||||
public var frequencies: [Int32]? = nil
|
||||
}
|
||||
|
||||
public struct info_s {
|
||||
@@ -319,6 +322,10 @@ public class SystemKit {
|
||||
cpu.pCores = cores.1
|
||||
cpu.cores = cores.2
|
||||
}
|
||||
if let freq = getFrequencies() {
|
||||
cpu.eCoreFrequencies = freq.0
|
||||
cpu.pCoreFrequencies = freq.1
|
||||
}
|
||||
|
||||
return cpu
|
||||
}
|
||||
@@ -386,10 +393,8 @@ public class SystemKit {
|
||||
IOObjectRelease(child)
|
||||
}
|
||||
IOObjectRelease(entry)
|
||||
|
||||
IOObjectRelease(service)
|
||||
}
|
||||
|
||||
IOObjectRelease(iterator)
|
||||
|
||||
return (eCores, pCores, list)
|
||||
@@ -417,6 +422,10 @@ public class SystemKit {
|
||||
gpu.vram = vram
|
||||
}
|
||||
|
||||
if let freq = getFrequencies() {
|
||||
gpu.frequencies = freq.2
|
||||
}
|
||||
|
||||
list.append(gpu)
|
||||
}
|
||||
}
|
||||
@@ -429,6 +438,36 @@ public class SystemKit {
|
||||
return list
|
||||
}
|
||||
|
||||
private func getFrequencies() -> ([Int32], [Int32], [Int32])? {
|
||||
var iterator = io_iterator_t()
|
||||
let result = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching("AppleARMIODevice"), &iterator)
|
||||
if result != kIOReturnSuccess {
|
||||
print("Error find AppleARMIODevice: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
|
||||
return nil
|
||||
}
|
||||
|
||||
var eFreq: [Int32] = []
|
||||
var pFreq: [Int32] = []
|
||||
var gpuFreq: [Int32] = []
|
||||
|
||||
while case let child = IOIteratorNext(iterator), child != 0 {
|
||||
defer { IOObjectRelease(child) }
|
||||
guard let name = getIOName(child), name == "pmgr", let props = getIOProperties(child) else { continue }
|
||||
|
||||
if let data = props.value(forKey: "voltage-states1-sram") {
|
||||
eFreq = convertCFDataToArr(data as! CFData)
|
||||
}
|
||||
if let data = props.value(forKey: "voltage-states5-sram") {
|
||||
pFreq = convertCFDataToArr(data as! CFData)
|
||||
}
|
||||
if let data = props.value(forKey: "voltage-states9-sram") {
|
||||
gpuFreq = convertCFDataToArr(data as! CFData)
|
||||
}
|
||||
}
|
||||
|
||||
return (eFreq, pFreq, gpuFreq)
|
||||
}
|
||||
|
||||
public func getRamInfo() -> ram_s? {
|
||||
guard let res = process(path: "/usr/sbin/system_profiler", arguments: ["SPMemoryDataType", "-json"]) else {
|
||||
return nil
|
||||
|
||||
32
Modules/CPU/bridge.h
Normal file
32
Modules/CPU/bridge.h
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// bridge.h
|
||||
// Stats
|
||||
//
|
||||
// Created by Serhiy Mytrovtsiy on 17/12/2024
|
||||
// Using Swift 6.0
|
||||
// Running on macOS 15.1
|
||||
//
|
||||
// Copyright © 2024 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);
|
||||
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);
|
||||
|
||||
#endif /* bridge_h */
|
||||
@@ -75,7 +75,7 @@ public class CPU: Module {
|
||||
return color.additional as! NSColor
|
||||
}
|
||||
private var pCoreColor: NSColor {
|
||||
let color = SColor.secondBlue
|
||||
let color = SColor.indigo
|
||||
let key = Store.shared.string(key: "\(self.config.name)_pCoresColor", defaultValue: color.key)
|
||||
if let c = SColor.fromString(key).additional as? NSColor {
|
||||
return c
|
||||
@@ -115,7 +115,8 @@ public class CPU: Module {
|
||||
self.limitReader = LimitReader(.CPU, popup: true) { [weak self] value in
|
||||
self?.popupView.limitCallback(value)
|
||||
}
|
||||
self.frequencyReader = FrequencyReader(.CPU, popup: true) { [weak self] value in
|
||||
#else
|
||||
self.frequencyReader = FrequencyReader(.CPU, popup: false) { [weak self] value in
|
||||
self?.popupView.frequencyCallback(value)
|
||||
}
|
||||
#endif
|
||||
@@ -135,12 +136,6 @@ public class CPU: Module {
|
||||
self.settingsView.setTopInterval = { [weak self] value in
|
||||
self?.processReader?.setInterval(value)
|
||||
}
|
||||
self.settingsView.IPGCallback = { [weak self] value in
|
||||
if value {
|
||||
self?.frequencyReader?.setup()
|
||||
}
|
||||
self?.popupView.toggleFrequency(state: value)
|
||||
}
|
||||
|
||||
self.setReaders([
|
||||
self.loadReader,
|
||||
|
||||
@@ -32,6 +32,18 @@ internal class Popup: PopupWrapper {
|
||||
}
|
||||
}
|
||||
private let averageHeight: CGFloat = (22*3) + Constants.Popup.separatorHeight
|
||||
private var frequencyHeight: CGFloat {
|
||||
get {
|
||||
var count: CGFloat = 1
|
||||
if SystemKit.shared.device.info.cpu?.eCores != nil {
|
||||
count += 1
|
||||
}
|
||||
if SystemKit.shared.device.info.cpu?.pCores != nil {
|
||||
count += 1
|
||||
}
|
||||
return (22*count) + Constants.Popup.separatorHeight
|
||||
}
|
||||
}
|
||||
private let processHeight: CGFloat = 22
|
||||
|
||||
private var systemField: NSTextField? = nil
|
||||
@@ -45,6 +57,11 @@ internal class Popup: PopupWrapper {
|
||||
private var average1Field: NSTextField? = nil
|
||||
private var average5Field: NSTextField? = nil
|
||||
private var average15Field: NSTextField? = nil
|
||||
private var coresFreqField: NSTextField? = nil
|
||||
private var eCoresFreqField: NSTextField? = nil
|
||||
private var pCoresFreqField: NSTextField? = nil
|
||||
private var eCoresFreqColorView: NSView? = nil
|
||||
private var pCoresFreqColorView: NSView? = nil
|
||||
|
||||
private var systemColorView: NSView? = nil
|
||||
private var userColorView: NSView? = nil
|
||||
@@ -83,7 +100,7 @@ internal class Popup: PopupWrapper {
|
||||
private var chartColor: NSColor { self.chartColorState.additional as? NSColor ?? NSColor.systemBlue }
|
||||
private var eCoresColorState: SColor = .teal
|
||||
private var eCoresColor: NSColor { self.eCoresColorState.additional as? NSColor ?? NSColor.systemTeal }
|
||||
private var pCoresColorState: SColor = .secondBlue
|
||||
private var pCoresColorState: SColor = .indigo
|
||||
private var pCoresColor: NSColor { self.pCoresColorState.additional as? NSColor ?? NSColor.systemBlue }
|
||||
|
||||
private var numberOfProcesses: Int {
|
||||
@@ -115,7 +132,7 @@ internal class Popup: PopupWrapper {
|
||||
width: Constants.Popup.width,
|
||||
height: self.dashboardHeight + self.chartHeight + self.averageHeight
|
||||
))
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: self.frame.height + self.detailsHeight + self.processesHeight))
|
||||
self.setFrameSize(NSSize(width: self.frame.width, height: self.frame.height + self.detailsHeight + self.frequencyHeight + self.processesHeight))
|
||||
|
||||
self.systemColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_systemColor", defaultValue: self.systemColorState.key))
|
||||
self.userColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_userColor", defaultValue: self.userColorState.key))
|
||||
@@ -135,12 +152,14 @@ internal class Popup: PopupWrapper {
|
||||
gridView.addRow(with: [self.initChart()])
|
||||
gridView.addRow(with: [self.initDetails()])
|
||||
gridView.addRow(with: [self.initAverage()])
|
||||
gridView.addRow(with: [self.initFrequency()])
|
||||
gridView.addRow(with: [self.initProcesses()])
|
||||
|
||||
gridView.row(at: 0).height = self.dashboardHeight
|
||||
gridView.row(at: 1).height = self.chartHeight
|
||||
gridView.row(at: 2).height = self.detailsHeight
|
||||
gridView.row(at: 3).height = self.averageHeight
|
||||
gridView.row(at: 4).height = self.frequencyHeight
|
||||
|
||||
self.addSubview(gridView)
|
||||
self.grid = gridView
|
||||
@@ -189,7 +208,7 @@ internal class Popup: PopupWrapper {
|
||||
|
||||
let usage = NSView(frame: NSRect(x: usageX, y: (view.frame.height - usageSize)/2, width: usageSize, height: usageSize))
|
||||
let temperature = NSView(frame: NSRect(x: (usageX - 50)/2, y: (view.frame.height - 50)/2 - 3, width: 50, height: 50))
|
||||
let frequency = NSView(frame: NSRect(x: (usageX+usageSize) + (usageX - 50)/2, y: (view.frame.height - 50)/2 - 3, width: 50, height: 50))
|
||||
let frequency = NSView(frame: NSRect(x: (usageX+usageSize) + (usageX - 50)/2, y: 0, width: 50, height: self.dashboardHeight))
|
||||
|
||||
self.circle = PieChartView(frame: NSRect(x: 0, y: 0, width: usage.frame.width, height: usage.frame.height), segments: [], drawValue: true)
|
||||
self.circle!.toolTip = localizedString("CPU usage")
|
||||
@@ -315,6 +334,30 @@ internal class Popup: PopupWrapper {
|
||||
return view
|
||||
}
|
||||
|
||||
private func initFrequency() -> NSView {
|
||||
let view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: self.frame.width, height: self.frequencyHeight))
|
||||
let separator = separatorView(localizedString("Frequency"), origin: NSPoint(x: 0, y: self.frequencyHeight-Constants.Popup.separatorHeight), width: self.frame.width)
|
||||
let container: NSStackView = NSStackView(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: separator.frame.origin.y))
|
||||
container.orientation = .vertical
|
||||
container.spacing = 0
|
||||
|
||||
self.coresFreqField = popupRow(container, title: "\(localizedString("All cores")):", value: "").1
|
||||
|
||||
if isARM {
|
||||
if SystemKit.shared.device.info.cpu?.eCores != nil {
|
||||
(self.eCoresFreqColorView, _, self.eCoresFreqField) = popupWithColorRow(container, color: self.eCoresColor, title: "\(localizedString("Efficiency cores")):", value: "")
|
||||
}
|
||||
if SystemKit.shared.device.info.cpu?.pCores != nil {
|
||||
(self.pCoresFreqColorView, _, self.pCoresFreqField) = popupWithColorRow(container, color: self.pCoresColor, title: "\(localizedString("Performance cores")):", value: "")
|
||||
}
|
||||
}
|
||||
|
||||
view.addSubview(separator)
|
||||
view.addSubview(container)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private func initProcesses() -> NSView {
|
||||
if self.numberOfProcesses == 0 { return NSView() }
|
||||
|
||||
@@ -388,7 +431,7 @@ internal class Popup: PopupWrapper {
|
||||
})
|
||||
}
|
||||
|
||||
public func frequencyCallback(_ value: Double?) {
|
||||
public func frequencyCallback(_ value: [Double]?) {
|
||||
guard let value else { return }
|
||||
|
||||
DispatchQueue.main.async(execute: {
|
||||
@@ -397,13 +440,33 @@ internal class Popup: PopupWrapper {
|
||||
}
|
||||
|
||||
if (self.window?.isVisible ?? false) || !self.initializedFrequency {
|
||||
if value > self.maxFreq {
|
||||
self.maxFreq = value
|
||||
}
|
||||
|
||||
if let freqCircle = self.frequencyCircle {
|
||||
freqCircle.setValue((100*value)/self.maxFreq)
|
||||
freqCircle.setText("\((value/1000).rounded(toPlaces: 2))")
|
||||
if value.count == 1 {
|
||||
let freq = value.first ?? 0
|
||||
if freq > self.maxFreq {
|
||||
self.maxFreq = freq
|
||||
}
|
||||
self.coresFreqField?.stringValue = "\(Int(freq)) MHz"
|
||||
if let circle = self.frequencyCircle {
|
||||
circle.setValue((100*freq)/self.maxFreq)
|
||||
circle.setText("\((freq/1000).rounded(toPlaces: 2))")
|
||||
}
|
||||
} else if value.count == 2 {
|
||||
let e = value.first ?? 0
|
||||
let p = value.last ?? 0
|
||||
self.eCoresFreqField?.stringValue = "\(Int(e)) MHz"
|
||||
self.pCoresFreqField?.stringValue = "\(Int(p)) MHz"
|
||||
|
||||
if let eCoreCount = SystemKit.shared.device.info.cpu?.eCores, let pCoreCount = SystemKit.shared.device.info.cpu?.pCores {
|
||||
let freq = ((e * Double(eCoreCount)) + (p * Double(pCoreCount))) / Double(eCoreCount + pCoreCount)
|
||||
if freq > self.maxFreq {
|
||||
self.maxFreq = freq
|
||||
}
|
||||
self.coresFreqField?.stringValue = "\(Int(freq)) MHz"
|
||||
if let circle = self.frequencyCircle {
|
||||
circle.setValue((100*freq)/self.maxFreq)
|
||||
circle.setText("\((freq/1000).rounded(toPlaces: 2))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.initializedFrequency = true
|
||||
@@ -461,15 +524,6 @@ internal class Popup: PopupWrapper {
|
||||
})
|
||||
}
|
||||
|
||||
public func toggleFrequency(state: Bool) {
|
||||
DispatchQueue.main.async(execute: {
|
||||
if let view = self.frequencyCircle {
|
||||
view.isHidden = !state
|
||||
}
|
||||
self.initializedFrequency = false
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
public override func settings() -> NSView? {
|
||||
@@ -578,7 +632,10 @@ internal class Popup: PopupWrapper {
|
||||
}
|
||||
self.eCoresColorState = newValue
|
||||
Store.shared.set(key: "\(self.title)_eCoresColor", value: key)
|
||||
self.eCoresColorView?.layer?.backgroundColor = (newValue.additional as? NSColor)?.cgColor
|
||||
if let color = (newValue.additional as? NSColor) {
|
||||
self.eCoresColorView?.layer?.backgroundColor = color.cgColor
|
||||
self.eCoresFreqColorView?.layer?.backgroundColor = color.cgColor
|
||||
}
|
||||
}
|
||||
@objc private func togglePCoresColor(_ sender: NSMenuItem) {
|
||||
guard let key = sender.representedObject as? String, let newValue = SColor.allColors.first(where: { $0.key == key }) else {
|
||||
@@ -586,7 +643,10 @@ internal class Popup: PopupWrapper {
|
||||
}
|
||||
self.pCoresColorState = newValue
|
||||
Store.shared.set(key: "\(self.title)_pCoresColor", value: key)
|
||||
self.pCoresColorView?.layer?.backgroundColor = (newValue.additional as? NSColor)?.cgColor
|
||||
if let color = (newValue.additional as? NSColor) {
|
||||
self.pCoresColorView?.layer?.backgroundColor = color.cgColor
|
||||
self.pCoresFreqColorView?.layer?.backgroundColor = color.cgColor
|
||||
}
|
||||
}
|
||||
@objc private func toggleLineChartHistory(_ sender: NSMenuItem) {
|
||||
guard let key = sender.representedObject as? String, let value = Int(key) else { return }
|
||||
|
||||
@@ -43,7 +43,7 @@ public class Portal: PortalWrapper {
|
||||
private var idleColor: NSColor { self.idleColorState.additional as? NSColor ?? NSColor.lightGray }
|
||||
private var eCoresColorState: SColor = .teal
|
||||
private var eCoresColor: NSColor { self.eCoresColorState.additional as? NSColor ?? NSColor.systemTeal }
|
||||
private var pCoresColorState: SColor = .secondBlue
|
||||
private var pCoresColorState: SColor = .indigo
|
||||
private var pCoresColor: NSColor { self.pCoresColorState.additional as? NSColor ?? NSColor.systemBlue }
|
||||
|
||||
public override func load() {
|
||||
|
||||
@@ -289,161 +289,173 @@ public class TemperatureReader: Reader<Double> {
|
||||
}
|
||||
}
|
||||
|
||||
public class FrequencyReader: Reader<Double> {
|
||||
private typealias PGSample = UInt64
|
||||
private typealias UDouble = UnsafeMutablePointer<Double>
|
||||
// inspired by https://github.com/shank03/StatsBar/blob/e175aa71c914ce882ce2e90163f3eb18262a8e25/StatsBar/Service/IOReport.swift
|
||||
public class FrequencyReader: Reader<[Double]> {
|
||||
private var eCoreFreqs: [Int32] = []
|
||||
private var pCoreFreqs: [Int32] = []
|
||||
|
||||
private typealias PG_InitializePointerFunction = @convention(c) () -> Bool
|
||||
private typealias PG_ShutdownPointerFunction = @convention(c) () -> Bool
|
||||
private typealias PG_ReadSamplePointerFunction = @convention(c) (Int, UnsafeMutablePointer<PGSample>) -> Bool
|
||||
private typealias PGSample_GetIAFrequencyPointerFunction = @convention(c) (PGSample, PGSample, UDouble, UDouble, UDouble) -> Bool
|
||||
private typealias PGSample_ReleasePointerFunction = @convention(c) (PGSample) -> Bool
|
||||
private var channels: CFMutableDictionary? = nil
|
||||
private var subscription: IOReportSubscriptionRef? = nil
|
||||
private var prev: (samples: CFDictionary, time: TimeInterval)? = nil
|
||||
|
||||
private var bundle: CFBundle? = nil
|
||||
private let measurementCount: Int = 4
|
||||
|
||||
private var pgIntialize: PG_InitializePointerFunction? = nil
|
||||
private var pgShutdown: PG_ShutdownPointerFunction? = nil
|
||||
private var pgReadSample: PG_ReadSamplePointerFunction? = nil
|
||||
private var pgSampleGetIAFrequency: PGSample_GetIAFrequencyPointerFunction? = nil
|
||||
private var pgSampleRelease: PGSample_ReleasePointerFunction? = nil
|
||||
|
||||
private var sample: PGSample = 0
|
||||
private var reconnectAttempt: Int = 0
|
||||
|
||||
private var isEnabled: Bool {
|
||||
get {
|
||||
return Store.shared.bool(key: "CPU_IPG", defaultValue: false)
|
||||
}
|
||||
private struct IOSample {
|
||||
let group: String
|
||||
let subGroup: String
|
||||
let channel: String
|
||||
let unit: String
|
||||
let delta: CFDictionary
|
||||
}
|
||||
|
||||
public override func setup() {
|
||||
guard self.isEnabled else { return }
|
||||
|
||||
let path: CFString = "/Library/Frameworks/IntelPowerGadget.framework" as CFString
|
||||
let bundleURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, path, CFURLPathStyle.cfurlposixPathStyle, true)
|
||||
|
||||
self.bundle = CFBundleCreate(kCFAllocatorDefault, bundleURL)
|
||||
if self.bundle == nil {
|
||||
error("IntelPowerGadget framework not found", log: self.log)
|
||||
return
|
||||
}
|
||||
|
||||
if !CFBundleLoadExecutable(self.bundle) {
|
||||
error("failed to load IPG framework", log: self.log)
|
||||
return
|
||||
}
|
||||
|
||||
guard let pgIntialize = CFBundleGetFunctionPointerForName(self.bundle, "PG_Initialize" as CFString) else {
|
||||
error("failed to find PG_Initialize", log: self.log)
|
||||
return
|
||||
}
|
||||
guard let pgShutdown = CFBundleGetFunctionPointerForName(self.bundle, "PG_Shutdown" as CFString) else {
|
||||
error("failed to find PG_Shutdown", log: self.log)
|
||||
return
|
||||
}
|
||||
guard let pgReadSample = CFBundleGetFunctionPointerForName(self.bundle, "PG_ReadSample" as CFString) else {
|
||||
error("failed to find PG_ReadSample", log: self.log)
|
||||
return
|
||||
}
|
||||
guard let pgSampleGetIAFrequency = CFBundleGetFunctionPointerForName(self.bundle, "PGSample_GetIAFrequency" as CFString) else {
|
||||
error("failed to find PGSample_GetIAFrequency", log: self.log)
|
||||
return
|
||||
}
|
||||
guard let pgSampleRelease = CFBundleGetFunctionPointerForName(self.bundle, "PGSample_Release" as CFString) else {
|
||||
error("failed to find PGSample_Release", log: self.log)
|
||||
return
|
||||
}
|
||||
|
||||
self.pgIntialize = unsafeBitCast(pgIntialize, to: PG_InitializePointerFunction.self)
|
||||
self.pgShutdown = unsafeBitCast(pgShutdown, to: PG_ShutdownPointerFunction.self)
|
||||
self.pgReadSample = unsafeBitCast(pgReadSample, to: PG_ReadSamplePointerFunction.self)
|
||||
self.pgSampleGetIAFrequency = unsafeBitCast(pgSampleGetIAFrequency, to: PGSample_GetIAFrequencyPointerFunction.self)
|
||||
self.pgSampleRelease = unsafeBitCast(pgSampleRelease, to: PGSample_ReleasePointerFunction.self)
|
||||
|
||||
if let initialize = self.pgIntialize {
|
||||
if !initialize() {
|
||||
error("IPG initialization failed", log: self.log)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let bundle = self.bundle {
|
||||
CFBundleUnloadExecutable(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
public override func terminate() {
|
||||
if let shutdown = self.pgShutdown {
|
||||
if !shutdown() {
|
||||
error("IPG shutdown failed", log: self.log)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let release = self.pgSampleRelease {
|
||||
if self.sample != 0 {
|
||||
_ = release(self.sample)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reconnect() {
|
||||
if self.reconnectAttempt >= 5 {
|
||||
return
|
||||
}
|
||||
|
||||
self.sample = 0
|
||||
self.terminate()
|
||||
if let initialize = self.pgIntialize {
|
||||
if !initialize() {
|
||||
error("IPG initialization failed", log: self.log)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.reconnectAttempt += 1
|
||||
self.eCoreFreqs = SystemKit.shared.device.info.cpu?.eCoreFrequencies ?? []
|
||||
self.pCoreFreqs = SystemKit.shared.device.info.cpu?.pCoreFrequencies ?? []
|
||||
self.channels = self.getChannels()
|
||||
var dict: Unmanaged<CFMutableDictionary>?
|
||||
self.subscription = IOReportCreateSubscription(nil, self.channels, &dict, 0, nil)
|
||||
}
|
||||
|
||||
public override func read() {
|
||||
if !self.isEnabled || self.pgReadSample == nil || self.pgSampleGetIAFrequency == nil || self.pgSampleRelease == nil {
|
||||
return
|
||||
}
|
||||
guard !self.eCoreFreqs.isEmpty && !self.pCoreFreqs.isEmpty, self.channels != nil, self.subscription != nil else { return }
|
||||
let minECoreFreq = Double(self.eCoreFreqs.min() ?? 0)
|
||||
let minPCoreFreq = Double(self.pCoreFreqs.min() ?? 0)
|
||||
|
||||
// first sample initlialization
|
||||
if self.sample == 0 {
|
||||
if !self.pgReadSample!(0, &self.sample) {
|
||||
error("read self.sample failed", log: self.log)
|
||||
Task {
|
||||
var eCores: [Double] = []
|
||||
var pCores: [Double] = []
|
||||
|
||||
for (samples, _) in await self.getSamples() {
|
||||
var eCore: [Double] = []
|
||||
var pCore: [Double] = []
|
||||
|
||||
for sample in samples {
|
||||
guard sample.group == "CPU Stats" else { continue }
|
||||
if sample.channel.starts(with: "ECPU") {
|
||||
eCore.append(self.calculateFrequencies(dict: sample.delta, freqs: self.eCoreFreqs))
|
||||
}
|
||||
if sample.channel.starts(with: "PCPU") {
|
||||
pCore.append(self.calculateFrequencies(dict: sample.delta, freqs: self.pCoreFreqs))
|
||||
}
|
||||
}
|
||||
|
||||
let eCoresAvgFreq: Double = eCore.isEmpty ? 0 : (eCore.reduce(0.0, { $0 + $1 }) / Double(eCore.count))
|
||||
let pCoresAvgFreq: Double = pCore.isEmpty ? 0 : (pCore.reduce(0.0, { $0 + $1 }) / Double(pCore.count))
|
||||
eCores.append(max(eCoresAvgFreq, minECoreFreq))
|
||||
pCores.append(max(pCoresAvgFreq, minPCoreFreq))
|
||||
}
|
||||
return
|
||||
|
||||
let eFreq: Double = eCores.reduce(0, { $0 + $1 }) / Double(self.measurementCount)
|
||||
let pFreq: Double = pCores.reduce(0, { $0 + $1 }) / Double(self.measurementCount)
|
||||
|
||||
self.callback([eFreq, pFreq])
|
||||
}
|
||||
}
|
||||
|
||||
private func calculateFrequencies(dict: CFDictionary, freqs: [Int32]) -> Double {
|
||||
let items = self.getResidencies(dict: dict)
|
||||
let offset = items.firstIndex { $0.0 != "IDLE" && $0.0 != "DOWN" && $0.0 != "OFF" }!
|
||||
let usage = items.dropFirst(offset).reduce(0.0) { $0 + Double($1.f) }
|
||||
let count = freqs.count
|
||||
var avgFreq: Double = 0
|
||||
|
||||
for i in 0..<count {
|
||||
let percent = usage == 0 ? 0 : Double(items[i + offset].f) / usage
|
||||
avgFreq += percent * Double(freqs[i])
|
||||
}
|
||||
|
||||
var local: PGSample = 0
|
||||
var value: Double = 0
|
||||
var min: Double = 0
|
||||
var max: Double = 0
|
||||
return avgFreq
|
||||
}
|
||||
|
||||
func getResidencies(dict: CFDictionary) -> [(ns: String, f: Int64)] {
|
||||
let count = IOReportStateGetCount(dict)
|
||||
var res: [(String, Int64)] = []
|
||||
|
||||
if !self.pgReadSample!(0, &local) {
|
||||
self.reconnect()
|
||||
error("read local sample failed", log: self.log)
|
||||
return
|
||||
for i in 0..<count {
|
||||
let name = IOReportStateGetNameForIndex(dict, i)?.takeUnretainedValue() ?? ("" as CFString)
|
||||
let val = IOReportStateGetResidency(dict, i)
|
||||
res.append((name as String, val))
|
||||
}
|
||||
|
||||
defer {
|
||||
if !self.pgSampleRelease!(self.sample) {
|
||||
error("release self.sample failed", log: self.log)
|
||||
return res
|
||||
}
|
||||
|
||||
private func getChannels() -> CFMutableDictionary? {
|
||||
let channelNames = [
|
||||
("CPU Stats", "CPU Complex Performance States"),
|
||||
("CPU Stats", "CPU Core Performance States")
|
||||
]
|
||||
|
||||
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 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)]()
|
||||
|
||||
for _ in 0..<self.measurementCount {
|
||||
let milliseconds = UInt64(step) * 1_000_000
|
||||
try? await Task.sleep(nanoseconds: milliseconds)
|
||||
guard let next = self.getSample() else { continue }
|
||||
guard let diff = IOReportCreateSamplesDelta(prev.samples, next.samples, nil)?.takeRetainedValue() else {
|
||||
continue
|
||||
}
|
||||
self.sample = local
|
||||
let elapsed = Date(timeIntervalSince1970: next.time).timeIntervalSince(Date(timeIntervalSince1970: prev.time))
|
||||
prev = next
|
||||
samples.append((self.collectIOSamples(data: diff), max(elapsed, TimeInterval(1))))
|
||||
}
|
||||
|
||||
if !self.pgSampleGetIAFrequency!(self.sample, local, &value, &min, &max) {
|
||||
error("read frequency failed", log: self.log)
|
||||
return
|
||||
self.prev = prev
|
||||
return samples
|
||||
}
|
||||
|
||||
private func getSample() -> (samples: CFDictionary, time: TimeInterval)? {
|
||||
guard let sample = IOReportCreateSamples(self.subscription, self.channels, nil)?.takeRetainedValue() else {
|
||||
return nil
|
||||
}
|
||||
return (sample, Date().timeIntervalSince1970)
|
||||
}
|
||||
|
||||
private func collectIOSamples(data: CFDictionary) -> [IOSample] {
|
||||
let dict = data as! [String: Any]
|
||||
let items = dict["IOReportChannels"] as! CFArray
|
||||
let itemSize = CFArrayGetCount(items)
|
||||
|
||||
var samples = [IOSample]()
|
||||
|
||||
for index in 0..<itemSize {
|
||||
let dict = CFArrayGetValueAtIndex(items, index)
|
||||
let item = unsafeBitCast(dict, to: CFDictionary.self)
|
||||
|
||||
let group = IOReportChannelGetGroup(item)?.takeUnretainedValue() ?? ("" as CFString)
|
||||
let subGroup = IOReportChannelGetSubGroup(item)?.takeUnretainedValue() ?? ("" as CFString)
|
||||
let channel = IOReportChannelGetChannelName(item)?.takeUnretainedValue() ?? ("" as CFString)
|
||||
let unit = IOReportChannelGetUnitLabel(item)?.takeUnretainedValue() ?? ("" as CFString)
|
||||
|
||||
samples.append(IOSample(group: group as String, subGroup: subGroup as String, channel: channel as String, unit: unit as String, delta: item))
|
||||
}
|
||||
|
||||
self.callback(value)
|
||||
return samples
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ internal class Settings: NSStackView, Settings_v {
|
||||
private var usagePerCoreState: Bool = false
|
||||
private var hyperthreadState: Bool = false
|
||||
private var splitValueState: Bool = false
|
||||
private var IPGState: Bool = false
|
||||
private var updateIntervalValue: Int = 1
|
||||
private var updateTopIntervalValue: Int = 1
|
||||
private var numberOfProcesses: Int = 8
|
||||
@@ -27,7 +26,6 @@ internal class Settings: NSStackView, Settings_v {
|
||||
|
||||
public var callback: (() -> Void) = {}
|
||||
public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {}
|
||||
public var IPGCallback: ((_ state: Bool) -> Void) = {_ in }
|
||||
public var setInterval: ((_ value: Int) -> Void) = {_ in }
|
||||
public var setTopInterval: ((_ value: Int) -> Void) = {_ in }
|
||||
|
||||
@@ -41,7 +39,6 @@ internal class Settings: NSStackView, Settings_v {
|
||||
self.hyperthreadState = Store.shared.bool(key: "\(self.title)_hyperhreading", defaultValue: self.hyperthreadState)
|
||||
self.usagePerCoreState = Store.shared.bool(key: "\(self.title)_usagePerCore", defaultValue: self.usagePerCoreState)
|
||||
self.splitValueState = Store.shared.bool(key: "\(self.title)_splitValue", defaultValue: self.splitValueState)
|
||||
self.IPGState = Store.shared.bool(key: "\(self.title)_IPG", defaultValue: self.IPGState)
|
||||
self.updateIntervalValue = Store.shared.int(key: "\(self.title)_updateInterval", defaultValue: self.updateIntervalValue)
|
||||
self.updateTopIntervalValue = Store.shared.int(key: "\(self.title)_updateTopInterval", defaultValue: self.updateTopIntervalValue)
|
||||
self.numberOfProcesses = Store.shared.int(key: "\(self.title)_processes", defaultValue: self.numberOfProcesses)
|
||||
@@ -135,17 +132,6 @@ internal class Settings: NSStackView, Settings_v {
|
||||
|
||||
self.addArrangedSubview(PreferencesSection(rows))
|
||||
}
|
||||
|
||||
#if arch(x86_64)
|
||||
if hasIPG {
|
||||
self.addArrangedSubview(PreferencesSection([
|
||||
PreferencesRow("\(localizedString("CPU frequency")) (IPG)", component: switchView(
|
||||
action: #selector(self.toggleIPG),
|
||||
state: self.IPGState
|
||||
))
|
||||
]))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@objc private func changeUpdateInterval(_ sender: NSMenuItem) {
|
||||
@@ -203,12 +189,6 @@ internal class Settings: NSStackView, Settings_v {
|
||||
self.callback()
|
||||
}
|
||||
|
||||
@objc func toggleIPG(_ sender: NSControl) {
|
||||
self.IPGState = controlState(sender)
|
||||
Store.shared.set(key: "\(self.title)_IPG", value: self.IPGState)
|
||||
self.IPGCallback(self.IPGState)
|
||||
}
|
||||
|
||||
@objc func toggleSplitValue(_ sender: NSControl) {
|
||||
self.splitValueState = controlState(sender)
|
||||
Store.shared.set(key: "\(self.title)_splitValue", value: self.splitValueState)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
5C0A9CAA2C46838A00EE6A89 /* GPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A90E18924EAD2BB00471E9A /* GPU.framework */; };
|
||||
5C0A9CAF2C46838F00EE6A89 /* RAM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A81C7562449A41400825D92 /* RAM.framework */; };
|
||||
5C0A9CB42C46839500EE6A89 /* Disk.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF9EE0224648751005D2270 /* Disk.framework */; };
|
||||
5C1E45562D11D66A00525864 /* libIOReport.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1E45552D11D66200525864 /* libIOReport.tbd */; };
|
||||
5C21D80B296C7B81005BA16D /* CombinedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C21D80A296C7B81005BA16D /* CombinedView.swift */; };
|
||||
5C2229A329CCB3C400F00E69 /* Clock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C22299D29CCB3C400F00E69 /* Clock.framework */; };
|
||||
5C2229A429CCB3C400F00E69 /* Clock.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5C22299D29CCB3C400F00E69 /* Clock.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
@@ -495,6 +496,8 @@
|
||||
5C0A9CA12C467AA300EE6A89 /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = "<group>"; };
|
||||
5C0A9CA32C467F7A00EE6A89 /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = "<group>"; };
|
||||
5C0E550A2B5D545A00FFF1FB /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5C1E45552D11D66200525864 /* libIOReport.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libIOReport.tbd; path = usr/lib/libIOReport.tbd; sourceTree = SDKROOT; };
|
||||
5C1E45572D11D67600525864 /* bridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bridge.h; sourceTree = "<group>"; };
|
||||
5C21D80A296C7B81005BA16D /* CombinedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombinedView.swift; sourceTree = "<group>"; };
|
||||
5C22299D29CCB3C400F00E69 /* Clock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Clock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5C2229A829CCB41900F00E69 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
@@ -804,6 +807,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C1E45562D11D66A00525864 /* libIOReport.tbd in Frameworks */,
|
||||
9A2847C22666AA8700EC1F6D /* Kit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1155,6 +1159,7 @@
|
||||
5C3068722C32E83200B05EFA /* widget.swift */,
|
||||
9A97CECD2537331B00742D8F /* Info.plist */,
|
||||
9A97CEFF2537340400742D8F /* config.plist */,
|
||||
5C1E45572D11D67600525864 /* bridge.h */,
|
||||
);
|
||||
path = CPU;
|
||||
sourceTree = "<group>";
|
||||
@@ -1162,6 +1167,7 @@
|
||||
9A998CD622A199920087ADE7 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C1E45552D11D66200525864 /* libIOReport.tbd */,
|
||||
9ADE7005265D039300D2FBA8 /* IOKit.framework */,
|
||||
9A97CE2A25371B2300742D8F /* IntelPowerGadget.framework */,
|
||||
9A998CD922A199970087ADE7 /* ServiceManagement.framework */,
|
||||
@@ -1699,7 +1705,7 @@
|
||||
New,
|
||||
);
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1600;
|
||||
LastUpgradeCheck = 1620;
|
||||
ORGANIZATIONNAME = "Serhiy Mytrovtsiy";
|
||||
TargetAttributes = {
|
||||
5C22299C29CCB3C400F00E69 = {
|
||||
@@ -2850,7 +2856,6 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
@@ -2888,7 +2893,6 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
@@ -3259,6 +3263,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = Modules/CPU/bridge.h;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
USE_HEADERMAP = YES;
|
||||
@@ -3299,6 +3304,7 @@
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = Modules/CPU/bridge.h;
|
||||
SWIFT_VERSION = 5.0;
|
||||
USE_HEADERMAP = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1610"
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1610"
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1610"
|
||||
LastUpgradeVersion = "1620"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>641</string>
|
||||
<string>642</string>
|
||||
<key>Description</key>
|
||||
<string>Simple macOS system monitor in your menu bar</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.11.21</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>641</string>
|
||||
<string>642</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
Reference in New Issue
Block a user