Skip to content

Commit c438152

Browse files
committed
feat: add instrumentation to extract round times
1 parent 51b72ff commit c438152

File tree

8 files changed

+141
-18
lines changed

8 files changed

+141
-18
lines changed

go-runner/testdata/projects/caddy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit fb22a26b1a08a2fa3b2526d1852467904ee140f6

go-runner/testdata/projects/fuego

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 15bb3a708ebfa1722b19bcb020408ac1bd4b7333

go-runner/testdata/projects/fzf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 04c4269db3f8058d2afc301dfba49d497249eb5c
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 96d27bb724d05ee920b4e4cf1be9493346673258

go-runner/testdata/projects/hugo

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 3aa22b09425ab9e4e4d79c328ae16692cb3df736
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 69e81088ad40f45a0764597326722dea8f3f00a8

testing/testing/benchmark.go

Lines changed: 129 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

@@ -221,6 +227,8 @@ func (b *B) runN(n int) {
221227
b.previousN = n
222228
b.previousDuration = b.duration
223229

230+
b.codspeedItersPerRound = append(b.codspeedItersPerRound, int64(n))
231+
224232
if b.loop.n > 0 && !b.loop.done && !b.failed {
225233
b.Error("benchmark function returned without B.Loop() == false (break or return in loop?)")
226234
}
@@ -274,6 +282,8 @@ var labelsOnce sync.Once
274282
// subbenchmarks. b must not have subbenchmarks.
275283
func (b *B) run() {
276284
labelsOnce.Do(func() {
285+
fmt.Fprintf(b.w, "Running with CodSpeed instrumentation\n")
286+
277287
fmt.Fprintf(b.w, "goos: %s\n", runtime.GOOS)
278288
fmt.Fprintf(b.w, "goarch: %s\n", runtime.GOARCH)
279289
if b.importPath != "" {
@@ -344,18 +354,34 @@ func (b *B) launch() {
344354
b.runN(b.benchTime.n)
345355
}
346356
} else {
347-
d := b.benchTime.d
348-
for n := int64(1); !b.failed && b.duration < d && n < 1e9; {
357+
warmupD := time.Millisecond * 500
358+
warmupN := int64(1)
359+
for n := int64(1); !b.failed && b.duration < warmupD && n < 1e9; {
349360
last := n
350361
// Predict required iterations.
351-
goalns := d.Nanoseconds()
362+
goalns := warmupD.Nanoseconds()
352363
prevIters := int64(b.N)
353364
n = int64(predictN(goalns, prevIters, b.duration.Nanoseconds(), last))
354365
b.runN(int(n))
366+
warmupN = n
367+
}
368+
369+
// Reset the fields from the warmup run
370+
b.codspeedItersPerRound = make([]int64, 0)
371+
b.codspeedTimePerRoundNs = make([]time.Duration, 0)
372+
373+
// Final run:
374+
benchD := time.Second * b.benchTime.d
375+
benchN := predictN(benchD.Nanoseconds(), int64(b.N), b.duration.Nanoseconds(), warmupN)
376+
rounds := 100 // TODO: Compute the rounds in a better way
377+
roundN := benchN / int(rounds)
378+
379+
for range rounds {
380+
b.runN(int(roundN))
355381
}
356382
}
357383
}
358-
b.result = BenchmarkResult{b.N, b.duration, b.bytes, b.netAllocs, b.netBytes, b.extra}
384+
b.result = BenchmarkResult{b.N, b.duration, b.bytes, b.netAllocs, b.netBytes, b.codspeedTimePerRoundNs, b.codspeedItersPerRound, b.extra}
359385
}
360386

361387
// Elapsed returns the measured elapsed time of the benchmark.
@@ -408,14 +434,16 @@ func (b *B) stopOrScaleBLoop() bool {
408434
panic("loop iteration target overflow")
409435
}
410436
b.loop.i++
437+
438+
b.StartTimer()
411439
return true
412440
}
413441

414442
func (b *B) loopSlowPath() bool {
415443
// Consistency checks
416-
if !b.timerOn {
417-
b.Fatal("B.Loop called with timer stopped")
418-
}
444+
// if !b.timerOn {
445+
// b.Fatal("B.Loop called with timer stopped")
446+
// }
419447
if b.loop.i&loopPoisonMask != 0 {
420448
panic(fmt.Sprintf("unknown loop stop condition: %#x", b.loop.i))
421449
}
@@ -428,14 +456,21 @@ func (b *B) loopSlowPath() bool {
428456
// Within a b.Loop loop, we don't use b.N (to avoid confusion).
429457
b.N = 0
430458
b.loop.i++
459+
460+
b.codspeedItersPerRound = make([]int64, 0)
461+
b.codspeedTimePerRoundNs = make([]time.Duration, 0)
462+
431463
b.ResetTimer()
464+
b.StartTimer()
432465
return true
433466
}
434467
// Handles fixed iterations case
435468
if b.benchTime.n > 0 {
436469
if b.loop.n < uint64(b.benchTime.n) {
437470
b.loop.n = uint64(b.benchTime.n)
438471
b.loop.i++
472+
b.ResetTimer()
473+
b.StartTimer()
439474
return true
440475
}
441476
b.StopTimer()
@@ -482,6 +517,7 @@ func (b *B) loopSlowPath() bool {
482517
// whereas b.N-based benchmarks must run the benchmark function (and any
483518
// associated setup and cleanup) several times.
484519
func (b *B) Loop() bool {
520+
b.StopTimer()
485521
// This is written such that the fast path is as fast as possible and can be
486522
// inlined.
487523
//
@@ -496,6 +532,7 @@ func (b *B) Loop() bool {
496532
// path can do consistency checks and fail.
497533
if b.loop.i < b.loop.n {
498534
b.loop.i++
535+
b.StartTimer()
499536
return true
500537
}
501538
return b.loopSlowPath()
@@ -522,6 +559,9 @@ type BenchmarkResult struct {
522559
MemAllocs uint64 // The total number of memory allocations.
523560
MemBytes uint64 // The total number of bytes allocated.
524561

562+
CodspeedTimePerRoundNs []time.Duration
563+
CodspeedItersPerRound []int64
564+
525565
// Extra records additional metrics reported by ReportMetric.
526566
Extra map[string]float64
527567
}
@@ -753,6 +793,79 @@ func (s *benchState) processBench(b *B) {
753793
continue
754794
}
755795
results := r.String()
796+
797+
// ############################################################################################
798+
// START CODSPEED
799+
type RawResults struct {
800+
BenchmarkName string `json:"benchmark_name"`
801+
Pid int `json:"pid"`
802+
CodspeedTimePerRoundNs []time.Duration `json:"codspeed_time_per_round_ns"`
803+
CodspeedItersPerRound []int64 `json:"codspeed_iters_per_round"`
804+
}
805+
806+
// Build custom bench name with :: separator
807+
var nameParts []string
808+
current := &b.common
809+
for current.parent != nil {
810+
// Extract the sub-benchmark part by removing parent prefix
811+
parentName := current.parent.name
812+
if strings.HasPrefix(current.name, parentName+"/") {
813+
subName := strings.TrimPrefix(current.name, parentName+"/")
814+
nameParts = append([]string{subName}, nameParts...)
815+
} else {
816+
nameParts = append([]string{current.name}, nameParts...)
817+
}
818+
819+
if current.parent.name == "Main" {
820+
break
821+
}
822+
current = current.parent
823+
}
824+
customBenchName := strings.Join(nameParts, "::")
825+
826+
rawResults := RawResults{
827+
BenchmarkName: customBenchName,
828+
Pid: os.Getpid(),
829+
CodspeedTimePerRoundNs: r.CodspeedTimePerRoundNs,
830+
CodspeedItersPerRound: r.CodspeedItersPerRound,
831+
}
832+
833+
codspeedProfileFolder := os.Getenv("CODSPEED_PROFILE_FOLDER")
834+
if codspeedProfileFolder == "" {
835+
panic("CODSPEED_PROFILE_FOLDER environment variable is not set")
836+
}
837+
if err := os.MkdirAll(filepath.Join(codspeedProfileFolder, "raw_results"), 0755); err != nil {
838+
fmt.Fprintf(os.Stderr, "failed to create raw results directory: %v\n", err)
839+
continue
840+
}
841+
// Generate random filename to avoid any overwrites
842+
randomBytes := make([]byte, 16)
843+
if _, err := rand.Read(randomBytes); err != nil {
844+
fmt.Fprintf(os.Stderr, "failed to generate random filename: %v\n", err)
845+
continue
846+
}
847+
rawResultsFile := filepath.Join(codspeedProfileFolder, "raw_results", fmt.Sprintf("%s.json", hex.EncodeToString(randomBytes)))
848+
file, err := os.Create(rawResultsFile)
849+
if err != nil {
850+
fmt.Fprintf(os.Stderr, "failed to create raw results file: %v\n", err)
851+
continue
852+
}
853+
output, err := json.MarshalIndent(rawResults, "", " ")
854+
if err != nil {
855+
fmt.Fprintf(os.Stderr, "failed to marshal raw results: %v\n", err)
856+
file.Close()
857+
continue
858+
}
859+
// FIXME: Don't overwrite the file if it already exists
860+
if _, err := file.Write(output); err != nil {
861+
fmt.Fprintf(os.Stderr, "failed to write raw results: %v\n", err)
862+
file.Close()
863+
continue
864+
}
865+
defer file.Close()
866+
// END CODSPEED
867+
// ############################################################################################
868+
756869
if b.chatty != nil {
757870
fmt.Fprintf(b.w, "%-*s\t", s.maxLen, benchName)
758871
}

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)