Repository: mlange-42/arche
Pull request #147 (2023-03-23 15:47:59)

Bugfix: Allow GC on pointers in removed components
Set component memory to zero when removing from or resetting archetype.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83a19d9..136238a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,10 @@
 * Speedup of archetype mask checks by 10% by checking mask before empty archetype (!139)
 * Speedup of generic queries and mappers to come closer to ID-based access (!144)
 
+### Bugfixes
+
+* Archetype storage buffers are "zeroed" when removing entities, to allow GC on pointers and slices in components (!147)
+
 ## [[v0.5.1]](https://github.com/mlange-42/arche/compare/v0.5.0...v0.5.1)
 
 ### Performance
diff --git a/ecs/archetype.go b/ecs/archetype.go
index 027c521..d6c6600 100644
--- a/ecs/archetype.go
+++ b/ecs/archetype.go
@@ -207,6 +207,7 @@ func (a *archetype) Remove(index uintptr) bool {
 			a.copy(src, dst, lay.itemSize)
 		}
 	}
+	a.ZeroAll(old)
 	a.len--
 
 	return swapped
@@ -264,6 +265,9 @@ func (a *archetype) SetPointer(index uintptr, id ID, comp unsafe.Pointer) unsafe
 // Does NOT free the reserved memory.
 func (a *archetype) Reset() {
 	a.len = 0
+	for _, buf := range a.buffers {
+		buf.SetZero()
+	}
 }
 
 // Components returns the component IDs for this archetype


Test patch:
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 5454c2f..1e638a8 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -2,7 +2,9 @@ package ecs
 
 import (
 	"fmt"
+	"math/rand"
 	"reflect"
+	"runtime"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -585,6 +587,85 @@ func TestWorldListener(t *testing.T) {
 
 }
 
+type withSlice struct {
+	Slice []int
+}
+
+func TestWorldRemoveGC(t *testing.T) {
+	w := NewWorld()
+	compID := ComponentID[withSlice](&w)
+
+	runtime.GC()
+	mem1 := runtime.MemStats{}
+	mem2 := runtime.MemStats{}
+	runtime.ReadMemStats(&mem1)
+
+	entities := []Entity{}
+	for i := 0; i < 100; i++ {
+		e := w.NewEntity(compID)
+		ws := (*withSlice)(w.Get(e, compID))
+		ws.Slice = make([]int, 10000)
+		entities = append(entities, e)
+	}
+
+	runtime.GC()
+	runtime.ReadMemStats(&mem2)
+	heap := int(mem2.HeapInuse - mem1.HeapInuse)
+	assert.Greater(t, heap, 8000000)
+	assert.Less(t, heap, 10000000)
+
+	rand.Shuffle(len(entities), func(i, j int) {
+		entities[i], entities[j] = entities[j], entities[i]
+	})
+
+	for _, e := range entities {
+		w.RemoveEntity(e)
+	}
+
+	runtime.GC()
+	runtime.ReadMemStats(&mem2)
+	heap = int(mem2.HeapInuse - mem1.HeapInuse)
+	assert.Less(t, heap, 800000)
+
+	w.NewEntity(compID)
+}
+
+func TestWorldResetGC(t *testing.T) {
+	w := NewWorld()
+	compID := ComponentID[withSlice](&w)
+
+	runtime.GC()
+	mem1 := runtime.MemStats{}
+	mem2 := runtime.MemStats{}
+	runtime.ReadMemStats(&mem1)
+
+	for i := 0; i < 100; i++ {
+		e := w.NewEntity(compID)
+		ws := (*withSlice)(w.Get(e, compID))
+		ws.Slice = make([]int, 10000)
+	}
+
+	runtime.ReadMemStats(&mem2)
+	heap := int(mem2.HeapInuse - mem1.HeapInuse)
+	assert.Greater(t, heap, 8000000)
+	assert.Less(t, heap, 10000000)
+
+	runtime.GC()
+	runtime.ReadMemStats(&mem2)
+	heap = int(mem2.HeapInuse - mem1.HeapInuse)
+	assert.Greater(t, heap, 8000000)
+	assert.Less(t, heap, 10000000)
+
+	w.Reset()
+
+	runtime.GC()
+	runtime.ReadMemStats(&mem2)
+	heap = int(mem2.HeapInuse - mem1.HeapInuse)
+	assert.Less(t, heap, 800000)
+
+	w.NewEntity(compID)
+}
+
 func Test1000Archetypes(t *testing.T) {
 	_ = testStruct0{1}
 	_ = testStruct1{1}