Skip to content

Commit 3aee627

Browse files
committed
feat: add instrumentation to extract round times
1 parent 13643f7 commit 3aee627

File tree

2 files changed

+111
-18
lines changed

2 files changed

+111
-18
lines changed

testing/testing/benchmark.go

Lines changed: 105 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ package testing
66

77
import (
88
"context"
9+
"crypto/rand"
10+
"encoding/hex"
11+
"encoding/json"
912
"flag"
1013
"fmt"
11-
"github.com/AvalancheHQ/codspeed-go/testing/internal/sysinfo"
1214
"io"
1315
"math"
1416
"os"
17+
"path/filepath"
1518
"runtime"
1619
"slices"
1720
"strconv"
@@ -20,6 +23,8 @@ import (
2023
"sync/atomic"
2124
"time"
2225
"unicode"
26+
27+
"github.com/AvalancheHQ/codspeed-go/testing/internal/sysinfo"
2328
)
2429

2530
func initBenchmarkFlags() {
@@ -137,26 +142,27 @@ type B struct {
137142
// a call to [B.StopTimer].
138143
func (b *B) StartTimer() {
139144
if !b.timerOn {
140-
runtime.ReadMemStats(&memStats)
141-
b.startAllocs = memStats.Mallocs
142-
b.startBytes = memStats.TotalAlloc
145+
// runtime.ReadMemStats(&memStats)
146+
// b.startAllocs = memStats.Mallocs
147+
// b.startBytes = memStats.TotalAlloc
143148
b.start = highPrecisionTimeNow()
144149
b.timerOn = true
145-
b.loop.i &^= loopPoisonTimer
150+
// b.loop.i &^= loopPoisonTimer
146151
}
147152
}
148153

149154
// StopTimer stops timing a test. This can be used to pause the timer
150155
// while performing steps that you don't want to measure.
151156
func (b *B) StopTimer() {
152157
if b.timerOn {
158+
b.codspeedTimePerRoundNs = append(b.codspeedTimePerRoundNs, highPrecisionTimeSince(b.start))
153159
b.duration += highPrecisionTimeSince(b.start)
154-
runtime.ReadMemStats(&memStats)
155-
b.netAllocs += memStats.Mallocs - b.startAllocs
156-
b.netBytes += memStats.TotalAlloc - b.startBytes
160+
// runtime.ReadMemStats(&memStats)
161+
// b.netAllocs += memStats.Mallocs - b.startAllocs
162+
// b.netBytes += memStats.TotalAlloc - b.startBytes
157163
b.timerOn = false
158164
// If we hit B.Loop with the timer stopped, fail.
159-
b.loop.i |= loopPoisonTimer
165+
// b.loop.i |= loopPoisonTimer
160166
}
161167
}
162168

@@ -344,18 +350,35 @@ func (b *B) launch() {
344350
b.runN(b.benchTime.n)
345351
}
346352
} else {
347-
d := b.benchTime.d
348-
for n := int64(1); !b.failed && b.duration < d && n < 1e9; {
353+
warmupD := time.Millisecond * 500
354+
warmupN := int64(1)
355+
for n := int64(1); !b.failed && b.duration < warmupD && n < 1e9; {
349356
last := n
350357
// Predict required iterations.
351-
goalns := d.Nanoseconds()
358+
goalns := warmupD.Nanoseconds()
352359
prevIters := int64(b.N)
353360
n = int64(predictN(goalns, prevIters, b.duration.Nanoseconds(), last))
354361
b.runN(int(n))
362+
warmupN = n
363+
}
364+
365+
// Reset the fields from the warmup run
366+
b.codspeedItersPerRound = make([]int64, 0)
367+
b.codspeedTimePerRoundNs = make([]time.Duration, 0)
368+
369+
// Final run:
370+
benchD := time.Second * b.benchTime.d
371+
benchN := predictN(benchD.Nanoseconds(), int64(b.N), b.duration.Nanoseconds(), warmupN)
372+
rounds := 100 // TODO: Compute the rounds in a better way
373+
roundN := benchN / int(rounds)
374+
375+
for range rounds {
376+
b.runN(int(roundN))
377+
b.codspeedItersPerRound = append(b.codspeedItersPerRound, int64(roundN))
355378
}
356379
}
357380
}
358-
b.result = BenchmarkResult{b.N, b.duration, b.bytes, b.netAllocs, b.netBytes, b.extra}
381+
b.result = BenchmarkResult{b.N, b.duration, b.bytes, b.netAllocs, b.netBytes, b.codspeedTimePerRoundNs, b.codspeedItersPerRound, b.extra}
359382
}
360383

361384
// Elapsed returns the measured elapsed time of the benchmark.
@@ -408,14 +431,16 @@ func (b *B) stopOrScaleBLoop() bool {
408431
panic("loop iteration target overflow")
409432
}
410433
b.loop.i++
434+
435+
b.StartTimer()
411436
return true
412437
}
413438

414439
func (b *B) loopSlowPath() bool {
415440
// Consistency checks
416-
if !b.timerOn {
417-
b.Fatal("B.Loop called with timer stopped")
418-
}
441+
// if !b.timerOn {
442+
// b.Fatal("B.Loop called with timer stopped")
443+
// }
419444
if b.loop.i&loopPoisonMask != 0 {
420445
panic(fmt.Sprintf("unknown loop stop condition: %#x", b.loop.i))
421446
}
@@ -428,14 +453,21 @@ func (b *B) loopSlowPath() bool {
428453
// Within a b.Loop loop, we don't use b.N (to avoid confusion).
429454
b.N = 0
430455
b.loop.i++
456+
457+
b.codspeedItersPerRound = make([]int64, 0)
458+
b.codspeedTimePerRoundNs = make([]time.Duration, 0)
459+
431460
b.ResetTimer()
461+
b.StartTimer()
432462
return true
433463
}
434464
// Handles fixed iterations case
435465
if b.benchTime.n > 0 {
436466
if b.loop.n < uint64(b.benchTime.n) {
437467
b.loop.n = uint64(b.benchTime.n)
438468
b.loop.i++
469+
b.ResetTimer()
470+
b.StartTimer()
439471
return true
440472
}
441473
b.StopTimer()
@@ -482,6 +514,7 @@ func (b *B) loopSlowPath() bool {
482514
// whereas b.N-based benchmarks must run the benchmark function (and any
483515
// associated setup and cleanup) several times.
484516
func (b *B) Loop() bool {
517+
b.StopTimer()
485518
// This is written such that the fast path is as fast as possible and can be
486519
// inlined.
487520
//
@@ -496,6 +529,7 @@ func (b *B) Loop() bool {
496529
// path can do consistency checks and fail.
497530
if b.loop.i < b.loop.n {
498531
b.loop.i++
532+
b.StartTimer()
499533
return true
500534
}
501535
return b.loopSlowPath()
@@ -522,6 +556,9 @@ type BenchmarkResult struct {
522556
MemAllocs uint64 // The total number of memory allocations.
523557
MemBytes uint64 // The total number of bytes allocated.
524558

559+
CodspeedTimePerRoundNs []time.Duration
560+
CodspeedItersPerRound []int64
561+
525562
// Extra records additional metrics reported by ReportMetric.
526563
Extra map[string]float64
527564
}
@@ -753,6 +790,58 @@ func (s *benchState) processBench(b *B) {
753790
continue
754791
}
755792
results := r.String()
793+
794+
// ############################################################################################
795+
// START CODSPEED
796+
type RawResults struct {
797+
BenchmarkName string `json:"benchmark_name"`
798+
Pid int `json:"pid"`
799+
CodspeedTimePerRoundNs []time.Duration `json:"codspeed_time_per_round_ns"`
800+
CodspeedItersPerRound []int64 `json:"codspeed_iters_per_round"`
801+
}
802+
rawResults := RawResults{
803+
BenchmarkName: benchName,
804+
Pid: os.Getpid(),
805+
CodspeedTimePerRoundNs: r.CodspeedTimePerRoundNs,
806+
CodspeedItersPerRound: r.CodspeedItersPerRound,
807+
}
808+
809+
codspeedProfileFolder := os.Getenv("CODSPEED_PROFILE_FOLDER")
810+
if codspeedProfileFolder == "" {
811+
panic("CODSPEED_PROFILE_FOLDER environment variable is not set")
812+
}
813+
if err := os.MkdirAll(filepath.Join(codspeedProfileFolder, "raw_results"), 0755); err != nil {
814+
fmt.Fprintf(os.Stderr, "failed to create raw results directory: %v\n", err)
815+
continue
816+
}
817+
// Generate random filename to avoid any overwrites
818+
randomBytes := make([]byte, 16)
819+
if _, err := rand.Read(randomBytes); err != nil {
820+
fmt.Fprintf(os.Stderr, "failed to generate random filename: %v\n", err)
821+
continue
822+
}
823+
rawResultsFile := filepath.Join(codspeedProfileFolder, "raw_results", fmt.Sprintf("%s.json", hex.EncodeToString(randomBytes)))
824+
file, err := os.Create(rawResultsFile)
825+
if err != nil {
826+
fmt.Fprintf(os.Stderr, "failed to create raw results file: %v\n", err)
827+
continue
828+
}
829+
output, err := json.MarshalIndent(rawResults, "", " ")
830+
if err != nil {
831+
fmt.Fprintf(os.Stderr, "failed to marshal raw results: %v\n", err)
832+
file.Close()
833+
continue
834+
}
835+
// FIXME: Don't overwrite the file if it already exists
836+
if _, err := file.Write(output); err != nil {
837+
fmt.Fprintf(os.Stderr, "failed to write raw results: %v\n", err)
838+
file.Close()
839+
continue
840+
}
841+
defer file.Close()
842+
// END CODSPEED
843+
// ############################################################################################
844+
756845
if b.chatty != nil {
757846
fmt.Fprintf(b.w, "%-*s\t", s.maxLen, benchName)
758847
}

testing/testing/testing.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,6 @@ import (
403403
"errors"
404404
"flag"
405405
"fmt"
406-
"github.com/AvalancheHQ/codspeed-go/testing/internal/goexperiment"
407-
"github.com/AvalancheHQ/codspeed-go/testing/internal/race"
408406
"io"
409407
"math/rand"
410408
"os"
@@ -421,6 +419,9 @@ import (
421419
"time"
422420
"unicode"
423421
"unicode/utf8"
422+
423+
"github.com/AvalancheHQ/codspeed-go/testing/internal/goexperiment"
424+
"github.com/AvalancheHQ/codspeed-go/testing/internal/race"
424425
)
425426

426427
var initRan bool
@@ -665,6 +666,9 @@ type common struct {
665666

666667
ctx context.Context
667668
cancelCtx context.CancelFunc
669+
670+
codspeedTimePerRoundNs []time.Duration
671+
codspeedItersPerRound []int64
668672
}
669673

670674
// Short reports whether the -test.short flag is set.

0 commit comments

Comments
 (0)