eq2go/internal/ground_spawn/benchmark_test.go

523 lines
13 KiB
Go

package ground_spawn
import (
"fmt"
"math/rand"
"sync"
"testing"
"eq2emu/internal/database"
)
// Mock implementations for benchmarking
// mockPlayer implements Player interface for benchmarks
type mockPlayer struct {
level int16
location int32
name string
}
func (p *mockPlayer) GetLevel() int16 { return p.level }
func (p *mockPlayer) GetLocation() int32 { return p.location }
func (p *mockPlayer) GetName() string { return p.name }
// mockSkill implements Skill interface for benchmarks
type mockSkill struct {
current int16
max int16
}
func (s *mockSkill) GetCurrentValue() int16 { return s.current }
func (s *mockSkill) GetMaxValue() int16 { return s.max }
// createTestGroundSpawn creates a ground spawn for benchmarking
func createTestGroundSpawn(b *testing.B, id int32) *GroundSpawn {
b.Helper()
// Don't create a database for every ground spawn - that's extremely expensive!
// Pass nil for the database since we're just benchmarking the in-memory operations
gs := New(nil)
gs.GroundSpawnID = id
gs.Name = fmt.Sprintf("Benchmark Node %d", id)
gs.CollectionSkill = "Mining"
gs.NumberHarvests = 5
gs.AttemptsPerHarvest = 1
gs.CurrentHarvests = 5
gs.X, gs.Y, gs.Z = float32(rand.Intn(1000)), float32(rand.Intn(1000)), float32(rand.Intn(1000))
gs.ZoneID = int32(rand.Intn(10) + 1)
gs.GridID = int32(rand.Intn(100))
// Add mock harvest entries for realistic benchmarking
gs.HarvestEntries = []*HarvestEntry{
{
GroundSpawnID: id,
MinSkillLevel: 10,
MinAdventureLevel: 1,
BonusTable: false,
Harvest1: 80.0,
Harvest3: 20.0,
Harvest5: 10.0,
HarvestImbue: 5.0,
HarvestRare: 2.0,
Harvest10: 1.0,
},
{
GroundSpawnID: id,
MinSkillLevel: 50,
MinAdventureLevel: 10,
BonusTable: true,
Harvest1: 90.0,
Harvest3: 30.0,
Harvest5: 15.0,
HarvestImbue: 8.0,
HarvestRare: 5.0,
Harvest10: 2.0,
},
}
// Add mock harvest items
gs.HarvestItems = []*HarvestEntryItem{
{GroundSpawnID: id, ItemID: 1001, IsRare: ItemRarityNormal, GridID: 0, Quantity: 1},
{GroundSpawnID: id, ItemID: 1002, IsRare: ItemRarityNormal, GridID: 0, Quantity: 1},
{GroundSpawnID: id, ItemID: 1003, IsRare: ItemRarityRare, GridID: 0, Quantity: 1},
{GroundSpawnID: id, ItemID: 1004, IsRare: ItemRarityImbue, GridID: 0, Quantity: 1},
}
return gs
}
// BenchmarkGroundSpawnCreation measures ground spawn creation performance
func BenchmarkGroundSpawnCreation(b *testing.B) {
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
if err != nil {
b.Fatalf("Failed to create test database: %v", err)
}
defer db.Close()
b.ResetTimer()
b.Run("Sequential", func(b *testing.B) {
for i := 0; i < b.N; i++ {
gs := New(db)
gs.GroundSpawnID = int32(i)
gs.Name = fmt.Sprintf("Node %d", i)
_ = gs
}
})
b.Run("Parallel", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
id := int32(0)
for pb.Next() {
gs := New(db)
gs.GroundSpawnID = id
gs.Name = fmt.Sprintf("Node %d", id)
id++
_ = gs
}
})
})
}
// BenchmarkGroundSpawnState measures state operations
func BenchmarkGroundSpawnState(b *testing.B) {
gs := createTestGroundSpawn(b, 1001)
b.Run("IsAvailable", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = gs.IsAvailable()
}
})
})
b.Run("IsDepleted", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = gs.IsDepleted()
}
})
})
b.Run("GetHarvestMessageName", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = gs.GetHarvestMessageName(true, false)
}
})
})
b.Run("GetHarvestSpellType", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = gs.GetHarvestSpellType()
}
})
})
}
// BenchmarkHarvestAlgorithm measures the core harvest processing performance
func BenchmarkHarvestAlgorithm(b *testing.B) {
gs := createTestGroundSpawn(b, 1001)
player := &mockPlayer{level: 50, location: 1, name: "BenchmarkPlayer"}
skill := &mockSkill{current: 75, max: 100}
b.Run("ProcessHarvest", func(b *testing.B) {
for b.Loop() {
// Reset harvest count for consistent benchmarking
gs.CurrentHarvests = gs.NumberHarvests
_, err := gs.ProcessHarvest(player, skill, 75)
if err != nil {
b.Fatalf("Harvest failed: %v", err)
}
}
})
b.Run("FilterHarvestTables", func(b *testing.B) {
for b.Loop() {
_ = gs.filterHarvestTables(player, 75)
}
})
b.Run("SelectHarvestTable", func(b *testing.B) {
tables := gs.filterHarvestTables(player, 75)
for b.Loop() {
_ = gs.selectHarvestTable(tables, 75)
}
})
b.Run("DetermineHarvestType", func(b *testing.B) {
tables := gs.filterHarvestTables(player, 75)
table := gs.selectHarvestTable(tables, 75)
for b.Loop() {
_ = gs.determineHarvestType(table, false)
}
})
b.Run("AwardHarvestItems", func(b *testing.B) {
for b.Loop() {
_ = gs.awardHarvestItems(HarvestType3Items, player)
}
})
}
// Global shared master list for benchmarks to avoid repeated setup
var (
sharedMasterList *MasterList
sharedSpawns []*GroundSpawn
setupOnce sync.Once
)
// setupSharedMasterList creates the shared master list once
func setupSharedMasterList(b *testing.B) {
setupOnce.Do(func() {
sharedMasterList = NewMasterList()
// Pre-populate with ground spawns for realistic testing
const numSpawns = 1000
sharedSpawns = make([]*GroundSpawn, numSpawns)
for i := range numSpawns {
sharedSpawns[i] = createTestGroundSpawn(b, int32(i+1))
sharedSpawns[i].ZoneID = int32(i%10 + 1) // Distribute across 10 zones
sharedSpawns[i].CollectionSkill = []string{"Mining", "Gathering", "Fishing", "Trapping"}[i%4]
// Create realistic spatial clusters within each zone
zoneBase := float32(sharedSpawns[i].ZoneID * 1000)
cluster := float32((i % 100) * 50) // Clusters of ~25 spawns
sharedSpawns[i].X = zoneBase + cluster + float32(rand.Intn(100)-50) // Add some spread
sharedSpawns[i].Y = zoneBase + cluster + float32(rand.Intn(100)-50)
sharedSpawns[i].Z = float32(rand.Intn(100))
sharedMasterList.AddGroundSpawn(sharedSpawns[i])
}
})
}
// BenchmarkMasterListOperations measures master list performance
func BenchmarkMasterListOperations(b *testing.B) {
setupSharedMasterList(b)
ml := sharedMasterList
b.Run("GetGroundSpawn", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
id := int32((rand.Intn(1000) + 1))
_ = ml.GetGroundSpawn(id)
}
})
})
b.Run("AddGroundSpawn", func(b *testing.B) {
// Create a separate master list for add operations to avoid contaminating shared list
addML := NewMasterList()
startID := int32(10000)
// Pre-create ground spawns to measure just the Add operation
spawnsToAdd := make([]*GroundSpawn, b.N)
for i := 0; i < b.N; i++ {
spawnsToAdd[i] = createTestGroundSpawn(b, startID+int32(i))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
addML.AddGroundSpawn(spawnsToAdd[i])
}
})
b.Run("GetByZone", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
zoneID := int32(rand.Intn(10) + 1)
_ = ml.GetByZone(zoneID)
}
})
})
b.Run("GetBySkill", func(b *testing.B) {
skills := []string{"Mining", "Gathering", "Fishing", "Trapping"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
skill := skills[rand.Intn(len(skills))]
_ = ml.GetBySkill(skill)
}
})
})
b.Run("GetAvailableSpawns", func(b *testing.B) {
for b.Loop() {
_ = ml.GetAvailableSpawns()
}
})
b.Run("GetDepletedSpawns", func(b *testing.B) {
for b.Loop() {
_ = ml.GetDepletedSpawns()
}
})
b.Run("GetStatistics", func(b *testing.B) {
for b.Loop() {
_ = ml.GetStatistics()
}
})
}
// BenchmarkConcurrentHarvesting measures concurrent harvest performance
func BenchmarkConcurrentHarvesting(b *testing.B) {
const numSpawns = 100
spawns := make([]*GroundSpawn, numSpawns)
for i := 0; i < numSpawns; i++ {
spawns[i] = createTestGroundSpawn(b, int32(i+1))
}
player := &mockPlayer{level: 50, location: 1, name: "BenchmarkPlayer"}
skill := &mockSkill{current: 75, max: 100}
b.Run("ConcurrentHarvesting", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
spawnIdx := rand.Intn(numSpawns)
gs := spawns[spawnIdx]
// Reset if depleted
if gs.IsDepleted() {
gs.Respawn()
}
_, _ = gs.ProcessHarvest(player, skill, 75)
}
})
})
}
// BenchmarkMemoryAllocation measures memory allocation patterns
func BenchmarkMemoryAllocation(b *testing.B) {
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
if err != nil {
b.Fatalf("Failed to create test database: %v", err)
}
defer db.Close()
b.Run("GroundSpawnAllocation", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
gs := New(db)
gs.GroundSpawnID = int32(i)
gs.HarvestEntries = make([]*HarvestEntry, 2)
gs.HarvestItems = make([]*HarvestEntryItem, 4)
_ = gs
}
})
b.Run("MasterListAllocation", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
ml := NewMasterList()
_ = ml
}
})
b.Run("HarvestResultAllocation", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
result := &HarvestResult{
Success: true,
HarvestType: HarvestType3Items,
ItemsAwarded: make([]*HarvestedItem, 3),
MessageText: "You harvested 3 items",
SkillGained: false,
}
_ = result
}
})
b.Run("SpatialAddGroundSpawn_Allocations", func(b *testing.B) {
b.ReportAllocs()
ml := NewMasterList()
for i := 0; i < b.N; i++ {
gs := createTestGroundSpawn(b, int32(i+1))
gs.ZoneID = int32(i%10 + 1)
gs.CollectionSkill = []string{"Mining", "Gathering", "Fishing", "Trapping"}[i%4]
gs.X = float32(rand.Intn(1000))
gs.Y = float32(rand.Intn(1000))
ml.AddGroundSpawn(gs)
}
})
b.Run("SpatialGetNearby_Allocations", func(b *testing.B) {
setupSharedMasterList(b)
ml := sharedMasterList
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
x := float32(rand.Intn(10000))
y := float32(rand.Intn(10000))
_ = ml.GetNearby(x, y, 100.0)
}
})
}
// BenchmarkRespawnOperations measures respawn performance
func BenchmarkRespawnOperations(b *testing.B) {
gs := createTestGroundSpawn(b, 1001)
b.Run("Respawn", func(b *testing.B) {
for b.Loop() {
gs.CurrentHarvests = 0 // Deplete
gs.Respawn()
}
})
b.Run("RespawnWithRandomHeading", func(b *testing.B) {
gs.RandomizeHeading = true
for b.Loop() {
gs.CurrentHarvests = 0 // Deplete
gs.Respawn()
}
})
}
// BenchmarkStringOperations measures string processing performance
func BenchmarkStringOperations(b *testing.B) {
skills := []string{"Mining", "Gathering", "Collecting", "Fishing", "Trapping", "Foresting", "Unknown"}
b.Run("HarvestMessageGeneration", func(b *testing.B) {
gs := createTestGroundSpawn(b, 1001)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
skill := skills[rand.Intn(len(skills))]
gs.CollectionSkill = skill
_ = gs.GetHarvestMessageName(rand.Intn(2) == 1, rand.Intn(2) == 1)
}
})
})
b.Run("SpellTypeGeneration", func(b *testing.B) {
gs := createTestGroundSpawn(b, 1001)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
skill := skills[rand.Intn(len(skills))]
gs.CollectionSkill = skill
_ = gs.GetHarvestSpellType()
}
})
})
}
// BenchmarkSpatialFeatures tests features unique to spatial implementation
func BenchmarkSpatialFeatures(b *testing.B) {
setupSharedMasterList(b)
ml := sharedMasterList
b.Run("GetByZoneAndSkill", func(b *testing.B) {
skills := []string{"Mining", "Gathering", "Fishing", "Trapping"}
for b.Loop() {
zoneID := int32(rand.Intn(10) + 1)
skill := skills[rand.Intn(len(skills))]
_ = ml.GetByZoneAndSkill(zoneID, skill)
}
})
b.Run("GetNearby_Small", func(b *testing.B) {
for b.Loop() {
x := float32(rand.Intn(10000))
y := float32(rand.Intn(10000))
_ = ml.GetNearby(x, y, 50.0) // Small radius
}
})
b.Run("GetNearby_Medium", func(b *testing.B) {
for b.Loop() {
x := float32(rand.Intn(10000))
y := float32(rand.Intn(10000))
_ = ml.GetNearby(x, y, 200.0) // Medium radius
}
})
b.Run("GetNearby_Large", func(b *testing.B) {
for b.Loop() {
x := float32(rand.Intn(10000))
y := float32(rand.Intn(10000))
_ = ml.GetNearby(x, y, 500.0) // Large radius
}
})
}
// BenchmarkConcurrentSpatialOperations tests thread safety and mixed workloads
func BenchmarkConcurrentSpatialOperations(b *testing.B) {
setupSharedMasterList(b)
ml := sharedMasterList
b.Run("MixedSpatialOperations", func(b *testing.B) {
skills := []string{"Mining", "Gathering", "Fishing", "Trapping"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
switch rand.Intn(6) {
case 0:
id := int32(rand.Intn(1000) + 1)
_ = ml.GetGroundSpawn(id)
case 1:
zoneID := int32(rand.Intn(10) + 1)
_ = ml.GetByZone(zoneID)
case 2:
skill := skills[rand.Intn(len(skills))]
_ = ml.GetBySkill(skill)
case 3:
zoneID := int32(rand.Intn(10) + 1)
skill := skills[rand.Intn(len(skills))]
_ = ml.GetByZoneAndSkill(zoneID, skill)
case 4:
x := float32(rand.Intn(10000))
y := float32(rand.Intn(10000))
_ = ml.GetNearby(x, y, 100.0)
case 5:
_ = ml.GetAvailableSpawns()
}
}
})
})
}