Skip to content

Commit 5f3f35c

Browse files
cmgracefacebook-github-bot
authored andcommitted
Add CPU breakdown min/max aggregations and cpu.nice metric for allotment ODS (#554)
Summary: Extends the allotment-level CPU metrics to support MySQL and ZippyDB stacking migration. This is part of the broader effort to expose allotment-level ODS metrics as these workloads move from whole-host shapes to stacked allotments. Changes: - Add cpu.nice breakdown metric from /proc/stat per-CPU nice field (not available in cgroup v2 cpu.stat) via CPUTimeMonitor, with avg/min/max aggregations - Add min.60/max.60 aggregations for cpu.iowait and cpu.saturation-pct from CPUTimeMonitor /proc/stat data - Add min.60/max.60 aggregations for cpu.user and cpu.sys from the cgroup ring buffer (AllotmentsMonitor::getCgroupCounterMinMax), keeping the existing cgroup data source for consistency with the avg metric - Add kCpuSaturationMin/Max key specs to AllotmentCpuOdsKeySpecs - Extract granularityToLevel() helper to deduplicate 6 identical switch blocks in CPUTimeMonitor - Fix readCgroupStat_ fallback in getMinCPUCoresUsage/getMaxCPUCoresUsage to match existing getStat() behavior All new ODS keys are gated behind TargetedOdsFilter::shouldPublish() with defaultPublish=false, so they only emit when an explicit allow rule is configured for the workload. New ODS keys (all require TargetedOdsFilter config to publish): - dyno.allotment.cpu.nice (avg via ODS suffix) - dyno.allotment.cpu.{nice,iowait,idle,softirq,hardirq}.min.60 / .max.60 - dyno.allotment.cpu.user.min.60 / .max.60 - dyno.allotment.cpu.sys.min.60 / .max.60 - dyno.allotment.cpu.saturation-pct.min.60 / .max.60 Design doc: https://docs.google.com/document/d/16ea49QzoL60PTtFQ0DXss4Bmq7aCnSGh4MuG9phMmO0/edit Differential Revision: D104745222
1 parent 1fe6a0b commit 5f3f35c

3 files changed

Lines changed: 171 additions & 17 deletions

File tree

dynolog/src/CPUTimeMonitor.cpp

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "dynolog/src/CPUTimeMonitor.h"
99
#include <dynolog/src/metric_frame/MetricFrameTsUnit.h>
1010

11+
#include <algorithm>
1112
#include <utility>
1213

1314
enum { IDX_MIN = 0, IDX_SEC = 1, IDX_HUNDRED_MS = 2 };
@@ -19,14 +20,16 @@ static const std::string kBreakdownIdle = ".idle";
1920
static const std::string kBreakdownSoftirq = ".softirq";
2021
static const std::string kBreakdownIowait = ".iowait";
2122
static const std::string kBreakdownHardirq = ".hardirq";
23+
static const std::string kBreakdownNice = ".nice";
2224

2325
// All breakdown suffixes in a single list to avoid iterating in multiple
2426
// places.
2527
static const std::vector<std::string> kBreakdownSuffixes = {
2628
kBreakdownIdle,
2729
kBreakdownSoftirq,
2830
kBreakdownIowait,
29-
kBreakdownHardirq};
31+
kBreakdownHardirq,
32+
kBreakdownNice};
3033

3134
// Helper to add breakdown series for a target to a MetricFrameMap.
3235
static void addBreakdownSeries(
@@ -51,13 +54,27 @@ static const std::string& breakdownSuffix(
5154
return kBreakdownIowait;
5255
case CPUTimeMonitor::CpuBreakdown::HARDIRQ:
5356
return kBreakdownHardirq;
57+
case CPUTimeMonitor::CpuBreakdown::NICE:
58+
return kBreakdownNice;
5459
}
5560
return kBreakdownIdle; // unreachable
5661
}
5762

5863
constexpr int kRingbufferSizeMinutes = 6;
5964
static const std::string kHostCgroupPath = "/sys/fs/cgroup";
6065

66+
static std::optional<int> granularityToLevel(CPUTimeMonitor::Granularity gran) {
67+
switch (gran) {
68+
case CPUTimeMonitor::Granularity::MINUTE:
69+
return IDX_MIN;
70+
case CPUTimeMonitor::Granularity::SECOND:
71+
return IDX_SEC;
72+
case CPUTimeMonitor::Granularity::HUNDRED_MS:
73+
return IDX_HUNDRED_MS;
74+
}
75+
return std::nullopt;
76+
}
77+
6178
std::array<MetricFrameMap, 3> CPUTimeMonitor::createMetricFrameArray() {
6279
return {
6380
MetricFrameMap(
@@ -194,20 +211,11 @@ std::optional<double> CPUTimeMonitor::getCpuBreakdownAvg(
194211
TimePoint now = std::chrono::steady_clock::now();
195212
std::shared_lock lock(dataLock_);
196213

197-
int level = 0;
198-
switch (gran) {
199-
case Granularity::MINUTE:
200-
level = IDX_MIN;
201-
break;
202-
case Granularity::SECOND:
203-
level = IDX_SEC;
204-
break;
205-
case Granularity::HUNDRED_MS:
206-
level = IDX_HUNDRED_MS;
207-
break;
208-
default:
209-
return std::nullopt;
214+
auto levelOpt = granularityToLevel(gran);
215+
if (!levelOpt.has_value()) {
216+
return std::nullopt;
210217
}
218+
int level = *levelOpt;
211219

212220
auto& frame = procUsageMetricFrames_[level];
213221
auto slice = frame.slice(now - std::chrono::seconds(seconds_ago), now);
@@ -248,6 +256,65 @@ const MetricFrameMap* CPUTimeMonitor::getMetricFrame(
248256
: &procUsageMetricFrames_[level];
249257
}
250258

259+
// Min/max for CPU breakdown metrics. Reads per-second samples from the
260+
// /proc/stat ring buffer and returns the min (or max) value over the window.
261+
// MetricSeries doesn't have built-in min/max, so we use raw() + std algorithms.
262+
std::optional<double> CPUTimeMonitor::getCpuBreakdownMin(
263+
Granularity gran,
264+
uint64_t seconds_ago,
265+
CpuBreakdown breakdown,
266+
const std::optional<std::string>& targetId) {
267+
TimePoint now = std::chrono::steady_clock::now();
268+
std::shared_lock lock(dataLock_);
269+
270+
auto levelOpt = granularityToLevel(gran);
271+
if (!levelOpt.has_value()) {
272+
return std::nullopt;
273+
}
274+
int level = *levelOpt;
275+
276+
auto& frame = procUsageMetricFrames_[level];
277+
auto slice = frame.slice(now - std::chrono::seconds(seconds_ago), now);
278+
if (slice == std::nullopt) {
279+
return std::nullopt;
280+
}
281+
std::string key = targetId.value_or("host") + breakdownSuffix(breakdown);
282+
auto series = slice->series<double>(key);
283+
if (series == std::nullopt || series->size() == 0) {
284+
return std::nullopt;
285+
}
286+
auto data = series->raw();
287+
return *std::min_element(data.begin(), data.end());
288+
}
289+
290+
std::optional<double> CPUTimeMonitor::getCpuBreakdownMax(
291+
Granularity gran,
292+
uint64_t seconds_ago,
293+
CpuBreakdown breakdown,
294+
const std::optional<std::string>& targetId) {
295+
TimePoint now = std::chrono::steady_clock::now();
296+
std::shared_lock lock(dataLock_);
297+
298+
auto levelOpt = granularityToLevel(gran);
299+
if (!levelOpt.has_value()) {
300+
return std::nullopt;
301+
}
302+
int level = *levelOpt;
303+
304+
auto& frame = procUsageMetricFrames_[level];
305+
auto slice = frame.slice(now - std::chrono::seconds(seconds_ago), now);
306+
if (slice == std::nullopt) {
307+
return std::nullopt;
308+
}
309+
std::string key = targetId.value_or("host") + breakdownSuffix(breakdown);
310+
auto series = slice->series<double>(key);
311+
if (series == std::nullopt || series->size() == 0) {
312+
return std::nullopt;
313+
}
314+
auto data = series->raw();
315+
return *std::max_element(data.begin(), data.end());
316+
}
317+
251318
void CPUTimeMonitor::tick(TMask mask) {
252319
TimePoint tickTime = std::chrono::steady_clock::now();
253320
bool readPerCore = false;
@@ -589,8 +656,11 @@ void CPUTimeMonitor::processProcUsage(
589656
auto softirqDeltaOpt = safeDelta(newCt.y, lastCt.y);
590657
auto iowaitDeltaOpt = safeDelta(newCt.w, lastCt.w);
591658
auto hardirqDeltaOpt = safeDelta(newCt.x, lastCt.x);
659+
// nice (CpuTime.n) from /proc/stat. Not available in cgroup v2 cpu.stat.
660+
auto niceDeltaOpt = safeDelta(newCt.n, lastCt.n);
592661

593-
if (idleDeltaOpt && softirqDeltaOpt && iowaitDeltaOpt && hardirqDeltaOpt) {
662+
if (idleDeltaOpt && softirqDeltaOpt && iowaitDeltaOpt && hardirqDeltaOpt &&
663+
niceDeltaOpt) {
594664
double idleCores =
595665
static_cast<double>(*idleDeltaOpt) * msPerJiffy / wallDelta;
596666
double softirqCores =
@@ -599,11 +669,14 @@ void CPUTimeMonitor::processProcUsage(
599669
static_cast<double>(*iowaitDeltaOpt) * msPerJiffy / wallDelta;
600670
double hardirqCores =
601671
static_cast<double>(*hardirqDeltaOpt) * msPerJiffy / wallDelta;
672+
double niceCores =
673+
static_cast<double>(*niceDeltaOpt) * msPerJiffy / wallDelta;
602674

603675
line.emplace_back(targetId + kBreakdownIdle, idleCores);
604676
line.emplace_back(targetId + kBreakdownSoftirq, softirqCores);
605677
line.emplace_back(targetId + kBreakdownIowait, iowaitCores);
606678
line.emplace_back(targetId + kBreakdownHardirq, hardirqCores);
679+
line.emplace_back(targetId + kBreakdownNice, niceCores);
607680
}
608681
}
609682
if (!line.empty()) {

dynolog/src/CPUTimeMonitor.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class CPUTimeMonitor : MonitorBase<Ticker<60000, 1000, 10, 3>> {
6060

6161
enum class Granularity { MINUTE, SECOND, HUNDRED_MS };
6262
enum class DataSource { PROC_STAT, CGROUP_STAT };
63-
enum class CpuBreakdown { IDLE, SOFTIRQ, IOWAIT, HARDIRQ };
63+
enum class CpuBreakdown { IDLE, SOFTIRQ, IOWAIT, HARDIRQ, NICE };
6464

6565
explicit CPUTimeMonitor(
6666
std::shared_ptr<TTicker> ticker,
@@ -97,14 +97,26 @@ class CPUTimeMonitor : MonitorBase<Ticker<60000, 1000, 10, 3>> {
9797
const std::optional<std::string>& targetId = std::nullopt,
9898
DataSource dataSource = DataSource::PROC_STAT);
9999

100-
// Get the average CPU breakdown value (idle, softirq, iowait, hardirq)
100+
// Get the average CPU breakdown value (idle, softirq, iowait, hardirq, nice)
101101
// for a target over the specified time window. Returns raw cores used.
102102
std::optional<double> getCpuBreakdownAvg(
103103
Granularity gran,
104104
uint64_t seconds_ago,
105105
CpuBreakdown breakdown,
106106
const std::optional<std::string>& targetId = std::nullopt);
107107

108+
std::optional<double> getCpuBreakdownMin(
109+
Granularity gran,
110+
uint64_t seconds_ago,
111+
CpuBreakdown breakdown,
112+
const std::optional<std::string>& targetId = std::nullopt);
113+
114+
std::optional<double> getCpuBreakdownMax(
115+
Granularity gran,
116+
uint64_t seconds_ago,
117+
CpuBreakdown breakdown,
118+
const std::optional<std::string>& targetId = std::nullopt);
119+
108120
private:
109121
// Reads CPU time data from /proc/stat
110122
// If read_per_core is true, reads per-core data in addition to the

dynolog/tests/CPUTimeMonitorTest.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,5 +1048,74 @@ TEST_F(CPUTimeMonitorTest, testBreakdownSeriesKeyPattern) {
10481048
monitor->deRegisterTarget("key_test");
10491049
}
10501050

1051+
TEST_F(CPUTimeMonitorTest, testNiceBreakdownType) {
1052+
monitor->registerTarget("target_a", {0, 1, 2, 3});
1053+
monitor->tick(major_tick_60s);
1054+
monitor->tick(major_tick_60s);
1055+
1056+
auto avg = monitor->getCpuBreakdownAvg(
1057+
CPUTimeMonitor::Granularity::MINUTE,
1058+
60,
1059+
CPUTimeMonitor::CpuBreakdown::NICE,
1060+
"target_a");
1061+
ASSERT_TRUE(avg.has_value());
1062+
EXPECT_DOUBLE_EQ(avg.value(), 0.0);
1063+
1064+
monitor->deRegisterTarget("target_a");
1065+
1066+
EXPECT_EQ(
1067+
monitor->getCpuBreakdownAvg(
1068+
CPUTimeMonitor::Granularity::MINUTE,
1069+
60,
1070+
CPUTimeMonitor::CpuBreakdown::NICE,
1071+
"target_a"),
1072+
std::nullopt);
1073+
}
1074+
1075+
TEST_F(CPUTimeMonitorTest, testBreakdownMinMax) {
1076+
monitor->registerTarget("minmax_test", {0, 1, 2, 3});
1077+
monitor->tick(major_tick_60s);
1078+
monitor->tick(major_tick_60s);
1079+
1080+
for (const auto& bd :
1081+
{CPUTimeMonitor::CpuBreakdown::IDLE,
1082+
CPUTimeMonitor::CpuBreakdown::SOFTIRQ,
1083+
CPUTimeMonitor::CpuBreakdown::IOWAIT,
1084+
CPUTimeMonitor::CpuBreakdown::HARDIRQ,
1085+
CPUTimeMonitor::CpuBreakdown::NICE}) {
1086+
auto minVal = monitor->getCpuBreakdownMin(
1087+
CPUTimeMonitor::Granularity::MINUTE, 60, bd, "minmax_test");
1088+
auto maxVal = monitor->getCpuBreakdownMax(
1089+
CPUTimeMonitor::Granularity::MINUTE, 60, bd, "minmax_test");
1090+
ASSERT_TRUE(minVal.has_value());
1091+
ASSERT_TRUE(maxVal.has_value());
1092+
EXPECT_LE(minVal.value(), maxVal.value());
1093+
}
1094+
1095+
monitor->deRegisterTarget("minmax_test");
1096+
}
1097+
1098+
TEST_F(CPUTimeMonitorTest, testBreakdownMinMaxNulloptAfterDeregister) {
1099+
monitor->registerTarget("dereg_test", {0, 1});
1100+
monitor->tick(major_tick_60s);
1101+
monitor->tick(major_tick_60s);
1102+
1103+
auto minBefore = monitor->getCpuBreakdownMin(
1104+
CPUTimeMonitor::Granularity::MINUTE,
1105+
60,
1106+
CPUTimeMonitor::CpuBreakdown::NICE,
1107+
"dereg_test");
1108+
ASSERT_TRUE(minBefore.has_value());
1109+
1110+
monitor->deRegisterTarget("dereg_test");
1111+
1112+
auto minAfter = monitor->getCpuBreakdownMin(
1113+
CPUTimeMonitor::Granularity::MINUTE,
1114+
60,
1115+
CPUTimeMonitor::CpuBreakdown::NICE,
1116+
"dereg_test");
1117+
EXPECT_EQ(minAfter, std::nullopt);
1118+
}
1119+
10511120
} // namespace dynolog
10521121
} // namespace facebook

0 commit comments

Comments
 (0)