Skip to content

Commit 16f28c8

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

File tree

2 files changed

+110
-28
lines changed

2 files changed

+110
-28
lines changed

testing/testing/benchmark.go

Lines changed: 104 additions & 26 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 * 5
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,21 +453,17 @@ 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 {
436-
if b.loop.n < uint64(b.benchTime.n) {
437-
b.loop.n = uint64(b.benchTime.n)
438-
b.loop.i++
439-
return true
440-
}
441-
b.StopTimer()
442-
// Commit iteration count
443-
b.N = int(b.loop.n)
444-
b.loop.done = true
445-
return false
466+
panic("not supported with codspeed")
446467
}
447468
// Handles fixed time case
448469
return b.stopOrScaleBLoop()
@@ -482,6 +503,7 @@ func (b *B) loopSlowPath() bool {
482503
// whereas b.N-based benchmarks must run the benchmark function (and any
483504
// associated setup and cleanup) several times.
484505
func (b *B) Loop() bool {
506+
b.StopTimer()
485507
// This is written such that the fast path is as fast as possible and can be
486508
// inlined.
487509
//
@@ -496,6 +518,7 @@ func (b *B) Loop() bool {
496518
// path can do consistency checks and fail.
497519
if b.loop.i < b.loop.n {
498520
b.loop.i++
521+
b.StartTimer()
499522
return true
500523
}
501524
return b.loopSlowPath()
@@ -522,6 +545,9 @@ type BenchmarkResult struct {
522545
MemAllocs uint64 // The total number of memory allocations.
523546
MemBytes uint64 // The total number of bytes allocated.
524547

548+
CodspeedTimePerRoundNs []time.Duration
549+
CodspeedItersPerRound []int64
550+
525551
// Extra records additional metrics reported by ReportMetric.
526552
Extra map[string]float64
527553
}
@@ -753,6 +779,58 @@ func (s *benchState) processBench(b *B) {
753779
continue
754780
}
755781
results := r.String()
782+
783+
// ############################################################################################
784+
// START CODSPEED
785+
type RawResults struct {
786+
BenchmarkName string `json:"benchmark_name"`
787+
Pid int `json:"pid"`
788+
CodspeedTimePerRoundNs []time.Duration `json:"codspeed_time_per_round_ns"`
789+
CodspeedItersPerRound []int64 `json:"codspeed_iters_per_round"`
790+
}
791+
rawResults := RawResults{
792+
BenchmarkName: benchName,
793+
Pid: os.Getpid(),
794+
CodspeedTimePerRoundNs: r.CodspeedTimePerRoundNs,
795+
CodspeedItersPerRound: r.CodspeedItersPerRound,
796+
}
797+
798+
codspeedProfileFolder := os.Getenv("CODSPEED_PROFILE_FOLDER")
799+
if codspeedProfileFolder == "" {
800+
panic("CODSPEED_PROFILE_FOLDER environment variable is not set")
801+
}
802+
if err := os.MkdirAll(filepath.Join(codspeedProfileFolder, "raw_results"), 0755); err != nil {
803+
fmt.Fprintf(os.Stderr, "failed to create raw results directory: %v\n", err)
804+
continue
805+
}
806+
// Generate random filename to avoid any overwrites
807+
randomBytes := make([]byte, 16)
808+
if _, err := rand.Read(randomBytes); err != nil {
809+
fmt.Fprintf(os.Stderr, "failed to generate random filename: %v\n", err)
810+
continue
811+
}
812+
rawResultsFile := filepath.Join(codspeedProfileFolder, "raw_results", fmt.Sprintf("%s.json", hex.EncodeToString(randomBytes)))
813+
file, err := os.Create(rawResultsFile)
814+
if err != nil {
815+
fmt.Fprintf(os.Stderr, "failed to create raw results file: %v\n", err)
816+
continue
817+
}
818+
output, err := json.MarshalIndent(rawResults, "", " ")
819+
if err != nil {
820+
fmt.Fprintf(os.Stderr, "failed to marshal raw results: %v\n", err)
821+
file.Close()
822+
continue
823+
}
824+
// FIXME: Don't overwrite the file if it already exists
825+
if _, err := file.Write(output); err != nil {
826+
fmt.Fprintf(os.Stderr, "failed to write raw results: %v\n", err)
827+
file.Close()
828+
continue
829+
}
830+
defer file.Close()
831+
// END CODSPEED
832+
// ############################################################################################
833+
756834
if b.chatty != nil {
757835
fmt.Fprintf(b.w, "%-*s\t", s.maxLen, benchName)
758836
}

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)