github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/src/testing/benchmark.go (about)

     1  // Copyright 2009 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  //
     5  // This file has been modified for use by the TinyGo compiler.
     6  
     7  package testing
     8  
     9  import (
    10  	"flag"
    11  	"fmt"
    12  	"io"
    13  	"math"
    14  	"os"
    15  	"runtime"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  )
    20  
    21  func initBenchmarkFlags() {
    22  	matchBenchmarks = flag.String("test.bench", "", "run only benchmarks matching `regexp`")
    23  	benchmarkMemory = flag.Bool("test.benchmem", false, "print memory allocations for benchmarks")
    24  	flag.Var(&benchTime, "test.benchtime", "run each benchmark for duration `d`")
    25  }
    26  
    27  var (
    28  	matchBenchmarks *string
    29  	benchmarkMemory *bool
    30  	benchTime       = benchTimeFlag{d: 1 * time.Second} // changed during test of testing package
    31  )
    32  
    33  type benchTimeFlag struct {
    34  	d time.Duration
    35  	n int
    36  }
    37  
    38  func (f *benchTimeFlag) String() string {
    39  	if f.n > 0 {
    40  		return fmt.Sprintf("%dx", f.n)
    41  	}
    42  	return time.Duration(f.d).String()
    43  }
    44  
    45  func (f *benchTimeFlag) Set(s string) error {
    46  	if strings.HasSuffix(s, "x") {
    47  		n, err := strconv.ParseInt(s[:len(s)-1], 10, 0)
    48  		if err != nil || n <= 0 {
    49  			return fmt.Errorf("invalid count")
    50  		}
    51  		*f = benchTimeFlag{n: int(n)}
    52  		return nil
    53  	}
    54  	d, err := time.ParseDuration(s)
    55  	if err != nil || d <= 0 {
    56  		return fmt.Errorf("invalid duration")
    57  	}
    58  	*f = benchTimeFlag{d: d}
    59  	return nil
    60  }
    61  
    62  // InternalBenchmark is an internal type but exported because it is cross-package;
    63  // it is part of the implementation of the "go test" command.
    64  type InternalBenchmark struct {
    65  	Name string
    66  	F    func(b *B)
    67  }
    68  
    69  // B is a type passed to Benchmark functions to manage benchmark
    70  // timing and to specify the number of iterations to run.
    71  //
    72  // A benchmark ends when its Benchmark function returns or calls any of the methods
    73  // FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods must be called
    74  // only from the goroutine running the Benchmark function.
    75  // The other reporting methods, such as the variations of Log and Error,
    76  // may be called simultaneously from multiple goroutines.
    77  //
    78  // Like in tests, benchmark logs are accumulated during execution
    79  // and dumped to standard output when done. Unlike in tests, benchmark logs
    80  // are always printed, so as not to hide output whose existence may be
    81  // affecting benchmark results.
    82  type B struct {
    83  	common
    84  	context      *benchContext
    85  	N            int
    86  	benchFunc    func(b *B)
    87  	bytes        int64
    88  	missingBytes bool // one of the subbenchmarks does not have bytes set.
    89  	benchTime    benchTimeFlag
    90  	timerOn      bool
    91  	result       BenchmarkResult
    92  
    93  	// report memory statistics
    94  	showAllocResult bool
    95  	// initial state of MemStats.Mallocs and MemStats.TotalAlloc
    96  	startAllocs uint64
    97  	startBytes  uint64
    98  	// net total after running benchmar
    99  	netAllocs uint64
   100  	netBytes  uint64
   101  }
   102  
   103  // StartTimer starts timing a test. This function is called automatically
   104  // before a benchmark starts, but it can also be used to resume timing after
   105  // a call to StopTimer.
   106  func (b *B) StartTimer() {
   107  	if !b.timerOn {
   108  		b.start = time.Now()
   109  		b.timerOn = true
   110  
   111  		var mstats runtime.MemStats
   112  		runtime.ReadMemStats(&mstats)
   113  		b.startAllocs = mstats.Mallocs
   114  		b.startBytes = mstats.TotalAlloc
   115  	}
   116  }
   117  
   118  // StopTimer stops timing a test. This can be used to pause the timer
   119  // while performing complex initialization that you don't
   120  // want to measure.
   121  func (b *B) StopTimer() {
   122  	if b.timerOn {
   123  		b.duration += time.Since(b.start)
   124  		b.timerOn = false
   125  
   126  		var mstats runtime.MemStats
   127  		runtime.ReadMemStats(&mstats)
   128  		b.netAllocs += mstats.Mallocs - b.startAllocs
   129  		b.netBytes += mstats.TotalAlloc - b.startBytes
   130  	}
   131  }
   132  
   133  // ResetTimer zeroes the elapsed benchmark time and memory allocation counters
   134  // and deletes user-reported metrics.
   135  func (b *B) ResetTimer() {
   136  	if b.timerOn {
   137  		b.start = time.Now()
   138  
   139  		var mstats runtime.MemStats
   140  		runtime.ReadMemStats(&mstats)
   141  		b.startAllocs = mstats.Mallocs
   142  		b.startBytes = mstats.TotalAlloc
   143  	}
   144  	b.duration = 0
   145  	b.netAllocs = 0
   146  	b.netBytes = 0
   147  }
   148  
   149  // SetBytes records the number of bytes processed in a single operation.
   150  // If this is called, the benchmark will report ns/op and MB/s.
   151  func (b *B) SetBytes(n int64) { b.bytes = n }
   152  
   153  // ReportAllocs enables malloc statistics for this benchmark.
   154  // It is equivalent to setting -test.benchmem, but it only affects the
   155  // benchmark function that calls ReportAllocs.
   156  func (b *B) ReportAllocs() {
   157  	b.showAllocResult = true
   158  }
   159  
   160  // runN runs a single benchmark for the specified number of iterations.
   161  func (b *B) runN(n int) {
   162  	b.N = n
   163  	runtime.GC()
   164  	b.ResetTimer()
   165  	b.StartTimer()
   166  	b.benchFunc(b)
   167  	b.StopTimer()
   168  }
   169  
   170  func min(x, y int64) int64 {
   171  	if x > y {
   172  		return y
   173  	}
   174  	return x
   175  }
   176  
   177  func max(x, y int64) int64 {
   178  	if x < y {
   179  		return y
   180  	}
   181  	return x
   182  }
   183  
   184  // run1 runs the first iteration of benchFunc. It reports whether more
   185  // iterations of this benchmarks should be run.
   186  func (b *B) run1() bool {
   187  	if ctx := b.context; ctx != nil {
   188  		// Extend maxLen, if needed.
   189  		if n := len(b.name); n > ctx.maxLen {
   190  			ctx.maxLen = n + 8 // Add additional slack to avoid too many jumps in size.
   191  		}
   192  	}
   193  	b.runN(1)
   194  	return !b.hasSub
   195  }
   196  
   197  // run executes the benchmark.
   198  func (b *B) run() {
   199  	if b.context != nil {
   200  		// Running go test --test.bench
   201  		b.processBench(b.context) // calls doBench and prints results
   202  	} else {
   203  		// Running func Benchmark.
   204  		b.doBench()
   205  	}
   206  }
   207  
   208  func (b *B) doBench() BenchmarkResult {
   209  	// in upstream, this uses a goroutine
   210  	b.launch()
   211  	return b.result
   212  }
   213  
   214  // launch launches the benchmark function. It gradually increases the number
   215  // of benchmark iterations until the benchmark runs for the requested benchtime.
   216  // run1 must have been called on b.
   217  func (b *B) launch() {
   218  	// Run the benchmark for at least the specified amount of time.
   219  	if b.benchTime.n > 0 {
   220  		b.runN(b.benchTime.n)
   221  	} else {
   222  		d := b.benchTime.d
   223  		b.failed = false
   224  		b.duration = 0
   225  		for n := int64(1); !b.failed && b.duration < d && n < 1e9; {
   226  			last := n
   227  			// Predict required iterations.
   228  			goalns := d.Nanoseconds()
   229  			prevIters := int64(b.N)
   230  			prevns := b.duration.Nanoseconds()
   231  			if prevns <= 0 {
   232  				// Round up, to avoid div by zero.
   233  				prevns = 1
   234  			}
   235  			// Order of operations matters.
   236  			// For very fast benchmarks, prevIters ~= prevns.
   237  			// If you divide first, you get 0 or 1,
   238  			// which can hide an order of magnitude in execution time.
   239  			// So multiply first, then divide.
   240  			n = goalns * prevIters / prevns
   241  			// Run more iterations than we think we'll need (1.2x).
   242  			n += n / 5
   243  			// Don't grow too fast in case we had timing errors previously.
   244  			n = min(n, 100*last)
   245  			// Be sure to run at least one more than last time.
   246  			n = max(n, last+1)
   247  			// Don't run more than 1e9 times. (This also keeps n in int range on 32 bit platforms.)
   248  			n = min(n, 1e9)
   249  			b.runN(int(n))
   250  		}
   251  	}
   252  	b.result = BenchmarkResult{b.N, b.duration, b.bytes, b.netAllocs, b.netBytes}
   253  }
   254  
   255  // BenchmarkResult contains the results of a benchmark run.
   256  type BenchmarkResult struct {
   257  	N     int           // The number of iterations.
   258  	T     time.Duration // The total time taken.
   259  	Bytes int64         // Bytes processed in one iteration.
   260  
   261  	MemAllocs uint64 // The total number of memory allocations.
   262  	MemBytes  uint64 // The total number of bytes allocated.
   263  }
   264  
   265  // NsPerOp returns the "ns/op" metric.
   266  func (r BenchmarkResult) NsPerOp() int64 {
   267  	if r.N <= 0 {
   268  		return 0
   269  	}
   270  	return r.T.Nanoseconds() / int64(r.N)
   271  }
   272  
   273  // mbPerSec returns the "MB/s" metric.
   274  func (r BenchmarkResult) mbPerSec() float64 {
   275  	if r.Bytes <= 0 || r.T <= 0 || r.N <= 0 {
   276  		return 0
   277  	}
   278  	return (float64(r.Bytes) * float64(r.N) / 1e6) / r.T.Seconds()
   279  }
   280  
   281  // AllocsPerOp returns the "allocs/op" metric,
   282  // which is calculated as r.MemAllocs / r.N.
   283  func (r BenchmarkResult) AllocsPerOp() int64 {
   284  	if r.N <= 0 {
   285  		return 0
   286  	}
   287  	return int64(r.MemAllocs) / int64(r.N)
   288  }
   289  
   290  // AllocedBytesPerOp returns the "B/op" metric,
   291  // which is calculated as r.MemBytes / r.N.
   292  func (r BenchmarkResult) AllocedBytesPerOp() int64 {
   293  	if r.N <= 0 {
   294  		return 0
   295  	}
   296  	return int64(r.MemBytes) / int64(r.N)
   297  }
   298  
   299  // String returns a summary of the benchmark results.
   300  // It follows the benchmark result line format from
   301  // https://golang.org/design/14313-benchmark-format, not including the
   302  // benchmark name.
   303  // Extra metrics override built-in metrics of the same name.
   304  // String does not include allocs/op or B/op, since those are reported
   305  // by MemString.
   306  func (r BenchmarkResult) String() string {
   307  	buf := new(strings.Builder)
   308  	fmt.Fprintf(buf, "%8d", r.N)
   309  
   310  	// Get ns/op as a float.
   311  	ns := float64(r.T.Nanoseconds()) / float64(r.N)
   312  	if ns != 0 {
   313  		buf.WriteByte('\t')
   314  		prettyPrint(buf, ns, "ns/op")
   315  	}
   316  
   317  	if mbs := r.mbPerSec(); mbs != 0 {
   318  		fmt.Fprintf(buf, "\t%7.2f MB/s", mbs)
   319  	}
   320  	return buf.String()
   321  }
   322  
   323  // MemString returns r.AllocedBytesPerOp and r.AllocsPerOp in the same format as 'go test'.
   324  func (r BenchmarkResult) MemString() string {
   325  	return fmt.Sprintf("%8d B/op\t%8d allocs/op",
   326  		r.AllocedBytesPerOp(), r.AllocsPerOp())
   327  }
   328  
   329  func prettyPrint(w io.Writer, x float64, unit string) {
   330  	// Print all numbers with 10 places before the decimal point
   331  	// and small numbers with four sig figs. Field widths are
   332  	// chosen to fit the whole part in 10 places while aligning
   333  	// the decimal point of all fractional formats.
   334  	var format string
   335  	switch y := math.Abs(x); {
   336  	case y == 0 || y >= 999.95:
   337  		format = "%10.0f %s"
   338  	case y >= 99.995:
   339  		format = "%12.1f %s"
   340  	case y >= 9.9995:
   341  		format = "%13.2f %s"
   342  	case y >= 0.99995:
   343  		format = "%14.3f %s"
   344  	case y >= 0.099995:
   345  		format = "%15.4f %s"
   346  	case y >= 0.0099995:
   347  		format = "%16.5f %s"
   348  	case y >= 0.00099995:
   349  		format = "%17.6f %s"
   350  	default:
   351  		format = "%18.7f %s"
   352  	}
   353  	fmt.Fprintf(w, format, x, unit)
   354  }
   355  
   356  type benchContext struct {
   357  	match *matcher
   358  
   359  	maxLen int // The largest recorded benchmark name.
   360  }
   361  
   362  func runBenchmarks(matchString func(pat, str string) (bool, error), benchmarks []InternalBenchmark) bool {
   363  	// If no flag was specified, don't run benchmarks.
   364  	if len(*matchBenchmarks) == 0 {
   365  		return true
   366  	}
   367  	ctx := &benchContext{
   368  		match: newMatcher(matchString, *matchBenchmarks, "-test.bench", flagSkipRegexp),
   369  	}
   370  	var bs []InternalBenchmark
   371  	for _, Benchmark := range benchmarks {
   372  		if _, matched, _ := ctx.match.fullName(nil, Benchmark.Name); matched {
   373  			bs = append(bs, Benchmark)
   374  			benchName := Benchmark.Name
   375  			if l := len(benchName); l > ctx.maxLen {
   376  				ctx.maxLen = l
   377  			}
   378  		}
   379  	}
   380  	main := &B{
   381  		common: common{
   382  			output: &logger{},
   383  			name:   "Main",
   384  		},
   385  		benchTime: benchTime,
   386  		benchFunc: func(b *B) {
   387  			for _, Benchmark := range bs {
   388  				b.Run(Benchmark.Name, Benchmark.F)
   389  			}
   390  		},
   391  		context: ctx,
   392  	}
   393  
   394  	main.runN(1)
   395  	return true
   396  }
   397  
   398  // processBench runs bench b and prints the results.
   399  func (b *B) processBench(ctx *benchContext) {
   400  	benchName := b.name
   401  
   402  	for i := 0; i < flagCount; i++ {
   403  		if ctx != nil {
   404  			fmt.Printf("%-*s\t", ctx.maxLen, benchName)
   405  		}
   406  		r := b.doBench()
   407  		if b.failed {
   408  			// The output could be very long here, but probably isn't.
   409  			// We print it all, regardless, because we don't want to trim the reason
   410  			// the benchmark failed.
   411  			fmt.Printf("--- FAIL: %s\n%s", benchName, "") // b.output)
   412  			return
   413  		}
   414  		if ctx != nil {
   415  			results := r.String()
   416  
   417  			if *benchmarkMemory || b.showAllocResult {
   418  				results += "\t" + r.MemString()
   419  			}
   420  			fmt.Println(results)
   421  
   422  			// Print any benchmark output
   423  			if b.output.Len() > 0 {
   424  				fmt.Printf("--- BENCH: %s\n", benchName)
   425  				b.output.WriteTo(os.Stdout)
   426  			}
   427  		}
   428  	}
   429  }
   430  
   431  // Run benchmarks f as a subbenchmark with the given name. It reports
   432  // true if the subbenchmark succeeded.
   433  //
   434  // A subbenchmark is like any other benchmark. A benchmark that calls Run at
   435  // least once will not be measured itself and will be called once with N=1.
   436  func (b *B) Run(name string, f func(b *B)) bool {
   437  	benchName, ok, partial := b.name, true, false
   438  	if b.context != nil {
   439  		benchName, ok, partial = b.context.match.fullName(&b.common, name)
   440  	}
   441  	if !ok {
   442  		return true
   443  	}
   444  	b.hasSub = true
   445  	sub := &B{
   446  		common: common{
   447  			output: &logger{},
   448  			name:   benchName,
   449  			level:  b.level + 1,
   450  		},
   451  		benchFunc: f,
   452  		benchTime: b.benchTime,
   453  		context:   b.context,
   454  	}
   455  	if partial {
   456  		// Partial name match, like -bench=X/Y matching BenchmarkX.
   457  		// Only process sub-benchmarks, if any.
   458  		sub.hasSub = true
   459  	}
   460  	if sub.run1() {
   461  		sub.run()
   462  	}
   463  	b.add(sub.result)
   464  	return !sub.failed
   465  }
   466  
   467  // add simulates running benchmarks in sequence in a single iteration. It is
   468  // used to give some meaningful results in case func Benchmark is used in
   469  // combination with Run.
   470  func (b *B) add(other BenchmarkResult) {
   471  	r := &b.result
   472  	// The aggregated BenchmarkResults resemble running all subbenchmarks as
   473  	// in sequence in a single benchmark.
   474  	r.N = 1
   475  	r.T += time.Duration(other.NsPerOp())
   476  	if other.Bytes == 0 {
   477  		// Summing Bytes is meaningless in aggregate if not all subbenchmarks
   478  		// set it.
   479  		b.missingBytes = true
   480  		r.Bytes = 0
   481  	}
   482  	if !b.missingBytes {
   483  		r.Bytes += other.Bytes
   484  	}
   485  }
   486  
   487  // A PB is used by RunParallel for running parallel benchmarks.
   488  type PB struct {
   489  }
   490  
   491  // Next reports whether there are more iterations to execute.
   492  func (pb *PB) Next() bool {
   493  	return false
   494  }
   495  
   496  // RunParallel runs a benchmark in parallel.
   497  //
   498  // Not implemented
   499  func (b *B) RunParallel(body func(*PB)) {
   500  	return
   501  }
   502  
   503  // Benchmark benchmarks a single function. It is useful for creating
   504  // custom benchmarks that do not use the "go test" command.
   505  //
   506  // If f calls Run, the result will be an estimate of running all its
   507  // subbenchmarks that don't call Run in sequence in a single benchmark.
   508  func Benchmark(f func(b *B)) BenchmarkResult {
   509  	b := &B{
   510  		benchFunc: f,
   511  		benchTime: benchTime,
   512  	}
   513  	if b.run1() {
   514  		b.run()
   515  	}
   516  	return b.result
   517  }