Skip to content
Open
6 changes: 6 additions & 0 deletions docs/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,12 @@ BENCHMARK(BM_SetInsert_With_Timer_Control)->Ranges({{1<<10, 8<<10}, {128, 512}})
```
<!-- {% endraw %} -->

Both calls are only valid inside the benchmark loop. Calling them outside it
stops a timer that was never started, which adds an absolute clock reading to
the reported time instead of a duration. This is asserted, and the assertion is
compiled into the benchmark rather than into the library, so it fires whenever
the benchmark itself is built without `NDEBUG`.

For convenience, a `ScopedPauseTiming` class is provided to manage pausing and
resuming timers within a scope. This is less error-prone than manually calling
`PauseTiming` and `ResumeTiming`.
Expand Down
25 changes: 23 additions & 2 deletions include/benchmark/state.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,24 @@ class BENCHMARK_EXPORT BENCHMARK_INTERNAL_CACHELINE_ALIGNED State {

inline bool KeepRunningBatch(IterationCount n);

void PauseTiming();
// Only valid while the benchmark loop is running.
//
// Forced inline so the assertion is compiled with the caller's NDEBUG.
// State is exported, and MSVC otherwise calls the copy of this function
// inside benchmark.dll, which was built with the library's NDEBUG.
inline BENCHMARK_ALWAYS_INLINE void PauseTiming() {
assert(started_ && !finished_ && !skipped() &&
"PauseTiming() called outside of the benchmark loop");
PauseTimingImpl();
}

void ResumeTiming();
// Only valid while the benchmark loop is running. Forced inline for the
// same reason as PauseTiming().
inline BENCHMARK_ALWAYS_INLINE void ResumeTiming() {
assert(started_ && !finished_ && !skipped() &&
"ResumeTiming() called outside of the benchmark loop");
ResumeTimingImpl();
}

void SkipWithMessage(const std::string& msg);

Expand Down Expand Up @@ -164,6 +179,12 @@ class BENCHMARK_EXPORT BENCHMARK_INTERNAL_CACHELINE_ALIGNED State {
inline bool KeepRunningInternal(IterationCount n, bool is_batch);
void FinishKeepRunning();

// The checked entry points above forward here; keeping the bodies out of
// line keeps the assertions with the caller, whose NDEBUG decides whether
// they are compiled in.
void PauseTimingImpl();
void ResumeTimingImpl();

const std::string name_;
const int thread_index_;
const int threads_;
Expand Down
6 changes: 2 additions & 4 deletions src/benchmark.cc
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,8 @@ State::State(std::string name, IterationCount max_iters,
"line";
}

void State::PauseTiming() {
void State::PauseTimingImpl() {
// Add in time accumulated so far
BM_CHECK(started_ && !finished_ && !skipped());
timer_->StopTimer();
if (perf_counters_measurement_ != nullptr) {
std::vector<std::pair<std::string, double>> measurements;
Expand All @@ -255,8 +254,7 @@ void State::PauseTiming() {
}
}

void State::ResumeTiming() {
BM_CHECK(started_ && !finished_ && !skipped());
void State::ResumeTimingImpl() {
timer_->StartTimer();
if (perf_counters_measurement_ != nullptr) {
perf_counters_measurement_->Start();
Expand Down
2 changes: 0 additions & 2 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,6 @@ benchmark_add_test(NAME basic_benchmark COMMAND basic_test --benchmark_min_time=
compile_output_test(repetitions_test)
benchmark_add_test(NAME repetitions_benchmark COMMAND repetitions_test --benchmark_min_time=0.01s --benchmark_repetitions=3)

compile_benchmark_test(diagnostics_test)
benchmark_add_test(NAME diagnostics_test COMMAND diagnostics_test --benchmark_min_time=0.01s)

compile_benchmark_test(skip_with_error_test)
benchmark_add_test(NAME skip_with_error_test COMMAND skip_with_error_test --benchmark_min_time=0.01s)
Expand Down
88 changes: 87 additions & 1 deletion test/benchmark_gtest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#include <vector>

#include "../src/benchmark_register.h"
#include "benchmark/benchmark_api.h"
#include "benchmark/benchmark.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

Expand Down Expand Up @@ -164,6 +164,92 @@ TEST(AddCustomContext, DuplicateKey) {
global_context = nullptr;
}

// PauseTiming() and ResumeTiming() assert when called outside the benchmark
// loop (#2235).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop the PR reference. it's not useful information.

void BM_pause_before_loop(benchmark::State& state) {
state.PauseTiming();
for (auto _ : state) {
}
}
BENCHMARK(BM_pause_before_loop)->Iterations(1);

void BM_resume_before_loop(benchmark::State& state) {
state.ResumeTiming();
for (auto _ : state) {
}
}
BENCHMARK(BM_resume_before_loop)->Iterations(1);

void BM_pause_after_loop(benchmark::State& state) {
for (auto _ : state) {
}
state.PauseTiming();
}
BENCHMARK(BM_pause_after_loop)->Iterations(1);

void BM_resume_after_loop(benchmark::State& state) {
for (auto _ : state) {
}
state.ResumeTiming();
}
BENCHMARK(BM_resume_after_loop)->Iterations(1);

void BM_pause_and_resume_in_loop(benchmark::State& state) {
for (auto _ : state) {
state.PauseTiming();
state.ResumeTiming();
}
}
BENCHMARK(BM_pause_and_resume_in_loop)->Iterations(1);

class CapturingReporter : public BenchmarkReporter {
public:
bool ReportContext(const Context& /*context*/) override { return true; }
void ReportRuns(const std::vector<Run>& runs) override {
runs_.insert(runs_.end(), runs.begin(), runs.end());
}

const std::vector<Run>& runs() const { return runs_; }

private:
std::vector<Run> runs_;
};

std::vector<BenchmarkReporter::Run> RunOne(const std::string& name) {
CapturingReporter reporter;
RunSpecifiedBenchmarks(&reporter, name);
return reporter.runs();
}

TEST(TimingDeathTest, PauseBeforeLoop) {
ASSERT_DEBUG_DEATH(RunOne("BM_pause_before_loop"), "PauseTiming");
}

TEST(TimingDeathTest, PauseAfterLoop) {
ASSERT_DEBUG_DEATH(RunOne("BM_pause_after_loop"), "PauseTiming");
}

TEST(TimingDeathTest, ResumeBeforeLoop) {
ASSERT_DEBUG_DEATH(RunOne("BM_resume_before_loop"), "ResumeTiming");
}

TEST(TimingDeathTest, ResumeAfterLoop) {
ASSERT_DEBUG_DEATH(RunOne("BM_resume_after_loop"), "ResumeTiming");
}

TEST(TimingTest, PauseAndResumeInLoopReportSaneTime) {
const std::vector<BenchmarkReporter::Run> runs =
RunOne("BM_pause_and_resume_in_loop");
ASSERT_EQ(runs.size(), 1u);
EXPECT_EQ(runs[0].skipped, 0u);
// One iteration of an empty loop. A whole second would mean an absolute
// clock reading was accumulated instead of a duration.
EXPECT_GE(runs[0].real_accumulated_time, 0.0);
EXPECT_LT(runs[0].real_accumulated_time, 1.0);
EXPECT_GE(runs[0].cpu_accumulated_time, 0.0);
EXPECT_LT(runs[0].cpu_accumulated_time, 1.0);
}

} // namespace
} // namespace internal
} // namespace benchmark
107 changes: 0 additions & 107 deletions test/diagnostics_test.cc

This file was deleted.

Loading