Skip to content

Commit e7a1994

Browse files
feat(google_benchmark): add walltime support
1 parent 4772cff commit e7a1994

File tree

8 files changed

+308
-4
lines changed

8 files changed

+308
-4
lines changed

core/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
1212
include_directories(include)
1313

1414
# Add the library
15-
add_library(codspeed src/codspeed.cpp)
15+
add_library(codspeed src/codspeed.cpp src/walltime.cpp)
1616

1717
# Version
1818
add_compile_definitions(CODSPEED_VERSION="${CODSPEED_VERSION}")

core/include/codspeed.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,18 @@ class CodSpeed {
2727
bool is_instrumented;
2828
};
2929

30+
// Times are per iteration
31+
struct RawWalltimeBenchmark {
32+
std::string name;
33+
std::string uri;
34+
long iter_per_round;
35+
double mean_ns;
36+
double median_ns;
37+
double stdev_ns;
38+
std::vector<double> round_times_ns;
39+
};
40+
41+
void generate_codspeed_walltime_report(
42+
const std::vector<RawWalltimeBenchmark> &walltime_data_list);
43+
3044
#endif // CODSPEED_H

core/src/codspeed.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,16 @@ void CodSpeed::pop_group() {
3838
}
3939

4040
void CodSpeed::start_benchmark(const std::string &name) {
41-
current_benchmark = name;
41+
std::string uri = name;
42+
43+
// Sanity check URI and add a placeholder if format is wrong
44+
if (name.find("::") == std::string::npos) {
45+
std::string uri = "unknown_file::" + name;
46+
std::cout << "WARNING: Benchmark name does not contain '::'. Using URI: "
47+
<< uri << std::endl;
48+
}
49+
50+
current_benchmark = uri;
4251
measurement_start();
4352
}
4453

core/src/walltime.cpp

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
#include "codspeed.h"
2+
#include <algorithm>
3+
#include <cmath>
4+
#include <cstdlib>
5+
#include <filesystem>
6+
#include <fstream>
7+
#include <iostream>
8+
#include <numeric>
9+
#include <sstream>
10+
#include <string>
11+
#include <thread>
12+
#include <vector>
13+
14+
const double IQR_OUTLIER_FACTOR = 1.5;
15+
const double STDEV_OUTLIER_FACTOR = 3.0;
16+
17+
// Times are per iteration
18+
struct BenchmarkStats {
19+
double min_ns;
20+
double max_ns;
21+
double mean_ns;
22+
double stdev_ns;
23+
double q1_ns;
24+
double median_ns;
25+
double q3_ns;
26+
uint64_t rounds;
27+
double total_time;
28+
uint64_t iqr_outlier_rounds;
29+
uint64_t stdev_outlier_rounds;
30+
long iter_per_round;
31+
uint64_t warmup_iters;
32+
};
33+
34+
struct BenchmarkMetadata {
35+
std::string name;
36+
std::string uri;
37+
};
38+
39+
struct CodspeedWalltimeBenchmark {
40+
BenchmarkMetadata metadata;
41+
BenchmarkStats stats;
42+
};
43+
44+
double compute_quantile(const std::vector<double> &data, double quantile) {
45+
size_t n = data.size();
46+
if (n == 0)
47+
return 0.0;
48+
49+
double pos = quantile * (n - 1);
50+
size_t k = static_cast<size_t>(pos);
51+
double d = pos - k;
52+
53+
if (k + 1 < n) {
54+
return data[k] + d * (data[k + 1] - data[k]);
55+
}
56+
return data[k];
57+
}
58+
59+
void compute_iqr_and_outliers(const std::vector<double> &times_ns, double mean,
60+
double median, double stdev, double &q1,
61+
double &q3, double &iqr,
62+
size_t &iqr_outlier_rounds,
63+
size_t &stdev_outlier_rounds) {
64+
std::vector<double> sorted_times = times_ns;
65+
std::sort(sorted_times.begin(), sorted_times.end());
66+
67+
q1 = compute_quantile(sorted_times, 0.25);
68+
q3 = compute_quantile(sorted_times, 0.75);
69+
70+
iqr = q3 - q1;
71+
72+
const double IQR_OUTLIER_FACTOR = 1.5;
73+
const double STDEV_OUTLIER_FACTOR = 3.0;
74+
75+
iqr_outlier_rounds =
76+
std::count_if(sorted_times.begin(), sorted_times.end(),
77+
[q1, q3, iqr, IQR_OUTLIER_FACTOR](double x) {
78+
return x < q1 - IQR_OUTLIER_FACTOR * iqr ||
79+
x > q3 + IQR_OUTLIER_FACTOR * iqr;
80+
});
81+
82+
stdev_outlier_rounds =
83+
std::count_if(sorted_times.begin(), sorted_times.end(),
84+
[mean, stdev, STDEV_OUTLIER_FACTOR](double x) {
85+
return x < mean - STDEV_OUTLIER_FACTOR * stdev ||
86+
x > mean + STDEV_OUTLIER_FACTOR * stdev;
87+
});
88+
}
89+
90+
void write_codspeed_benchmarks_to_json(
91+
const std::vector<CodspeedWalltimeBenchmark> &benchmarks) {
92+
std::ostringstream oss;
93+
94+
std::string creator_name = "codspeed-cpp";
95+
std::string creator_version = CODSPEED_VERSION;
96+
std::thread::id creator_pid = std::this_thread::get_id();
97+
std::string instrument_type = "walltime";
98+
99+
oss << "{\n";
100+
oss << " \"creator\": {\n";
101+
oss << " \"name\": \"" << creator_name << "\",\n";
102+
oss << " \"version\": \"" << creator_version << "\",\n";
103+
oss << " \"pid\": " << creator_pid << "\n";
104+
oss << " },\n";
105+
oss << " \"instrument\": {\n";
106+
oss << " \"type\": \"" << instrument_type << "\"\n";
107+
oss << " },\n";
108+
oss << " \"benchmarks\": [\n";
109+
110+
for (size_t i = 0; i < benchmarks.size(); ++i) {
111+
const auto &benchmark = benchmarks[i];
112+
const auto &stats = benchmark.stats;
113+
const auto &metadata = benchmark.metadata;
114+
115+
oss << " {\n";
116+
oss << " \"name\": \"" << metadata.name << "\",\n";
117+
oss << " \"uri\": \"" << metadata.uri << "\",\n";
118+
// TODO: Manage config fields from actual configuration
119+
oss << " \"config\": {\n";
120+
oss << " \"warmup_time_ns\": null,\n";
121+
oss << " \"min_round_time_ns\": null,\n";
122+
oss << " \"max_time_ns\": null,\n";
123+
oss << " \"max_rounds\": null\n";
124+
oss << " },\n";
125+
oss << " \"stats\": {\n";
126+
oss << " \"min_ns\": " << stats.min_ns << ",\n";
127+
oss << " \"max_ns\": " << stats.max_ns << ",\n";
128+
oss << " \"mean_ns\": " << stats.mean_ns << ",\n";
129+
oss << " \"stdev_ns\": " << stats.stdev_ns << ",\n";
130+
oss << " \"q1_ns\": " << stats.q1_ns << ",\n";
131+
oss << " \"median_ns\": " << stats.median_ns << ",\n";
132+
oss << " \"q3_ns\": " << stats.q3_ns << ",\n";
133+
oss << " \"rounds\": " << stats.rounds << ",\n";
134+
oss << " \"total_time\": " << stats.total_time << ",\n";
135+
oss << " \"iqr_outlier_rounds\": " << stats.iqr_outlier_rounds
136+
<< ",\n";
137+
oss << " \"stdev_outlier_rounds\": " << stats.stdev_outlier_rounds
138+
<< ",\n";
139+
oss << " \"iter_per_round\": " << stats.iter_per_round << ",\n";
140+
oss << " \"warmup_iters\": " << stats.warmup_iters << "\n";
141+
oss << " }\n";
142+
oss << " }";
143+
144+
if (i < benchmarks.size() - 1) {
145+
oss << ",";
146+
}
147+
oss << "\n";
148+
}
149+
150+
oss << " ]\n";
151+
oss << "}";
152+
153+
// Determine the directory path
154+
const char *profile_folder = std::getenv("CODSPEED_PROFILE_FOLDER");
155+
std::string directory = profile_folder ? profile_folder : ".";
156+
157+
// Create the results directory if it does not exist
158+
std::filesystem::path results_path = directory + "/results";
159+
if (!std::filesystem::exists(results_path)) {
160+
if (!std::filesystem::create_directories(results_path)) {
161+
std::cerr << "Failed to create directory: " << results_path << std::endl;
162+
return;
163+
}
164+
}
165+
166+
// Create the file path
167+
std::ostringstream file_path;
168+
file_path << results_path.string() << "/" << creator_pid << ".json";
169+
170+
// Write to file
171+
std::ofstream out_file(file_path.str());
172+
if (out_file.is_open()) {
173+
out_file << oss.str();
174+
out_file.close();
175+
std::cout << "JSON written to " << file_path.str() << std::endl;
176+
} else {
177+
std::cerr << "Unable to open file " << file_path.str() << std::endl;
178+
}
179+
}
180+
181+
void generate_codspeed_walltime_report(
182+
const std::vector<RawWalltimeBenchmark> &raw_walltime_benchmarks) {
183+
std::vector<CodspeedWalltimeBenchmark> codspeed_walltime_benchmarks;
184+
185+
for (const auto &raw_benchmark : raw_walltime_benchmarks) {
186+
CodspeedWalltimeBenchmark codspeed_benchmark;
187+
codspeed_benchmark.metadata = {raw_benchmark.name, raw_benchmark.uri};
188+
189+
double total_time =
190+
std::accumulate(raw_benchmark.round_times_ns.begin(),
191+
raw_benchmark.round_times_ns.end(), 0.0);
192+
193+
double mean = raw_benchmark.mean_ns;
194+
double median = raw_benchmark.median_ns;
195+
double stdev = raw_benchmark.stdev_ns;
196+
double q1, q3, iqr;
197+
size_t iqr_outlier_rounds, stdev_outlier_rounds;
198+
compute_iqr_and_outliers(raw_benchmark.round_times_ns, mean, median, stdev,
199+
q1, q3, iqr, iqr_outlier_rounds,
200+
stdev_outlier_rounds);
201+
202+
// Populate stats
203+
codspeed_benchmark.stats = {
204+
*std::min_element(raw_benchmark.round_times_ns.begin(),
205+
raw_benchmark.round_times_ns.end()),
206+
*std::max_element(raw_benchmark.round_times_ns.begin(),
207+
raw_benchmark.round_times_ns.end()),
208+
mean,
209+
stdev,
210+
q1,
211+
median,
212+
q3,
213+
raw_benchmark.round_times_ns.size(),
214+
total_time,
215+
iqr_outlier_rounds,
216+
stdev_outlier_rounds,
217+
raw_benchmark.iter_per_round,
218+
0 // TODO: warmup_iters
219+
};
220+
221+
codspeed_walltime_benchmarks.push_back(codspeed_benchmark);
222+
}
223+
224+
write_codspeed_benchmarks_to_json(codspeed_walltime_benchmarks);
225+
}

google_benchmark/cmake/Codspeed.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ target_compile_definitions(
2424
)
2525

2626
if(DEFINED CODSPEED_MODE)
27+
target_compile_definitions(codspeed INTERFACE -DCODSPEED_ENABLED)
2728
# Define a preprocessor macro based on the build mode
2829
if(CODSPEED_MODE STREQUAL "instrumentation")
2930
target_compile_definitions(

google_benchmark/include/benchmark/benchmark.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1438,7 +1438,7 @@ class Fixture : public internal::Benchmark {
14381438
static ::benchmark::internal::Benchmark const* const BENCHMARK_PRIVATE_NAME( \
14391439
n) [[maybe_unused]]
14401440

1441-
#ifdef CODSPEED_INSTRUMENTATION
1441+
#ifdef CODSPEED_ENABLED
14421442
#include <filesystem>
14431443
#define BENCHMARK(...) \
14441444
BENCHMARK_PRIVATE_DECLARE(_benchmark_) = \

google_benchmark/src/benchmark.cc

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,51 @@ void FlushStreams(BenchmarkReporter* reporter) {
349349
std::flush(reporter->GetErrorStream());
350350
}
351351

352+
#ifdef CODSPEED_WALLTIME
353+
// We use real time by default, but we could offer CPU time usage as a build
354+
// option, open an issue if you need it.
355+
RawWalltimeBenchmark generate_raw_walltime_data(const RunResults& run_results) {
356+
RawWalltimeBenchmark walltime_data;
357+
358+
for (const auto& run : run_results.non_aggregates) {
359+
walltime_data.uri = run.benchmark_name();
360+
size_t pos = walltime_data.uri.rfind("::");
361+
362+
if (pos != std::string::npos) {
363+
walltime_data.name = walltime_data.uri.substr(pos + 2);
364+
} else {
365+
// Fallback to a placeholder uri, but something is wrong
366+
walltime_data.name = walltime_data.uri;
367+
walltime_data.uri = "unknown_file::" + walltime_data.uri;
368+
}
369+
370+
walltime_data.iter_per_round = run.iterations;
371+
walltime_data.round_times_ns.push_back(run.GetAdjustedRealTime());
372+
}
373+
374+
if (run_results.aggregates_only.empty()) {
375+
// If run has no aggreagates, it means that only one round was performed.
376+
// Use this time as a mean, median, and set stdev to 0.
377+
double only_round_time_ns = walltime_data.round_times_ns[0];
378+
walltime_data.mean_ns = only_round_time_ns;
379+
walltime_data.median_ns = only_round_time_ns;
380+
walltime_data.stdev_ns = 0;
381+
} else {
382+
for (const auto& aggregate_run : run_results.aggregates_only) {
383+
if (aggregate_run.aggregate_name == "mean") {
384+
walltime_data.mean_ns = aggregate_run.GetAdjustedRealTime();
385+
} else if (aggregate_run.aggregate_name == "median") {
386+
walltime_data.median_ns = aggregate_run.GetAdjustedRealTime();
387+
} else if (aggregate_run.aggregate_name == "stddev") {
388+
walltime_data.stdev_ns = aggregate_run.GetAdjustedRealTime();
389+
}
390+
}
391+
}
392+
393+
return walltime_data;
394+
}
395+
#endif
396+
352397
// Reports in both display and file reporters.
353398
void Report(BenchmarkReporter* display_reporter,
354399
BenchmarkReporter* file_reporter, const RunResults& run_results) {
@@ -481,6 +526,9 @@ void RunBenchmarks(const std::vector<BenchmarkInstance>& benchmarks,
481526
std::shuffle(repetition_indices.begin(), repetition_indices.end(), g);
482527
}
483528

529+
#ifdef CODSPEED_WALLTIME
530+
std::vector<RawWalltimeBenchmark> codspeed_walltime_data;
531+
#endif
484532
for (size_t repetition_index : repetition_indices) {
485533
internal::BenchmarkRunner& runner = runners[repetition_index];
486534
runner.DoOneRepetition();
@@ -511,8 +559,15 @@ void RunBenchmarks(const std::vector<BenchmarkInstance>& benchmarks,
511559
}
512560
}
513561

562+
#ifdef CODSPEED_WALLTIME
563+
codspeed_walltime_data.push_back(generate_raw_walltime_data(run_results));
564+
#endif
565+
514566
Report(display_reporter, file_reporter, run_results);
515567
}
568+
#ifdef CODSPEED_WALLTIME
569+
generate_codspeed_walltime_report(codspeed_walltime_data);
570+
#endif
516571
}
517572
display_reporter->Finalize();
518573
if (file_reporter != nullptr) {

google_benchmark/src/benchmark_name.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
namespace benchmark {
1818

19-
#ifdef CODSPEED_INSTRUMENTATION
19+
#ifdef CODSPEED_ENABLED
2020
BENCHMARK_EXPORT std::string BenchmarkName::str() const {
2121
if (args.empty()) {
2222
return function_name;

0 commit comments

Comments
 (0)