523 lines
13 KiB
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()
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|