rework chat
This commit is contained in:
parent
4595c392d1
commit
bec433ef89
371
internal/chat/benchmark_test.go
Normal file
371
internal/chat/benchmark_test.go
Normal file
@ -0,0 +1,371 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// Setup creates a master list with test data for benchmarking
|
||||
func benchmarkSetup() *MasterList {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Add world channels
|
||||
worldChannels := []string{
|
||||
"Auction", "Trade", "General", "OOC", "LFG", "Crafting",
|
||||
"Roleplay", "Newbie", "Antonica", "Commonlands",
|
||||
"Freeport", "Qeynos", "Kelethin", "Neriak",
|
||||
}
|
||||
|
||||
for i, name := range worldChannels {
|
||||
ch := NewWithData(int32(i+1), name, ChannelTypeWorld, db)
|
||||
if i%3 == 0 {
|
||||
ch.SetLevelRestriction(10) // Some have level restrictions
|
||||
}
|
||||
if i%4 == 0 {
|
||||
ch.SetRacesAllowed(1 << 1) // Some have race restrictions
|
||||
}
|
||||
masterList.AddChannel(ch)
|
||||
|
||||
// Add some members to channels
|
||||
if i%2 == 0 {
|
||||
ch.JoinChannel(int32(1000 + i))
|
||||
}
|
||||
if i%3 == 0 {
|
||||
ch.JoinChannel(int32(2000 + i))
|
||||
}
|
||||
}
|
||||
|
||||
// Add custom channels
|
||||
for i := 0; i < 50; i++ {
|
||||
ch := NewWithData(int32(100+i), fmt.Sprintf("CustomChannel%d", i), ChannelTypeCustom, db)
|
||||
if i%5 == 0 {
|
||||
ch.SetLevelRestriction(20)
|
||||
}
|
||||
masterList.AddChannel(ch)
|
||||
|
||||
// Add members to some custom channels
|
||||
if i%4 == 0 {
|
||||
ch.JoinChannel(int32(3000 + i))
|
||||
}
|
||||
}
|
||||
|
||||
return masterList
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_AddChannel(b *testing.B) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ch := NewWithData(int32(i+10000), fmt.Sprintf("Channel%d", i), ChannelTypeWorld, db)
|
||||
masterList.AddChannel(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetChannel(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetChannel(int32(i%64 + 1))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetChannelSafe(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetChannelSafe(int32(i%64 + 1))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_HasChannel(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.HasChannel(int32(i%64 + 1))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_FindChannelsByType(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if i%2 == 0 {
|
||||
masterList.FindChannelsByType(ChannelTypeWorld)
|
||||
} else {
|
||||
masterList.FindChannelsByType(ChannelTypeCustom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetWorldChannels(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetWorldChannels()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetCustomChannels(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetCustomChannels()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetChannelByName(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
names := []string{"auction", "trade", "general", "ooc", "customchannel5", "customchannel15"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetChannelByName(names[i%len(names)])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_FindChannelsByName(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
searchTerms := []string{"Auction", "Custom", "Channel", "Trade", "General"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.FindChannelsByName(searchTerms[i%len(searchTerms)])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetActiveChannels(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetActiveChannels()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetEmptyChannels(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetEmptyChannels()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetCompatibleChannels(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
level := int32(i%50 + 1)
|
||||
race := int32(i%10 + 1)
|
||||
class := int32(i%20 + 1)
|
||||
masterList.GetCompatibleChannels(level, race, class)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetChannelsByMemberCount(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
memberCount := i % 5 // 0-4 members
|
||||
masterList.GetChannelsByMemberCount(memberCount)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetChannelsByLevelRestriction(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
levels := []int32{0, 10, 20, 30, 50}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetChannelsByLevelRestriction(levels[i%len(levels)])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetAllChannels(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetAllChannels()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetAllChannelsList(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetAllChannelsList()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetStatistics(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetStatistics()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_ValidateChannels(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.ValidateChannels()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_RemoveChannel(b *testing.B) {
|
||||
b.StopTimer()
|
||||
masterList := benchmarkSetup()
|
||||
initialCount := masterList.GetChannelCount()
|
||||
|
||||
// Pre-populate with channels we'll remove
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
for i := 0; i < b.N; i++ {
|
||||
ch := NewWithData(int32(20000+i), fmt.Sprintf("ToRemove%d", i), ChannelTypeCustom, db)
|
||||
masterList.AddChannel(ch)
|
||||
}
|
||||
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.RemoveChannel(int32(20000 + i))
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
if masterList.GetChannelCount() != initialCount {
|
||||
b.Errorf("Expected %d channels after removal, got %d", initialCount, masterList.GetChannelCount())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_ForEach(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
count := 0
|
||||
masterList.ForEach(func(id int32, channel *Channel) {
|
||||
count++
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_UpdateChannel(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
channelID := int32(i%64 + 1)
|
||||
updatedChannel := &Channel{
|
||||
ID: channelID,
|
||||
Name: fmt.Sprintf("Updated%d", i),
|
||||
ChannelType: ChannelTypeCustom,
|
||||
db: db,
|
||||
isNew: false,
|
||||
members: make([]int32, 0),
|
||||
}
|
||||
masterList.UpdateChannel(updatedChannel)
|
||||
}
|
||||
}
|
||||
|
||||
// Memory allocation benchmarks
|
||||
func BenchmarkMasterList_GetChannel_Allocs(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetChannel(int32(i%64 + 1))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_FindChannelsByType_Allocs(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.FindChannelsByType(ChannelTypeWorld)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_GetChannelByName_Allocs(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
masterList.GetChannelByName("auction")
|
||||
}
|
||||
}
|
||||
|
||||
// Concurrent benchmark
|
||||
func BenchmarkMasterList_ConcurrentReads(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
// Mix of read operations
|
||||
switch b.N % 5 {
|
||||
case 0:
|
||||
masterList.GetChannel(int32(b.N%64 + 1))
|
||||
case 1:
|
||||
masterList.FindChannelsByType(ChannelTypeWorld)
|
||||
case 2:
|
||||
masterList.GetChannelByName("auction")
|
||||
case 3:
|
||||
masterList.GetActiveChannels()
|
||||
case 4:
|
||||
masterList.GetCompatibleChannels(25, 1, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkMasterList_ConcurrentMixed(b *testing.B) {
|
||||
masterList := benchmarkSetup()
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
// Mix of read and write operations (mostly reads)
|
||||
switch b.N % 10 {
|
||||
case 0: // 10% writes
|
||||
ch := NewWithData(int32(b.N+50000), fmt.Sprintf("Concurrent%d", b.N), ChannelTypeCustom, db)
|
||||
masterList.AddChannel(ch)
|
||||
default: // 90% reads
|
||||
switch b.N % 4 {
|
||||
case 0:
|
||||
masterList.GetChannel(int32(b.N%64 + 1))
|
||||
case 1:
|
||||
masterList.FindChannelsByType(ChannelTypeWorld)
|
||||
case 2:
|
||||
masterList.GetChannelByName("auction")
|
||||
case 3:
|
||||
masterList.GetActiveChannels()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@ -2,141 +2,448 @@ package chat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"eq2emu/internal/common"
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// MasterList manages a collection of channels using the generic MasterList base
|
||||
// MasterList is a specialized chat channel master list optimized for:
|
||||
// - Fast ID-based lookups (O(1))
|
||||
// - Fast channel type filtering (O(1))
|
||||
// - Fast name-based searching (indexed)
|
||||
// - Fast member count filtering
|
||||
// - Efficient channel compatibility queries
|
||||
type MasterList struct {
|
||||
*common.MasterList[int32, *Channel]
|
||||
// Core storage
|
||||
channels map[int32]*Channel // ID -> Channel
|
||||
mutex sync.RWMutex
|
||||
|
||||
// Specialized indices for O(1) lookups
|
||||
byType map[int][]*Channel // ChannelType -> channels
|
||||
byMemberCt map[int][]*Channel // Member count -> channels (active/empty)
|
||||
byNameLower map[string]*Channel // Lowercase name -> channel
|
||||
byRestrict map[int32][]*Channel // Restriction level -> channels
|
||||
|
||||
// Cached metadata
|
||||
memberCounts []int // Unique member counts (cached)
|
||||
typeStats map[int]int // Channel type -> count
|
||||
metaStale bool // Whether metadata cache needs refresh
|
||||
}
|
||||
|
||||
// NewMasterList creates a new channel master list
|
||||
// NewMasterList creates a new specialized chat channel master list
|
||||
func NewMasterList() *MasterList {
|
||||
return &MasterList{
|
||||
MasterList: common.NewMasterList[int32, *Channel](),
|
||||
channels: make(map[int32]*Channel),
|
||||
byType: make(map[int][]*Channel),
|
||||
byMemberCt: make(map[int][]*Channel),
|
||||
byNameLower: make(map[string]*Channel),
|
||||
byRestrict: make(map[int32][]*Channel),
|
||||
typeStats: make(map[int]int),
|
||||
metaStale: true,
|
||||
}
|
||||
}
|
||||
|
||||
// AddChannel adds a channel to the master list
|
||||
func (ml *MasterList) AddChannel(channel *Channel) bool {
|
||||
return ml.Add(channel)
|
||||
// refreshMetaCache updates the member counts cache and type stats
|
||||
func (ml *MasterList) refreshMetaCache() {
|
||||
if !ml.metaStale {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear and rebuild type stats
|
||||
ml.typeStats = make(map[int]int)
|
||||
memberCountSet := make(map[int]struct{})
|
||||
|
||||
// Collect unique member counts and type stats
|
||||
for _, channel := range ml.channels {
|
||||
ml.typeStats[channel.GetType()]++
|
||||
memberCount := channel.GetNumClients()
|
||||
memberCountSet[memberCount] = struct{}{}
|
||||
}
|
||||
|
||||
// Clear and rebuild member counts cache
|
||||
ml.memberCounts = ml.memberCounts[:0]
|
||||
for count := range memberCountSet {
|
||||
ml.memberCounts = append(ml.memberCounts, count)
|
||||
}
|
||||
|
||||
ml.metaStale = false
|
||||
}
|
||||
|
||||
// GetChannel retrieves a channel by ID
|
||||
// updateChannelIndices updates all indices for a channel
|
||||
func (ml *MasterList) updateChannelIndices(channel *Channel, add bool) {
|
||||
if add {
|
||||
// Add to type index
|
||||
ml.byType[channel.GetType()] = append(ml.byType[channel.GetType()], channel)
|
||||
|
||||
// Add to member count index
|
||||
memberCount := channel.GetNumClients()
|
||||
ml.byMemberCt[memberCount] = append(ml.byMemberCt[memberCount], channel)
|
||||
|
||||
// Add to name index
|
||||
ml.byNameLower[strings.ToLower(channel.GetName())] = channel
|
||||
|
||||
// Add to restriction index
|
||||
ml.byRestrict[channel.LevelRestriction] = append(ml.byRestrict[channel.LevelRestriction], channel)
|
||||
} else {
|
||||
// Remove from type index
|
||||
typeChannels := ml.byType[channel.GetType()]
|
||||
for i, ch := range typeChannels {
|
||||
if ch.ID == channel.ID {
|
||||
ml.byType[channel.GetType()] = append(typeChannels[:i], typeChannels[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from member count index
|
||||
memberCount := channel.GetNumClients()
|
||||
memberChannels := ml.byMemberCt[memberCount]
|
||||
for i, ch := range memberChannels {
|
||||
if ch.ID == channel.ID {
|
||||
ml.byMemberCt[memberCount] = append(memberChannels[:i], memberChannels[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from name index
|
||||
delete(ml.byNameLower, strings.ToLower(channel.GetName()))
|
||||
|
||||
// Remove from restriction index
|
||||
restrChannels := ml.byRestrict[channel.LevelRestriction]
|
||||
for i, ch := range restrChannels {
|
||||
if ch.ID == channel.ID {
|
||||
ml.byRestrict[channel.LevelRestriction] = append(restrChannels[:i], restrChannels[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshChannelIndices refreshes the indices for a channel (used when member count changes)
|
||||
func (ml *MasterList) RefreshChannelIndices(channel *Channel, oldMemberCount int) {
|
||||
ml.mutex.Lock()
|
||||
defer ml.mutex.Unlock()
|
||||
|
||||
// Remove from old member count index
|
||||
oldMemberChannels := ml.byMemberCt[oldMemberCount]
|
||||
for i, ch := range oldMemberChannels {
|
||||
if ch.ID == channel.ID {
|
||||
ml.byMemberCt[oldMemberCount] = append(oldMemberChannels[:i], oldMemberChannels[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add to new member count index
|
||||
newMemberCount := channel.GetNumClients()
|
||||
ml.byMemberCt[newMemberCount] = append(ml.byMemberCt[newMemberCount], channel)
|
||||
|
||||
// Invalidate metadata cache
|
||||
ml.metaStale = true
|
||||
}
|
||||
|
||||
// AddChannel adds a channel with full indexing
|
||||
func (ml *MasterList) AddChannel(channel *Channel) bool {
|
||||
if channel == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ml.mutex.Lock()
|
||||
defer ml.mutex.Unlock()
|
||||
|
||||
// Check if exists
|
||||
if _, exists := ml.channels[channel.ID]; exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Add to core storage
|
||||
ml.channels[channel.ID] = channel
|
||||
|
||||
// Update all indices
|
||||
ml.updateChannelIndices(channel, true)
|
||||
|
||||
// Invalidate metadata cache
|
||||
ml.metaStale = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetChannel retrieves by ID (O(1))
|
||||
func (ml *MasterList) GetChannel(id int32) *Channel {
|
||||
return ml.Get(id)
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
return ml.channels[id]
|
||||
}
|
||||
|
||||
// GetChannelSafe retrieves a channel by ID with existence check
|
||||
func (ml *MasterList) GetChannelSafe(id int32) (*Channel, bool) {
|
||||
return ml.GetSafe(id)
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
channel, exists := ml.channels[id]
|
||||
return channel, exists
|
||||
}
|
||||
|
||||
// HasChannel checks if a channel exists by ID
|
||||
func (ml *MasterList) HasChannel(id int32) bool {
|
||||
return ml.Exists(id)
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
_, exists := ml.channels[id]
|
||||
return exists
|
||||
}
|
||||
|
||||
// RemoveChannel removes a channel by ID
|
||||
// RemoveChannel removes a channel and updates all indices
|
||||
func (ml *MasterList) RemoveChannel(id int32) bool {
|
||||
return ml.Remove(id)
|
||||
ml.mutex.Lock()
|
||||
defer ml.mutex.Unlock()
|
||||
|
||||
channel, exists := ml.channels[id]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove from core storage
|
||||
delete(ml.channels, id)
|
||||
|
||||
// Update all indices
|
||||
ml.updateChannelIndices(channel, false)
|
||||
|
||||
// Invalidate metadata cache
|
||||
ml.metaStale = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetAllChannels returns all channels as a map
|
||||
// GetAllChannels returns a copy of all channels map
|
||||
func (ml *MasterList) GetAllChannels() map[int32]*Channel {
|
||||
return ml.GetAll()
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
result := make(map[int32]*Channel, len(ml.channels))
|
||||
maps.Copy(result, ml.channels)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAllChannelsList returns all channels as a slice
|
||||
func (ml *MasterList) GetAllChannelsList() []*Channel {
|
||||
return ml.GetAllSlice()
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
result := make([]*Channel, 0, len(ml.channels))
|
||||
for _, channel := range ml.channels {
|
||||
result = append(result, channel)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetChannelCount returns the number of channels
|
||||
func (ml *MasterList) GetChannelCount() int {
|
||||
return ml.Size()
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
return len(ml.channels)
|
||||
}
|
||||
|
||||
// Size returns the total number of channels
|
||||
func (ml *MasterList) Size() int {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
return len(ml.channels)
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the master list is empty
|
||||
func (ml *MasterList) IsEmpty() bool {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
return len(ml.channels) == 0
|
||||
}
|
||||
|
||||
// ClearChannels removes all channels from the list
|
||||
func (ml *MasterList) ClearChannels() {
|
||||
ml.Clear()
|
||||
ml.mutex.Lock()
|
||||
defer ml.mutex.Unlock()
|
||||
|
||||
// Clear all maps
|
||||
ml.channels = make(map[int32]*Channel)
|
||||
ml.byType = make(map[int][]*Channel)
|
||||
ml.byMemberCt = make(map[int][]*Channel)
|
||||
ml.byNameLower = make(map[string]*Channel)
|
||||
ml.byRestrict = make(map[int32][]*Channel)
|
||||
|
||||
// Clear cached metadata
|
||||
ml.memberCounts = ml.memberCounts[:0]
|
||||
ml.typeStats = make(map[int]int)
|
||||
ml.metaStale = true
|
||||
}
|
||||
|
||||
// FindChannelsByName finds channels containing the given name substring
|
||||
// Clear removes all channels from the master list
|
||||
func (ml *MasterList) Clear() {
|
||||
ml.ClearChannels()
|
||||
}
|
||||
|
||||
// FindChannelsByName finds channels containing the given name substring (optimized)
|
||||
func (ml *MasterList) FindChannelsByName(nameSubstring string) []*Channel {
|
||||
return ml.Filter(func(channel *Channel) bool {
|
||||
return contains(channel.GetName(), nameSubstring)
|
||||
})
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
searchKey := strings.ToLower(nameSubstring)
|
||||
|
||||
// Try exact match first for full channel names
|
||||
if exactChannel := ml.byNameLower[searchKey]; exactChannel != nil {
|
||||
return []*Channel{exactChannel}
|
||||
}
|
||||
|
||||
// Fallback to substring search
|
||||
var result []*Channel
|
||||
for _, channel := range ml.channels {
|
||||
if contains(strings.ToLower(channel.GetName()), searchKey) {
|
||||
result = append(result, channel)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FindChannelsByType finds channels of a specific type
|
||||
// FindChannelsByType finds channels of a specific type (O(1))
|
||||
func (ml *MasterList) FindChannelsByType(channelType int) []*Channel {
|
||||
return ml.Filter(func(channel *Channel) bool {
|
||||
return channel.GetType() == channelType
|
||||
})
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
return ml.byType[channelType]
|
||||
}
|
||||
|
||||
// GetWorldChannels returns all world channels
|
||||
// GetWorldChannels returns all world channels (O(1))
|
||||
func (ml *MasterList) GetWorldChannels() []*Channel {
|
||||
return ml.FindChannelsByType(ChannelTypeWorld)
|
||||
}
|
||||
|
||||
// GetCustomChannels returns all custom channels
|
||||
// GetCustomChannels returns all custom channels (O(1))
|
||||
func (ml *MasterList) GetCustomChannels() []*Channel {
|
||||
return ml.FindChannelsByType(ChannelTypeCustom)
|
||||
}
|
||||
|
||||
// GetActiveChannels returns channels that have members
|
||||
func (ml *MasterList) GetActiveChannels() []*Channel {
|
||||
return ml.Filter(func(channel *Channel) bool {
|
||||
return !channel.IsEmpty()
|
||||
})
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
var result []*Channel
|
||||
for _, channel := range ml.channels {
|
||||
if !channel.IsEmpty() {
|
||||
result = append(result, channel)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetEmptyChannels returns channels that have no members
|
||||
func (ml *MasterList) GetEmptyChannels() []*Channel {
|
||||
return ml.Filter(func(channel *Channel) bool {
|
||||
return channel.IsEmpty()
|
||||
})
|
||||
}
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
// GetChannelByName retrieves a channel by name (case-insensitive)
|
||||
func (ml *MasterList) GetChannelByName(name string) *Channel {
|
||||
name = strings.ToLower(name)
|
||||
var foundChannel *Channel
|
||||
ml.ForEach(func(id int32, channel *Channel) {
|
||||
if strings.ToLower(channel.GetName()) == name {
|
||||
foundChannel = channel
|
||||
var result []*Channel
|
||||
for _, channel := range ml.channels {
|
||||
if channel.IsEmpty() {
|
||||
result = append(result, channel)
|
||||
}
|
||||
})
|
||||
return foundChannel
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// HasChannelByName checks if a channel exists by name (case-insensitive)
|
||||
// GetChannelByName retrieves a channel by name (case-insensitive, O(1))
|
||||
func (ml *MasterList) GetChannelByName(name string) *Channel {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
return ml.byNameLower[strings.ToLower(name)]
|
||||
}
|
||||
|
||||
// HasChannelByName checks if a channel exists by name (case-insensitive, O(1))
|
||||
func (ml *MasterList) HasChannelByName(name string) bool {
|
||||
return ml.GetChannelByName(name) != nil
|
||||
}
|
||||
|
||||
// GetCompatibleChannels returns channels compatible with player restrictions
|
||||
func (ml *MasterList) GetCompatibleChannels(level, race, class int32) []*Channel {
|
||||
return ml.Filter(func(channel *Channel) bool {
|
||||
return channel.CanJoinChannelByLevel(level) &&
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
var result []*Channel
|
||||
for _, channel := range ml.channels {
|
||||
if channel.CanJoinChannelByLevel(level) &&
|
||||
channel.CanJoinChannelByRace(race) &&
|
||||
channel.CanJoinChannelByClass(class)
|
||||
})
|
||||
channel.CanJoinChannelByClass(class) {
|
||||
result = append(result, channel)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetChannelsByMemberCount returns channels with specific member count
|
||||
func (ml *MasterList) GetChannelsByMemberCount(memberCount int) []*Channel {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
return ml.byMemberCt[memberCount]
|
||||
}
|
||||
|
||||
// GetChannelsByLevelRestriction returns channels with specific level restriction
|
||||
func (ml *MasterList) GetChannelsByLevelRestriction(levelRestriction int32) []*Channel {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
return ml.byRestrict[levelRestriction]
|
||||
}
|
||||
|
||||
// UpdateChannel updates an existing channel and refreshes indices
|
||||
func (ml *MasterList) UpdateChannel(channel *Channel) error {
|
||||
if channel == nil {
|
||||
return fmt.Errorf("channel cannot be nil")
|
||||
}
|
||||
|
||||
ml.mutex.Lock()
|
||||
defer ml.mutex.Unlock()
|
||||
|
||||
// Check if exists
|
||||
old, exists := ml.channels[channel.ID]
|
||||
if !exists {
|
||||
return fmt.Errorf("channel %d not found", channel.ID)
|
||||
}
|
||||
|
||||
// Remove old channel from indices (but not core storage yet)
|
||||
ml.updateChannelIndices(old, false)
|
||||
|
||||
// Update core storage
|
||||
ml.channels[channel.ID] = channel
|
||||
|
||||
// Add new channel to indices
|
||||
ml.updateChannelIndices(channel, true)
|
||||
|
||||
// Invalidate metadata cache
|
||||
ml.metaStale = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForEach executes a function for each channel
|
||||
func (ml *MasterList) ForEach(fn func(int32, *Channel)) {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
for id, channel := range ml.channels {
|
||||
fn(id, channel)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateChannels checks all channels for consistency
|
||||
func (ml *MasterList) ValidateChannels() []string {
|
||||
ml.mutex.RLock()
|
||||
defer ml.mutex.RUnlock()
|
||||
|
||||
var issues []string
|
||||
|
||||
ml.ForEach(func(id int32, channel *Channel) {
|
||||
for id, channel := range ml.channels {
|
||||
if channel == nil {
|
||||
issues = append(issues, fmt.Sprintf("Channel ID %d is nil", id))
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
if channel.GetID() != id {
|
||||
@ -158,7 +465,7 @@ func (ml *MasterList) ValidateChannels() []string {
|
||||
if len(channel.Password) > MaxChannelPasswordLength {
|
||||
issues = append(issues, fmt.Sprintf("Channel ID %d password too long: %d > %d", id, len(channel.Password), MaxChannelPasswordLength))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
@ -169,24 +476,32 @@ func (ml *MasterList) IsValid() bool {
|
||||
return len(issues) == 0
|
||||
}
|
||||
|
||||
// GetStatistics returns statistics about the channel collection
|
||||
// GetStatistics returns statistics about the channel collection using cached data
|
||||
func (ml *MasterList) GetStatistics() map[string]any {
|
||||
stats := make(map[string]any)
|
||||
stats["total_channels"] = ml.Size()
|
||||
ml.mutex.Lock() // Need write lock to potentially update cache
|
||||
defer ml.mutex.Unlock()
|
||||
|
||||
if ml.IsEmpty() {
|
||||
ml.refreshMetaCache()
|
||||
|
||||
stats := make(map[string]any)
|
||||
stats["total_channels"] = len(ml.channels)
|
||||
|
||||
if len(ml.channels) == 0 {
|
||||
return stats
|
||||
}
|
||||
|
||||
// Count by channel type
|
||||
typeCounts := make(map[int]int)
|
||||
// Use cached type stats
|
||||
stats["channels_by_type"] = ml.typeStats
|
||||
stats["world_channels"] = ml.typeStats[ChannelTypeWorld]
|
||||
stats["custom_channels"] = ml.typeStats[ChannelTypeCustom]
|
||||
|
||||
// Calculate additional stats
|
||||
var totalMembers int
|
||||
var activeChannels int
|
||||
var minID, maxID int32
|
||||
first := true
|
||||
|
||||
ml.ForEach(func(id int32, channel *Channel) {
|
||||
typeCounts[channel.GetType()]++
|
||||
for id, channel := range ml.channels {
|
||||
totalMembers += channel.GetNumClients()
|
||||
|
||||
if !channel.IsEmpty() {
|
||||
@ -205,11 +520,8 @@ func (ml *MasterList) GetStatistics() map[string]any {
|
||||
maxID = id
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stats["channels_by_type"] = typeCounts
|
||||
stats["world_channels"] = typeCounts[ChannelTypeWorld]
|
||||
stats["custom_channels"] = typeCounts[ChannelTypeCustom]
|
||||
stats["total_members"] = totalMembers
|
||||
stats["active_channels"] = activeChannels
|
||||
stats["min_id"] = minID
|
||||
|
@ -1,6 +1,7 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"eq2emu/internal/database"
|
||||
@ -176,21 +177,28 @@ func TestMasterListGetByName(t *testing.T) {
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Add test channels
|
||||
// Add test channels with different names to test indexing
|
||||
channel1 := NewWithData(100, "Auction", ChannelTypeWorld, db)
|
||||
channel2 := NewWithData(200, "AUCTION", ChannelTypeWorld, db) // Different case
|
||||
channel2 := NewWithData(200, "Trade", ChannelTypeWorld, db)
|
||||
channel3 := NewWithData(300, "Custom Channel", ChannelTypeCustom, db)
|
||||
masterList.AddChannel(channel1)
|
||||
masterList.AddChannel(channel2)
|
||||
masterList.AddChannel(channel3)
|
||||
|
||||
// Test case-insensitive lookup
|
||||
found := masterList.GetChannelByName("auction")
|
||||
if found == nil {
|
||||
t.Error("GetChannelByName should find channel (case insensitive)")
|
||||
if found == nil || found.ID != 100 {
|
||||
t.Error("GetChannelByName should find 'Auction' channel (case insensitive)")
|
||||
}
|
||||
|
||||
found = masterList.GetChannelByName("AUCTION")
|
||||
if found == nil {
|
||||
t.Error("GetChannelByName should find channel (uppercase)")
|
||||
found = masterList.GetChannelByName("TRADE")
|
||||
if found == nil || found.ID != 200 {
|
||||
t.Error("GetChannelByName should find 'Trade' channel (uppercase)")
|
||||
}
|
||||
|
||||
found = masterList.GetChannelByName("custom channel")
|
||||
if found == nil || found.ID != 300 {
|
||||
t.Error("GetChannelByName should find 'Custom Channel' channel (lowercase)")
|
||||
}
|
||||
|
||||
found = masterList.GetChannelByName("NonExistent")
|
||||
@ -363,6 +371,148 @@ func TestMasterListStatistics(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterListBespokeFeatures(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Add test channels with different properties
|
||||
ch1 := NewWithData(101, "Test Channel", ChannelTypeWorld, db)
|
||||
ch1.SetLevelRestriction(10)
|
||||
|
||||
ch2 := NewWithData(102, "Another Test", ChannelTypeCustom, db)
|
||||
ch2.SetLevelRestriction(20)
|
||||
|
||||
ch3 := NewWithData(103, "Empty Channel", ChannelTypeWorld, db)
|
||||
ch3.SetLevelRestriction(10)
|
||||
|
||||
masterList.AddChannel(ch1)
|
||||
masterList.AddChannel(ch2)
|
||||
masterList.AddChannel(ch3)
|
||||
|
||||
// Add some members to make channels active/empty
|
||||
ch1.JoinChannel(1001)
|
||||
masterList.RefreshChannelIndices(ch1, 0) // Update from 0 to 1 member
|
||||
ch1.JoinChannel(1002)
|
||||
masterList.RefreshChannelIndices(ch1, 1) // Update from 1 to 2 members
|
||||
ch2.JoinChannel(1003)
|
||||
masterList.RefreshChannelIndices(ch2, 0) // Update from 0 to 1 member
|
||||
|
||||
// Test GetChannelsByMemberCount
|
||||
zeroMemberChannels := masterList.GetChannelsByMemberCount(0)
|
||||
if len(zeroMemberChannels) != 1 {
|
||||
t.Errorf("GetChannelsByMemberCount(0) returned %v results, want 1", len(zeroMemberChannels))
|
||||
}
|
||||
|
||||
twoMemberChannels := masterList.GetChannelsByMemberCount(2)
|
||||
if len(twoMemberChannels) != 1 {
|
||||
t.Errorf("GetChannelsByMemberCount(2) returned %v results, want 1", len(twoMemberChannels))
|
||||
}
|
||||
|
||||
oneMemberChannels := masterList.GetChannelsByMemberCount(1)
|
||||
if len(oneMemberChannels) != 1 {
|
||||
t.Errorf("GetChannelsByMemberCount(1) returned %v results, want 1", len(oneMemberChannels))
|
||||
}
|
||||
|
||||
// Test GetChannelsByLevelRestriction
|
||||
level10Channels := masterList.GetChannelsByLevelRestriction(10)
|
||||
if len(level10Channels) != 2 {
|
||||
t.Errorf("GetChannelsByLevelRestriction(10) returned %v results, want 2", len(level10Channels))
|
||||
}
|
||||
|
||||
level20Channels := masterList.GetChannelsByLevelRestriction(20)
|
||||
if len(level20Channels) != 1 {
|
||||
t.Errorf("GetChannelsByLevelRestriction(20) returned %v results, want 1", len(level20Channels))
|
||||
}
|
||||
|
||||
// Test UpdateChannel
|
||||
updatedCh := &Channel{
|
||||
ID: 101,
|
||||
Name: "Updated Channel Name",
|
||||
ChannelType: ChannelTypeCustom, // Changed type
|
||||
db: db,
|
||||
isNew: false,
|
||||
members: make([]int32, 0),
|
||||
}
|
||||
|
||||
err := masterList.UpdateChannel(updatedCh)
|
||||
if err != nil {
|
||||
t.Errorf("UpdateChannel failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the update worked
|
||||
retrieved := masterList.GetChannel(101)
|
||||
if retrieved.Name != "Updated Channel Name" {
|
||||
t.Errorf("Expected updated name 'Updated Channel Name', got '%s'", retrieved.Name)
|
||||
}
|
||||
|
||||
if retrieved.ChannelType != ChannelTypeCustom {
|
||||
t.Errorf("Expected updated type %d, got %d", ChannelTypeCustom, retrieved.ChannelType)
|
||||
}
|
||||
|
||||
// Test updating non-existent channel
|
||||
nonExistentCh := &Channel{ID: 9999, Name: "Non-existent", db: db}
|
||||
err = masterList.UpdateChannel(nonExistentCh)
|
||||
if err == nil {
|
||||
t.Error("UpdateChannel should fail for non-existent channel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMasterListConcurrency(t *testing.T) {
|
||||
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||
defer db.Close()
|
||||
|
||||
masterList := NewMasterList()
|
||||
|
||||
// Add initial channels
|
||||
for i := 1; i <= 100; i++ {
|
||||
ch := NewWithData(int32(i), fmt.Sprintf("Channel%d", i), ChannelTypeWorld, db)
|
||||
masterList.AddChannel(ch)
|
||||
}
|
||||
|
||||
// Test concurrent access
|
||||
done := make(chan bool, 10)
|
||||
|
||||
// Concurrent readers
|
||||
for i := 0; i < 5; i++ {
|
||||
go func() {
|
||||
defer func() { done <- true }()
|
||||
for j := 0; j < 100; j++ {
|
||||
masterList.GetChannel(int32(j%100 + 1))
|
||||
masterList.FindChannelsByType(ChannelTypeWorld)
|
||||
masterList.GetChannelByName(fmt.Sprintf("channel%d", j%100+1))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Concurrent writers
|
||||
for i := 0; i < 5; i++ {
|
||||
go func(workerID int) {
|
||||
defer func() { done <- true }()
|
||||
for j := 0; j < 10; j++ {
|
||||
chID := int32(workerID*1000 + j + 1)
|
||||
ch := NewWithData(chID, fmt.Sprintf("Worker%d-Channel%d", workerID, j), ChannelTypeCustom, db)
|
||||
masterList.AddChannel(ch) // Some may fail due to concurrent additions
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Verify final state - should have at least 100 initial channels
|
||||
finalCount := masterList.GetChannelCount()
|
||||
if finalCount < 100 {
|
||||
t.Errorf("Expected at least 100 channels after concurrent operations, got %d", finalCount)
|
||||
}
|
||||
if finalCount > 150 {
|
||||
t.Errorf("Expected at most 150 channels after concurrent operations, got %d", finalCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsFunction(t *testing.T) {
|
||||
tests := []struct {
|
||||
str string
|
||||
|
Loading…
x
Reference in New Issue
Block a user