Update to go1.24.2
This commit is contained in:
@@ -114,10 +114,22 @@ type B struct {
|
||||
netBytes uint64
|
||||
// Extra metrics collected by ReportMetric.
|
||||
extra map[string]float64
|
||||
// For Loop() to be executed in benchFunc.
|
||||
// Loop() has its own control logic that skips the loop scaling.
|
||||
// See issue #61515.
|
||||
loopN int
|
||||
|
||||
// loop tracks the state of B.Loop
|
||||
loop struct {
|
||||
// n is the target number of iterations. It gets bumped up as we go.
|
||||
// When the benchmark loop is done, we commit this to b.N so users can
|
||||
// do reporting based on it, but we avoid exposing it until then.
|
||||
n uint64
|
||||
// i is the current Loop iteration. It's strictly monotonically
|
||||
// increasing toward n.
|
||||
//
|
||||
// The high bit is used to poison the Loop fast path and fall back to
|
||||
// the slow path.
|
||||
i uint64
|
||||
|
||||
done bool // set when B.Loop return false
|
||||
}
|
||||
}
|
||||
|
||||
// StartTimer starts timing a test. This function is called automatically
|
||||
@@ -130,6 +142,7 @@ func (b *B) StartTimer() {
|
||||
b.startBytes = memStats.TotalAlloc
|
||||
b.start = highPrecisionTimeNow()
|
||||
b.timerOn = true
|
||||
b.loop.i &^= loopPoisonTimer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +155,8 @@ func (b *B) StopTimer() {
|
||||
b.netAllocs += memStats.Mallocs - b.startAllocs
|
||||
b.netBytes += memStats.TotalAlloc - b.startBytes
|
||||
b.timerOn = false
|
||||
// If we hit B.Loop with the timer stopped, fail.
|
||||
b.loop.i |= loopPoisonTimer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +207,9 @@ func (b *B) runN(n int) {
|
||||
runtime.GC()
|
||||
b.resetRaces()
|
||||
b.N = n
|
||||
b.loopN = 0
|
||||
b.loop.n = 0
|
||||
b.loop.i = 0
|
||||
b.loop.done = false
|
||||
b.ctx = ctx
|
||||
b.cancelCtx = cancelCtx
|
||||
|
||||
@@ -203,6 +220,10 @@ func (b *B) runN(n int) {
|
||||
b.StopTimer()
|
||||
b.previousN = n
|
||||
b.previousDuration = b.duration
|
||||
|
||||
if b.loop.n > 0 && !b.loop.done && !b.failed {
|
||||
b.Error("benchmark function returned without B.Loop() == false (break or return in loop?)")
|
||||
}
|
||||
}
|
||||
|
||||
// run1 runs the first iteration of benchFunc. It reports whether more
|
||||
@@ -312,8 +333,8 @@ func (b *B) launch() {
|
||||
}()
|
||||
|
||||
// b.Loop does its own ramp-up logic so we just need to run it once.
|
||||
// If b.loopN is non zero, it means b.Loop has already run.
|
||||
if b.loopN == 0 {
|
||||
// If b.loop.n is non zero, it means b.Loop has already run.
|
||||
if b.loop.n == 0 {
|
||||
// Run the benchmark for at least the specified amount of time.
|
||||
if b.benchTime.n > 0 {
|
||||
// We already ran a single iteration in run1.
|
||||
@@ -368,38 +389,59 @@ func (b *B) ReportMetric(n float64, unit string) {
|
||||
}
|
||||
|
||||
func (b *B) stopOrScaleBLoop() bool {
|
||||
timeElapsed := highPrecisionTimeSince(b.start)
|
||||
if timeElapsed >= b.benchTime.d {
|
||||
t := b.Elapsed()
|
||||
if t >= b.benchTime.d {
|
||||
// Stop the timer so we don't count cleanup time
|
||||
b.StopTimer()
|
||||
// Commit iteration count
|
||||
b.N = int(b.loop.n)
|
||||
b.loop.done = true
|
||||
return false
|
||||
}
|
||||
// Loop scaling
|
||||
goalns := b.benchTime.d.Nanoseconds()
|
||||
prevIters := int64(b.N)
|
||||
b.N = predictN(goalns, prevIters, timeElapsed.Nanoseconds(), prevIters)
|
||||
b.loopN++
|
||||
prevIters := int64(b.loop.n)
|
||||
b.loop.n = uint64(predictN(goalns, prevIters, t.Nanoseconds(), prevIters))
|
||||
if b.loop.n&loopPoisonMask != 0 {
|
||||
// The iteration count should never get this high, but if it did we'd be
|
||||
// in big trouble.
|
||||
panic("loop iteration target overflow")
|
||||
}
|
||||
b.loop.i++
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *B) loopSlowPath() bool {
|
||||
if b.loopN == 0 {
|
||||
// Consistency checks
|
||||
if !b.timerOn {
|
||||
b.Fatal("B.Loop called with timer stopped")
|
||||
}
|
||||
if b.loop.i&loopPoisonMask != 0 {
|
||||
panic(fmt.Sprintf("unknown loop stop condition: %#x", b.loop.i))
|
||||
}
|
||||
|
||||
if b.loop.n == 0 {
|
||||
// If it's the first call to b.Loop() in the benchmark function.
|
||||
// Allows more precise measurement of benchmark loop cost counts.
|
||||
// Also initialize b.N to 1 to kick start loop scaling.
|
||||
b.N = 1
|
||||
b.loopN = 1
|
||||
// Also initialize target to 1 to kick start loop scaling.
|
||||
b.loop.n = 1
|
||||
// Within a b.Loop loop, we don't use b.N (to avoid confusion).
|
||||
b.N = 0
|
||||
b.loop.i++
|
||||
b.ResetTimer()
|
||||
return true
|
||||
}
|
||||
// Handles fixed iterations case
|
||||
if b.benchTime.n > 0 {
|
||||
if b.N < b.benchTime.n {
|
||||
b.N = b.benchTime.n
|
||||
b.loopN++
|
||||
if b.loop.n < uint64(b.benchTime.n) {
|
||||
b.loop.n = uint64(b.benchTime.n)
|
||||
b.loop.i++
|
||||
return true
|
||||
}
|
||||
b.StopTimer()
|
||||
// Commit iteration count
|
||||
b.N = int(b.loop.n)
|
||||
b.loop.done = true
|
||||
return false
|
||||
}
|
||||
// Handles fixed time case
|
||||
@@ -440,13 +482,38 @@ func (b *B) loopSlowPath() bool {
|
||||
// whereas b.N-based benchmarks must run the benchmark function (and any
|
||||
// associated setup and cleanup) several times.
|
||||
func (b *B) Loop() bool {
|
||||
if b.loopN != 0 && b.loopN < b.N {
|
||||
b.loopN++
|
||||
// This is written such that the fast path is as fast as possible and can be
|
||||
// inlined.
|
||||
//
|
||||
// There are three cases where we'll fall out of the fast path:
|
||||
//
|
||||
// - On the first call, both i and n are 0.
|
||||
//
|
||||
// - If the loop reaches the n'th iteration, then i == n and we need
|
||||
// to figure out the new target iteration count or if we're done.
|
||||
//
|
||||
// - If the timer is stopped, it poisons the top bit of i so the slow
|
||||
// path can do consistency checks and fail.
|
||||
if b.loop.i < b.loop.n {
|
||||
b.loop.i++
|
||||
return true
|
||||
}
|
||||
return b.loopSlowPath()
|
||||
}
|
||||
|
||||
// The loopPoison constants can be OR'd into B.loop.i to cause it to fall back
|
||||
// to the slow path.
|
||||
const (
|
||||
loopPoisonTimer = uint64(1 << (63 - iota))
|
||||
// If necessary, add more poison bits here.
|
||||
|
||||
// loopPoisonMask is the set of all loop poison bits. (iota-1) is the index
|
||||
// of the bit we just set, from which we recreate that bit mask. We subtract
|
||||
// 1 to set all of the bits below that bit, then complement the result to
|
||||
// get the mask. Sorry, not sorry.
|
||||
loopPoisonMask = ^uint64((1 << (63 - (iota - 1))) - 1)
|
||||
)
|
||||
|
||||
// BenchmarkResult contains the results of a benchmark run.
|
||||
type BenchmarkResult struct {
|
||||
N int // The number of iterations.
|
||||
|
||||
@@ -4,13 +4,22 @@
|
||||
|
||||
package testing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// See also TestBenchmarkBLoop* in other files.
|
||||
|
||||
func TestBenchmarkBLoop(t *T) {
|
||||
var initialStart highPrecisionTime
|
||||
var firstStart highPrecisionTime
|
||||
var lastStart highPrecisionTime
|
||||
var scaledStart highPrecisionTime
|
||||
var runningEnd bool
|
||||
runs := 0
|
||||
iters := 0
|
||||
firstBN := 0
|
||||
restBN := 0
|
||||
finalBN := 0
|
||||
bRet := Benchmark(func(b *B) {
|
||||
initialStart = b.start
|
||||
@@ -18,8 +27,13 @@ func TestBenchmarkBLoop(t *T) {
|
||||
for b.Loop() {
|
||||
if iters == 0 {
|
||||
firstStart = b.start
|
||||
firstBN = b.N
|
||||
} else {
|
||||
restBN = max(restBN, b.N)
|
||||
}
|
||||
if iters == 1 {
|
||||
scaledStart = b.start
|
||||
}
|
||||
lastStart = b.start
|
||||
iters++
|
||||
}
|
||||
finalBN = b.N
|
||||
@@ -37,6 +51,13 @@ func TestBenchmarkBLoop(t *T) {
|
||||
if finalBN != iters || bRet.N != iters {
|
||||
t.Errorf("benchmark iterations mismatch: %d loop iterations, final b.N=%d, bRet.N=%d", iters, finalBN, bRet.N)
|
||||
}
|
||||
// Verify that b.N was 0 inside the loop
|
||||
if firstBN != 0 {
|
||||
t.Errorf("want b.N == 0 on first iteration, got %d", firstBN)
|
||||
}
|
||||
if restBN != 0 {
|
||||
t.Errorf("want b.N == 0 on subsequent iterations, got %d", restBN)
|
||||
}
|
||||
// Make sure the benchmark ran for an appropriate amount of time.
|
||||
if bRet.T < benchTime.d {
|
||||
t.Fatalf("benchmark ran for %s, want >= %s", bRet.T, benchTime.d)
|
||||
@@ -45,8 +66,8 @@ func TestBenchmarkBLoop(t *T) {
|
||||
if firstStart == initialStart {
|
||||
t.Errorf("b.Loop did not reset the timer")
|
||||
}
|
||||
if lastStart != firstStart {
|
||||
t.Errorf("timer was reset during iteration")
|
||||
if scaledStart != firstStart {
|
||||
t.Errorf("b.Loop stops and restarts the timer during iteration")
|
||||
}
|
||||
// Verify that it stopped the timer after the last loop.
|
||||
if runningEnd {
|
||||
@@ -54,4 +75,80 @@ func TestBenchmarkBLoop(t *T) {
|
||||
}
|
||||
}
|
||||
|
||||
// See also TestBenchmarkBLoop* in other files.
|
||||
func TestBenchmarkBLoopBreak(t *T) {
|
||||
var bState *B
|
||||
var bLog bytes.Buffer
|
||||
bRet := Benchmark(func(b *B) {
|
||||
// The Benchmark function provides no access to the failure state and
|
||||
// discards the log, so capture the B and save its log.
|
||||
bState = b
|
||||
b.common.w = &bLog
|
||||
|
||||
for i := 0; b.Loop(); i++ {
|
||||
if i == 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
if !bState.failed {
|
||||
t.Errorf("benchmark should have failed")
|
||||
}
|
||||
const wantLog = "benchmark function returned without B.Loop"
|
||||
if log := bLog.String(); !strings.Contains(log, wantLog) {
|
||||
t.Errorf("missing error %q in output:\n%s", wantLog, log)
|
||||
}
|
||||
// A benchmark that exits early should not report its target iteration count
|
||||
// because it's not meaningful.
|
||||
if bRet.N != 0 {
|
||||
t.Errorf("want N == 0, got %d", bRet.N)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBenchmarkBLoopError(t *T) {
|
||||
// Test that a benchmark that exits early because of an error doesn't *also*
|
||||
// complain that the benchmark exited early.
|
||||
var bState *B
|
||||
var bLog bytes.Buffer
|
||||
bRet := Benchmark(func(b *B) {
|
||||
bState = b
|
||||
b.common.w = &bLog
|
||||
|
||||
for i := 0; b.Loop(); i++ {
|
||||
b.Error("error")
|
||||
return
|
||||
}
|
||||
})
|
||||
if !bState.failed {
|
||||
t.Errorf("benchmark should have failed")
|
||||
}
|
||||
const noWantLog = "benchmark function returned without B.Loop"
|
||||
if log := bLog.String(); strings.Contains(log, noWantLog) {
|
||||
t.Errorf("unexpected error %q in output:\n%s", noWantLog, log)
|
||||
}
|
||||
if bRet.N != 0 {
|
||||
t.Errorf("want N == 0, got %d", bRet.N)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBenchmarkBLoopStop(t *T) {
|
||||
var bState *B
|
||||
var bLog bytes.Buffer
|
||||
bRet := Benchmark(func(b *B) {
|
||||
bState = b
|
||||
b.common.w = &bLog
|
||||
|
||||
for i := 0; b.Loop(); i++ {
|
||||
b.StopTimer()
|
||||
}
|
||||
})
|
||||
if !bState.failed {
|
||||
t.Errorf("benchmark should have failed")
|
||||
}
|
||||
const wantLog = "B.Loop called with timer stopped"
|
||||
if log := bLog.String(); !strings.Contains(log, wantLog) {
|
||||
t.Errorf("missing error %q in output:\n%s", wantLog, log)
|
||||
}
|
||||
if bRet.N != 0 {
|
||||
t.Errorf("want N == 0, got %d", bRet.N)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user