File indexing completed on 2026-10-20 08:45:52
0001 #include <cassert>
0002 #include <cmath>
0003 #include <cstdint>
0004 #include <filesystem>
0005 #include <fstream>
0006 #include <functional>
0007 #include <stdexcept>
0008 #include <string>
0009 #include <string_view>
0010 #include <unistd.h>
0011 #include <unordered_map>
0012 #include <vector>
0013
0014 #include "EventTiming.hh"
0015 #include "nlohmann/json.hpp"
0016
0017 namespace
0018 {
0019 namespace fs = std::filesystem;
0020
0021 std::vector<EventTimingSample> sequence;
0022 std::size_t sequence_index = 0;
0023
0024 EventTimingSample TimedSample(
0025 std::int64_t steady_ns,
0026 std::int64_t process_cpu_ns,
0027 std::int64_t thread_cpu_ns)
0028 {
0029 EventTimingSample sample{};
0030 sample.valid_mask = EventTimingSample::bit(EventTimingCapture::Monotonic) | EventTimingSample::bit(EventTimingCapture::ProcessCpu) | EventTimingSample::bit(EventTimingCapture::ThreadCpu);
0031 sample.steady_time_ns = steady_ns;
0032 sample.process_cpu_ns = process_cpu_ns;
0033 sample.thread_cpu_ns = thread_cpu_ns;
0034 return sample;
0035 }
0036
0037 EventTimingSample NextSample(EventTimingCapture mask)
0038 {
0039 assert((EventTimingSample::bit(mask) & EventTimingSample::bit(EventTimingCapture::Monotonic)) != 0u);
0040 assert((EventTimingSample::bit(mask) & EventTimingSample::bit(EventTimingCapture::ProcessCpu)) != 0u);
0041 assert((EventTimingSample::bit(mask) & EventTimingSample::bit(EventTimingCapture::ThreadCpu)) != 0u);
0042 assert(sequence_index < sequence.size());
0043 return sequence[sequence_index++];
0044 }
0045
0046 void SetSequence(std::initializer_list<EventTimingSample> samples)
0047 {
0048 sequence.assign(samples.begin(), samples.end());
0049 sequence_index = 0;
0050 }
0051
0052 fs::path TestDirectory()
0053 {
0054 return fs::temp_directory_path() /
0055 ("EventTimingRecorderTest-" + std::to_string(static_cast<long long>(::getpid())));
0056 }
0057
0058 std::vector<std::string> Split(std::string_view text, char delimiter)
0059 {
0060 std::vector<std::string> fields;
0061 std::size_t start = 0;
0062 for (std::size_t index = 0; index <= text.size(); ++index)
0063 {
0064 if (index == text.size() || text[index] == delimiter)
0065 {
0066 fields.emplace_back(text.substr(start, index - start));
0067 start = index + 1;
0068 }
0069 }
0070 return fields;
0071 }
0072
0073 std::unordered_map<std::string, std::string> ReadSingleRow(const fs::path& path)
0074 {
0075 std::ifstream input(path);
0076 assert(input);
0077 std::string header;
0078 std::string values;
0079 const bool read_header = static_cast<bool>(std::getline(input, header));
0080 const bool read_values = static_cast<bool>(std::getline(input, values));
0081 assert(read_header);
0082 assert(read_values);
0083 std::string extra;
0084 const bool read_extra = static_cast<bool>(std::getline(input, extra));
0085 assert(!read_extra);
0086
0087 const auto names = Split(header, ',');
0088 const auto fields = Split(values, ',');
0089 assert(names.size() == fields.size());
0090 std::unordered_map<std::string, std::string> row;
0091 for (std::size_t index = 0; index < names.size(); ++index)
0092 row.emplace(names[index], fields[index]);
0093 return row;
0094 }
0095
0096 void AssertNear(double actual, double expected)
0097 {
0098 assert(std::abs(actual - expected) < 1e-9);
0099 }
0100
0101 EventTimingMetadata Metadata()
0102 {
0103 EventTimingMetadata metadata{};
0104 metadata.geometry = "geom\"etry\\name\nline";
0105 metadata.config = "config=value";
0106 metadata.macro = "/run/beamOn 1";
0107 metadata.simphony_version = "test-version";
0108 metadata.geant4_version = "11.test";
0109 metadata.primary_particle = "mu-";
0110 metadata.primary_momentum_gev_c = 5.0;
0111 metadata.primary_multiplicity = 100000;
0112 metadata.random_seed = 12345;
0113 metadata.gpu_name = "GPU \"test\"";
0114 metadata.gpu_device_id = 2;
0115 metadata.gpu_memory_bytes = 24'000'000'000ull;
0116 metadata.cuda_driver_version = 12040;
0117 metadata.cuda_runtime_version = 12030;
0118 return metadata;
0119 }
0120
0121 void CompleteOneEvent(EventTimingRecorder& recorder)
0122 {
0123 recorder.BeginRun();
0124 recorder.BeginEvent(0);
0125 recorder.SubmitGpu(4, 500);
0126 recorder.BeginGpu();
0127 recorder.EndGpu();
0128 recorder.EndEvent(25, 7);
0129 }
0130
0131 void TestGoldenCsvAndManifest(const fs::path& directory)
0132 {
0133 const fs::path output = directory / "events.csv";
0134 SetSequence({
0135 TimedSample(1'000'000'000, 100'000, 50'000),
0136 TimedSample(1'100'000'000, 1'000'000, 500'000),
0137 TimedSample(1'300'000'000, 1'300'000, 650'000),
0138 TimedSample(1'350'000'000, 1'350'000, 700'000),
0139 TimedSample(1'750'000'000, 1'600'000, 850'000),
0140 TimedSample(2'000'000'000, 1'900'000, 1'050'000),
0141 });
0142
0143 EventTimingRecorder recorder(output, &NextSample);
0144 CompleteOneEvent(recorder);
0145 recorder.Write(Metadata());
0146 assert(sequence_index == sequence.size());
0147
0148 std::ifstream csv(output);
0149 assert(csv);
0150 std::string header;
0151 const bool read_header = static_cast<bool>(std::getline(csv, header));
0152 assert(read_header);
0153 const std::string expected_header =
0154 "scenario,dispatch_mode,event_id,start_time,end_time,cpu_start_time,cpu_end_time,"
0155 "cpu_pre_start_time,cpu_pre_end_time,gpu_submit_time,gpu_start_time,gpu_end_time,"
0156 "gpu_wait_start_time,gpu_wait_end_time,cpu_post_start_time,cpu_post_end_time,"
0157 "start_offset_s,end_offset_s,cpu_start_time_offset_s,cpu_end_time_offset_s,"
0158 "cpu_pre_start_time_offset_s,cpu_pre_end_time_offset_s,gpu_submit_time_offset_s,"
0159 "gpu_start_time_offset_s,gpu_end_time_offset_s,gpu_wait_start_time_offset_s,"
0160 "gpu_wait_end_time_offset_s,cpu_post_start_time_offset_s,cpu_post_end_time_offset_s,"
0161 "runtime_s,cpu_pre_runtime_s,cpu_post_runtime_s,cpu_runtime_s,gpu_queue_delay_s,"
0162 "gpu_runtime_s,gpu_wait_runtime_s,primary_particle,primary_momentum_gev_c,"
0163 "primary_multiplicity,num_gensteps,num_photons,num_gpu_hits,num_g4_hits,"
0164 "process_cpu_pre_s,process_cpu_post_s,process_cpu_s,thread_cpu_pre_s,"
0165 "thread_cpu_post_s,thread_cpu_s";
0166 assert(header == expected_header);
0167
0168 const auto row = ReadSingleRow(output);
0169 assert(row.at("scenario") == "simg4ox_blocking");
0170 assert(row.at("dispatch_mode") == "blocking");
0171 assert(row.at("event_id") == "0");
0172 assert(row.at("primary_particle") == "mu-");
0173 assert(row.at("num_gensteps") == "4");
0174 assert(row.at("num_photons") == "500");
0175 assert(row.at("num_gpu_hits") == "25");
0176 assert(row.at("num_g4_hits") == "7");
0177
0178 const auto value = [&row](const char* name) { return std::stod(row.at(name)); };
0179 AssertNear(value("cpu_pre_runtime_s"),
0180 value("cpu_pre_end_time_offset_s") - value("cpu_pre_start_time_offset_s"));
0181 AssertNear(value("gpu_queue_delay_s"),
0182 value("gpu_start_time_offset_s") - value("gpu_submit_time_offset_s"));
0183 AssertNear(value("gpu_runtime_s"),
0184 value("gpu_end_time_offset_s") - value("gpu_start_time_offset_s"));
0185 AssertNear(value("cpu_post_runtime_s"),
0186 value("cpu_post_end_time_offset_s") - value("cpu_post_start_time_offset_s"));
0187 AssertNear(value("cpu_runtime_s"),
0188 value("cpu_pre_runtime_s") + value("cpu_post_runtime_s"));
0189 AssertNear(value("gpu_wait_runtime_s"),
0190 value("gpu_queue_delay_s") + value("gpu_runtime_s"));
0191 AssertNear(value("process_cpu_pre_s"), 0.0003);
0192 AssertNear(value("process_cpu_post_s"), 0.0003);
0193 AssertNear(value("thread_cpu_pre_s"), 0.00015);
0194 AssertNear(value("thread_cpu_post_s"), 0.0002);
0195
0196 const fs::path manifest_path = directory / "events.manifest.json";
0197 std::ifstream manifest_input(manifest_path);
0198 assert(manifest_input);
0199 const nlohmann::json manifest = nlohmann::json::parse(manifest_input);
0200 const EventTimingMetadata metadata = Metadata();
0201 assert(manifest.at("schema_version") == 2);
0202 assert(manifest.at("application") == "simg4ox");
0203 assert(manifest.at("dispatch_mode") == "blocking");
0204 assert(manifest.at("event_count") == 1);
0205 assert(manifest.at("geometry") == metadata.geometry);
0206 assert(manifest.at("gpu_name") == metadata.gpu_name);
0207 assert(manifest.at("gpu_device_id") == metadata.gpu_device_id);
0208 assert(manifest.at("gpu_memory_bytes") == metadata.gpu_memory_bytes);
0209 assert(manifest.at("cuda_driver_version") == metadata.cuda_driver_version);
0210 assert(manifest.at("cuda_runtime_version") == metadata.cuda_runtime_version);
0211
0212 for (const fs::directory_entry& entry : fs::directory_iterator(directory))
0213 assert(entry.path().filename().string().find(".tmp.") == std::string::npos);
0214 }
0215
0216 void TestAbsoluteTimestampsRoundTripAtLongUptime(const fs::path& directory)
0217 {
0218 constexpr std::int64_t origin_ns = 10'000'000'000'000'000ll;
0219 constexpr std::int64_t start_ns = origin_ns + 101;
0220 constexpr std::int64_t end_ns = origin_ns + 541;
0221 SetSequence({
0222 TimedSample(origin_ns, 0, 0),
0223 TimedSample(start_ns, 0, 0),
0224 TimedSample(origin_ns + 211, 0, 0),
0225 TimedSample(origin_ns + 307, 0, 0),
0226 TimedSample(origin_ns + 419, 0, 0),
0227 TimedSample(end_ns, 0, 0),
0228 });
0229
0230 const fs::path output = directory / "long-uptime.csv";
0231 EventTimingRecorder recorder(output, &NextSample);
0232 CompleteOneEvent(recorder);
0233 recorder.Write(Metadata());
0234
0235 const auto row = ReadSingleRow(output);
0236 assert(std::stod(row.at("start_time")) == static_cast<double>(start_ns) / 1'000'000'000.0);
0237 assert(std::stod(row.at("end_time")) == static_cast<double>(end_ns) / 1'000'000'000.0);
0238 }
0239
0240 void ExpectLogicError(
0241 const fs::path& output,
0242 const std::function<void(EventTimingRecorder&)>& action)
0243 {
0244 EventTimingRecorder recorder(output);
0245 recorder.BeginRun();
0246 bool threw = false;
0247 try
0248 {
0249 action(recorder);
0250 }
0251 catch (const std::logic_error& error)
0252 {
0253 threw = std::string(error.what()).find("EventTimingRecorder") != std::string::npos;
0254 }
0255 assert(threw);
0256 assert(!fs::exists(output));
0257 fs::path manifest = output;
0258 manifest.replace_extension(".manifest.json");
0259 assert(!fs::exists(manifest));
0260 }
0261
0262 void TestInvalidTransitions(const fs::path& directory)
0263 {
0264 int index = 0;
0265 const auto path = [&] { return directory / ("invalid-" + std::to_string(index++) + ".csv"); };
0266
0267 ExpectLogicError(path(), [](EventTimingRecorder& recorder) { recorder.SubmitGpu(1, 2); });
0268 ExpectLogicError(path(), [](EventTimingRecorder& recorder) { recorder.BeginGpu(); });
0269 ExpectLogicError(path(), [](EventTimingRecorder& recorder) { recorder.EndGpu(); });
0270 ExpectLogicError(path(), [](EventTimingRecorder& recorder) { recorder.EndEvent(1, 2); });
0271 ExpectLogicError(path(), [](EventTimingRecorder& recorder) {
0272 recorder.BeginEvent(0);
0273 recorder.BeginEvent(1);
0274 });
0275 ExpectLogicError(path(), [](EventTimingRecorder& recorder) {
0276 recorder.BeginEvent(0);
0277 recorder.BeginGpu();
0278 });
0279 ExpectLogicError(path(), [](EventTimingRecorder& recorder) {
0280 recorder.BeginEvent(0);
0281 recorder.SubmitGpu(1, 2);
0282 recorder.EndGpu();
0283 });
0284 ExpectLogicError(path(), [](EventTimingRecorder& recorder) {
0285 recorder.BeginEvent(0);
0286 recorder.SubmitGpu(1, 2);
0287 recorder.BeginGpu();
0288 recorder.EndEvent(1, 2);
0289 });
0290 ExpectLogicError(path(), [](EventTimingRecorder& recorder) {
0291 recorder.BeginEvent(0);
0292 recorder.Write(Metadata());
0293 });
0294 ExpectLogicError(path(), [](EventTimingRecorder& recorder) {
0295 recorder.BeginEvent(0);
0296 recorder.BeginRun();
0297 });
0298
0299 EventTimingRecorder disabled;
0300 disabled.SubmitGpu(1, 2);
0301 disabled.BeginGpu();
0302 disabled.EndGpu();
0303 disabled.EndEvent(1, 2);
0304 disabled.Write(Metadata());
0305 }
0306
0307 void TestClockInversion(const fs::path& directory)
0308 {
0309 SetSequence({
0310 TimedSample(100, 0, 0),
0311 TimedSample(200, 0, 0),
0312 TimedSample(150, 0, 0),
0313 });
0314 const fs::path output = directory / "inversion.csv";
0315 EventTimingRecorder recorder(output, &NextSample);
0316 recorder.BeginRun();
0317 recorder.BeginEvent(3);
0318 bool threw = false;
0319 try
0320 {
0321 recorder.SubmitGpu(1, 2);
0322 }
0323 catch (const std::logic_error&)
0324 {
0325 threw = true;
0326 }
0327 assert(threw);
0328 assert(!fs::exists(output));
0329 }
0330
0331 void TestIoFailure(const fs::path& directory)
0332 {
0333 const fs::path regular_file = directory / "not-a-directory";
0334 {
0335 std::ofstream output(regular_file);
0336 output << "file";
0337 }
0338 const fs::path destination = regular_file / "events.csv";
0339 EventTimingRecorder recorder(destination);
0340 CompleteOneEvent(recorder);
0341
0342 bool threw = false;
0343 try
0344 {
0345 recorder.Write(Metadata());
0346 }
0347 catch (const std::runtime_error& error)
0348 {
0349 threw = std::string(error.what()).find(destination.string()) != std::string::npos;
0350 }
0351 assert(threw);
0352 assert(!fs::exists(destination));
0353 }
0354 }
0355
0356 int main()
0357 {
0358 const fs::path directory = TestDirectory();
0359 fs::remove_all(directory);
0360 fs::create_directories(directory);
0361
0362 TestGoldenCsvAndManifest(directory);
0363 TestAbsoluteTimestampsRoundTripAtLongUptime(directory);
0364 TestInvalidTransitions(directory);
0365 TestClockInversion(directory);
0366 TestIoFailure(directory);
0367
0368 fs::remove_all(directory);
0369 return 0;
0370 }