diff --git a/tools/README.md b/tools/README.md index f826e6a..b12aa5c 100644 --- a/tools/README.md +++ b/tools/README.md @@ -49,13 +49,13 @@ Information about the machines used to record benchmark stats ### Stats -| Machine | Pixel | Benchmark | Duration | Frames | FPS Avg | FPS Min | FPS Max | FPS Stdev | -| ------------------ | ------ | ------------------- | ---------- | ------- | ------- | ------- | ------- | --------- | -| bhperry-wsl | 2.2.0 | sprite | 30.02s | 1481 | 49.34 | 44 | 51 | 1.24 | -| bhperry-wsl | 2.2.0 | sprite-batched | 30.02s | 4981 | 165.95 | 148 | 169 | 4.12 | -| bhperry-wsl | 2.2.0 | imdraw-static | 30s | 2381 | 79.36 | 73 | 81 | 1.5 | -| bhperry-wsl | 2.2.0 | imdraw-moving | 30.02s | 2210 | 79.36 | 73 | 81 | 1.5 | -| bhperry-win10 | 2.2.0 | sprite | 30.02s | 1236 | 41.17 | 19 | 43 | 4.2 | -| bhperry-win10 | 2.2.0 | sprite-batched | 30s | 42320 | 1410.67 | 1316 | 1507 | 31.38 | -| bhperry-win10 | 2.2.0 | imdraw-static | 30.02s | 1509 | 50.26 | 26 | 52 | 4.53 | -| bhperry-win10 | 2.2.0 | imdraw-moving | 30.01s | 1446 | 48.19 | 21 | 50 | 5.19 | +| Machine | Pixel | Benchmark | Duration | Frames | FPS Avg | FPS Min | FPS Max | FPS Stdev | +|--------------------|--------|------------------|----------|----------|----------|----------|----------|-----------| +| bhperry-wsl | 2.2.0 | imdraw-moving | 30s | 2148 | 71.59 | 64 | 74 | 1.77 | +| bhperry-wsl | 2.2.0 | imdraw-static | 30.02s | 2272 | 75.68 | 71 | 77 | 1.19 | +| bhperry-wsl | 2.2.0 | sprite | 30.03s | 1503 | 50.06 | 47 | 51 | 0.81 | +| bhperry-wsl | 2.2.0 | sprite-batched | 30.01s | 5038 | 167.91 | 149 | 173 | 5.58 | +| bhperry-win10 | 2.2.0 | imdraw-moving | 30.01s | 1446 | 48.19 | 21 | 50 | 5.19 | +| bhperry-win10 | 2.2.0 | imdraw-static | 30.02s | 1509 | 50.26 | 26 | 52 | 4.53 | +| bhperry-win10 | 2.2.0 | sprite | 30.02s | 1236 | 41.17 | 19 | 43 | 4.2 | +| bhperry-win10 | 2.2.0 | sprite-batched | 30s | 42320 | 1410.67 | 1316 | 1507 | 31.38 | diff --git a/tools/benchmark/benchmark.go b/tools/benchmark/benchmark.go index 44c5374..3be0cf6 100644 --- a/tools/benchmark/benchmark.go +++ b/tools/benchmark/benchmark.go @@ -25,7 +25,7 @@ type Config struct { // Run executes the benchmark and calculates statistics about its performance func (c Config) Run() (stats *Stats, err error) { - fmt.Printf("Benchmark %s\n", c.Name) + fmt.Printf("Running benchmark %s\n", c.Name) run := func() { windowConfig := c.WindowConfig @@ -38,6 +38,9 @@ func (c Config) Run() (stats *Stats, err error) { if windowConfig.Bounds.Empty() { windowConfig.Bounds = pixel.R(0, 0, 1024, 1024) } + if windowConfig.Position.Eq(pixel.ZV) { + windowConfig.Position = pixel.V(100, 100) + } duration := c.Duration if duration == 0 { @@ -79,7 +82,6 @@ func (c Config) Run() (stats *Stats, err error) { } } stats = NewStats(c.Name, time.Since(start), frame, frameSeconds) - stats.Print() if win.Closed() { err = fmt.Errorf("window closed early") diff --git a/tools/benchmark/stats.go b/tools/benchmark/stats.go index b676cd7..2f37d7f 100644 --- a/tools/benchmark/stats.go +++ b/tools/benchmark/stats.go @@ -5,11 +5,26 @@ import ( "fmt" "math" "os" + "os/user" "slices" - "text/tabwriter" "time" + + "github.com/olekukonko/tablewriter" +) + +var ( + username string + pixelVersion = "x.y.z" ) +func init() { + if u, err := user.Current(); err == nil { + username = u.Username + } else { + username = "unknown" + } +} + // NewStats calculates statistics about a benchmark run func NewStats(name string, duration time.Duration, frames int, frameSeconds []int) *Stats { stats := &Stats{ @@ -54,20 +69,52 @@ type Stats struct { // Print stats to stdout in a human-readable format func (s *Stats) Print() { - w := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0) - fmt.Fprintf(w, " Frames:\t%v\n", s.Frames) - fmt.Fprintf(w, " Duration:\t%v\n", roundDuration(s.Duration, 2)) - fmt.Fprintf(w, " FPS:\n") - fmt.Fprintf(w, " Avg:\t%v\n", roundFloat(s.AvgFPS, 2)) - fmt.Fprintf(w, " Min:\t%v\n", roundFloat(s.MinFPS, 2)) - fmt.Fprintf(w, " Max:\t%v\n", roundFloat(s.MaxFPS, 2)) - fmt.Fprintf(w, " Stdev:\t%v\n", roundFloat(s.StdevFPS, 2)) - w.Flush() + StatsCollection{s}.Print() } // StatsCollection holds stats from multiple benchmark runs type StatsCollection []*Stats +func (sc StatsCollection) Print() { + data := make([][]string, len(sc)) + for i, stats := range sc { + data[i] = []string{ + username, + pixelVersion, + stats.Name, + roundDuration(stats.Duration, 2).String(), + toString(stats.Frames), + toString(stats.AvgFPS), + toString(stats.MinFPS), + toString(stats.MaxFPS), + toString(stats.StdevFPS), + } + } + + table := tablewriter.NewWriter(os.Stdout) + headers := []string{"Machine", "Pixel", "Benchmark", "Duration", "Frames", "FPS Avg", "FPS Min", "FPS Max", "FPS Stdev"} + widths := map[string]int{ + "Machine": 18, + "Benchmark": 16, + "Pixel": 6, + } + for i, header := range headers { + minWidth := widths[header] + if minWidth == 0 { + minWidth = 8 + } + table.SetColMinWidth(i, minWidth) + } + table.SetHeader(headers) + table.SetAutoFormatHeaders(false) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + table.AppendBulk(data) + table.Render() +} + // Dump writes a JSON file of all stored statistics to the given path func (sc StatsCollection) Dump(path string) error { bytes, err := json.Marshal(sc) @@ -101,6 +148,17 @@ func roundDuration(duration time.Duration, precision uint) time.Duration { } } +func toString(val any) string { + switch v := val.(type) { + case float64: + return fmt.Sprintf("%v", roundFloat(v, 2)) + case float32: + return fmt.Sprintf("%v", roundFloat(float64(v), 2)) + default: + return fmt.Sprintf("%v", v) + } +} + // standardDeviation calulates the variation of the given values relative to the average func standardDeviation(values []float64) float64 { var sum, avg, stdev float64 diff --git a/tools/cmd/bench.go b/tools/cmd/bench.go index e95e685..68d5fbd 100644 --- a/tools/cmd/bench.go +++ b/tools/cmd/bench.go @@ -6,6 +6,7 @@ import ( "os" "runtime" "runtime/pprof" + "slices" "text/tabwriter" "time" @@ -14,6 +15,7 @@ import ( ) var ( + benchRunAll bool benchRunOutput, benchRunCpuprofile, benchRunMemprofile string @@ -36,11 +38,21 @@ func NewBenchCmd() *cobra.Command { func NewBenchRunCmd() *cobra.Command { run := &cobra.Command{ - Use: "run [name...] [opts]", - Short: "Run one or more benchmark tests", - Args: cobra.MinimumNArgs(1), - SilenceUsage: true, + Use: "run [name...] [opts]", + Short: "Run one or more benchmark tests", RunE: func(cmd *cobra.Command, args []string) error { + if benchRunAll { + benchmarks := benchmark.Benchmarks.List() + args = make([]string, len(benchmarks)) + for i, config := range benchmarks { + args[i] = config.Name + } + slices.Sort(args) + } else if len(args) == 0 { + return fmt.Errorf("requires at least one benchmark") + } + cmd.SilenceUsage = true + // Start CPU profile if benchRunCpuprofile != "" { f, err := os.Create(benchRunCpuprofile) @@ -73,6 +85,9 @@ func NewBenchRunCmd() *cobra.Command { benchStats[i] = stats } + fmt.Println() + benchStats.Print() + // Dump memory profile if benchRunMemprofile != "" { f, err := os.Create(benchRunMemprofile) @@ -97,6 +112,7 @@ func NewBenchRunCmd() *cobra.Command { }, } + run.Flags().BoolVarP(&benchRunAll, "all", "a", false, "Run all registered benchmarks") run.Flags().StringVarP(&benchRunOutput, "output", "o", "", "Output path for statistics file") run.Flags().DurationVarP(&benchRunDuration, "duration", "d", 0, "Override duration for benchmark runs") run.Flags().StringVarP(&benchRunCpuprofile, "cpuprofile", "c", "", "CPU profiling file") @@ -134,11 +150,7 @@ func NewBenchStatsCmd() *cobra.Command { if err := json.Unmarshal(bytes, &benchStats); err != nil { return err } - - for _, stats := range benchStats { - fmt.Printf("Benchmark %s\n", stats.Name) - stats.Print() - } + benchStats.Print() return nil }, diff --git a/tools/go.mod b/tools/go.mod index e146c3d..c4c82cc 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/gopxl/pixel/v2 v2.2.0-local + github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.8.1 ) @@ -16,6 +17,7 @@ require ( github.com/gopxl/glhf/v2 v2.0.0 // indirect github.com/gopxl/mainthread/v2 v2.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/image v0.18.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index ad238f5..32abffd 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -11,6 +11,10 @@ github.com/gopxl/mainthread/v2 v2.0.0 h1:jRbeWFzX6/UyhRab00xS3xIVYywBgc0DgwPgwS6 github.com/gopxl/mainthread/v2 v2.0.0/go.mod h1:/uFQhUiSP53SSU/RQ5w0FFkljRArJlaQkDPza3zE2V8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=