mirror of
https://github.com/aristocratos/btop.git
synced 2026-04-24 00:53:17 -04:00
- Add CFRef<T> template for CoreFoundation types (CFRelease) - Add IORef wrapper for IOKit object types (IOObjectRelease) - Refactor GPU code to use RAII wrappers for all local CF/IO objects - Add 'apple' to shown_gpus description in btop_menu.cpp
1982 lines
70 KiB
C++
1982 lines
70 KiB
C++
/* Copyright 2021 Aristocratos (jakob@qvantnet.com)
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
indent = tab
|
|
tab-size = 4
|
|
*/
|
|
|
|
#include <Availability.h>
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <IOKit/IOKitLib.h>
|
|
#include <arpa/inet.h>
|
|
#include <libproc.h>
|
|
#include <mach/mach.h>
|
|
#include <mach/mach_host.h>
|
|
#include <mach/mach_init.h>
|
|
#include <mach/mach_types.h>
|
|
#include <mach/processor_info.h>
|
|
#include <mach/vm_statistics.h>
|
|
#include <mach/mach_time.h>
|
|
// BUGS
|
|
// If both <net/if.h> and <ifaddrs.h> are being included, <net/if.h> must be
|
|
// included before <ifaddrs.h>.
|
|
// from: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/getifaddrs.3.html
|
|
#include <net/if.h>
|
|
#include <ifaddrs.h>
|
|
#include <net/if_dl.h>
|
|
#include <netdb.h>
|
|
#include <netinet/tcp_fsm.h>
|
|
#include <pwd.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/statvfs.h>
|
|
#include <sys/sysctl.h>
|
|
#include <sys/types.h>
|
|
#include <netinet/in.h> // for inet_ntop
|
|
#include <unistd.h>
|
|
#include <stdexcept>
|
|
#include <utility>
|
|
|
|
#include <cmath>
|
|
#include <fstream>
|
|
#include <mutex>
|
|
#include <numeric>
|
|
#include <ranges>
|
|
#include <regex>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
|
|
#include <fmt/format.h>
|
|
|
|
#include "../btop_config.hpp"
|
|
#include "../btop_log.hpp"
|
|
#include "../btop_shared.hpp"
|
|
#include "../btop_tools.hpp"
|
|
|
|
#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504
|
|
#include "sensors.hpp"
|
|
#endif
|
|
#include "smc.hpp"
|
|
|
|
#if defined(GPU_SUPPORT)
|
|
#include <dlfcn.h>
|
|
#include <mach/mach_time.h>
|
|
|
|
//? IOReport C function declarations for Apple Silicon GPU metrics
|
|
extern "C" {
|
|
typedef struct IOReportSubscription* IOReportSubscriptionRef;
|
|
|
|
CFDictionaryRef IOReportCopyChannelsInGroup(CFStringRef group, CFStringRef subgroup,
|
|
uint64_t a, uint64_t b, uint64_t c);
|
|
void IOReportMergeChannels(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef cfnull);
|
|
IOReportSubscriptionRef IOReportCreateSubscription(void* a, CFMutableDictionaryRef b,
|
|
CFMutableDictionaryRef* c, uint64_t d, CFTypeRef cfnull);
|
|
CFDictionaryRef IOReportCreateSamples(IOReportSubscriptionRef sub,
|
|
CFMutableDictionaryRef chan, CFTypeRef cfnull);
|
|
CFDictionaryRef IOReportCreateSamplesDelta(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef cfnull);
|
|
CFStringRef IOReportChannelGetGroup(CFDictionaryRef item);
|
|
CFStringRef IOReportChannelGetSubGroup(CFDictionaryRef item);
|
|
CFStringRef IOReportChannelGetChannelName(CFDictionaryRef item);
|
|
int64_t IOReportSimpleGetIntegerValue(CFDictionaryRef item, int32_t idx);
|
|
CFStringRef IOReportChannelGetUnitLabel(CFDictionaryRef item);
|
|
int32_t IOReportStateGetCount(CFDictionaryRef item);
|
|
CFStringRef IOReportStateGetNameForIndex(CFDictionaryRef item, int32_t idx);
|
|
int64_t IOReportStateGetResidency(CFDictionaryRef item, int32_t idx);
|
|
|
|
//? IOHIDEvent declarations for GPU temperature
|
|
typedef struct __IOHIDEvent* IOHIDEventRef;
|
|
typedef struct __IOHIDServiceClient* IOHIDServiceClientRef;
|
|
typedef struct __IOHIDEventSystemClient* IOHIDEventSystemClientRef;
|
|
#ifdef __LP64__
|
|
typedef double IOHIDFloat;
|
|
#else
|
|
typedef float IOHIDFloat;
|
|
#endif
|
|
IOHIDEventSystemClientRef IOHIDEventSystemClientCreate(CFAllocatorRef allocator);
|
|
int IOHIDEventSystemClientSetMatching(IOHIDEventSystemClientRef client, CFDictionaryRef match);
|
|
CFArrayRef IOHIDEventSystemClientCopyServices(IOHIDEventSystemClientRef client);
|
|
IOHIDEventRef IOHIDServiceClientCopyEvent(IOHIDServiceClientRef sc, int64_t type, int32_t a, int64_t b);
|
|
CFStringRef IOHIDServiceClientCopyProperty(IOHIDServiceClientRef service, CFStringRef property);
|
|
IOHIDFloat IOHIDEventGetFloatValue(IOHIDEventRef event, int32_t field);
|
|
}
|
|
#endif // GPU_SUPPORT
|
|
|
|
#if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000
|
|
#define kIOMainPortDefault kIOMasterPortDefault
|
|
#endif
|
|
|
|
using std::clamp, std::string_literals::operator""s, std::cmp_equal, std::cmp_less, std::cmp_greater;
|
|
using std::ifstream, std::numeric_limits, std::streamsize, std::round, std::max, std::min;
|
|
namespace fs = std::filesystem;
|
|
namespace rng = std::ranges;
|
|
using namespace Tools;
|
|
|
|
//? RAII wrapper for CoreFoundation types — releases via CFRelease() on destruction
|
|
template <typename T>
|
|
struct CFRef {
|
|
T ref;
|
|
CFRef() : ref(nullptr) {}
|
|
CFRef(T ref) : ref(ref) {}
|
|
~CFRef() { if (ref) CFRelease((CFTypeRef)ref); }
|
|
CFRef(const CFRef&) = delete;
|
|
CFRef& operator=(const CFRef&) = delete;
|
|
CFRef(CFRef&& other) noexcept : ref(other.ref) { other.ref = nullptr; }
|
|
CFRef& operator=(CFRef&& other) noexcept {
|
|
if (this != &other) { reset(); ref = other.ref; other.ref = nullptr; }
|
|
return *this;
|
|
}
|
|
operator T() const { return ref; }
|
|
T get() const { return ref; }
|
|
T* ptr() { return &ref; }
|
|
void reset(T new_ref = nullptr) {
|
|
if (ref) CFRelease((CFTypeRef)ref);
|
|
ref = new_ref;
|
|
}
|
|
T release() { T r = ref; ref = nullptr; return r; }
|
|
};
|
|
|
|
//? RAII wrapper for IOKit object types — releases via IOObjectRelease() on destruction
|
|
struct IORef {
|
|
io_object_t ref;
|
|
IORef() : ref(0) {}
|
|
IORef(io_object_t ref) : ref(ref) {}
|
|
~IORef() { if (ref) IOObjectRelease(ref); }
|
|
IORef(const IORef&) = delete;
|
|
IORef& operator=(const IORef&) = delete;
|
|
operator io_object_t() const { return ref; }
|
|
io_object_t get() const { return ref; }
|
|
io_object_t* ptr() { return &ref; }
|
|
};
|
|
|
|
//? --------------------------------------------------- FUNCTIONS -----------------------------------------------------
|
|
|
|
namespace Cpu {
|
|
vector<long long> core_old_totals;
|
|
vector<long long> core_old_idles;
|
|
vector<string> available_fields = {"Auto", "total"};
|
|
vector<string> available_sensors = {"Auto"};
|
|
cpu_info current_cpu;
|
|
bool got_sensors = false, cpu_temp_only = false, supports_watts = false;
|
|
int core_offset = 0;
|
|
|
|
//* Populate found_sensors map
|
|
bool get_sensors();
|
|
|
|
//* Get current cpu clock speed
|
|
string get_cpuHz();
|
|
|
|
//* Search /proc/cpuinfo for a cpu name
|
|
string get_cpuName();
|
|
|
|
struct Sensor {
|
|
fs::path path;
|
|
string label;
|
|
int64_t temp = 0;
|
|
int64_t high = 0;
|
|
int64_t crit = 0;
|
|
};
|
|
|
|
string cpu_sensor;
|
|
vector<string> core_sensors;
|
|
std::unordered_map<int, int> core_mapping;
|
|
} // namespace Cpu
|
|
|
|
namespace Mem {
|
|
double old_uptime;
|
|
}
|
|
|
|
#if defined(GPU_SUPPORT)
|
|
namespace Gpu {
|
|
vector<gpu_info> gpus;
|
|
|
|
//? Stub shutdown for backends not available on macOS
|
|
namespace Nvml { bool shutdown() { return false; } }
|
|
namespace Rsmi { bool shutdown() { return false; } }
|
|
|
|
//? Apple Silicon GPU data collection via IOReport
|
|
namespace AppleSilicon {
|
|
bool initialized = false;
|
|
unsigned int device_count = 0;
|
|
|
|
//? Forward declaration
|
|
template <bool is_init>
|
|
bool collect(gpu_info* gpus_slice);
|
|
|
|
//? IOReport subscription state
|
|
IOReportSubscriptionRef ior_sub = nullptr;
|
|
CFMutableDictionaryRef ior_chan = nullptr;
|
|
CFDictionaryRef prev_sample = nullptr;
|
|
uint64_t prev_sample_time = 0;
|
|
|
|
//? GPU frequency table from DVFS
|
|
vector<uint32_t> gpu_freqs;
|
|
|
|
static string cfstring_to_string(CFStringRef cfstr) {
|
|
if (not cfstr) return "";
|
|
char buf[256];
|
|
if (CFStringGetCString(cfstr, buf, sizeof(buf), kCFStringEncodingUTF8))
|
|
return string(buf);
|
|
return "";
|
|
}
|
|
|
|
static string get_chip_name() {
|
|
char buf[256];
|
|
size_t size = sizeof(buf);
|
|
if (sysctlbyname("machdep.cpu.brand_string", buf, &size, nullptr, 0) == 0)
|
|
return string(buf);
|
|
return "Apple Silicon GPU";
|
|
}
|
|
|
|
static uint64_t get_mach_time_ms() {
|
|
static mach_timebase_info_data_t timebase = {0, 0};
|
|
if (timebase.denom == 0) mach_timebase_info(&timebase);
|
|
return (mach_absolute_time() * timebase.numer / timebase.denom) / 1000000;
|
|
}
|
|
|
|
//? Read GPU DVFS frequency table from IORegistry pmgr node
|
|
static void get_gpu_freqs_from_pmgr() {
|
|
io_iterator_t iter_raw;
|
|
//? matchDict ownership is consumed by IOServiceGetMatchingServices
|
|
CFMutableDictionaryRef matchDict = IOServiceMatching("AppleARMIODevice");
|
|
if (IOServiceGetMatchingServices(kIOMainPortDefault, matchDict, &iter_raw) != kIOReturnSuccess)
|
|
return;
|
|
IORef iter(iter_raw);
|
|
|
|
io_object_t entry_raw;
|
|
while ((entry_raw = IOIteratorNext(iter)) != 0) {
|
|
IORef entry(entry_raw);
|
|
char name[128];
|
|
if (IORegistryEntryGetName(entry, name) == kIOReturnSuccess and string(name) == "pmgr") {
|
|
CFMutableDictionaryRef props_raw = nullptr;
|
|
if (IORegistryEntryCreateCFProperties(entry, &props_raw, kCFAllocatorDefault, 0) == kIOReturnSuccess and props_raw) {
|
|
CFRef<CFMutableDictionaryRef> props(props_raw);
|
|
CFDataRef dvfs_data = (CFDataRef)CFDictionaryGetValue(props, CFSTR("voltage-states9"));
|
|
if (dvfs_data) {
|
|
auto len = CFDataGetLength(dvfs_data);
|
|
auto ptr = CFDataGetBytePtr(dvfs_data);
|
|
//? Pairs of (freq, voltage), 4 bytes each
|
|
for (CFIndex i = 0; i + 7 < len; i += 8) {
|
|
uint32_t freq = 0;
|
|
memcpy(&freq, ptr + i, 4);
|
|
if (freq > 0) gpu_freqs.push_back(freq / (1000 * 1000)); // Hz -> MHz
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool init() {
|
|
if (initialized) return false;
|
|
|
|
//? Get GPU frequency table
|
|
get_gpu_freqs_from_pmgr();
|
|
|
|
//? Set up IOReport channels for GPU Stats and Energy Model
|
|
CFRef<CFStringRef> gpu_stats_group(CFStringCreateWithCString(kCFAllocatorDefault, "GPU Stats", kCFStringEncodingUTF8));
|
|
CFRef<CFStringRef> gpu_perf_subgroup(CFStringCreateWithCString(kCFAllocatorDefault, "GPU Performance States", kCFStringEncodingUTF8));
|
|
CFRef<CFStringRef> energy_group(CFStringCreateWithCString(kCFAllocatorDefault, "Energy Model", kCFStringEncodingUTF8));
|
|
|
|
CFRef<CFDictionaryRef> gpu_chan(IOReportCopyChannelsInGroup(gpu_stats_group, gpu_perf_subgroup, 0, 0, 0));
|
|
CFRef<CFDictionaryRef> energy_chan(IOReportCopyChannelsInGroup(energy_group, nullptr, 0, 0, 0));
|
|
|
|
if (not gpu_chan.get() and not energy_chan.get()) {
|
|
Logger::info("Apple Silicon GPU: No IOReport channels found, GPU monitoring unavailable");
|
|
return false;
|
|
}
|
|
|
|
//? Merge channels into a single subscription
|
|
if (gpu_chan.get() and energy_chan.get()) {
|
|
IOReportMergeChannels(gpu_chan, energy_chan, nullptr);
|
|
}
|
|
CFDictionaryRef base_chan = gpu_chan.get() ? gpu_chan.get() : energy_chan.get();
|
|
|
|
auto size = CFDictionaryGetCount(base_chan);
|
|
ior_chan = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, size, base_chan);
|
|
|
|
//? Create IOReport subscription
|
|
CFMutableDictionaryRef sub_dict = nullptr;
|
|
ior_sub = IOReportCreateSubscription(nullptr, ior_chan, &sub_dict, 0, nullptr);
|
|
if (not ior_sub) {
|
|
Logger::warning("Apple Silicon GPU: Failed to create IOReport subscription");
|
|
CFRelease(ior_chan);
|
|
ior_chan = nullptr;
|
|
return false;
|
|
}
|
|
|
|
device_count = 1; //? Apple Silicon has one integrated GPU
|
|
gpus.resize(gpus.size() + device_count);
|
|
gpu_names.resize(gpu_names.size() + device_count);
|
|
|
|
initialized = true;
|
|
|
|
//? Take initial sample for delta computation
|
|
prev_sample = IOReportCreateSamples(ior_sub, ior_chan, nullptr);
|
|
prev_sample_time = get_mach_time_ms();
|
|
|
|
//? Run init collect to populate names and supported functions
|
|
collect<1>(gpus.data());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool shutdown() {
|
|
if (not initialized) return false;
|
|
if (prev_sample) { CFRelease(prev_sample); prev_sample = nullptr; }
|
|
if (ior_chan) { CFRelease(ior_chan); ior_chan = nullptr; }
|
|
if (ior_sub) { CFRelease((CFTypeRef)ior_sub); ior_sub = nullptr; }
|
|
initialized = false;
|
|
return true;
|
|
}
|
|
|
|
//? Read GPU temperature via IOHIDEventSystem thermal sensors
|
|
static long long get_gpu_temp_iohid() {
|
|
#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504
|
|
constexpr int kHIDPage_AppleVendor = 0xff00;
|
|
constexpr int kHIDUsage_TemperatureSensor = 5;
|
|
constexpr int64_t kIOHIDEventTypeTemperature = 15;
|
|
|
|
CFStringRef keys[2] = { CFSTR("PrimaryUsagePage"), CFSTR("PrimaryUsage") };
|
|
int page = kHIDPage_AppleVendor, usage = kHIDUsage_TemperatureSensor;
|
|
CFRef<CFNumberRef> num0(CFNumberCreate(nullptr, kCFNumberSInt32Type, &page));
|
|
CFRef<CFNumberRef> num1(CFNumberCreate(nullptr, kCFNumberSInt32Type, &usage));
|
|
const void* values[] = { num0.get(), num1.get() };
|
|
CFRef<CFDictionaryRef> match(CFDictionaryCreate(nullptr,
|
|
(const void**)keys, values, 2,
|
|
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
|
|
|
|
CFRef<IOHIDEventSystemClientRef> system(IOHIDEventSystemClientCreate(kCFAllocatorDefault));
|
|
if (not system.get()) return -1;
|
|
IOHIDEventSystemClientSetMatching(system, match);
|
|
CFRef<CFArrayRef> services(IOHIDEventSystemClientCopyServices(system));
|
|
|
|
if (not services.get()) return -1;
|
|
|
|
double gpu_temp_sum = 0;
|
|
int gpu_temp_count = 0;
|
|
long count = CFArrayGetCount(services);
|
|
for (long i = 0; i < count; i++) {
|
|
auto sc = (IOHIDServiceClientRef)CFArrayGetValueAtIndex(services, i);
|
|
if (not sc) continue;
|
|
CFRef<CFStringRef> name(IOHIDServiceClientCopyProperty(sc, CFSTR("Product")));
|
|
if (not name.get()) continue;
|
|
char buf[200];
|
|
CFStringGetCString(name, buf, 200, kCFStringEncodingASCII);
|
|
string n(buf);
|
|
//? "GPU MTR Temp Sensor" is the standard Apple Silicon GPU temp sensor name
|
|
if (n.find("GPU") != string::npos) {
|
|
CFRef<IOHIDEventRef> event(IOHIDServiceClientCopyEvent(sc, kIOHIDEventTypeTemperature, 0, 0));
|
|
if (event.get()) {
|
|
double temp = IOHIDEventGetFloatValue(event, kIOHIDEventTypeTemperature << 16);
|
|
if (temp > 0 and temp < 150) {
|
|
gpu_temp_sum += temp;
|
|
gpu_temp_count++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (gpu_temp_count > 0)
|
|
return static_cast<long long>(round(gpu_temp_sum / gpu_temp_count));
|
|
#endif
|
|
return -1;
|
|
}
|
|
|
|
template <bool is_init>
|
|
bool collect(gpu_info* gpus_slice) {
|
|
if (not initialized) return false;
|
|
|
|
if constexpr (is_init) {
|
|
//? Device name
|
|
string chip = get_chip_name();
|
|
gpu_names[0] = chip + " GPU";
|
|
|
|
//? Power max (typical Apple Silicon GPU TDP ~15-20W)
|
|
gpus_slice[0].pwr_max_usage = 20000; // 20W in mW
|
|
gpu_pwr_total_max += gpus_slice[0].pwr_max_usage;
|
|
|
|
//? Temperature max
|
|
gpus_slice[0].temp_max = 110;
|
|
|
|
//? Memory total (unified memory architecture — GPU shares system RAM)
|
|
int64_t memsize = 0;
|
|
size_t size = sizeof(memsize);
|
|
if (sysctlbyname("hw.memsize", &memsize, &size, nullptr, 0) == 0)
|
|
gpus_slice[0].mem_total = memsize;
|
|
|
|
//? Supported functions
|
|
gpus_slice[0].supported_functions = {
|
|
.gpu_utilization = true,
|
|
.mem_utilization = true,
|
|
.gpu_clock = not gpu_freqs.empty(),
|
|
.mem_clock = false,
|
|
.pwr_usage = true,
|
|
.pwr_state = false,
|
|
.temp_info = true,
|
|
.mem_total = true,
|
|
.mem_used = true,
|
|
.pcie_txrx = false,
|
|
.encoder_utilization = false,
|
|
.decoder_utilization = false
|
|
};
|
|
}
|
|
|
|
//? Take new IOReport sample and compute delta
|
|
CFDictionaryRef cur_sample = IOReportCreateSamples(ior_sub, ior_chan, nullptr);
|
|
if (not cur_sample) return false;
|
|
|
|
uint64_t cur_time = get_mach_time_ms();
|
|
uint64_t dt = cur_time - prev_sample_time;
|
|
if (dt == 0) dt = 1;
|
|
|
|
CFRef<CFDictionaryRef> delta;
|
|
if (prev_sample) {
|
|
delta.reset(IOReportCreateSamplesDelta(prev_sample, cur_sample, nullptr));
|
|
CFRelease(prev_sample);
|
|
}
|
|
prev_sample = cur_sample;
|
|
prev_sample_time = cur_time;
|
|
|
|
if (not delta.get()) return false;
|
|
|
|
//? Parse delta samples
|
|
CFArrayRef channels = (CFArrayRef)CFDictionaryGetValue(delta, CFSTR("IOReportChannels"));
|
|
if (not channels) return false;
|
|
|
|
long long gpu_utilization = 0;
|
|
bool got_gpu_util = false;
|
|
double gpu_power_watts = 0;
|
|
bool got_gpu_power = false;
|
|
|
|
long chan_count = CFArrayGetCount(channels);
|
|
for (long i = 0; i < chan_count; i++) {
|
|
CFDictionaryRef item = (CFDictionaryRef)CFArrayGetValueAtIndex(channels, i);
|
|
if (not item) continue;
|
|
|
|
string group = cfstring_to_string(IOReportChannelGetGroup(item));
|
|
string subgroup = cfstring_to_string(IOReportChannelGetSubGroup(item));
|
|
string channel = cfstring_to_string(IOReportChannelGetChannelName(item));
|
|
|
|
//? GPU utilization from residency states
|
|
if (group == "GPU Stats" and subgroup == "GPU Performance States" and channel == "GPUPH") {
|
|
int32_t state_count = IOReportStateGetCount(item);
|
|
if (state_count <= 0) continue;
|
|
|
|
int64_t total_residency = 0;
|
|
int64_t active_residency = 0;
|
|
double weighted_freq = 0;
|
|
|
|
//? Find offset past IDLE/OFF/DOWN states
|
|
int offset = 0;
|
|
for (int32_t s = 0; s < state_count; s++) {
|
|
string name = cfstring_to_string(IOReportStateGetNameForIndex(item, s));
|
|
if (name == "IDLE" or name == "OFF" or name == "DOWN")
|
|
offset = s + 1;
|
|
total_residency += IOReportStateGetResidency(item, s);
|
|
}
|
|
|
|
int freq_count = static_cast<int>(gpu_freqs.size());
|
|
for (int32_t s = offset; s < state_count; s++) {
|
|
int64_t res = IOReportStateGetResidency(item, s);
|
|
active_residency += res;
|
|
int freq_idx = s - offset;
|
|
if (freq_idx < freq_count and active_residency > 0)
|
|
weighted_freq += static_cast<double>(res) * gpu_freqs[freq_idx];
|
|
}
|
|
|
|
if (total_residency > 0) {
|
|
double usage_ratio = static_cast<double>(active_residency) / static_cast<double>(total_residency);
|
|
gpu_utilization = clamp(static_cast<long long>(round(usage_ratio * 100.0)), 0ll, 100ll);
|
|
got_gpu_util = true;
|
|
|
|
//? Calculate average frequency
|
|
if (active_residency > 0 and not gpu_freqs.empty()) {
|
|
double avg_freq = weighted_freq / static_cast<double>(active_residency);
|
|
gpus_slice[0].gpu_clock_speed = static_cast<unsigned int>(round(avg_freq));
|
|
}
|
|
}
|
|
}
|
|
|
|
//? GPU power from Energy Model
|
|
if (group == "Energy Model" and channel == "GPU Energy") {
|
|
string unit = cfstring_to_string(IOReportChannelGetUnitLabel(item));
|
|
int64_t val = IOReportSimpleGetIntegerValue(item, 0);
|
|
double energy = static_cast<double>(val);
|
|
double divisor = static_cast<double>(dt) / 1000.0; // dt is in ms
|
|
|
|
if (unit.find("nJ") != string::npos) energy /= 1e9;
|
|
else if (unit.find("uJ") != string::npos or unit.find("\xc2\xb5J") != string::npos) energy /= 1e6;
|
|
else if (unit.find("mJ") != string::npos) energy /= 1e3;
|
|
//? energy is now in Joules
|
|
|
|
if (divisor > 0) {
|
|
gpu_power_watts = energy / divisor;
|
|
got_gpu_power = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
//? Store GPU utilization
|
|
if (got_gpu_util) {
|
|
gpus_slice[0].gpu_percent.at("gpu-totals").push_back(gpu_utilization);
|
|
gpus_slice[0].mem_utilization_percent.push_back(gpu_utilization);
|
|
}
|
|
|
|
//? Store power usage (convert W to mW)
|
|
if (got_gpu_power) {
|
|
gpus_slice[0].pwr_usage = static_cast<long long>(round(gpu_power_watts * 1000.0));
|
|
if (gpus_slice[0].pwr_usage > gpus_slice[0].pwr_max_usage)
|
|
gpus_slice[0].pwr_max_usage = gpus_slice[0].pwr_usage;
|
|
gpus_slice[0].gpu_percent.at("gpu-pwr-totals").push_back(
|
|
clamp(static_cast<long long>(round(static_cast<double>(gpus_slice[0].pwr_usage) * 100.0 / static_cast<double>(gpus_slice[0].pwr_max_usage))), 0ll, 100ll));
|
|
}
|
|
|
|
//? GPU temperature
|
|
if (gpus_slice[0].supported_functions.temp_info and Config::getB("check_temp")) {
|
|
long long temp = get_gpu_temp_iohid();
|
|
if (temp > 0)
|
|
gpus_slice[0].temp.push_back(temp);
|
|
}
|
|
|
|
//? Memory usage (unified memory — report system memory usage)
|
|
if (gpus_slice[0].supported_functions.mem_total) {
|
|
vm_size_t page_size;
|
|
mach_port_t mach_port = mach_host_self();
|
|
vm_statistics64_data_t vm_stats;
|
|
mach_msg_type_number_t count = sizeof(vm_stats) / sizeof(natural_t);
|
|
host_page_size(mach_port, &page_size);
|
|
|
|
if (host_statistics64(mach_port, HOST_VM_INFO64, (host_info64_t)&vm_stats, &count) == KERN_SUCCESS) {
|
|
long long used = (static_cast<int64_t>(vm_stats.active_count)
|
|
+ static_cast<int64_t>(vm_stats.inactive_count)
|
|
+ static_cast<int64_t>(vm_stats.wire_count)
|
|
+ static_cast<int64_t>(vm_stats.speculative_count)
|
|
+ static_cast<int64_t>(vm_stats.compressor_page_count)
|
|
- static_cast<int64_t>(vm_stats.purgeable_count)
|
|
- static_cast<int64_t>(vm_stats.external_page_count)) * static_cast<int64_t>(page_size);
|
|
if (used < 0) used = 0;
|
|
gpus_slice[0].mem_used = used;
|
|
if (gpus_slice[0].mem_total > 0) {
|
|
auto used_pct = static_cast<long long>(round(static_cast<double>(used) * 100.0 / static_cast<double>(gpus_slice[0].mem_total)));
|
|
gpus_slice[0].gpu_percent.at("gpu-vram-totals").push_back(clamp(used_pct, 0ll, 100ll));
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//? Explicit template instantiations
|
|
template bool collect<true>(gpu_info*);
|
|
template bool collect<false>(gpu_info*);
|
|
} // namespace AppleSilicon
|
|
|
|
//? Collect data from Apple Silicon GPU
|
|
auto collect(bool no_update) -> vector<gpu_info>& {
|
|
if (Runner::stopping or (no_update and not gpus.empty())) return gpus;
|
|
|
|
AppleSilicon::collect<0>(gpus.data());
|
|
|
|
//* Calculate averages
|
|
long long avg = 0;
|
|
long long mem_usage_total = 0;
|
|
long long mem_total = 0;
|
|
long long pwr_total = 0;
|
|
for (auto& gpu : gpus) {
|
|
if (gpu.supported_functions.gpu_utilization and not gpu.gpu_percent.at("gpu-totals").empty())
|
|
avg += gpu.gpu_percent.at("gpu-totals").back();
|
|
if (gpu.supported_functions.mem_used)
|
|
mem_usage_total += gpu.mem_used;
|
|
if (gpu.supported_functions.mem_total)
|
|
mem_total += gpu.mem_total;
|
|
if (gpu.supported_functions.pwr_usage)
|
|
pwr_total += gpu.pwr_usage;
|
|
|
|
//* Trim vectors if there are more values than needed for graphs
|
|
if (width != 0) {
|
|
while (cmp_greater(gpu.gpu_percent.at("gpu-totals").size(), width * 2)) gpu.gpu_percent.at("gpu-totals").pop_front();
|
|
while (cmp_greater(gpu.mem_utilization_percent.size(), width)) gpu.mem_utilization_percent.pop_front();
|
|
while (cmp_greater(gpu.gpu_percent.at("gpu-pwr-totals").size(), width)) gpu.gpu_percent.at("gpu-pwr-totals").pop_front();
|
|
while (cmp_greater(gpu.temp.size(), 18)) gpu.temp.pop_front();
|
|
while (cmp_greater(gpu.gpu_percent.at("gpu-vram-totals").size(), width/2)) gpu.gpu_percent.at("gpu-vram-totals").pop_front();
|
|
}
|
|
}
|
|
|
|
if (not gpus.empty()) {
|
|
shared_gpu_percent.at("gpu-average").push_back(avg / static_cast<long long>(gpus.size()));
|
|
if (mem_total != 0)
|
|
shared_gpu_percent.at("gpu-vram-total").push_back(mem_usage_total * 100 / mem_total);
|
|
if (gpu_pwr_total_max != 0)
|
|
shared_gpu_percent.at("gpu-pwr-total").push_back(pwr_total * 100 / gpu_pwr_total_max);
|
|
}
|
|
|
|
if (width != 0) {
|
|
while (cmp_greater(shared_gpu_percent.at("gpu-average").size(), width * 2)) shared_gpu_percent.at("gpu-average").pop_front();
|
|
while (cmp_greater(shared_gpu_percent.at("gpu-vram-total").size(), width)) shared_gpu_percent.at("gpu-vram-total").pop_front();
|
|
while (cmp_greater(shared_gpu_percent.at("gpu-pwr-total").size(), width)) shared_gpu_percent.at("gpu-pwr-total").pop_front();
|
|
}
|
|
|
|
return gpus;
|
|
}
|
|
} // namespace Gpu
|
|
#endif // GPU_SUPPORT
|
|
|
|
class MachProcessorInfo {
|
|
public:
|
|
processor_info_array_t info_array;
|
|
mach_msg_type_number_t info_count;
|
|
MachProcessorInfo() {}
|
|
virtual ~MachProcessorInfo() {vm_deallocate(mach_task_self(), (vm_address_t)info_array, (vm_size_t)sizeof(processor_info_array_t) * info_count);}
|
|
};
|
|
|
|
namespace Shared {
|
|
|
|
fs::path passwd_path;
|
|
uint64_t totalMem;
|
|
long pageSize, coreCount, clkTck, physicalCoreCount, arg_max;
|
|
double machTck;
|
|
int totalMem_len;
|
|
|
|
void init() {
|
|
//? Shared global variables init
|
|
|
|
coreCount = sysconf(_SC_NPROCESSORS_ONLN); // this returns all logical cores (threads)
|
|
if (coreCount < 1) {
|
|
coreCount = 1;
|
|
Logger::warning("Could not determine number of cores, defaulting to 1.");
|
|
}
|
|
|
|
size_t physicalCoreCountSize = sizeof(physicalCoreCount);
|
|
if (sysctlbyname("hw.physicalcpu", &physicalCoreCount, &physicalCoreCountSize, nullptr, 0) < 0) {
|
|
Logger::error("Could not get physical core count");
|
|
}
|
|
|
|
pageSize = sysconf(_SC_PAGE_SIZE);
|
|
if (pageSize <= 0) {
|
|
pageSize = 4096;
|
|
Logger::warning("Could not get system page size. Defaulting to 4096, processes memory usage might be incorrect.");
|
|
}
|
|
|
|
mach_timebase_info_data_t convf;
|
|
if (mach_timebase_info(&convf) == KERN_SUCCESS) {
|
|
machTck = convf.numer / convf.denom;
|
|
} else {
|
|
Logger::warning("Could not get mach clock tick conversion factor. Defaulting to 100, processes cpu usage might be incorrect.");
|
|
machTck = 100;
|
|
}
|
|
|
|
clkTck = sysconf(_SC_CLK_TCK);
|
|
if (clkTck <= 0) {
|
|
clkTck = 100;
|
|
Logger::warning("Could not get system clock ticks per second. Defaulting to 100, processes cpu usage might be incorrect.");
|
|
}
|
|
|
|
int64_t memsize = 0;
|
|
size_t size = sizeof(memsize);
|
|
if (sysctlbyname("hw.memsize", &memsize, &size, nullptr, 0) < 0) {
|
|
Logger::warning("Could not get memory size");
|
|
}
|
|
totalMem = memsize;
|
|
|
|
//* Get maximum length of process arguments
|
|
arg_max = sysconf(_SC_ARG_MAX);
|
|
|
|
//? Init for namespace Cpu
|
|
Cpu::current_cpu.core_percent.insert(Cpu::current_cpu.core_percent.begin(), Shared::coreCount, {});
|
|
Cpu::current_cpu.temp.insert(Cpu::current_cpu.temp.begin(), Shared::coreCount + 1, {});
|
|
Cpu::core_old_totals.insert(Cpu::core_old_totals.begin(), Shared::coreCount, 0);
|
|
Cpu::core_old_idles.insert(Cpu::core_old_idles.begin(), Shared::coreCount, 0);
|
|
Cpu::collect();
|
|
for (auto &[field, vec] : Cpu::current_cpu.cpu_percent) {
|
|
if (not vec.empty() and not v_contains(Cpu::available_fields, field)) Cpu::available_fields.push_back(field);
|
|
}
|
|
Cpu::cpuName = Cpu::get_cpuName();
|
|
Cpu::got_sensors = Cpu::get_sensors();
|
|
Cpu::core_mapping = Cpu::get_core_mapping();
|
|
|
|
//? Init for namespace Gpu
|
|
#ifdef GPU_SUPPORT
|
|
auto shown_gpus = Config::getS("shown_gpus");
|
|
if (shown_gpus.contains("apple")) {
|
|
Gpu::AppleSilicon::init();
|
|
}
|
|
|
|
if (not Gpu::gpu_names.empty()) {
|
|
for (auto const& [key, _] : Gpu::gpus[0].gpu_percent)
|
|
Cpu::available_fields.push_back(key);
|
|
for (auto const& [key, _] : Gpu::shared_gpu_percent)
|
|
Cpu::available_fields.push_back(key);
|
|
|
|
using namespace Gpu;
|
|
count = gpus.size();
|
|
gpu_b_height_offsets.resize(gpus.size());
|
|
for (size_t i = 0; i < gpu_b_height_offsets.size(); ++i)
|
|
gpu_b_height_offsets[i] = gpus[i].supported_functions.gpu_utilization
|
|
+ gpus[i].supported_functions.pwr_usage
|
|
+ (gpus[i].supported_functions.encoder_utilization or gpus[i].supported_functions.decoder_utilization)
|
|
+ (gpus[i].supported_functions.mem_total or gpus[i].supported_functions.mem_used)
|
|
* (1 + 2*(gpus[i].supported_functions.mem_total and gpus[i].supported_functions.mem_used) + 2*gpus[i].supported_functions.mem_utilization);
|
|
}
|
|
#endif
|
|
|
|
//? Init for namespace Mem
|
|
Mem::old_uptime = system_uptime();
|
|
Mem::collect();
|
|
}
|
|
|
|
} // namespace Shared
|
|
|
|
namespace Cpu {
|
|
string cpuName;
|
|
string cpuHz;
|
|
bool has_battery = true;
|
|
bool macM1 = false;
|
|
tuple<int, float, long, string> current_bat;
|
|
|
|
const array<string, 10> time_names = {"user", "nice", "system", "idle"};
|
|
|
|
std::unordered_map<string, long long> cpu_old = {
|
|
{"totals", 0},
|
|
{"idles", 0},
|
|
{"user", 0},
|
|
{"nice", 0},
|
|
{"system", 0},
|
|
{"idle", 0}
|
|
};
|
|
|
|
string get_cpuName() {
|
|
string name;
|
|
char buffer[1024];
|
|
size_t size = sizeof(buffer);
|
|
if (sysctlbyname("machdep.cpu.brand_string", &buffer, &size, nullptr, 0) < 0) {
|
|
Logger::error("Failed to get CPU name");
|
|
return name;
|
|
}
|
|
return trim_name(string(buffer));
|
|
}
|
|
|
|
bool get_sensors() {
|
|
Logger::debug("get_sensors(): show_coretemp={} check_temp={}", Config::getB("show_coretemp"), Config::getB("check_temp"));
|
|
got_sensors = false;
|
|
if (Config::getB("show_coretemp") and Config::getB("check_temp")) {
|
|
#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504
|
|
ThermalSensors sensors;
|
|
if (sensors.getSensors() > 0) {
|
|
Logger::debug("M1 sensors found");
|
|
got_sensors = true;
|
|
cpu_temp_only = true;
|
|
macM1 = true;
|
|
} else {
|
|
#endif
|
|
// try SMC (intel)
|
|
Logger::debug("checking intel");
|
|
try {
|
|
SMCConnection smcCon;
|
|
Logger::debug("SMC connection established");
|
|
long long t = smcCon.getTemp(-1); // check if we have package T
|
|
if (t > -1) {
|
|
Logger::debug("intel sensors found");
|
|
got_sensors = true;
|
|
t = smcCon.getTemp(0);
|
|
if (t == -1) {
|
|
// for some macs the core offset is 1 - check if we get a sane value with 1
|
|
if (smcCon.getTemp(1) > -1) {
|
|
Logger::debug("intel sensors with offset 1");
|
|
core_offset = 1;
|
|
}
|
|
}
|
|
} else {
|
|
Logger::debug("no intel sensors found");
|
|
got_sensors = false;
|
|
}
|
|
} catch (std::runtime_error &e) {
|
|
Logger::debug("SMC not available: {}", e.what());
|
|
// ignore, we don't have temp (common in VMs)
|
|
got_sensors = false;
|
|
}
|
|
#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504
|
|
}
|
|
#endif
|
|
}
|
|
return got_sensors;
|
|
}
|
|
|
|
void update_sensors() {
|
|
current_cpu.temp_max = 95; // we have no idea how to get the critical temp
|
|
try {
|
|
if (macM1) {
|
|
#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504
|
|
ThermalSensors sensors;
|
|
current_cpu.temp.at(0).push_back(sensors.getSensors());
|
|
if (current_cpu.temp.at(0).size() > 20)
|
|
current_cpu.temp.at(0).pop_front();
|
|
#endif
|
|
} else {
|
|
SMCConnection smcCon;
|
|
int threadsPerCore = Shared::coreCount / Shared::physicalCoreCount;
|
|
long long packageT = smcCon.getTemp(-1); // -1 returns package T
|
|
current_cpu.temp.at(0).push_back(packageT);
|
|
|
|
for (int core = 0; core < Shared::coreCount; core++) {
|
|
long long temp = smcCon.getTemp((core / threadsPerCore) + core_offset); // same temp for all threads of same physical core
|
|
if (cmp_less(core + 1, current_cpu.temp.size())) {
|
|
current_cpu.temp.at(core + 1).push_back(temp);
|
|
if (current_cpu.temp.at(core + 1).size() > 20)
|
|
current_cpu.temp.at(core + 1).pop_front();
|
|
}
|
|
}
|
|
}
|
|
} catch (std::runtime_error &e) {
|
|
got_sensors = false;
|
|
Logger::error("failed getting CPU temp");
|
|
}
|
|
}
|
|
|
|
string get_cpuHz() {
|
|
unsigned int freq = 1;
|
|
size_t size = sizeof(freq);
|
|
|
|
int mib[] = {CTL_HW, HW_CPU_FREQ};
|
|
|
|
if (sysctl(mib, 2, &freq, &size, nullptr, 0) < 0) {
|
|
// this fails on Apple Silicon macs. Apparently you're not allowed to know
|
|
return "";
|
|
}
|
|
return std::to_string(freq / 1000.0 / 1000.0 / 1000.0).substr(0, 3);
|
|
}
|
|
|
|
auto get_core_mapping() -> std::unordered_map<int, int> {
|
|
std::unordered_map<int, int> core_map;
|
|
if (cpu_temp_only) return core_map;
|
|
|
|
natural_t cpu_count;
|
|
natural_t i;
|
|
MachProcessorInfo info {};
|
|
kern_return_t error;
|
|
|
|
error = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info.info_array, &info.info_count);
|
|
if (error != KERN_SUCCESS) {
|
|
Logger::error("Failed getting CPU info");
|
|
return core_map;
|
|
}
|
|
for (i = 0; i < cpu_count; i++) {
|
|
core_map[i] = i;
|
|
}
|
|
|
|
//? If core mapping from cpuinfo was incomplete try to guess remainder, if missing completely, map 0-0 1-1 2-2 etc.
|
|
if (cmp_less(core_map.size(), Shared::coreCount)) {
|
|
if (Shared::coreCount % 2 == 0 and (long) core_map.size() == Shared::coreCount / 2) {
|
|
for (int i = 0, n = 0; i < Shared::coreCount / 2; i++) {
|
|
if (std::cmp_greater_equal(n, core_sensors.size())) n = 0;
|
|
core_map[Shared::coreCount / 2 + i] = n++;
|
|
}
|
|
} else {
|
|
core_map.clear();
|
|
for (int i = 0, n = 0; i < Shared::coreCount; i++) {
|
|
if (std::cmp_greater_equal(n, core_sensors.size())) n = 0;
|
|
core_map[i] = n++;
|
|
}
|
|
}
|
|
}
|
|
|
|
//? Apply user set custom mapping if any
|
|
const auto &custom_map = Config::getS("cpu_core_map");
|
|
if (not custom_map.empty()) {
|
|
try {
|
|
for (const auto &split : ssplit(custom_map)) {
|
|
const auto vals = ssplit(split, ':');
|
|
if (vals.size() != 2) continue;
|
|
int change_id = std::stoi(vals.at(0));
|
|
int new_id = std::stoi(vals.at(1));
|
|
if (not core_map.contains(change_id) or cmp_greater(new_id, core_sensors.size())) continue;
|
|
core_map.at(change_id) = new_id;
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
return core_map;
|
|
}
|
|
|
|
class IOPSInfo_Wrap {
|
|
CFTypeRef data;
|
|
public:
|
|
IOPSInfo_Wrap() { data = IOPSCopyPowerSourcesInfo(); }
|
|
CFTypeRef& operator()() { return data; }
|
|
~IOPSInfo_Wrap() { CFRelease(data); }
|
|
};
|
|
|
|
class IOPSList_Wrap {
|
|
CFArrayRef data;
|
|
public:
|
|
IOPSList_Wrap(CFTypeRef cft_ref) { data = IOPSCopyPowerSourcesList(cft_ref); }
|
|
CFArrayRef& operator()() { return data; }
|
|
~IOPSList_Wrap() { CFRelease(data); }
|
|
};
|
|
|
|
auto get_battery() -> tuple<int, float, long, string> {
|
|
if (not has_battery) return {0, 0, 0, ""};
|
|
|
|
uint32_t percent = -1;
|
|
long seconds = -1;
|
|
string status = "discharging";
|
|
IOPSInfo_Wrap ps_info{};
|
|
if (ps_info()) {
|
|
IOPSList_Wrap one_ps_descriptor(ps_info());
|
|
if (one_ps_descriptor()) {
|
|
if (CFArrayGetCount(one_ps_descriptor())) {
|
|
CFDictionaryRef one_ps = IOPSGetPowerSourceDescription(ps_info(), CFArrayGetValueAtIndex(one_ps_descriptor(), 0));
|
|
has_battery = true;
|
|
CFNumberRef remaining = (CFNumberRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSTimeToEmptyKey));
|
|
int32_t estimatedMinutesRemaining;
|
|
if (remaining) {
|
|
CFNumberGetValue(remaining, kCFNumberSInt32Type, &estimatedMinutesRemaining);
|
|
seconds = estimatedMinutesRemaining * 60;
|
|
}
|
|
CFNumberRef charge = (CFNumberRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSCurrentCapacityKey));
|
|
if (charge) {
|
|
CFNumberGetValue(charge, kCFNumberSInt32Type, &percent);
|
|
}
|
|
CFBooleanRef charging = (CFBooleanRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSIsChargingKey));
|
|
if (charging) {
|
|
bool isCharging = CFBooleanGetValue(charging);
|
|
if (isCharging) {
|
|
status = "charging";
|
|
}
|
|
}
|
|
if (percent == 100) {
|
|
status = "full";
|
|
}
|
|
} else {
|
|
has_battery = false;
|
|
}
|
|
} else {
|
|
has_battery = false;
|
|
}
|
|
}
|
|
return {percent, -1, seconds, status};
|
|
}
|
|
|
|
auto collect(bool no_update) -> cpu_info & {
|
|
if (Runner::stopping or (no_update and not current_cpu.cpu_percent.at("total").empty()))
|
|
return current_cpu;
|
|
auto &cpu = current_cpu;
|
|
|
|
if (getloadavg(cpu.load_avg.data(), cpu.load_avg.size()) < 0) {
|
|
Logger::error("failed to get load averages");
|
|
}
|
|
|
|
natural_t cpu_count;
|
|
natural_t i;
|
|
kern_return_t error;
|
|
processor_cpu_load_info_data_t *cpu_load_info = nullptr;
|
|
|
|
MachProcessorInfo info{};
|
|
error = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info.info_array, &info.info_count);
|
|
if (error != KERN_SUCCESS) {
|
|
Logger::error("Failed getting CPU load info");
|
|
}
|
|
cpu_load_info = (processor_cpu_load_info_data_t *)info.info_array;
|
|
long long global_totals = 0;
|
|
long long global_idles = 0;
|
|
vector<long long> times_summed = {0, 0, 0, 0};
|
|
for (i = 0; i < cpu_count; i++) {
|
|
vector<long long> times;
|
|
//? 0=user, 1=nice, 2=system, 3=idle
|
|
for (int x = 0; const unsigned int c_state : {CPU_STATE_USER, CPU_STATE_NICE, CPU_STATE_SYSTEM, CPU_STATE_IDLE}) {
|
|
auto val = cpu_load_info[i].cpu_ticks[c_state];
|
|
times.push_back(val);
|
|
times_summed.at(x++) += val;
|
|
}
|
|
|
|
try {
|
|
//? All values
|
|
const long long totals = std::accumulate(times.begin(), times.end(), 0ll);
|
|
|
|
//? Idle time
|
|
const long long idles = times.at(3);
|
|
|
|
global_totals += totals;
|
|
global_idles += idles;
|
|
|
|
//? Calculate cpu total for each core
|
|
if (i > Shared::coreCount) break;
|
|
const long long calc_totals = max(0ll, totals - core_old_totals.at(i));
|
|
const long long calc_idles = max(0ll, idles - core_old_idles.at(i));
|
|
core_old_totals.at(i) = totals;
|
|
core_old_idles.at(i) = idles;
|
|
|
|
cpu.core_percent.at(i).push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll));
|
|
|
|
//? Reduce size if there are more values than needed for graph
|
|
if (cpu.core_percent.at(i).size() > 40) cpu.core_percent.at(i).pop_front();
|
|
|
|
} catch (const std::exception &e) {
|
|
Logger::error("Cpu::collect() : {}", e.what());
|
|
throw std::runtime_error(fmt::format("collect() : {}", e.what()));
|
|
}
|
|
}
|
|
|
|
const long long calc_totals = max(1ll, global_totals - cpu_old.at("totals"));
|
|
const long long calc_idles = max(1ll, global_idles - cpu_old.at("idles"));
|
|
|
|
//? Populate cpu.cpu_percent with all fields from syscall
|
|
for (int ii = 0; const auto &val : times_summed) {
|
|
cpu.cpu_percent.at(time_names.at(ii)).push_back(clamp((long long)round((double)(val - cpu_old.at(time_names.at(ii))) * 100 / calc_totals), 0ll, 100ll));
|
|
cpu_old.at(time_names.at(ii)) = val;
|
|
|
|
//? Reduce size if there are more values than needed for graph
|
|
while (cmp_greater(cpu.cpu_percent.at(time_names.at(ii)).size(), width * 2)) cpu.cpu_percent.at(time_names.at(ii)).pop_front();
|
|
|
|
ii++;
|
|
}
|
|
|
|
cpu_old.at("totals") = global_totals;
|
|
cpu_old.at("idles") = global_idles;
|
|
|
|
//? Total usage of cpu
|
|
cpu.cpu_percent.at("total").push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll));
|
|
|
|
//? Reduce size if there are more values than needed for graph
|
|
while (cmp_greater(cpu.cpu_percent.at("total").size(), width * 2)) cpu.cpu_percent.at("total").pop_front();
|
|
|
|
if (Config::getB("show_cpu_freq")) {
|
|
auto hz = get_cpuHz();
|
|
if (hz != "") {
|
|
cpuHz = hz;
|
|
}
|
|
}
|
|
|
|
if (Config::getB("check_temp") and got_sensors)
|
|
update_sensors();
|
|
|
|
if (Config::getB("show_battery") and has_battery)
|
|
current_bat = get_battery();
|
|
|
|
return cpu;
|
|
}
|
|
} // namespace Cpu
|
|
|
|
namespace Mem {
|
|
bool has_swap = false;
|
|
vector<string> fstab;
|
|
fs::file_time_type fstab_time;
|
|
int disk_ios = 0;
|
|
vector<string> last_found;
|
|
static std::mutex iokit_mutex; // Protect concurrent IOKit calls
|
|
static std::mutex interface_mutex; // Protect concurrent interface access during USB device changes
|
|
|
|
mem_info current_mem{};
|
|
|
|
uint64_t get_totalMem() {
|
|
return Shared::totalMem;
|
|
}
|
|
|
|
int64_t getCFNumber(CFDictionaryRef dict, const void *key) {
|
|
CFNumberRef ref = (CFNumberRef)CFDictionaryGetValue(dict, key);
|
|
if (ref) {
|
|
int64_t value;
|
|
CFNumberGetValue(ref, kCFNumberSInt64Type, &value);
|
|
return value;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
string getCFString(io_registry_entry_t volumeRef, CFStringRef key) {
|
|
CFStringRef bsdNameRef = (CFStringRef)IORegistryEntryCreateCFProperty(volumeRef, key, kCFAllocatorDefault, 0);
|
|
if (bsdNameRef) {
|
|
char buf[200];
|
|
CFStringGetCString(bsdNameRef, buf, 200, kCFStringEncodingASCII);
|
|
CFRelease(bsdNameRef);
|
|
return string(buf);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
bool isWhole(io_registry_entry_t volumeRef) {
|
|
CFBooleanRef isWhole = (CFBooleanRef)IORegistryEntryCreateCFProperty(volumeRef, CFSTR("Whole"), kCFAllocatorDefault, 0);
|
|
Boolean val = CFBooleanGetValue(isWhole);
|
|
CFRelease(isWhole);
|
|
return bool(val);
|
|
}
|
|
|
|
class IOObject {
|
|
public:
|
|
IOObject(string name, io_object_t& obj) : name(name), object(obj) {}
|
|
virtual ~IOObject() { IOObjectRelease(object); }
|
|
private:
|
|
string name;
|
|
io_object_t &object;
|
|
};
|
|
|
|
void collect_disk(std::unordered_map<string, disk_info> &disks, std::unordered_map<string, string> &mapping) {
|
|
// Lock mutex to prevent concurrent IOKit access
|
|
std::lock_guard<std::mutex> lock(iokit_mutex);
|
|
|
|
io_registry_entry_t drive;
|
|
io_iterator_t drive_list;
|
|
|
|
/* Get the list of all drive objects. */
|
|
if (IOServiceGetMatchingServices(kIOMainPortDefault,
|
|
IOServiceMatching("IOMediaBSDClient"), &drive_list)) {
|
|
Logger::error("Error in IOServiceGetMatchingServices()");
|
|
return;
|
|
}
|
|
auto d = IOObject("drive list", drive_list); // dummy var so it gets destroyed
|
|
while ((drive = IOIteratorNext(drive_list)) != 0) {
|
|
auto dr = IOObject("drive", drive);
|
|
io_registry_entry_t volumeRef;
|
|
IORegistryEntryGetParentEntry(drive, kIOServicePlane, &volumeRef);
|
|
if (volumeRef) {
|
|
if (!isWhole(volumeRef)) {
|
|
string bsdName = getCFString(volumeRef, CFSTR("BSD Name"));
|
|
string device = getCFString(volumeRef, CFSTR("VolGroupMntFromName"));
|
|
if (!mapping.contains(device)) {
|
|
device = "/dev/" + bsdName; // try again with BSD name - not all volumes seem to have VolGroupMntFromName property
|
|
}
|
|
if (device != "") {
|
|
if (mapping.contains(device)) {
|
|
string mountpoint = mapping.at(device);
|
|
if (disks.contains(mountpoint)) {
|
|
auto& disk = disks.at(mountpoint);
|
|
CFDictionaryRef properties;
|
|
IORegistryEntryCreateCFProperties(volumeRef, (CFMutableDictionaryRef *)&properties, kCFAllocatorDefault, 0);
|
|
if (properties) {
|
|
CFDictionaryRef statistics = (CFDictionaryRef)CFDictionaryGetValue(properties, CFSTR("Statistics"));
|
|
if (statistics) {
|
|
disk_ios++;
|
|
int64_t readBytes = getCFNumber(statistics, CFSTR("Bytes read from block device"));
|
|
if (disk.io_read.empty())
|
|
disk.io_read.push_back(0);
|
|
else
|
|
disk.io_read.push_back(max((int64_t)0, (readBytes - disk.old_io.at(0))));
|
|
disk.old_io.at(0) = readBytes;
|
|
while (cmp_greater(disk.io_read.size(), width * 2)) disk.io_read.pop_front();
|
|
|
|
int64_t writeBytes = getCFNumber(statistics, CFSTR("Bytes written to block device"));
|
|
if (disk.io_write.empty())
|
|
disk.io_write.push_back(0);
|
|
else
|
|
disk.io_write.push_back(max((int64_t)0, (writeBytes - disk.old_io.at(1))));
|
|
disk.old_io.at(1) = writeBytes;
|
|
while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front();
|
|
|
|
// IOKit does not give us IO times, (use IO read + IO write with 1 MiB being 100% to get some activity indication)
|
|
if (disk.io_activity.empty())
|
|
disk.io_activity.push_back(0);
|
|
else
|
|
disk.io_activity.push_back(clamp((long)round((double)(disk.io_write.back() + disk.io_read.back()) / (1 << 20)), 0l, 100l));
|
|
while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front();
|
|
}
|
|
}
|
|
CFRelease(properties);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
auto collect(bool no_update) -> mem_info & {
|
|
if (Runner::stopping or (no_update and not current_mem.percent.at("used").empty()))
|
|
return current_mem;
|
|
|
|
auto show_swap = Config::getB("show_swap");
|
|
auto show_disks = Config::getB("show_disks");
|
|
auto swap_disk = Config::getB("swap_disk");
|
|
auto &mem = current_mem;
|
|
static bool snapped = (getenv("BTOP_SNAPPED") != nullptr);
|
|
|
|
vm_statistics64 p;
|
|
mach_msg_type_number_t info_size = HOST_VM_INFO64_COUNT;
|
|
if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t)&p, &info_size) == 0) {
|
|
mem.stats.at("free") = p.free_count * Shared::pageSize;
|
|
mem.stats.at("cached") = p.external_page_count * Shared::pageSize;
|
|
mem.stats.at("used") = (p.active_count + p.wire_count) * Shared::pageSize;
|
|
mem.stats.at("available") = Shared::totalMem - mem.stats.at("used");
|
|
}
|
|
|
|
int mib[2] = {CTL_VM, VM_SWAPUSAGE};
|
|
|
|
struct xsw_usage swap;
|
|
size_t len = sizeof(struct xsw_usage);
|
|
if (sysctl(mib, 2, &swap, &len, nullptr, 0) == 0) {
|
|
mem.stats.at("swap_total") = swap.xsu_total;
|
|
mem.stats.at("swap_free") = swap.xsu_avail;
|
|
mem.stats.at("swap_used") = swap.xsu_used;
|
|
}
|
|
|
|
if (show_swap and mem.stats.at("swap_total") > 0) {
|
|
for (const auto &name : swap_names) {
|
|
mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / mem.stats.at("swap_total")));
|
|
while (cmp_greater(mem.percent.at(name).size(), width * 2))
|
|
mem.percent.at(name).pop_front();
|
|
}
|
|
has_swap = true;
|
|
} else
|
|
has_swap = false;
|
|
//? Calculate percentages
|
|
for (const auto &name : mem_names) {
|
|
mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / Shared::totalMem));
|
|
while (cmp_greater(mem.percent.at(name).size(), width * 2))
|
|
mem.percent.at(name).pop_front();
|
|
}
|
|
|
|
if (show_disks) {
|
|
std::unordered_map<string, string> mapping; // keep mapping from device -> mountpoint, since IOKit doesn't give us the mountpoint
|
|
double uptime = system_uptime();
|
|
auto &disks_filter = Config::getS("disks_filter");
|
|
bool filter_exclude = false;
|
|
// auto only_physical = Config::getB("only_physical");
|
|
auto &disks = mem.disks;
|
|
vector<string> filter;
|
|
if (not disks_filter.empty()) {
|
|
filter = ssplit(disks_filter);
|
|
if (filter.at(0).starts_with("exclude=")) {
|
|
filter_exclude = true;
|
|
filter.at(0) = filter.at(0).substr(8);
|
|
}
|
|
}
|
|
|
|
struct statfs *stfs;
|
|
int count = getmntinfo(&stfs, MNT_WAIT);
|
|
vector<string> found;
|
|
found.reserve(last_found.size());
|
|
for (int i = 0; i < count; i++) {
|
|
std::error_code ec;
|
|
string mountpoint = stfs[i].f_mntonname;
|
|
string dev = stfs[i].f_mntfromname;
|
|
mapping[dev] = mountpoint;
|
|
|
|
if (string(stfs[i].f_fstypename) == "autofs") {
|
|
continue;
|
|
}
|
|
|
|
//? Match filter if not empty
|
|
if (not filter.empty()) {
|
|
bool match = v_contains(filter, mountpoint);
|
|
if ((filter_exclude and match) or (not filter_exclude and not match))
|
|
continue;
|
|
}
|
|
|
|
found.push_back(mountpoint);
|
|
if (not disks.contains(mountpoint)) {
|
|
disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename()};
|
|
|
|
if (disks.at(mountpoint).dev.empty())
|
|
disks.at(mountpoint).dev = dev;
|
|
|
|
if (disks.at(mountpoint).name.empty())
|
|
disks.at(mountpoint).name = (mountpoint == "/" ? "root" : mountpoint);
|
|
}
|
|
|
|
|
|
if (not v_contains(last_found, mountpoint))
|
|
redraw = true;
|
|
|
|
disks.at(mountpoint).free = stfs[i].f_bfree;
|
|
disks.at(mountpoint).total = stfs[i].f_iosize;
|
|
}
|
|
|
|
//? Remove disks no longer mounted or filtered out
|
|
if (swap_disk and has_swap) found.push_back("swap");
|
|
for (auto it = disks.begin(); it != disks.end();) {
|
|
if (not v_contains(found, it->first))
|
|
it = disks.erase(it);
|
|
else
|
|
it++;
|
|
}
|
|
if (found.size() != last_found.size()) redraw = true;
|
|
last_found = std::move(found);
|
|
|
|
//? Get disk/partition stats
|
|
for (auto &[mountpoint, disk] : disks) {
|
|
if (std::error_code ec; not fs::exists(mountpoint, ec))
|
|
continue;
|
|
struct statvfs vfs;
|
|
if (statvfs(mountpoint.c_str(), &vfs) < 0) {
|
|
Logger::warning("Failed to get disk/partition stats with statvfs() for: {}", mountpoint);
|
|
continue;
|
|
}
|
|
disk.total = vfs.f_blocks * vfs.f_frsize;
|
|
disk.free = vfs.f_bfree * vfs.f_frsize;
|
|
disk.used = disk.total - disk.free;
|
|
if (disk.total != 0) {
|
|
disk.used_percent = round((double)disk.used * 100 / disk.total);
|
|
disk.free_percent = 100 - disk.used_percent;
|
|
} else {
|
|
disk.used_percent = 0;
|
|
disk.free_percent = 0;
|
|
}
|
|
}
|
|
|
|
//? Setup disks order in UI and add swap if enabled
|
|
mem.disks_order.clear();
|
|
if (snapped and disks.contains("/mnt"))
|
|
mem.disks_order.push_back("/mnt");
|
|
else if (disks.contains("/"))
|
|
mem.disks_order.push_back("/");
|
|
if (swap_disk and has_swap) {
|
|
mem.disks_order.push_back("swap");
|
|
if (not disks.contains("swap"))
|
|
disks["swap"] = {"", "swap"};
|
|
disks.at("swap").total = mem.stats.at("swap_total");
|
|
disks.at("swap").used = mem.stats.at("swap_used");
|
|
disks.at("swap").free = mem.stats.at("swap_free");
|
|
disks.at("swap").used_percent = mem.percent.at("swap_used").back();
|
|
disks.at("swap").free_percent = mem.percent.at("swap_free").back();
|
|
}
|
|
for (const auto &name : last_found)
|
|
if (not is_in(name, "/", "swap", "/dev"))
|
|
mem.disks_order.push_back(name);
|
|
|
|
disk_ios = 0;
|
|
collect_disk(disks, mapping);
|
|
|
|
old_uptime = uptime;
|
|
}
|
|
return mem;
|
|
}
|
|
|
|
} // namespace Mem
|
|
|
|
namespace Net {
|
|
std::unordered_map<string, net_info> current_net;
|
|
net_info empty_net = {};
|
|
vector<string> interfaces;
|
|
string selected_iface;
|
|
int errors = 0;
|
|
std::unordered_map<string, uint64_t> graph_max = {{"download", {}}, {"upload", {}}};
|
|
std::unordered_map<string, array<int, 2>> max_count = {{"download", {}}, {"upload", {}}};
|
|
bool rescale = true;
|
|
uint64_t timestamp = 0;
|
|
|
|
//* RAII wrapper for getifaddrs
|
|
class getifaddr_wrapper {
|
|
struct ifaddrs *ifaddr;
|
|
|
|
public:
|
|
int status;
|
|
getifaddr_wrapper() { status = getifaddrs(&ifaddr); }
|
|
~getifaddr_wrapper() { freeifaddrs(ifaddr); }
|
|
auto operator()() -> struct ifaddrs * { return ifaddr; }
|
|
};
|
|
|
|
auto collect(bool no_update) -> net_info & {
|
|
// Lock mutex to prevent concurrent interface access during USB device changes
|
|
std::lock_guard<std::mutex> lock(Mem::interface_mutex);
|
|
auto &net = current_net;
|
|
auto &config_iface = Config::getS("net_iface");
|
|
auto net_sync = Config::getB("net_sync");
|
|
auto net_auto = Config::getB("net_auto");
|
|
auto new_timestamp = time_ms();
|
|
|
|
if (not no_update and errors < 3) {
|
|
//? Get interface list using getifaddrs() wrapper
|
|
getifaddr_wrapper if_wrap{};
|
|
if (if_wrap.status != 0) {
|
|
errors++;
|
|
Logger::error("Net::collect() -> getifaddrs() failed with id {}", if_wrap.status);
|
|
redraw = true;
|
|
return empty_net;
|
|
}
|
|
int family = 0;
|
|
static_assert(INET6_ADDRSTRLEN >= INET_ADDRSTRLEN); // 46 >= 16, compile-time assurance.
|
|
enum { IPBUFFER_MAXSIZE = INET6_ADDRSTRLEN }; // manually using the known biggest value, guarded by the above static_assert
|
|
char ip[IPBUFFER_MAXSIZE];
|
|
interfaces.clear();
|
|
string ipv4, ipv6;
|
|
|
|
//? Iteration over all items in getifaddrs() list
|
|
for (auto *ifa = if_wrap(); ifa != nullptr; ifa = ifa->ifa_next) {
|
|
if (ifa->ifa_addr == nullptr) continue;
|
|
family = ifa->ifa_addr->sa_family;
|
|
const auto &iface = ifa->ifa_name;
|
|
//? Update available interfaces vector and get status of interface
|
|
if (not v_contains(interfaces, iface)) {
|
|
interfaces.push_back(iface);
|
|
net[iface].connected = (ifa->ifa_flags & IFF_RUNNING);
|
|
// An interface can have more than one IP of the same family associated with it,
|
|
// but we pick only the first one to show in the NET box.
|
|
// Note: Interfaces without any IPv4 and IPv6 set are still valid and monitorable!
|
|
net[iface].ipv4.clear();
|
|
net[iface].ipv6.clear();
|
|
}
|
|
//? Get IPv4 address
|
|
if (family == AF_INET) {
|
|
if (net[iface].ipv4.empty()) {
|
|
if (nullptr != inet_ntop(family, &(reinterpret_cast<struct sockaddr_in*>(ifa->ifa_addr)->sin_addr), ip, IPBUFFER_MAXSIZE)) {
|
|
net[iface].ipv4 = ip;
|
|
} else {
|
|
int errsv = errno;
|
|
Logger::error("Net::collect() -> Failed to convert IPv4 to string for iface {}, errno: {}", iface, strerror(errsv));
|
|
}
|
|
}
|
|
}
|
|
//? Get IPv6 address
|
|
else if (family == AF_INET6) {
|
|
if (net[iface].ipv6.empty()) {
|
|
if (nullptr != inet_ntop(family, &(reinterpret_cast<struct sockaddr_in6*>(ifa->ifa_addr)->sin6_addr), ip, IPBUFFER_MAXSIZE)) {
|
|
net[iface].ipv6 = ip;
|
|
} else {
|
|
int errsv = errno;
|
|
Logger::error("Net::collect() -> Failed to convert IPv6 to string for iface {}, errno: {}", iface, strerror(errsv));
|
|
}
|
|
}
|
|
} // else, ignoring family==AF_LINK (see man 3 getifaddrs)
|
|
}
|
|
|
|
std::unordered_map<string, std::tuple<uint64_t, uint64_t>> ifstats;
|
|
int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST2, 0};
|
|
size_t len;
|
|
if (sysctl(mib, 6, nullptr, &len, nullptr, 0) < 0) {
|
|
Logger::error("failed getting network interfaces");
|
|
} else {
|
|
std::unique_ptr<char[]> buf(new char[len]);
|
|
if (sysctl(mib, 6, buf.get(), &len, nullptr, 0) < 0) {
|
|
Logger::error("failed getting network interfaces");
|
|
} else {
|
|
char *lim = buf.get() + len;
|
|
char *next = nullptr;
|
|
for (next = buf.get(); next < lim;) {
|
|
struct if_msghdr *ifm = (struct if_msghdr *)next;
|
|
next += ifm->ifm_msglen;
|
|
if (ifm->ifm_type == RTM_IFINFO2) {
|
|
struct if_msghdr2 *if2m = (struct if_msghdr2 *)ifm;
|
|
struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1);
|
|
char iface[32];
|
|
strncpy(iface, sdl->sdl_data, sdl->sdl_nlen);
|
|
iface[sdl->sdl_nlen] = 0;
|
|
ifstats[iface] = std::tuple(if2m->ifm_data.ifi_ibytes, if2m->ifm_data.ifi_obytes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//? Get total received and transmitted bytes + device address if no ip was found
|
|
for (const auto &iface : interfaces) {
|
|
for (const string dir : {"download", "upload"}) {
|
|
auto &saved_stat = net.at(iface).stat.at(dir);
|
|
auto &bandwidth = net.at(iface).bandwidth.at(dir);
|
|
uint64_t val = dir == "download" ? std::get<0>(ifstats[iface]) : std::get<1>(ifstats[iface]);
|
|
|
|
//? Update speed, total and top values
|
|
if (val < saved_stat.last) {
|
|
saved_stat.rollover += saved_stat.last;
|
|
saved_stat.last = 0;
|
|
}
|
|
if (cmp_greater((unsigned long long)saved_stat.rollover + (unsigned long long)val, numeric_limits<uint64_t>::max())) {
|
|
saved_stat.rollover = 0;
|
|
saved_stat.last = 0;
|
|
}
|
|
saved_stat.speed = round((double)(val - saved_stat.last) / ((double)(new_timestamp - timestamp) / 1000));
|
|
if (saved_stat.speed > saved_stat.top) saved_stat.top = saved_stat.speed;
|
|
if (saved_stat.offset > val + saved_stat.rollover) saved_stat.offset = 0;
|
|
saved_stat.total = (val + saved_stat.rollover) - saved_stat.offset;
|
|
saved_stat.last = val;
|
|
|
|
//? Add values to graph
|
|
bandwidth.push_back(saved_stat.speed);
|
|
while (cmp_greater(bandwidth.size(), width * 2)) bandwidth.pop_front();
|
|
|
|
//? Set counters for auto scaling
|
|
if (net_auto and selected_iface == iface) {
|
|
if (saved_stat.speed > graph_max[dir]) {
|
|
++max_count[dir][0];
|
|
if (max_count[dir][1] > 0) --max_count[dir][1];
|
|
} else if (graph_max[dir] > 10 << 10 and saved_stat.speed < graph_max[dir] / 10) {
|
|
++max_count[dir][1];
|
|
if (max_count[dir][0] > 0) --max_count[dir][0];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//? Clean up net map if needed
|
|
if (net.size() > interfaces.size()) {
|
|
for (auto it = net.begin(); it != net.end();) {
|
|
if (not v_contains(interfaces, it->first))
|
|
it = net.erase(it);
|
|
else
|
|
it++;
|
|
}
|
|
}
|
|
|
|
timestamp = new_timestamp;
|
|
}
|
|
//? Return empty net_info struct if no interfaces was found
|
|
if (net.empty())
|
|
return empty_net;
|
|
|
|
//? Find an interface to display if selected isn't set or valid
|
|
if (selected_iface.empty() or not v_contains(interfaces, selected_iface)) {
|
|
max_count["download"][0] = max_count["download"][1] = max_count["upload"][0] = max_count["upload"][1] = 0;
|
|
redraw = true;
|
|
if (net_auto) rescale = true;
|
|
if (not config_iface.empty() and v_contains(interfaces, config_iface))
|
|
selected_iface = config_iface;
|
|
else {
|
|
//? Sort interfaces by total upload + download bytes
|
|
auto sorted_interfaces = interfaces;
|
|
rng::sort(sorted_interfaces, [&](const auto &a, const auto &b) {
|
|
return cmp_greater(net.at(a).stat["download"].total + net.at(a).stat["upload"].total,
|
|
net.at(b).stat["download"].total + net.at(b).stat["upload"].total);
|
|
});
|
|
selected_iface.clear();
|
|
//? Try to set to a connected interface
|
|
for (const auto &iface : sorted_interfaces) {
|
|
if (net.at(iface).connected) selected_iface = iface;
|
|
break;
|
|
}
|
|
//? If no interface is connected set to first available
|
|
if (selected_iface.empty() and not sorted_interfaces.empty())
|
|
selected_iface = sorted_interfaces.at(0);
|
|
else if (sorted_interfaces.empty())
|
|
return empty_net;
|
|
}
|
|
}
|
|
|
|
//? Calculate max scale for graphs if needed
|
|
if (net_auto) {
|
|
bool sync = false;
|
|
for (const auto &dir : {"download", "upload"}) {
|
|
for (const auto &sel : {0, 1}) {
|
|
if (rescale or max_count[dir][sel] >= 5) {
|
|
const long long avg_speed = (net[selected_iface].bandwidth[dir].size() > 5
|
|
? std::accumulate(net.at(selected_iface).bandwidth.at(dir).rbegin(), net.at(selected_iface).bandwidth.at(dir).rbegin() + 5, 0ll) / 5
|
|
: net[selected_iface].stat[dir].speed);
|
|
graph_max[dir] = max(uint64_t(avg_speed * (sel == 0 ? 1.3 : 3.0)), (uint64_t)10 << 10);
|
|
max_count[dir][0] = max_count[dir][1] = 0;
|
|
redraw = true;
|
|
if (net_sync) sync = true;
|
|
break;
|
|
}
|
|
}
|
|
//? Sync download/upload graphs if enabled
|
|
if (sync) {
|
|
const auto other = (string(dir) == "upload" ? "download" : "upload");
|
|
graph_max[other] = graph_max[dir];
|
|
max_count[other][0] = max_count[other][1] = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
rescale = false;
|
|
return net.at(selected_iface);
|
|
}
|
|
} // namespace Net
|
|
|
|
namespace Proc {
|
|
|
|
vector<proc_info> current_procs;
|
|
std::unordered_map<string, string> uid_user;
|
|
string current_sort;
|
|
string current_filter;
|
|
bool current_rev = false;
|
|
bool is_tree_mode;
|
|
|
|
fs::file_time_type passwd_time;
|
|
|
|
uint64_t cputimes;
|
|
int collapse = -1, expand = -1, toggle_children = -1;
|
|
uint64_t old_cputimes = 0;
|
|
atomic<int> numpids = 0;
|
|
int filter_found = 0;
|
|
|
|
detail_container detailed;
|
|
static std::unordered_set<size_t> dead_procs;
|
|
|
|
string get_status(char s) {
|
|
if (s & SRUN) return "Running";
|
|
if (s & SSLEEP) return "Sleeping";
|
|
if (s & SIDL) return "Idle";
|
|
if (s & SSTOP) return "Stopped";
|
|
if (s & SZOMB) return "Zombie";
|
|
return "Unknown";
|
|
}
|
|
|
|
//* Get detailed info for selected process
|
|
void _collect_details(const size_t pid, vector<proc_info> &procs) {
|
|
if (pid != detailed.last_pid) {
|
|
detailed = {};
|
|
detailed.last_pid = pid;
|
|
detailed.skip_smaps = not Config::getB("proc_info_smaps");
|
|
}
|
|
|
|
//? Copy proc_info for process from proc vector
|
|
auto p_info = rng::find(procs, pid, &proc_info::pid);
|
|
detailed.entry = *p_info;
|
|
|
|
//? Update cpu percent deque for process cpu graph
|
|
if (not Config::getB("proc_per_core")) detailed.entry.cpu_p *= Shared::coreCount;
|
|
detailed.cpu_percent.push_back(clamp((long long)round(detailed.entry.cpu_p), 0ll, 100ll));
|
|
while (cmp_greater(detailed.cpu_percent.size(), width)) detailed.cpu_percent.pop_front();
|
|
|
|
//? Process runtime : current time - start time (both in unix time - seconds since epoch)
|
|
struct timeval currentTime;
|
|
gettimeofday(¤tTime, nullptr);
|
|
//? Get elapsed time if process isn't dead
|
|
if (detailed.entry.state != 'X') detailed.elapsed = sec_to_dhms(currentTime.tv_sec - (detailed.entry.cpu_s / 1'000'000));
|
|
else detailed.elapsed = sec_to_dhms(detailed.entry.death_time);
|
|
if (detailed.elapsed.size() > 8) detailed.elapsed.resize(detailed.elapsed.size() - 3);
|
|
|
|
//? Get parent process name
|
|
if (detailed.parent.empty()) {
|
|
auto p_entry = rng::find(procs, detailed.entry.ppid, &proc_info::pid);
|
|
if (p_entry != procs.end()) detailed.parent = p_entry->name;
|
|
}
|
|
|
|
//? Expand process status from single char to explanative string
|
|
detailed.status = get_status(detailed.entry.state);
|
|
|
|
detailed.mem_bytes.push_back(detailed.entry.mem);
|
|
detailed.memory = floating_humanizer(detailed.entry.mem);
|
|
|
|
if (detailed.first_mem == -1 or detailed.first_mem < detailed.mem_bytes.back() / 2 or detailed.first_mem > detailed.mem_bytes.back() * 4) {
|
|
detailed.first_mem = min((uint64_t)detailed.mem_bytes.back() * 2, Mem::get_totalMem());
|
|
redraw = true;
|
|
}
|
|
|
|
while (cmp_greater(detailed.mem_bytes.size(), width)) detailed.mem_bytes.pop_front();
|
|
|
|
rusage_info_current rusage;
|
|
if (proc_pid_rusage(pid, RUSAGE_INFO_CURRENT, (void **)&rusage) == 0) {
|
|
// this fails for processes we don't own - same as in Linux
|
|
detailed.io_read = floating_humanizer(rusage.ri_diskio_bytesread);
|
|
detailed.io_write = floating_humanizer(rusage.ri_diskio_byteswritten);
|
|
}
|
|
}
|
|
|
|
//* Collects and sorts process information from /proc
|
|
auto collect(bool no_update) -> vector<proc_info> & {
|
|
const auto &sorting = Config::getS("proc_sorting");
|
|
auto reverse = Config::getB("proc_reversed");
|
|
const auto &filter = Config::getS("proc_filter");
|
|
auto per_core = Config::getB("proc_per_core");
|
|
auto tree = Config::getB("proc_tree");
|
|
auto show_detailed = Config::getB("show_detailed");
|
|
const auto pause_proc_list = Config::getB("pause_proc_list");
|
|
const size_t detailed_pid = Config::getI("detailed_pid");
|
|
bool should_filter = current_filter != filter;
|
|
if (should_filter) current_filter = filter;
|
|
bool sorted_change = (sorting != current_sort or reverse != current_rev or should_filter);
|
|
bool tree_mode_change = tree != is_tree_mode;
|
|
if (sorted_change) {
|
|
current_sort = sorting;
|
|
current_rev = reverse;
|
|
}
|
|
if (tree_mode_change) is_tree_mode = tree;
|
|
|
|
const int cmult = (per_core) ? Shared::coreCount : 1;
|
|
bool got_detailed = false;
|
|
|
|
static vector<size_t> found;
|
|
|
|
//* Use pids from last update if only changing filter, sorting or tree options
|
|
if (no_update and not current_procs.empty()) {
|
|
if (show_detailed and detailed_pid != detailed.last_pid) _collect_details(detailed_pid, current_procs);
|
|
} else {
|
|
//* ---------------------------------------------Collection start----------------------------------------------
|
|
|
|
{ //* Get CPU totals
|
|
natural_t cpu_count;
|
|
kern_return_t error;
|
|
processor_cpu_load_info_data_t *cpu_load_info = nullptr;
|
|
MachProcessorInfo info{};
|
|
error = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info.info_array, &info.info_count);
|
|
if (error != KERN_SUCCESS) {
|
|
Logger::error("Failed getting CPU load info");
|
|
}
|
|
cpu_load_info = (processor_cpu_load_info_data_t *)info.info_array;
|
|
cputimes = 0;
|
|
for (natural_t i = 0; i < cpu_count; i++) {
|
|
cputimes += (cpu_load_info[i].cpu_ticks[CPU_STATE_USER]
|
|
+ cpu_load_info[i].cpu_ticks[CPU_STATE_NICE]
|
|
+ cpu_load_info[i].cpu_ticks[CPU_STATE_SYSTEM]
|
|
+ cpu_load_info[i].cpu_ticks[CPU_STATE_IDLE]);
|
|
}
|
|
}
|
|
|
|
should_filter = true;
|
|
int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0};
|
|
found.clear();
|
|
size_t size = 0;
|
|
const auto timeNow = time_micros();
|
|
|
|
if (sysctl(mib, 4, nullptr, &size, nullptr, 0) < 0 || size == 0) {
|
|
Logger::error("Unable to get size of kproc_infos");
|
|
}
|
|
uint64_t cpu_t = 0;
|
|
|
|
std::unique_ptr<kinfo_proc[]> processes(new kinfo_proc[size / sizeof(kinfo_proc)]);
|
|
if (sysctl(mib, 4, processes.get(), &size, nullptr, 0) == 0) {
|
|
size_t count = size / sizeof(struct kinfo_proc);
|
|
for (size_t i = 0; i < count; i++) { //* iterate over all processes in kinfo_proc
|
|
struct kinfo_proc& kproc = processes.get()[i];
|
|
const size_t pid = (size_t)kproc.kp_proc.p_pid;
|
|
if (pid < 1) continue;
|
|
found.push_back(pid);
|
|
|
|
//? Check if pid already exists in current_procs
|
|
bool no_cache = false;
|
|
auto find_old = rng::find(current_procs, pid, &proc_info::pid);
|
|
//? Only add new processes if not paused
|
|
if (find_old == current_procs.end()) {
|
|
if (not pause_proc_list) {
|
|
current_procs.push_back({pid});
|
|
find_old = current_procs.end() - 1;
|
|
no_cache = true;
|
|
}
|
|
else continue;
|
|
}
|
|
else if (dead_procs.contains(pid)) continue;
|
|
|
|
auto &new_proc = *find_old;
|
|
|
|
//? Get program name, command, username, parent pid, nice and status
|
|
if (no_cache) {
|
|
char fullname[PROC_PIDPATHINFO_MAXSIZE];
|
|
int rc = proc_pidpath(pid, fullname, sizeof(fullname));
|
|
string f_name = "<defunct>";
|
|
if (rc != 0) {
|
|
f_name = std::string(fullname);
|
|
size_t lastSlash = f_name.find_last_of('/');
|
|
f_name = f_name.substr(lastSlash + 1);
|
|
}
|
|
new_proc.name = f_name;
|
|
//? Get process arguments if possible, fallback to process path in case of failure
|
|
if (Shared::arg_max > 0) {
|
|
std::unique_ptr<char[]> proc_chars(new char[Shared::arg_max]);
|
|
int mib[] = {CTL_KERN, KERN_PROCARGS2, (int)pid};
|
|
size_t argmax = Shared::arg_max;
|
|
if (sysctl(mib, 3, proc_chars.get(), &argmax, nullptr, 0) == 0) {
|
|
int argc = 0;
|
|
memcpy(&argc, &proc_chars.get()[0], sizeof(argc));
|
|
std::string_view proc_args(proc_chars.get(), argmax);
|
|
if (size_t null_pos = proc_args.find('\0', sizeof(argc)); null_pos != string::npos) {
|
|
if (size_t start_pos = proc_args.find_first_not_of('\0', null_pos); start_pos != string::npos) {
|
|
while (argc-- > 0 and null_pos != string::npos and cmp_less(new_proc.cmd.size(), 1000)) {
|
|
null_pos = proc_args.find('\0', start_pos);
|
|
new_proc.cmd += (string)proc_args.substr(start_pos, null_pos - start_pos) + ' ';
|
|
start_pos = null_pos + 1;
|
|
}
|
|
}
|
|
}
|
|
if (not new_proc.cmd.empty()) new_proc.cmd.pop_back();
|
|
}
|
|
}
|
|
if (new_proc.cmd.empty()) new_proc.cmd = f_name;
|
|
if (new_proc.cmd.size() > 1000) {
|
|
new_proc.cmd.resize(1000);
|
|
new_proc.cmd.shrink_to_fit();
|
|
}
|
|
new_proc.ppid = kproc.kp_eproc.e_ppid;
|
|
new_proc.cpu_s = kproc.kp_proc.p_starttime.tv_sec * 1'000'000 + kproc.kp_proc.p_starttime.tv_usec;
|
|
struct passwd *pwd = getpwuid(kproc.kp_eproc.e_ucred.cr_uid);
|
|
if (pwd != nullptr) {
|
|
new_proc.user = pwd->pw_name;
|
|
} else {
|
|
new_proc.user = std::to_string(kproc.kp_eproc.e_ucred.cr_uid);
|
|
}
|
|
}
|
|
new_proc.p_nice = kproc.kp_proc.p_nice;
|
|
new_proc.state = kproc.kp_proc.p_stat;
|
|
|
|
//? Get threads, mem and cpu usage
|
|
struct proc_taskinfo pti{};
|
|
if (sizeof(pti) == proc_pidinfo(new_proc.pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti))) {
|
|
new_proc.threads = pti.pti_threadnum;
|
|
new_proc.mem = pti.pti_resident_size;
|
|
cpu_t = pti.pti_total_user + pti.pti_total_system;
|
|
|
|
if (new_proc.cpu_t == 0) new_proc.cpu_t = cpu_t;
|
|
} else {
|
|
// Reset memory value if process info cannot be accessed (bad permissions or zombie processes)
|
|
new_proc.threads = 0;
|
|
new_proc.mem = 0;
|
|
cpu_t = new_proc.cpu_t;
|
|
}
|
|
|
|
//? Process cpu usage since last update
|
|
new_proc.cpu_p = clamp(round(((cpu_t - new_proc.cpu_t) * Shared::machTck) / ((cputimes - old_cputimes) * Shared::clkTck)) * cmult / 1000.0, 0.0, 100.0 * Shared::coreCount);
|
|
|
|
//? Process cumulative cpu usage since process start
|
|
new_proc.cpu_c = (double)(cpu_t * Shared::machTck) / (timeNow - new_proc.cpu_s);
|
|
|
|
//? Update cached value with latest cpu times
|
|
new_proc.cpu_t = cpu_t;
|
|
|
|
if (show_detailed and not got_detailed and new_proc.pid == detailed_pid) {
|
|
got_detailed = true;
|
|
}
|
|
}
|
|
|
|
//? Clear dead processes from current_procs if not paused
|
|
if (not pause_proc_list) {
|
|
auto eraser = rng::remove_if(current_procs, [&](const auto& element) { return not v_contains(found, element.pid); });
|
|
current_procs.erase(eraser.begin(), eraser.end());
|
|
if (!dead_procs.empty()) dead_procs.clear();
|
|
}
|
|
//? Set correct state of dead processes if paused
|
|
else {
|
|
const bool keep_dead_proc_usage = Config::getB("keep_dead_proc_usage");
|
|
for (auto& r : current_procs) {
|
|
if (rng::find(found, r.pid) == found.end()) {
|
|
if (r.state != 'X') {
|
|
struct timeval currentTime;
|
|
gettimeofday(¤tTime, nullptr);
|
|
r.death_time = currentTime.tv_sec - (r.cpu_s / 1'000'000);
|
|
}
|
|
r.state = 'X';
|
|
dead_procs.emplace(r.pid);
|
|
//? Reset cpu usage for dead processes if paused and option is set
|
|
if (!keep_dead_proc_usage) {
|
|
r.cpu_p = 0.0;
|
|
r.mem = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//? Update the details info box for process if active
|
|
if (show_detailed and got_detailed) {
|
|
_collect_details(detailed_pid, current_procs);
|
|
} else if (show_detailed and not got_detailed and detailed.status != "Dead") {
|
|
detailed.status = "Dead";
|
|
redraw = true;
|
|
}
|
|
|
|
old_cputimes = cputimes;
|
|
}
|
|
}
|
|
|
|
//* ---------------------------------------------Collection done-----------------------------------------------
|
|
|
|
//* Match filter if defined
|
|
if (should_filter) {
|
|
filter_found = 0;
|
|
for (auto &p : current_procs) {
|
|
if (not tree and not filter.empty()) {
|
|
if (!matches_filter(p, filter)) {
|
|
p.filtered = true;
|
|
filter_found++;
|
|
} else {
|
|
p.filtered = false;
|
|
}
|
|
} else {
|
|
p.filtered = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
//* Sort processes
|
|
if ((sorted_change or tree_mode_change) or (not no_update and not pause_proc_list)) {
|
|
proc_sorter(current_procs, sorting, reverse, tree);
|
|
}
|
|
|
|
//* Generate tree view if enabled
|
|
if (tree and (not no_update or should_filter or sorted_change)) {
|
|
bool locate_selection = false;
|
|
|
|
if (toggle_children != -1) {
|
|
auto collapser = rng::find(current_procs, toggle_children, &proc_info::pid);
|
|
if (collapser != current_procs.end()){
|
|
for (auto& p : current_procs) {
|
|
if (p.ppid == collapser->pid) {
|
|
auto child = rng::find(current_procs, p.pid, &proc_info::pid);
|
|
if (child != current_procs.end()){
|
|
child->collapsed = not child->collapsed;
|
|
}
|
|
}
|
|
}
|
|
if (Config::ints.at("proc_selected") > 0) locate_selection = true;
|
|
}
|
|
toggle_children = -1;
|
|
}
|
|
|
|
if (auto find_pid = (collapse != -1 ? collapse : expand); find_pid != -1) {
|
|
auto collapser = rng::find(current_procs, find_pid, &proc_info::pid);
|
|
if (collapser != current_procs.end()) {
|
|
if (collapse == expand) {
|
|
collapser->collapsed = not collapser->collapsed;
|
|
}
|
|
else if (collapse > -1) {
|
|
collapser->collapsed = true;
|
|
}
|
|
else if (expand > -1) {
|
|
collapser->collapsed = false;
|
|
}
|
|
if (Config::ints.at("proc_selected") > 0) locate_selection = true;
|
|
}
|
|
collapse = expand = -1;
|
|
}
|
|
if (should_filter or not filter.empty()) filter_found = 0;
|
|
|
|
vector<tree_proc> tree_procs;
|
|
tree_procs.reserve(current_procs.size());
|
|
|
|
if (!pause_proc_list) {
|
|
for (auto& p : current_procs) {
|
|
if (not v_contains(found, p.ppid)) p.ppid = 0;
|
|
}
|
|
}
|
|
|
|
//? Stable sort to retain selected sorting among processes with the same parent
|
|
rng::stable_sort(current_procs, rng::less{}, & proc_info::ppid);
|
|
|
|
//? Start recursive iteration over processes with the lowest shared parent pids
|
|
for (auto& p : rng::equal_range(current_procs, current_procs.at(0).ppid, rng::less{}, &proc_info::ppid)) {
|
|
_tree_gen(p, current_procs, tree_procs, 0, false, filter, false, no_update, should_filter);
|
|
}
|
|
|
|
//? Recursive sort over tree structure to account for collapsed processes in the tree
|
|
int index = 0;
|
|
tree_sort(tree_procs, sorting, reverse, (pause_proc_list and not (sorted_change or tree_mode_change)), index, current_procs.size());
|
|
|
|
//? Recursive construction of ASCII tree prefixes.
|
|
for (auto t = tree_procs.begin(); t != tree_procs.end(); ++t) {
|
|
_collect_prefixes(*t, t == tree_procs.end() - 1);
|
|
}
|
|
|
|
//? Final sort based on tree index
|
|
rng::stable_sort(current_procs, rng::less {}, &proc_info::tree_index);
|
|
|
|
//? Move current selection/view to the selected process when collapsing/expanding in the tree
|
|
if (locate_selection) {
|
|
int loc = rng::find(current_procs, Proc::selected_pid, &proc_info::pid)->tree_index;
|
|
if (Config::ints.at("proc_start") >= loc or Config::ints.at("proc_start") <= loc - Proc::select_max)
|
|
Config::ints.at("proc_start") = max(0, loc - 1);
|
|
Config::ints.at("proc_selected") = loc - Config::ints.at("proc_start") + 1;
|
|
}
|
|
}
|
|
|
|
numpids = (int)current_procs.size() - filter_found;
|
|
return current_procs;
|
|
}
|
|
} // namespace Proc
|
|
|
|
namespace Tools {
|
|
double system_uptime() {
|
|
struct timeval ts, currTime;
|
|
std::size_t len = sizeof(ts);
|
|
int mib[2] = {CTL_KERN, KERN_BOOTTIME};
|
|
if (sysctl(mib, 2, &ts, &len, nullptr, 0) != -1) {
|
|
gettimeofday(&currTime, nullptr);
|
|
return currTime.tv_sec - ts.tv_sec;
|
|
}
|
|
return 0.0;
|
|
}
|
|
} // namespace Tools
|