Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions internal/ecs/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Comment on lines +43 to +51

Copilot AI Mar 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BenchmarkArchetypeTransition alternates Set/Remove based on the loop index, but the chosen entity also changes each iteration. That means the Remove branch usually targets entities that never had testVelocity, so it mostly exercises the early-return path (no archetype move) rather than a real transition. Consider toggling per-entity state (e.g., track whether each entity currently has testVelocity, or alternate Set/Remove on the same entity) so both add/remove paths actually move between archetypes.

Suggested change
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)
}
hasVelocity := make([]bool, len(entities))
b.ResetTimer()
for i := range b.N {
idx := i % len(entities)
e := entities[idx]
if hasVelocity[idx] {
Remove[testVelocity](w, e)
} else {
Set(w, e, testVelocity{DX: 1})
}
hasVelocity[idx] = !hasVelocity[idx]

Copilot uses AI. Check for mistakes.
}
}

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)
Expand Down Expand Up @@ -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)
Expand All @@ -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()
}
}
4 changes: 4 additions & 0 deletions internal/ecs/component_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading