Skip to content

Commit fd89e6d

Browse files
committed
Fix CPU accounting for external generator subprocesses
GeneratorFileOrCmd forks an external generator but the child was only reaped from ~GeneratorHepMC(), which is not reached on the DPL device teardown path. Its CPU therefore never showed up in RUSAGE_CHILDREN. Generators: add Generator::stop(), called from GeneratorTask::run() once the stream ends, to reap the child deterministically. Framework: add a Monitoring .stop callback to take the final CPU measurement on the RUNNING -> READY transition. This also covers devices that end their own stream via readyToQuit(), for which postEOS is never invoked. Details: * Reaping alone is not enough. terminateCmd() SIGKILLs the process group, and a generator started through a wrapper script - epos.sh, and any command line that /bin/sh does not exec in place - is a grandchild whose CPU reaches us only through the intermediate shell's own reap. Killing that shell first loses the accounting completely, which made the fix a race that usually lost. terminateCmd() now waits a bounded time for the command to exit by itself before escalating to SIGKILL, and GeneratorHepMC::stop() closes the read end of the pipe first, so a generator still blocked writing to it sees EPIPE and exits promptly instead of sitting out the grace period. Measured with a Pythia8 -> HepMC3 external command (same sh -> binary -> fifo topology as EPOS4, 3.5s of child CPU, ground truth from the child's own getrusage): cpuTimeConsumedByProcess was correct in 3 of 10 runs before and in 10 of 10 after, with no change in wall time. Same result when the generator produces more events than the device consumes (0.18s reported before, 3.62s after). * GeneratorTParticle spawns a command the same way but never called terminateCmd() at all, leaking the process as well as its CPU. It now overrides stop() too. * The .stop callback stops the sampling thread, while process monitoring is armed only once, at device instantiation. Without a counterpart a device would report nothing at all from its second run onwards (reproduced: no periodic samples and no aggregates after the first stop), so .start re-arms it. That call is a no-op while monitoring is already running. * Requires AliceO2Group/Monitoring#378, which introduces finalizeProcessMonitoring() and the RUSAGE_CHILDREN sampling. That has to be merged, tagged and pinned in alidist before this one, or dev stops compiling. Fixes https://its.cern.ch/jira/browse/O2-7096 Assisted by Claude Opus 5
1 parent 94d9198 commit fd89e6d

11 files changed

Lines changed: 115 additions & 9 deletions

Framework/Core/src/CommonServices.cxx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@
5454
#include "DecongestionService.h"
5555
#include "ArrowSupport.h"
5656
#include "DPLMonitoringBackend.h"
57+
#include "ResourcesMonitoringHelper.h"
5758
#include "Headers/STFHeader.h"
5859
#include "Headers/DataHeader.h"
5960

6061
#include <Configuration/ConfigurationInterface.h>
6162
#include <Configuration/ConfigurationFactory.h>
6263
#include <Monitoring/MonitoringFactory.h>
64+
#include <Monitoring/ProcessMonitor.h>
6365
#include "Framework/Signpost.h"
6466

6567
#include <fairmq/Device.h>
@@ -119,6 +121,15 @@ o2::framework::ServiceSpec CommonServices::monitoringSpec()
119121
.start = [](ServiceRegistryRef services, void* service) {
120122
auto* monitoring = (o2::monitoring::Monitoring*)service;
121123

124+
// Re-arm process monitoring: .stop takes the final measurement and stops
125+
// the sampling thread, so without this a device would report nothing at
126+
// all from its second run onwards. A no-op while already running.
127+
auto interval = services.get<DeviceSpec const>().resourceMonitoringInterval;
128+
if (ResourcesMonitoringHelper::isResourcesMonitoringEnabled(interval)) {
129+
using o2::monitoring::PmMeasurement;
130+
monitoring->enableProcessMonitoring(interval, {PmMeasurement::Cpu, PmMeasurement::Mem, PmMeasurement::Smaps});
131+
}
132+
122133
auto extRunNumber = services.get<RawDeviceService>().device()->fConfig->GetProperty<std::string>("runNumber", "unspecified");
123134
if (extRunNumber == "unspecified") {
124135
return;
@@ -127,6 +138,12 @@ o2::framework::ServiceSpec CommonServices::monitoringSpec()
127138
monitoring->setRunNumber(std::stoul(extRunNumber));
128139
} catch (...) {
129140
} },
141+
// Final measurement here rather than in ~Monitoring() at .exit, which is
142+
// not reliably reached before the process exits. Unlike postEOS this also
143+
// covers devices that quit themselves via readyToQuit().
144+
.stop = [](ServiceRegistryRef, void* service) {
145+
auto* monitoring = reinterpret_cast<Monitoring*>(service);
146+
monitoring->finalizeProcessMonitoring(); },
130147
.exit = [](ServiceRegistryRef registry, void* service) {
131148
auto* monitoring = reinterpret_cast<Monitoring*>(service);
132149
monitoring->flushBuffer();

Generators/include/Generators/Generator.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ class Generator : public FairGenerator
105105
/** notification methods **/
106106
virtual void notifyEmbedding(const o2::dataformats::MCEventHeader* eventHeader){};
107107

108+
/** Release external resources (forked subprocesses, open files, ...) once
109+
* the generator is no longer needed, without relying on destructor timing **/
110+
virtual void stop() {}
111+
108112
void setTriggerOkHook(std::function<void(std::vector<TParticle> const& p, int eventCount)> f) { mTriggerOkHook = f; }
109113
void setTriggerFalseHook(std::function<void(std::vector<TParticle> const& p, int eventCount)> f) { mTriggerFalseHook = f; }
110114

Generators/include/Generators/GeneratorFileOrCmd.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,23 @@ struct GeneratorFileOrCmd {
143143
* Terminates the background command using PID of the child
144144
* process generated by fork.
145145
*
146+
* @param graceMillis How long to wait for the command to exit on its
147+
* own before killing its process group. A command started
148+
* through an intermediate shell (a wrapper script, say) only
149+
* contributes the CPU time of the actual generator to this
150+
* process' RUSAGE_CHILDREN once that shell has reaped it, which
151+
* it can no longer do once we have killed it. Zero preserves
152+
* the previous kill-immediately behaviour.
153+
*
146154
* @return true if the process was terminated successfully
147155
*/
148-
virtual bool terminateCmd();
156+
virtual bool terminateCmd(unsigned int graceMillis = 0);
157+
/**
158+
* Time granted to the command to exit by itself when the generator is
159+
* stopped normally. It has produced everything that was asked of it by
160+
* then, so this only covers its teardown.
161+
*/
162+
static constexpr unsigned int sStopGraceMillis = 5000;
149163
/**
150164
* Create a temporary file (and close it immediately). On success,
151165
* the list of file names is cleared and the name of the temporary

Generators/include/Generators/GeneratorHepMC.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ class GeneratorHepMC : public Generator, public GeneratorFileOrCmd
8686
*/
8787
Bool_t importParticles() override;
8888

89+
/** Terminate the background command (if any), see Generator::stop(). */
90+
void stop() override;
91+
8992
/** setters **/
9093
void setEventsToSkip(uint64_t val) { mEventsToSkip = val; };
9194
void setVersion(const int& ver) { mVersion = ver; };

Generators/include/Generators/GeneratorService.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ class GeneratorService
7070
void generateEvent_MCTracks(o2::pmr::vector<MCTrack>& tracks, o2::dataformats::MCEventHeader& header);
7171
void generateEvent_TParticles(std::vector<TParticle>& tparts, o2::dataformats::MCEventHeader& header);
7272

73+
/** Calls Generator::stop() on all registered generators **/
74+
void stopGenerators();
75+
7376
private:
7477
PrimaryGenerator mPrimGen;
7578
o2::data::Stack mStack;

Generators/include/Generators/GeneratorTParticle.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ class GeneratorTParticle : public Generator, public GeneratorFileOrCmd
7373
* program */
7474
Bool_t Init() override;
7575

76+
/** Terminate the background command (if any), see Generator::stop(). */
77+
void stop() override;
78+
7679
/**
7780
* Configure the generator from parameters and the general
7881
* simulation configuration. This is implemented as a member

Generators/src/GeneratorFileOrCmd.cxx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,32 @@ bool GeneratorFileOrCmd::executeCmdLine(const std::string& cmd)
142142
return true;
143143
}
144144
// -----------------------------------------------------------------
145-
bool GeneratorFileOrCmd::terminateCmd()
145+
bool GeneratorFileOrCmd::terminateCmd(unsigned int graceMillis)
146146
{
147147
if (mCmdPid == -1) {
148148
LOG(info) << "No command is currently running";
149149
return false;
150150
}
151151

152+
// Let the command finish by itself if it is about to: killing the process
153+
// group first would deprive an intermediate shell of the chance to reap the
154+
// actual generator, and with it this process of the generator's CPU time,
155+
// which only reaches RUSAGE_CHILDREN through that reap.
156+
constexpr unsigned int pollMillis = 10;
157+
for (unsigned int waited = 0; waited < graceMillis; waited += pollMillis) {
158+
int status;
159+
pid_t reaped = waitpid(mCmdPid, &status, WNOHANG);
160+
if (reaped == mCmdPid) {
161+
LOG(info) << "Command with process ID " << mCmdPid << " exited by itself";
162+
mCmdPid = -1;
163+
return true;
164+
}
165+
if (reaped == -1) {
166+
break; // not our child (any more): let the kill path report it
167+
}
168+
std::this_thread::sleep_for(std::chrono::milliseconds(pollMillis));
169+
}
170+
152171
LOG(info) << "Terminating process ID group " << mCmdPid;
153172
if (kill(-mCmdPid, SIGKILL) == -1) {
154173
LOG(fatal) << "Failed to kill process: " << std::strerror(errno);

Generators/src/GeneratorHepMC.cxx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,29 @@ GeneratorHepMC::~GeneratorHepMC()
6666
if (mEvent) {
6767
delete mEvent;
6868
}
69-
if (not mCmd.empty()) {
70-
// Must be executed before removing the temporary file
71-
// otherwise the current child process might still be writing on it
72-
// causing unwanted stdout messages which could slow down the system
73-
terminateCmd();
74-
}
69+
stop();
7570
removeTemp();
7671
}
7772

73+
/*****************************************************************/
74+
75+
void GeneratorHepMC::stop()
76+
{
77+
if (mCmd.empty()) {
78+
return;
79+
}
80+
// Close our end of the pipe first: a generator still blocked writing to it
81+
// then sees EPIPE and exits promptly, instead of sitting out the whole grace
82+
// period and being killed - which would lose its CPU time (see terminateCmd).
83+
if (mReader) {
84+
mReader->close();
85+
}
86+
// Must be executed before removing the temporary file
87+
// otherwise the current child process might still be writing on it
88+
// causing unwanted stdout messages which could slow down the system
89+
terminateCmd(sStopGraceMillis);
90+
}
91+
7892
/*****************************************************************/
7993
void GeneratorHepMC::setup(const GeneratorFileOrCmdParam& param0,
8094
const GeneratorHepMCParam& param,

Generators/src/GeneratorService.cxx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include "SimConfig/SimConfig.h"
1515
#include "Generators/Generator.h"
1616
#include "DataFormatsCalibration/MeanVertexObject.h"
17+
#include <TCollection.h>
18+
#include <TObjArray.h>
1719

1820
using namespace o2::eventgen;
1921

@@ -90,3 +92,17 @@ void GeneratorService::generateEvent_TParticles(std::vector<TParticle>& tracks,
9092
tracks.clear();
9193
tracks = mStack.getPrimaries();
9294
}
95+
96+
void GeneratorService::stopGenerators()
97+
{
98+
auto* generators = mPrimGen.GetListOfGenerators();
99+
if (!generators) {
100+
return;
101+
}
102+
TIter next(generators);
103+
while (TObject* obj = next()) {
104+
if (auto* gen = dynamic_cast<o2::eventgen::Generator*>(obj)) {
105+
gen->stop();
106+
}
107+
}
108+
}

Generators/src/GeneratorTParticle.cxx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,20 @@ GeneratorTParticle::~GeneratorTParticle()
4343
if (mCmd.empty()) {
4444
return;
4545
}
46-
46+
// Must be executed before removing the temporary file, otherwise the child
47+
// process might still be writing to it
48+
stop();
4749
removeTemp();
4850
}
4951
/*****************************************************************/
52+
void GeneratorTParticle::stop()
53+
{
54+
if (mCmd.empty()) {
55+
return;
56+
}
57+
terminateCmd(sStopGraceMillis);
58+
}
59+
/*****************************************************************/
5060
Bool_t GeneratorTParticle::Init()
5161
{
5262
mChain = new TChain(mTreeName.c_str());

0 commit comments

Comments
 (0)