diff options
Diffstat (limited to 'src/system-stats/report.cpp')
-rw-r--r-- | src/system-stats/report.cpp | 545 |
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(); +} |