/** * @file src/platform/macos/host_stats.mm * @brief macOS implementation of @ref platf::host_stats_provider_t. * * Uses Mach + sysctl + getifaddrs for CPU%, RAM, network throughput, and * CPU/host model. GPU, GPU encoder utilization, GPU temperature, and VRAM * are not exposed by stable public APIs on modern macOS (especially Apple * Silicon, where IOReport is the only viable path and is private). Those * fields are left at the documented sentinels. */ // standard includes #include #include #include #include #include #include // lib includes #include #include #include #include #include #include #include #include #include #include #include // local includes #include "src/logging.h" #include "src/platform/common.h" namespace { using std::chrono::steady_clock; std::string sysctl_string(const char *name) { size_t len = 0; if (sysctlbyname(name, nullptr, &len, nullptr, 0) != 0 || len == 0) return {}; std::string buf(len, '\0'); if (sysctlbyname(name, buf.data(), &len, nullptr, 0) != 0) return {}; if (!buf.empty() && buf.back() == '\0') buf.pop_back(); return buf; } std::uint64_t sysctl_u64(const char *name) { std::uint64_t v = 0; size_t len = sizeof(v); if (sysctlbyname(name, &v, &len, nullptr, 0) != 0) return 0; return v; } int sysctl_int(const char *name) { int v = 0; size_t len = sizeof(v); if (sysctlbyname(name, &v, &len, nullptr, 0) != 0) return 0; return v; } struct cpu_ticks_t { std::uint64_t user = 0; std::uint64_t system = 0; std::uint64_t idle = 0; std::uint64_t nice = 0; std::uint64_t total() const { return user + system + idle + nice; } std::uint64_t busy() const { return user + system + nice; } }; std::optional read_cpu_ticks() { host_cpu_load_info_data_t load {}; mach_msg_type_number_t count = HOST_CPU_LOAD_INFO_COUNT; if (host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, reinterpret_cast(&load), &count) != KERN_SUCCESS) { return std::nullopt; } cpu_ticks_t t; t.user = load.cpu_ticks[CPU_STATE_USER]; t.system = load.cpu_ticks[CPU_STATE_SYSTEM]; t.idle = load.cpu_ticks[CPU_STATE_IDLE]; t.nice = load.cpu_ticks[CPU_STATE_NICE]; return t; } struct memory_t { std::uint64_t total_bytes = 0; std::uint64_t used_bytes = 0; }; std::optional read_memory() { memory_t m; m.total_bytes = sysctl_u64("hw.memsize"); if (!m.total_bytes) return std::nullopt; vm_size_t page_size = 0; host_page_size(mach_host_self(), &page_size); if (!page_size) page_size = 4096; vm_statistics64_data_t stats {}; mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; if (host_statistics64(mach_host_self(), HOST_VM_INFO64, reinterpret_cast(&stats), &count) != KERN_SUCCESS) { return std::nullopt; } // "Used" approximates Activity Monitor's App Memory + Wired + Compressed. std::uint64_t used_pages = static_cast(stats.active_count) + static_cast(stats.wire_count) + static_cast(stats.compressor_page_count); m.used_bytes = used_pages * static_cast(page_size); return m; } // Walk the kernel's routing table and find the interface that owns the // default route (destination 0.0.0.0). Returns the interface name or empty. std::string default_route_iface() { int mib[6] = {CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_DUMP, 0}; size_t needed = 0; if (sysctl(mib, 6, nullptr, &needed, nullptr, 0) != 0 || !needed) return {}; std::string buf(needed, '\0'); if (sysctl(mib, 6, buf.data(), &needed, nullptr, 0) != 0) return {}; auto *next = buf.data(); auto *end = next + needed; while (next < end) { auto *rtm = reinterpret_cast(next); if (rtm->rtm_version != RTM_VERSION) break; if ((rtm->rtm_flags & (RTF_UP | RTF_GATEWAY)) == (RTF_UP | RTF_GATEWAY)) { auto *sa = reinterpret_cast(rtm + 1); if (sa->sa_family == AF_INET) { auto *sin = reinterpret_cast(sa); if (sin->sin_addr.s_addr == 0) { char name[IF_NAMESIZE] = {}; if (if_indextoname(rtm->rtm_index, name)) return name; } } } next += rtm->rtm_msglen; } return {}; } struct net_counters_t { std::uint64_t rx_bytes = 0; std::uint64_t tx_bytes = 0; }; std::optional read_net_counters(const std::string &iface) { if (iface.empty()) return std::nullopt; struct ifaddrs *list = nullptr; if (getifaddrs(&list) != 0 || !list) return std::nullopt; std::optional result; for (auto *p = list; p; p = p->ifa_next) { if (!p->ifa_addr || p->ifa_addr->sa_family != AF_LINK) continue; if (iface != p->ifa_name) continue; auto *data = reinterpret_cast(p->ifa_data); if (!data) continue; net_counters_t c; c.rx_bytes = data->ifi_ibytes; c.tx_bytes = data->ifi_obytes; result = c; break; } freeifaddrs(list); return result; } std::uint64_t read_iface_link_speed_mbps(const std::string &iface) { if (iface.empty()) return 0; struct ifaddrs *list = nullptr; if (getifaddrs(&list) != 0 || !list) return 0; std::uint64_t mbps = 0; for (auto *p = list; p; p = p->ifa_next) { if (!p->ifa_addr || p->ifa_addr->sa_family != AF_LINK) continue; if (iface != p->ifa_name) continue; auto *data = reinterpret_cast(p->ifa_data); if (!data) continue; // ifi_baudrate is in bits/sec on macOS. mbps = data->ifi_baudrate / 1'000'000ull; break; } freeifaddrs(list); return mbps; } double clamp_throughput_bps(double bits_per_second, std::uint64_t link_speed_mbps) { if (bits_per_second < 0.0 || link_speed_mbps == 0) { return bits_per_second; } const double max_bits_per_second = static_cast(link_speed_mbps) * 1'000'000.0 * 1.10; return std::min(bits_per_second, max_bits_per_second); } class macos_host_stats_t: public platf::host_stats_provider_t { public: platf::host_stats_t sample() override { platf::host_stats_t out; sample_cpu(out); sample_memory(out); sample_network(out); // GPU / GPU encoder / GPU temp / VRAM: no stable public API on modern // macOS. Leave at sentinels. return out; } platf::host_info_t info() override { platf::host_info_t out; out.cpu_model = sysctl_string("machdep.cpu.brand_string"); if (out.cpu_model.empty()) out.cpu_model = sysctl_string("hw.model"); out.cpu_logical_cores = sysctl_int("hw.logicalcpu"); out.ram_total_bytes = sysctl_u64("hw.memsize"); // GPU model: best-effort via hw.model string (Apple Silicon collapses // CPU + GPU under a single SoC name). out.gpu_model = sysctl_string("hw.model"); auto iface = pick_iface(); out.net_interface = iface; out.net_link_speed_mbps = read_iface_link_speed_mbps(iface); return out; } private: void sample_cpu(platf::host_stats_t &out) { auto t = read_cpu_ticks(); if (!t) return; if (_have_cpu_baseline) { auto dt = t->total() - _last_cpu.total(); auto db = t->busy() - _last_cpu.busy(); if (dt > 0) { out.cpu_percent = std::clamp(static_cast(db) * 100.f / static_cast(dt), 0.f, 100.f); } } _last_cpu = *t; _have_cpu_baseline = true; } void sample_memory(platf::host_stats_t &out) { auto m = read_memory(); if (!m) return; out.ram_total_bytes = m->total_bytes; out.ram_used_bytes = m->used_bytes; } std::string pick_iface() { auto now = steady_clock::now(); if (_iface.empty() || (now - _iface_picked_at) > std::chrono::seconds(30)) { auto candidate = default_route_iface(); if (!candidate.empty()) { _iface = candidate; _net_link_speed_mbps = read_iface_link_speed_mbps(candidate); } _iface_picked_at = now; } return _iface; } void sample_network(platf::host_stats_t &out) { auto iface = pick_iface(); if (iface.empty()) return; auto c = read_net_counters(iface); if (!c) return; auto now = steady_clock::now(); if (_have_net_baseline && _last_net_iface == iface) { auto dt = std::chrono::duration(now - _last_net_at).count(); if (c->rx_bytes < _last_net.rx_bytes || c->tx_bytes < _last_net.tx_bytes) { out.net_rx_bps = 0.0; out.net_tx_bps = 0.0; } else if (dt > 0.05) { double drx = static_cast(c->rx_bytes - _last_net.rx_bytes); double dtx = static_cast(c->tx_bytes - _last_net.tx_bytes); out.net_rx_bps = clamp_throughput_bps((drx * 8.0) / dt, _net_link_speed_mbps); out.net_tx_bps = clamp_throughput_bps((dtx * 8.0) / dt, _net_link_speed_mbps); } else { out.net_rx_bps = 0.0; out.net_tx_bps = 0.0; } } else { out.net_rx_bps = 0.0; out.net_tx_bps = 0.0; } _last_net = *c; _last_net_iface = iface; _last_net_at = now; _have_net_baseline = true; } cpu_ticks_t _last_cpu {}; bool _have_cpu_baseline = false; std::string _iface; steady_clock::time_point _iface_picked_at {}; net_counters_t _last_net {}; std::string _last_net_iface; steady_clock::time_point _last_net_at {}; bool _have_net_baseline = false; std::uint64_t _net_link_speed_mbps = 0; }; } // namespace namespace platf { std::unique_ptr create_host_stats_provider() { return std::make_unique(); } } // namespace platf