Files
aego/tools/bench/main.go
T

260 lines
6.6 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"math"
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"sort"
"aego/core/log"
"aego/demos"
_ "aego/demos/stress"
_ "aego/demos/stress-scene"
"aego/engine"
renderhost "aego/render/host"
"aego/tools/bench/stat"
)
type renderDemo interface {
Module() *renderhost.Module
}
type record struct {
Demo string `json:"demo"`
Renderer string `json:"renderer"`
Frames int `json:"frames"`
AllocsPerFrame int64 `json:"allocsPerFrame"`
DrawCalls float64 `json:"drawCalls"`
Sprites float64 `json:"sprites"`
Metrics map[string][]float64 `json:"metrics"`
}
func (r record) key() string {
return r.Demo + "|" + r.Renderer
}
func main() {
demoName := flag.String("demo", "stress", "demo to benchmark")
frames := flag.Int("frames", 120, "frames per sample")
count := flag.Int("count", 10, "samples")
warmup := flag.Int("warmup", 120, "frames excluded from measurement")
baseline := flag.String("baseline", ".cache/bench/baseline.json", "baseline file")
write := flag.Bool("update", false, "write the current run into the baseline")
alpha := flag.Float64("alpha", 0.05, "significance level")
tolerance := flag.Float64("tolerance", 10, "allowed regression of the median in percent")
memprofile := flag.String("memprofile", "", "write an allocation profile to this file")
timing := flag.Bool("time", true, "compare timings; false checks allocations only")
flag.Parse()
if *memprofile != "" {
runtime.MemProfileRate = 1
}
ctor, ok := demos.Registry[*demoName]
if !ok {
fail(fmt.Errorf("demo %q is not registered", *demoName))
}
demo := ctor()
rd, ok := demo.(renderDemo)
if !ok {
fail(fmt.Errorf("demo %q does not expose a render module", *demoName))
}
m := rd.Module()
e, err := engine.NewBuilder(engine.Config{
Title: "aego bench",
Width: 1280,
Height: 720,
Hidden: true,
ResRoot: "demos/res",
AppID: "aego-bench",
MaxFixedSteps: 1,
}).WithLogger(log.Nop()).Use(m).Build()
if err != nil {
fail(err)
}
reporter, _ := demo.(demos.MetricsReporter)
var buf []demos.Metric
for i := 0; i < *warmup; i++ {
if !e.Step(demo) {
fail(fmt.Errorf("engine stopped during warmup"))
}
if i == 0 {
if o, ok := demo.(interface{ SetOverlay(bool) }); ok {
o.SetOverlay(false)
}
}
}
rec := record{Demo: *demoName, Renderer: e.Caps().Renderer, Metrics: map[string][]float64{}}
var before, after runtime.MemStats
runtime.ReadMemStats(&before)
total := 0
var draws, sprites float64
for c := 0; c < *count; c++ {
var cpu, gpu float64
gpuN, n := 0, 0
extra := map[string]float64{}
for i := 0; i < *frames; i++ {
if !e.Step(demo) {
break
}
fs := e.Stats()
rs := m.Stats()
cpu += fs.RenderTime * 1000
if rs.GPUValid {
gpu += float64(rs.GPUNS) / 1e6
gpuN++
}
draws += float64(rs.DrawCalls)
sprites += float64(rs.Visible)
if reporter != nil {
buf = reporter.ReportMetrics(buf[:0])
for _, mt := range buf {
extra[mt.Name] += mt.Value
}
}
n++
}
if n == 0 {
fail(fmt.Errorf("no frames measured"))
}
rec.Metrics["cpuRenderMs"] = append(rec.Metrics["cpuRenderMs"], cpu/float64(n))
if gpuN > 0 {
rec.Metrics["gpuMs"] = append(rec.Metrics["gpuMs"], gpu/float64(gpuN))
}
for name, v := range extra {
rec.Metrics[name] = append(rec.Metrics[name], v/float64(n))
}
total += n
}
runtime.ReadMemStats(&after)
if *memprofile != "" {
writeProfile(*memprofile)
}
e.Close()
rec.Frames = total
rec.AllocsPerFrame = int64(math.Round(float64(after.Mallocs-before.Mallocs) / float64(total)))
rec.DrawCalls = draws / float64(total)
rec.Sprites = sprites / float64(total)
blob, _ := json.MarshalIndent(rec, "", " ")
fmt.Println(string(blob))
if *write {
update(*baseline, rec)
return
}
check(*baseline, rec, *alpha, *tolerance, *timing)
}
func writeProfile(path string) {
f, err := os.Create(path)
if err != nil {
fail(err)
}
runtime.GC()
if err := pprof.WriteHeapProfile(f); err != nil {
f.Close()
fail(err)
}
f.Close()
}
func load(path string) map[string]record {
out := map[string]record{}
data, err := os.ReadFile(path)
if err != nil {
return out
}
if json.Unmarshal(data, &out) != nil {
return map[string]record{}
}
return out
}
func update(path string, rec record) {
all := load(path)
all[rec.key()] = rec
blob, err := json.MarshalIndent(all, "", " ")
if err != nil {
fail(err)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
fail(err)
}
if err := os.WriteFile(path, append(blob, '\n'), 0o644); err != nil {
fail(err)
}
fmt.Println("baseline updated for", rec.key())
}
// baselineFor finds rec's baseline; timings need the same renderer, allocations any of the demo.
func baselineFor(all map[string]record, rec record, timing bool) (record, string, bool) {
if b, ok := all[rec.key()]; ok {
return b, rec.key(), true
}
if timing {
return record{}, "", false
}
keys := make([]string, 0, len(all))
for k, b := range all {
if b.Demo == rec.Demo {
keys = append(keys, k)
}
}
if len(keys) == 0 {
return record{}, "", false
}
sort.Strings(keys)
return all[keys[0]], keys[0], true
}
func check(path string, rec record, alpha, tolerance float64, timing bool) {
base, key, ok := baselineFor(load(path), rec, timing)
if !ok {
fmt.Fprintf(os.Stderr, "no baseline for %q, run make bench-baseline\n", rec.key())
return
}
if key != rec.key() {
fmt.Printf("allocations compared with the baseline of %q\n", key)
}
failed := false
if rec.AllocsPerFrame != base.AllocsPerFrame {
fmt.Fprintf(os.Stderr, "FAIL allocations per frame: %d, baseline %d\n", rec.AllocsPerFrame, base.AllocsPerFrame)
failed = true
}
fmt.Printf("draw calls %.2f (baseline %.2f), sprites %.0f (baseline %.0f)\n", rec.DrawCalls, base.DrawCalls, rec.Sprites, base.Sprites)
if timing {
names := make([]string, 0, len(rec.Metrics))
for name := range rec.Metrics {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
b := base.Metrics[name]
if len(b) == 0 {
fmt.Printf("%-24s %10.3f (no baseline)\n", name, stat.Median(rec.Metrics[name]))
continue
}
v := stat.Compare(b, rec.Metrics[name], alpha, tolerance)
mark := "ok"
if v.Regressed {
mark = "FAIL"
failed = true
}
fmt.Printf("%-24s %10.3f -> %10.3f %+6.1f%% p=%.3f %s\n", name, v.Base, v.Head, v.Delta, v.P, mark)
}
}
if failed {
os.Exit(1)
}
fmt.Println("bench: ok")
}
func fail(err error) {
fmt.Fprintln(os.Stderr, "bench:", err)
os.Exit(2)
}