Back to home page

EIC code displayed by LXR

 
 

    


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  * @file EventTiming.cc
0003  * Implements timing capture, lifecycle-profile CSV I/O, and blocking event
0004  * timeline serialization.
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  * Returns the mutex that protects the process-wide profile record collection.
0042  *
0043  * @return mutex shared by all record readers and writers
0044  */
0045 std::mutex& ProfileMutex()
0046 {
0047     static std::mutex mutex;
0048     return mutex;
0049 }
0050 
0051 /**
0052  * Returns the process-wide lifecycle record collection.
0053  *
0054  * Access must be protected by ProfileMutex(). Function-local construction
0055  * avoids static-initialization-order dependencies with instrumented callers.
0056  *
0057  * @return mutable profile record collection
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 }