Diagnose invalid PauseTiming()/ResumeTiming() calls (#2235) - #2274
Diagnose invalid PauseTiming()/ResumeTiming() calls (#2235)#2274devtejasx wants to merge 1 commit into
Conversation
|
what impact does this have on the generated assembly for the core timing loop? did you consider why we keep these methods light weight or did you just point some AI crap at the project and suggest changes without a deeper understanding of the library? |
8de0cc9 to
c5b4aae
Compare
|
Fair question. I've reworked the patch around it and measured. The core timing loop is not touched.
one load and a predicted-not-taken branch, in a function that already reads two clocks ( AI assistance is disclosed in the description, per AGENTS.md. On the substance: If the position is that misuse of these two functions is a debug-only concern, that is a reasonable call and I'll close this. The reason I thought it was worth raising is that the release-build symptom is plausible-looking wrong numbers rather than a crash. |
| // stopped while timing is paused. Stopping it at any other point would fold | ||
| // an absolute clock reading into the accumulated time instead of a | ||
| // duration, so report the misuse rather than produce a meaningless result. | ||
| if (BENCHMARK_BUILTIN_EXPECT(!timer_->running(), false)) { |
There was a problem hiding this comment.
why is this check !_timer_->running() rather than the original started_ && !finished_ && !skipped()?
| // Restarting a timer that is already running discards the slice it is in the | ||
| // middle of, and starting one outside of the loop leaves it running past the | ||
| // point where its value is read. | ||
| if (BENCHMARK_BUILTIN_EXPECT(timer_->running() || !started_ || finished_, |
There was a problem hiding this comment.
same here. this has changed the check
| namespace { | ||
|
|
||
| // Kept out of line so that the timer entry points below stay small: the string | ||
| // temporary that SkipWithError() takes is built here, off their fast path. |
There was a problem hiding this comment.
this comment makes no sense. no temporary is "built" as the strings are literals.
| state.ResumeTiming(); | ||
| std::abort(); | ||
| } catch (std::logic_error const&) { | ||
| // Set by each benchmark once it has observed its own misuse being diagnosed, |
There was a problem hiding this comment.
having these be global increases the risk of order dependent testing if tests accidentally share the variables. is there any reason why they're global? presumably they can be local to the benchmark and asserted on after the for loop in each case.
| } catch (std::logic_error const&) { | ||
| // Set by each benchmark once it has observed its own misuse being diagnosed, | ||
| // so that main() can tell a diagnosed misuse from a benchmark that never ran. | ||
| // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) |
There was a problem hiding this comment.
this is a big red flag right here.
LebedevRI
left a comment
There was a problem hiding this comment.
I don't like this.
Can we instead move State::PauseTiming() into a header, use normal assert(),
and then call the original State::PauseTiming() renamed as State::PauseTimingImpl() and kept in a non-header?
c5b4aae to
c6a29ef
Compare
|
Done — rebuilt the way you suggested, @LebedevRI.
@dmah42 — that also answers both of your questions on the check. The predicate is back to the original One consequence worth flagging: Release and debug suites pass. |
| TEST(Diagnostics, PauseAndResumeInsideLoop) { | ||
| EXPECT_EQ(benchmark::RunSpecifiedBenchmarks("BM_valid"), 1u); | ||
| } | ||
|
|
There was a problem hiding this comment.
what about pause of paused, resume of resumed, pause of skipped, resume of skipped, etc?
There was a problem hiding this comment.
And note that skipping can happen before/during/after the loop, and time may be either running or paused, so some more variations to test.
Oh, and we should actually test that the timings reported aren't absurd in the cases that aren't diagnosed.
56bee21 to
6fbe416
Compare
|
Covered, @LebedevRI — and working through the list changed the patch, not just the test. pause of paused / resume of resumed. pause of skipped / resume of skipped. These must not be diagnosed, and the test proves it: with an assertion there, for (auto _ : state) {
benchmark::ScopedPauseTiming pause(state);
state.SkipWithMessage("...");
}— aborts on the The variations. Timings that are not diagnosed. The last test captures the report through a reporter and asserts the real and CPU time of the undiagnosed run are non-negative and under a second; the bug this PR is about produces values in the tens of billions of nanoseconds, so the bound is loose on purpose. One limitation worth stating: the tests are compiled with |
|
Am i talking to an LLM or did human write last comment? |
|
I wrote that comment myself. I do use AI as a drafting tool (and disclosed that in the previous PR), but I review, test, and understand every change before submitting it. The previous reply was too long and probably read like an LLM-generated summary rather than a code review discussion—that's on me. I'll keep the responses focused on the specific technical questions from here. |
| assert(started_ && !finished_ && TimerIsRunning() && | ||
| "PauseTiming() called outside of the benchmark loop, or while " | ||
| "timing was already paused"); |
There was a problem hiding this comment.
Does TimerIsRunning() return true when outside of the measuring loop?
|
Thanks, I went through those cases and updated both the implementation and the tests. I found that calling PauseTiming or ResumeTiming more than once also gives incorrect timing results, so the tests now check for those cases too. For skipped benchmarks, PauseTiming and ResumeTiming should not fail. If a benchmark is skipped while timing is paused, ScopedPauseTiming will later call ResumeTiming during cleanup. Since the benchmark has already been skipped and timing has already stopped, that call is simply ignored. I also added tests for Calling PauseTiming and ResumeTiming before the loop I also added a test for the normal case to make sure valid benchmarks still report reasonable timing values. This description focuses on the expected behavior instead of the internal implementation details. |
| // timing is not already paused. Does nothing once the benchmark has been | ||
| // skipped, so that a pause left open by SkipWith*() unwinds cleanly. | ||
| void PauseTiming() { | ||
| if (skipped()) return; |
There was a problem hiding this comment.
I'm confused, why are we relaxing the check, and now always adding control flow to every callsite that wasn't there before?
|
The change is meant to keep scopedpauseTiming wroking correctly .when skipwith is called,the benchmark has already stopped and been marked as skipped.if a benchmark::scopepausetiming object is still active,its destructor will automatically call resumetiming.making pausetiming and resumetiming do nothing after a skip allow that automatic cleanup to finish safely instead of triggering an assertion .This behavior only applies after a benchmark has been skipped .neomal benchmark behaviour and checks for incorrect API usage stay the same |
|
But you see the problem, right? The PR is intended to simply move the existing assertion so that it actually triggers not just when the whole library is built with assertions. |
|
Yes, I see the concern. The original goal was only to make the existing misuse checks apply based on the benchmark user's build configuration instead of the library build configuration. |
…e#2235) Calling PauseTiming() outside the benchmark loop stops a timer that was never started, so StopTimer() adds `ChronoClockNow() - 0` to the run's real time -- an absolute clock reading, not a duration. The same happens to the CPU time. That is the ~86 s offset in google#2235, and why every later benchmark in the process reports a bigger number. The precondition is already checked. The problem is where: BM_CHECK is compiled into the library, so it only fires if the library was built with assertions. Distributions ship a release build, and then the check is gone no matter how the benchmark itself was compiled. Move it. PauseTiming() and ResumeTiming() become inline wrappers in state.h that assert and call PauseTimingImpl()/ResumeTimingImpl(), which hold the existing bodies. The condition is the same and the runtime behaviour is the same; the assertion now follows the NDEBUG of whoever writes the benchmark, and disappears once they define it. diagnostics_test caught what BM_CHECK threw through the library's abort handler. A plain assert() does not use that handler, so the test becomes diagnostics_gtest with ASSERT_DEATH_IF_SUPPORTED, like min_time_parse_gtest and profiler_manager_gtest. It covers pause and resume before and after the loop, and checks that a run which is not diagnosed still reports a real and CPU time below a second.
6fbe416 to
b3feaeb
Compare
|
Reverted. The patch is back to only moving the assertion. void PauseTiming() {
assert(started_ && !finished_ && !skipped() &&
"PauseTiming() called outside of the benchmark loop");
PauseTimingImpl();
}Same condition as the BM_CHECK it replaces. No To answer the two questions in case they matter later:
The Both are behaviour changes, so they are out. I can file the double-pause case as its own issue if it is worth fixing. Tests are back to the four out-of-loop cases, plus one check that a run which is not diagnosed reports a real and CPU time below a second. Release and debug both pass. |
PauseTiming() and ResumeTiming() guarded their preconditions with BM_CHECK, which compiles away under NDEBUG. In a release build, calling PauseTiming() outside the benchmark loop -- e.g. before it, to try to exclude setup work -- therefore reached ThreadTimer::StopTimer() with a timer that was never started, and accumulated
ChronoClockNow() - 0, an absolute clock reading, into the run's real time. The same happens to the CPU time. Every benchmark in the process then reports a huge constant offset plus whatever the clocks had advanced by, which is why the results in #2235 grow monotonically with registration order and are unrelated to the work being measured.Turn the preconditions into a reported benchmark error: an invalid call now leaves the timer untouched and skips the run with a message naming the misuse, in every build configuration. The user sees what went wrong instead of plausible-looking numbers, and a run that would have been garbage is no longer reported as a result.
Rewrite diagnostics_test to cover the new contract -- pause/resume before the loop, pause after it, and pausing or resuming twice -- in both debug and release builds; previously it could only run in debug, since the behaviour it checked existed only there.
Per AGENTS.md: AI-assisted — the patch was drafted with AI assistance
(Claude) and then reviewed, tested, and understood by me. I take full
responsibility for it.