aboutsummaryrefslogtreecommitdiff
path: root/src/system-stats/report.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/system-stats/report.cpp')
-rw-r--r--src/system-stats/report.cpp545
1 files changed, 545 insertions, 0 deletions
diff --git a/src/system-stats/report.cpp b/src/system-stats/report.cpp
new file mode 100644
index 0000000..2ab3916
--- /dev/null
+++ b/src/system-stats/report.cpp
@@ -0,0 +1,545 @@
+#include <cstdlib>
+#include <memory>
+#include <iostream>
+#include <iomanip>
+#include <cstring>
+#include <cerrno>
+#include <chrono>
+#include <cstdint>
+#include <sstream>
+#include <limits>
+#include <string>
+#include <cctype>
+#include <cmath>
+#include <ctime>
+#include <vector>
+#include <map>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <unistd.h>
+#include "common.h"
+
+namespace fs = std::filesystem;
+#define VA_STR(x) dynamic_cast<std::ostringstream const&>(std::ostringstream().flush() << x).str()
+
+auto constexpr MB_i = 1000000LL;
+auto constexpr MB_f = 1000000.0;
+auto constexpr GB_i = 1000000000LL;
+auto constexpr GB_f = 1000000000.0;
+auto constexpr TB_i = 1000000000000LL;
+auto constexpr TB_f = 1000000000000.0; // only C++14 has a readable alternative
+
+static std::string stateFilePath;
+
+void abortApp(const char* reason)
+{
+ std::cout << "ERROR " << errno << ":\n" << (reason? reason : "") << ":" << strerror(errno) << std::endl;
+ exit(2);
+}
+
+std::string ENV(const char *name)
+{
+ if (char *val = std::getenv(name))
+ return val;
+ return "";
+}
+
+bool readFile(const char* inputFile, std::vector<char>& buffer, bool mustExist = true)
+{
+ int fd = open(inputFile, O_RDONLY);
+ if (fd < 0) {
+ if (mustExist) abortApp(inputFile);
+ return false;
+ } else {
+ buffer.resize(4000);
+ for (std::size_t offset = 0;;) {
+ int bytes = ::read(fd, &buffer[offset], buffer.size() - offset - 1);
+ if (bytes < 0) abortApp(inputFile);
+ offset += bytes;
+ if (offset + 1 == buffer.size()) buffer.resize(buffer.size() * 2);
+ else if (bytes == 0) {
+ close(fd);
+ buffer[offset] = 0;
+ buffer.resize(offset);
+ return true;
+ }
+ }
+ }
+}
+
+bool writeFile(const char* outputFile, const std::ostringstream& data, int flag = O_CREAT | O_WRONLY | O_TRUNC)
+{
+ bool success = false;
+ int fd = open(outputFile, flag, 0640);
+ if (fd >= 0) {
+ const std::string& buffer = data.str();
+ if (write(fd, buffer.c_str(), buffer.length()) == (ssize_t) buffer.length()) success = true;
+ close(fd);
+ }
+ return success;
+}
+
+template <typename K, typename V> std::ostream& operator<<(std::ostream& out, const std::map<K,V>& container)
+{
+ for (const auto& item : container) out << item.first << '|' << item.second << '\t'; // write into storage
+ return out << '\n';
+}
+
+template <typename T> bool fromString(const std::string& from, T& to)
+{
+ return bool(std::istringstream(from) >> to);
+}
+
+template <> bool fromString(const std::string& from, std::string& to)
+{
+ to = from;
+ return true;
+}
+
+template <typename K, typename V> std::istream& operator>>(std::istream& in, std::map<K,V>& container)
+{
+ while (!in.eof() && !in.fail() && (in.peek() != '\n')) { // read from storage
+ std::string skey;
+ if (std::getline(in, skey, '|')) {
+ K key;
+ V value;
+ if (fromString(skey, key) && (in >> value)) {
+ container.insert( { key, value } );
+ in.ignore(); // tab
+ }
+ }
+ }
+ return in.ignore();
+}
+
+struct CPU {
+ typedef int16_t Number; // -1 for all cores summary and 0,1,2,... for each core
+
+ struct Core {
+ int64_t user, nice, system, idle, iowait, irq, softirq, steal, guest, guestnice;
+ uint64_t freq_hz;
+ int64_t cpuUsed() const
+ {
+ return user + nice + system + irq + softirq + guest + guestnice;
+ }
+ int64_t cpuTotal() const
+ {
+ return cpuUsed() + idle + iowait + steal;
+ }
+ Core operator-(const Core& that) const
+ {
+ int64_t roundUserFix = user == that.user-1? 1 : 0; // compensate for physical host & virtual
+ int64_t roundNiceFix = nice == that.nice-1? 1 : 0; // guests counters evil rounding
+ return {
+ user - that.user + roundUserFix, nice - that.nice + roundNiceFix, system - that.system,
+ idle - that.idle, iowait - that.iowait, irq - that.irq, softirq - that.softirq,
+ steal - that.steal, guest - that.guest + roundUserFix, guestnice - that.guestnice + roundNiceFix,
+ (freq_hz + that.freq_hz) / 2 // <-- a more useful abstraction
+ };
+ }
+ };
+
+ std::map<Number, Core> cores;
+
+ void readProc()
+ {
+ std::vector<char> buffer;
+ readFile("/proc/stat", buffer);
+ std::istringstream cpustat(&buffer[0]);
+ std::string name;
+ Number number;
+ while (cpustat >> name) {
+ if (name.find("cpu") == 0) {
+ name.erase(0,3);
+ if (name.empty()) number = -1;
+ else if (!fromString(name, number)) continue;
+ Core& core = cores[number];
+ cpustat >> core.user >> core.nice >> core.system >> core.idle
+ >> core.iowait >> core.irq >> core.softirq;
+ if (cpustat.peek() != '\n') cpustat >> core.steal;
+ else core.steal = 0; // caution with old kernels
+ if (cpustat.peek() != '\n') cpustat >> core.guest;
+ else core.guest = 0;
+ if (cpustat.peek() != '\n') cpustat >> core.guestnice;
+ else core.guestnice = 0;
+ core.freq_hz = 0;
+ core.user -= core.guest; // adjust for the already accounted values (may cause evil rounding
+ core.nice -= core.guestnice; // effects, though)
+ }
+ cpustat.ignore(buffer.size(), '\n');
+ }
+ readFile("/proc/cpuinfo", buffer);
+ std::istringstream cpuinfo(&buffer[0]);
+ std::string key;
+ std::string skip;
+ uint64_t sum_freq = 0;
+ number = -1;
+ while (cpuinfo >> key) {
+ if (key == "processor") cpuinfo >> skip >> number;
+ else if ((key == "cpu") && (number >= 0)) {
+ if ((cpuinfo >> skip) && (skip == "MHz")) {
+ double mhz;
+ cpuinfo >> skip >> mhz;
+ cores[number].freq_hz = uint64_t(mhz * MB_i);
+ sum_freq += cores[number].freq_hz;
+ number = -1;
+ }
+ }
+ cpuinfo.ignore(buffer.size(), '\n');
+ }
+ auto allcpu = cores.find(-1);
+ if (allcpu != cores.end()) allcpu->second.freq_hz = sum_freq;
+ }
+};
+
+std::ostream& operator<<(std::ostream& out, const CPU::Core& core) // write into storage
+{
+ return out << core.user << ' ' << core.nice << ' ' << core.system << ' ' << core.idle << ' '
+ << core.iowait << ' ' << core.irq << ' ' << core.softirq << ' '
+ << core.steal << ' ' << core.guest << ' ' << core.guestnice << ' ' << core.freq_hz;
+}
+
+std::istream& operator>>(std::istream& in, CPU::Core& core) // read from storage
+{
+ return in >> core.user >> core.nice >> core.system >> core.idle >> core.iowait >> core.irq
+ >> core.softirq >> core.steal >> core.guest >> core.guestnice >> core.freq_hz;
+}
+
+struct Memory {
+ struct RAM {
+ uint64_t total;
+ uint64_t available;
+ uint64_t free;
+ uint64_t shared;
+ uint64_t buffers;
+ uint64_t cached;
+ uint64_t swapTotal;
+ uint64_t swapFree;
+ };
+
+ RAM ram;
+
+ void readProc()
+ {
+ std::vector<char> buffer;
+ readFile("/proc/meminfo", buffer);
+ std::istringstream meminfo(&buffer[0]);
+ bool hasAvailable = false;
+ std::string key;
+ for (int count = 0; meminfo >> key;) {
+ if (key == "MemTotal:") {
+ count++;
+ meminfo >> ram.total;
+ } else if (key == "MemFree:") {
+ count++;
+ meminfo >> ram.free;
+ } else if (key == "MemAvailable:") {
+ count++;
+ meminfo >> ram.available;
+ hasAvailable = true;
+ } else if (key == "Buffers:") {
+ count++;
+ meminfo >> ram.buffers;
+ } else if (key == "Cached:") {
+ count++;
+ meminfo >> ram.cached;
+ } else if (key == "SwapTotal:") {
+ count++;
+ meminfo >> ram.swapTotal;
+ } else if (key == "SwapFree:") {
+ count++;
+ meminfo >> ram.swapFree;
+ } else if (key == "Shmem:") {
+ count++;
+ meminfo >> ram.shared;
+ }
+ if (count == 7 + (hasAvailable? 1:0)) break;
+ meminfo.ignore(buffer.size(), '\n');
+ }
+ if (!hasAvailable) ram.available = ram.free + ram.buffers + ram.cached; // pre-2014 kernels
+ }
+};
+
+std::ostream& operator<<(std::ostream& out, const Memory::RAM& ram)
+{
+ return out << ram.total << ' ' << ram.available << ' ' << ram.free << ' ' << ram.shared << ' '
+ << ram.buffers << ' ' << ram.cached << ' ' << ram.swapTotal << ' ' << ram.swapFree << '\n';
+}
+
+std::istream& operator>>(std::istream& in, Memory::RAM& ram)
+{
+ return in >> ram.total >> ram.available >> ram.free >> ram.shared
+ >> ram.buffers >> ram.cached >> ram.swapTotal >> ram.swapFree;
+}
+
+struct Health {
+ typedef std::string Name;
+
+ struct Thermometer {
+ int32_t tempMilliCelsius;
+ };
+
+ std::map<Name, Thermometer> thermometers;
+
+ void readProc()
+ {
+ std::string coretemp;
+ for (int hwmon = 0; hwmon < 256; hwmon++) {
+ std::string base = VA_STR("/sys/class/hwmon/hwmon" << hwmon);
+ std::vector<char> buffer;
+ if (!readFile(VA_STR(base << "/name").c_str(), buffer, false)) { // pre-3.15 kernel?
+ base.append("/device");
+ if (!readFile(VA_STR(base << "/name").c_str(), buffer, false)) break;
+ }
+ std::istringstream sname(&buffer[0]);
+ std::string name;
+ if ((sname >> name) && (name == "coretemp")) {
+ coretemp = base;
+ break;
+ }
+ }
+
+ if (!coretemp.empty()) for (int ic = 1; ic < 64; ic++) {
+ std::vector<char> buffer;
+ if (!readFile(VA_STR(coretemp << "/temp" << ic << "_label").c_str(), buffer, false)) {
+ if (thermometers.empty()) continue;
+ else break; // Atom CPU may start at 2 (!?)
+ }
+ std::istringstream sname(&buffer[0]);
+ std::string name;
+ if (!std::getline(sname, name) || name.empty()) break;
+ if (!readFile(VA_STR(coretemp << "/temp" << ic << "_input").c_str(), buffer, false)) break;
+ std::istringstream temp(&buffer[0]);
+ int32_t tempMilliCelsius;
+ if (!(temp >> tempMilliCelsius)) break;
+ thermometers[name].tempMilliCelsius = tempMilliCelsius;
+ }
+ }
+};
+
+std::ostream& operator<<(std::ostream& out, const Health::Thermometer& thm)
+{
+ return out << thm.tempMilliCelsius;
+}
+
+std::istream& operator>>(std::istream& in, Health::Thermometer& thm)
+{
+ return in >> thm.tempMilliCelsius;
+}
+
+template <typename T> struct Padded {
+ uint64_t max;
+ T value;
+};
+
+template <typename T> std::ostream& operator<<(std::ostream& out, const Padded<T>& data)
+{
+ if (!std::isnan(data.value)) for (T div = data.max;; div /= 10) { // avoid infinite loop with NaN numbers
+ if ((data.value >= div) && (data.value >= 1)) break;
+ if ((data.value < 1) && (div <= 1)) break;
+ out << " "; // two spaces in place of each missing digit (same width in XFCE Generic Monitor applet)
+ }
+ return out << data.value;
+}
+
+std::string dumpReport()
+{
+ std::shared_ptr<CPU> new_CPU, old_CPU;
+ std::shared_ptr<Memory> new_Memory;
+ std::shared_ptr<Health> new_Health;
+
+ new_CPU.reset(new CPU());
+ new_Memory.reset(new Memory());
+ new_Health.reset(new Health());
+
+ timespec tp;
+ if (clock_gettime(CLOCK_MONOTONIC, &tp) != 0) abortApp("clock_gettime");
+ uint64_t nowIs = tp.tv_sec * GB_i + tp.tv_nsec;
+
+ std::ostringstream newState;
+ newState << APP_VERSION << " " << nowIs << "\n";
+ if (new_CPU) {
+ new_CPU->readProc();
+ newState << "CPU|" << new_CPU->cores;
+ }
+ if (new_Memory) {
+ new_Memory->readProc();
+ }
+ if (new_Health) {
+ new_Health->readProc();
+ }
+
+ for (int locTry = 0;; locTry++) { // read the previous state from disk and store the new state
+ std::vector<char> oldStateData;
+ std::ostringstream stateFileName;
+
+ if (stateFilePath.length())
+ stateFileName << stateFilePath;
+ else if (locTry == 0)
+ stateFileName << "/run/" APP_NAME "/" APP_NAME << ".dat";
+ else if (locTry == 1)
+ stateFileName << "/tmp/" APP_NAME "-" << getuid() << "/" APP_NAME << ".dat";
+ else
+ abortApp("can't write tmpfile");
+
+ auto stateDir = fs::path(stateFileName.str()).parent_path();
+
+ if(not fs::exists(stateDir)) {
+ try {
+ fs::create_directories(stateDir);
+ } catch (const std::exception& e) {
+ std::cerr << e.what() << std::endl;
+ }
+ }
+
+ if (readFile(stateFileName.str().c_str(), oldStateData, false)) {
+ std::istringstream oldState(&oldStateData[0]);
+ std::string version;
+ uint64_t previouslyWas;
+ oldState >> version >> previouslyWas;
+ oldState.ignore(oldStateData.size(), '\n');
+ if (version == APP_VERSION) {
+ std::string category;
+ while (std::getline(oldState, category, '|')) {
+ if (category == "CPU") {
+ old_CPU.reset(new CPU());
+ oldState >> old_CPU->cores;
+ } else oldState.ignore(oldStateData.size(), '\n');
+ }
+ }
+ }
+
+ if (writeFile(stateFileName.str().c_str(), newState)) {
+ if(not stateFilePath.length()) stateFilePath = stateFileName.str();
+ break;
+ }
+ }
+
+ std::ostringstream report;
+
+ if (new_CPU && old_CPU) { // CPU report
+ struct CpuStat {
+ CPU::Number number;
+ double percent;
+ double ghz;
+ };
+ std::multimap<double, CpuStat> rankByGhzUsage;
+ double cum_weighted_ghz = 0;
+ for (auto itn = new_CPU->cores.cbegin(); itn != new_CPU->cores.cend(); ++itn) {
+ if (itn->first < 0) continue;
+ auto ito = old_CPU->cores.find(itn->first);
+ if (ito == old_CPU->cores.end()) continue;
+ CPU::Core diff = itn->second - ito->second;
+ if (diff.cpuTotal() == 0) continue;
+ double unityUsage = 1.0 * diff.cpuUsed() / diff.cpuTotal();
+ double ghz = diff.freq_hz / GB_f;
+ double ghzUsage = ghz * unityUsage;
+ cum_weighted_ghz += ghzUsage;
+ rankByGhzUsage.insert({ ghzUsage, CpuStat { itn->first, 100.0 * unityUsage, ghz } });
+ }
+
+ auto allnew = new_CPU->cores.find(-1);
+ auto allold = old_CPU->cores.find(-1);
+ if ((allnew != new_CPU->cores.end()) && (allold != old_CPU->cores.end())) {
+ const CPU::Core& ncpu = allnew->second;
+ const CPU::Core& ocpu = allold->second;
+ CPU::Core diff = ncpu - ocpu;
+ auto cpuTotal = diff.cpuTotal();
+ if (cpuTotal > 0) {
+ auto cpuTotalSinceBoot = ncpu.cpuTotal();
+ double usagePercent = 100.0 * diff.cpuUsed() / cpuTotal;
+
+ auto dumpPercent = [&](const char* title, int64_t user_hz, int64_t user_hz__sinceBoot) {
+ report << "cpu\t" << title << "%\t"
+ << 100.0 * user_hz / cpuTotal << "\t"
+ << 100.0 * user_hz__sinceBoot / cpuTotalSinceBoot << std::endl;
+ };
+
+ report << "cpu\tbase\t" << std::fixed
+ << std::setprecision(2) << usagePercent
+ << "\t" << uint64_t(cum_weighted_ghz * 1000)
+ << std::setprecision(2) << std::endl;
+
+ dumpPercent("user", diff.user, ncpu.user);
+ dumpPercent("nice", diff.nice, ncpu.nice);
+ dumpPercent("system", diff.system, ncpu.system);
+ dumpPercent("idle", diff.idle, ncpu.idle);
+ dumpPercent("iowait", diff.iowait, ncpu.iowait);
+ if (ncpu.irq) dumpPercent("irq", diff.irq, ncpu.irq);
+ if (ncpu.softirq) dumpPercent("softirq", diff.softirq, ncpu.softirq);
+ if (ncpu.steal) dumpPercent("steal", diff.steal, ncpu.steal);
+ if (ncpu.guest) dumpPercent("guest", diff.guest, ncpu.guest);
+ if (ncpu.guestnice) dumpPercent("guestnice", diff.guestnice, ncpu.guestnice);
+
+ int maxCpu = 8;
+ for (auto itc = rankByGhzUsage.crbegin(); maxCpu-- && (itc != rankByGhzUsage.crend()); ++itc) {
+ report << "cpu\tcore:" << itc->second.number << "\t"
+ << std::fixed << std::setprecision(2)
+ << itc->second.percent << "\t"
+ << std::setprecision(3) << uint64_t(itc->second.ghz * 1000)
+ << std::endl;
+ }
+ }
+ }
+ }
+
+ if (new_Memory) { // RAM report
+ if (new_CPU)
+ report << "mem\t" << new_Memory->ram.total
+ << "\tavail:" << new_Memory->ram.available
+ << "\tcache:" << new_Memory->ram.cached + new_Memory->ram.buffers;
+
+ if (new_Memory->ram.shared)
+ report << "\tshare:" << new_Memory->ram.shared;
+
+ if (new_Memory->ram.swapTotal)
+ report << "\tswap:" << new_Memory->ram.swapTotal - new_Memory->ram.swapFree << "/"
+ << new_Memory->ram.swapTotal;
+ report << std::endl;
+ }
+
+ if (new_Health) { // TEMP report
+ struct ThermalStat {
+ ThermalStat() : min(std::numeric_limits<int32_t>::max()),
+ max(std::numeric_limits<int32_t>::min()),
+ avg(0), count(0)
+ {}
+ int32_t min;
+ int32_t max;
+ int32_t avg;
+ std::size_t count;
+ Health::Name firstName;
+ };
+
+ std::map<std::string, ThermalStat> statByCategory;
+ int32_t maxAbsTemp = std::numeric_limits<int32_t>::min();
+ for (const auto& itt : new_Health->thermometers) {
+ std::string key = itt.first;
+ auto catEnd = key.find(" ");
+ if (catEnd != std::string::npos) key.erase(catEnd);
+ auto its = statByCategory.find(key);
+ if (its == statByCategory.end()) {
+ its = statByCategory.insert( { key, ThermalStat() } ).first;
+ its->second.firstName = itt.first;
+ }
+ if (itt.second.tempMilliCelsius < its->second.min) its->second.min = itt.second.tempMilliCelsius;
+ if (itt.second.tempMilliCelsius > its->second.max) its->second.max = itt.second.tempMilliCelsius;
+ if (itt.second.tempMilliCelsius > maxAbsTemp) maxAbsTemp = itt.second.tempMilliCelsius;
+ auto prevTotal = its->second.avg * its->second.count;
+ its->second.count++;
+ its->second.avg = (prevTotal + itt.second.tempMilliCelsius) / its->second.count;
+ }
+
+ for (auto its = statByCategory.crbegin(); its != statByCategory.crend(); ++its) {
+ if (its->second.count == 1)
+ report << "tmp\t" << its->second.firstName << "\tcur:" << its->second.max / 1000 << "\t";
+ else
+ report << "max:" << its->second.max / 1000
+ << "\tmin:" << its->second.min / 1000
+ << "\tavg:" << its->second.avg / 1000
+ << std::endl;
+ }
+ }
+ return report.str();
+}