254 lines
8.1 KiB
C++
254 lines
8.1 KiB
C++
#include "fgc/HostMetrics.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
#include <sys/statvfs.h>
|
|
#include <unistd.h>
|
|
|
|
namespace fgc::hostmetrics {
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
std::string readFile(const fs::path& p) {
|
|
std::ifstream f(p);
|
|
if (!f) return {};
|
|
std::ostringstream ss;
|
|
ss << f.rdbuf();
|
|
return ss.str();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
double parseMilliCelsius(const std::string& s) {
|
|
try {
|
|
return std::stod(s) / 1000.0; // sysfs reports milli-degrees C
|
|
} catch (...) {
|
|
return 0.0;
|
|
}
|
|
}
|
|
|
|
MemInfo parseMeminfo(const std::string& text) {
|
|
MemInfo m;
|
|
std::istringstream in(text);
|
|
std::string line;
|
|
double mem_total_kb = 0, mem_avail_kb = 0, swap_total_kb = 0, swap_free_kb = 0;
|
|
bool have_total = false, have_avail = false;
|
|
while (std::getline(in, line)) {
|
|
std::istringstream ls(line);
|
|
std::string key, val, unit;
|
|
if (!(ls >> key >> val)) continue;
|
|
double kb = 0;
|
|
try { kb = std::stod(val); } catch (...) { continue; }
|
|
if (key == "MemTotal:") { mem_total_kb = kb; have_total = true; }
|
|
else if (key == "MemAvailable:") { mem_avail_kb = kb; have_avail = true; }
|
|
else if (key == "SwapTotal:") swap_total_kb = kb;
|
|
else if (key == "SwapFree:") swap_free_kb = kb;
|
|
}
|
|
m.ok = have_total && have_avail;
|
|
m.total_mb = mem_total_kb / 1024.0;
|
|
m.avail_mb = mem_avail_kb / 1024.0;
|
|
m.swap_used_mb = (swap_total_kb - swap_free_kb) / 1024.0;
|
|
return m;
|
|
}
|
|
|
|
CpuTimes parseProcStatCpu(const std::string& text) {
|
|
CpuTimes t;
|
|
std::istringstream in(text);
|
|
std::string line;
|
|
while (std::getline(in, line)) {
|
|
std::istringstream ls(line);
|
|
std::string key;
|
|
if (!(ls >> key) || key != "cpu") continue;
|
|
// user nice system idle iowait irq softirq steal [guest guest_nice]
|
|
// guest time is already inside user/nice, so it is not added again.
|
|
long long v[8] = {0, 0, 0, 0, 0, 0, 0, 0};
|
|
int n = 0;
|
|
while (n < 8 && (ls >> v[n])) ++n;
|
|
if (n < 4) return t;
|
|
const long long idle = v[3] + v[4];
|
|
long long total = 0;
|
|
for (int i = 0; i < n; ++i) total += v[i];
|
|
t.ok = true;
|
|
t.total = total;
|
|
t.busy = total - idle;
|
|
return t;
|
|
}
|
|
return t;
|
|
}
|
|
|
|
double busyPercent(const CpuTimes& before, const CpuTimes& after) {
|
|
if (!before.ok || !after.ok) return -1.0;
|
|
const long long dt = after.total - before.total;
|
|
if (dt <= 0) return -1.0;
|
|
const double pct = 100.0 * static_cast<double>(after.busy - before.busy) / static_cast<double>(dt);
|
|
return pct < 0.0 ? 0.0 : (pct > 100.0 ? 100.0 : pct);
|
|
}
|
|
|
|
namespace {
|
|
|
|
fs::path under(const std::string& root, const char* path) {
|
|
return fs::path(root.empty() ? "/" : root) / path;
|
|
}
|
|
|
|
// cpu0, cpu1, ... in number order - not cpufreq/, cpuidle/ and the rest.
|
|
std::vector<fs::path> cpuDirs(const std::string& root) {
|
|
std::vector<std::pair<int, fs::path>> found;
|
|
std::error_code ec;
|
|
for (auto& e : fs::directory_iterator(under(root, "sys/devices/system/cpu"), ec)) {
|
|
const auto name = e.path().filename().string();
|
|
if (name.size() < 4 || name.rfind("cpu", 0) != 0) continue;
|
|
bool digits = true;
|
|
for (size_t i = 3; i < name.size(); ++i)
|
|
if (name[i] < '0' || name[i] > '9') digits = false;
|
|
if (digits) found.emplace_back(std::stoi(name.substr(3)), e.path());
|
|
}
|
|
std::sort(found.begin(), found.end(),
|
|
[](const auto& a, const auto& b) { return a.first < b.first; });
|
|
std::vector<fs::path> out;
|
|
for (auto& f : found) out.push_back(f.second);
|
|
return out;
|
|
}
|
|
|
|
// A sysfs number, or -1 when the file is missing or unreadable.
|
|
long long readNumber(const fs::path& p) {
|
|
std::string s = readFile(p);
|
|
if (s.empty()) return -1;
|
|
try {
|
|
return std::stoll(s);
|
|
} catch (...) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
CpuFreqInfo readCpuFreq(const std::string& root) {
|
|
CpuFreqInfo f;
|
|
double sum = 0.0;
|
|
for (const auto& dir : cpuDirs(root)) {
|
|
const long long cur = readNumber(dir / "cpufreq/scaling_cur_freq"); // kHz
|
|
if (cur < 0) continue;
|
|
f.core_mhz.push_back(static_cast<double>(cur) / 1000.0);
|
|
sum += static_cast<double>(cur) / 1000.0;
|
|
const long long hw = readNumber(dir / "cpufreq/cpuinfo_max_freq");
|
|
if (hw > 0) f.hw_max_mhz = std::max(f.hw_max_mhz, static_cast<double>(hw) / 1000.0);
|
|
const long long cap = readNumber(dir / "cpufreq/scaling_max_freq");
|
|
if (cap > 0) {
|
|
const double mhz = static_cast<double>(cap) / 1000.0;
|
|
f.cap_mhz = f.cap_mhz > 0.0 ? std::min(f.cap_mhz, mhz) : mhz;
|
|
}
|
|
}
|
|
if (!f.core_mhz.empty()) {
|
|
f.ok = true;
|
|
f.mean_mhz = sum / static_cast<double>(f.core_mhz.size());
|
|
}
|
|
return f;
|
|
}
|
|
|
|
ThrottleCount readThrottleCount(const std::string& root) {
|
|
ThrottleCount c;
|
|
bool package_seen = false;
|
|
for (const auto& dir : cpuDirs(root)) {
|
|
// Every core shows the same package count; take it once.
|
|
if (!package_seen) {
|
|
const long long pkg = readNumber(dir / "thermal_throttle/package_throttle_count");
|
|
if (pkg >= 0) {
|
|
package_seen = true;
|
|
c.ok = true;
|
|
c.events += pkg;
|
|
}
|
|
}
|
|
const long long core = readNumber(dir / "thermal_throttle/core_throttle_count");
|
|
if (core >= 0) {
|
|
c.ok = true;
|
|
c.events += core;
|
|
}
|
|
}
|
|
return c;
|
|
}
|
|
|
|
CpuTimes readCpuTimes(const std::string& root) {
|
|
// A fake tree without its own proc/stat uses the real one: how busy the CPU
|
|
// is stays real in a simulation (raise it with tools/thermal-sim/heat-load.sh).
|
|
const fs::path p = under(root, "proc/stat");
|
|
std::error_code ec;
|
|
return parseProcStatCpu(readFile(fs::exists(p, ec) ? p : fs::path("/proc/stat")));
|
|
}
|
|
|
|
ThermalInfo readThermal(const std::string& root_dir) {
|
|
ThermalInfo t;
|
|
const fs::path root = under(root_dir, "sys/class/thermal");
|
|
std::error_code ec;
|
|
if (!fs::exists(root, ec)) return t;
|
|
bool have_pkg = false;
|
|
for (auto& e : fs::directory_iterator(root, ec)) {
|
|
const auto name = e.path().filename().string();
|
|
if (name.rfind("thermal_zone", 0) != 0) continue;
|
|
std::string temp_s = readFile(e.path() / "temp");
|
|
if (temp_s.empty()) continue;
|
|
double c = parseMilliCelsius(temp_s);
|
|
std::string type = readFile(e.path() / "type");
|
|
// strip trailing newline
|
|
while (!type.empty() && (type.back() == '\n' || type.back() == '\r')) type.pop_back();
|
|
if (type.empty()) type = name;
|
|
if (type == "x86_pkg_temp" && (!have_pkg || c > t.cpu_temp_c)) {
|
|
have_pkg = true;
|
|
t.cpu_temp_c = c;
|
|
t.cpu_zone = type;
|
|
}
|
|
if (!t.ok || c > t.max_temp_c) {
|
|
t.ok = true;
|
|
t.max_temp_c = c;
|
|
t.hottest_zone = type;
|
|
}
|
|
}
|
|
if (t.ok && !have_pkg) {
|
|
t.cpu_temp_c = t.max_temp_c;
|
|
t.cpu_zone = t.hottest_zone;
|
|
}
|
|
return t;
|
|
}
|
|
|
|
DiskInfo readDisk(const std::string& path) {
|
|
DiskInfo d;
|
|
d.path = path;
|
|
struct statvfs vfs {};
|
|
if (statvfs(path.c_str(), &vfs) != 0) return d;
|
|
const double frsize = static_cast<double>(vfs.f_frsize);
|
|
const double total = static_cast<double>(vfs.f_blocks) * frsize;
|
|
const double avail = static_cast<double>(vfs.f_bavail) * frsize;
|
|
const double gb = 1024.0 * 1024.0 * 1024.0;
|
|
d.ok = true;
|
|
d.total_gb = total / gb;
|
|
d.free_gb = avail / gb;
|
|
d.used_pct = total > 0 ? 100.0 * (total - avail) / total : 0.0;
|
|
return d;
|
|
}
|
|
|
|
MemInfo readMeminfo() {
|
|
return parseMeminfo(readFile("/proc/meminfo"));
|
|
}
|
|
|
|
LoadInfo readLoad() {
|
|
LoadInfo l;
|
|
double avg[3] = {0, 0, 0};
|
|
if (getloadavg(avg, 3) == 3) {
|
|
l.ok = true;
|
|
l.load1 = avg[0];
|
|
l.load5 = avg[1];
|
|
l.load15 = avg[2];
|
|
}
|
|
long n = sysconf(_SC_NPROCESSORS_ONLN);
|
|
l.cpus = (n > 0) ? static_cast<int>(n) : 0;
|
|
return l;
|
|
}
|
|
|
|
} // namespace fgc::hostmetrics
|