@@ -6,12 +6,15 @@ package testing
6
6
7
7
import (
8
8
"context"
9
+ "crypto/rand"
10
+ "encoding/hex"
11
+ "encoding/json"
9
12
"flag"
10
13
"fmt"
11
- "github.com/AvalancheHQ/codspeed-go/testing/internal/sysinfo"
12
14
"io"
13
15
"math"
14
16
"os"
17
+ "path/filepath"
15
18
"runtime"
16
19
"slices"
17
20
"strconv"
@@ -20,6 +23,8 @@ import (
20
23
"sync/atomic"
21
24
"time"
22
25
"unicode"
26
+
27
+ "github.com/AvalancheHQ/codspeed-go/testing/internal/sysinfo"
23
28
)
24
29
25
30
func initBenchmarkFlags () {
@@ -137,26 +142,27 @@ type B struct {
137
142
// a call to [B.StopTimer].
138
143
func (b * B ) StartTimer () {
139
144
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
143
148
b .start = highPrecisionTimeNow ()
144
149
b .timerOn = true
145
- b .loop .i &^= loopPoisonTimer
150
+ // b.loop.i &^= loopPoisonTimer
146
151
}
147
152
}
148
153
149
154
// StopTimer stops timing a test. This can be used to pause the timer
150
155
// while performing steps that you don't want to measure.
151
156
func (b * B ) StopTimer () {
152
157
if b .timerOn {
158
+ b .codspeedTimePerRoundNs = append (b .codspeedTimePerRoundNs , highPrecisionTimeSince (b .start ))
153
159
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
157
163
b .timerOn = false
158
164
// If we hit B.Loop with the timer stopped, fail.
159
- b .loop .i |= loopPoisonTimer
165
+ // b.loop.i |= loopPoisonTimer
160
166
}
161
167
}
162
168
@@ -221,6 +227,8 @@ func (b *B) runN(n int) {
221
227
b .previousN = n
222
228
b .previousDuration = b .duration
223
229
230
+ b .codspeedItersPerRound = append (b .codspeedItersPerRound , int64 (n ))
231
+
224
232
if b .loop .n > 0 && ! b .loop .done && ! b .failed {
225
233
b .Error ("benchmark function returned without B.Loop() == false (break or return in loop?)" )
226
234
}
@@ -274,6 +282,8 @@ var labelsOnce sync.Once
274
282
// subbenchmarks. b must not have subbenchmarks.
275
283
func (b * B ) run () {
276
284
labelsOnce .Do (func () {
285
+ fmt .Fprintf (b .w , "Running with CodSpeed instrumentation\n " )
286
+
277
287
fmt .Fprintf (b .w , "goos: %s\n " , runtime .GOOS )
278
288
fmt .Fprintf (b .w , "goarch: %s\n " , runtime .GOARCH )
279
289
if b .importPath != "" {
@@ -344,18 +354,34 @@ func (b *B) launch() {
344
354
b .runN (b .benchTime .n )
345
355
}
346
356
} 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 ; {
349
360
last := n
350
361
// Predict required iterations.
351
- goalns := d .Nanoseconds ()
362
+ goalns := warmupD .Nanoseconds ()
352
363
prevIters := int64 (b .N )
353
364
n = int64 (predictN (goalns , prevIters , b .duration .Nanoseconds (), last ))
354
365
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 ))
355
381
}
356
382
}
357
383
}
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 }
359
385
}
360
386
361
387
// Elapsed returns the measured elapsed time of the benchmark.
@@ -408,14 +434,16 @@ func (b *B) stopOrScaleBLoop() bool {
408
434
panic ("loop iteration target overflow" )
409
435
}
410
436
b .loop .i ++
437
+
438
+ b .StartTimer ()
411
439
return true
412
440
}
413
441
414
442
func (b * B ) loopSlowPath () bool {
415
443
// 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
+ // }
419
447
if b .loop .i & loopPoisonMask != 0 {
420
448
panic (fmt .Sprintf ("unknown loop stop condition: %#x" , b .loop .i ))
421
449
}
@@ -428,14 +456,21 @@ func (b *B) loopSlowPath() bool {
428
456
// Within a b.Loop loop, we don't use b.N (to avoid confusion).
429
457
b .N = 0
430
458
b .loop .i ++
459
+
460
+ b .codspeedItersPerRound = make ([]int64 , 0 )
461
+ b .codspeedTimePerRoundNs = make ([]time.Duration , 0 )
462
+
431
463
b .ResetTimer ()
464
+ b .StartTimer ()
432
465
return true
433
466
}
434
467
// Handles fixed iterations case
435
468
if b .benchTime .n > 0 {
436
469
if b .loop .n < uint64 (b .benchTime .n ) {
437
470
b .loop .n = uint64 (b .benchTime .n )
438
471
b .loop .i ++
472
+ b .ResetTimer ()
473
+ b .StartTimer ()
439
474
return true
440
475
}
441
476
b .StopTimer ()
@@ -482,6 +517,7 @@ func (b *B) loopSlowPath() bool {
482
517
// whereas b.N-based benchmarks must run the benchmark function (and any
483
518
// associated setup and cleanup) several times.
484
519
func (b * B ) Loop () bool {
520
+ b .StopTimer ()
485
521
// This is written such that the fast path is as fast as possible and can be
486
522
// inlined.
487
523
//
@@ -496,6 +532,7 @@ func (b *B) Loop() bool {
496
532
// path can do consistency checks and fail.
497
533
if b .loop .i < b .loop .n {
498
534
b .loop .i ++
535
+ b .StartTimer ()
499
536
return true
500
537
}
501
538
return b .loopSlowPath ()
@@ -522,6 +559,9 @@ type BenchmarkResult struct {
522
559
MemAllocs uint64 // The total number of memory allocations.
523
560
MemBytes uint64 // The total number of bytes allocated.
524
561
562
+ CodspeedTimePerRoundNs []time.Duration
563
+ CodspeedItersPerRound []int64
564
+
525
565
// Extra records additional metrics reported by ReportMetric.
526
566
Extra map [string ]float64
527
567
}
@@ -753,6 +793,79 @@ func (s *benchState) processBench(b *B) {
753
793
continue
754
794
}
755
795
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
+
756
869
if b .chatty != nil {
757
870
fmt .Fprintf (b .w , "%-*s\t " , s .maxLen , benchName )
758
871
}
0 commit comments