#include "common/hostStats.h" #include "common/config.h" #include #include #include #include #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS #ifndef NOMINMAX #define NOMINMAX #endif #include // IWYU pragma: keep // clang-format off #include // clang-format on #elif defined(__APPLE__) #include #include #include #include #include #include #include #include #include #else #include #include #include #include #include #endif namespace Common { namespace { // Cumulative counters; rates come from the difference between two samples. struct Counters { struct Thread { uint64_t cpu_ns; std::string name; }; std::optional process_cpu_ns; std::optional system_busy; std::optional system_total; std::optional context_switches; std::optional page_faults; std::optional page_ins; std::optional disk_read; std::optional disk_write; std::unordered_map threads; uint64_t wall_ns = 0; }; uint64_t WallNanoseconds() { return static_cast(std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()) .count()); } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS uint64_t FileTimeTo100Ns(const FILETIME& time) { return (static_cast(time.dwHighDateTime) << 32u) | time.dwLowDateTime; } struct PlatformState {}; void SamplePlatform(PlatformState& /*state*/, HostStats& stats, Counters& now) { const auto process = GetCurrentProcess(); FILETIME creation {}; FILETIME exit {}; FILETIME kernel {}; FILETIME user {}; if (GetProcessTimes(process, &creation, &exit, &kernel, &user) != 0) { now.process_cpu_ns = (FileTimeTo100Ns(kernel) + FileTimeTo100Ns(user)) * 100u; } FILETIME idle {}; if (GetSystemTimes(&idle, &kernel, &user) != 0) { // Kernel time includes idle time. now.system_total = FileTimeTo100Ns(kernel) + FileTimeTo100Ns(user); now.system_busy = *now.system_total - FileTimeTo100Ns(idle); } PROCESS_MEMORY_COUNTERS memory {}; if (K32GetProcessMemoryInfo(process, &memory, sizeof(memory)) != 0) { stats.process_memory = memory.WorkingSetSize; stats.process_resident = memory.WorkingSetSize; now.page_faults = memory.PageFaultCount; } IO_COUNTERS io {}; if (GetProcessIoCounters(process, &io) != 0) { now.disk_read = io.ReadTransferCount; now.disk_write = io.WriteTransferCount; } MEMORYSTATUSEX system {}; system.dwLength = sizeof(system); if (GlobalMemoryStatusEx(&system) != 0) { stats.system_memory_total = system.ullTotalPhys; stats.system_memory_used = system.ullTotalPhys - system.ullAvailPhys; stats.system_free = system.ullAvailPhys; stats.memory_pressure = system.dwMemoryLoad >= 90 ? MemoryPressure::Critical : system.dwMemoryLoad >= 80 ? MemoryPressure::Warning : MemoryPressure::Normal; } } #else void SampleRusage(Counters& now) { rusage usage {}; if (getrusage(RUSAGE_SELF, &usage) != 0) { return; } const auto to_ns = [](const timeval& time) { return static_cast(time.tv_sec) * 1000000000u + static_cast(time.tv_usec) * 1000u; }; now.process_cpu_ns = to_ns(usage.ru_utime) + to_ns(usage.ru_stime); now.context_switches = static_cast(usage.ru_nvcsw + usage.ru_nivcsw); now.page_faults = static_cast(usage.ru_minflt + usage.ru_majflt); now.page_ins = static_cast(usage.ru_majflt); } #if defined(__APPLE__) template std::optional SysctlValue(const char* name) { T value {}; size_t size = sizeof(value); if (sysctlbyname(name, &value, &size, nullptr, 0) != 0 || size != sizeof(value)) { return std::nullopt; } return value; } struct PlatformState { mach_port_t host = mach_host_self(); int thermal_token = -1; bool thermal_ready = false; std::string gpu_model; std::optional gpu_cores; ~PlatformState() { if (thermal_ready) { notify_cancel(thermal_token); } mach_port_deallocate(mach_task_self(), host); } }; std::optional DictionaryNumber(CFDictionaryRef dictionary, CFStringRef key) { const auto* value = CFDictionaryGetValue(dictionary, key); int64_t number = 0; if (value == nullptr || CFGetTypeID(value) != CFNumberGetTypeID() || !CFNumberGetValue(static_cast(value), kCFNumberSInt64Type, &number) || number < 0) { return std::nullopt; } return number; } std::string RegistryString(io_object_t service, CFStringRef key) { std::string result; auto* value = IORegistryEntryCreateCFProperty(service, key, kCFAllocatorDefault, 0); if (value == nullptr) { return result; } if (CFGetTypeID(value) == CFStringGetTypeID()) { char buffer[128] {}; if (CFStringGetCString(static_cast(value), buffer, sizeof(buffer), kCFStringEncodingUTF8)) { result = buffer; } } else if (CFGetTypeID(value) == CFDataGetTypeID()) { // Intel Macs store the model as NUL-terminated data. const auto* data = static_cast(value); const auto* bytes = reinterpret_cast(CFDataGetBytePtr(data)); const auto length = static_cast(CFDataGetLength(data)); result.assign(bytes, strnlen(bytes, length)); } CFRelease(value); return result; } // IOAccelerator exposes whole-device statistics; there is no per-process figure. void SampleGpu(PlatformState& state, HostStats& stats) { io_iterator_t iterator = 0; if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching("IOAccelerator"), &iterator) != KERN_SUCCESS) { return; } const auto take_max = [](std::optional& target, std::optional value) { if (value) { target = std::max(target.value_or(0.0), std::min(100.0, static_cast(*value))); } }; const auto add = [](std::optional& target, std::optional value) { if (value) { target = target.value_or(0) + static_cast(*value); } }; for (io_object_t service = IOIteratorNext(iterator); service != 0; service = IOIteratorNext(iterator)) { if (state.gpu_model.empty()) { state.gpu_model = RegistryString(service, CFSTR("model")); auto* cores = IORegistryEntryCreateCFProperty(service, CFSTR("gpu-core-count"), kCFAllocatorDefault, 0); int64_t count = 0; if (cores != nullptr) { if (CFGetTypeID(cores) == CFNumberGetTypeID() && CFNumberGetValue(static_cast(cores), kCFNumberSInt64Type, &count) && count > 0) { state.gpu_cores = static_cast(count); } CFRelease(cores); } } auto* statistics = IORegistryEntryCreateCFProperty(service, CFSTR("PerformanceStatistics"), kCFAllocatorDefault, 0); if (statistics != nullptr) { if (CFGetTypeID(statistics) == CFDictionaryGetTypeID()) { const auto* values = static_cast(statistics); take_max(stats.gpu_percent, DictionaryNumber(values, CFSTR("Device Utilization %"))); take_max(stats.gpu_renderer_percent, DictionaryNumber(values, CFSTR("Renderer Utilization %"))); take_max(stats.gpu_tiler_percent, DictionaryNumber(values, CFSTR("Tiler Utilization %"))); add(stats.gpu_memory_in_use, DictionaryNumber(values, CFSTR("In use system memory"))); add(stats.gpu_memory_allocated, DictionaryNumber(values, CFSTR("Alloc system memory"))); add(stats.gpu_recoveries, DictionaryNumber(values, CFSTR("recoveryCount"))); } CFRelease(statistics); } IOObjectRelease(service); } IOObjectRelease(iterator); stats.gpu_model = state.gpu_model; stats.gpu_cores = state.gpu_cores; } void SampleThreads(HostStats& stats, Counters& now) { thread_act_array_t threads = nullptr; mach_msg_type_number_t count = 0; const auto task = mach_task_self(); if (task_threads(task, &threads, &count) != KERN_SUCCESS) { return; } stats.thread_count = count; for (mach_msg_type_number_t i = 0; i < count; i++) { thread_identifier_info_data_t identifier {}; mach_msg_type_number_t size = THREAD_IDENTIFIER_INFO_COUNT; thread_basic_info_data_t basic {}; mach_msg_type_number_t basic_size = THREAD_BASIC_INFO_COUNT; if (thread_info(threads[i], THREAD_IDENTIFIER_INFO, reinterpret_cast(&identifier), &size) == KERN_SUCCESS && thread_info(threads[i], THREAD_BASIC_INFO, reinterpret_cast(&basic), &basic_size) == KERN_SUCCESS) { const auto to_ns = [](const time_value_t& time) { return static_cast(time.seconds) * 1000000000u + static_cast(time.microseconds) * 1000u; }; auto& thread = now.threads[identifier.thread_id]; thread.cpu_ns = to_ns(basic.user_time) + to_ns(basic.system_time); thread_extended_info_data_t extended {}; mach_msg_type_number_t extended_size = THREAD_EXTENDED_INFO_COUNT; if (thread_info(threads[i], THREAD_EXTENDED_INFO, reinterpret_cast(&extended), &extended_size) == KERN_SUCCESS) { thread.name = std::string(extended.pth_name, strnlen(extended.pth_name, sizeof(extended.pth_name))); } } mach_port_deallocate(task, threads[i]); } vm_deallocate(task, reinterpret_cast(threads), count * sizeof(thread_act_t)); } void SamplePlatform(PlatformState& state, HostStats& stats, Counters& now) { SampleRusage(now); host_cpu_load_info_data_t load {}; mach_msg_type_number_t load_count = HOST_CPU_LOAD_INFO_COUNT; if (host_statistics(state.host, HOST_CPU_LOAD_INFO, reinterpret_cast(&load), &load_count) == KERN_SUCCESS) { const uint64_t busy = static_cast(load.cpu_ticks[CPU_STATE_USER]) + load.cpu_ticks[CPU_STATE_SYSTEM] + load.cpu_ticks[CPU_STATE_NICE]; now.system_busy = busy; now.system_total = busy + load.cpu_ticks[CPU_STATE_IDLE]; } // Physical footprint is what Activity Monitor shows in its Memory column. task_vm_info_data_t vm_info {}; mach_msg_type_number_t vm_info_count = TASK_VM_INFO_COUNT; if (task_info(mach_task_self(), TASK_VM_INFO, reinterpret_cast(&vm_info), &vm_info_count) == KERN_SUCCESS) { stats.process_memory = vm_info.phys_footprint; stats.process_resident = vm_info.resident_size; stats.process_compressed = vm_info.compressed; stats.process_file_backed = vm_info.external; stats.process_virtual = vm_info.virtual_size; } rusage_info_v2 usage {}; if (proc_pid_rusage(getpid(), RUSAGE_INFO_V2, reinterpret_cast(&usage)) == 0) { now.disk_read = usage.ri_diskio_bytesread; now.disk_write = usage.ri_diskio_byteswritten; } stats.system_memory_total = SysctlValue("hw.memsize"); vm_size_t page = 0; vm_statistics64_data_t vm {}; mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; if (host_page_size(state.host, &page) == KERN_SUCCESS && host_statistics64(state.host, HOST_VM_INFO64, reinterpret_cast(&vm), &count) == KERN_SUCCESS) { // Activity Monitor's "Memory Used": app memory + wired + compressed. const uint64_t app = vm.internal_page_count > vm.purgeable_count ? vm.internal_page_count - vm.purgeable_count : 0; stats.system_memory_used = (app + vm.wire_count + vm.compressor_page_count) * page; stats.system_wired = static_cast(vm.wire_count) * page; stats.system_compressed = static_cast(vm.compressor_page_count) * page; stats.system_cached = (static_cast(vm.external_page_count) + vm.purgeable_count) * page; stats.system_free = static_cast(vm.free_count) * page; } if (const auto swap = SysctlValue("vm.swapusage")) { stats.swap_used = swap->xsu_used; stats.swap_total = swap->xsu_total; } // 1 = normal, 2 = warning, 4 = critical, as reported to dispatch memory-pressure sources. if (const auto level = SysctlValue("kern.memorystatus_vm_pressure_level")) { stats.memory_pressure = *level >= 4 ? MemoryPressure::Critical : *level >= 2 ? MemoryPressure::Warning : MemoryPressure::Normal; } // 0 nominal, 1 moderate, 2 heavy, 3 trapping, 4 sleeping. if (!state.thermal_ready) { state.thermal_ready = notify_register_check("com.apple.system.thermalpressurelevel", &state.thermal_token) == NOTIFY_STATUS_OK; } uint64_t thermal = 0; if (state.thermal_ready && notify_get_state(state.thermal_token, &thermal) == NOTIFY_STATUS_OK) { stats.thermal_pressure = thermal >= 3 ? ThermalPressure::Critical : thermal == 2 ? ThermalPressure::Heavy : thermal == 1 ? ThermalPressure::Moderate : ThermalPressure::Nominal; } SampleThreads(stats, now); SampleGpu(state, stats); } #else using ProcValues = std::unordered_map; // Reads ": [kB]" lines; kB values are returned in bytes. ProcValues ReadProcValues(const char* path) { ProcValues values; std::ifstream file(path); std::string line; while (std::getline(file, line)) { const auto colon = line.find(':'); if (colon == std::string::npos) { continue; } std::istringstream stream(line.substr(colon + 1)); uint64_t number = 0; std::string unit; if (stream >> number) { stream >> unit; values[line.substr(0, colon)] = unit == "kB" ? number * 1024u : number; } } return values; } std::optional Find(const ProcValues& values, const char* key) { const auto it = values.find(key); return it != values.end() ? std::optional(it->second) : std::nullopt; } struct PlatformState {}; void SampleThreads(HostStats& stats, Counters& now) { static const double ns_per_tick = 1e9 / static_cast(std::max(sysconf(_SC_CLK_TCK), 1L)); std::error_code error; uint32_t count = 0; for (std::filesystem::directory_iterator it("/proc/self/task", error), end; !error && it != end; it.increment(error)) { count++; std::ifstream stat_file(it->path() / "stat"); std::string stat; uint64_t id = 0; if (!(stat_file >> id)) { continue; // The thread may have exited during enumeration. } std::getline(stat_file, stat); const auto close = stat.rfind(')'); if (close == std::string::npos) { continue; } // Fields after the command name start at field 3 (state); utime and stime are 14 and 15. std::istringstream fields(stat.substr(close + 2)); std::string field; for (int index = 3; index < 14 && fields >> field; index++) { } uint64_t user = 0; uint64_t system = 0; if (!(fields >> user >> system)) { continue; } auto& thread = now.threads[id]; thread.cpu_ns = static_cast(static_cast(user + system) * ns_per_tick); std::ifstream name_file(it->path() / "comm"); std::getline(name_file, thread.name); } if (!error) { stats.thread_count = count; } } void SamplePlatform(PlatformState& /*state*/, HostStats& stats, Counters& now) { SampleRusage(now); std::ifstream cpu_file("/proc/stat"); std::string label; if (cpu_file >> label && label == "cpu") { uint64_t total = 0; uint64_t idle = 0; uint64_t value = 0; for (int index = 0; index < 8 && cpu_file >> value; index++) { total += value; if (index == 3 || index == 4) { // idle, iowait idle += value; } } now.system_total = total; now.system_busy = total - idle; } const auto status = ReadProcValues("/proc/self/status"); stats.process_memory = Find(status, "VmRSS"); stats.process_resident = Find(status, "VmRSS"); stats.process_file_backed = Find(status, "RssFile"); stats.process_compressed = Find(status, "VmSwap"); stats.process_virtual = Find(status, "VmSize"); const auto io = ReadProcValues("/proc/self/io"); now.disk_read = Find(io, "read_bytes"); now.disk_write = Find(io, "write_bytes"); const auto memory = ReadProcValues("/proc/meminfo"); const auto total = Find(memory, "MemTotal"); const auto available = Find(memory, "MemAvailable"); if (total && available && *available <= *total) { stats.system_memory_total = total; stats.system_memory_used = *total - *available; } stats.system_free = Find(memory, "MemFree"); if (const auto cached = Find(memory, "Cached")) { stats.system_cached = *cached + Find(memory, "Buffers").value_or(0); } const auto swap_total = Find(memory, "SwapTotal"); const auto swap_free = Find(memory, "SwapFree"); if (swap_total && swap_free && *swap_free <= *swap_total) { stats.swap_total = swap_total; stats.swap_used = *swap_total - *swap_free; } // Pressure stall information: share of the last 10 s some task waited on memory. std::ifstream pressure("/proc/pressure/memory"); std::string kind; std::string average; double stalled = 0.0; if (pressure >> kind >> average && kind == "some" && average.rfind("avg10=", 0) == 0 && std::istringstream(average.substr(6)) >> stalled) { stats.memory_pressure = stalled >= 20.0 ? MemoryPressure::Critical : stalled >= 5.0 ? MemoryPressure::Warning : MemoryPressure::Normal; } else if (total && available && *total != 0) { const double free_share = static_cast(*available) / static_cast(*total); stats.memory_pressure = free_share < 0.10 ? MemoryPressure::Critical : free_share < 0.25 ? MemoryPressure::Warning : MemoryPressure::Normal; } SampleThreads(stats, now); } #endif #endif constexpr size_t BUSIEST_THREADS = 6; } // namespace struct HostStatsSampler::Impl { PlatformState state; Counters last; uint32_t logical_cores = std::max(std::thread::hardware_concurrency(), 1u); bool has_last = false; }; HostStatsSampler::HostStatsSampler(): m_impl(std::make_unique()) {} HostStatsSampler::~HostStatsSampler() = default; HostStats HostStatsSampler::Sample() { HostStats stats; stats.logical_cores = m_impl->logical_cores; Counters now; now.wall_ns = WallNanoseconds(); SamplePlatform(m_impl->state, stats, now); const auto& last = m_impl->last; const auto elapsed = static_cast(now.wall_ns - last.wall_ns) / 1e9; if (m_impl->has_last && elapsed > 0.0) { const auto rate = [&](std::optional current, std::optional previous) -> std::optional { if (!current || !previous || *current < *previous) { return std::nullopt; } return static_cast(*current - *previous) / elapsed; }; if (const auto cpu = rate(now.process_cpu_ns, last.process_cpu_ns)) { stats.process_cpu_percent = std::clamp(*cpu / 1e9 * 100.0 / stats.logical_cores, 0.0, 100.0); } if (now.system_total && last.system_total && now.system_busy && last.system_busy && *now.system_total > *last.system_total && *now.system_busy >= *last.system_busy) { stats.system_cpu_percent = std::clamp(static_cast(*now.system_busy - *last.system_busy) / static_cast(*now.system_total - *last.system_total) * 100.0, 0.0, 100.0); } stats.context_switches_per_s = rate(now.context_switches, last.context_switches); stats.page_faults_per_s = rate(now.page_faults, last.page_faults); stats.page_ins_per_s = rate(now.page_ins, last.page_ins); stats.disk_read_per_s = rate(now.disk_read, last.disk_read); stats.disk_write_per_s = rate(now.disk_write, last.disk_write); for (const auto& [id, thread]: now.threads) { // A thread missing from the last sample started during this interval. const auto previous = last.threads.find(id); const uint64_t before = previous != last.threads.end() ? previous->second.cpu_ns : 0; if (thread.cpu_ns < before) { continue; } const double percent = static_cast(thread.cpu_ns - before) / 1e9 / elapsed * 100.0; stats.busiest_threads.push_back( {thread.name.empty() ? "thread " + std::to_string(id) : thread.name, percent}); } std::sort(stats.busiest_threads.begin(), stats.busiest_threads.end(), [](const auto& a, const auto& b) { return a.cpu_percent > b.cpu_percent; }); if (stats.busiest_threads.size() > BUSIEST_THREADS) { stats.busiest_threads.resize(BUSIEST_THREADS); } } m_impl->last = std::move(now); m_impl->has_last = true; return stats; } } // namespace Common