diff --git a/internal/ecs/benchmark_test.go b/internal/ecs/benchmark_test.go index 7fe8166..7924dc4 100644 --- a/internal/ecs/benchmark_test.go +++ b/internal/ecs/benchmark_test.go @@ -17,6 +17,60 @@ func BenchmarkEntityCreation(b *testing.B) { } } +func BenchmarkEntityRecycling(b *testing.B) { + w := NewWorld() + RegisterComponent[testPosition](w) + + b.ResetTimer() + for range b.N { + e := w.NewEntity() + Set(w, e, testPosition{X: 1, Y: 2}) + w.DestroyEntity(e) + } +} + +func BenchmarkArchetypeTransition(b *testing.B) { + w := NewWorld() + RegisterComponent[testPosition](w) + RegisterComponent[testVelocity](w) + RegisterComponent[testHealth](w) + + entities := make([]Entity, 1000) + for i := range entities { + entities[i] = w.NewEntity() + Set(w, entities[i], testPosition{X: float64(i)}) + } + + b.ResetTimer() + for i := range b.N { + e := entities[i%len(entities)] + if i%2 == 0 { + Set(w, e, testVelocity{DX: 1}) + } else { + Remove[testVelocity](w, e) + } + } +} + +func BenchmarkQuery1_1k(b *testing.B) { + w := NewWorld() + RegisterComponent[testPosition](w) + + for range 1_000 { + e := w.NewEntity() + Set(w, e, testPosition{X: 1, Y: 2}) + } + + q := NewQuery1[testPosition](w) + b.ResetTimer() + + for range b.N { + q.Each(func(_ Entity, pos *testPosition) { + pos.X += 1 + }) + } +} + func BenchmarkQuery2_10k(b *testing.B) { w := NewWorld() RegisterComponent[testPosition](w) @@ -61,6 +115,146 @@ func BenchmarkQuery2_100k(b *testing.B) { } } +func BenchmarkQuery3_10k(b *testing.B) { + w := NewWorld() + RegisterComponent[testPosition](w) + RegisterComponent[testVelocity](w) + RegisterComponent[testHealth](w) + + for range 10_000 { + e := w.NewEntity() + Set(w, e, testPosition{X: 1, Y: 2}) + Set(w, e, testVelocity{DX: 0.5, DY: 0.1}) + Set(w, e, testHealth{Current: 100, Max: 100}) + } + + q := NewQuery3[testPosition, testVelocity, testHealth](w) + b.ResetTimer() + + for range b.N { + q.Each(func(_ Entity, pos *testPosition, vel *testVelocity, hp *testHealth) { + pos.X += vel.DX + pos.Y += vel.DY + hp.Current-- + }) + } +} + +func BenchmarkQuery4_10k(b *testing.B) { + w := NewWorld() + RegisterComponent[testPosition](w) + RegisterComponent[testVelocity](w) + RegisterComponent[testHealth](w) + RegisterComponent[testDamage](w) + + for range 10_000 { + e := w.NewEntity() + Set(w, e, testPosition{X: 1, Y: 2}) + Set(w, e, testVelocity{DX: 0.5, DY: 0.1}) + Set(w, e, testHealth{Current: 100, Max: 100}) + Set(w, e, testDamage{Amount: 10}) + } + + q := NewQuery4[testPosition, testVelocity, testHealth, testDamage](w) + b.ResetTimer() + + for range b.N { + q.Each(func(_ Entity, pos *testPosition, vel *testVelocity, hp *testHealth, dmg *testDamage) { + pos.X += vel.DX + hp.Current -= dmg.Amount + }) + } +} + +func BenchmarkQuery2_Iter_10k(b *testing.B) { + w := NewWorld() + RegisterComponent[testPosition](w) + RegisterComponent[testVelocity](w) + + for range 10_000 { + e := w.NewEntity() + Set(w, e, testPosition{X: 1, Y: 2}) + Set(w, e, testVelocity{DX: 0.5, DY: 0.1}) + } + + q := NewQuery2[testPosition, testVelocity](w) + b.ResetTimer() + + for range b.N { + for _, pos := range q.Iter() { + pos.X += 1 + } + } +} + +func BenchmarkEventBus_SyncDispatch(b *testing.B) { + bus := NewEventBus() + count := 0 + bus.On(func(_ Event) { count++ }) + + evt := Event{Type: EventEntityCreated} + b.ResetTimer() + + for range b.N { + bus.publish(evt) + } +} + +func BenchmarkEventBus_AsyncDispatch(b *testing.B) { + bus := NewEventBus() + ch := bus.Subscribe(1024) + + // Drain subscriber in background to prevent blocking. + done := make(chan struct{}) + go func() { + for range ch { + } + close(done) + }() + + evt := Event{Type: EventEntityCreated} + b.ResetTimer() + + for range b.N { + bus.publish(evt) + } + + b.StopTimer() + bus.Unsubscribe(ch) + <-done +} + +func BenchmarkGetComponentValue(b *testing.B) { + w := NewWorld() + posID := RegisterComponent[testPosition](w) + + e := w.NewEntity() + Set(w, e, testPosition{X: 42, Y: 99}) + + b.ResetTimer() + for range b.N { + v, ok := w.GetComponentValue(e, posID) + if !ok { + b.Fatal("expected ok") + } + _ = v + } +} + +func BenchmarkSetComponentValue(b *testing.B) { + w := NewWorld() + posID := RegisterComponent[testPosition](w) + + e := w.NewEntity() + Set(w, e, testPosition{X: 1, Y: 2}) + val := testPosition{X: 42, Y: 99} + + b.ResetTimer() + for range b.N { + w.SetComponentValue(e, posID, val) + } +} + func BenchmarkWorldUpdate_10k(b *testing.B) { w := NewWorld() RegisterComponent[testPosition](w) @@ -80,3 +274,23 @@ func BenchmarkWorldUpdate_10k(b *testing.B) { w.Update() } } + +func BenchmarkWorldUpdate_100k(b *testing.B) { + w := NewWorld() + RegisterComponent[testPosition](w) + RegisterComponent[testVelocity](w) + + sys := &testMovementSystem{} + w.AddSystem(sys) + + for range 100_000 { + e := w.NewEntity() + Set(w, e, testPosition{X: 1, Y: 2}) + Set(w, e, testVelocity{DX: 0.5, DY: 0.1}) + } + + b.ResetTimer() + for range b.N { + w.Update() + } +} diff --git a/internal/ecs/component_test.go b/internal/ecs/component_test.go index 53614d9..366e396 100644 --- a/internal/ecs/component_test.go +++ b/internal/ecs/component_test.go @@ -16,6 +16,10 @@ type testHealth struct { Current, Max int } +type testDamage struct { + Amount int +} + func TestRegisterComponent(t *testing.T) { w := NewWorld() id1 := RegisterComponent[testPosition](w)