revert achievements to bespoke master list
This commit is contained in:
parent
c31866d080
commit
4a17075783
@ -1,6 +1,7 @@
|
||||
package achievements
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
@ -60,8 +61,8 @@ func TestSimpleAchievement(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMasterListWithGeneric tests the master list with generic base
|
||||
func TestMasterListWithGeneric(t *testing.T) {
|
||||
// TestMasterList tests the bespoke master list implementation
|
||||
func TestMasterList(t *testing.T) {
|
||||
masterList := NewMasterList()
|
||||
|
||||
if masterList == nil {
|
||||
@ -72,22 +73,52 @@ func TestMasterListWithGeneric(t *testing.T) {
|
||||
t.Errorf("Expected size 0, got %d", masterList.Size())
|
||||
}
|
||||
|
||||
// Create an achievement (need database for new pattern)
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
// Create test database
|
||||
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
achievement := New(db)
|
||||
achievement.AchievementID = 1001
|
||||
achievement.Title = "Test Achievement"
|
||||
achievement.Category = "Testing"
|
||||
// Create achievements for testing
|
||||
achievement1 := New(db)
|
||||
achievement1.AchievementID = 1001
|
||||
achievement1.Title = "Test Achievement 1"
|
||||
achievement1.Category = "Testing"
|
||||
achievement1.Expansion = "Classic"
|
||||
|
||||
achievement2 := New(db)
|
||||
achievement2.AchievementID = 1002
|
||||
achievement2.Title = "Test Achievement 2"
|
||||
achievement2.Category = "Combat"
|
||||
achievement2.Expansion = "Classic"
|
||||
|
||||
achievement3 := New(db)
|
||||
achievement3.AchievementID = 1003
|
||||
achievement3.Title = "Test Achievement 3"
|
||||
achievement3.Category = "Testing"
|
||||
achievement3.Expansion = "Expansion1"
|
||||
|
||||
// Test adding
|
||||
if !masterList.AddAchievement(achievement) {
|
||||
t.Error("Should successfully add achievement")
|
||||
if !masterList.AddAchievement(achievement1) {
|
||||
t.Error("Should successfully add achievement1")
|
||||
}
|
||||
|
||||
if masterList.Size() != 1 {
|
||||
t.Errorf("Expected size 1, got %d", masterList.Size())
|
||||
if !masterList.AddAchievement(achievement2) {
|
||||
t.Error("Should successfully add achievement2")
|
||||
}
|
||||
|
||||
if !masterList.AddAchievement(achievement3) {
|
||||
t.Error("Should successfully add achievement3")
|
||||
}
|
||||
|
||||
if masterList.Size() != 3 {
|
||||
t.Errorf("Expected size 3, got %d", masterList.Size())
|
||||
}
|
||||
|
||||
// Test duplicate add (should fail)
|
||||
if masterList.AddAchievement(achievement1) {
|
||||
t.Error("Should not add duplicate achievement")
|
||||
}
|
||||
|
||||
// Test retrieving
|
||||
@ -96,13 +127,160 @@ func TestMasterListWithGeneric(t *testing.T) {
|
||||
t.Error("Should retrieve added achievement")
|
||||
}
|
||||
|
||||
if retrieved.Title != "Test Achievement" {
|
||||
t.Errorf("Expected title 'Test Achievement', got '%s'", retrieved.Title)
|
||||
if retrieved.Title != "Test Achievement 1" {
|
||||
t.Errorf("Expected title 'Test Achievement 1', got '%s'", retrieved.Title)
|
||||
}
|
||||
|
||||
// Test filtering
|
||||
achievements := masterList.GetAchievementsByCategory("Testing")
|
||||
if len(achievements) != 1 {
|
||||
t.Errorf("Expected 1 achievement in Testing category, got %d", len(achievements))
|
||||
// Test category filtering
|
||||
testingAchievements := masterList.GetAchievementsByCategory("Testing")
|
||||
if len(testingAchievements) != 2 {
|
||||
t.Errorf("Expected 2 achievements in Testing category, got %d", len(testingAchievements))
|
||||
}
|
||||
|
||||
combatAchievements := masterList.GetAchievementsByCategory("Combat")
|
||||
if len(combatAchievements) != 1 {
|
||||
t.Errorf("Expected 1 achievement in Combat category, got %d", len(combatAchievements))
|
||||
}
|
||||
|
||||
// Test expansion filtering
|
||||
classicAchievements := masterList.GetAchievementsByExpansion("Classic")
|
||||
if len(classicAchievements) != 2 {
|
||||
t.Errorf("Expected 2 achievements in Classic expansion, got %d", len(classicAchievements))
|
||||
}
|
||||
|
||||
expansion1Achievements := masterList.GetAchievementsByExpansion("Expansion1")
|
||||
if len(expansion1Achievements) != 1 {
|
||||
t.Errorf("Expected 1 achievement in Expansion1, got %d", len(expansion1Achievements))
|
||||
}
|
||||
|
||||
// Test combined filtering
|
||||
combined := masterList.GetAchievementsByCategoryAndExpansion("Testing", "Classic")
|
||||
if len(combined) != 1 {
|
||||
t.Errorf("Expected 1 achievement matching Testing+Classic, got %d", len(combined))
|
||||
}
|
||||
|
||||
// Test metadata caching
|
||||
categories := masterList.GetCategories()
|
||||
if len(categories) != 2 {
|
||||
t.Errorf("Expected 2 unique categories, got %d", len(categories))
|
||||
}
|
||||
|
||||
expansions := masterList.GetExpansions()
|
||||
if len(expansions) != 2 {
|
||||
t.Errorf("Expected 2 unique expansions, got %d", len(expansions))
|
||||
}
|
||||
|
||||
// Test clone
|
||||
clone := masterList.GetAchievementClone(1001)
|
||||
if clone == nil {
|
||||
t.Error("Should return cloned achievement")
|
||||
}
|
||||
|
||||
if clone.Title != "Test Achievement 1" {
|
||||
t.Errorf("Expected cloned title 'Test Achievement 1', got '%s'", clone.Title)
|
||||
}
|
||||
|
||||
// Test GetAllAchievements
|
||||
allAchievements := masterList.GetAllAchievements()
|
||||
if len(allAchievements) != 3 {
|
||||
t.Errorf("Expected 3 achievements in GetAll, got %d", len(allAchievements))
|
||||
}
|
||||
|
||||
// Test update
|
||||
updatedAchievement := New(db)
|
||||
updatedAchievement.AchievementID = 1001
|
||||
updatedAchievement.Title = "Updated Achievement"
|
||||
updatedAchievement.Category = "Updated"
|
||||
updatedAchievement.Expansion = "Updated"
|
||||
|
||||
if err := masterList.UpdateAchievement(updatedAchievement); err != nil {
|
||||
t.Errorf("Update should succeed: %v", err)
|
||||
}
|
||||
|
||||
// Verify update worked
|
||||
retrievedUpdated := masterList.GetAchievement(1001)
|
||||
if retrievedUpdated.Title != "Updated Achievement" {
|
||||
t.Errorf("Expected updated title 'Updated Achievement', got '%s'", retrievedUpdated.Title)
|
||||
}
|
||||
|
||||
// Verify category index updated
|
||||
updatedCategoryAchievements := masterList.GetAchievementsByCategory("Updated")
|
||||
if len(updatedCategoryAchievements) != 1 {
|
||||
t.Errorf("Expected 1 achievement in Updated category, got %d", len(updatedCategoryAchievements))
|
||||
}
|
||||
|
||||
// Test removal
|
||||
if !masterList.RemoveAchievement(1001) {
|
||||
t.Error("Should successfully remove achievement")
|
||||
}
|
||||
|
||||
if masterList.Size() != 2 {
|
||||
t.Errorf("Expected size 2 after removal, got %d", masterList.Size())
|
||||
}
|
||||
|
||||
// Test clear
|
||||
masterList.Clear()
|
||||
if masterList.Size() != 0 {
|
||||
t.Errorf("Expected size 0 after clear, got %d", masterList.Size())
|
||||
}
|
||||
}
|
||||
|
||||
// TestMasterListConcurrency tests thread safety of the master list
|
||||
func TestMasterListConcurrency(t *testing.T) {
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Create test database
|
||||
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
const numWorkers = 10
|
||||
const achievementsPerWorker = 100
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Concurrently add achievements
|
||||
wg.Add(numWorkers)
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
go func(workerID int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < achievementsPerWorker; j++ {
|
||||
achievement := New(db)
|
||||
achievement.AchievementID = uint32(workerID*achievementsPerWorker + j + 1)
|
||||
achievement.Title = "Concurrent Test"
|
||||
achievement.Category = "Concurrency"
|
||||
achievement.Expansion = "Test"
|
||||
masterList.AddAchievement(achievement)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Concurrently read achievements
|
||||
wg.Add(numWorkers)
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < achievementsPerWorker; j++ {
|
||||
// Random reads
|
||||
_ = masterList.GetAchievement(uint32(j + 1))
|
||||
_ = masterList.GetAchievementsByCategory("Concurrency")
|
||||
_ = masterList.GetAchievementsByExpansion("Test")
|
||||
_ = masterList.Size()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify final state
|
||||
expectedSize := numWorkers * achievementsPerWorker
|
||||
if masterList.Size() != expectedSize {
|
||||
t.Errorf("Expected size %d, got %d", expectedSize, masterList.Size())
|
||||
}
|
||||
|
||||
categories := masterList.GetCategories()
|
||||
if len(categories) != 1 || categories[0] != "Concurrency" {
|
||||
t.Errorf("Expected 1 category 'Concurrency', got %v", categories)
|
||||
}
|
||||
}
|
||||
|
364
internal/achievements/benchmark_test.go
Normal file
364
internal/achievements/benchmark_test.go
Normal file
@ -0,0 +1,364 @@
|
||||
package achievements
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// Global shared master list for benchmarks to avoid repeated setup
|
||||
var (
|
||||
sharedAchievementMasterList *MasterList
|
||||
sharedAchievements []*Achievement
|
||||
achievementSetupOnce sync.Once
|
||||
)
|
||||
|
||||
// setupSharedAchievementMasterList creates the shared master list once
|
||||
func setupSharedAchievementMasterList(b *testing.B) {
|
||||
achievementSetupOnce.Do(func() {
|
||||
// Create test database
|
||||
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
|
||||
sharedAchievementMasterList = NewMasterList()
|
||||
|
||||
// Pre-populate with achievements for realistic testing
|
||||
const numAchievements = 1000
|
||||
sharedAchievements = make([]*Achievement, numAchievements)
|
||||
|
||||
categories := []string{"Combat", "Crafting", "Exploration", "Social", "PvP", "Quests", "Collections", "Dungeons"}
|
||||
expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer", "Rise of Kunark", "The Shadow Odyssey", "Sentinel's Fate"}
|
||||
|
||||
for i := range numAchievements {
|
||||
sharedAchievements[i] = New(db)
|
||||
sharedAchievements[i].AchievementID = uint32(i + 1)
|
||||
sharedAchievements[i].Title = fmt.Sprintf("Achievement %d", i+1)
|
||||
sharedAchievements[i].Category = categories[i%len(categories)]
|
||||
sharedAchievements[i].Expansion = expansions[i%len(expansions)]
|
||||
sharedAchievements[i].PointValue = uint32(rand.Intn(50) + 10)
|
||||
sharedAchievements[i].QtyRequired = uint32(rand.Intn(100) + 1)
|
||||
|
||||
// Add some requirements and rewards
|
||||
sharedAchievements[i].AddRequirement(fmt.Sprintf("task_%d", i%10), uint32(rand.Intn(10)+1))
|
||||
sharedAchievements[i].AddReward(fmt.Sprintf("reward_%d", i%5))
|
||||
|
||||
sharedAchievementMasterList.AddAchievement(sharedAchievements[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// createTestAchievement creates an achievement for benchmarking
|
||||
func createTestAchievement(b *testing.B, id uint32) *Achievement {
|
||||
b.Helper()
|
||||
|
||||
// Use nil database for benchmarking in-memory operations
|
||||
achievement := New(nil)
|
||||
achievement.AchievementID = id
|
||||
achievement.Title = fmt.Sprintf("Benchmark Achievement %d", id)
|
||||
achievement.Category = []string{"Combat", "Crafting", "Exploration", "Social"}[id%4]
|
||||
achievement.Expansion = []string{"Classic", "Expansion1", "Expansion2"}[id%3]
|
||||
achievement.PointValue = uint32(rand.Intn(50) + 10)
|
||||
achievement.QtyRequired = uint32(rand.Intn(100) + 1)
|
||||
|
||||
// Add mock requirements and rewards
|
||||
achievement.AddRequirement(fmt.Sprintf("task_%d", id%10), uint32(rand.Intn(10)+1))
|
||||
achievement.AddReward(fmt.Sprintf("reward_%d", id%5))
|
||||
|
||||
return achievement
|
||||
}
|
||||
|
||||
// BenchmarkAchievementCreation measures achievement creation performance
|
||||
func BenchmarkAchievementCreation(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++ {
|
||||
achievement := New(db)
|
||||
achievement.AchievementID = uint32(i)
|
||||
achievement.Title = fmt.Sprintf("Achievement %d", i)
|
||||
_ = achievement
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Parallel", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
id := uint32(0)
|
||||
for pb.Next() {
|
||||
achievement := New(db)
|
||||
achievement.AchievementID = id
|
||||
achievement.Title = fmt.Sprintf("Achievement %d", id)
|
||||
id++
|
||||
_ = achievement
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkAchievementOperations measures individual achievement operations
|
||||
func BenchmarkAchievementOperations(b *testing.B) {
|
||||
achievement := createTestAchievement(b, 1001)
|
||||
|
||||
b.Run("GetID", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_ = achievement.GetID()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("IsNew", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_ = achievement.IsNew()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("Clone", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_ = achievement.Clone()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkMasterListOperations measures master list performance
|
||||
func BenchmarkMasterListOperations(b *testing.B) {
|
||||
setupSharedAchievementMasterList(b)
|
||||
ml := sharedAchievementMasterList
|
||||
|
||||
b.Run("GetAchievement", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
id := uint32(rand.Intn(1000) + 1)
|
||||
_ = ml.GetAchievement(id)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("AddAchievement", func(b *testing.B) {
|
||||
// Create a separate master list for add operations
|
||||
addML := NewMasterList()
|
||||
startID := uint32(10000)
|
||||
// Pre-create achievements to measure just the Add operation
|
||||
achievementsToAdd := make([]*Achievement, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
achievementsToAdd[i] = createTestAchievement(b, startID+uint32(i))
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
addML.AddAchievement(achievementsToAdd[i])
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetAchievementsByCategory", func(b *testing.B) {
|
||||
categories := []string{"Combat", "Crafting", "Exploration", "Social", "PvP", "Quests", "Collections", "Dungeons"}
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
category := categories[rand.Intn(len(categories))]
|
||||
_ = ml.GetAchievementsByCategory(category)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("GetAchievementsByExpansion", func(b *testing.B) {
|
||||
expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer", "Rise of Kunark", "The Shadow Odyssey", "Sentinel's Fate"}
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
expansion := expansions[rand.Intn(len(expansions))]
|
||||
_ = ml.GetAchievementsByExpansion(expansion)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("GetAchievementsByCategoryAndExpansion", func(b *testing.B) {
|
||||
categories := []string{"Combat", "Crafting", "Exploration", "Social"}
|
||||
expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer"}
|
||||
for b.Loop() {
|
||||
category := categories[rand.Intn(len(categories))]
|
||||
expansion := expansions[rand.Intn(len(expansions))]
|
||||
_ = ml.GetAchievementsByCategoryAndExpansion(category, expansion)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetCategories", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_ = ml.GetCategories()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetExpansions", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_ = ml.GetExpansions()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Size", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_ = ml.Size()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkConcurrentOperations tests mixed workload performance
|
||||
func BenchmarkConcurrentOperations(b *testing.B) {
|
||||
setupSharedAchievementMasterList(b)
|
||||
ml := sharedAchievementMasterList
|
||||
|
||||
b.Run("MixedOperations", func(b *testing.B) {
|
||||
categories := []string{"Combat", "Crafting", "Exploration", "Social", "PvP", "Quests", "Collections", "Dungeons"}
|
||||
expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer", "Rise of Kunark", "The Shadow Odyssey", "Sentinel's Fate"}
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
switch rand.Intn(7) {
|
||||
case 0:
|
||||
id := uint32(rand.Intn(1000) + 1)
|
||||
_ = ml.GetAchievement(id)
|
||||
case 1:
|
||||
category := categories[rand.Intn(len(categories))]
|
||||
_ = ml.GetAchievementsByCategory(category)
|
||||
case 2:
|
||||
expansion := expansions[rand.Intn(len(expansions))]
|
||||
_ = ml.GetAchievementsByExpansion(expansion)
|
||||
case 3:
|
||||
category := categories[rand.Intn(len(categories))]
|
||||
expansion := expansions[rand.Intn(len(expansions))]
|
||||
_ = ml.GetAchievementsByCategoryAndExpansion(category, expansion)
|
||||
case 4:
|
||||
_ = ml.GetCategories()
|
||||
case 5:
|
||||
_ = ml.GetExpansions()
|
||||
case 6:
|
||||
_ = ml.Size()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 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("AchievementAllocation", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
achievement := New(db)
|
||||
achievement.AchievementID = uint32(i)
|
||||
achievement.Requirements = make([]Requirement, 2)
|
||||
achievement.Rewards = make([]Reward, 3)
|
||||
_ = achievement
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("MasterListAllocation", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
ml := NewMasterList()
|
||||
_ = ml
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("AddAchievement_Allocations", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
ml := NewMasterList()
|
||||
for i := 0; i < b.N; i++ {
|
||||
achievement := createTestAchievement(b, uint32(i+1))
|
||||
ml.AddAchievement(achievement)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetAchievementsByCategory_Allocations", func(b *testing.B) {
|
||||
setupSharedAchievementMasterList(b)
|
||||
ml := sharedAchievementMasterList
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = ml.GetAchievementsByCategory("Combat")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetCategories_Allocations", func(b *testing.B) {
|
||||
setupSharedAchievementMasterList(b)
|
||||
ml := sharedAchievementMasterList
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
_ = ml.GetCategories()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkUpdateOperations measures update performance
|
||||
func BenchmarkUpdateOperations(b *testing.B) {
|
||||
setupSharedAchievementMasterList(b)
|
||||
ml := sharedAchievementMasterList
|
||||
|
||||
b.Run("UpdateAchievement", func(b *testing.B) {
|
||||
// Create achievements to update
|
||||
updateAchievements := make([]*Achievement, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
updateAchievements[i] = createTestAchievement(b, uint32((i%1000)+1))
|
||||
updateAchievements[i].Title = "Updated Title"
|
||||
updateAchievements[i].Category = "Updated"
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ml.UpdateAchievement(updateAchievements[i])
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("RemoveAchievement", func(b *testing.B) {
|
||||
// Create a separate master list for removal testing
|
||||
removeML := NewMasterList()
|
||||
|
||||
// Add achievements to remove
|
||||
for i := 0; i < b.N; i++ {
|
||||
achievement := createTestAchievement(b, uint32(i+1))
|
||||
removeML.AddAchievement(achievement)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
removeML.RemoveAchievement(uint32(i + 1))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkCloneOperations measures cloning performance
|
||||
func BenchmarkCloneOperations(b *testing.B) {
|
||||
setupSharedAchievementMasterList(b)
|
||||
ml := sharedAchievementMasterList
|
||||
|
||||
b.Run("GetAchievementClone", func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
id := uint32(rand.Intn(1000) + 1)
|
||||
_ = ml.GetAchievementClone(id)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("DirectClone", func(b *testing.B) {
|
||||
achievement := createTestAchievement(b, 1001)
|
||||
for b.Loop() {
|
||||
_ = achievement.Clone()
|
||||
}
|
||||
})
|
||||
}
|
@ -2,112 +2,329 @@ package achievements
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"eq2emu/internal/common"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MasterList manages the global list of all achievements
|
||||
// MasterList is a specialized achievement master list optimized for:
|
||||
// - Fast ID-based lookups (O(1))
|
||||
// - Fast category-based lookups (O(1))
|
||||
// - Fast expansion-based lookups (O(1))
|
||||
// - Efficient filtering and iteration
|
||||
type MasterList struct {
|
||||
*common.MasterList[uint32, *Achievement]
|
||||
// Core storage
|
||||
achievements map[uint32]*Achievement // ID -> Achievement
|
||||
mutex sync.RWMutex
|
||||
|
||||
// Category indices for O(1) lookups
|
||||
byCategory map[string][]*Achievement // Category -> achievements
|
||||
byExpansion map[string][]*Achievement // Expansion -> achievements
|
||||
|
||||
// Cached metadata
|
||||
categories []string // Unique categories (cached)
|
||||
expansions []string // Unique expansions (cached)
|
||||
metaStale bool // Whether metadata cache needs refresh
|
||||
}
|
||||
|
||||
// NewMasterList creates a new master achievement list
|
||||
// NewMasterList creates a new specialized achievement master list
|
||||
func NewMasterList() *MasterList {
|
||||
return &MasterList{
|
||||
MasterList: common.NewMasterList[uint32, *Achievement](),
|
||||
achievements: make(map[uint32]*Achievement),
|
||||
byCategory: make(map[string][]*Achievement),
|
||||
byExpansion: make(map[string][]*Achievement),
|
||||
metaStale: true,
|
||||
}
|
||||
}
|
||||
|
||||
// AddAchievement adds an achievement to the master list
|
||||
// Returns false if achievement with same ID already exists
|
||||
// refreshMetaCache updates the categories and expansions cache
|
||||
func (m *MasterList) refreshMetaCache() {
|
||||
if !m.metaStale {
|
||||
return
|
||||
}
|
||||
|
||||
categorySet := make(map[string]struct{})
|
||||
expansionSet := make(map[string]struct{})
|
||||
|
||||
// Collect unique categories and expansions
|
||||
for _, achievement := range m.achievements {
|
||||
if achievement.Category != "" {
|
||||
categorySet[achievement.Category] = struct{}{}
|
||||
}
|
||||
if achievement.Expansion != "" {
|
||||
expansionSet[achievement.Expansion] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear existing caches and rebuild
|
||||
m.categories = m.categories[:0]
|
||||
for category := range categorySet {
|
||||
m.categories = append(m.categories, category)
|
||||
}
|
||||
|
||||
m.expansions = m.expansions[:0]
|
||||
for expansion := range expansionSet {
|
||||
m.expansions = append(m.expansions, expansion)
|
||||
}
|
||||
|
||||
m.metaStale = false
|
||||
}
|
||||
|
||||
// AddAchievement adds an achievement with full indexing
|
||||
func (m *MasterList) AddAchievement(achievement *Achievement) bool {
|
||||
if achievement == nil {
|
||||
return false
|
||||
}
|
||||
return m.MasterList.Add(achievement)
|
||||
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// Check if exists
|
||||
if _, exists := m.achievements[achievement.AchievementID]; exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Add to core storage
|
||||
m.achievements[achievement.AchievementID] = achievement
|
||||
|
||||
// Update category index
|
||||
if achievement.Category != "" {
|
||||
m.byCategory[achievement.Category] = append(m.byCategory[achievement.Category], achievement)
|
||||
}
|
||||
|
||||
// Update expansion index
|
||||
if achievement.Expansion != "" {
|
||||
m.byExpansion[achievement.Expansion] = append(m.byExpansion[achievement.Expansion], achievement)
|
||||
}
|
||||
|
||||
// Invalidate metadata cache
|
||||
m.metaStale = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetAchievement retrieves an achievement by ID
|
||||
// Returns nil if not found
|
||||
// GetAchievement retrieves by ID (O(1))
|
||||
func (m *MasterList) GetAchievement(id uint32) *Achievement {
|
||||
return m.MasterList.Get(id)
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.achievements[id]
|
||||
}
|
||||
|
||||
// GetAchievementClone retrieves a cloned copy of an achievement by ID
|
||||
// Returns nil if not found. Safe for modification without affecting master list
|
||||
func (m *MasterList) GetAchievementClone(id uint32) *Achievement {
|
||||
achievement := m.MasterList.Get(id)
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
achievement := m.achievements[id]
|
||||
if achievement == nil {
|
||||
return nil
|
||||
}
|
||||
return achievement.Clone()
|
||||
}
|
||||
|
||||
// GetAllAchievements returns a map of all achievements (read-only access)
|
||||
// The returned map should not be modified
|
||||
// GetAllAchievements returns a copy of all achievements map
|
||||
func (m *MasterList) GetAllAchievements() map[uint32]*Achievement {
|
||||
return m.MasterList.GetAll()
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
result := make(map[uint32]*Achievement, len(m.achievements))
|
||||
for id, achievement := range m.achievements {
|
||||
result[id] = achievement
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAchievementsByCategory returns achievements filtered by category
|
||||
// GetAchievementsByCategory returns all achievements in a category (O(1))
|
||||
func (m *MasterList) GetAchievementsByCategory(category string) []*Achievement {
|
||||
return m.MasterList.Filter(func(achievement *Achievement) bool {
|
||||
return achievement.Category == category
|
||||
})
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.byCategory[category]
|
||||
}
|
||||
|
||||
// GetAchievementsByExpansion returns achievements filtered by expansion
|
||||
// GetAchievementsByExpansion returns all achievements in an expansion (O(1))
|
||||
func (m *MasterList) GetAchievementsByExpansion(expansion string) []*Achievement {
|
||||
return m.MasterList.Filter(func(achievement *Achievement) bool {
|
||||
return achievement.Expansion == expansion
|
||||
})
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.byExpansion[expansion]
|
||||
}
|
||||
|
||||
// Removes an achievement from the master list
|
||||
// Returns true if achievement was found and removed
|
||||
// GetAchievementsByCategoryAndExpansion returns achievements matching both category and expansion
|
||||
func (m *MasterList) GetAchievementsByCategoryAndExpansion(category, expansion string) []*Achievement {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
categoryAchievements := m.byCategory[category]
|
||||
expansionAchievements := m.byExpansion[expansion]
|
||||
|
||||
// Use smaller set for iteration efficiency
|
||||
if len(categoryAchievements) > len(expansionAchievements) {
|
||||
categoryAchievements, expansionAchievements = expansionAchievements, categoryAchievements
|
||||
}
|
||||
|
||||
// Set intersection using map lookup
|
||||
expansionSet := make(map[*Achievement]struct{}, len(expansionAchievements))
|
||||
for _, achievement := range expansionAchievements {
|
||||
expansionSet[achievement] = struct{}{}
|
||||
}
|
||||
|
||||
var result []*Achievement
|
||||
for _, achievement := range categoryAchievements {
|
||||
if _, exists := expansionSet[achievement]; exists {
|
||||
result = append(result, achievement)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetCategories returns all unique categories using cached results
|
||||
func (m *MasterList) GetCategories() []string {
|
||||
m.mutex.Lock() // Need write lock to potentially update cache
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
m.refreshMetaCache()
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
result := make([]string, len(m.categories))
|
||||
copy(result, m.categories)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetExpansions returns all unique expansions using cached results
|
||||
func (m *MasterList) GetExpansions() []string {
|
||||
m.mutex.Lock() // Need write lock to potentially update cache
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
m.refreshMetaCache()
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
result := make([]string, len(m.expansions))
|
||||
copy(result, m.expansions)
|
||||
return result
|
||||
}
|
||||
|
||||
// RemoveAchievement removes an achievement and updates all indices
|
||||
func (m *MasterList) RemoveAchievement(id uint32) bool {
|
||||
return m.MasterList.Remove(id)
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
achievement, exists := m.achievements[id]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove from core storage
|
||||
delete(m.achievements, id)
|
||||
|
||||
// Remove from category index
|
||||
if achievement.Category != "" {
|
||||
categoryAchievements := m.byCategory[achievement.Category]
|
||||
for i, a := range categoryAchievements {
|
||||
if a.AchievementID == id {
|
||||
m.byCategory[achievement.Category] = append(categoryAchievements[:i], categoryAchievements[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from expansion index
|
||||
if achievement.Expansion != "" {
|
||||
expansionAchievements := m.byExpansion[achievement.Expansion]
|
||||
for i, a := range expansionAchievements {
|
||||
if a.AchievementID == id {
|
||||
m.byExpansion[achievement.Expansion] = append(expansionAchievements[:i], expansionAchievements[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate metadata cache
|
||||
m.metaStale = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// UpdateAchievement updates an existing achievement
|
||||
// Returns error if achievement doesn't exist
|
||||
func (m *MasterList) UpdateAchievement(achievement *Achievement) error {
|
||||
if achievement == nil {
|
||||
return fmt.Errorf("achievement cannot be nil")
|
||||
}
|
||||
return m.MasterList.Update(achievement)
|
||||
}
|
||||
|
||||
// Returns all unique categories
|
||||
func (m *MasterList) GetCategories() []string {
|
||||
categories := make(map[string]bool)
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
m.MasterList.ForEach(func(_ uint32, achievement *Achievement) {
|
||||
if achievement.Category != "" {
|
||||
categories[achievement.Category] = true
|
||||
}
|
||||
})
|
||||
|
||||
result := make([]string, 0, len(categories))
|
||||
for category := range categories {
|
||||
result = append(result, category)
|
||||
// Check if exists
|
||||
old, exists := m.achievements[achievement.AchievementID]
|
||||
if !exists {
|
||||
return fmt.Errorf("achievement %d not found", achievement.AchievementID)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Returns all unique expansions
|
||||
func (m *MasterList) GetExpansions() []string {
|
||||
expansions := make(map[string]bool)
|
||||
|
||||
m.MasterList.ForEach(func(_ uint32, achievement *Achievement) {
|
||||
if achievement.Expansion != "" {
|
||||
expansions[achievement.Expansion] = true
|
||||
// Remove old achievement from indices (but not core storage yet)
|
||||
if old.Category != "" {
|
||||
categoryAchievements := m.byCategory[old.Category]
|
||||
for i, a := range categoryAchievements {
|
||||
if a.AchievementID == achievement.AchievementID {
|
||||
m.byCategory[old.Category] = append(categoryAchievements[:i], categoryAchievements[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
result := make([]string, 0, len(expansions))
|
||||
for expansion := range expansions {
|
||||
result = append(result, expansion)
|
||||
}
|
||||
return result
|
||||
|
||||
if old.Expansion != "" {
|
||||
expansionAchievements := m.byExpansion[old.Expansion]
|
||||
for i, a := range expansionAchievements {
|
||||
if a.AchievementID == achievement.AchievementID {
|
||||
m.byExpansion[old.Expansion] = append(expansionAchievements[:i], expansionAchievements[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update core storage
|
||||
m.achievements[achievement.AchievementID] = achievement
|
||||
|
||||
// Add new achievement to indices
|
||||
if achievement.Category != "" {
|
||||
m.byCategory[achievement.Category] = append(m.byCategory[achievement.Category], achievement)
|
||||
}
|
||||
|
||||
if achievement.Expansion != "" {
|
||||
m.byExpansion[achievement.Expansion] = append(m.byExpansion[achievement.Expansion], achievement)
|
||||
}
|
||||
|
||||
// Invalidate metadata cache
|
||||
m.metaStale = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size returns the total number of achievements
|
||||
func (m *MasterList) Size() int {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return len(m.achievements)
|
||||
}
|
||||
|
||||
// Clear removes all achievements from the master list
|
||||
func (m *MasterList) Clear() {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// Clear all maps
|
||||
m.achievements = make(map[uint32]*Achievement)
|
||||
m.byCategory = make(map[string][]*Achievement)
|
||||
m.byExpansion = make(map[string][]*Achievement)
|
||||
|
||||
// Clear cached metadata
|
||||
m.categories = m.categories[:0]
|
||||
m.expansions = m.expansions[:0]
|
||||
m.metaStale = true
|
||||
}
|
||||
|
||||
// ForEach executes a function for each achievement
|
||||
func (m *MasterList) ForEach(fn func(uint32, *Achievement)) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
for id, achievement := range m.achievements {
|
||||
fn(id, achievement)
|
||||
}
|
||||
}
|
||||
|
@ -1,229 +0,0 @@
|
||||
# Common Package
|
||||
|
||||
The common package provides shared utilities and patterns used across multiple EQ2Go game systems.
|
||||
|
||||
## Generic Master List
|
||||
|
||||
### Overview
|
||||
|
||||
The generic `MasterList[K, V]` type provides a thread-safe, reusable collection management pattern that eliminates code duplication across the EQ2Go codebase. It implements the master list pattern used by 15+ game systems including achievements, items, spells, factions, skills, etc.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Generic Type Safety**: Full compile-time type checking with `MasterList[KeyType, ValueType]`
|
||||
- **Thread Safety**: All operations use `sync.RWMutex` for concurrent access
|
||||
- **Consistent API**: Standardized CRUD operations across all master lists
|
||||
- **Performance Optimized**: Efficient filtering, searching, and bulk operations
|
||||
- **Extension Support**: Compose with specialized interfaces for domain-specific features
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```go
|
||||
// Any type implementing Identifiable can be stored
|
||||
type Achievement struct {
|
||||
ID uint32 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
// ... other fields
|
||||
}
|
||||
|
||||
func (a *Achievement) GetID() uint32 {
|
||||
return a.ID
|
||||
}
|
||||
|
||||
// Create a master list
|
||||
masterList := common.NewMasterList[uint32, *Achievement]()
|
||||
|
||||
// Add items
|
||||
achievement := &Achievement{ID: 1, Title: "Dragon Slayer"}
|
||||
added := masterList.Add(achievement)
|
||||
|
||||
// Retrieve items
|
||||
retrieved := masterList.Get(1)
|
||||
item, exists := masterList.GetSafe(1)
|
||||
|
||||
// Check existence
|
||||
if masterList.Exists(1) {
|
||||
// Item exists
|
||||
}
|
||||
|
||||
// Update items
|
||||
achievement.Title = "Master Dragon Slayer"
|
||||
masterList.Update(achievement) // Returns error if not found
|
||||
masterList.AddOrUpdate(achievement) // Always succeeds
|
||||
|
||||
// Remove items
|
||||
removed := masterList.Remove(1)
|
||||
|
||||
// Bulk operations
|
||||
allItems := masterList.GetAll() // Map copy
|
||||
allSlice := masterList.GetAllSlice() // Slice copy
|
||||
allIDs := masterList.GetAllIDs() // ID slice
|
||||
|
||||
// Query operations
|
||||
filtered := masterList.Filter(func(a *Achievement) bool {
|
||||
return strings.Contains(a.Title, "Dragon")
|
||||
})
|
||||
|
||||
found, exists := masterList.Find(func(a *Achievement) bool {
|
||||
return a.Title == "Dragon Slayer"
|
||||
})
|
||||
|
||||
count := masterList.Count(func(a *Achievement) bool {
|
||||
return strings.HasPrefix(a.Title, "Master")
|
||||
})
|
||||
|
||||
// Iteration
|
||||
masterList.ForEach(func(id uint32, achievement *Achievement) {
|
||||
fmt.Printf("Achievement %d: %s\n", id, achievement.Title)
|
||||
})
|
||||
```
|
||||
|
||||
### Migration from Existing Master Lists
|
||||
|
||||
#### Before (Manual Implementation)
|
||||
```go
|
||||
type MasterList struct {
|
||||
achievements map[uint32]*Achievement
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *MasterList) AddAchievement(achievement *Achievement) bool {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
if _, exists := m.achievements[achievement.ID]; exists {
|
||||
return false
|
||||
}
|
||||
|
||||
m.achievements[achievement.ID] = achievement
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *MasterList) GetAchievement(id uint32) *Achievement {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.achievements[id]
|
||||
}
|
||||
|
||||
// ... 15+ more methods with manual mutex handling
|
||||
```
|
||||
|
||||
#### After (Generic Implementation)
|
||||
```go
|
||||
type MasterList struct {
|
||||
*common.MasterList[uint32, *Achievement]
|
||||
}
|
||||
|
||||
func NewMasterList() *MasterList {
|
||||
return &MasterList{
|
||||
MasterList: common.NewMasterList[uint32, *Achievement](),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MasterList) AddAchievement(achievement *Achievement) bool {
|
||||
if achievement == nil {
|
||||
return false
|
||||
}
|
||||
return m.MasterList.Add(achievement)
|
||||
}
|
||||
|
||||
func (m *MasterList) GetAchievement(id uint32) *Achievement {
|
||||
return m.MasterList.Get(id)
|
||||
}
|
||||
|
||||
// Domain-specific extensions
|
||||
func (m *MasterList) GetAchievementsByCategory(category string) []*Achievement {
|
||||
return m.MasterList.Filter(func(achievement *Achievement) bool {
|
||||
return achievement.Category == category
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Code Reduction**: 80%+ reduction in boilerplate code per master list
|
||||
2. **Consistency**: Identical behavior across all master lists
|
||||
3. **Thread Safety**: Guaranteed concurrent access safety
|
||||
4. **Performance**: Optimized operations with minimal overhead
|
||||
5. **Type Safety**: Compile-time guarantees prevent runtime errors
|
||||
6. **Extensibility**: Easy to add domain-specific functionality
|
||||
7. **Testing**: Single well-tested implementation vs 15+ custom implementations
|
||||
8. **Maintenance**: Changes benefit all master lists simultaneously
|
||||
|
||||
### Advanced Features
|
||||
|
||||
#### Thread-Safe Batch Operations
|
||||
```go
|
||||
// Complex read operation
|
||||
masterList.WithReadLock(func(items map[uint32]*Achievement) {
|
||||
// Direct access to internal map while holding read lock
|
||||
for id, achievement := range items {
|
||||
// Complex processing...
|
||||
}
|
||||
})
|
||||
|
||||
// Complex write operation
|
||||
masterList.WithWriteLock(func(items map[uint32]*Achievement) {
|
||||
// Direct access to internal map while holding write lock
|
||||
// Atomic multi-item modifications
|
||||
})
|
||||
```
|
||||
|
||||
#### Specialized Interface Implementations
|
||||
|
||||
The package provides optional interfaces for advanced functionality:
|
||||
|
||||
- **`DatabaseIntegrated`**: Load/save from database
|
||||
- **`Validatable`**: Item validation and integrity checks
|
||||
- **`Searchable`**: Advanced search capabilities
|
||||
- **`Cacheable`**: Cache management
|
||||
- **`Statistician`**: Usage statistics tracking
|
||||
- **`Indexable`**: Multiple index support
|
||||
- **`Categorizable`**: Category-based organization
|
||||
- **`Versioned`**: Version compatibility filtering
|
||||
- **`Relationship`**: Entity relationship management
|
||||
- **`Hierarchical`**: Tree structure support
|
||||
- **`Observable`**: Event notifications
|
||||
|
||||
### Migration Steps
|
||||
|
||||
1. **Add Identifiable Interface**: Ensure your type implements `GetID() KeyType`
|
||||
2. **Embed Generic MasterList**: Replace custom struct with embedded generic
|
||||
3. **Update Constructor**: Use `common.NewMasterList[K, V]()`
|
||||
4. **Replace Manual Methods**: Use generic methods or create thin wrappers
|
||||
5. **Update Domain Methods**: Convert filters to use `Filter()`, `Find()`, etc.
|
||||
6. **Test**: Existing API should work unchanged with thin wrapper methods
|
||||
|
||||
### Performance Comparison
|
||||
|
||||
Based on benchmarks with 10,000 items:
|
||||
|
||||
| Operation | Before (Manual) | After (Generic) | Improvement |
|
||||
|-----------|-----------------|------------------|-------------|
|
||||
| Get | 15ns | 15ns | Same |
|
||||
| Add | 45ns | 45ns | Same |
|
||||
| Filter | 125μs | 120μs | 4% faster |
|
||||
| Memory | Various | Consistent | Predictable |
|
||||
|
||||
The generic implementation maintains identical performance while providing consistency and type safety.
|
||||
|
||||
### Compatibility
|
||||
|
||||
The generic master list is fully backward compatible when used with thin wrapper methods. Existing code continues to work without modification while gaining:
|
||||
|
||||
- Thread safety guarantees
|
||||
- Performance optimizations
|
||||
- Consistent behavior
|
||||
- Type safety
|
||||
- Reduced maintenance burden
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
Planned additions to the generic master list:
|
||||
|
||||
- **Persistence**: Automatic database synchronization
|
||||
- **Metrics**: Built-in performance monitoring
|
||||
- **Events**: Change notification system
|
||||
- **Indexing**: Automatic secondary index management
|
||||
- **Validation**: Built-in data integrity checks
|
||||
- **Sharding**: Horizontal scaling support
|
@ -1,210 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// testItem implements Identifiable for benchmarking
|
||||
type testItem struct {
|
||||
id int32
|
||||
name string
|
||||
value int32
|
||||
flag bool
|
||||
}
|
||||
|
||||
func (t *testItem) GetID() int32 { return t.id }
|
||||
|
||||
// BenchmarkMasterListOperations benchmarks the generic MasterList
|
||||
func BenchmarkMasterListOperations(b *testing.B) {
|
||||
// Create master list with test data
|
||||
ml := NewMasterList[int32, *testItem]()
|
||||
const numItems = 10000
|
||||
|
||||
// Pre-populate
|
||||
b.StopTimer()
|
||||
for i := 0; i < numItems; i++ {
|
||||
item := &testItem{
|
||||
id: int32(i + 1),
|
||||
name: fmt.Sprintf("Item %d", i+1),
|
||||
value: int32(rand.Intn(100)),
|
||||
flag: rand.Intn(2) == 1,
|
||||
}
|
||||
ml.Add(item)
|
||||
}
|
||||
b.StartTimer()
|
||||
|
||||
b.Run("Get", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
id := int32(rand.Intn(numItems) + 1)
|
||||
_ = ml.Get(id)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("Filter_10Percent", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ml.Filter(func(item *testItem) bool {
|
||||
return item.value < 10 // ~10% match
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Filter_50Percent", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ml.Filter(func(item *testItem) bool {
|
||||
return item.value < 50 // ~50% match
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Filter_90Percent", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ml.Filter(func(item *testItem) bool {
|
||||
return item.value < 90 // ~90% match
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Count_10Percent", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ml.Count(func(item *testItem) bool {
|
||||
return item.value < 10
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Count_50Percent", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ml.Count(func(item *testItem) bool {
|
||||
return item.value < 50
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Find", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
targetValue := int32(rand.Intn(100))
|
||||
_, _ = ml.Find(func(item *testItem) bool {
|
||||
return item.value == targetValue
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ForEach", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ml.ForEach(func(id int32, item *testItem) {
|
||||
_ = item.value + 1 // Minimal work
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("WithReadLock", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ml.WithReadLock(func(items map[int32]*testItem) {
|
||||
count := 0
|
||||
for _, item := range items {
|
||||
if item.value < 50 {
|
||||
count++
|
||||
}
|
||||
}
|
||||
_ = count
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FilterWithCapacity_Accurate", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ml.FilterWithCapacity(func(item *testItem) bool {
|
||||
return item.value < 50
|
||||
}, 5000) // Accurate estimate: 50% of 10k = 5k
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FilterInto_Reuse", func(b *testing.B) {
|
||||
var reusableSlice []*testItem
|
||||
for i := 0; i < b.N; i++ {
|
||||
reusableSlice = ml.FilterInto(func(item *testItem) bool {
|
||||
return item.value < 50
|
||||
}, reusableSlice)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("CountAndFilter_Combined", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = ml.CountAndFilter(func(item *testItem) bool {
|
||||
return item.value < 50
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkMemoryAllocations tests allocation patterns
|
||||
func BenchmarkMemoryAllocations(b *testing.B) {
|
||||
ml := NewMasterList[int32, *testItem]()
|
||||
const numItems = 1000
|
||||
|
||||
// Pre-populate
|
||||
for i := 0; i < numItems; i++ {
|
||||
item := &testItem{
|
||||
id: int32(i + 1),
|
||||
name: fmt.Sprintf("Item %d", i+1),
|
||||
value: int32(rand.Intn(100)),
|
||||
flag: rand.Intn(2) == 1,
|
||||
}
|
||||
ml.Add(item)
|
||||
}
|
||||
|
||||
b.Run("Filter_Allocations", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ml.Filter(func(item *testItem) bool {
|
||||
return item.value < 50
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetAll_Allocations", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ml.GetAll()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetAllSlice_Allocations", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ml.GetAllSlice()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FilterWithCapacity_Allocations", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ml.FilterWithCapacity(func(item *testItem) bool {
|
||||
return item.value < 50
|
||||
}, 500) // Accurate capacity estimate
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FilterInto_Allocations", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
reusableSlice := make([]*testItem, 0, 600) // Pre-sized
|
||||
for i := 0; i < b.N; i++ {
|
||||
reusableSlice = ml.FilterInto(func(item *testItem) bool {
|
||||
return item.value < 50
|
||||
}, reusableSlice)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("CountAndFilter_Allocations", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = ml.CountAndFilter(func(item *testItem) bool {
|
||||
return item.value < 50
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
@ -1,205 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// DatabaseIntegrated defines the interface for master lists that can load from database
|
||||
type DatabaseIntegrated interface {
|
||||
// LoadFromDatabase loads all items from the database
|
||||
LoadFromDatabase(db *database.Database) error
|
||||
|
||||
// SaveToDatabase saves all items to the database (if supported)
|
||||
SaveToDatabase(db *database.Database) error
|
||||
}
|
||||
|
||||
// ContextAware defines the interface for master lists that need context for initialization
|
||||
type ContextAware interface {
|
||||
// Initialize performs setup operations that may require external dependencies
|
||||
Initialize(ctx context.Context) error
|
||||
}
|
||||
|
||||
// Validatable defines the interface for master lists that support validation
|
||||
type Validatable interface {
|
||||
// Validate checks the integrity of all items in the list
|
||||
Validate() []error
|
||||
|
||||
// ValidateItem checks the integrity of a specific item
|
||||
ValidateItem(item interface{}) error
|
||||
}
|
||||
|
||||
// Searchable defines the interface for master lists that support advanced search
|
||||
type Searchable[V any] interface {
|
||||
// Search finds items matching the given criteria
|
||||
Search(criteria SearchCriteria) []V
|
||||
|
||||
// SearchByName finds items by name (case-insensitive)
|
||||
SearchByName(name string) []V
|
||||
}
|
||||
|
||||
// SearchCriteria defines search parameters for advanced search operations
|
||||
type SearchCriteria struct {
|
||||
Name string // Name-based search (case-insensitive)
|
||||
Category string // Category-based search
|
||||
Filters map[string]interface{} // Custom filters
|
||||
Limit int // Maximum results to return (0 = no limit)
|
||||
}
|
||||
|
||||
// Cacheable defines the interface for master lists that support caching
|
||||
type Cacheable interface {
|
||||
// ClearCache clears any cached data
|
||||
ClearCache()
|
||||
|
||||
// RefreshCache rebuilds cached data
|
||||
RefreshCache() error
|
||||
|
||||
// IsCacheValid returns true if cache is valid
|
||||
IsCacheValid() bool
|
||||
}
|
||||
|
||||
// Statistician defines the interface for master lists that track statistics
|
||||
type Statistician interface {
|
||||
// GetStatistics returns usage statistics for the list
|
||||
GetStatistics() Statistics
|
||||
|
||||
// ResetStatistics resets all tracked statistics
|
||||
ResetStatistics()
|
||||
}
|
||||
|
||||
// Statistics represents usage statistics for a master list
|
||||
type Statistics struct {
|
||||
TotalItems int `json:"total_items"`
|
||||
AccessCount int64 `json:"access_count"`
|
||||
HitRate float64 `json:"hit_rate"`
|
||||
MissCount int64 `json:"miss_count"`
|
||||
LastAccessed int64 `json:"last_accessed"`
|
||||
MemoryUsage int64 `json:"memory_usage"`
|
||||
}
|
||||
|
||||
// Indexable defines the interface for master lists that support multiple indexes
|
||||
type Indexable[K comparable, V any] interface {
|
||||
// GetByIndex retrieves items using an alternate index
|
||||
GetByIndex(indexName string, key interface{}) []V
|
||||
|
||||
// GetIndexes returns the names of all available indexes
|
||||
GetIndexes() []string
|
||||
|
||||
// RebuildIndex rebuilds a specific index
|
||||
RebuildIndex(indexName string) error
|
||||
|
||||
// RebuildAllIndexes rebuilds all indexes
|
||||
RebuildAllIndexes() error
|
||||
}
|
||||
|
||||
// Categorizable defines the interface for master lists that support categorization
|
||||
type Categorizable[V any] interface {
|
||||
// GetByCategory returns all items in a specific category
|
||||
GetByCategory(category string) []V
|
||||
|
||||
// GetCategories returns all available categories
|
||||
GetCategories() []string
|
||||
|
||||
// GetCategoryCount returns the number of items in a category
|
||||
GetCategoryCount(category string) int
|
||||
}
|
||||
|
||||
// Versioned defines the interface for master lists that support version filtering
|
||||
type Versioned[V any] interface {
|
||||
// GetByVersion returns items compatible with a specific version
|
||||
GetByVersion(version uint32) []V
|
||||
|
||||
// GetByVersionRange returns items compatible within a version range
|
||||
GetByVersionRange(minVersion, maxVersion uint32) []V
|
||||
}
|
||||
|
||||
// Relationship defines the interface for master lists that manage entity relationships
|
||||
type Relationship[K comparable, V any] interface {
|
||||
// GetRelated returns items related to the given item
|
||||
GetRelated(id K, relationshipType string) []V
|
||||
|
||||
// AddRelationship adds a relationship between two items
|
||||
AddRelationship(fromID K, toID K, relationshipType string) error
|
||||
|
||||
// RemoveRelationship removes a relationship between two items
|
||||
RemoveRelationship(fromID K, toID K, relationshipType string) error
|
||||
|
||||
// GetRelationshipTypes returns all supported relationship types
|
||||
GetRelationshipTypes() []string
|
||||
}
|
||||
|
||||
// Grouped defines the interface for master lists that support grouping
|
||||
type Grouped[K comparable, V any] interface {
|
||||
// GetByGroup returns all items in a specific group
|
||||
GetByGroup(groupID K) []V
|
||||
|
||||
// GetGroups returns all available groups
|
||||
GetGroups() []K
|
||||
|
||||
// GetGroupSize returns the number of items in a group
|
||||
GetGroupSize(groupID K) int
|
||||
}
|
||||
|
||||
// Hierarchical defines the interface for master lists that support tree structures
|
||||
type Hierarchical[K comparable, V any] interface {
|
||||
// GetChildren returns direct children of an item
|
||||
GetChildren(parentID K) []V
|
||||
|
||||
// GetDescendants returns all descendants of an item
|
||||
GetDescendants(parentID K) []V
|
||||
|
||||
// GetParent returns the parent of an item
|
||||
GetParent(childID K) (V, bool)
|
||||
|
||||
// GetRoot returns the root item(s)
|
||||
GetRoot() []V
|
||||
|
||||
// IsAncestor checks if one item is an ancestor of another
|
||||
IsAncestor(ancestorID K, descendantID K) bool
|
||||
}
|
||||
|
||||
// Observable defines the interface for master lists that support event notifications
|
||||
type Observable[K comparable, V any] interface {
|
||||
// Subscribe adds a listener for list events
|
||||
Subscribe(listener EventListener[K, V])
|
||||
|
||||
// Unsubscribe removes a listener
|
||||
Unsubscribe(listener EventListener[K, V])
|
||||
|
||||
// NotifyEvent sends an event to all listeners
|
||||
NotifyEvent(event Event[K, V])
|
||||
}
|
||||
|
||||
// EventListener receives notifications about list changes
|
||||
type EventListener[K comparable, V any] interface {
|
||||
// OnItemAdded is called when an item is added
|
||||
OnItemAdded(id K, item V)
|
||||
|
||||
// OnItemRemoved is called when an item is removed
|
||||
OnItemRemoved(id K, item V)
|
||||
|
||||
// OnItemUpdated is called when an item is updated
|
||||
OnItemUpdated(id K, oldItem V, newItem V)
|
||||
|
||||
// OnListCleared is called when the list is cleared
|
||||
OnListCleared()
|
||||
}
|
||||
|
||||
// Event represents a change event in a master list
|
||||
type Event[K comparable, V any] struct {
|
||||
Type EventType `json:"type"`
|
||||
ItemID K `json:"item_id"`
|
||||
Item V `json:"item,omitempty"`
|
||||
OldItem V `json:"old_item,omitempty"`
|
||||
}
|
||||
|
||||
// EventType represents the type of event that occurred
|
||||
type EventType int
|
||||
|
||||
const (
|
||||
EventItemAdded EventType = iota
|
||||
EventItemRemoved
|
||||
EventItemUpdated
|
||||
EventListCleared
|
||||
)
|
@ -1,311 +0,0 @@
|
||||
// Package common provides shared utilities and patterns used across multiple game systems.
|
||||
//
|
||||
// The MasterList type provides a generic, thread-safe collection management pattern
|
||||
// that is used extensively throughout the EQ2Go server implementation for managing
|
||||
// game entities like items, spells, spawns, achievements, etc.
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Identifiable represents any type that can be identified by a key
|
||||
type Identifiable[K comparable] interface {
|
||||
GetID() K
|
||||
}
|
||||
|
||||
// MasterList provides a generic, thread-safe collection for managing game entities.
|
||||
// It implements the common pattern used across all EQ2Go master lists with consistent
|
||||
// CRUD operations, bulk operations, and thread safety.
|
||||
//
|
||||
// K is the key type (typically int32, uint32, or string)
|
||||
// V is the value type (must implement Identifiable[K])
|
||||
type MasterList[K comparable, V Identifiable[K]] struct {
|
||||
items map[K]V
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMasterList creates a new master list instance
|
||||
func NewMasterList[K comparable, V Identifiable[K]]() *MasterList[K, V] {
|
||||
return &MasterList[K, V]{
|
||||
items: make(map[K]V),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds an item to the master list. Returns true if added, false if it already exists.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) Add(item V) bool {
|
||||
ml.mutex.Lock()
|
||||
defer ml.mutex.Unlock()
|
||||
|
||||
id := item.GetID()
|
||||
if _, exists := ml.items[id]; exists {
|
||||
return false
|
||||
}
|
||||
|
||||
ml.items[id] = item
|
||||
return true
|
||||
}
|
||||
|
||||
// AddOrUpdate adds an item to the master list or updates it if it already exists.
|
||||
// Always returns true. Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) AddOrUpdate(item V) bool {
|
||||
ml.mutex.Lock()
|
||||
defer ml.mutex.Unlock()
|
||||
|
||||
id := item.GetID()
|
||||
ml.items[id] = item
|
||||
return true
|
||||
}
|
||||
|
||||
// Get retrieves an item by its ID. Returns the zero value of V if not found.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) Get(id K) V {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
return ml.items[id]
|
||||
}
|
||||
|
||||
// GetSafe retrieves an item by its ID with existence check.
|
||||
// Returns the item and true if found, zero value and false if not found.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) GetSafe(id K) (V, bool) {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
item, exists := ml.items[id]
|
||||
return item, exists
|
||||
}
|
||||
|
||||
// Exists checks if an item with the given ID exists in the list.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) Exists(id K) bool {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
_, exists := ml.items[id]
|
||||
return exists
|
||||
}
|
||||
|
||||
// Remove removes an item by its ID. Returns true if removed, false if not found.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) Remove(id K) bool {
|
||||
ml.mutex.Lock()
|
||||
defer ml.mutex.Unlock()
|
||||
|
||||
if _, exists := ml.items[id]; !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
delete(ml.items, id)
|
||||
return true
|
||||
}
|
||||
|
||||
// Update updates an existing item. Returns error if the item doesn't exist.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) Update(item V) error {
|
||||
ml.mutex.Lock()
|
||||
defer ml.mutex.Unlock()
|
||||
|
||||
id := item.GetID()
|
||||
if _, exists := ml.items[id]; !exists {
|
||||
return fmt.Errorf("item with ID %v not found", id)
|
||||
}
|
||||
|
||||
ml.items[id] = item
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size returns the number of items in the list.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) Size() int {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
return len(ml.items)
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the list contains no items.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) IsEmpty() bool {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
return len(ml.items) == 0
|
||||
}
|
||||
|
||||
// Clear removes all items from the list.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) Clear() {
|
||||
ml.mutex.Lock()
|
||||
defer ml.mutex.Unlock()
|
||||
|
||||
// Create new map to ensure memory is freed
|
||||
ml.items = make(map[K]V)
|
||||
}
|
||||
|
||||
// GetAll returns a copy of all items in the list.
|
||||
// The returned map is safe to modify without affecting the master list.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) GetAll() map[K]V {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
result := make(map[K]V, len(ml.items))
|
||||
maps.Copy(result, ml.items)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAllSlice returns a slice containing all items in the list.
|
||||
// The returned slice is safe to modify without affecting the master list.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) GetAllSlice() []V {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
result := make([]V, 0, len(ml.items))
|
||||
for _, v := range ml.items {
|
||||
result = append(result, v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAllIDs returns a slice containing all IDs in the list.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) GetAllIDs() []K {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
result := make([]K, 0, len(ml.items))
|
||||
for k := range ml.items {
|
||||
result = append(result, k)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ForEach executes a function for each item in the list.
|
||||
// The function receives a copy of each item, so modifications won't affect the list.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) ForEach(fn func(K, V)) {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
for k, v := range ml.items {
|
||||
fn(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter returns a new slice containing items that match the predicate function.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) Filter(predicate func(V) bool) []V {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
// Pre-allocate with estimated capacity to reduce allocations
|
||||
result := make([]V, 0, len(ml.items)/4) // Assume ~25% match rate
|
||||
for _, v := range ml.items {
|
||||
if predicate(v) {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Find returns the first item that matches the predicate function.
|
||||
// Returns zero value and false if no match is found.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) Find(predicate func(V) bool) (V, bool) {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
for _, v := range ml.items {
|
||||
if predicate(v) {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
|
||||
var zero V
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// Count returns the number of items that match the predicate function.
|
||||
// Thread-safe for concurrent access.
|
||||
func (ml *MasterList[K, V]) Count(predicate func(V) bool) int {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
count := 0
|
||||
for _, v := range ml.items {
|
||||
if predicate(v) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// WithReadLock executes a function while holding a read lock on the list.
|
||||
// Use this for complex operations that need consistent read access to multiple items.
|
||||
func (ml *MasterList[K, V]) WithReadLock(fn func(map[K]V)) {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
fn(ml.items)
|
||||
}
|
||||
|
||||
// WithWriteLock executes a function while holding a write lock on the list.
|
||||
// Use this for complex operations that need to modify multiple items atomically.
|
||||
func (ml *MasterList[K, V]) WithWriteLock(fn func(map[K]V)) {
|
||||
ml.mutex.Lock()
|
||||
defer ml.mutex.Unlock()
|
||||
fn(ml.items)
|
||||
}
|
||||
|
||||
// FilterWithCapacity returns items matching predicate with pre-allocated capacity.
|
||||
// Use when you have a good estimate of result size to optimize allocations.
|
||||
func (ml *MasterList[K, V]) FilterWithCapacity(predicate func(V) bool, expectedSize int) []V {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
result := make([]V, 0, expectedSize)
|
||||
for _, v := range ml.items {
|
||||
if predicate(v) {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterInto appends matching items to the provided slice, avoiding new allocations.
|
||||
// Returns the updated slice. Use this for repeated filtering to reuse memory.
|
||||
func (ml *MasterList[K, V]) FilterInto(predicate func(V) bool, result []V) []V {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
// Clear the slice but keep capacity
|
||||
result = result[:0]
|
||||
for _, v := range ml.items {
|
||||
if predicate(v) {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CountAndFilter performs both count and filter in a single pass.
|
||||
// More efficient than calling Count() and Filter() separately.
|
||||
func (ml *MasterList[K, V]) CountAndFilter(predicate func(V) bool) (int, []V) {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
count := 0
|
||||
result := make([]V, 0, len(ml.items)/4)
|
||||
for _, v := range ml.items {
|
||||
if predicate(v) {
|
||||
count++
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return count, result
|
||||
}
|
@ -1,305 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestItem implements Identifiable for testing
|
||||
type TestItem struct {
|
||||
ID int32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
func (t *TestItem) GetID() int32 {
|
||||
return t.ID
|
||||
}
|
||||
|
||||
// TestMasterList tests the basic functionality of the generic master list
|
||||
func TestMasterList(t *testing.T) {
|
||||
ml := NewMasterList[int32, *TestItem]()
|
||||
|
||||
// Test initial state
|
||||
if !ml.IsEmpty() {
|
||||
t.Error("New master list should be empty")
|
||||
}
|
||||
|
||||
if ml.Size() != 0 {
|
||||
t.Error("New master list should have size 0")
|
||||
}
|
||||
|
||||
// Test adding items
|
||||
item1 := &TestItem{ID: 1, Name: "Item One", Category: "A"}
|
||||
item2 := &TestItem{ID: 2, Name: "Item Two", Category: "B"}
|
||||
item3 := &TestItem{ID: 3, Name: "Item Three", Category: "A"}
|
||||
|
||||
if !ml.Add(item1) {
|
||||
t.Error("Should successfully add item1")
|
||||
}
|
||||
|
||||
if !ml.Add(item2) {
|
||||
t.Error("Should successfully add item2")
|
||||
}
|
||||
|
||||
if !ml.Add(item3) {
|
||||
t.Error("Should successfully add item3")
|
||||
}
|
||||
|
||||
// Test duplicate addition
|
||||
if ml.Add(item1) {
|
||||
t.Error("Should not add duplicate item")
|
||||
}
|
||||
|
||||
// Test size
|
||||
if ml.Size() != 3 {
|
||||
t.Errorf("Expected size 3, got %d", ml.Size())
|
||||
}
|
||||
|
||||
if ml.IsEmpty() {
|
||||
t.Error("List should not be empty")
|
||||
}
|
||||
|
||||
// Test retrieval
|
||||
retrieved := ml.Get(1)
|
||||
if retrieved == nil || retrieved.Name != "Item One" {
|
||||
t.Error("Failed to retrieve item1")
|
||||
}
|
||||
|
||||
// Test safe retrieval
|
||||
retrievedSafe, exists := ml.GetSafe(1)
|
||||
if !exists || retrievedSafe.Name != "Item One" {
|
||||
t.Error("Failed to safely retrieve item1")
|
||||
}
|
||||
|
||||
_, exists = ml.GetSafe(999)
|
||||
if exists {
|
||||
t.Error("Should not find non-existent item")
|
||||
}
|
||||
|
||||
// Test existence
|
||||
if !ml.Exists(1) {
|
||||
t.Error("Item 1 should exist")
|
||||
}
|
||||
|
||||
if ml.Exists(999) {
|
||||
t.Error("Item 999 should not exist")
|
||||
}
|
||||
|
||||
// Test update
|
||||
updatedItem := &TestItem{ID: 1, Name: "Updated Item One", Category: "A"}
|
||||
if err := ml.Update(updatedItem); err != nil {
|
||||
t.Errorf("Should successfully update item: %v", err)
|
||||
}
|
||||
|
||||
retrieved = ml.Get(1)
|
||||
if retrieved.Name != "Updated Item One" {
|
||||
t.Error("Item was not updated correctly")
|
||||
}
|
||||
|
||||
// Test update non-existent item
|
||||
nonExistent := &TestItem{ID: 999, Name: "Non Existent", Category: "Z"}
|
||||
if err := ml.Update(nonExistent); err == nil {
|
||||
t.Error("Should fail to update non-existent item")
|
||||
}
|
||||
|
||||
// Test AddOrUpdate
|
||||
newItem := &TestItem{ID: 4, Name: "Item Four", Category: "C"}
|
||||
if !ml.AddOrUpdate(newItem) {
|
||||
t.Error("Should successfully add new item with AddOrUpdate")
|
||||
}
|
||||
|
||||
updateExisting := &TestItem{ID: 1, Name: "Double Updated Item One", Category: "A"}
|
||||
if !ml.AddOrUpdate(updateExisting) {
|
||||
t.Error("Should successfully update existing item with AddOrUpdate")
|
||||
}
|
||||
|
||||
retrieved = ml.Get(1)
|
||||
if retrieved.Name != "Double Updated Item One" {
|
||||
t.Error("Item was not updated correctly with AddOrUpdate")
|
||||
}
|
||||
|
||||
if ml.Size() != 4 {
|
||||
t.Errorf("Expected size 4 after AddOrUpdate, got %d", ml.Size())
|
||||
}
|
||||
|
||||
// Test removal
|
||||
if !ml.Remove(2) {
|
||||
t.Error("Should successfully remove item2")
|
||||
}
|
||||
|
||||
if ml.Remove(2) {
|
||||
t.Error("Should not remove already removed item")
|
||||
}
|
||||
|
||||
if ml.Size() != 3 {
|
||||
t.Errorf("Expected size 3 after removal, got %d", ml.Size())
|
||||
}
|
||||
|
||||
// Test GetAll
|
||||
all := ml.GetAll()
|
||||
if len(all) != 3 {
|
||||
t.Errorf("Expected 3 items in GetAll, got %d", len(all))
|
||||
}
|
||||
|
||||
// Verify we can modify the returned map without affecting the original
|
||||
all[999] = &TestItem{ID: 999, Name: "Should not affect original", Category: "Z"}
|
||||
if ml.Exists(999) {
|
||||
t.Error("Modifying returned map should not affect original list")
|
||||
}
|
||||
|
||||
// Test GetAllSlice
|
||||
slice := ml.GetAllSlice()
|
||||
if len(slice) != 3 {
|
||||
t.Errorf("Expected 3 items in GetAllSlice, got %d", len(slice))
|
||||
}
|
||||
|
||||
// Test GetAllIDs
|
||||
ids := ml.GetAllIDs()
|
||||
if len(ids) != 3 {
|
||||
t.Errorf("Expected 3 IDs in GetAllIDs, got %d", len(ids))
|
||||
}
|
||||
|
||||
// Test Clear
|
||||
ml.Clear()
|
||||
if !ml.IsEmpty() {
|
||||
t.Error("List should be empty after Clear")
|
||||
}
|
||||
|
||||
if ml.Size() != 0 {
|
||||
t.Error("List should have size 0 after Clear")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMasterListSearch tests search functionality
|
||||
func TestMasterListSearch(t *testing.T) {
|
||||
ml := NewMasterList[int32, *TestItem]()
|
||||
|
||||
// Add test items
|
||||
items := []*TestItem{
|
||||
{ID: 1, Name: "Alpha", Category: "A"},
|
||||
{ID: 2, Name: "Beta", Category: "B"},
|
||||
{ID: 3, Name: "Gamma", Category: "A"},
|
||||
{ID: 4, Name: "Delta", Category: "C"},
|
||||
{ID: 5, Name: "Alpha Two", Category: "A"},
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
ml.Add(item)
|
||||
}
|
||||
|
||||
// Test Filter
|
||||
categoryA := ml.Filter(func(item *TestItem) bool {
|
||||
return item.Category == "A"
|
||||
})
|
||||
|
||||
if len(categoryA) != 3 {
|
||||
t.Errorf("Expected 3 items in category A, got %d", len(categoryA))
|
||||
}
|
||||
|
||||
// Test Find
|
||||
found, exists := ml.Find(func(item *TestItem) bool {
|
||||
return item.Name == "Beta"
|
||||
})
|
||||
|
||||
if !exists || found.ID != 2 {
|
||||
t.Error("Should find Beta with ID 2")
|
||||
}
|
||||
|
||||
notFound, exists := ml.Find(func(item *TestItem) bool {
|
||||
return item.Name == "Nonexistent"
|
||||
})
|
||||
|
||||
if exists || notFound != nil {
|
||||
t.Error("Should not find nonexistent item")
|
||||
}
|
||||
|
||||
// Test Count
|
||||
count := ml.Count(func(item *TestItem) bool {
|
||||
return item.Category == "A"
|
||||
})
|
||||
|
||||
if count != 3 {
|
||||
t.Errorf("Expected count of 3 for category A, got %d", count)
|
||||
}
|
||||
|
||||
// Test ForEach
|
||||
var visitedIDs []int32
|
||||
ml.ForEach(func(id int32, item *TestItem) {
|
||||
visitedIDs = append(visitedIDs, id)
|
||||
})
|
||||
|
||||
if len(visitedIDs) != 5 {
|
||||
t.Errorf("Expected to visit 5 items, visited %d", len(visitedIDs))
|
||||
}
|
||||
}
|
||||
|
||||
// TestMasterListConcurrency tests thread safety (basic test)
|
||||
func TestMasterListConcurrency(t *testing.T) {
|
||||
ml := NewMasterList[int32, *TestItem]()
|
||||
|
||||
// Test WithReadLock
|
||||
ml.Add(&TestItem{ID: 1, Name: "Test", Category: "A"})
|
||||
|
||||
var foundItem *TestItem
|
||||
ml.WithReadLock(func(items map[int32]*TestItem) {
|
||||
foundItem = items[1]
|
||||
})
|
||||
|
||||
if foundItem == nil || foundItem.Name != "Test" {
|
||||
t.Error("WithReadLock should provide access to internal map")
|
||||
}
|
||||
|
||||
// Test WithWriteLock
|
||||
ml.WithWriteLock(func(items map[int32]*TestItem) {
|
||||
items[2] = &TestItem{ID: 2, Name: "Added via WriteLock", Category: "B"}
|
||||
})
|
||||
|
||||
if !ml.Exists(2) {
|
||||
t.Error("Item added via WithWriteLock should exist")
|
||||
}
|
||||
|
||||
retrieved := ml.Get(2)
|
||||
if retrieved.Name != "Added via WriteLock" {
|
||||
t.Error("Item added via WithWriteLock not found correctly")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// BenchmarkMasterList tests performance of basic operations
|
||||
func BenchmarkMasterList(b *testing.B) {
|
||||
ml := NewMasterList[int32, *TestItem]()
|
||||
|
||||
// Pre-populate for benchmarks
|
||||
for i := int32(0); i < 1000; i++ {
|
||||
ml.Add(&TestItem{
|
||||
ID: i,
|
||||
Name: fmt.Sprintf("Item %d", i),
|
||||
Category: fmt.Sprintf("Category %d", i%10),
|
||||
})
|
||||
}
|
||||
|
||||
b.Run("Get", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ml.Get(int32(i % 1000))
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Add", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ml.AddOrUpdate(&TestItem{
|
||||
ID: int32(1000 + i),
|
||||
Name: fmt.Sprintf("Bench Item %d", i),
|
||||
Category: "Bench",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Filter", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ml.Filter(func(item *TestItem) bool {
|
||||
return item.Category == "Category 5"
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user