Files

167 lines
3.9 KiB
Go

package runtime
import (
scriptstdlib "aego/modules/scripting/stdlib"
"strconv"
"testing"
"aego/engine"
inputmod "aego/modules/input"
"aego/platform/input"
"aego/scene"
"aego/scene/scriptcomp"
"aego/schema"
"aego/script/ast"
"aego/script/bind"
"aego/script/bytecode"
"aego/script/checker"
"aego/script/compiler"
"aego/script/diag"
"aego/script/parser"
"aego/script/source"
"aego/script/stdlib"
)
const benchProgram = `package game
import "aego/input"
script Walker {
@export var Speed float = 3
func Update(dt float) {
p := node.Position()
p.X += input.Axis("horizontal")*Speed*dt + 0.01
node.SetPosition(p)
}
}
script Idle {
@export var Note int = 1
}
`
type bench struct {
rt *Runtime
ctx engine.Context
s *scene.Scene
}
func prepare(tb testing.TB, walkers, idle int) *bench {
tb.Helper()
types := scene.NewTypes(schema.NewRegistry())
if err := scriptcomp.Register(types); err != nil {
tb.Fatal(err)
}
if err := types.Schema().Freeze(); err != nil {
tb.Fatal(err)
}
reg := bind.New()
scriptstdlib.Install(reg, stdlib.Options{Seed: 1})
inputmod.InstallBindings(reg)
BindComponents(reg, types.Schema())
if err := reg.Freeze(); err != nil {
tb.Fatal(err)
}
sm := source.NewManager()
f, ds := parser.ParseSource(sm, "game/main.aes", []byte(benchProgram))
a := checker.Check(sm, map[string][]*ast.File{"game": {f}}, reg.Surface(), checker.Options{Module: "game"})
ds = append(ds, a.Diagnostics...)
mod, cds := compiler.Compile(a, "game", compiler.Options{Sources: sm})
if mod == nil || diag.HasErrors(append(ds, cds...)) {
tb.Fatal("bench program does not compile")
}
rt := New(Options{Bindings: reg, Schema: types.Schema()})
if err := rt.Link([]*bytecode.Module{mod}); err != nil {
tb.Fatal(err)
}
s := scene.New(types, nil, scene.Options{InitialNodes: walkers + idle + 1})
add := func(n int, typ string) {
for i := 0; i < n; i++ {
h := s.Create(s.Root(), typ+strconv.Itoa(i))
sc := scene.Add[scriptcomp.ScriptComponent](s, h)
sc.Attachments = append(sc.Attachments, scriptcomp.Attachment{TypeName: typ, Enabled: true})
}
}
add(walkers, "game.Walker")
add(idle, "game.Idle")
s.Flush()
if err := rt.Instantiate(s); err != nil {
tb.Fatal(err)
}
w := engine.NewWorld("bench")
w.Input = &input.State{}
return &bench{rt: rt, s: s, ctx: engine.Context{World: w, Dt: 1.0 / 60}}
}
func (b *bench) frame() {
b.ctx.Frame++
b.rt.Update(&b.ctx)
}
func TestUpdateZeroAllocs2k(t *testing.T) {
b := prepare(t, 2000, 0)
for i := 0; i < 30; i++ {
b.frame()
}
if allocs := testing.AllocsPerRun(50, b.frame); allocs > 0 {
t.Fatalf("%.2f Go allocations per frame with 2k scripted nodes, want 0", allocs)
}
if n := len(b.rt.Faults()); n != 0 {
t.Fatalf("%d faults: %v", n, b.rt.Faults()[0].Fault)
}
}
func BenchmarkUpdate2k(bm *testing.B) {
b := prepare(bm, 2000, 0)
for i := 0; i < 30; i++ {
b.frame()
}
bm.ReportAllocs()
bm.ResetTimer()
for i := 0; i < bm.N; i++ {
b.frame()
}
bm.ReportMetric(float64(b.rt.Stats().Instructions), "instr/frame")
}
func BenchmarkSchedule10k(bm *testing.B) {
b := prepare(bm, 2000, 8000)
b.frame()
b.frame()
if st := b.rt.Stats(); st.Update != 2000 || st.Instances != 10000 {
bm.Fatalf("update schedule %d of %d instances, want 2000 of 10000", st.Update, st.Instances)
}
bm.ReportAllocs()
bm.ResetTimer()
for i := 0; i < bm.N; i++ {
b.frame()
}
}
func TestSparseSchedule(t *testing.T) {
b := prepare(t, 20, 80)
b.frame()
b.frame()
if st := b.rt.Stats(); st.Update != 20 || st.Instances != 100 {
t.Fatalf("update schedule %d of %d instances, want 20 of 100", st.Update, st.Instances)
}
}
func BenchmarkEmit1k(bm *testing.B) {
g := newRig(bm, asyncProgram, 1000)
for i := 0; i < 1000; i++ {
attach(g.s, "Hub"+strconv.Itoa(i), "game.Hub", schema.PropertyBag{})
}
g.run(bm)
for i := 0; i < 30; i++ {
g.frame()
}
bm.ReportAllocs()
bm.ResetTimer()
for i := 0; i < bm.N; i++ {
g.frame()
}
bm.ReportMetric(float64(g.rt.Stats().Handlers), "handlers/frame")
}