From 29e5cd30ad6594c688d7b06d91a5caed37c4d4dd Mon Sep 17 00:00:00 2001 From: Jussi Kuokkanen Date: Fri, 13 Oct 2023 15:49:55 +0300 Subject: [PATCH] add CPU utilizations --- src/plugins/CPU.cpp | 172 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/src/plugins/CPU.cpp b/src/plugins/CPU.cpp index 36f1708..aa172ec 100644 --- a/src/plugins/CPU.cpp +++ b/src/plugins/CPU.cpp @@ -37,8 +37,47 @@ struct CPUData { uint firstCoreIndex; uint coreCount; std::string name; + uint cpuIndex; }; +// CPU utilization sample +struct CPUTimeStat { + uint64_t totalTime; + uint64_t idleTime; +}; + +uint utilizationPercentage(CPUTimeStat stat) { + auto idle = static_cast(stat.idleTime) / static_cast(stat.totalTime); + auto active = 1 - idle; + return std::round(active * 100); +} + +std::optional fromStatLine(const std::string &line) { + auto words = fplus::split(' ', true, line); + // Remove cpuX from the start + words.erase(words.begin()); + + if (words.size() < 4) + return std::nullopt; + + uint64_t total = 0; + for (auto &word : words) { + total += std::stoull(word); + } + return CPUTimeStat{ + .totalTime = total, + .idleTime = std::stoull(words[3]), + }; +} + +std::ifstream &jumptoLine(std::ifstream &file, unsigned int num) { + file.seekg(std::ios::beg); + for (int i = 0; i < num - 1; ++i) { + file.ignore(std::numeric_limits::max(), '\n'); + } + return file; +} + std::vector fromCPUInfoData(std::vector dataVec) { auto samePhysId = [](CPUInfoData a, CPUInfoData b) { return a.physicalId == b.physicalId; }; auto cpus = group_by(samePhysId, dataVec); @@ -56,6 +95,7 @@ std::vector fromCPUInfoData(std::vector dataVec) { .firstCoreIndex = firstCore, .coreCount = first.cores, .name = first.name, + .cpuIndex = first.physicalId, }; retval.push_back(data); } @@ -199,6 +239,77 @@ std::optional coretempReadable(const char *hwmonPath, uint inde return std::nullopt; } +std::vector readCPUStatsFromRange(uint minId, uint maxId) { + std::ifstream stat{"/proc/stat"}; + if (!stat.good()) + return {}; + + std::vector retval; + // cpu0 is on the second line + jumptoLine(stat, minId + 2); + // TODO: How did I end up with this :D at least debugger shows it works as intended + for (uint i = minId + 1; i < maxId + 2; i++) { + std::string line; + // Seeks to next line + std::getline(stat, line); + + auto timeStat = fromStatLine(line); + if (timeStat.has_value()) + retval.push_back(timeStat.value()); + else + return {}; + } + return retval; +} + +CPUTimeStat timeStatDelta(CPUTimeStat prev, CPUTimeStat cur) { + return CPUTimeStat{ + .totalTime = cur.totalTime - prev.totalTime, + .idleTime = cur.idleTime - prev.idleTime, + }; +} + +// Returns a list of readings from minId to maxId +// This is done in groups like this so we can avoid traversing the file again for every core +std::vector utilizationsFromRange(uint minId, uint maxId) { + // Saves the sample so we can compare to it on the next call + static std::unordered_map timeStatMap; + + auto newTimeStats = readCPUStatsFromRange(minId, maxId); + if (newTimeStats.empty()) + return {}; + + std::vector retval; + // Try to get previous reading from map + auto first = timeStatMap.find(minId); + if (first != timeStatMap.end()) { + // Found previous reading + for (uint i = 0; i < newTimeStats.size(); i++) { + try { + auto coreId = i + minId; + auto curStat = newTimeStats[i]; + auto prevStat = timeStatMap.at(coreId); + + auto deltaStat = timeStatDelta(prevStat, curStat); + retval.push_back(utilizationPercentage(deltaStat)); + // Save new value into hashmap + timeStatMap[coreId] = curStat; + } catch (std::out_of_range) { + return {}; + } + } + } else { + // Use overall utilization + for (uint i = 0; i < newTimeStats.size(); i++) { + auto coreId = i + minId; + // Save current stats so we can calculate delta next call + timeStatMap[coreId] = newTimeStats[i]; + retval.push_back(utilizationPercentage(newTimeStats[i])); + } + } + return retval; +} + std::vector> getCoretempTemperatures(CPUData data) { // Temperature nodes for Intel CPUs @@ -286,6 +397,56 @@ std::vector> getFreqs(CPUData data) { return retval; } +ReadResult utilizationBuffered(CPUData data, uint coreId) { + // NOTE: relies on the looping order in getUtilizations going from low to high + // Use cached results until we fetch for the first core id again + static std::unordered_map> cachedUtils; + + auto vectorIndex = coreId - data.firstCoreIndex; + auto lastId = data.firstCoreIndex + data.coreCount - 1; + if (cachedUtils.find(data.cpuIndex) != cachedUtils.end()) { + // Cache has value + auto utils = cachedUtils[data.cpuIndex]; + // Position relative to CPU + if (coreId == lastId) { + // Remove from map so we retrieve new value next time + cachedUtils.erase(data.cpuIndex); + } + return utils[vectorIndex]; + } + + auto utils = utilizationsFromRange(data.firstCoreIndex, lastId); + if (utils.empty()) + return ReadError::UnknownError; + + cachedUtils.insert({data.cpuIndex, utils}); + return utils[vectorIndex]; +} + +std::vector> getUtilizations(CPUData data) { + std::vector> retval; + for (uint i = data.firstCoreIndex; i < data.firstCoreIndex + data.coreCount; i++) { + auto func = [=]() -> ReadResult { return utilizationBuffered(data, i); }; + + if (hasReadableValue(func())) { + char idStr[64]; + char name[32]; + snprintf(idStr, 64, "%sCore%uUtilization", data.identifier.c_str(), i); + snprintf(name, 32, "%s %u", _("Core"), i); + + DynamicReadable dr{func, _("%")}; + + DeviceNode node{ + .name = name, + .interface = dr, + .hash = md5(idStr), + }; + retval.push_back(node); + } + }; + return retval; +} + std::vector> getFreqsRoot(CPUData data) { return {DeviceNode{ .name = _("Frequencies"), @@ -294,6 +455,14 @@ std::vector> getFreqsRoot(CPUData data) { }}; } +std::vector> getUtilizationsRoot(CPUData data) { + return {DeviceNode{ + .name = _("Utilizations"), + .interface = std::nullopt, + .hash = md5(data.identifier + "Utilizations"), + }}; +} + std::vector> getTemperaturesRoot(CPUData data) { return {DeviceNode{ .name = _("Temperatures"), @@ -323,6 +492,9 @@ auto cpuTree = TreeConstructor{ }}, {getTemperaturesRoot, { {getCoretempTemperatures, {}}, + }}, + {getUtilizationsRoot, { + {getUtilizations, {}} }} } };