Skip to content

Commit dd2feea

Browse files
committed
added event and summary log options, and using only clhep, g4 includes in gemc_dep
1 parent 7929ed4 commit dd2feea

11 files changed

Lines changed: 333 additions & 21 deletions

File tree

gemc.cc

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,17 @@
2020
#include "gfield_options.h"
2121

2222
// c++
23-
#include <cstdio>
23+
#include <chrono>
2424
#include <cstdlib>
25+
#if defined(__linux__)
26+
#include <cstdio> // IWYU pragma: keep -- std::fflush on the Linux fast-exit path in main()
27+
#endif
2528

2629
int main(int argc, char* argv[]) {
2730

31+
// Anchor for the optional -print_summary timing report (total wall-clock time).
32+
const auto program_start = std::chrono::steady_clock::now();
33+
2834
auto base_schema = gemc::defineOptions();
2935
base_schema += gemc::collectPluginOptions(argc, argv);
3036
auto gopts = std::make_shared<GOptions>(argc, argv, base_schema);
@@ -150,6 +156,23 @@ int main(int argc, char* argv[]) {
150156
delete session;
151157
}
152158

159+
// print_summary is a switch, on by default; disable with -print_summary=false.
160+
if (gopts->getSwitch("print_summary")) {
161+
const auto now = std::chrono::steady_clock::now();
162+
const double total_s = std::chrono::duration<double>(now - program_start).count();
163+
log->info(0, "Timing summary: total simulation time ", total_s, " seconds.");
164+
if (geventDispenser->beamOnIssued()) {
165+
const double beam_s = std::chrono::duration<double>(now - geventDispenser->beamOnStartTime()).count();
166+
const int total_n = geventDispenser->getTotalNumberOfEvents();
167+
log->info(0, "Timing summary: total time since beamOn was issued ", beam_s, " seconds.");
168+
if (beam_s > 0.0) {
169+
const double avg_rate = static_cast<double>(total_n) / beam_s;
170+
log->info(0, "Timing summary: average event rate ", avg_rate, " events / second (",
171+
total_n, " events / ", beam_s, " seconds).");
172+
}
173+
}
174+
}
175+
153176
geventDispenser.reset();
154177
preloaded_gstreamer_map.reset();
155178
runManager.reset();

gemc/actions/event/gEventAction.cc

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
#include "G4Event.hh"
66
#include "G4Threading.hh"
77

8+
// c++
9+
#include <string>
10+
811
// gemc
912
#include "event/gEventDataCollection.h"
1013
#include "../generator/gPrimaryGeneratorAction.h"
1114

1215
// c++
1316
#include <algorithm>
1417
#include <cctype>
18+
#include <chrono>
1519

1620
namespace {
1721
GGeneratedParticleBank make_generated_particle_bank(const GParticleRecordEvent& particles) {
@@ -42,6 +46,18 @@ bool scalar_bool_option_enabled(const std::shared_ptr<GOptions>& goptions, const
4246
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
4347
return value == "true" || value == "1" || value == "yes" || value == "on";
4448
}
49+
50+
// Convert a substring to a non-negative integer, returning false on any malformed input.
51+
bool to_non_negative_int(const std::string& text, int& out) {
52+
if (text.empty()) return false;
53+
size_t pos = 0;
54+
int value;
55+
try { value = std::stoi(text, &pos); }
56+
catch (...) { return false; }
57+
if (pos != text.size() || value < 0) return false;
58+
out = value;
59+
return true;
60+
}
4561
}
4662

4763

@@ -51,8 +67,65 @@ GEventAction::GEventAction(const std::shared_ptr<GOptions>& gopt, GRunAction* ru
5167
GBase(gopt, EVENTACTION_LOGGER),
5268
goptions(gopt),
5369
run_action(run_a) {
54-
const auto desc = "GEventAction " + std::to_string(G4Threading::G4GetThreadId());
70+
const auto thread_id = G4Threading::G4GetThreadId();
71+
const auto desc = "GEventAction " + std::to_string(thread_id);
5572
log->debug(CONSTRUCTOR, FUNCTION_NAME, desc);
73+
74+
// Parse the log_every option of the form N or N-NTH. Anything malformed disables the
75+
// feature and is reported once (from thread 0) to avoid duplicated warnings across workers.
76+
std::string spec = goptions->getScalarString(LOG_EVERY_OPTION);
77+
if (!spec.empty() && spec != UNINITIALIZEDSTRINGQUANTITY) {
78+
const auto dash = spec.find('-');
79+
std::string n_part = dash == std::string::npos ? spec : spec.substr(0, dash);
80+
std::string nth_part = dash == std::string::npos ? std::string() : spec.substr(dash + 1);
81+
82+
// Effective worker-thread count, mirroring gemc::get_nthreads clamping (0 means all cores).
83+
int nthreads = goptions->getScalarInt("nthreads");
84+
const int ncores = G4Threading::G4GetNumberOfCores();
85+
if (nthreads == 0 || nthreads > ncores) nthreads = ncores;
86+
87+
int n = 0;
88+
if (!to_non_negative_int(n_part, n) || n == 0) {
89+
if (thread_id <= 0)
90+
log->warning("Ignoring invalid -", LOG_EVERY_OPTION, "=", spec,
91+
" : N must be a positive integer.");
92+
}
93+
else if (dash != std::string::npos) {
94+
int nth = 0;
95+
if (!to_non_negative_int(nth_part, nth) || nth >= nthreads) {
96+
if (thread_id <= 0)
97+
log->warning("Ignoring invalid -", LOG_EVERY_OPTION, "=", spec,
98+
" : thread id must be in [0, ", nthreads - 1, "].");
99+
}
100+
else {
101+
log_every_n = n;
102+
log_every_thread = nth;
103+
}
104+
}
105+
else { log_every_n = n; }
106+
}
107+
}
108+
109+
// Print the periodic "Starting event" line, honoring the log module and optional thread filter.
110+
// The reported event number, count and rate are all per worker thread: each enabled thread logs
111+
// every N events it processes, showing its own 1-based event count and average rate (events / second).
112+
void GEventAction::log_event_start(int thread_id) {
113+
if (log_every_n <= 0) return;
114+
if (log_every_thread >= 0 && log_every_thread != thread_id) return;
115+
116+
// Anchor this thread's clock on its first counted event, then count this event.
117+
const auto now = std::chrono::steady_clock::now();
118+
if (log_events_seen == 0) { log_start_time = now; }
119+
++log_events_seen;
120+
121+
if (log_events_seen % log_every_n != 0) return;
122+
123+
const double elapsed_s = std::chrono::duration<double>(now - log_start_time).count();
124+
const double rate = elapsed_s > 0.0 ? static_cast<double>(log_events_seen) / elapsed_s : 0.0;
125+
126+
// log_events_seen is this thread's own 1-based count, not the global Geant4 event id.
127+
log->info(0, "Starting event n. ", log_events_seen, " in thread ", thread_id,
128+
". Average rate: ", rate, " events / second");
56129
}
57130

58131
// Begin-of-event hook used mainly for tracing event and thread identifiers.
@@ -61,6 +134,8 @@ void GEventAction::BeginOfEventAction([[maybe_unused]] const G4Event* event) {
61134
const auto event_id = event->GetEventID();
62135

63136
log->debug(NORMAL, FUNCTION_NAME, " event id ", event_id, " in thread ", thread_id);
137+
138+
log_event_start(thread_id);
64139
}
65140

66141
// Finalize the event by reading hit collections, digitizing them, routing the

gemc/actions/event/gEventAction.h

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
#include <gemc/gbase/gbase.h>
88
#include <gemc/actions/run/gRunAction.h>
99

10+
// c++
11+
#include <chrono>
12+
1013
/**
1114
* \file gEventAction.h
1215
* \brief Declares GEventAction, the per-event processing action for the GEMC actions module.
@@ -16,6 +19,16 @@
1619

1720
constexpr const char* EVENTACTION_LOGGER = "geventaction";
1821

22+
/**
23+
* \brief Name of the option controlling periodic per-event log messages.
24+
*
25+
* Accepts a string of the form \c N or \c N-NTH (N is the log module):
26+
* - \c N : every worker thread prints a message each time it has processed a multiple of N
27+
* events (the count is per thread, not the global event id).
28+
* - \c N-NTH: as above, but only the worker thread with id NTH prints (0 <= NTH < nthreads).
29+
*/
30+
constexpr const char* LOG_EVERY_OPTION = "log_every";
31+
1932
/**
2033
* \brief Namespace containing helpers related to event-action configuration.
2134
*
@@ -27,7 +40,35 @@ namespace geventaction {
2740
*
2841
* \return A GOptions object scoped to the event-action logger name.
2942
*/
30-
inline GOptions defineOptions() { return GOptions(EVENTACTION_LOGGER); }
43+
inline GOptions defineOptions() {
44+
GOptions goptions(EVENTACTION_LOGGER);
45+
46+
std::string help = "Print a progress log line every N events, with the average event rate.\n \n";
47+
help += GTAB;
48+
help += "The value is a string of the form N or N-NTH, where N is the log module:\n";
49+
help += GTABTAB;
50+
help += "N : every worker thread prints 'Starting event n. <k> in thread <tid>.\n";
51+
help += GTABTABTAB;
52+
help += "Average rate: <r> events / second' every time it has processed a multiple\n";
53+
help += GTABTABTAB;
54+
help += "of N events. <k> is that thread's own 1-based event count (not the global\n";
55+
help += GTABTABTAB;
56+
help += "Geant4 event id), and <r> is that thread's average rate.\n";
57+
help += GTABTAB;
58+
help += "N-NTH : as above, but only the worker thread with id NTH prints. NTH must be\n";
59+
help += GTABTABTAB;
60+
help += "in the range [0, nthreads-1].\n \n";
61+
help += GTAB;
62+
help += "Examples: -log_every=100 (all threads, every 100 events)\n";
63+
help += GTABTAB;
64+
help += "-log_every=100-2 (only thread 2, every 100 events)\n";
65+
goptions.defineOption(
66+
GVariable(LOG_EVERY_OPTION, UNINITIALIZEDSTRINGQUANTITY,
67+
"log module: print event progress and average rate every N events per thread"),
68+
help);
69+
70+
return goptions;
71+
}
3172
} // namespace geventaction
3273

3374

@@ -108,11 +149,45 @@ class GEventAction : public GBase<GEventAction>, public G4UserEventAction
108149
*/
109150
void publish_event_data(const std::shared_ptr<GEventDataCollection>& event_data) const;
110151

152+
/**
153+
* \brief Emits the periodic "Starting event" log line when enabled and due.
154+
*
155+
* Honors the \c log_every option: prints only when the log module is configured (> 0)
156+
* and (when a thread filter is set) the running thread matches the requested one. The
157+
* count is kept per worker thread, so the line is printed every time this thread has
158+
* processed a multiple of N events. The reported event number is this thread's own
159+
* 1-based count (not the global Geant4 event id), along with this thread's average rate
160+
* (events / second) since its first counted event.
161+
*
162+
* \param thread_id Id of the worker thread processing the event.
163+
*/
164+
void log_event_start(int thread_id);
165+
166+
/**
167+
* \brief Number of events processed by this worker thread while logging is enabled.
168+
*/
169+
long long log_events_seen = 0;
170+
171+
/**
172+
* \brief Steady-clock anchor set on this thread's first counted event.
173+
*/
174+
std::chrono::steady_clock::time_point log_start_time;
175+
111176
/**
112177
* \brief Shared configuration used to build event products and control logging.
113178
*/
114179
std::shared_ptr<GOptions> goptions;
115180

181+
/**
182+
* \brief Event-id modulo for periodic logging; 0 disables the feature.
183+
*/
184+
int log_every_n = 0;
185+
186+
/**
187+
* \brief Worker-thread id allowed to log, or -1 to allow all threads.
188+
*/
189+
int log_every_thread = -1;
190+
116191
/**
117192
* \brief Non-owning pointer to the thread-local run action associated with this event action.
118193
*

gemc/eventDispenser/eventDispenser.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ int EventDispenser::processEvents() {
200200
// The command string is a standard Geant4 UI command: \c /run/beamOn <N>.
201201
log->info(1, "Processing ", nevents, " events in one go");
202202
closeOpenGeometryBeforeBeamOn(log);
203+
// Record the moment the first BeamOn is issued so a timing summary can be produced later.
204+
if (!beamOnTime.has_value()) { beamOnTime = std::chrono::steady_clock::now(); }
203205
g4uim->ApplyCommand("/run/beamOn " + to_string(nevents));
204206
// Take the screenshot after BeamOn returns. At this point G4VisManager::EndOfRun()
205207
// has already joined the vis subthread (ARM64 offset 0xa35f8: bl thread::join), so

gemc/eventDispenser/eventDispenser.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
#include <gemc/gbase/gbase.h>
44
#include <gemc/gdynamicDigitization/gdynamicdigitization.h>
55

6+
#include <chrono>
67
#include <map>
78
#include <memory>
9+
#include <optional>
810
#include <vector>
911

1012
/**
@@ -85,6 +87,9 @@ class EventDispenser : public GBase<EventDispenser>
8587
/// Index into \ref listOfRuns used by \ref getCurrentRun "getCurrentRun()".
8688
int currentRunIndex{};
8789

90+
/// Wall-clock time when the first \c /run/beamOn was issued; unset until then.
91+
std::optional<std::chrono::steady_clock::time_point> beamOnTime;
92+
8893
/** @} */
8994

9095
/**
@@ -174,6 +179,22 @@ class EventDispenser : public GBase<EventDispenser>
174179
*/
175180
int processEvents();
176181

182+
/**
183+
* \brief Returns whether at least one \c /run/beamOn has been issued.
184+
*
185+
* \return True once \ref processEvents() has dispatched its first run.
186+
*/
187+
[[nodiscard]] bool beamOnIssued() const { return beamOnTime.has_value(); }
188+
189+
/**
190+
* \brief Returns the wall-clock time of the first \c /run/beamOn.
191+
*
192+
* \warning Only meaningful when \ref beamOnIssued() returns true.
193+
*
194+
* \return Steady-clock time point captured just before the first BeamOn command.
195+
*/
196+
[[nodiscard]] std::chrono::steady_clock::time_point beamOnStartTime() const { return beamOnTime.value(); }
197+
177198
/**
178199
* \brief Sets the total number of events to process in single-run mode.
179200
*

gemc/goptions/goptions.cc

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,28 @@
2020
#include "gutilities.h"
2121

2222
// c++
23+
#include <algorithm>
2324
#include <iostream>
2425
#include <cstring>
2526
#include <cctype>
2627
#include <cstdlib>
2728

2829
using namespace std;
2930

31+
namespace {
32+
// Parse a human-friendly boolean token used for switches (-switch=value and YAML switch values).
33+
// Accepts, case-insensitively: true/1/yes/y/on and false/0/no/n/off. Returns false if @p raw is
34+
// not a recognized token, leaving @p out untouched.
35+
bool parse_bool_token(const std::string& raw, bool& out) {
36+
std::string v = raw;
37+
std::transform(v.begin(), v.end(), v.begin(),
38+
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
39+
if (v == "true" || v == "1" || v == "yes" || v == "y" || v == "on") { out = true; return true; }
40+
if (v == "false" || v == "0" || v == "no" || v == "n" || v == "off") { out = false; return true; }
41+
return false;
42+
}
43+
}
44+
3045
// See goptions.h for full constructor API documentation.
3146
/*
3247
* Parsing precedence implemented here:
@@ -155,10 +170,10 @@ GOptions::GOptions(int argc, char* argv[], const GOptions& user_defined_options)
155170
// Switch with an explicit boolean value: -gui=false / -gui=true. This gives the
156171
// CLI a way to turn a switch off, honoring the documented CLI-over-YAML precedence.
157172
if (switches.find(keyPart) != switches.end()) {
158-
if (valuePart == "true" || valuePart == "1") { switches[keyPart].turnOn(); }
159-
else if (valuePart == "false" || valuePart == "0") { switches[keyPart].turnOff(); }
173+
bool on = false;
174+
if (parse_bool_token(valuePart, on)) { on ? switches[keyPart].turnOn() : switches[keyPart].turnOff(); }
160175
else {
161-
cerr << "The switch " << keyPart << " accepts only true/false." << endl;
176+
cerr << "The switch " << keyPart << " accepts only true/false/yes/no/on/off/1/0." << endl;
162177
exit(EC__NOOPTIONFOUND);
163178
}
164179
continue;
@@ -224,9 +239,9 @@ GOptions::GOptions(int argc, char* argv[], const GOptions& user_defined_options)
224239
}
225240

226241
// Implementation note: public API docs are in goptions.h (avoid duplicate \param blocks).
227-
void GOptions::defineSwitch(const std::string& name, const std::string& description) {
242+
void GOptions::defineSwitch(const std::string& name, const std::string& description, bool default_status) {
228243
if (switches.find(name) == switches.end()) {
229-
switches[name] = GSwitch(description);
244+
switches[name] = GSwitch(description, default_status);
230245
}
231246
else {
232247
std::cerr << FATALERRORL << "The " << YELLOWHHL << name << RSTHHR
@@ -409,7 +424,15 @@ void GOptions::setOptionsValuesFromYamlFile(const std::string& yaml) {
409424
exit(EC__NOOPTIONFOUND);
410425
}
411426
else {
412-
switches[option_name].turnOn();
427+
// A bare key (null value) means "present" -> on; an explicit boolean value is honored,
428+
// so a default-on switch can be turned off from YAML (e.g. print_summary: false).
429+
bool on = true;
430+
if (it->second.IsScalar() && !parse_bool_token(it->second.as<std::string>(), on)) {
431+
cerr << FATALERRORL << "The switch " << YELLOWHHL << option_name << RSTHHR
432+
<< " accepts only true/false/yes/no/on/off/1/0." << endl;
433+
exit(EC__NOOPTIONFOUND);
434+
}
435+
on ? switches[option_name].turnOn() : switches[option_name].turnOff();
413436
}
414437
}
415438
else {

0 commit comments

Comments
 (0)