Skip to content

Commit 3bd55d0

Browse files
fix(core): fix walltime stats computation to handle total round time as an input
The computation is now in line with that codspeed-rust does, the interface is ready for other frameworks that may different iterations count from one round to another.
1 parent 8718448 commit 3bd55d0

File tree

3 files changed

+118
-93
lines changed

3 files changed

+118
-93
lines changed

core/include/codspeed.h

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,10 @@ class CodSpeed {
3434
struct RawWalltimeBenchmark {
3535
std::string name;
3636
std::string uri;
37-
uint64_t iter_per_round;
38-
double mean_ns;
39-
double median_ns;
40-
double stdev_ns;
41-
std::vector<double> round_times_ns;
37+
// Number of iterations of each round
38+
std::vector<uint64_t> iters_per_round;
39+
// Time taken by each round, meaning all its iterations, in nanoseconds
40+
std::vector<double> times_per_round_ns;
4241
};
4342

4443
void generate_codspeed_walltime_report(

core/src/walltime.cpp

Lines changed: 102 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <algorithm>
2+
#include <cassert>
23
#include <cmath>
34
#include <cstdlib>
45
#include <filesystem>
@@ -34,6 +35,27 @@ struct BenchmarkStats {
3435
uint64_t stdev_outlier_rounds;
3536
uint64_t iter_per_round;
3637
uint64_t warmup_iters;
38+
39+
BenchmarkStats(double min_ns = 0.0, double max_ns = 0.0, double mean_ns = 0.0,
40+
double stdev_ns = 0.0, double q1_ns = 0.0,
41+
double median_ns = 0.0, double q3_ns = 0.0,
42+
uint64_t rounds = 0, double total_time = 0.0,
43+
uint64_t iqr_outlier_rounds = 0,
44+
uint64_t stdev_outlier_rounds = 0, uint64_t iter_per_round = 0,
45+
uint64_t warmup_iters = 0)
46+
: min_ns(min_ns),
47+
max_ns(max_ns),
48+
mean_ns(mean_ns),
49+
stdev_ns(stdev_ns),
50+
q1_ns(q1_ns),
51+
median_ns(median_ns),
52+
q3_ns(q3_ns),
53+
rounds(rounds),
54+
total_time(total_time),
55+
iqr_outlier_rounds(iqr_outlier_rounds),
56+
stdev_outlier_rounds(stdev_outlier_rounds),
57+
iter_per_round(iter_per_round),
58+
warmup_iters(warmup_iters) {}
3759
};
3860

3961
struct BenchmarkMetadata {
@@ -46,51 +68,22 @@ struct CodspeedWalltimeBenchmark {
4668
BenchmarkStats stats;
4769
};
4870

49-
double compute_quantile(const std::vector<double> &data, double quantile) {
50-
size_t n = data.size();
71+
static double compute_quantile(const std::vector<double> &sorted_data,
72+
double quantile) {
73+
size_t n = sorted_data.size();
5174
if (n == 0) return 0.0;
5275

5376
double pos = quantile * (n - 1);
5477
size_t k = static_cast<size_t>(pos);
5578
double d = pos - k;
5679

5780
if (k + 1 < n) {
58-
return data[k] + d * (data[k + 1] - data[k]);
81+
return sorted_data[k] + d * (sorted_data[k + 1] - sorted_data[k]);
5982
}
60-
return data[k];
61-
}
62-
63-
void compute_iqr_and_outliers(const std::vector<double> &times_ns, double mean,
64-
double stdev, double &q1, double &q3, double &iqr,
65-
size_t &iqr_outlier_rounds,
66-
size_t &stdev_outlier_rounds) {
67-
std::vector<double> sorted_times = times_ns;
68-
std::sort(sorted_times.begin(), sorted_times.end());
69-
70-
q1 = compute_quantile(sorted_times, 0.25);
71-
q3 = compute_quantile(sorted_times, 0.75);
72-
73-
iqr = q3 - q1;
74-
75-
const double IQR_OUTLIER_FACTOR = 1.5;
76-
const double STDEV_OUTLIER_FACTOR = 3.0;
77-
78-
iqr_outlier_rounds =
79-
std::count_if(sorted_times.begin(), sorted_times.end(),
80-
[q1, q3, iqr, IQR_OUTLIER_FACTOR](double x) {
81-
return x < q1 - IQR_OUTLIER_FACTOR * iqr ||
82-
x > q3 + IQR_OUTLIER_FACTOR * iqr;
83-
});
84-
85-
stdev_outlier_rounds =
86-
std::count_if(sorted_times.begin(), sorted_times.end(),
87-
[mean, stdev, STDEV_OUTLIER_FACTOR](double x) {
88-
return x < mean - STDEV_OUTLIER_FACTOR * stdev ||
89-
x > mean + STDEV_OUTLIER_FACTOR * stdev;
90-
});
83+
return sorted_data[k];
9184
}
9285

93-
std::string escapeBackslashes(const std::string &input) {
86+
static std::string escape_backslashes(const std::string &input) {
9487
std::string output;
9588
for (char c : input) {
9689
if (c == '\\') {
@@ -102,7 +95,7 @@ std::string escapeBackslashes(const std::string &input) {
10295
return output;
10396
}
10497

105-
void write_codspeed_benchmarks_to_json(
98+
static void write_codspeed_benchmarks_to_json(
10699
const std::vector<CodspeedWalltimeBenchmark> &benchmarks) {
107100
std::ostringstream oss;
108101

@@ -132,8 +125,8 @@ void write_codspeed_benchmarks_to_json(
132125
const auto &metadata = benchmark.metadata;
133126

134127
oss << " {\n";
135-
oss << " \"name\": \"" << escapeBackslashes(metadata.name) << "\",\n";
136-
oss << " \"uri\": \"" << escapeBackslashes(metadata.uri) << "\",\n";
128+
oss << " \"name\": \"" << escape_backslashes(metadata.name) << "\",\n";
129+
oss << " \"uri\": \"" << escape_backslashes(metadata.uri) << "\",\n";
137130
// TODO: Manage config fields from actual configuration
138131
oss << " \"config\": {\n";
139132
oss << " \"warmup_time_ns\": null,\n";
@@ -202,40 +195,83 @@ void generate_codspeed_walltime_report(
202195
std::vector<CodspeedWalltimeBenchmark> codspeed_walltime_benchmarks;
203196

204197
for (const auto &raw_benchmark : raw_walltime_benchmarks) {
205-
CodspeedWalltimeBenchmark codspeed_benchmark;
206-
codspeed_benchmark.metadata = {raw_benchmark.name, raw_benchmark.uri};
198+
assert(raw_benchmark.iters_per_round.size() ==
199+
raw_benchmark.times_per_round_ns.size());
200+
201+
assert(raw_benchmark.iters_per_round.size() != 0);
202+
203+
// Convert total round times to per-iteration times
204+
std::vector<double> per_iteration_times_ns;
205+
for (size_t i = 0; i < raw_benchmark.times_per_round_ns.size(); i++) {
206+
assert(raw_benchmark.iters_per_round[i] != 0);
207+
double per_iter_time_ns = raw_benchmark.times_per_round_ns[i] /
208+
raw_benchmark.iters_per_round[i];
209+
per_iteration_times_ns.push_back(per_iter_time_ns);
210+
}
207211

212+
// Sort for quantile computation
213+
std::vector<double> sorted_per_iter_times_ns = per_iteration_times_ns;
214+
std::sort(sorted_per_iter_times_ns.begin(), sorted_per_iter_times_ns.end());
215+
216+
// Compute statistics from per-iteration times
217+
double mean_ns = std::accumulate(per_iteration_times_ns.begin(),
218+
per_iteration_times_ns.end(), 0.0) /
219+
per_iteration_times_ns.size();
220+
221+
double variance = 0.0;
222+
for (double time_ns : per_iteration_times_ns) {
223+
double diff = time_ns - mean_ns;
224+
variance += diff * diff;
225+
}
226+
double stdev_ns = std::sqrt(variance / per_iteration_times_ns.size());
227+
const double STDEV_OUTLIER_FACTOR = 3.0;
228+
size_t stdev_outlier_rounds = std::count_if(
229+
sorted_per_iter_times_ns.begin(), sorted_per_iter_times_ns.end(),
230+
[mean_ns, stdev_ns, STDEV_OUTLIER_FACTOR](double x) {
231+
return x < mean_ns - STDEV_OUTLIER_FACTOR * stdev_ns ||
232+
x > mean_ns + STDEV_OUTLIER_FACTOR * stdev_ns;
233+
});
234+
235+
double q1_ns = compute_quantile(sorted_per_iter_times_ns, 0.25);
236+
double median_ns = compute_quantile(sorted_per_iter_times_ns, 0.5);
237+
double q3_ns = compute_quantile(sorted_per_iter_times_ns, 0.75);
238+
239+
double iqr_ns = q3_ns - q1_ns;
240+
const double IQR_OUTLIER_FACTOR = 1.5;
241+
size_t iqr_outlier_rounds = std::count_if(
242+
sorted_per_iter_times_ns.begin(), sorted_per_iter_times_ns.end(),
243+
[q1_ns, q3_ns, iqr_ns, IQR_OUTLIER_FACTOR](double x) {
244+
return x < q1_ns - IQR_OUTLIER_FACTOR * iqr_ns ||
245+
x > q3_ns + IQR_OUTLIER_FACTOR * iqr_ns;
246+
});
247+
248+
// Compute total time in seconds
208249
double total_time =
209-
std::accumulate(raw_benchmark.round_times_ns.begin(),
210-
raw_benchmark.round_times_ns.end(), 0.0) /
250+
std::accumulate(raw_benchmark.times_per_round_ns.begin(),
251+
raw_benchmark.times_per_round_ns.end(), 0.0) /
211252
1e9;
212253

213-
double mean = raw_benchmark.mean_ns;
214-
double median = raw_benchmark.median_ns;
215-
double stdev = raw_benchmark.stdev_ns;
216-
double q1, q3, iqr;
217-
size_t iqr_outlier_rounds, stdev_outlier_rounds;
218-
compute_iqr_and_outliers(raw_benchmark.round_times_ns, mean, stdev, q1, q3,
219-
iqr, iqr_outlier_rounds, stdev_outlier_rounds);
254+
// TODO: CodSpeed format only supports one iter_per_round for all rounds,
255+
// for now take the average
256+
uint64_t avg_iters_per_round =
257+
std::accumulate(raw_benchmark.iters_per_round.begin(),
258+
raw_benchmark.iters_per_round.end(), 0ULL) /
259+
raw_benchmark.iters_per_round.size();
220260

221261
// Populate stats
222-
codspeed_benchmark.stats = {
223-
*std::min_element(raw_benchmark.round_times_ns.begin(),
224-
raw_benchmark.round_times_ns.end()),
225-
*std::max_element(raw_benchmark.round_times_ns.begin(),
226-
raw_benchmark.round_times_ns.end()),
227-
mean,
228-
stdev,
229-
q1,
230-
median,
231-
q3,
232-
raw_benchmark.round_times_ns.size(),
233-
total_time,
234-
iqr_outlier_rounds,
235-
stdev_outlier_rounds,
236-
raw_benchmark.iter_per_round,
237-
0 // TODO: warmup_iters
238-
};
262+
BenchmarkStats stats(*std::min_element(sorted_per_iter_times_ns.begin(),
263+
sorted_per_iter_times_ns.end()),
264+
*std::max_element(sorted_per_iter_times_ns.begin(),
265+
sorted_per_iter_times_ns.end()),
266+
mean_ns, stdev_ns, q1_ns, median_ns, q3_ns,
267+
raw_benchmark.times_per_round_ns.size(), total_time,
268+
iqr_outlier_rounds, stdev_outlier_rounds,
269+
avg_iters_per_round,
270+
0 // TODO: warmup_iters
271+
);
272+
CodspeedWalltimeBenchmark codspeed_benchmark;
273+
codspeed_benchmark.metadata = {raw_benchmark.name, raw_benchmark.uri};
274+
codspeed_benchmark.stats = stats;
239275

240276
codspeed_walltime_benchmarks.push_back(codspeed_benchmark);
241277
}

google_benchmark/src/benchmark.cc

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,8 @@ void FlushStreams(BenchmarkReporter* reporter) {
382382
}
383383

384384
#ifdef CODSPEED_WALLTIME
385+
// NOTE: What Google Benchmark calls a repetition, Codspeed calls a round.
386+
//
385387
// We use real time by default, but we could offer CPU time usage as a build
386388
// option, open an issue if you need it.
387389
codspeed::RawWalltimeBenchmark generate_raw_walltime_data(
@@ -402,27 +404,15 @@ codspeed::RawWalltimeBenchmark generate_raw_walltime_data(
402404
walltime_data.uri = "unknown_file::" + walltime_data.uri;
403405
}
404406

405-
walltime_data.iter_per_round = run.iterations;
406-
walltime_data.round_times_ns.push_back(run.GetAdjustedRealTime());
407-
}
407+
// Collect iteration count for this round/repetition
408+
walltime_data.iters_per_round.push_back(
409+
static_cast<uint64_t>(run.iterations));
408410

409-
if (run_results.aggregates_only.empty()) {
410-
// If run has no aggreagates, it means that only one round was performed.
411-
// Use this time as a mean, median, and set stdev to 0.
412-
double only_round_time_ns = walltime_data.round_times_ns[0];
413-
walltime_data.mean_ns = only_round_time_ns;
414-
walltime_data.median_ns = only_round_time_ns;
415-
walltime_data.stdev_ns = 0;
416-
} else {
417-
for (const auto& aggregate_run : run_results.aggregates_only) {
418-
if (aggregate_run.aggregate_name == "mean") {
419-
walltime_data.mean_ns = aggregate_run.GetAdjustedRealTime();
420-
} else if (aggregate_run.aggregate_name == "median") {
421-
walltime_data.median_ns = aggregate_run.GetAdjustedRealTime();
422-
} else if (aggregate_run.aggregate_name == "stddev") {
423-
walltime_data.stdev_ns = aggregate_run.GetAdjustedRealTime();
424-
}
425-
}
411+
// Collect total round time in nanoseconds
412+
// real_accumulated_time is in seconds, convert to nanoseconds
413+
double round_time_ns =
414+
run.real_accumulated_time * GetTimeUnitMultiplier(kNanosecond);
415+
walltime_data.times_per_round_ns.push_back(round_time_ns);
426416
}
427417

428418
return walltime_data;
@@ -568,12 +558,12 @@ void RunBenchmarks(const std::vector<BenchmarkInstance>& benchmarks,
568558
internal::BenchmarkRunner& runner = runners[repetition_index];
569559

570560
#ifdef CODSPEED_WALLTIME
571-
auto codspeed = codspeed::CodSpeed::getInstance();
561+
auto* codspeed = codspeed::CodSpeed::getInstance();
572562
if (codspeed != nullptr) {
573563
codspeed->start_benchmark(runner.GetBenchmarkName());
574564
}
575565
#endif
576-
566+
577567
runner.DoOneRepetition();
578568
if (runner.HasRepeatsRemaining()) {
579569
continue;

0 commit comments

Comments
 (0)