go.starlark.net@v0.0.0-20231101134539-556fd59b42f6/starlark/bench_test.go (about)

     1  // Copyright 2018 The Bazel 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  package starlark_test
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"testing"
    14  
    15  	"go.starlark.net/lib/json"
    16  	"go.starlark.net/starlark"
    17  	"go.starlark.net/starlarktest"
    18  )
    19  
    20  func BenchmarkStarlark(b *testing.B) {
    21  	starlark.Universe["json"] = json.Module
    22  
    23  	testdata := starlarktest.DataFile("starlark", ".")
    24  	thread := new(starlark.Thread)
    25  	for _, file := range []string{
    26  		"testdata/benchmark.star",
    27  		// ...
    28  	} {
    29  
    30  		filename := filepath.Join(testdata, file)
    31  
    32  		src, err := os.ReadFile(filename)
    33  		if err != nil {
    34  			b.Error(err)
    35  			continue
    36  		}
    37  		opts := getOptions(string(src))
    38  
    39  		// Evaluate the file once.
    40  		globals, err := starlark.ExecFileOptions(opts, thread, filename, src, nil)
    41  		if err != nil {
    42  			reportEvalError(b, err)
    43  		}
    44  
    45  		// Repeatedly call each global function named bench_* as a benchmark.
    46  		for _, name := range globals.Keys() {
    47  			value := globals[name]
    48  			if fn, ok := value.(*starlark.Function); ok && strings.HasPrefix(name, "bench_") {
    49  				b.Run(name, func(b *testing.B) {
    50  					_, err := starlark.Call(thread, fn, starlark.Tuple{benchmark{b}}, nil)
    51  					if err != nil {
    52  						reportEvalError(b, err)
    53  					}
    54  				})
    55  			}
    56  		}
    57  	}
    58  }
    59  
    60  // A benchmark is passed to each bench_xyz(b) function in a bench_*.star file.
    61  // It provides b.n, the number of iterations that must be executed by the function,
    62  // which is typically of the form:
    63  //
    64  //	def bench_foo(b):
    65  //	   for _ in range(b.n):
    66  //	      ...work...
    67  //
    68  // It also provides stop, start, and restart methods to stop the clock in case
    69  // there is significant set-up work that should not count against the measured
    70  // operation.
    71  //
    72  // (This interface is inspired by Go's testing.B, and is also implemented
    73  // by the java.starlark.net implementation; see
    74  // https://github.com/bazelbuild/starlark/pull/75#pullrequestreview-275604129.)
    75  type benchmark struct {
    76  	b *testing.B
    77  }
    78  
    79  func (benchmark) Freeze()               {}
    80  func (benchmark) Truth() starlark.Bool  { return true }
    81  func (benchmark) Type() string          { return "benchmark" }
    82  func (benchmark) String() string        { return "<benchmark>" }
    83  func (benchmark) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: benchmark") }
    84  func (benchmark) AttrNames() []string   { return []string{"n", "restart", "start", "stop"} }
    85  func (b benchmark) Attr(name string) (starlark.Value, error) {
    86  	switch name {
    87  	case "n":
    88  		return starlark.MakeInt(b.b.N), nil
    89  	case "restart":
    90  		return benchmarkRestart.BindReceiver(b), nil
    91  	case "start":
    92  		return benchmarkStart.BindReceiver(b), nil
    93  	case "stop":
    94  		return benchmarkStop.BindReceiver(b), nil
    95  	}
    96  	return nil, nil
    97  }
    98  
    99  var (
   100  	benchmarkRestart = starlark.NewBuiltin("restart", benchmarkRestartImpl)
   101  	benchmarkStart   = starlark.NewBuiltin("start", benchmarkStartImpl)
   102  	benchmarkStop    = starlark.NewBuiltin("stop", benchmarkStopImpl)
   103  )
   104  
   105  func benchmarkRestartImpl(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   106  	b.Receiver().(benchmark).b.ResetTimer()
   107  	return starlark.None, nil
   108  }
   109  
   110  func benchmarkStartImpl(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   111  	b.Receiver().(benchmark).b.StartTimer()
   112  	return starlark.None, nil
   113  }
   114  
   115  func benchmarkStopImpl(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   116  	b.Receiver().(benchmark).b.StopTimer()
   117  	return starlark.None, nil
   118  }
   119  
   120  // BenchmarkProgram measures operations relevant to compiled programs.
   121  // TODO(adonovan): use a bigger testdata program.
   122  func BenchmarkProgram(b *testing.B) {
   123  	// Measure time to read a source file (approx 600us but depends on hardware and file system).
   124  	filename := starlarktest.DataFile("starlark", "testdata/paths.star")
   125  	var src []byte
   126  	b.Run("read", func(b *testing.B) {
   127  		for i := 0; i < b.N; i++ {
   128  			var err error
   129  			src, err = os.ReadFile(filename)
   130  			if err != nil {
   131  				b.Fatal(err)
   132  			}
   133  		}
   134  	})
   135  
   136  	// Measure time to turn a source filename into a compiled program (approx 450us).
   137  	var prog *starlark.Program
   138  	b.Run("compile", func(b *testing.B) {
   139  		for i := 0; i < b.N; i++ {
   140  			var err error
   141  			_, prog, err = starlark.SourceProgram(filename, src, starlark.StringDict(nil).Has)
   142  			if err != nil {
   143  				b.Fatal(err)
   144  			}
   145  		}
   146  	})
   147  
   148  	// Measure time to encode a compiled program to a memory buffer
   149  	// (approx 20us; was 75-120us with gob encoding).
   150  	var out bytes.Buffer
   151  	b.Run("encode", func(b *testing.B) {
   152  		for i := 0; i < b.N; i++ {
   153  			out.Reset()
   154  			if err := prog.Write(&out); err != nil {
   155  				b.Fatal(err)
   156  			}
   157  		}
   158  	})
   159  
   160  	// Measure time to decode a compiled program from a memory buffer
   161  	// (approx 20us; was 135-250us with gob encoding)
   162  	b.Run("decode", func(b *testing.B) {
   163  		for i := 0; i < b.N; i++ {
   164  			in := bytes.NewReader(out.Bytes())
   165  			if _, err := starlark.CompiledProgram(in); err != nil {
   166  				b.Fatal(err)
   167  			}
   168  		}
   169  	})
   170  }