Skip to content

Commit 96afdef

Browse files
sawenzelclaude
andauthored
Account for CPU used by child processes (#378)
* Account for CPU used by child processes getCpuAndContexts() only sampled RUSAGE_SELF, so CPU burned by forked children was never reported. Sample RUSAGE_CHILDREN as well. The final measurement is reachable via finalizeProcessMonitoring() instead of only ~Monitoring(), and bypasses the 1s rate guard, which would otherwise discard a delta that no later call can pick up. Details: * RUSAGE_CHILDREN only becomes non-zero once a child has been reaped, so this is one half of the fix: the caller has to reap the child, and has to do so without killing an intermediate shell first. See the companion change in AliceO2Group/AliceO2#15636. * Forced (final) measurements are deliberately excluded from the percentage series. A reaped child's CPU becomes visible as one lump, and lump / (time since the last sample) is a meaningless rate - 14315% was observed before this exclusion. Only the absolute and accumulated fields carry meaning for such a sample, so consumers that care about external subprocesses (Hyperloop accounting) should read cpuTimeConsumedByProcess, not the percentage series. * Consequently a process that ends before the first periodic sample has no percentage at all - pushLoop() sleeps 100ms before sampling - and averaging over the empty series produced a NaN averageCpuUsedPercentage. Verified with a Monitoring instance destroyed after 10ms: 'nan' before, '0.14' on the unpatched library. Such a measurement now reports no average instead of a NaN one. * init() clears the aggregates, so monitoring that is stopped and started again reports the new period rather than blending it with the previous one. DPL devices go RUNNING -> READY -> RUNNING across runs and re-arm process monitoring on start. Related to https://its.cern.ch/jira/browse/O2-7096 Assisted by Claude Opus 5 * Account for children in the context switches too The previous commit made the CPU numbers the sum over this process and its reaped children, but left the two context-switch counts on RUSAGE_SELF alone, so a single measurement mixed a process-tree quantity with a parent-only one. Both counts now sum the same pair of snapshots. Raised in review. The surrounding measurement code is tidied while we are here: the CPU delta is expressed in the same shape as the context-switch deltas, and the percentage is computed only on the path that actually reports one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 30281f8 commit 96afdef

4 files changed

Lines changed: 67 additions & 20 deletions

File tree

include/Monitoring/Monitoring.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ class Monitoring
7373
/// \param enabledMeasurements vector of monitor measurements, eg. PmMeasurement::Cpu
7474
void enableProcessMonitoring(const unsigned int interval = 5, std::vector<PmMeasurement> enabledMeasurements = {PmMeasurement::Cpu, PmMeasurement::Mem, PmMeasurement::Smaps});
7575

76+
/// Stops process monitoring and transmits the final measurement. Idempotent;
77+
/// call it explicitly where destructor timing is not guaranteed, e.g. on a
78+
/// DPL device's RUNNING->READY transition.
79+
void finalizeProcessMonitoring();
80+
7681
/// Flushes metric buffer (this can also happen when buffer is full)
7782
void flushBuffer();
7883

include/Monitoring/ProcessMonitor.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ class ProcessMonitor
104104
/// 'getrusage' values from last execution
105105
struct rusage mPreviousGetrUsage;
106106

107+
/// 'getrusage(RUSAGE_CHILDREN)' values from last execution
108+
struct rusage mPreviousGetrUsageChildren;
109+
107110
/// Retired-instructions hardware counter (perf_event_open, Linux only);
108111
/// -1 when unavailable (high perf_event_paranoid, container seccomp, or no PMU).
109112
int mInstructionsFd = -1;
@@ -128,7 +131,9 @@ class ProcessMonitor
128131
std::vector<Metric> getSmaps();
129132

130133
/// Retrieves CPU usage (%) and number of context switches during the interval
131-
std::vector<Metric> getCpuAndContexts();
134+
/// \param force ignore the 1s minimum interval and report no percentage;
135+
/// for the final measurement, whose delta no later call would pick up
136+
std::vector<Metric> getCpuAndContexts(bool force = false);
132137

133138
std::vector<Metric> makeLastMeasurementAndGetMetrics();
134139
};

src/Monitoring.cxx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,21 @@ void Monitoring::addBackend(std::unique_ptr<Backend> backend)
130130
mBackends.push_back(std::move(backend));
131131
}
132132

133-
Monitoring::~Monitoring()
133+
void Monitoring::finalizeProcessMonitoring()
134134
{
135+
if (!mMonitorRunning) {
136+
return;
137+
}
135138
mMonitorRunning = false;
136139
if (mMonitorThread.joinable()) {
137140
mMonitorThread.join();
138141
transmit(mProcessMonitor->makeLastMeasurementAndGetMetrics());
139142
}
143+
}
144+
145+
Monitoring::~Monitoring()
146+
{
147+
finalizeProcessMonitoring();
140148
flushBuffer();
141149
}
142150

src/ProcessMonitor.cxx

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ ProcessMonitor::ProcessMonitor()
6060
mPid = static_cast<unsigned int>(::getpid());
6161
mTimeLastRun = std::chrono::high_resolution_clock::now();
6262
getrusage(RUSAGE_SELF, &mPreviousGetrUsage);
63+
getrusage(RUSAGE_CHILDREN, &mPreviousGetrUsageChildren);
6364
#ifdef O2_MONITORING_OS_LINUX
6465
setTotalMemory();
6566
#endif
@@ -99,6 +100,13 @@ void ProcessMonitor::init()
99100
{
100101
mTimeLastRun = std::chrono::high_resolution_clock::now();
101102
getrusage(RUSAGE_SELF, &mPreviousGetrUsage);
103+
getrusage(RUSAGE_CHILDREN, &mPreviousGetrUsageChildren);
104+
// The aggregates cover one monitoring period: monitoring stopped and started
105+
// again reports the new period, not both blended together.
106+
mCpuPerctange.clear();
107+
mCpuMicroSeconds.clear();
108+
mVmSizeMeasurements.clear();
109+
mVmRssMeasurements.clear();
102110
}
103111

104112
void ProcessMonitor::enable(PmMeasurement measurement)
@@ -167,31 +175,45 @@ std::vector<Metric> ProcessMonitor::getSmaps()
167175
return {{pssTotal, metricsNames[PSS]}, {cleanTotal, metricsNames[PRIVATE_CLEAN]}, {dirtyTotal, metricsNames[PRIVATE_DIRTY]}};
168176
}
169177

170-
std::vector<Metric> ProcessMonitor::getCpuAndContexts()
178+
std::vector<Metric> ProcessMonitor::getCpuAndContexts(bool force)
171179
{
172180
std::vector<Metric> metrics;
181+
// RUSAGE_SELF does not see work done by reaped children (e.g. an external
182+
// event generator forked by o2-sim), so every counter below sums the two.
173183
struct rusage currentUsage;
184+
struct rusage currentUsageChildren;
174185
getrusage(RUSAGE_SELF, &currentUsage);
186+
getrusage(RUSAGE_CHILDREN, &currentUsageChildren);
175187
auto timeNow = std::chrono::high_resolution_clock::now();
176188
double timePassed = std::chrono::duration_cast<std::chrono::microseconds>(timeNow - mTimeLastRun).count();
177-
if (timePassed < 950) {
189+
if (timePassed < 950 && !force) {
178190
MonLogger::Get(Severity::Warn) << "Do not invoke Process Monitor more frequent then every 1s" << MonLogger::End();
179191
metrics.emplace_back("processPerformance");
180192
return metrics;
181193
}
182194

183-
uint64_t cpuUsedInMicroSeconds = currentUsage.ru_utime.tv_sec * 1000000.0 + currentUsage.ru_utime.tv_usec - (mPreviousGetrUsage.ru_utime.tv_sec * 1000000.0 + mPreviousGetrUsage.ru_utime.tv_usec) + currentUsage.ru_stime.tv_sec * 1000000.0 + currentUsage.ru_stime.tv_usec - (mPreviousGetrUsage.ru_stime.tv_sec * 1000000.0 + mPreviousGetrUsage.ru_stime.tv_usec);
184-
double fractionCpuUsed = cpuUsedInMicroSeconds / timePassed;
185-
186-
double cpuUsedPerctange = std::round(fractionCpuUsed * 100.0 * 100.0) / 100.0;
187-
mCpuPerctange.push_back(cpuUsedPerctange);
195+
// CPU time (user + system) of one snapshot, in microseconds
196+
auto cpuMicros = [](const struct rusage& usage) {
197+
return (usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) * 1000000.0 + usage.ru_utime.tv_usec + usage.ru_stime.tv_usec;
198+
};
199+
uint64_t cpuUsedInMicroSeconds = (cpuMicros(currentUsage) - cpuMicros(mPreviousGetrUsage)) +
200+
(cpuMicros(currentUsageChildren) - cpuMicros(mPreviousGetrUsageChildren));
188201
mCpuMicroSeconds.push_back(cpuUsedInMicroSeconds);
189202

190-
metrics.emplace_back(Metric{cpuUsedPerctange, metricsNames[CPU_USED_PERCENTAGE]});
191-
metrics.emplace_back(Metric{
192-
static_cast<uint64_t>(currentUsage.ru_nivcsw - mPreviousGetrUsage.ru_nivcsw), metricsNames[INVOLUNTARY_CONTEXT_SWITCHES]});
193-
metrics.emplace_back(Metric{
194-
static_cast<uint64_t>(currentUsage.ru_nvcsw - mPreviousGetrUsage.ru_nvcsw), metricsNames[VOLUNTARY_CONTEXT_SWITCHES]});
203+
// A child's CPU time appears all at once when it is reaped, so the delta of a
204+
// forced (final) measurement is not a rate over the interval: absolute time only.
205+
if (!force) {
206+
double fractionCpuUsed = cpuUsedInMicroSeconds / timePassed;
207+
double cpuUsedPerctange = std::round(fractionCpuUsed * 100.0 * 100.0) / 100.0;
208+
mCpuPerctange.push_back(cpuUsedPerctange);
209+
metrics.emplace_back(Metric{cpuUsedPerctange, metricsNames[CPU_USED_PERCENTAGE]});
210+
}
211+
uint64_t involuntaryContextSwitches = (currentUsage.ru_nivcsw - mPreviousGetrUsage.ru_nivcsw) +
212+
(currentUsageChildren.ru_nivcsw - mPreviousGetrUsageChildren.ru_nivcsw);
213+
uint64_t voluntaryContextSwitches = (currentUsage.ru_nvcsw - mPreviousGetrUsage.ru_nvcsw) +
214+
(currentUsageChildren.ru_nvcsw - mPreviousGetrUsageChildren.ru_nvcsw);
215+
metrics.emplace_back(Metric{involuntaryContextSwitches, metricsNames[INVOLUNTARY_CONTEXT_SWITCHES]});
216+
metrics.emplace_back(Metric{voluntaryContextSwitches, metricsNames[VOLUNTARY_CONTEXT_SWITCHES]});
195217
metrics.emplace_back(cpuUsedInMicroSeconds, metricsNames[CPU_USED_ABSOLUTE]);
196218

197219
#ifdef O2_MONITORING_OS_LINUX
@@ -212,6 +234,7 @@ std::vector<Metric> ProcessMonitor::getCpuAndContexts()
212234

213235
mTimeLastRun = timeNow;
214236
mPreviousGetrUsage = currentUsage;
237+
mPreviousGetrUsageChildren = currentUsageChildren;
215238
return metrics;
216239
}
217240

@@ -262,14 +285,20 @@ std::vector<Metric> ProcessMonitor::makeLastMeasurementAndGetMetrics()
262285
}
263286
#endif
264287
if (mEnabledMeasurements.at(static_cast<short>(PmMeasurement::Cpu))) {
265-
getCpuAndContexts();
266-
267-
auto avgCpuUsage = std::accumulate(mCpuPerctange.begin(), mCpuPerctange.end(), 0.0) /
268-
mCpuPerctange.size();
288+
// forced: no later call would pick up a delta the rate guard discards here
289+
auto lastCpuMetrics = getCpuAndContexts(true);
290+
std::move(lastCpuMetrics.begin(), lastCpuMetrics.end(), std::back_inserter(metrics));
291+
292+
// A process that ends before the first periodic measurement has no
293+
// percentages at all (the forced one contributes none), and averaging an
294+
// empty vector would give NaN.
295+
if (!mCpuPerctange.empty()) {
296+
auto avgCpuUsage = std::accumulate(mCpuPerctange.begin(), mCpuPerctange.end(), 0.0) /
297+
mCpuPerctange.size();
298+
metrics.emplace_back(avgCpuUsage, metricsNames[AVG_CPU_USED_PERCENTAGE]);
299+
}
269300
uint64_t accumulationOfCpuTimeConsumption = std::accumulate(mCpuMicroSeconds.begin(),
270301
mCpuMicroSeconds.end(), 0UL);
271-
272-
metrics.emplace_back(avgCpuUsage, metricsNames[AVG_CPU_USED_PERCENTAGE]);
273302
metrics.emplace_back(accumulationOfCpuTimeConsumption, metricsNames[ACCUMULATED_CPU_TIME]);
274303
}
275304
return metrics;

0 commit comments

Comments
 (0)