Warning, file /eic-opticks/sysrap/EventTiming.cc was not indexed
or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001
0002
0003
0004
0005
0006
0007 #include "EventTiming.hh"
0008
0009 #include <algorithm>
0010 #include <array>
0011 #include <atomic>
0012 #include <charconv>
0013 #include <chrono>
0014 #include <filesystem>
0015 #include <fstream>
0016 #include <iomanip>
0017 #include <iostream>
0018 #include <limits>
0019 #include <mutex>
0020 #include <sstream>
0021 #include <stdexcept>
0022 #include <system_error>
0023 #include <time.h>
0024 #include <unistd.h>
0025 #include <vector>
0026
0027 #include "NPU.hh"
0028 #include "SMeta.hh"
0029 #include "sproc.h"
0030
0031 namespace
0032 {
0033 std::atomic_flag memory_warning_emitted = ATOMIC_FLAG_INIT;
0034 std::atomic<std::uint64_t> temporary_sequence{0};
0035 thread_local std::string profile_tag;
0036 std::atomic<bool> profile_enabled{false};
0037 std::filesystem::path profile_output{"event_timing_profile.csv"};
0038 int profile_path_index{0};
0039
0040
0041
0042
0043
0044
0045 std::mutex& ProfileMutex()
0046 {
0047 static std::mutex mutex;
0048 return mutex;
0049 }
0050
0051
0052
0053
0054
0055
0056
0057
0058
0059 std::vector<EventTimingProfileRecord>& ProfileRecords()
0060 {
0061 static std::vector<EventTimingProfileRecord> records;
0062 return records;
0063 }
0064
0065 std::int64_t TimespecNs(const timespec& value)
0066 {
0067 return static_cast<std::int64_t>(value.tv_sec) * 1'000'000'000ll + static_cast<std::int64_t>(value.tv_nsec);
0068 }
0069
0070 /**
0071 * Parses one required signed-integer CSV field.
0072 *
0073 * @param field field contents without CSV quoting
0074 * @param name logical field name included in diagnostics
0075 * @return parsed signed integer
0076 * @throws std::invalid_argument if field is empty or not an integer
0077 */
0078 std::int64_t ParseInteger(std::string_view field, std::string_view name)
0079 {
0080 if (field.empty())
0081 throw std::invalid_argument("EventTimingSample missing " + std::string(name));
0082
0083 std::int64_t value = 0;
0084 const char* begin = field.data();
0085 const char* end = begin + field.size();
0086 const auto result = std::from_chars(begin, end, value);
0087 if (result.ec != std::errc{} || result.ptr != end)
0088 throw std::invalid_argument("EventTimingSample invalid " + std::string(name));
0089 return value;
0090 }
0091
0092 std::string OptionalValue(
0093 const EventTimingSample& sample,
0094 EventTimingCapture field,
0095 std::int64_t value)
0096 {
0097 return sample.has(field) ? std::to_string(value) : std::string{};
0098 }
0099
0100 void Require(const EventTimingSample& sample, EventTimingCapture field, const char* operation)
0101 {
0102 if (!sample.has(field))
0103 throw std::logic_error(std::string("EventTimingSample ") + operation + " requires captured fields");
0104 }
0105
0106 std::string PadInteger(int index, int width, int precision, bool zero_fill, char conversion)
0107 {
0108 if (conversion == 'u' && index < 0)
0109 throw std::invalid_argument("EventTiming unsigned index cannot be negative");
0110
0111 const bool negative = index < 0;
0112 const std::uint64_t magnitude = negative
0113 ? static_cast<std::uint64_t>(-static_cast<std::int64_t>(index))
0114 : static_cast<std::uint64_t>(index);
0115 std::string digits = std::to_string(magnitude);
0116
0117 const int numeric_width = precision >= 0 ? precision : (zero_fill ? width : 0);
0118 if (numeric_width > static_cast<int>(digits.size()))
0119 digits.insert(0, static_cast<std::size_t>(numeric_width - digits.size()), '0');
0120 if (negative)
0121 digits.insert(digits.begin(), '-');
0122
0123 if (precision < 0 && !zero_fill && width > static_cast<int>(digits.size()))
0124 digits.insert(0, static_cast<std::size_t>(width - digits.size()), ' ');
0125 return digits;
0126 }
0127
0128 /**
0129 * Expands the supported printf-style integer conversion in a path or tag.
0130 *
0131 * The accepted subset is `%[0][width][.precision][d|i|u]`; `%%` emits a
0132 * literal percent sign. Exactly one integer conversion is required, and very
0133 * large widths and precisions are rejected before arithmetic can overflow.
0134 *
0135 * @param pattern path or tag pattern to expand
0136 * @param index integer substituted for the conversion
0137 * @return expanded text
0138 * @throws std::invalid_argument if the pattern is unsupported or ambiguous
0139 */
0140 std::string FormatIndex(std::string_view pattern, int index)
0141 {
0142 std::string result;
0143 int conversions = 0;
0144
0145 for (std::size_t cursor = 0; cursor < pattern.size();)
0146 {
0147 if (pattern[cursor] != '%')
0148 {
0149 result.push_back(pattern[cursor++]);
0150 continue;
0151 }
0152
0153 ++cursor;
0154 if (cursor < pattern.size() && pattern[cursor] == '%')
0155 {
0156 result.push_back('%');
0157 ++cursor;
0158 continue;
0159 }
0160 if (conversions != 0)
0161 throw std::invalid_argument("EventTiming format must contain exactly one integer conversion");
0162
0163 bool zero_fill = false;
0164 if (cursor < pattern.size() && pattern[cursor] == '0')
0165 {
0166 zero_fill = true;
0167 ++cursor;
0168 }
0169
0170 int width = 0;
0171 while (cursor < pattern.size() && pattern[cursor] >= '0' && pattern[cursor] <= '9')
0172 {
0173 if (width > 10000)
0174 throw std::invalid_argument("EventTiming format width is too large");
0175 width = width * 10 + (pattern[cursor++] - '0');
0176 }
0177
0178 int precision = -1;
0179 if (cursor < pattern.size() && pattern[cursor] == '.')
0180 {
0181 ++cursor;
0182 precision = 0;
0183 const std::size_t precision_start = cursor;
0184 while (cursor < pattern.size() && pattern[cursor] >= '0' && pattern[cursor] <= '9')
0185 {
0186 if (precision > 10000)
0187 throw std::invalid_argument("EventTiming format precision is too large");
0188 precision = precision * 10 + (pattern[cursor++] - '0');
0189 }
0190 if (cursor == precision_start)
0191 throw std::invalid_argument("EventTiming format precision requires digits");
0192 }
0193
0194 if (cursor >= pattern.size() || (pattern[cursor] != 'd' && pattern[cursor] != 'i' && pattern[cursor] != 'u'))
0195 throw std::invalid_argument("EventTiming format contains an unsupported conversion");
0196
0197 const char conversion = pattern[cursor++];
0198 result += PadInteger(index, width, precision, zero_fill, conversion);
0199 ++conversions;
0200 }
0201
0202 if (conversions != 1)
0203 throw std::invalid_argument("EventTiming format must contain exactly one integer conversion");
0204 return result;
0205 }
0206
0207 /**
0208 * Escapes one value for RFC 4180-compatible CSV output.
0209 *
0210 * Fields containing a comma, quote, carriage return, or newline are quoted;
0211 * embedded quotes are doubled.
0212 *
0213 * @param value unescaped field value
0214 * @return CSV field suitable for direct insertion into a row
0215 */
0216 std::string CsvField(std::string_view value)
0217 {
0218 if (value.find_first_of(",\"\r\n") == std::string_view::npos)
0219 return std::string(value);
0220
0221 std::string quoted;
0222 quoted.push_back('"');
0223 for (const char c : value)
0224 {
0225 if (c == '"')
0226 quoted.push_back('"');
0227 quoted.push_back(c);
0228 }
0229 quoted.push_back('"');
0230 return quoted;
0231 }
0232
0233 /** Stores one parsed CSV record and its first physical source line. */
0234 struct CsvRow
0235 {
0236 /** Unescaped field values in source order. */
0237 std::vector<std::string> fields;
0238 /** One-based source line on which this logical record begins. */
0239 std::size_t line{1};
0240 };
0241
0242 /**
0243 * Reports a CSV error with its source path and physical line number.
0244 *
0245 * @param source source name used in the diagnostic
0246 * @param line one-based physical line number
0247 * @param message description of the malformed input
0248 * @throws std::runtime_error unconditionally
0249 */
0250 [[noreturn]] void CsvError(
0251 const std::filesystem::path& source,
0252 std::size_t line,
0253 std::string_view message)
0254 {
0255 throw std::runtime_error(
0256 source.string() + ": line " + std::to_string(line) + ": " + std::string(message));
0257 }
0258
0259 /**
0260 * Parses RFC 4180-style records while preserving embedded quoted newlines.
0261 *
0262 * Both LF and CRLF record endings are accepted. Quotes may only begin an
0263 * empty field, doubled quotes are unescaped, and characters following a
0264 * closing quote are limited to a delimiter or record ending.
0265 *
0266 * @param input stream to consume through end-of-file
0267 * @param source source name used in diagnostics
0268 * @return parsed records with their starting line numbers
0269 * @throws std::runtime_error if quoting or field termination is malformed
0270 */
0271 std::vector<CsvRow> ParseRows(
0272 std::istream& input,
0273 const std::filesystem::path& source)
0274 {
0275 std::vector<CsvRow> rows;
0276 std::vector<std::string> fields;
0277 std::string field;
0278 std::size_t line = 1;
0279 std::size_t record_line = 1;
0280 bool in_quotes = false;
0281 bool after_quote = false;
0282 bool row_has_data = false;
0283
0284 auto finish_row = [&] {
0285 fields.push_back(field);
0286 field.clear();
0287 rows.push_back({fields, record_line});
0288 fields.clear();
0289 after_quote = false;
0290 row_has_data = false;
0291 };
0292
0293 char c = '\0';
0294 while (input.get(c))
0295 {
0296 if (in_quotes)
0297 {
0298 if (c == '"')
0299 {
0300 if (input.peek() == '"')
0301 {
0302 (void)input.get(c);
0303 field.push_back('"');
0304 }
0305 else
0306 {
0307 in_quotes = false;
0308 after_quote = true;
0309 }
0310 }
0311 else
0312 {
0313 field.push_back(c);
0314 if (c == '\n')
0315 ++line;
0316 }
0317 continue;
0318 }
0319
0320 if (after_quote && c != ',' && c != '\r' && c != '\n')
0321 CsvError(source, record_line, "unexpected character after closing quote");
0322
0323 if (c == '"')
0324 {
0325 if (!field.empty() || after_quote)
0326 CsvError(source, record_line, "quote must begin a field");
0327 in_quotes = true;
0328 row_has_data = true;
0329 }
0330 else if (c == ',')
0331 {
0332 fields.push_back(field);
0333 field.clear();
0334 after_quote = false;
0335 row_has_data = true;
0336 }
0337 else if (c == '\n' || c == '\r')
0338 {
0339 if (c == '\r' && input.peek() == '\n')
0340 (void)input.get(c);
0341 finish_row();
0342 ++line;
0343 record_line = line;
0344 }
0345 else
0346 {
0347 field.push_back(c);
0348 row_has_data = true;
0349 }
0350 }
0351
0352 if (in_quotes)
0353 CsvError(source, record_line, "unterminated quoted field");
0354 if (after_quote || row_has_data || !fields.empty() || !field.empty())
0355 finish_row();
0356 return rows;
0357 }
0358
0359 /**
0360 * Creates a process-locally unique sibling path for staged output.
0361 *
0362 * The path combines the destination, process identifier, and a process-local
0363 * atomic sequence number to distinguish concurrent writes in this process.
0364 *
0365 * @param destination final output path
0366 * @return temporary path in the same directory as destination
0367 */
0368 std::filesystem::path TemporaryPath(const std::filesystem::path& destination)
0369 {
0370 const std::uint64_t sequence = temporary_sequence.fetch_add(1);
0371 return destination.string() + ".tmp." + std::to_string(static_cast<long long>(::getpid())) + "." + std::to_string(sequence);
0372 }
0373
0374 /**
0375 * Formats the current wall-clock time for the timeline manifest.
0376 *
0377 * @return UTC timestamp in `YYYY-MM-DDTHH:MM:SSZ` form
0378 */
0379 std::string UtcNow()
0380 {
0381 const std::time_t now = std::time(nullptr);
0382 std::tm utc{};
0383 gmtime_r(&now, &utc);
0384 std::ostringstream out;
0385 out << std::put_time(&utc, "%Y-%m-%dT%H:%M:%SZ");
0386 return out.str();
0387 }
0388
0389 double Seconds(std::int64_t nanoseconds)
0390 {
0391 return static_cast<double>(nanoseconds) * 1e-9;
0392 }
0393
0394 /**
0395 * Computes a process or thread CPU-time interval in seconds.
0396 *
0397 * @param begin interval start
0398 * @param end interval end
0399 * @param field ProcessCpu or ThreadCpu
0400 * @return signed interval in seconds, or zero when either sample lacks field
0401 */
0402 double CpuSeconds(
0403 const EventTimingSample& begin,
0404 const EventTimingSample& end,
0405 EventTimingCapture field)
0406 {
0407 if (!begin.has(field) || !end.has(field))
0408 return 0.0;
0409 const std::int64_t delta = field == EventTimingCapture::ProcessCpu
0410 ? end.process_cpu_ns - begin.process_cpu_ns
0411 : end.thread_cpu_ns - begin.thread_cpu_ns;
0412 return Seconds(delta);
0413 }
0414
0415 /**
0416 * Writes, flushes, and closes one complete output file.
0417 *
0418 * @param path destination to truncate
0419 * @param content complete file contents
0420 * @throws std::runtime_error if opening, writing, flushing, or closing fails
0421 */
0422 void WriteChecked(const std::filesystem::path& path, std::string_view content)
0423 {
0424 std::ofstream output(path, std::ios::binary | std::ios::trunc);
0425 if (!output)
0426 throw std::runtime_error("Unable to open timing output: " + path.string());
0427 output << content;
0428 output.flush();
0429 if (!output)
0430 throw std::runtime_error("Unable to write timing output: " + path.string());
0431 output.close();
0432 if (!output)
0433 throw std::runtime_error("Unable to close timing output: " + path.string());
0434 }
0435
0436 /**
0437 * Publishes a staged file by renaming it over its destination.
0438 *
0439 * @param temporary fully written temporary path
0440 * @param destination final output path
0441 * @throws std::runtime_error if the rename fails
0442 */
0443 void ReplaceFile(
0444 const std::filesystem::path& temporary,
0445 const std::filesystem::path& destination)
0446 {
0447 std::error_code error;
0448 std::filesystem::rename(temporary, destination, error);
0449 if (error)
0450 throw std::runtime_error(
0451 "Unable to replace timing output " + destination.string() + ": " + error.message());
0452 }
0453
0454 } // namespace
0455
0456 EventTimingSample EventTimingSample::Capture(EventTimingCapture mask)
0457 {
0458 return Capture(mask, &sproc::Query);
0459 }
0460
0461 EventTimingSample EventTimingSample::Capture(EventTimingCapture mask, MemoryQuery query)
0462 {
0463 EventTimingSample sample{};
0464
0465 const auto steady_now = std::chrono::steady_clock::now().time_since_epoch();
0466 sample.steady_time_ns =
0467 std::chrono::duration_cast<std::chrono::nanoseconds>(steady_now).count();
0468 sample.valid_mask |= bit(EventTimingCapture::Monotonic);
0469
0470 if ((bit(mask) & bit(EventTimingCapture::Wall)) != 0u)
0471 {
0472 const auto wall_now = std::chrono::system_clock::now().time_since_epoch();
0473 sample.wall_time_us =
0474 std::chrono::duration_cast<std::chrono::microseconds>(wall_now).count();
0475 sample.valid_mask |= bit(EventTimingCapture::Wall);
0476 }
0477
0478 if ((bit(mask) & bit(EventTimingCapture::ProcessCpu)) != 0u)
0479 {
0480 timespec value{};
0481 if (::clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &value) == 0)
0482 {
0483 sample.process_cpu_ns = TimespecNs(value);
0484 sample.valid_mask |= bit(EventTimingCapture::ProcessCpu);
0485 }
0486 }
0487
0488 if ((bit(mask) & bit(EventTimingCapture::ThreadCpu)) != 0u)
0489 {
0490 timespec value{};
0491 if (::clock_gettime(CLOCK_THREAD_CPUTIME_ID, &value) == 0)
0492 {
0493 sample.thread_cpu_ns = TimespecNs(value);
0494 sample.valid_mask |= bit(EventTimingCapture::ThreadCpu);
0495 }
0496 }
0497
0498 if ((bit(mask) & bit(EventTimingCapture::Memory)) != 0u)
0499 {
0500 std::int32_t vm_kb = 0;
0501 std::int32_t rss_kb = 0;
0502 if (query != nullptr && query(vm_kb, rss_kb) == 0)
0503 {
0504 sample.vm_kb = vm_kb;
0505 sample.rss_kb = rss_kb;
0506 sample.valid_mask |= bit(EventTimingCapture::Memory);
0507 }
0508 else if (!memory_warning_emitted.test_and_set())
0509 {
0510 std::cerr << "EventTimingSample memory sample failed; continuing without VM/RSS\n";
0511 }
0512 }
0513
0514 return sample;
0515 }
0516
0517 bool EventTimingSample::has(EventTimingCapture field) const
0518 {
0519 return (valid_mask & bit(field)) != 0u;
0520 }
0521
0522 std::string EventTimingSample::serialize() const
0523 {
0524 Require(*this, EventTimingCapture::Monotonic, "serialization");
0525 std::ostringstream out;
0526 out << OptionalValue(*this, EventTimingCapture::Wall, wall_time_us) << ','
0527 << steady_time_ns << ','
0528 << OptionalValue(*this, EventTimingCapture::ProcessCpu, process_cpu_ns) << ','
0529 << OptionalValue(*this, EventTimingCapture::ThreadCpu, thread_cpu_ns) << ','
0530 << OptionalValue(*this, EventTimingCapture::Memory, vm_kb) << ','
0531 << OptionalValue(*this, EventTimingCapture::Memory, rss_kb);
0532 return out.str();
0533 }
0534
0535 EventTimingSample EventTimingSample::parse(std::string_view text)
0536 {
0537 std::array<int64_t, 6> fields{};
0538 uint32_t field_mask = 0;
0539 U::ParseEventTimingSample(fields, field_mask, text);
0540
0541 EventTimingSample sample{};
0542 sample.steady_time_ns = fields[1];
0543 sample.valid_mask |= bit(EventTimingCapture::Monotonic);
0544
0545 if ((field_mask & (1u << 0)) != 0u)
0546 {
0547 sample.wall_time_us = fields[0];
0548 sample.valid_mask |= bit(EventTimingCapture::Wall);
0549 }
0550 if ((field_mask & (1u << 2)) != 0u)
0551 {
0552 sample.process_cpu_ns = fields[2];
0553 sample.valid_mask |= bit(EventTimingCapture::ProcessCpu);
0554 }
0555 if ((field_mask & (1u << 3)) != 0u)
0556 {
0557 sample.thread_cpu_ns = fields[3];
0558 sample.valid_mask |= bit(EventTimingCapture::ThreadCpu);
0559 }
0560 if ((field_mask & (1u << 4)) != 0u)
0561 {
0562 sample.vm_kb = fields[4];
0563 sample.rss_kb = fields[5];
0564 sample.valid_mask |= bit(EventTimingCapture::Memory);
0565 }
0566 return sample;
0567 }
0568
0569 bool EventTimingSample::looksLikeSerialized(std::string_view text)
0570 {
0571 try
0572 {
0573 (void)parse(text);
0574 return true;
0575 }
0576 catch (const std::exception&)
0577 {
0578 return false;
0579 }
0580 }
0581
0582 std::string EventTimingSample::desc() const
0583 {
0584 std::ostringstream out;
0585 out << "EventTimingSample("
0586 << "wall_time_us=" << OptionalValue(*this, EventTimingCapture::Wall, wall_time_us)
0587 << ", steady_time_ns=" << steady_time_ns
0588 << ", process_cpu_ns=" << OptionalValue(*this, EventTimingCapture::ProcessCpu, process_cpu_ns)
0589 << ", thread_cpu_ns=" << OptionalValue(*this, EventTimingCapture::ThreadCpu, thread_cpu_ns)
0590 << ", vm_kb=" << OptionalValue(*this, EventTimingCapture::Memory, vm_kb)
0591 << ", rss_kb=" << OptionalValue(*this, EventTimingCapture::Memory, rss_kb)
0592 << ')';
0593 return out.str();
0594 }
0595
0596 std::int64_t EventTimingSample::elapsedNs(
0597 const EventTimingSample& begin,
0598 const EventTimingSample& end)
0599 {
0600 Require(begin, EventTimingCapture::Monotonic, "elapsedNs");
0601 Require(end, EventTimingCapture::Monotonic, "elapsedNs");
0602 return end.steady_time_ns - begin.steady_time_ns;
0603 }
0604
0605 std::int64_t EventTimingSample::deltaVmKb(
0606 const EventTimingSample& begin,
0607 const EventTimingSample& end)
0608 {
0609 Require(begin, EventTimingCapture::Memory, "deltaVmKb");
0610 Require(end, EventTimingCapture::Memory, "deltaVmKb");
0611 return end.vm_kb - begin.vm_kb;
0612 }
0613
0614 std::int64_t EventTimingSample::deltaRssKb(
0615 const EventTimingSample& begin,
0616 const EventTimingSample& end)
0617 {
0618 Require(begin, EventTimingCapture::Memory, "deltaRssKb");
0619 Require(end, EventTimingCapture::Memory, "deltaRssKb");
0620 return end.rss_kb - begin.rss_kb;
0621 }
0622
0623 void EventTimingProfile::Configure(
0624 bool enabled,
0625 const std::filesystem::path& output,
0626 int path_index)
0627 {
0628 profile_output = output;
0629 profile_path_index = path_index;
0630 profile_enabled.store(enabled, std::memory_order_release);
0631 }
0632
0633 bool EventTimingProfile::Enabled()
0634 {
0635 return profile_enabled.load(std::memory_order_acquire);
0636 }
0637
0638 EventTimingSample EventTimingProfile::Mark(
0639 std::string_view name,
0640 std::string_view metadata)
0641 {
0642 if (!Enabled())
0643 return EventTimingSample::Capture(EventTimingCapture::Monotonic);
0644
0645 const EventTimingSample sample = EventTimingSample::Capture(
0646 EventTimingCapture::Monotonic | EventTimingCapture::Wall | EventTimingCapture::Memory);
0647 Add(name, sample, metadata);
0648 return sample;
0649 }
0650
0651 EventTimingSample EventTimingProfile::MarkUntagged(
0652 std::string_view name,
0653 std::string_view metadata)
0654 {
0655 std::string tag = std::move(profile_tag);
0656 profile_tag.clear();
0657 const EventTimingSample sample = Mark(name, metadata);
0658 profile_tag = std::move(tag);
0659 return sample;
0660 }
0661
0662 void EventTimingProfile::Add(
0663 std::string_view name,
0664 const EventTimingSample& sample,
0665 std::string_view metadata)
0666 {
0667 if (!Enabled())
0668 return;
0669
0670 EventTimingProfileRecord record{
0671 profile_tag + std::string(name),
0672 sample,
0673 std::string(metadata),
0674 };
0675 std::lock_guard<std::mutex> lock(ProfileMutex());
0676 auto& records = ProfileRecords();
0677 const auto position = std::upper_bound(
0678 records.begin(),
0679 records.end(),
0680 record.sample.steady_time_ns,
0681 [](std::int64_t timestamp, const EventTimingProfileRecord& existing) {
0682 return timestamp < existing.sample.steady_time_ns;
0683 });
0684 records.insert(position, std::move(record));
0685 }
0686
0687 void EventTimingProfile::SetTag(int index, std::string_view format)
0688 {
0689 profile_tag = FormatIndex(format, index);
0690 }
0691
0692 bool EventTimingProfile::HasTag()
0693 {
0694 return !profile_tag.empty();
0695 }
0696
0697 std::string EventTimingProfile::Tag()
0698 {
0699 return profile_tag;
0700 }
0701
0702 void EventTimingProfile::UnsetTag()
0703 {
0704 profile_tag.clear();
0705 }
0706
0707 std::string EventTimingProfile::Annotation(
0708 std::initializer_list<std::pair<std::string_view, std::uint64_t>> values)
0709 {
0710 std::ostringstream out;
0711 bool first = true;
0712 for (const auto& [name, value] : values)
0713 {
0714 if (!first)
0715 out << ',';
0716 out << name << '=' << value;
0717 first = false;
0718 }
0719 return out.str();
0720 }
0721
0722 std::int64_t EventTimingProfile::DeltaRssKb()
0723 {
0724 std::lock_guard<std::mutex> lock(ProfileMutex());
0725 const auto& records = ProfileRecords();
0726 if (records.size() < 2u)
0727 return -1;
0728 const auto& begin = records[records.size() - 2u].sample;
0729 const auto& end = records.back().sample;
0730 if (!begin.has(EventTimingCapture::Memory) || !end.has(EventTimingCapture::Memory))
0731 return -1;
0732 return EventTimingSample::deltaRssKb(begin, end);
0733 }
0734
0735 std::int64_t EventTimingProfile::RangeRssKb()
0736 {
0737 std::lock_guard<std::mutex> lock(ProfileMutex());
0738 const auto& records = ProfileRecords();
0739 if (records.empty())
0740 return -1;
0741 const auto& begin = records.front().sample;
0742 const auto& end = records.back().sample;
0743 if (!begin.has(EventTimingCapture::Memory) || !end.has(EventTimingCapture::Memory))
0744 return -1;
0745 return EventTimingSample::deltaRssKb(begin, end);
0746 }
0747
0748 void EventTimingProfile::Clear()
0749 {
0750 std::lock_guard<std::mutex> lock(ProfileMutex());
0751 ProfileRecords().clear();
0752 }
0753
0754 std::size_t EventTimingProfile::Size()
0755 {
0756 std::lock_guard<std::mutex> lock(ProfileMutex());
0757 return ProfileRecords().size();
0758 }
0759
0760 std::vector<EventTimingProfileRecord> EventTimingProfile::Records()
0761 {
0762 std::lock_guard<std::mutex> lock(ProfileMutex());
0763 return ProfileRecords();
0764 }
0765
0766 std::string EventTimingProfile::Describe()
0767 {
0768 std::ostringstream out;
0769 for (const EventTimingProfileRecord& record : Records())
0770 {
0771 out << record.name << ':' << record.sample.desc();
0772 if (!record.metadata.empty())
0773 out << " # " << record.metadata;
0774 out << '\n';
0775 }
0776 return out.str();
0777 }
0778
0779 std::string EventTimingProfile::SerializeCsv(
0780 const std::vector<EventTimingProfileRecord>& records)
0781 {
0782 std::ostringstream out;
0783 out << "name,wall_time_us,steady_time_ns,vm_kb,rss_kb,metadata\n";
0784 for (const EventTimingProfileRecord& record : records)
0785 {
0786 Require(record.sample, EventTimingCapture::Monotonic, "profile serialization");
0787 out << CsvField(record.name) << ','
0788 << OptionalValue(record.sample, EventTimingCapture::Wall, record.sample.wall_time_us) << ','
0789 << record.sample.steady_time_ns << ','
0790 << OptionalValue(record.sample, EventTimingCapture::Memory, record.sample.vm_kb) << ','
0791 << OptionalValue(record.sample, EventTimingCapture::Memory, record.sample.rss_kb) << ','
0792 << CsvField(record.metadata) << '\n';
0793 }
0794 return out.str();
0795 }
0796
0797 std::vector<EventTimingProfileRecord> EventTimingProfile::ParseCsv(
0798 std::istream& input,
0799 const std::filesystem::path& source)
0800 {
0801 const std::vector<CsvRow> rows = ParseRows(input, source);
0802 const std::vector<std::string> header = {
0803 "name", "wall_time_us", "steady_time_ns", "vm_kb", "rss_kb", "metadata"};
0804 if (rows.empty() || rows.front().fields != header)
0805 CsvError(source, 1, "expected EventTiming profile CSV header");
0806
0807 std::vector<EventTimingProfileRecord> records;
0808 records.reserve(rows.size() - 1u);
0809 for (std::size_t row_index = 1; row_index < rows.size(); ++row_index)
0810 {
0811 const CsvRow& row = rows[row_index];
0812 if (row.fields.size() != header.size())
0813 CsvError(source, row.line, "expected six columns");
0814
0815 try
0816 {
0817 EventTimingSample sample{};
0818 sample.steady_time_ns = ParseInteger(row.fields[2], "steady_time_ns");
0819 sample.valid_mask |= EventTimingSample::bit(EventTimingCapture::Monotonic);
0820 if (!row.fields[1].empty())
0821 {
0822 sample.wall_time_us = ParseInteger(row.fields[1], "wall_time_us");
0823 sample.valid_mask |= EventTimingSample::bit(EventTimingCapture::Wall);
0824 }
0825 if (row.fields[3].empty() != row.fields[4].empty())
0826 throw std::invalid_argument("both VM and RSS must be present");
0827 if (!row.fields[3].empty())
0828 {
0829 sample.vm_kb = ParseInteger(row.fields[3], "vm_kb");
0830 sample.rss_kb = ParseInteger(row.fields[4], "rss_kb");
0831 sample.valid_mask |= EventTimingSample::bit(EventTimingCapture::Memory);
0832 }
0833 records.push_back({row.fields[0], sample, row.fields[5]});
0834 }
0835 catch (const std::exception& error)
0836 {
0837 CsvError(source, row.line, error.what());
0838 }
0839 }
0840 return records;
0841 }
0842
0843 std::vector<EventTimingProfileRecord> EventTimingProfile::ReadFile(
0844 const std::filesystem::path& path)
0845 {
0846 std::ifstream input(path, std::ios::binary);
0847 if (!input)
0848 throw std::runtime_error("Unable to open EventTiming profile: " + path.string());
0849 return ParseCsv(input, path);
0850 }
0851
0852 std::filesystem::path EventTimingProfile::Path()
0853 {
0854 const std::string pattern = profile_output.string();
0855 if (pattern.empty())
0856 throw std::invalid_argument("EventTiming profile path is empty");
0857 if (pattern.find('%') == std::string::npos)
0858 return pattern;
0859
0860 return FormatIndex(pattern, profile_path_index);
0861 }
0862
0863 void EventTimingProfile::Write(EventTimingWriteMode mode)
0864 {
0865 if (!Enabled())
0866 return;
0867
0868 const std::filesystem::path destination = Path();
0869 std::vector<EventTimingProfileRecord> records = Records();
0870 if (mode == EventTimingWriteMode::Append && std::filesystem::exists(destination))
0871 {
0872 std::vector<EventTimingProfileRecord> existing = ReadFile(destination);
0873 existing.insert(existing.end(), records.begin(), records.end());
0874 records = std::move(existing);
0875 }
0876
0877 const std::filesystem::path parent = destination.parent_path();
0878 if (!parent.empty())
0879 {
0880 std::error_code directory_error;
0881 std::filesystem::create_directories(parent, directory_error);
0882 if (directory_error)
0883 throw std::runtime_error(
0884 "Unable to create EventTiming profile directory for " + destination.string() + ": " + directory_error.message());
0885 }
0886
0887 const std::filesystem::path temporary = TemporaryPath(destination);
0888 try
0889 {
0890 std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
0891 if (!output)
0892 throw std::runtime_error("Unable to open EventTiming profile: " + temporary.string());
0893 output << SerializeCsv(records);
0894 output.flush();
0895 if (!output)
0896 throw std::runtime_error("Unable to write EventTiming profile: " + temporary.string());
0897 output.close();
0898 if (!output)
0899 throw std::runtime_error("Unable to close EventTiming profile: " + temporary.string());
0900
0901 std::error_code rename_error;
0902 std::filesystem::rename(temporary, destination, rename_error);
0903 if (rename_error)
0904 throw std::runtime_error(
0905 "Unable to replace EventTiming profile " + destination.string() + ": " + rename_error.message());
0906 }
0907 catch (...)
0908 {
0909 std::error_code ignored;
0910 std::filesystem::remove(temporary, ignored);
0911 throw;
0912 }
0913 }
0914
0915 EventTimingRecorder::EventTimingRecorder(
0916 std::filesystem::path output,
0917 SampleProvider provider) :
0918 output_(std::move(output)),
0919 provider_(provider)
0920 {
0921 }
0922
0923 bool EventTimingRecorder::enabled() const
0924 {
0925 return !output_.empty();
0926 }
0927
0928 const std::filesystem::path& EventTimingRecorder::output() const
0929 {
0930 return output_;
0931 }
0932
0933 const char* EventTimingRecorder::StateName(State state)
0934 {
0935 switch (state)
0936 {
0937 case State::Idle:
0938 return "idle";
0939 case State::CpuPre:
0940 return "cpu-pre";
0941 case State::GpuQueued:
0942 return "gpu-queued";
0943 case State::GpuRunning:
0944 return "gpu-running";
0945 case State::CpuPost:
0946 return "cpu-post";
0947 }
0948 return "unknown";
0949 }
0950
0951 EventTimingSample EventTimingRecorder::Capture() const
0952 {
0953 constexpr EventTimingCapture mask =
0954 EventTimingCapture::Monotonic | EventTimingCapture::ProcessCpu | EventTimingCapture::ThreadCpu;
0955 return provider_ == nullptr ? EventTimingSample::Capture(mask) : provider_(mask);
0956 }
0957
0958 EventTimingRecorder::Row& EventTimingRecorder::ActiveRow()
0959 {
0960 if (rows_.empty() || active_event_id_ < 0)
0961 throw std::logic_error("EventTimingRecorder has no active event");
0962 return rows_.back();
0963 }
0964
0965 const EventTimingRecorder::Row& EventTimingRecorder::ActiveRow() const
0966 {
0967 if (rows_.empty() || active_event_id_ < 0)
0968 throw std::logic_error("EventTimingRecorder has no active event");
0969 return rows_.back();
0970 }
0971
0972 void EventTimingRecorder::RequireState(State expected, std::string_view operation) const
0973 {
0974 if (state_ == expected)
0975 return;
0976 throw std::logic_error(
0977 "EventTimingRecorder event " + std::to_string(active_event_id_) + " cannot " + std::string(operation) + " in state " + StateName(state_) + "; expected " + StateName(expected));
0978 }
0979
0980 void EventTimingRecorder::RequireOrdered(
0981 const EventTimingSample& begin,
0982 const EventTimingSample& end,
0983 std::string_view operation) const
0984 {
0985 if (EventTimingSample::elapsedNs(begin, end) >= 0)
0986 return;
0987 throw std::logic_error(
0988 "EventTimingRecorder event " + std::to_string(active_event_id_) + " monotonic clock moved backwards during " + std::string(operation));
0989 }
0990
0991 void EventTimingRecorder::BeginRun()
0992 {
0993 if (!enabled())
0994 return;
0995 RequireState(State::Idle, "begin run");
0996 if (active_event_id_ >= 0)
0997 throw std::logic_error("EventTimingRecorder cannot begin run with an active event");
0998 rows_.clear();
0999 run_origin_ = Capture();
1000 run_started_ = true;
1001 }
1002
1003 void EventTimingRecorder::BeginEvent(int event_id)
1004 {
1005 if (!enabled())
1006 return;
1007 RequireState(State::Idle, "begin event");
1008 if (!run_started_)
1009 throw std::logic_error("EventTimingRecorder BeginEvent requires BeginRun");
1010
1011 Row row{};
1012 row.event_id = event_id;
1013 row.event_start = Capture();
1014 active_event_id_ = event_id;
1015 RequireOrdered(run_origin_, row.event_start, "event start");
1016 rows_.push_back(row);
1017 state_ = State::CpuPre;
1018 }
1019
1020 void EventTimingRecorder::SubmitGpu(
1021 std::int64_t num_gensteps,
1022 std::int64_t num_photons)
1023 {
1024 if (!enabled())
1025 return;
1026 RequireState(State::CpuPre, "submit GPU work");
1027 const EventTimingSample sample = Capture();
1028 Row& row = ActiveRow();
1029 RequireOrdered(row.event_start, sample, "GPU submission");
1030 row.num_gensteps = num_gensteps;
1031 row.num_photons = num_photons;
1032 row.gpu_submit = sample;
1033 state_ = State::GpuQueued;
1034 }
1035
1036 void EventTimingRecorder::BeginGpu()
1037 {
1038 if (!enabled())
1039 return;
1040 RequireState(State::GpuQueued, "begin GPU work");
1041 const EventTimingSample sample = Capture();
1042 Row& row = ActiveRow();
1043 RequireOrdered(row.gpu_submit, sample, "GPU start");
1044 row.gpu_start = sample;
1045 state_ = State::GpuRunning;
1046 }
1047
1048 void EventTimingRecorder::EndGpu()
1049 {
1050 if (!enabled())
1051 return;
1052 RequireState(State::GpuRunning, "end GPU work");
1053 const EventTimingSample sample = Capture();
1054 Row& row = ActiveRow();
1055 RequireOrdered(row.gpu_start, sample, "GPU end");
1056 row.gpu_end = sample;
1057 state_ = State::CpuPost;
1058 }
1059
1060 void EventTimingRecorder::EndEvent(
1061 std::size_t num_gpu_hits,
1062 std::size_t num_g4_hits)
1063 {
1064 if (!enabled())
1065 return;
1066 RequireState(State::CpuPost, "end event");
1067 const EventTimingSample sample = Capture();
1068 Row& row = ActiveRow();
1069 RequireOrdered(row.gpu_end, sample, "event end");
1070 row.num_gpu_hits = num_gpu_hits;
1071 row.num_g4_hits = num_g4_hits;
1072 row.event_end = sample;
1073 state_ = State::Idle;
1074 active_event_id_ = -1;
1075 }
1076
1077 std::string EventTimingRecorder::SerializeCsv(const EventTimingMetadata& metadata) const
1078 {
1079 std::ostringstream csv;
1080 csv << "scenario,dispatch_mode,event_id,"
1081 "start_time,end_time,cpu_start_time,cpu_end_time,cpu_pre_start_time,cpu_pre_end_time,"
1082 "gpu_submit_time,gpu_start_time,gpu_end_time,gpu_wait_start_time,gpu_wait_end_time,"
1083 "cpu_post_start_time,cpu_post_end_time,"
1084 "start_offset_s,end_offset_s,cpu_start_time_offset_s,cpu_end_time_offset_s,"
1085 "cpu_pre_start_time_offset_s,cpu_pre_end_time_offset_s,gpu_submit_time_offset_s,"
1086 "gpu_start_time_offset_s,gpu_end_time_offset_s,gpu_wait_start_time_offset_s,"
1087 "gpu_wait_end_time_offset_s,cpu_post_start_time_offset_s,"
1088 "cpu_post_end_time_offset_s,runtime_s,cpu_pre_runtime_s,cpu_post_runtime_s,cpu_runtime_s,"
1089 "gpu_queue_delay_s,gpu_runtime_s,gpu_wait_runtime_s,"
1090 "primary_particle,primary_momentum_gev_c,primary_multiplicity,"
1091 "num_gensteps,num_photons,num_gpu_hits,num_g4_hits,process_cpu_pre_s,process_cpu_post_s,"
1092 "process_cpu_s,thread_cpu_pre_s,thread_cpu_post_s,thread_cpu_s\n";
1093
1094 csv << std::setprecision(std::numeric_limits<double>::max_digits10);
1095 for (const Row& row : rows_)
1096 {
1097 const auto absolute = [](const EventTimingSample& sample) {
1098 return Seconds(sample.steady_time_ns);
1099 };
1100 const auto offset = [this](const EventTimingSample& sample) {
1101 return Seconds(EventTimingSample::elapsedNs(run_origin_, sample));
1102 };
1103 const auto duration = [](const EventTimingSample& begin, const EventTimingSample& end) {
1104 return Seconds(EventTimingSample::elapsedNs(begin, end));
1105 };
1106
1107 const double cpu_pre_runtime = duration(row.event_start, row.gpu_submit);
1108 const double cpu_post_runtime = duration(row.gpu_end, row.event_end);
1109 const double gpu_queue = duration(row.gpu_submit, row.gpu_start);
1110 const double gpu_runtime = duration(row.gpu_start, row.gpu_end);
1111 const double gpu_wait = duration(row.gpu_submit, row.gpu_end);
1112 const double process_pre = CpuSeconds(
1113 row.event_start, row.gpu_submit, EventTimingCapture::ProcessCpu);
1114 const double process_post = CpuSeconds(
1115 row.gpu_end, row.event_end, EventTimingCapture::ProcessCpu);
1116 const double thread_pre = CpuSeconds(
1117 row.event_start, row.gpu_submit, EventTimingCapture::ThreadCpu);
1118 const double thread_post = CpuSeconds(
1119 row.gpu_end, row.event_end, EventTimingCapture::ThreadCpu);
1120
1121 csv << "simg4ox_blocking,blocking," << row.event_id << ','
1122 << absolute(row.event_start) << ',' << absolute(row.event_end) << ','
1123 << absolute(row.event_start) << ',' << absolute(row.event_end) << ','
1124 << absolute(row.event_start) << ',' << absolute(row.gpu_submit) << ','
1125 << absolute(row.gpu_submit) << ',' << absolute(row.gpu_start) << ','
1126 << absolute(row.gpu_end) << ',' << absolute(row.gpu_submit) << ','
1127 << absolute(row.gpu_end) << ',' << absolute(row.gpu_end) << ','
1128 << absolute(row.event_end) << ','
1129 << offset(row.event_start) << ',' << offset(row.event_end) << ','
1130 << offset(row.event_start) << ',' << offset(row.event_end) << ','
1131 << offset(row.event_start) << ',' << offset(row.gpu_submit) << ','
1132 << offset(row.gpu_submit) << ',' << offset(row.gpu_start) << ','
1133 << offset(row.gpu_end) << ',' << offset(row.gpu_submit) << ','
1134 << offset(row.gpu_end) << ',' << offset(row.gpu_end) << ','
1135 << offset(row.event_end) << ','
1136 << duration(row.event_start, row.event_end) << ','
1137 << cpu_pre_runtime << ',' << cpu_post_runtime << ','
1138 << cpu_pre_runtime + cpu_post_runtime << ','
1139 << gpu_queue << ',' << gpu_runtime << ',' << gpu_wait << ','
1140 << CsvField(metadata.primary_particle) << ','
1141 << metadata.primary_momentum_gev_c << ',' << metadata.primary_multiplicity << ','
1142 << row.num_gensteps << ',' << row.num_photons << ','
1143 << row.num_gpu_hits << ',' << row.num_g4_hits << ','
1144 << process_pre << ',' << process_post << ',' << process_pre + process_post << ','
1145 << thread_pre << ',' << thread_post << ',' << thread_pre + thread_post << '\n';
1146 }
1147 return csv.str();
1148 }
1149
1150 std::string EventTimingRecorder::SerializeManifest(const EventTimingMetadata& metadata) const
1151 {
1152 SMeta manifest;
1153 manifest.js["schema_version"] = 2;
1154 manifest.js["created_utc"] = UtcNow();
1155 manifest.js["application"] = "simg4ox";
1156 manifest.js["dispatch_mode"] = "blocking";
1157 manifest.js["clock"] = "std::chrono::steady_clock";
1158 manifest.js["geometry"] = metadata.geometry;
1159 manifest.js["config"] = metadata.config;
1160 manifest.js["macro"] = metadata.macro;
1161 manifest.js["simphony_version"] = metadata.simphony_version;
1162 manifest.js["geant4_version"] = metadata.geant4_version;
1163 manifest.js["primary_particle"] = metadata.primary_particle;
1164 manifest.js["primary_momentum_gev_c"] = metadata.primary_momentum_gev_c;
1165 manifest.js["primary_multiplicity"] = metadata.primary_multiplicity;
1166 manifest.js["random_seed"] = metadata.random_seed;
1167 manifest.js["gpu_name"] = metadata.gpu_name;
1168 manifest.js["gpu_device_id"] = metadata.gpu_device_id;
1169 manifest.js["gpu_memory_bytes"] = metadata.gpu_memory_bytes;
1170 manifest.js["cuda_driver_version"] = metadata.cuda_driver_version;
1171 manifest.js["cuda_runtime_version"] = metadata.cuda_runtime_version;
1172 manifest.js["event_count"] = rows_.size();
1173 return manifest.js.dump(2) + '\n';
1174 }
1175
1176 std::filesystem::path EventTimingRecorder::ManifestPath() const
1177 {
1178 std::filesystem::path manifest = output_;
1179 manifest.replace_extension(".manifest.json");
1180 return manifest;
1181 }
1182
1183 void EventTimingRecorder::Write(const EventTimingMetadata& metadata) const
1184 {
1185 if (!enabled())
1186 return;
1187 RequireState(State::Idle, "write output");
1188 if (active_event_id_ >= 0)
1189 throw std::logic_error("EventTimingRecorder cannot write with an active event");
1190
1191 const std::filesystem::path manifest = ManifestPath();
1192 const std::filesystem::path parent = output_.parent_path();
1193 if (!parent.empty())
1194 {
1195 std::error_code directory_error;
1196 std::filesystem::create_directories(parent, directory_error);
1197 if (directory_error)
1198 throw std::runtime_error(
1199 "Unable to create timing output directory for " + output_.string() + ": " + directory_error.message());
1200 }
1201
1202 const std::filesystem::path csv_temporary = TemporaryPath(output_);
1203 const std::filesystem::path manifest_temporary = TemporaryPath(manifest);
1204 try
1205 {
1206 WriteChecked(csv_temporary, SerializeCsv(metadata));
1207 WriteChecked(manifest_temporary, SerializeManifest(metadata));
1208 ReplaceFile(csv_temporary, output_);
1209 ReplaceFile(manifest_temporary, manifest);
1210 }
1211 catch (...)
1212 {
1213 std::error_code ignored;
1214 std::filesystem::remove(csv_temporary, ignored);
1215 std::filesystem::remove(manifest_temporary, ignored);
1216 throw;
1217 }
1218 }