fix(db): eliminate O(n²) iteration in generateBotProfiles
The generateBotProfiles function had two nested loops that caused O(n²) memory usage: - Iterating through all rating history entries (10,000) for each bot (10,000) = 100M iterations - Iterating through all matches (1,000) for each bot (10,000) = 10M iterations This caused acb-index-builder to run out of memory and get OOMKilled during the build cycle. Fixed by pre-building lookup maps (O(n) build + O(1) lookup): - historyMap[botID] -> []RatingHistoryEntry - matchMap[botID] -> []MatchSummary Reduces complexity from O(bots × matches) to O(matches + bots) for lookups. Resolves acb-index-builder CrashLoopBackOff after 45 days of failure.
This commit is contained in:
parent
be7588434d
commit
7befe516bf
1 changed files with 30 additions and 23 deletions
|
|
@ -272,37 +272,44 @@ func generateBotDirectory(data *IndexData, outputDir string) error {
|
||||||
func generateBotProfiles(data *IndexData, outputDir string, cfg *Config) error {
|
func generateBotProfiles(data *IndexData, outputDir string, cfg *Config) error {
|
||||||
botsDir := filepath.Join(outputDir, "data", "bots")
|
botsDir := filepath.Join(outputDir, "data", "bots")
|
||||||
|
|
||||||
|
// Pre-build lookup maps to avoid O(n²) iteration
|
||||||
|
// botID -> []RatingHistoryEntry (O(n) build + O(1) lookup vs O(n²) linear scan)
|
||||||
|
historyMap := make(map[string][]RatingHistoryEntry, len(data.Bots))
|
||||||
|
for _, h := range data.RatingHistory {
|
||||||
|
historyMap[h.BotID] = append(historyMap[h.BotID], h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// botID -> []MatchSummary for recent matches (O(n) build + O(1) lookup)
|
||||||
|
// We store up to 20 matches per bot, pre-computed to avoid per-bot match iteration
|
||||||
|
matchMap := make(map[string][]MatchSummary, len(data.Bots))
|
||||||
|
for _, m := range data.Matches {
|
||||||
|
// Track which bots participated in this match
|
||||||
|
for _, p := range m.Participants {
|
||||||
|
// Skip if this bot already has 20 recent matches
|
||||||
|
if len(matchMap[p.BotID]) >= 20 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
summary := matchToSummary(m, data, cfg)
|
||||||
|
matchMap[p.BotID] = append(matchMap[p.BotID], summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, bot := range data.Bots {
|
for _, bot := range data.Bots {
|
||||||
winRate := 0.0
|
winRate := 0.0
|
||||||
if bot.MatchesPlayed > 0 {
|
if bot.MatchesPlayed > 0 {
|
||||||
winRate = float64(bot.MatchesWon) / float64(bot.MatchesPlayed) * 100
|
winRate = float64(bot.MatchesWon) / float64(bot.MatchesPlayed) * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get rating history for this bot
|
// O(1) map lookup instead of O(n) linear scan
|
||||||
history := make([]RatingHistoryEntry, 0)
|
history := historyMap[bot.ID]
|
||||||
for _, h := range data.RatingHistory {
|
if len(history) == 0 {
|
||||||
if h.BotID == bot.ID {
|
history = []RatingHistoryEntry{} // Ensure non-nil slice for JSON
|
||||||
history = append(history, h)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get recent matches for this bot (last 20)
|
// O(1) map lookup instead of O(n) linear scan through all matches
|
||||||
recentMatches := make([]MatchSummary, 0)
|
recentMatches := matchMap[bot.ID]
|
||||||
for _, m := range data.Matches {
|
if len(recentMatches) == 0 {
|
||||||
participated := false
|
recentMatches = []MatchSummary{} // Ensure non-nil slice for JSON
|
||||||
for _, p := range m.Participants {
|
|
||||||
if p.BotID == bot.ID {
|
|
||||||
participated = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if participated {
|
|
||||||
summary := matchToSummary(m, data, cfg)
|
|
||||||
recentMatches = append(recentMatches, summary)
|
|
||||||
if len(recentMatches) >= 20 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
profile := BotProfile{
|
profile := BotProfile{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue