Line data Source code
1 : // benchmark_io.cpp
2 : // Serialization helpers (CSV/JSON write) and environment metadata capture for
3 : // BenchmarkSuite. Extracted from benchmark_suite.cpp to keep the run-loop
4 : // translation unit focused on timing logic.
5 :
6 : #include "benchmark_suite.h"
7 :
8 : #include <algorithm>
9 : #include <cctype>
10 : #include <cstdio>
11 : #include <ctime>
12 : #include <fstream>
13 : #include <sstream>
14 : #include <thread>
15 :
16 : #if defined(__unix__) || defined(__APPLE__)
17 : #include <sys/utsname.h>
18 : #endif
19 : #ifdef __APPLE__
20 : #include <sys/sysctl.h>
21 : #endif
22 : #ifdef _WIN32
23 : #define NOMINMAX
24 : #include <windows.h>
25 : #endif
26 :
27 : namespace hashbrowns {
28 :
29 : namespace {
30 :
31 8 : void write_string_array(std::ofstream& out, const std::vector<std::string>& values) {
32 8 : out << "[";
33 8 : for (std::size_t i = 0; i < values.size(); ++i) {
34 0 : out << "\"" << values[i] << "\"";
35 0 : if (i + 1 < values.size())
36 0 : out << ",";
37 : }
38 8 : out << "]";
39 8 : }
40 :
41 4 : void write_profile_manifest(std::ofstream& out, const BenchmarkConfig& config) {
42 4 : out << " \"profile_manifest\": {\n";
43 4 : out << " \"selected_profile\": \"" << config.profile_name << "\",\n";
44 4 : out << " \"applied_defaults\": ";
45 4 : write_string_array(out, config.profile_applied_defaults);
46 4 : out << ",\n";
47 4 : out << " \"explicit_overrides\": ";
48 4 : write_string_array(out, config.profile_explicit_overrides);
49 4 : out << "\n }";
50 4 : }
51 :
52 : } // namespace
53 :
54 : // ---------------------------------------------------------------------------
55 : // Environment metadata helpers (file-local)
56 : // ---------------------------------------------------------------------------
57 :
58 1 : static std::string read_cpu_governor() {
59 : #ifdef __linux__
60 1 : std::ifstream f("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor");
61 1 : std::string g;
62 1 : if (f)
63 0 : std::getline(f, g);
64 2 : return g;
65 : #else
66 : return "unknown";
67 : #endif
68 1 : }
69 :
70 1 : static std::string git_commit_sha() {
71 : #ifdef _WIN32
72 : return "unknown";
73 : #else
74 1 : FILE* pipe = popen("git rev-parse --short HEAD 2>/dev/null", "r");
75 1 : if (!pipe)
76 0 : return "unknown";
77 1 : char buf[64];
78 1 : std::string out;
79 1 : if (fgets(buf, sizeof(buf), pipe))
80 1 : out = buf;
81 1 : pclose(pipe);
82 1 : if (!out.empty() && out.back() == '\n')
83 1 : out.pop_back();
84 1 : return out.empty() ? "unknown" : out;
85 : #endif
86 1 : }
87 :
88 1 : static std::string cpu_model() {
89 : #ifdef __linux__
90 1 : std::ifstream in("/proc/cpuinfo");
91 1 : std::string line;
92 5 : while (std::getline(in, line)) {
93 5 : auto pos = line.find(":");
94 5 : if (pos == std::string::npos)
95 0 : continue;
96 5 : auto key = line.substr(0, pos);
97 5 : if (key.find("model name") != std::string::npos) {
98 1 : std::string val = line.substr(pos + 1);
99 1 : val.erase(0, val.find_first_not_of(" \t"));
100 1 : return val;
101 1 : }
102 5 : }
103 0 : return "unknown";
104 : #elif defined(__APPLE__)
105 : char buf[256];
106 : size_t sz = sizeof(buf);
107 : if (sysctlbyname("machdep.cpu.brand_string", &buf, &sz, nullptr, 0) == 0) {
108 : return std::string(buf, strnlen(buf, sizeof(buf)));
109 : }
110 : return "unknown";
111 : #else
112 : return "unknown";
113 : #endif
114 1 : }
115 :
116 1 : static unsigned long long total_ram_bytes() {
117 : #ifdef __linux__
118 1 : std::ifstream in("/proc/meminfo");
119 1 : std::string key, unit;
120 1 : unsigned long long val = 0ULL;
121 1 : while (in >> key >> val >> unit) {
122 1 : if (key == "MemTotal:") {
123 1 : if (unit == "kB")
124 1 : return val * 1024ULL;
125 0 : return val;
126 : }
127 : }
128 0 : return 0ULL;
129 : #elif defined(__APPLE__)
130 : uint64_t mem = 0;
131 : size_t sz = sizeof(mem);
132 : if (sysctlbyname("hw.memsize", &mem, &sz, nullptr, 0) == 0)
133 : return static_cast<unsigned long long>(mem);
134 : return 0ULL;
135 : #else
136 : return 0ULL;
137 : #endif
138 1 : }
139 :
140 1 : static std::string kernel_version() {
141 : #if defined(__unix__) || defined(__APPLE__)
142 1 : struct utsname u{};
143 1 : if (uname(&u) == 0) {
144 1 : std::ostringstream oss;
145 1 : oss << u.sysname << " " << u.release;
146 1 : return oss.str();
147 1 : }
148 0 : return "unknown";
149 : #else
150 : return "unknown";
151 : #endif
152 : }
153 :
154 1 : static const char* cpp_standard_str() {
155 : #if __cplusplus >= 202302L
156 : return "C++23+";
157 : #elif __cplusplus >= 202002L
158 : return "C++20";
159 : #elif __cplusplus >= 201703L
160 1 : return "C++17";
161 : #elif __cplusplus >= 201402L
162 : return "C++14";
163 : #elif __cplusplus >= 201103L
164 : return "C++11";
165 : #else
166 : return "pre-C++11";
167 : #endif
168 : }
169 :
170 1 : static const char* build_type_str() {
171 : #ifdef HASHBROWNS_BUILD_TYPE
172 1 : return HASHBROWNS_BUILD_TYPE;
173 : #else
174 : return "Unknown";
175 : #endif
176 : }
177 :
178 : // ---------------------------------------------------------------------------
179 : // Pattern / HashStrategy -> string helpers (file-local)
180 : // ---------------------------------------------------------------------------
181 :
182 4 : static const char* to_string(BenchmarkConfig::Pattern p) {
183 : using P = BenchmarkConfig::Pattern;
184 4 : switch (p) {
185 4 : case P::SEQUENTIAL:
186 4 : return "sequential";
187 0 : case P::RANDOM:
188 0 : return "random";
189 0 : case P::MIXED:
190 0 : return "mixed";
191 : }
192 0 : return "sequential";
193 : }
194 :
195 1 : static const char* to_string(HashStrategy s) {
196 1 : switch (s) {
197 1 : case HashStrategy::OPEN_ADDRESSING:
198 1 : return "open";
199 0 : case HashStrategy::SEPARATE_CHAINING:
200 0 : return "chain";
201 : }
202 0 : return "open";
203 : }
204 :
205 : // ---------------------------------------------------------------------------
206 : // write_results_csv (file-local helper called from BenchmarkSuite::run)
207 : // ---------------------------------------------------------------------------
208 :
209 1 : void write_results_csv_impl(const std::string& path, const std::vector<BenchmarkResult>& results,
210 : [[maybe_unused]] const BenchmarkConfig& cfg, unsigned long long actual_seed) {
211 1 : std::ofstream out(path);
212 1 : if (!out)
213 0 : return;
214 : out << "structure,seed,insert_ms_mean,insert_ms_stddev,insert_ms_median,insert_ms_p95,insert_ci_low,insert_ci_"
215 1 : "high,";
216 1 : out << "search_ms_mean,search_ms_stddev,search_ms_median,search_ms_p95,search_ci_low,search_ci_high,";
217 1 : out << "remove_ms_mean,remove_ms_stddev,remove_ms_median,remove_ms_p95,remove_ci_low,remove_ci_high,";
218 : out << "memory_bytes,memory_insert_mean,memory_insert_stddev,memory_search_mean,memory_search_stddev,memory_remove_"
219 1 : "mean,memory_remove_stddev,";
220 : out << "insert_probes_mean,insert_probes_stddev,search_probes_mean,search_probes_stddev,remove_probes_mean,remove_"
221 1 : "probes_stddev\n";
222 2 : for (const auto& r : results) {
223 1 : out << r.structure << "," << actual_seed << "," << r.insert_ms_mean << "," << r.insert_ms_stddev << ","
224 1 : << r.insert_ms_median << "," << r.insert_ms_p95 << "," << r.insert_ci_low << "," << r.insert_ci_high << ","
225 1 : << r.search_ms_mean << "," << r.search_ms_stddev << "," << r.search_ms_median << "," << r.search_ms_p95
226 1 : << "," << r.search_ci_low << "," << r.search_ci_high << "," << r.remove_ms_mean << "," << r.remove_ms_stddev
227 1 : << "," << r.remove_ms_median << "," << r.remove_ms_p95 << "," << r.remove_ci_low << "," << r.remove_ci_high
228 1 : << "," << r.memory_bytes << "," << r.memory_insert_bytes_mean << "," << r.memory_insert_bytes_stddev << ","
229 1 : << r.memory_search_bytes_mean << "," << r.memory_search_bytes_stddev << "," << r.memory_remove_bytes_mean
230 1 : << "," << r.memory_remove_bytes_stddev << "," << r.insert_probes_mean << "," << r.insert_probes_stddev
231 1 : << "," << r.search_probes_mean << "," << r.search_probes_stddev << "," << r.remove_probes_mean << ","
232 1 : << r.remove_probes_stddev << "\n";
233 : }
234 1 : }
235 :
236 : // ---------------------------------------------------------------------------
237 : // write_results_json (file-local helper called from BenchmarkSuite::run)
238 : // ---------------------------------------------------------------------------
239 :
240 1 : void write_results_json_impl(const std::string& path, const std::vector<BenchmarkResult>& results,
241 : const BenchmarkConfig& config, unsigned long long actual_seed) {
242 1 : std::ofstream out(path);
243 1 : if (!out)
244 0 : return;
245 1 : out << "{\n";
246 1 : out << " \"meta\": {\n";
247 1 : out << " \"schema_version\": 1,\n";
248 1 : out << " \"size\": " << config.size << ",\n";
249 1 : out << " \"runs\": " << config.runs << ",\n";
250 1 : out << " \"warmup_runs\": " << config.warmup_runs << ",\n";
251 1 : out << " \"bootstrap_iters\": " << config.bootstrap_iters << ",\n";
252 1 : out << " \"structures\": [";
253 2 : for (std::size_t i = 0; i < config.structures.size(); ++i) {
254 1 : out << "\"" << config.structures[i] << "\"";
255 1 : if (i + 1 < config.structures.size())
256 0 : out << ",";
257 : }
258 1 : out << "],\n";
259 1 : out << " \"pattern\": \"" << to_string(config.pattern) << "\",\n";
260 1 : out << " \"seed\": " << actual_seed << ",\n";
261 1 : out << " \"timestamp\": \"";
262 : {
263 1 : std::time_t t = std::time(nullptr);
264 1 : char buf[64];
265 1 : std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", std::gmtime(&t));
266 1 : out << buf;
267 : }
268 1 : out << "\",\n";
269 1 : out << " \"cpu_governor\": \"" << read_cpu_governor() << "\",\n";
270 1 : out << " \"git_commit\": \"" << git_commit_sha() << "\",\n";
271 2 : auto compiler_version = []() -> std::string {
272 : #if defined(__clang__)
273 : return std::string("Clang ") + __clang_version__;
274 : #elif defined(_MSC_VER)
275 : return std::string("MSVC ") + std::to_string(_MSC_FULL_VER);
276 : #elif defined(__GNUC__)
277 3 : return std::string("GCC ") + __VERSION__;
278 : #else
279 : return std::string("unknown");
280 : #endif
281 1 : }();
282 1 : out << " \"compiler\": \"" << compiler_version << "\",\n";
283 1 : out << " \"cpp_standard\": \"" << cpp_standard_str() << "\",\n";
284 1 : out << " \"build_type\": \"" << build_type_str() << "\",\n";
285 1 : out << " \"cpu_model\": \"" << cpu_model() << "\",\n";
286 1 : out << " \"profile\": \"" << config.profile_name << "\",\n";
287 1 : out << " \"cores\": " << std::thread::hardware_concurrency() << ",\n";
288 1 : out << " \"total_ram_bytes\": " << total_ram_bytes() << ",\n";
289 1 : out << " \"kernel\": \"" << kernel_version() << "\",\n";
290 1 : out << " \"hash_strategy\": \"" << to_string(config.hash_strategy) << "\"";
291 1 : if (config.hash_initial_capacity)
292 0 : out << ",\n \"hash_capacity\": " << *config.hash_initial_capacity;
293 1 : if (config.hash_max_load_factor)
294 0 : out << ",\n \"hash_load\": " << *config.hash_max_load_factor;
295 1 : out << ",\n \"pinned_cpu\": " << (config.pin_cpu ? config.pin_cpu_index : -1);
296 1 : out << ",\n \"turbo_disabled\": " << (config.disable_turbo ? 1 : 0) << ",\n";
297 1 : write_profile_manifest(out, config);
298 1 : out << "\n },\n";
299 1 : out << " \"results\": [\n";
300 2 : for (std::size_t i = 0; i < results.size(); ++i) {
301 1 : const auto& r = results[i];
302 : out << " {"
303 1 : << "\"structure\": \"" << r.structure << "\","
304 2 : << "\"insert_ms_mean\": " << r.insert_ms_mean << ","
305 1 : << "\"insert_ms_stddev\": " << r.insert_ms_stddev << ","
306 1 : << "\"insert_ms_median\": " << r.insert_ms_median << ","
307 1 : << "\"insert_ms_p95\": " << r.insert_ms_p95 << ","
308 1 : << "\"insert_ci_low\": " << r.insert_ci_low << ","
309 1 : << "\"insert_ci_high\": " << r.insert_ci_high << ","
310 1 : << "\"search_ms_mean\": " << r.search_ms_mean << ","
311 1 : << "\"search_ms_stddev\": " << r.search_ms_stddev << ","
312 1 : << "\"search_ms_median\": " << r.search_ms_median << ","
313 1 : << "\"search_ms_p95\": " << r.search_ms_p95 << ","
314 1 : << "\"search_ci_low\": " << r.search_ci_low << ","
315 1 : << "\"search_ci_high\": " << r.search_ci_high << ","
316 1 : << "\"remove_ms_mean\": " << r.remove_ms_mean << ","
317 1 : << "\"remove_ms_stddev\": " << r.remove_ms_stddev << ","
318 1 : << "\"remove_ms_median\": " << r.remove_ms_median << ","
319 1 : << "\"remove_ms_p95\": " << r.remove_ms_p95 << ","
320 1 : << "\"remove_ci_low\": " << r.remove_ci_low << ","
321 1 : << "\"remove_ci_high\": " << r.remove_ci_high << ","
322 1 : << "\"memory_bytes\": " << r.memory_bytes << ","
323 1 : << "\"memory_insert_mean\": " << r.memory_insert_bytes_mean << ","
324 1 : << "\"memory_insert_stddev\": " << r.memory_insert_bytes_stddev << ","
325 1 : << "\"memory_search_mean\": " << r.memory_search_bytes_mean << ","
326 1 : << "\"memory_search_stddev\": " << r.memory_search_bytes_stddev << ","
327 1 : << "\"memory_remove_mean\": " << r.memory_remove_bytes_mean << ","
328 1 : << "\"memory_remove_stddev\": " << r.memory_remove_bytes_stddev << ","
329 1 : << "\"insert_probes_mean\": " << r.insert_probes_mean << ","
330 1 : << "\"insert_probes_stddev\": " << r.insert_probes_stddev << ","
331 1 : << "\"search_probes_mean\": " << r.search_probes_mean << ","
332 1 : << "\"search_probes_stddev\": " << r.search_probes_stddev << ","
333 1 : << "\"remove_probes_mean\": " << r.remove_probes_mean << ","
334 2 : << "\"remove_probes_stddev\": " << r.remove_probes_stddev << "}" << (i + 1 < results.size() ? "," : "")
335 2 : << "\n";
336 : }
337 1 : out << " ]\n}\n";
338 1 : }
339 :
340 : // ---------------------------------------------------------------------------
341 : // BenchmarkSuite public serialization methods
342 : // ---------------------------------------------------------------------------
343 :
344 1 : void BenchmarkSuite::write_crossover_csv(const std::string& path, const std::vector<CrossoverInfo>& info) {
345 1 : std::ofstream out(path);
346 1 : if (!out)
347 0 : return;
348 1 : out << "operation,a,b,size_at_crossover\n";
349 4 : for (const auto& c : info) {
350 3 : out << c.operation << "," << c.a << "," << c.b << "," << c.size_at_crossover << "\n";
351 : }
352 1 : }
353 :
354 1 : void BenchmarkSuite::write_crossover_json(const std::string& path, const std::vector<CrossoverInfo>& info,
355 : const BenchmarkConfig& config) {
356 1 : std::ofstream out(path);
357 1 : if (!out)
358 0 : return;
359 1 : out << "{\n";
360 1 : out << " \"meta\": {\n";
361 1 : out << " \"schema_version\": 1,\n";
362 1 : out << " \"runs\": " << config.runs << ",\n";
363 1 : out << " \"profile\": \"" << config.profile_name << "\",\n";
364 1 : out << " \"structures\": [";
365 3 : for (std::size_t i = 0; i < config.structures.size(); ++i) {
366 2 : out << "\"" << config.structures[i] << "\"";
367 2 : if (i + 1 < config.structures.size())
368 1 : out << ",";
369 : }
370 1 : out << "],\n";
371 1 : out << " \"pattern\": \"" << to_string(config.pattern) << "\"";
372 1 : if (config.seed.has_value())
373 0 : out << ",\n \"seed\": " << *config.seed;
374 1 : out << ",\n";
375 1 : write_profile_manifest(out, config);
376 1 : out << "\n },\n";
377 1 : out << " \"crossovers\": [\n";
378 4 : for (std::size_t i = 0; i < info.size(); ++i) {
379 3 : const auto& c = info[i];
380 : out << " {"
381 3 : << "\"operation\": \"" << c.operation << "\","
382 3 : << "\"a\": \"" << c.a << "\","
383 3 : << "\"b\": \"" << c.b << "\","
384 12 : << "\"size_at_crossover\": " << c.size_at_crossover << "}" << (i + 1 < info.size() ? "," : "") << "\n";
385 : }
386 1 : out << " ]\n}\n";
387 1 : }
388 :
389 1 : void BenchmarkSuite::write_series_csv(const std::string& path, const Series& series) {
390 1 : std::ofstream out(path);
391 1 : if (!out)
392 0 : return;
393 1 : out << "size,structure,insert_ms,search_ms,remove_ms\n";
394 5 : for (const auto& p : series) {
395 4 : out << p.size << "," << p.structure << "," << p.insert_ms << "," << p.search_ms << "," << p.remove_ms << "\n";
396 : }
397 1 : }
398 :
399 2 : void BenchmarkSuite::write_series_json(const std::string& path, const Series& series, const BenchmarkConfig& config) {
400 2 : std::ofstream out(path);
401 2 : if (!out)
402 0 : return;
403 2 : out << "{\n";
404 2 : out << " \"meta\": {\n";
405 2 : out << " \"schema_version\": 1,\n";
406 2 : out << " \"runs_per_size\": " << config.runs << ",\n";
407 2 : out << " \"profile\": \"" << config.profile_name << "\",\n";
408 2 : out << " \"structures\": [";
409 6 : for (std::size_t i = 0; i < config.structures.size(); ++i) {
410 4 : out << "\"" << config.structures[i] << "\"";
411 4 : if (i + 1 < config.structures.size())
412 2 : out << ",";
413 : }
414 2 : out << "],\n";
415 2 : out << " \"pattern\": \"" << to_string(config.pattern) << "\"";
416 2 : if (config.seed.has_value())
417 0 : out << ",\n \"seed\": " << *config.seed;
418 2 : out << ",\n";
419 2 : write_profile_manifest(out, config);
420 2 : out << "\n },\n";
421 2 : out << " \"series\": [\n";
422 10 : for (std::size_t i = 0; i < series.size(); ++i) {
423 8 : const auto& p = series[i];
424 16 : out << " {\"size\": " << p.size << ", \"structure\": \"" << p.structure << "\""
425 16 : << ", \"insert_ms\": " << p.insert_ms << ", \"search_ms\": " << p.search_ms
426 8 : << ", \"remove_ms\": " << p.remove_ms << "}" << (i + 1 < series.size() ? "," : "") << "\n";
427 : }
428 2 : out << " ]\n}\n";
429 2 : }
430 :
431 : } // namespace hashbrowns
|