@@ -6,11 +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
14
"io"
12
15
"math"
13
16
"os"
17
+ "path/filepath"
14
18
"runtime"
15
19
"slices"
16
20
"strconv"
@@ -138,26 +142,27 @@ type B struct {
138
142
// a call to [B.StopTimer].
139
143
func (b * B ) StartTimer () {
140
144
if ! b .timerOn {
141
- runtime .ReadMemStats (& memStats )
142
- b .startAllocs = memStats .Mallocs
143
- b .startBytes = memStats .TotalAlloc
145
+ // runtime.ReadMemStats(&memStats)
146
+ // b.startAllocs = memStats.Mallocs
147
+ // b.startBytes = memStats.TotalAlloc
144
148
b .start = highPrecisionTimeNow ()
145
149
b .timerOn = true
146
- b .loop .i &^= loopPoisonTimer
150
+ // b.loop.i &^= loopPoisonTimer
147
151
}
148
152
}
149
153
150
154
// StopTimer stops timing a test. This can be used to pause the timer
151
155
// while performing steps that you don't want to measure.
152
156
func (b * B ) StopTimer () {
153
157
if b .timerOn {
158
+ b .codspeedTimePerRoundNs = append (b .codspeedTimePerRoundNs , highPrecisionTimeSince (b .start ))
154
159
b .duration += highPrecisionTimeSince (b .start )
155
- runtime .ReadMemStats (& memStats )
156
- b .netAllocs += memStats .Mallocs - b .startAllocs
157
- b .netBytes += memStats .TotalAlloc - b .startBytes
160
+ // runtime.ReadMemStats(&memStats)
161
+ // b.netAllocs += memStats.Mallocs - b.startAllocs
162
+ // b.netBytes += memStats.TotalAlloc - b.startBytes
158
163
b .timerOn = false
159
164
// If we hit B.Loop with the timer stopped, fail.
160
- b .loop .i |= loopPoisonTimer
165
+ // b.loop.i |= loopPoisonTimer
161
166
}
162
167
}
163
168
@@ -222,6 +227,8 @@ func (b *B) runN(n int) {
222
227
b .previousN = n
223
228
b .previousDuration = b .duration
224
229
230
+ b .codspeedItersPerRound = append (b .codspeedItersPerRound , int64 (n ))
231
+
225
232
if b .loop .n > 0 && ! b .loop .done && ! b .failed {
226
233
b .Error ("benchmark function returned without B.Loop() == false (break or return in loop?)" )
227
234
}
@@ -275,6 +282,8 @@ var labelsOnce sync.Once
275
282
// subbenchmarks. b must not have subbenchmarks.
276
283
func (b * B ) run () {
277
284
labelsOnce .Do (func () {
285
+ fmt .Fprintf (b .w , "Running with CodSpeed instrumentation\n " )
286
+
278
287
fmt .Fprintf (b .w , "goos: %s\n " , runtime .GOOS )
279
288
fmt .Fprintf (b .w , "goarch: %s\n " , runtime .GOARCH )
280
289
if b .importPath != "" {
@@ -345,18 +354,46 @@ func (b *B) launch() {
345
354
b .runN (b .benchTime .n )
346
355
}
347
356
} else {
348
- d := b .benchTime .d
349
- 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 ; {
350
360
last := n
351
361
// Predict required iterations.
352
- goalns := d .Nanoseconds ()
362
+ goalns := warmupD .Nanoseconds ()
353
363
prevIters := int64 (b .N )
354
364
n = int64 (predictN (goalns , prevIters , b .duration .Nanoseconds (), last ))
355
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
+
377
+ // When we have a very slow benchmark (e.g. taking 500ms), we have to:
378
+ // 1. Reduce the number of rounds to not slow down the process (e.g. by executing a 1s bench 100 times)
379
+ // 2. Not end up with roundN of 0 when dividing benchN (which can be < 100) by rounds
380
+ const minRounds = 100
381
+ var rounds int
382
+ var roundN int
383
+ if benchN < minRounds {
384
+ rounds = benchN
385
+ roundN = 1
386
+ } else {
387
+ rounds = minRounds
388
+ roundN = benchN / int (rounds )
389
+ }
390
+
391
+ for range rounds {
392
+ b .runN (int (roundN ))
356
393
}
357
394
}
358
395
}
359
- b .result = BenchmarkResult {b .N , b .duration , b .bytes , b .netAllocs , b .netBytes , b .extra }
396
+ b .result = BenchmarkResult {b .N , b .duration , b .bytes , b .netAllocs , b .netBytes , b .codspeedTimePerRoundNs , b . codspeedItersPerRound , b . extra }
360
397
}
361
398
362
399
// Elapsed returns the measured elapsed time of the benchmark.
@@ -409,14 +446,16 @@ func (b *B) stopOrScaleBLoop() bool {
409
446
panic ("loop iteration target overflow" )
410
447
}
411
448
b .loop .i ++
449
+
450
+ b .StartTimer ()
412
451
return true
413
452
}
414
453
415
454
func (b * B ) loopSlowPath () bool {
416
455
// Consistency checks
417
- if ! b .timerOn {
418
- b .Fatal ("B.Loop called with timer stopped" )
419
- }
456
+ // if !b.timerOn {
457
+ // b.Fatal("B.Loop called with timer stopped")
458
+ // }
420
459
if b .loop .i & loopPoisonMask != 0 {
421
460
panic (fmt .Sprintf ("unknown loop stop condition: %#x" , b .loop .i ))
422
461
}
@@ -429,14 +468,21 @@ func (b *B) loopSlowPath() bool {
429
468
// Within a b.Loop loop, we don't use b.N (to avoid confusion).
430
469
b .N = 0
431
470
b .loop .i ++
471
+
472
+ b .codspeedItersPerRound = make ([]int64 , 0 )
473
+ b .codspeedTimePerRoundNs = make ([]time.Duration , 0 )
474
+
432
475
b .ResetTimer ()
476
+ b .StartTimer ()
433
477
return true
434
478
}
435
479
// Handles fixed iterations case
436
480
if b .benchTime .n > 0 {
437
481
if b .loop .n < uint64 (b .benchTime .n ) {
438
482
b .loop .n = uint64 (b .benchTime .n )
439
483
b .loop .i ++
484
+ b .ResetTimer ()
485
+ b .StartTimer ()
440
486
return true
441
487
}
442
488
b .StopTimer ()
@@ -483,6 +529,7 @@ func (b *B) loopSlowPath() bool {
483
529
// whereas b.N-based benchmarks must run the benchmark function (and any
484
530
// associated setup and cleanup) several times.
485
531
func (b * B ) Loop () bool {
532
+ b .StopTimer ()
486
533
// This is written such that the fast path is as fast as possible and can be
487
534
// inlined.
488
535
//
@@ -497,6 +544,7 @@ func (b *B) Loop() bool {
497
544
// path can do consistency checks and fail.
498
545
if b .loop .i < b .loop .n {
499
546
b .loop .i ++
547
+ b .StartTimer ()
500
548
return true
501
549
}
502
550
return b .loopSlowPath ()
@@ -523,6 +571,9 @@ type BenchmarkResult struct {
523
571
MemAllocs uint64 // The total number of memory allocations.
524
572
MemBytes uint64 // The total number of bytes allocated.
525
573
574
+ CodspeedTimePerRoundNs []time.Duration
575
+ CodspeedItersPerRound []int64
576
+
526
577
// Extra records additional metrics reported by ReportMetric.
527
578
Extra map [string ]float64
528
579
}
@@ -754,6 +805,79 @@ func (s *benchState) processBench(b *B) {
754
805
continue
755
806
}
756
807
results := r .String ()
808
+
809
+ // ############################################################################################
810
+ // START CODSPEED
811
+ type RawResults struct {
812
+ BenchmarkName string `json:"benchmark_name"`
813
+ Pid int `json:"pid"`
814
+ CodspeedTimePerRoundNs []time.Duration `json:"codspeed_time_per_round_ns"`
815
+ CodspeedItersPerRound []int64 `json:"codspeed_iters_per_round"`
816
+ }
817
+
818
+ // Build custom bench name with :: separator
819
+ var nameParts []string
820
+ current := & b .common
821
+ for current .parent != nil {
822
+ // Extract the sub-benchmark part by removing parent prefix
823
+ parentName := current .parent .name
824
+ if strings .HasPrefix (current .name , parentName + "/" ) {
825
+ subName := strings .TrimPrefix (current .name , parentName + "/" )
826
+ nameParts = append ([]string {subName }, nameParts ... )
827
+ } else {
828
+ nameParts = append ([]string {current .name }, nameParts ... )
829
+ }
830
+
831
+ if current .parent .name == "Main" {
832
+ break
833
+ }
834
+ current = current .parent
835
+ }
836
+ customBenchName := strings .Join (nameParts , "::" )
837
+
838
+ rawResults := RawResults {
839
+ BenchmarkName : customBenchName ,
840
+ Pid : os .Getpid (),
841
+ CodspeedTimePerRoundNs : r .CodspeedTimePerRoundNs ,
842
+ CodspeedItersPerRound : r .CodspeedItersPerRound ,
843
+ }
844
+
845
+ codspeedProfileFolder := os .Getenv ("CODSPEED_PROFILE_FOLDER" )
846
+ if codspeedProfileFolder == "" {
847
+ panic ("CODSPEED_PROFILE_FOLDER environment variable is not set" )
848
+ }
849
+ if err := os .MkdirAll (filepath .Join (codspeedProfileFolder , "raw_results" ), 0755 ); err != nil {
850
+ fmt .Fprintf (os .Stderr , "failed to create raw results directory: %v\n " , err )
851
+ continue
852
+ }
853
+ // Generate random filename to avoid any overwrites
854
+ randomBytes := make ([]byte , 16 )
855
+ if _ , err := rand .Read (randomBytes ); err != nil {
856
+ fmt .Fprintf (os .Stderr , "failed to generate random filename: %v\n " , err )
857
+ continue
858
+ }
859
+ rawResultsFile := filepath .Join (codspeedProfileFolder , "raw_results" , fmt .Sprintf ("%s.json" , hex .EncodeToString (randomBytes )))
860
+ file , err := os .Create (rawResultsFile )
861
+ if err != nil {
862
+ fmt .Fprintf (os .Stderr , "failed to create raw results file: %v\n " , err )
863
+ continue
864
+ }
865
+ output , err := json .MarshalIndent (rawResults , "" , " " )
866
+ if err != nil {
867
+ fmt .Fprintf (os .Stderr , "failed to marshal raw results: %v\n " , err )
868
+ file .Close ()
869
+ continue
870
+ }
871
+ // FIXME: Don't overwrite the file if it already exists
872
+ if _ , err := file .Write (output ); err != nil {
873
+ fmt .Fprintf (os .Stderr , "failed to write raw results: %v\n " , err )
874
+ file .Close ()
875
+ continue
876
+ }
877
+ defer file .Close ()
878
+ // END CODSPEED
879
+ // ############################################################################################
880
+
757
881
if b .chatty != nil {
758
882
fmt .Fprintf (b .w , "%-*s\t " , s .maxLen , benchName )
759
883
}
0 commit comments