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 }