Skip to content

testing: Fix various panics on B methods - #5831

Open
msujew wants to merge 3 commits into
tinygo-org:devfrom
msujew:testing-benchmark-api
Open

msujew wants to merge 3 commits into
tinygo-org:devfrom
msujew:testing-benchmark-api

Conversation

@msujew

@msujew msujew commented Oct 5, 2026

Copy link
Copy Markdown

Fixes #5830

  1. For fixing ReportMetric: Uses the same code that upstream Go uses to allocate the map.
  2. For Loop: An implementation that scales N (like upstream Go) would require more involved changes. This is a minimal implementation that roughly does what it's supposed to do.
  3. common.ctx and common.cancelCtx were never set, leading to nil return values on B.Context() (eventually resulting in panics).

@dgryski dgryski left a comment

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.

Missing handling an error condition to match upstream.

Comment thread src/testing/benchmark.go
// iteration count is not scaled inside Loop. runN already ramps b.N, so Loop
// simply runs b.N iterations per call of the benchmark function.
func (b *B) Loop() bool {
panic("unimplemented: testing.B.Loop")

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.

B.Loop should panic if called with the timer stopped, for example if you stop the timer during your iteration and forget to start it again.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Right, didn't look too closely at the upstream impl. Will now panic if !b.timerOn 👍

@msujew
msujew force-pushed the testing-benchmark-api branch from c885179 to 3a2e448 Compare October 6, 2026 16:04

@dgryski dgryski left a comment

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.

LGTM

@deadprogram deadprogram left a comment

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.

Thanks for the fixes @msujew. I ran the tests on my local machine and took a good look. The following is edited from an automated review:

  1. Upstream B.Loop also promises that the compiler will not optimize away the loop body. That does not hold in TinyGo. When the body's input does not change between iterations, LLVM hoists or removes it, even when the result is stored to a global. A body like for b.Loop() { sink = f(size) } measures about 1.2 ns/op in TinyGo and about 900 ns/op in Go. The old for i := 0; i < b.N; i++ style has the same problem, so this PR does not make it worse. Bodies that depend on the previous iteration measure correctly in both styles.

    Fixing it properly would need compiler support, so it is out of scope here. I am mentioning it in this PR just in case someone reads it in the future.

@dgryski

dgryski commented Oct 16, 2026

Copy link
Copy Markdown
Member

@deadprogram Good point re: lack of optimization protection. I wonder if we should add that as a comment to Loop and/or the benchmark stuff in general. (And also file a bug so we don't forget about it..)

@deadprogram

Copy link
Copy Markdown
Member

@deadprogram Good point re: lack of optimization protection. I wonder if we should add that as a comment to Loop and/or the benchmark stuff in general. (And also file a bug so we don't forget about it..)

@dgryski good point. @msujew can you please do this before we merge?

@msujew

msujew commented Oct 18, 2026 •

Copy link
Copy Markdown
Author

Sure, I've left #5917 and added a comment to B that hints towards the problematic benchmarking/compiler behavior. Thanks for the reviews!

@dgryski dgryski left a comment

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.

LGTM

@deadprogram

Copy link
Copy Markdown
Member

@msujew can you please rebase against the latest dev so we can see if the CI passes? Thanks.

Benchmarks written against the Go 1.24 API call b.Loop() and currently
die with:

    panic: unimplemented: testing.B.Loop

Loop runs b.N iterations per invocation of the benchmark function.
runN already ramps b.N, so no iteration scaling is done inside Loop.
Like upstream, the timer is reset on the first call and stopped when
Loop returns false, so setup and cleanup are not measured.
b.extra was never allocated, so any call to ReportMetric failed with:

    panic: assignment to entry in nil map

Allocate the map in ResetTimer like upstream Go does. runN calls
ResetTimer before every invocation of the benchmark function, so each
run starts with an empty map and the metrics end up in b.result.
B.Context() returned a nil interface, so benchmarks that pass it into
code calling ctx.Err() or ctx.Done() crashed with:

    panic: runtime error: nil pointer dereference

Create the context in runN like upstream Go does, so every B built by
runBenchmarks, B.Run or Benchmark gets one. runCleanup already cancels
it.
@msujew
msujew force-pushed the testing-benchmark-api branch from 2348231 to 3233238 Compare October 20, 2026 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

testing: B produces panics on Loop and ReportMetric

3 participants