src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/eval/builtin_fn_time.go (about)

     1  package eval
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"math/big"
     7  	"strconv"
     8  	"time"
     9  
    10  	"src.elv.sh/pkg/eval/errs"
    11  	"src.elv.sh/pkg/eval/vals"
    12  	"src.elv.sh/pkg/parse"
    13  )
    14  
    15  func init() {
    16  	addBuiltinFns(map[string]any{
    17  		"sleep":     sleep,
    18  		"time":      timeCmd,
    19  		"benchmark": benchmark,
    20  	})
    21  }
    22  
    23  var (
    24  	// Reference to [time.After] that can be mutated for testing. Takes an
    25  	// additional Frame argument to allow inspection of the value of d in tests.
    26  	timeAfter = func(fm *Frame, d time.Duration) <-chan time.Time { return time.After(d) }
    27  	// Reference to [time.Now] that can be overridden in tests.
    28  	timeNow = time.Now
    29  )
    30  
    31  func sleep(fm *Frame, duration any) error {
    32  	var f float64
    33  	var d time.Duration
    34  
    35  	if err := vals.ScanToGo(duration, &f); err == nil {
    36  		d = time.Duration(f * float64(time.Second))
    37  	} else {
    38  		// See if it is a duration string rather than a simple number.
    39  		switch duration := duration.(type) {
    40  		case string:
    41  			d, err = time.ParseDuration(duration)
    42  			if err != nil {
    43  				return ErrInvalidSleepDuration
    44  			}
    45  		default:
    46  			return ErrInvalidSleepDuration
    47  		}
    48  	}
    49  
    50  	if d < 0 {
    51  		return ErrNegativeSleepDuration
    52  	}
    53  
    54  	select {
    55  	case <-fm.Context().Done():
    56  		return ErrInterrupted
    57  	case <-timeAfter(fm, d):
    58  		return nil
    59  	}
    60  }
    61  
    62  type timeOpt struct{ OnEnd Callable }
    63  
    64  func (o *timeOpt) SetDefaultOptions() {}
    65  
    66  func timeCmd(fm *Frame, opts timeOpt, f Callable) error {
    67  	t0 := time.Now()
    68  	err := f.Call(fm, NoArgs, NoOpts)
    69  	t1 := time.Now()
    70  
    71  	dt := t1.Sub(t0)
    72  	if opts.OnEnd != nil {
    73  		newFm := fm.Fork("on-end callback of time")
    74  		errCb := opts.OnEnd.Call(newFm, []any{dt.Seconds()}, NoOpts)
    75  		if err == nil {
    76  			err = errCb
    77  		}
    78  	} else {
    79  		_, errWrite := fmt.Fprintln(fm.ByteOutput(), dt)
    80  		if err == nil {
    81  			err = errWrite
    82  		}
    83  	}
    84  
    85  	return err
    86  }
    87  
    88  type benchmarkOpts struct {
    89  	OnEnd    Callable
    90  	OnRunEnd Callable
    91  	MinRuns  int
    92  	MinTime  string
    93  	minTime  time.Duration
    94  }
    95  
    96  func (o *benchmarkOpts) SetDefaultOptions() {
    97  	o.MinRuns = 5
    98  	o.minTime = time.Second
    99  }
   100  
   101  func (opts *benchmarkOpts) parse() error {
   102  	if opts.MinRuns < 0 {
   103  		return errs.BadValue{What: "min-runs option",
   104  			Valid: "non-negative integer", Actual: strconv.Itoa(opts.MinRuns)}
   105  	}
   106  
   107  	if opts.MinTime != "" {
   108  		d, err := time.ParseDuration(opts.MinTime)
   109  		if err != nil {
   110  			return errs.BadValue{What: "min-time option",
   111  				Valid: "duration string", Actual: parse.Quote(opts.MinTime)}
   112  		}
   113  		if d < 0 {
   114  			return errs.BadValue{What: "min-time option",
   115  				Valid: "non-negative duration", Actual: parse.Quote(opts.MinTime)}
   116  		}
   117  		opts.minTime = d
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  func benchmark(fm *Frame, opts benchmarkOpts, f Callable) error {
   124  	if err := opts.parse(); err != nil {
   125  		return err
   126  	}
   127  
   128  	// Standard deviation is calculated using https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm
   129  	var (
   130  		min   = time.Duration(math.MaxInt64)
   131  		max   = time.Duration(math.MinInt64)
   132  		runs  int64
   133  		total time.Duration
   134  		m2    float64
   135  		err   error
   136  	)
   137  	for {
   138  		t0 := timeNow()
   139  		err = f.Call(fm, NoArgs, NoOpts)
   140  		if err != nil {
   141  			break
   142  		}
   143  		dt := timeNow().Sub(t0)
   144  
   145  		if min > dt {
   146  			min = dt
   147  		}
   148  		if max < dt {
   149  			max = dt
   150  		}
   151  		var oldDelta float64
   152  		if runs > 0 {
   153  			oldDelta = float64(dt) - float64(total)/float64(runs)
   154  		}
   155  		runs++
   156  		total += dt
   157  		if runs > 0 {
   158  			newDelta := float64(dt) - float64(total)/float64(runs)
   159  			m2 += oldDelta * newDelta
   160  		}
   161  
   162  		if opts.OnRunEnd != nil {
   163  			newFm := fm.Fork("on-run-end callback of benchmark")
   164  			err = opts.OnRunEnd.Call(newFm, []any{dt.Seconds()}, NoOpts)
   165  			if err != nil {
   166  				break
   167  			}
   168  		}
   169  
   170  		if runs >= int64(opts.MinRuns) && total >= opts.minTime {
   171  			break
   172  		}
   173  	}
   174  
   175  	if runs == 0 {
   176  		return err
   177  	}
   178  
   179  	avg := total / time.Duration(runs)
   180  	stddev := time.Duration(math.Sqrt(m2 / float64(runs)))
   181  	if opts.OnEnd == nil {
   182  		_, errOut := fmt.Fprintf(fm.ByteOutput(),
   183  			"%v ± %v (min %v, max %v, %d runs)\n", avg, stddev, min, max, runs)
   184  		if err == nil {
   185  			err = errOut
   186  		}
   187  	} else {
   188  		stats := vals.MakeMap(
   189  			"avg", avg.Seconds(), "stddev", stddev.Seconds(),
   190  			"min", min.Seconds(), "max", max.Seconds(), "runs", int64ToElv(runs))
   191  		newFm := fm.Fork("on-end callback of benchmark")
   192  		errOnEnd := opts.OnEnd.Call(newFm, []any{stats}, NoOpts)
   193  		if err == nil {
   194  			err = errOnEnd
   195  		}
   196  	}
   197  	return err
   198  }
   199  
   200  func int64ToElv(i int64) any {
   201  	if i <= int64(math.MaxInt) {
   202  		return int(i)
   203  	} else {
   204  		return big.NewInt(i)
   205  	}
   206  }