cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / ui / speedmeasure_test.go
speedmeasure_test.go 4.2K
1package ui23import (4 "encoding/json"5 "fmt"6 "math"7 "os"8 "runtime"9 "slices"10 "strconv"11 "testing"12 "time"13)1415// speedBaselinePath holds the speed gate's baseline, rewritten by16// make speed-update.17const speedBaselinePath = "testdata/speed.json"1819// The margins a case may exceed its baseline by before it fails: time20// relative to the calibration may double (the ratio still moves with21// the CPU, the Go version and the machine's load, and a clear22// regression is a multiple, not a percentage), and allocations, which23// are counted, not timed, may grow by a quarter plus a few.24const (25 ratioMargin = 2.026 allocsMargin = 1.2527 allocsSlack = 1628)2930// speedRounds is how many times each case is timed, beside the31// calibration, keeping the fastest of each; speedBatch is about how long32// one timing of a case runs.33const (34 speedRounds = 735 speedBatch = 2 * time.Millisecond36)3738// speedResult is one case measured: nanoseconds per operation (for39// reading), the same relative to the calibration workload, and40// allocations per operation.41type speedResult struct {42 NS float64 `json:"ns"`43 Ratio float64 `json:"ratio"`44 Allocs float64 `json:"allocs"`45}4647// speedBaseline is testdata/speed.json: the machine it was measured on48// and each case's result. Ratios are compared only on the same OS and49// architecture; allocations everywhere.50type speedBaseline struct {51 GOOS string `json:"goos"`52 GOARCH string `json:"goarch"`53 Cases map[string]speedResult `json:"cases"`54}5556func readSpeedBaseline() (speedBaseline, error) {57 var b speedBaseline58 data, err := os.ReadFile(speedBaselinePath)59 if err != nil {60 return b, fmt.Errorf("%w (make speed-update writes it)", err)61 }62 return b, json.Unmarshal(data, &b)63}6465func (b speedBaseline) write() error {66 for name, r := range b.Cases {67 r.NS = math.Round(r.NS)68 r.Ratio = math.Round(r.Ratio*1000) / 100069 r.Allocs = math.Round(r.Allocs)70 b.Cases[name] = r71 }72 data, err := json.MarshalIndent(b, "", " ")73 if err != nil {74 return err75 }76 return os.WriteFile(speedBaselinePath, append(data, '\n'), 0o644)77}7879// judge says how got is over the baseline's margins, or "" when it80// isn't.81func (b speedBaseline) judge(name string, got speedResult) string {82 want, ok := b.Cases[name]83 if !ok {84 return "not in " + speedBaselinePath + " (make speed-update)"85 }86 if limit := want.Allocs*allocsMargin + allocsSlack; got.Allocs > limit {87 return fmt.Sprintf("%.0f allocations an operation, over %.0f (baseline %.0f)", got.Allocs, limit, want.Allocs)88 }89 sameMachine := b.GOOS == runtime.GOOS && b.GOARCH == runtime.GOARCH90 if limit := want.Ratio * ratioMargin; sameMachine && got.Ratio > limit {91 return fmt.Sprintf("%.2fx the calibration, over %.2fx (baseline %.2fx, %.3f ms now)", got.Ratio, limit, want.Ratio, got.NS/1e6)92 }93 return ""94}9596// measureSpeed times op against the calibration and counts its allocations.97// Calls come in even numbers, so an operation alternating two states98// ends where it started.99func measureSpeed(op func()) speedResult {100 op()101 op()102 n := 2103 for start := time.Now(); time.Since(start) < speedBatch/2; n += 2 {104 op()105 op()106 }107 allocs := testing.AllocsPerRun(n, op)108 op() // AllocsPerRun calls it once more than n109 best, calib := time.Duration(math.MaxInt64), time.Duration(math.MaxInt64)110 for range speedRounds {111 calib = min(calib, calibrate())112 start := time.Now()113 for range n {114 op()115 }116 best = min(best, time.Since(start))117 }118 per := float64(best) / float64(n)119 return speedResult{NS: per, Ratio: per / float64(calib), Allocs: allocs}120}121122// calibrationSink keeps the calibration's result alive.123var calibrationSink float64124125// calibrate times a fixed workload of the kinds of work the cases do126// (formatting numbers, map lookups by string, sorting, allocating) and127// returns how long it took: the unit the cases' times are measured in.128func calibrate() time.Duration {129 start := time.Now()130 for range 4 {131 m := make(map[string]float64)132 xs := make([]float64, 0, 2048)133 for i := range 2048 {134 k := strconv.Itoa(i * 7919 % 4099)135 m[k] += float64(i)136 xs = append(xs, float64(i*7919%4099))137 }138 slices.Sort(xs)139 for i := range 2048 {140 calibrationSink += m[strconv.Itoa(i)] + xs[i]141 }142 }143 return time.Since(start)144}