1887 lines
57 KiB
Markdown
1887 lines
57 KiB
Markdown
# TrinityCore Playerbot Performance Profiling Report
|
||||
|
|
**Target:** 5000 Concurrent Bots with <10% Server Impact
|
|||
|
|
**Date:** 2025-10-03
|
|||
|
|
**Platform:** Windows (Visual Studio 2022, MSBuild)
|
|||
|
|
**Analysis Type:** Static Code Analysis + Theoretical Performance Modeling
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Executive Summary
|
|||
|
|
|
|||
|
|
### Current Estimated Capacity
|
|||
|
|
Based on static code analysis, the current architecture can support **approximately 500-800 concurrent bots** before reaching critical performance thresholds. To achieve 5000 concurrent bots, **significant optimizations are required** across multiple subsystems.
|
|||
|
|
|
|||
|
|
### Bottleneck Identification (Priority Ranked)
|
|||
|
|
1. **CRITICAL - BotSession Update Loop**: Excessive mutex contention, packet processing overhead
|
|||
|
|
2. **CRITICAL - Database Query Patterns**: Synchronous queries blocking AI updates
|
|||
|
|
3. **HIGH - Memory Allocations**: Frequent heap allocations in hot paths (UpdateAI, strategies)
|
|||
|
|
4. **HIGH - Scheduler Queue Management**: Priority queue with mutex every frame
|
|||
|
|
5. **MEDIUM - ClassAI Combat Rotations**: Complex spell checking without caching
|
|||
|
|
6. **MEDIUM - Movement Calculations**: Per-frame pathfinding recalculation
|
|||
|
|
|
|||
|
|
### Risk Assessment for 5000 Bot Target
|
|||
|
|
- **Memory**: ⚠️ **HIGH RISK** - Estimated 80-100GB total memory usage (exceeds target of 50GB)
|
|||
|
|
- **CPU**: ⚠️ **HIGH RISK** - Estimated 700-900% CPU usage (exceeds target of 500%)
|
|||
|
|
- **Database**: 🔴 **CRITICAL RISK** - Synchronous queries will cause deadlocks at scale
|
|||
|
|
- **Thread Contention**: ⚠️ **HIGH RISK** - Mutex contention will increase exponentially
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 1: Static Analysis Results
|
|||
|
|
|
|||
|
|
### 1.1 Memory Analysis
|
|||
|
|
|
|||
|
|
#### Per-Bot Memory Breakdown (Theoretical Calculation)
|
|||
|
|
|
|||
|
|
| Component | Size (bytes) | Notes |
|
|||
|
|
|-----------|--------------|-------|
|
|||
|
|
| **WorldSession** | ~2048 | Base TrinityCore session overhead |
|
|||
|
|
| **Player Object** | ~8192 | Full player data structure |
|
|||
|
|
| **BotSession** | ~512 | Bot-specific session data |
|
|||
|
|
| **BotAI** | ~256 | Base AI state machine |
|
|||
|
|
| **ClassAI** (derived) | ~384 | Class-specific AI data |
|
|||
|
|
| **Strategies (3 active)** | ~768 | Follow, combat, idle strategies |
|
|||
|
|
| **Action Queue** | ~512 | Queued actions (avg 10 actions) |
|
|||
|
|
| **Trigger System** | ~256 | Registered triggers |
|
|||
|
|
| **Movement Data** | ~384 | MotionMaster state |
|
|||
|
|
| **Combat State** | ~256 | Target, cooldowns, resources |
|
|||
|
|
| **Group Data** | ~128 | Group membership info |
|
|||
|
|
| **Packet Queues** | ~2048 | Incoming/outgoing packet buffers |
|
|||
|
|
| **Performance Metrics** | ~256 | Monitoring overhead |
|
|||
|
|
| **Misc Overhead** | ~1024 | String allocations, caching |
|
|||
|
|
| **TOTAL PER BOT** | **~17,024 bytes** | **~16.6 MB per bot** |
|
|||
|
|
|
|||
|
|
#### Scaling Calculations
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1 bot = 17 MB
|
|||
|
|
10 bots = 170 MB
|
|||
|
|
100 bots = 1.7 GB
|
|||
|
|
1000 bots = 17 GB
|
|||
|
|
5000 bots = 85 GB ⚠️ EXCEEDS 50GB TARGET
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Memory Leak Risk Areas (Code Analysis):**
|
|||
|
|
- `BotSession.cpp:499-500` - WorldPacket copying in SendPacket (every outgoing packet creates heap allocation)
|
|||
|
|
- `BotSession.cpp:511-512` - WorldPacket copying in QueuePacket (every incoming packet creates heap allocation)
|
|||
|
|
- `BotScheduler.cpp:394` - Priority queue insertions (no object pooling)
|
|||
|
|
- `BotAI.cpp:196-227` - Strategy update creates temporary objects per frame
|
|||
|
|
- `LeaderFollowBehavior.cpp` - Movement vector allocations every frame (9 matches with `new`/`make_unique`)
|
|||
|
|
|
|||
|
|
**RAII Violations Found:**
|
|||
|
|
- `BotAI.cpp:46-52` - Raw pointer assignment for `_groupInvitationHandler`, but uses `std::unique_ptr` (✓ SAFE)
|
|||
|
|
- `BotSession.cpp:421-428` - Manual AI deletion in destructor with try-catch (acceptable for cleanup)
|
|||
|
|
- `BotScheduler.cpp:71-75` - Manual queue clearing (acceptable for shutdown)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 1.2 CPU Analysis
|
|||
|
|
|
|||
|
|
#### Hotspot Identification (Static Analysis)
|
|||
|
|
|
|||
|
|
**File: `BotSession.cpp` - BotSession::Update()**
|
|||
|
|
```cpp
|
|||
|
|
// Line 515-676: Main update loop
|
|||
|
|
// CRITICAL HOTSPOT: Runs for EVERY bot EVERY frame
|
|||
|
|
bool BotSession::Update(uint32 diff, PacketFilter& updater)
|
|||
|
|
{
|
|||
|
|
// ⚠️ ISSUE 1: Multiple validation checks per frame (lines 522-544)
|
|||
|
|
// - 7 atomic load operations
|
|||
|
|
// - 2 function calls (GetAccountId twice)
|
|||
|
|
// - Memory corruption detection (pointer validation)
|
|||
|
|
// COST: ~50-100 CPU cycles per bot per frame
|
|||
|
|
|
|||
|
|
// ⚠️ ISSUE 2: Recursive call detection with thread_local (lines 547-558)
|
|||
|
|
// COST: ~20 CPU cycles per bot per frame
|
|||
|
|
|
|||
|
|
// Line 563: ProcessPendingLogin() - async login check
|
|||
|
|
// Line 567: ProcessQueryCallbacks() - database callback processing
|
|||
|
|
// Line 570: ProcessBotPackets() - packet processing with mutex
|
|||
|
|
|
|||
|
|
// ⚠️ ISSUE 3: AI Update with extensive validation (lines 574-660)
|
|||
|
|
// - Pointer corruption detection (lines 580-597)
|
|||
|
|
// - Exception handling overhead
|
|||
|
|
// - Multiple nested try-catch blocks
|
|||
|
|
// COST: ~200-500 CPU cycles per bot per frame
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Performance Metrics:**
|
|||
|
|
- **Lines of code in critical path:** 161 lines (includes extensive safety checks)
|
|||
|
|
- **Atomic operations per update:** 7-10 loads/stores
|
|||
|
|
- **Mutex acquisitions per update:** 0-2 (packet processing, query callbacks)
|
|||
|
|
- **Function call depth:** 4-5 levels deep
|
|||
|
|
- **Estimated CPU cycles per bot per update:** 500-1000 cycles
|
|||
|
|
- **At 5000 bots × 10 updates/sec:** **50-100 million cycles/sec** (~2-4% CPU on modern CPU)
|
|||
|
|
|
|||
|
|
**File: `BotAI.cpp` - BotAI::UpdateAI()**
|
|||
|
|
```cpp
|
|||
|
|
// Line 83-190: Main AI update method
|
|||
|
|
void BotAI::UpdateAI(uint32 diff)
|
|||
|
|
{
|
|||
|
|
// Line 91-94: Performance tracking with high_resolution_clock
|
|||
|
|
// COST: ~100 CPU cycles per bot
|
|||
|
|
|
|||
|
|
// ⚠️ HOTSPOT: Line 196-227: UpdateStrategies() with shared_lock
|
|||
|
|
// - Acquires read lock on _mutex
|
|||
|
|
// - Iterates all active strategies (typically 3-5)
|
|||
|
|
// - Dynamic cast check for follow strategy (line 212)
|
|||
|
|
// COST: ~200-400 CPU cycles per bot
|
|||
|
|
|
|||
|
|
// Line 310-332: ProcessTriggers()
|
|||
|
|
// - Evaluates ALL triggers (typically 10-20)
|
|||
|
|
// - Pushes results to priority queue
|
|||
|
|
// COST: ~500-1000 CPU cycles per bot (trigger-dependent)
|
|||
|
|
|
|||
|
|
// Line 338-389: UpdateActions()
|
|||
|
|
// - Checks current action validity
|
|||
|
|
// - Processes triggered actions (priority queue operations)
|
|||
|
|
// - Processes queued actions
|
|||
|
|
// COST: ~100-300 CPU cycles per bot
|
|||
|
|
|
|||
|
|
// Line 177-189: Performance metrics update
|
|||
|
|
// - Chrono calculations
|
|||
|
|
// - Atomic operations
|
|||
|
|
// COST: ~100 CPU cycles per bot
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Total Estimated CPU per Bot per Update:**
|
|||
|
|
- BotSession::Update(): 500-1000 cycles
|
|||
|
|
- BotAI::UpdateAI(): 900-1700 cycles
|
|||
|
|
- ClassAI::OnCombatUpdate() (when in combat): 500-2000 cycles (class-dependent)
|
|||
|
|
- Movement updates: 200-500 cycles
|
|||
|
|
- **TOTAL: 2100-5200 cycles per bot per update**
|
|||
|
|
|
|||
|
|
**Scaling Calculations (Assuming 3.5 GHz CPU, 10 updates/sec):**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1 bot: 2100-5200 cycles × 10 updates/sec = 21,000-52,000 cycles/sec = 0.006-0.015% CPU
|
|||
|
|
10 bots: 0.06-0.15% CPU
|
|||
|
|
100 bots: 0.6-1.5% CPU
|
|||
|
|
1000 bots: 6-15% CPU ✓ ACCEPTABLE
|
|||
|
|
5000 bots: 30-75% CPU ⚠️ EXCEEDS 5% TARGET (500% = 100% of 5 cores)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**CRITICAL FINDING:** Current architecture will consume **30-75% CPU for 5000 bots**, significantly exceeding the 5% target per-core usage.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 1.3 Database Analysis
|
|||
|
|
|
|||
|
|
#### Query Pattern Analysis
|
|||
|
|
|
|||
|
|
**Grep Results:** 23 occurrences of `CharacterDatabase.Query` or `WorldDatabase.Query`
|
|||
|
|
|
|||
|
|
**Critical Files:**
|
|||
|
|
1. `BotSession.cpp` (Lines 43-324): **BotLoginQueryHolder** - 75+ prepared statements
|
|||
|
|
2. `PlayerbotCharacterDBInterface.cpp`: 3 synchronous queries
|
|||
|
|
3. `BotWorldEntry.cpp`: 2 synchronous queries
|
|||
|
|
4. `BotCharacterCreator.cpp`: 2 synchronous queries
|
|||
|
|
5. `BotSessionEnhanced.cpp`: 8 synchronous queries
|
|||
|
|
|
|||
|
|
**BotLoginQueryHolder Analysis (BotSession.cpp:43-324):**
|
|||
|
|
```cpp
|
|||
|
|
bool BotSession::BotLoginQueryHolder::Initialize()
|
|||
|
|
{
|
|||
|
|
// Sets up 75+ prepared statements for character login
|
|||
|
|
// CRITICAL ISSUE: All queries are batched but still blocking
|
|||
|
|
// - CHAR_SEL_CHARACTER
|
|||
|
|
// - CHAR_SEL_CHARACTER_CUSTOMIZATIONS
|
|||
|
|
// - CHAR_SEL_CHARACTER_AURAS (+ EFFECTS + STORED_LOCATIONS)
|
|||
|
|
// - CHAR_SEL_CHARACTER_SPELL (+ FAVORITES)
|
|||
|
|
// - CHAR_SEL_CHARACTER_QUESTSTATUS (+ 5 sub-queries)
|
|||
|
|
// - CHAR_SEL_CHARACTER_REPUTATION
|
|||
|
|
// - CHAR_SEL_CHARACTER_INVENTORY
|
|||
|
|
// - ... 60+ more queries
|
|||
|
|
|
|||
|
|
// ⚠️ PERFORMANCE IMPACT:
|
|||
|
|
// - Login time: ~50-200ms per bot (database latency dependent)
|
|||
|
|
// - Async pattern used (DelayQueryHolder) ✓ GOOD
|
|||
|
|
// - But 5000 bots logging in simultaneously = 250-1000 seconds total
|
|||
|
|
// - Connection pool exhaustion risk
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Database Connection Pool Analysis:**
|
|||
|
|
- `BotDatabasePool.cpp`: Custom connection pooling implementation (8 matches with `new`)
|
|||
|
|
- No evidence of connection pooling limits in code
|
|||
|
|
- **Risk:** 5000 bots × 75 queries = 375,000 queries during mass login
|
|||
|
|
- **Estimated time:** 250-1000 seconds for 5000 bot login (unacceptable)
|
|||
|
|
|
|||
|
|
**Synchronous Query Hotspots:**
|
|||
|
|
- `BotCharacterCreator.cpp:2` - Character creation queries (blocking)
|
|||
|
|
- `BotWorldEntry.cpp:2` - World entry validation (blocking)
|
|||
|
|
- `PlayerbotCharacterDBInterface.cpp:3` - Character data queries (blocking)
|
|||
|
|
|
|||
|
|
**Optimization Opportunities:**
|
|||
|
|
1. Implement aggressive query result caching (spell data, item stats)
|
|||
|
|
2. Batch character loading (load 10-50 bots per query holder)
|
|||
|
|
3. Pre-populate static data at server startup
|
|||
|
|
4. Use in-memory cache for frequently accessed data (DBC/DB2 already cached)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 1.4 Threading and Lock Contention Analysis
|
|||
|
|
|
|||
|
|
**Grep Results:** 725 occurrences of mutex/lock operations across 79 files
|
|||
|
|
|
|||
|
|
**Critical Lock Hotspots:**
|
|||
|
|
|
|||
|
|
**1. BotSession Packet Processing (BotSession.cpp:677-763)**
|
|||
|
|
```cpp
|
|||
|
|
void BotSession::ProcessBotPackets()
|
|||
|
|
{
|
|||
|
|
// Line 699: Atomic compare-exchange for processing flag (✓ GOOD - lock-free)
|
|||
|
|
// Line 720-725: Mutex acquisition with 5ms timeout
|
|||
|
|
std::unique_lock<std::recursive_timed_mutex> lock(_packetMutex, std::defer_lock);
|
|||
|
|
if (!lock.try_lock_for(std::chrono::milliseconds(5)))
|
|||
|
|
{
|
|||
|
|
return; // Defer processing
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ⚠️ ISSUE: Recursive timed mutex is expensive
|
|||
|
|
// - 2-3x slower than std::mutex
|
|||
|
|
// - Used for every packet batch (every update)
|
|||
|
|
// COST: ~500-1000 CPU cycles per lock acquisition
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Contention Risk:** With 5000 bots, packet mutex will become a severe bottleneck.
|
|||
|
|
|
|||
|
|
**2. BotScheduler Queue Operations (BotScheduler.cpp:384-432)**
|
|||
|
|
```cpp
|
|||
|
|
void BotScheduler::ScheduleAction(ScheduleEntry const& entry)
|
|||
|
|
{
|
|||
|
|
// Line 394: Priority queue push with mutex
|
|||
|
|
_scheduleQueue.push(entry);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void BotScheduler::ProcessSchedule()
|
|||
|
|
{
|
|||
|
|
// Line 412-417: Mutex acquisition per queue operation
|
|||
|
|
std::lock_guard<std::mutex> lock(_scheduleQueueMutex);
|
|||
|
|
if (_scheduleQueue.empty())
|
|||
|
|
break;
|
|||
|
|
entry = _scheduleQueue.top();
|
|||
|
|
_scheduleQueue.pop();
|
|||
|
|
|
|||
|
|
// ⚠️ ISSUE: Mutex held during queue operations
|
|||
|
|
// With 5000 bots, this becomes a global bottleneck
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**3. Strategy Updates (BotAI.cpp:196-227)**
|
|||
|
|
```cpp
|
|||
|
|
void BotAI::UpdateStrategies(uint32 diff)
|
|||
|
|
{
|
|||
|
|
std::shared_lock lock(_mutex); // Line 201
|
|||
|
|
|
|||
|
|
// ⚠️ ISSUE: Shared lock acquired for EVERY update for EVERY bot
|
|||
|
|
// With 5000 bots × 10 updates/sec = 50,000 lock acquisitions/sec
|
|||
|
|
// Even with shared_mutex, this has cache coherency overhead
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Lock Contention Estimates:**
|
|||
|
|
|
|||
|
|
| Lock Type | Acquisitions/Second (5000 bots) | Contention Level |
|
|||
|
|
|-----------|--------------------------------|------------------|
|
|||
|
|
| BotSession::_packetMutex | 10,000 - 50,000 | 🔴 **CRITICAL** |
|
|||
|
|
| BotScheduler::_scheduleQueueMutex | 5,000 - 10,000 | 🔴 **CRITICAL** |
|
|||
|
|
| BotAI::_mutex (shared_lock) | 50,000 | ⚠️ **HIGH** |
|
|||
|
|
| GroupInvitationHandler locks | 1,000 - 5,000 | ⚠️ **MODERATE** |
|
|||
|
|
| Performance monitor locks | 50,000 - 100,000 | ⚠️ **HIGH** |
|
|||
|
|
|
|||
|
|
**Thread Pool Analysis:**
|
|||
|
|
- `BotScheduler.cpp`: No explicit thread pool (relies on Trinity core world updater)
|
|||
|
|
- `BotPerformanceMonitor.cpp:604-611`: 2 worker threads for metrics processing ✓ GOOD
|
|||
|
|
- **Missing:** Dedicated bot AI thread pool for parallel updates
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 2: Memory Profiling Strategy
|
|||
|
|
|
|||
|
|
### 2.1 Theoretical Memory Budget
|
|||
|
|
|
|||
|
|
**Per-Bot Breakdown (Detailed):**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
=== BotSession ===
|
|||
|
|
WorldSession base: 2,048 bytes
|
|||
|
|
BotSession extensions: 512 bytes
|
|||
|
|
Packet queues (2 queues): 2,048 bytes (avg 8 packets × 256 bytes each)
|
|||
|
|
Account data: 128 bytes
|
|||
|
|
Subtotal: 4,736 bytes
|
|||
|
|
|
|||
|
|
=== Player Object ===
|
|||
|
|
Player class (TrinityCore): 8,192 bytes (estimated from TrinityCore codebase)
|
|||
|
|
Inventory data: 2,048 bytes
|
|||
|
|
Spell book: 1,024 bytes
|
|||
|
|
Aura data: 512 bytes
|
|||
|
|
Quest data: 512 bytes
|
|||
|
|
Subtotal: 12,288 bytes
|
|||
|
|
|
|||
|
|
=== BotAI System ===
|
|||
|
|
BotAI base: 256 bytes
|
|||
|
|
ClassAI derived: 384 bytes
|
|||
|
|
Strategies (3 active): 768 bytes (256 bytes each)
|
|||
|
|
Action queue: 512 bytes (avg 10 actions × 51 bytes overhead)
|
|||
|
|
Trigger system: 256 bytes
|
|||
|
|
Performance metrics: 256 bytes
|
|||
|
|
Values cache: 256 bytes
|
|||
|
|
Subtotal: 2,688 bytes
|
|||
|
|
|
|||
|
|
=== Movement & Combat ===
|
|||
|
|
MotionMaster state: 384 bytes
|
|||
|
|
Combat state: 256 bytes
|
|||
|
|
Target tracking: 128 bytes
|
|||
|
|
Subtotal: 768 bytes
|
|||
|
|
|
|||
|
|
=== Group & Social ===
|
|||
|
|
Group data: 128 bytes
|
|||
|
|
Social list: 128 bytes
|
|||
|
|
Subtotal: 256 bytes
|
|||
|
|
|
|||
|
|
=== GRAND TOTAL === 20,736 bytes = 20.2 MB per bot
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Scaling (Conservative Estimate):**
|
|||
|
|
```
|
|||
|
|
1 bot = 20.2 MB
|
|||
|
|
10 bots = 202 MB
|
|||
|
|
100 bots = 2.02 GB
|
|||
|
|
500 bots = 10.1 GB
|
|||
|
|
1000 bots = 20.2 GB
|
|||
|
|
5000 bots = 101 GB ⚠️ EXCEEDS 50GB TARGET BY 2X
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Heap Fragmentation Risk:**
|
|||
|
|
- With 5000 bots creating/destroying objects continuously, heap fragmentation is inevitable
|
|||
|
|
- Estimated fragmentation overhead: **10-20%** additional memory
|
|||
|
|
- **Adjusted 5000-bot memory:** 101 GB × 1.15 = **116 GB total**
|
|||
|
|
|
|||
|
|
### 2.2 Memory Leak Detection Plan
|
|||
|
|
|
|||
|
|
**Static Analysis Results:**
|
|||
|
|
|
|||
|
|
**Potential Leak Locations (Grep Results: 523 occurrences of `new`/`make_unique`):**
|
|||
|
|
|
|||
|
|
1. **BotSession.cpp (10 occurrences):**
|
|||
|
|
- Line 499, 511: WorldPacket heap allocations (confirmed leak risk - every packet creates new allocation)
|
|||
|
|
- Line 866: Player object creation (managed by TrinityCore - ✓ SAFE)
|
|||
|
|
- Line 921: BotAI creation with .release() (ownership transfer - ✓ SAFE if deleted properly)
|
|||
|
|
|
|||
|
|
2. **BotScheduler.cpp (No raw `new` found - uses std containers):**
|
|||
|
|
- ✓ GOOD: No manual memory management
|
|||
|
|
|
|||
|
|
3. **LeaderFollowBehavior.cpp (9 occurrences):**
|
|||
|
|
- Movement vector allocations - **POTENTIAL LEAK** if not cleared
|
|||
|
|
|
|||
|
|
4. **BotAI.cpp (6 occurrences):**
|
|||
|
|
- Line 58: GroupInvitationHandler (std::make_unique - ✓ SAFE)
|
|||
|
|
- Strategy creation (std::unique_ptr - ✓ SAFE)
|
|||
|
|
- Action creation (std::shared_ptr - ✓ SAFE)
|
|||
|
|
|
|||
|
|
**Circular Reference Risk (shared_ptr):**
|
|||
|
|
- `BotAI.cpp`: Action queue uses `std::shared_ptr<Action>` (Line 274)
|
|||
|
|
- Triggers use `std::shared_ptr<Trigger>` (Line 278)
|
|||
|
|
- **Risk:** Low - no evidence of circular references in code
|
|||
|
|
|
|||
|
|
**Memory Management Grade:**
|
|||
|
|
- **Raw `new`/`delete` usage:** ⚠️ **MODERATE** (10-20 occurrences)
|
|||
|
|
- **Smart pointer usage:** ✓ **EXCELLENT** (majority of allocations)
|
|||
|
|
- **RAII compliance:** ✓ **GOOD** (most classes use RAII)
|
|||
|
|
- **Leak risk:** ⚠️ **MODERATE** (packet allocations, movement data)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 3: CPU Profiling Strategy
|
|||
|
|
|
|||
|
|
### 3.1 Hotspot Identification (Static Analysis)
|
|||
|
|
|
|||
|
|
**Critical Path Functions (Estimated CPU Cycles):**
|
|||
|
|
|
|||
|
|
| Function | CPU Cycles | Frequency (5000 bots) | Total Cycles/sec |
|
|||
|
|
|----------|------------|----------------------|------------------|
|
|||
|
|
| `BotSession::Update()` | 500-1000 | 50,000/sec | 25-50M |
|
|||
|
|
| `BotAI::UpdateAI()` | 900-1700 | 50,000/sec | 45-85M |
|
|||
|
|
| `BotAI::UpdateStrategies()` | 200-400 | 50,000/sec | 10-20M |
|
|||
|
|
| `BotAI::ProcessTriggers()` | 500-1000 | 50,000/sec | 25-50M |
|
|||
|
|
| `LeaderFollowBehavior::Update()` | 300-800 | 20,000/sec (active) | 6-16M |
|
|||
|
|
| `ClassAI::OnCombatUpdate()` | 500-2000 | 10,000/sec (in combat) | 5-20M |
|
|||
|
|
| `BotSession::ProcessBotPackets()` | 200-500 | 50,000/sec | 10-25M |
|
|||
|
|
| `BotScheduler::ProcessSchedule()` | 100-300 | 1,000/sec (global) | 0.1-0.3M |
|
|||
|
|
| **TOTAL** | | | **126-266M cycles/sec** |
|
|||
|
|
|
|||
|
|
**At 3.5 GHz:** 126-266M cycles = **3.6-7.6% CPU usage**
|
|||
|
|
|
|||
|
|
**But this assumes:**
|
|||
|
|
- ✓ Perfect thread scaling (no contention)
|
|||
|
|
- ✓ Zero cache misses
|
|||
|
|
- ✓ No memory allocation overhead
|
|||
|
|
- ✓ No exception handling overhead
|
|||
|
|
|
|||
|
|
**Realistic estimate with overhead (+50-100%):** **5.4-15.2% CPU** ⚠️ **EXCEEDS 5% TARGET**
|
|||
|
|
|
|||
|
|
### 3.2 Algorithm Complexity Analysis
|
|||
|
|
|
|||
|
|
**BotAI::ProcessTriggers() - O(n) per bot where n = trigger count**
|
|||
|
|
```cpp
|
|||
|
|
// Line 310-332
|
|||
|
|
for (auto const& trigger : _triggers) // O(n) where n = 10-20
|
|||
|
|
{
|
|||
|
|
if (trigger && trigger->Check(this)) // O(1)
|
|||
|
|
{
|
|||
|
|
auto result = trigger->Evaluate(this); // O(k) where k varies by trigger
|
|||
|
|
if (result.triggered && result.suggestedAction)
|
|||
|
|
{
|
|||
|
|
_triggeredActions.push(result); // O(log m) where m = queue size
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
**Complexity:** O(n × k × log m) per bot
|
|||
|
|
**With 5000 bots:** O(5000 × 15 × log 5) = O(5000 × 15 × 2.3) = **~172,500 operations/update**
|
|||
|
|
|
|||
|
|
**BotAI::UpdateStrategies() - O(s) per bot where s = active strategies**
|
|||
|
|
```cpp
|
|||
|
|
// Line 196-227
|
|||
|
|
for (auto const& strategyName : _activeStrategies) // O(s) where s = 3-5
|
|||
|
|
{
|
|||
|
|
if (auto* strategy = GetStrategy(strategyName)) // O(1) - hash map lookup
|
|||
|
|
{
|
|||
|
|
if (strategy->IsActive(this)) // O(1)
|
|||
|
|
{
|
|||
|
|
strategy->UpdateBehavior(this, diff); // O(k) - strategy-dependent
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
**Complexity:** O(s × k) per bot where k varies widely
|
|||
|
|
**Follow strategy:** O(100-500) - pathfinding checks
|
|||
|
|
**Combat strategy:** O(50-200) - target validation
|
|||
|
|
**Idle strategy:** O(10-50) - simple checks
|
|||
|
|
|
|||
|
|
**With 5000 bots × 3 strategies:** ~**500,000-2,500,000 operations/update**
|
|||
|
|
|
|||
|
|
### 3.3 String Operations & Logging
|
|||
|
|
|
|||
|
|
**Grep Results:** Extensive logging in hot paths
|
|||
|
|
|
|||
|
|
**Critical Logging Hotspots:**
|
|||
|
|
- `BotSession.cpp:265-304` - Combat state logging (TC_LOG_ERROR every combat transition)
|
|||
|
|
- `BotAI.cpp` - Debug logging (TC_LOG_DEBUG frequently called)
|
|||
|
|
- `BotScheduler.cpp:270-272` - Info logging on every schedule
|
|||
|
|
|
|||
|
|
**String Allocation Overhead:**
|
|||
|
|
- Each TC_LOG_* call creates temporary std::string objects
|
|||
|
|
- With 5000 bots, logging overhead can be **5-15% CPU**
|
|||
|
|
- **Recommendation:** Disable DEBUG logs in production, throttle INFO logs
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 4: Optimization Recommendations
|
|||
|
|
|
|||
|
|
### 4.1 CRITICAL Priority - Memory Optimizations
|
|||
|
|
|
|||
|
|
#### **Optimization #1: Object Pooling for Packets**
|
|||
|
|
|
|||
|
|
**Problem:**
|
|||
|
|
`BotSession.cpp:499, 511` - Every packet creates a new heap allocation via `std::make_unique<WorldPacket>`
|
|||
|
|
|
|||
|
|
**Current Code:**
|
|||
|
|
```cpp
|
|||
|
|
void BotSession::SendPacket(WorldPacket const* packet, bool forced)
|
|||
|
|
{
|
|||
|
|
auto packetCopy = std::make_unique<WorldPacket>(*packet); // ⚠️ HEAP ALLOCATION
|
|||
|
|
_outgoingPackets.push(std::move(packetCopy));
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Optimized Code (Object Pool):**
|
|||
|
|
```cpp
|
|||
|
|
// In BotSession.h
|
|||
|
|
class WorldPacketPool
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
static WorldPacketPool& Instance()
|
|||
|
|
{
|
|||
|
|
static WorldPacketPool instance;
|
|||
|
|
return instance;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
std::unique_ptr<WorldPacket> Acquire()
|
|||
|
|
{
|
|||
|
|
std::lock_guard<std::mutex> lock(_mutex);
|
|||
|
|
if (_pool.empty())
|
|||
|
|
{
|
|||
|
|
return std::make_unique<WorldPacket>(); // Only allocate if pool empty
|
|||
|
|
}
|
|||
|
|
auto packet = std::move(_pool.back());
|
|||
|
|
_pool.pop_back();
|
|||
|
|
return packet;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Release(std::unique_ptr<WorldPacket> packet)
|
|||
|
|
{
|
|||
|
|
if (!packet) return;
|
|||
|
|
packet->clear(); // Reset for reuse
|
|||
|
|
|
|||
|
|
std::lock_guard<std::mutex> lock(_mutex);
|
|||
|
|
if (_pool.size() < MAX_POOL_SIZE)
|
|||
|
|
{
|
|||
|
|
_pool.push_back(std::move(packet));
|
|||
|
|
}
|
|||
|
|
// else let it deallocate
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
static constexpr size_t MAX_POOL_SIZE = 1000; // Per-pool limit
|
|||
|
|
std::vector<std::unique_ptr<WorldPacket>> _pool;
|
|||
|
|
std::mutex _mutex;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Optimized SendPacket
|
|||
|
|
void BotSession::SendPacket(WorldPacket const* packet, bool forced)
|
|||
|
|
{
|
|||
|
|
auto packetCopy = WorldPacketPool::Instance().Acquire();
|
|||
|
|
*packetCopy = *packet; // Copy data without allocation
|
|||
|
|
_outgoingPackets.push(std::move(packetCopy));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// In packet processing, return packets to pool
|
|||
|
|
void BotSession::ProcessBotPackets()
|
|||
|
|
{
|
|||
|
|
// ... existing code ...
|
|||
|
|
for (auto& packet : outgoingBatch)
|
|||
|
|
{
|
|||
|
|
// Process packet...
|
|||
|
|
WorldPacketPool::Instance().Release(std::move(packet)); // Return to pool
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Expected Impact:**
|
|||
|
|
- **Memory reduction:** 50-80% reduction in packet-related allocations
|
|||
|
|
- **Performance gain:** 10-20% reduction in memory allocator overhead
|
|||
|
|
- **Fragmentation:** 30-50% reduction in heap fragmentation
|
|||
|
|
- **5000 bots:** Saves ~10-15GB of fragmented memory
|
|||
|
|
|
|||
|
|
**Implementation Complexity:** MEDIUM (1-2 hours)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### **Optimization #2: Reduce Per-Bot Memory Footprint**
|
|||
|
|
|
|||
|
|
**Problem:** Each bot consumes 20.2 MB, totaling 101 GB for 5000 bots
|
|||
|
|
|
|||
|
|
**Current Breakdown:**
|
|||
|
|
- WorldSession: 2048 bytes
|
|||
|
|
- Player: 8192 bytes
|
|||
|
|
- Packet queues: 2048 bytes ⚠️ **OPTIMIZABLE**
|
|||
|
|
- Inventory: 2048 bytes ⚠️ **OPTIMIZABLE**
|
|||
|
|
- Spell book: 1024 bytes ⚠️ **OPTIMIZABLE**
|
|||
|
|
|
|||
|
|
**Optimization Strategy:**
|
|||
|
|
|
|||
|
|
**A. Shared Spell Data (Copy-on-Write)**
|
|||
|
|
```cpp
|
|||
|
|
// Current: Each bot has full spell book copy (1024 bytes × 5000 = 5MB)
|
|||
|
|
// Optimized: Shared spell template + bot-specific overrides
|
|||
|
|
|
|||
|
|
class SharedSpellBook
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
static SharedSpellBook& Instance()
|
|||
|
|
{
|
|||
|
|
static SharedSpellBook instance;
|
|||
|
|
return instance;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Load class/spec spell templates at startup
|
|||
|
|
void LoadTemplate(uint8 classId, uint8 spec, std::vector<uint32> const& spellIds)
|
|||
|
|
{
|
|||
|
|
uint64 key = (uint64(classId) << 8) | spec;
|
|||
|
|
_templates[key] = std::make_shared<std::vector<uint32>>(spellIds);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
std::shared_ptr<std::vector<uint32>> GetTemplate(uint8 classId, uint8 spec) const
|
|||
|
|
{
|
|||
|
|
uint64 key = (uint64(classId) << 8) | spec;
|
|||
|
|
auto it = _templates.find(key);
|
|||
|
|
return it != _templates.end() ? it->second : nullptr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
std::unordered_map<uint64, std::shared_ptr<std::vector<uint32>>> _templates;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// In Player/BotAI:
|
|||
|
|
std::shared_ptr<std::vector<uint32>> _spellTemplate; // Shared pointer (8 bytes)
|
|||
|
|
std::vector<uint32> _spellOverrides; // Only custom spells (typically empty)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Memory Savings:**
|
|||
|
|
- **Before:** 1024 bytes × 5000 bots = 5 MB
|
|||
|
|
- **After:** 8 bytes (shared_ptr) × 5000 bots + shared templates = 40 KB + ~500 KB templates = **540 KB**
|
|||
|
|
- **Savings:** 4.5 MB (~0.1% but compound effect with other optimizations)
|
|||
|
|
|
|||
|
|
**B. Lazy-Load Inventory**
|
|||
|
|
```cpp
|
|||
|
|
// Only load full inventory when bot interacts with items
|
|||
|
|
// For idle bots, just store equipped items (96 bytes vs 2048 bytes)
|
|||
|
|
|
|||
|
|
class LazyInventory
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
void LoadEquippedOnly(ObjectGuid botGuid); // Load 16 equipped slots
|
|||
|
|
void LoadFull(ObjectGuid botGuid); // Load all 256 slots on demand
|
|||
|
|
void Unload(); // Unload bags when idle
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
bool _fullLoaded = false;
|
|||
|
|
std::array<Item*, 16> _equipped; // Only equipped items
|
|||
|
|
std::vector<Item*> _bags; // Loaded on demand
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Memory Savings:**
|
|||
|
|
- **Per idle bot:** 2048 - 96 = 1952 bytes saved
|
|||
|
|
- **For 4000 idle bots (80%):** 1952 × 4000 = **7.8 MB saved**
|
|||
|
|
|
|||
|
|
**C. Compress Packet Queues**
|
|||
|
|
```cpp
|
|||
|
|
// Use ring buffer instead of std::queue to prevent allocations
|
|||
|
|
template<typename T, size_t Size = 64>
|
|||
|
|
class RingBuffer
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
bool push(T&& item)
|
|||
|
|
{
|
|||
|
|
if ((_head + 1) % Size == _tail)
|
|||
|
|
return false; // Full
|
|||
|
|
|
|||
|
|
_buffer[_head] = std::move(item);
|
|||
|
|
_head = (_head + 1) % Size;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool pop(T& item)
|
|||
|
|
{
|
|||
|
|
if (_head == _tail)
|
|||
|
|
return false; // Empty
|
|||
|
|
|
|||
|
|
item = std::move(_buffer[_tail]);
|
|||
|
|
_tail = (_tail + 1) % Size;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
std::array<T, Size> _buffer; // Pre-allocated, no heap allocations
|
|||
|
|
size_t _head = 0, _tail = 0;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Replace std::queue with RingBuffer
|
|||
|
|
RingBuffer<std::unique_ptr<WorldPacket>, 64> _incomingPackets;
|
|||
|
|
RingBuffer<std::unique_ptr<WorldPacket>, 64> _outgoingPackets;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Memory Savings:**
|
|||
|
|
- **Per bot:** 2048 - (64 × 8 pointers) = 2048 - 512 = **1536 bytes saved**
|
|||
|
|
- **For 5000 bots:** 1536 × 5000 = **7.5 MB saved**
|
|||
|
|
|
|||
|
|
**Total Memory Savings (All Optimizations):**
|
|||
|
|
- Spell book: 4.5 MB
|
|||
|
|
- Lazy inventory: 7.8 MB
|
|||
|
|
- Packet queues: 7.5 MB
|
|||
|
|
- **TOTAL:** ~20 MB (0.02% - modest but helps)
|
|||
|
|
|
|||
|
|
**More Aggressive:** Reduce Player object size by offloading rarely-used data to database
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4.2 HIGH Priority - CPU Optimizations
|
|||
|
|
|
|||
|
|
#### **Optimization #3: Batch Bot Updates (Critical)**
|
|||
|
|
|
|||
|
|
**Problem:**
|
|||
|
|
Current architecture updates each bot individually, causing poor cache locality and excessive function call overhead.
|
|||
|
|
|
|||
|
|
**Current Code Flow:**
|
|||
|
|
```cpp
|
|||
|
|
// In BotSessionManager or equivalent
|
|||
|
|
for (auto& botSession : _sessions)
|
|||
|
|
{
|
|||
|
|
botSession->Update(diff, packetFilter); // Individual update
|
|||
|
|
}
|
|||
|
|
// ⚠️ ISSUE: Poor cache locality, no SIMD potential
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Optimized Code (Batch Processing):**
|
|||
|
|
```cpp
|
|||
|
|
class BotUpdateBatcher
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
void AddBot(BotSession* bot)
|
|||
|
|
{
|
|||
|
|
_bots.push_back(bot);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void UpdateAll(uint32 diff, PacketFilter& filter)
|
|||
|
|
{
|
|||
|
|
// PHASE 1: Batch validation (cache-friendly)
|
|||
|
|
std::vector<BotSession*> validBots;
|
|||
|
|
validBots.reserve(_bots.size());
|
|||
|
|
|
|||
|
|
for (auto* bot : _bots)
|
|||
|
|
{
|
|||
|
|
if (bot && bot->IsActive())
|
|||
|
|
validBots.push_back(bot);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// PHASE 2: Parallel batch update
|
|||
|
|
const size_t BATCH_SIZE = 100; // Process 100 bots per thread
|
|||
|
|
std::vector<std::future<void>> futures;
|
|||
|
|
|
|||
|
|
for (size_t i = 0; i < validBots.size(); i += BATCH_SIZE)
|
|||
|
|
{
|
|||
|
|
size_t end = std::min(i + BATCH_SIZE, validBots.size());
|
|||
|
|
|
|||
|
|
futures.push_back(std::async(std::launch::async, [&, i, end]()
|
|||
|
|
{
|
|||
|
|
for (size_t j = i; j < end; ++j)
|
|||
|
|
{
|
|||
|
|
validBots[j]->Update(diff, filter);
|
|||
|
|
}
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Wait for all batches
|
|||
|
|
for (auto& future : futures)
|
|||
|
|
future.wait();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
std::vector<BotSession*> _bots;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Expected Impact:**
|
|||
|
|
- **CPU reduction:** 20-40% reduction via parallel processing
|
|||
|
|
- **Cache efficiency:** 30-50% improvement from batch memory access
|
|||
|
|
- **5000 bots:** Reduces CPU from 7.6% to **4.5-6%** (closer to target)
|
|||
|
|
|
|||
|
|
**Windows-Specific Optimization (Thread Pool):**
|
|||
|
|
```cpp
|
|||
|
|
#include <Windows.h>
|
|||
|
|
#include <threadpoolapiset.h>
|
|||
|
|
|
|||
|
|
class WindowsThreadPoolBotUpdater
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
WindowsThreadPoolBotUpdater()
|
|||
|
|
{
|
|||
|
|
// Create thread pool with optimal thread count
|
|||
|
|
_pool = CreateThreadpool(nullptr);
|
|||
|
|
SetThreadpoolThreadMaximum(_pool, std::thread::hardware_concurrency());
|
|||
|
|
SetThreadpoolThreadMinimum(_pool, 2);
|
|||
|
|
|
|||
|
|
InitializeThreadpoolEnvironment(&_env);
|
|||
|
|
SetThreadpoolCallbackPool(&_env, _pool);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
~WindowsThreadPoolBotUpdater()
|
|||
|
|
{
|
|||
|
|
CloseThreadpool(_pool);
|
|||
|
|
DestroyThreadpoolEnvironment(&_env);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void UpdateBots(std::vector<BotSession*> const& bots, uint32 diff)
|
|||
|
|
{
|
|||
|
|
struct WorkContext
|
|||
|
|
{
|
|||
|
|
std::vector<BotSession*> const* bots;
|
|||
|
|
size_t start, end;
|
|||
|
|
uint32 diff;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
std::vector<WorkContext> contexts;
|
|||
|
|
const size_t BATCH_SIZE = 50;
|
|||
|
|
|
|||
|
|
for (size_t i = 0; i < bots.size(); i += BATCH_SIZE)
|
|||
|
|
{
|
|||
|
|
contexts.push_back({
|
|||
|
|
&bots,
|
|||
|
|
i,
|
|||
|
|
std::min(i + BATCH_SIZE, bots.size()),
|
|||
|
|
diff
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
std::vector<PTP_WORK> workItems;
|
|||
|
|
workItems.reserve(contexts.size());
|
|||
|
|
|
|||
|
|
for (auto& ctx : contexts)
|
|||
|
|
{
|
|||
|
|
auto work = CreateThreadpoolWork([](PTP_CALLBACK_INSTANCE, PVOID param, PTP_WORK)
|
|||
|
|
{
|
|||
|
|
auto* context = static_cast<WorkContext*>(param);
|
|||
|
|
PacketFilter filter; // Thread-local packet filter
|
|||
|
|
|
|||
|
|
for (size_t i = context->start; i < context->end; ++i)
|
|||
|
|
{
|
|||
|
|
(*context->bots)[i]->Update(context->diff, filter);
|
|||
|
|
}
|
|||
|
|
}, &ctx, &_env);
|
|||
|
|
|
|||
|
|
SubmitThreadpoolWork(work);
|
|||
|
|
workItems.push_back(work);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Wait for all work items
|
|||
|
|
for (auto work : workItems)
|
|||
|
|
{
|
|||
|
|
WaitForThreadpoolWorkCallbacks(work, FALSE);
|
|||
|
|
CloseThreadpoolWork(work);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
PTP_POOL _pool;
|
|||
|
|
TP_CALLBACK_ENVIRON _env;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Windows Thread Pool Benefits:**
|
|||
|
|
- **Lower overhead:** Windows kernel thread pool is ~30% faster than std::async
|
|||
|
|
- **Better scheduling:** Automatic work stealing between cores
|
|||
|
|
- **NUMA-aware:** Optimal thread placement on multi-socket systems
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### **Optimization #4: Remove Validation Overhead in Hot Path**
|
|||
|
|
|
|||
|
|
**Problem:**
|
|||
|
|
`BotSession::Update()` has extensive safety checks that run every frame for every bot
|
|||
|
|
|
|||
|
|
**Current Code (BotSession.cpp:522-544):**
|
|||
|
|
```cpp
|
|||
|
|
bool BotSession::Update(uint32 diff, PacketFilter& updater)
|
|||
|
|
{
|
|||
|
|
// ⚠️ EXCESSIVE VALIDATION (runs 50,000 times/sec with 5000 bots)
|
|||
|
|
if (!_active.load() || _destroyed.load()) // 2 atomic loads
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
uint32 accountId = GetAccountId(); // Function call
|
|||
|
|
if (accountId == 0) // Validation
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
if (_bnetAccountId == 0 || _bnetAccountId != accountId) // 2 more checks
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
// Recursive call detection
|
|||
|
|
static thread_local bool inUpdateCall = false;
|
|||
|
|
if (inUpdateCall)
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
// ... actual update logic ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Optimized Code (Reduce Checks):**
|
|||
|
|
```cpp
|
|||
|
|
// Move validation to initialization/destruction only
|
|||
|
|
// Add fast-path for common case
|
|||
|
|
|
|||
|
|
bool BotSession::Update(uint32 diff, PacketFilter& updater)
|
|||
|
|
{
|
|||
|
|
// FAST PATH: Single atomic check (most common case)
|
|||
|
|
if (!_active.load(std::memory_order_relaxed)) // Relaxed ordering for speed
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
// Full validation only runs on first update after state change
|
|||
|
|
if (UNLIKELY(_validationRequired.load(std::memory_order_acquire)))
|
|||
|
|
{
|
|||
|
|
if (!ValidateSessionState())
|
|||
|
|
return false;
|
|||
|
|
_validationRequired.store(false, std::memory_order_release);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ... actual update logic ...
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
std::atomic<bool> _validationRequired{true}; // Set to true only on state changes
|
|||
|
|
|
|||
|
|
bool ValidateSessionState()
|
|||
|
|
{
|
|||
|
|
// All the expensive validation logic moved here
|
|||
|
|
// Only called once or when state changes
|
|||
|
|
if (_destroyed.load()) return false;
|
|||
|
|
if (GetAccountId() == 0) return false;
|
|||
|
|
// ... other checks ...
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Expected Impact:**
|
|||
|
|
- **CPU reduction:** 10-15% reduction in BotSession::Update overhead
|
|||
|
|
- **Cache efficiency:** Better instruction cache utilization
|
|||
|
|
- **5000 bots:** Saves ~0.5-1% CPU
|
|||
|
|
|
|||
|
|
**Implementation Complexity:** LOW (30 minutes)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### **Optimization #5: Cache Combat Spell Availability**
|
|||
|
|
|
|||
|
|
**Problem:**
|
|||
|
|
ClassAI combat rotations check spell availability repeatedly without caching
|
|||
|
|
|
|||
|
|
**Current Pattern (Common across ClassAI implementations):**
|
|||
|
|
```cpp
|
|||
|
|
void MageAI::OnCombatUpdate(uint32 diff)
|
|||
|
|
{
|
|||
|
|
// Every update checks spell availability from scratch
|
|||
|
|
if (CanCast(SPELL_FIREBALL)) // ⚠️ Queries spell book every time
|
|||
|
|
CastSpell(SPELL_FIREBALL);
|
|||
|
|
|
|||
|
|
if (CanCast(SPELL_PYROBLAST))
|
|||
|
|
CastSpell(SPELL_PYROBLAST);
|
|||
|
|
|
|||
|
|
// ... 10-20 more spell checks per update ...
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool CanCast(uint32 spellId)
|
|||
|
|
{
|
|||
|
|
// Expensive checks:
|
|||
|
|
// - Player::HasSpell() - hash map lookup
|
|||
|
|
// - SpellInfo::CheckCooldown() - time calculation
|
|||
|
|
// - Player::GetPower() - resource check
|
|||
|
|
return _bot->HasSpell(spellId) && // Hash lookup
|
|||
|
|
!_bot->HasSpellCooldown(spellId) && // Cooldown check
|
|||
|
|
_bot->GetPower(POWER_MANA) >= GetSpellCost(spellId); // Resource check
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Optimized Code (Cached Availability):**
|
|||
|
|
```cpp
|
|||
|
|
class CachedSpellAvailability
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
void Update(Player* bot, uint32 diff)
|
|||
|
|
{
|
|||
|
|
_updateTimer += diff;
|
|||
|
|
if (_updateTimer < 100) // Update cache every 100ms instead of every frame
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
_updateTimer = 0;
|
|||
|
|
|
|||
|
|
// Batch update all spell availability
|
|||
|
|
for (auto spellId : _spellsToCheck)
|
|||
|
|
{
|
|||
|
|
bool available = bot->HasSpell(spellId) &&
|
|||
|
|
!bot->HasSpellCooldown(spellId) &&
|
|||
|
|
bot->GetPower(POWER_MANA) >= GetSpellCost(spellId);
|
|||
|
|
|
|||
|
|
_availability[spellId] = available;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool IsAvailable(uint32 spellId) const
|
|||
|
|
{
|
|||
|
|
auto it = _availability.find(spellId);
|
|||
|
|
return it != _availability.end() && it->second;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void RegisterSpell(uint32 spellId)
|
|||
|
|
{
|
|||
|
|
_spellsToCheck.push_back(spellId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
std::vector<uint32> _spellsToCheck;
|
|||
|
|
std::unordered_map<uint32, bool> _availability;
|
|||
|
|
uint32 _updateTimer = 0;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// In ClassAI:
|
|||
|
|
CachedSpellAvailability _spellCache;
|
|||
|
|
|
|||
|
|
void MageAI::OnCombatUpdate(uint32 diff)
|
|||
|
|
{
|
|||
|
|
_spellCache.Update(_bot, diff); // Update cache every 100ms
|
|||
|
|
|
|||
|
|
// Fast cached lookup instead of expensive checks
|
|||
|
|
if (_spellCache.IsAvailable(SPELL_FIREBALL))
|
|||
|
|
CastSpell(SPELL_FIREBALL);
|
|||
|
|
|
|||
|
|
if (_spellCache.IsAvailable(SPELL_PYROBLAST))
|
|||
|
|
CastSpell(SPELL_PYROBLAST);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Expected Impact:**
|
|||
|
|
- **CPU reduction:** 30-50% reduction in combat rotation overhead
|
|||
|
|
- **For 1000 bots in combat:** Saves ~2-4% CPU
|
|||
|
|
- **Cache hit rate:** 95%+ (spell availability changes infrequently)
|
|||
|
|
|
|||
|
|
**Implementation Complexity:** MEDIUM (1-2 hours per class)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4.3 MEDIUM Priority - Database Optimizations
|
|||
|
|
|
|||
|
|
#### **Optimization #6: Batch Character Loading**
|
|||
|
|
|
|||
|
|
**Problem:**
|
|||
|
|
5000 bots logging in simultaneously creates 375,000 database queries (75 queries × 5000 bots)
|
|||
|
|
|
|||
|
|
**Current Code (BotSession.cpp:799-819):**
|
|||
|
|
```cpp
|
|||
|
|
bool BotSession::LoginCharacter(ObjectGuid characterGuid)
|
|||
|
|
{
|
|||
|
|
// Creates a LoginQueryHolder with 75+ prepared statements
|
|||
|
|
std::shared_ptr<BotLoginQueryHolder> holder =
|
|||
|
|
std::make_shared<BotLoginQueryHolder>(GetAccountId(), characterGuid);
|
|||
|
|
|
|||
|
|
// ⚠️ ISSUE: Each bot gets a separate query holder
|
|||
|
|
AddQueryHolderCallback(CharacterDatabase.DelayQueryHolder(holder))
|
|||
|
|
.AfterComplete([this](SQLQueryHolderBase const& holder) {
|
|||
|
|
HandleBotPlayerLogin(static_cast<BotLoginQueryHolder const&>(holder));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Optimized Code (Batch Loader):**
|
|||
|
|
```cpp
|
|||
|
|
class BatchCharacterLoader
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
struct LoadRequest
|
|||
|
|
{
|
|||
|
|
ObjectGuid characterGuid;
|
|||
|
|
uint32 accountId;
|
|||
|
|
std::function<void(Player*)> callback;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
void QueueLoad(ObjectGuid guid, uint32 accountId, std::function<void(Player*)> callback)
|
|||
|
|
{
|
|||
|
|
std::lock_guard lock(_mutex);
|
|||
|
|
_pendingLoads.push_back({guid, accountId, std::move(callback)});
|
|||
|
|
|
|||
|
|
// Trigger batch load when queue reaches threshold
|
|||
|
|
if (_pendingLoads.size() >= BATCH_SIZE)
|
|||
|
|
{
|
|||
|
|
ProcessBatch();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void ProcessBatch()
|
|||
|
|
{
|
|||
|
|
if (_pendingLoads.empty()) return;
|
|||
|
|
|
|||
|
|
// Build single query to load multiple characters
|
|||
|
|
std::ostringstream query;
|
|||
|
|
query << "SELECT guid, account, name, race, class, level, ... FROM characters WHERE guid IN (";
|
|||
|
|
|
|||
|
|
for (size_t i = 0; i < _pendingLoads.size(); ++i)
|
|||
|
|
{
|
|||
|
|
if (i > 0) query << ",";
|
|||
|
|
query << _pendingLoads[i].characterGuid.GetCounter();
|
|||
|
|
}
|
|||
|
|
query << ")";
|
|||
|
|
|
|||
|
|
// Execute single batch query instead of N individual queries
|
|||
|
|
QueryResult result = CharacterDatabase.Query(query.str());
|
|||
|
|
|
|||
|
|
// Create Player objects and invoke callbacks
|
|||
|
|
while (result && result->NextRow())
|
|||
|
|
{
|
|||
|
|
Field* fields = result->Fetch();
|
|||
|
|
ObjectGuid guid(HighGuid::Player, fields[0].GetUInt64());
|
|||
|
|
|
|||
|
|
// Find matching request
|
|||
|
|
auto it = std::find_if(_pendingLoads.begin(), _pendingLoads.end(),
|
|||
|
|
[guid](LoadRequest const& req) { return req.characterGuid == guid; });
|
|||
|
|
|
|||
|
|
if (it != _pendingLoads.end())
|
|||
|
|
{
|
|||
|
|
Player* player = CreatePlayerFromFields(fields);
|
|||
|
|
it->callback(player);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_pendingLoads.clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
static constexpr size_t BATCH_SIZE = 50; // Load 50 characters per query
|
|||
|
|
std::vector<LoadRequest> _pendingLoads;
|
|||
|
|
std::mutex _mutex;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Usage:
|
|||
|
|
sBatchCharacterLoader.QueueLoad(characterGuid, accountId, [this](Player* player) {
|
|||
|
|
SetPlayer(player);
|
|||
|
|
// Complete login...
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Expected Impact:**
|
|||
|
|
- **Query reduction:** 75 queries → 2-3 queries per batch (96%+ reduction)
|
|||
|
|
- **Login time:** 250-1000 seconds → 10-30 seconds for 5000 bots (95%+ faster)
|
|||
|
|
- **Connection pool:** Reduces peak connections from 5000 to 100-200
|
|||
|
|
|
|||
|
|
**Implementation Complexity:** HIGH (4-6 hours, requires careful TrinityCore integration)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### **Optimization #7: Aggressive Caching of Static Data**
|
|||
|
|
|
|||
|
|
**Problem:**
|
|||
|
|
Repeated queries for spell data, item stats, and quest information
|
|||
|
|
|
|||
|
|
**Optimization Strategy:**
|
|||
|
|
|
|||
|
|
**A. Spell Data Cache (Startup Preload)**
|
|||
|
|
```cpp
|
|||
|
|
class SpellDataCache
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
void Initialize()
|
|||
|
|
{
|
|||
|
|
TC_LOG_INFO("playerbot", "Preloading spell data cache...");
|
|||
|
|
|
|||
|
|
// Load all spell info at startup (one-time cost)
|
|||
|
|
QueryResult result = WorldDatabase.Query(
|
|||
|
|
"SELECT ID, ManaCost, CastTime, Cooldown, Range FROM spell_template"
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
while (result && result->NextRow())
|
|||
|
|
{
|
|||
|
|
Field* fields = result->Fetch();
|
|||
|
|
SpellCacheEntry entry;
|
|||
|
|
entry.spellId = fields[0].GetUInt32();
|
|||
|
|
entry.manaCost = fields[1].GetUInt32();
|
|||
|
|
entry.castTime = fields[2].GetUInt32();
|
|||
|
|
entry.cooldown = fields[3].GetUInt32();
|
|||
|
|
entry.range = fields[4].GetFloat();
|
|||
|
|
|
|||
|
|
_cache[entry.spellId] = entry;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
TC_LOG_INFO("playerbot", "Loaded {} spell entries into cache", _cache.size());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
SpellCacheEntry const* GetSpell(uint32 spellId) const
|
|||
|
|
{
|
|||
|
|
auto it = _cache.find(spellId);
|
|||
|
|
return it != _cache.end() ? &it->second : nullptr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
struct SpellCacheEntry
|
|||
|
|
{
|
|||
|
|
uint32 spellId;
|
|||
|
|
uint32 manaCost;
|
|||
|
|
uint32 castTime;
|
|||
|
|
uint32 cooldown;
|
|||
|
|
float range;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
std::unordered_map<uint32, SpellCacheEntry> _cache;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**B. Quest Data Cache**
|
|||
|
|
```cpp
|
|||
|
|
// Similar pattern for quest objectives, rewards, requirements
|
|||
|
|
// Pre-load at startup, never query during runtime
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Expected Impact:**
|
|||
|
|
- **Database load:** 90%+ reduction in spell/quest queries
|
|||
|
|
- **Response time:** Instant cache lookup vs 1-10ms database query
|
|||
|
|
- **Memory cost:** ~20-50 MB for complete spell/quest cache (acceptable)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4.4 MEDIUM Priority - Lock-Free Optimizations
|
|||
|
|
|
|||
|
|
#### **Optimization #8: Replace Scheduler Mutex with Lock-Free Queue**
|
|||
|
|
|
|||
|
|
**Problem:**
|
|||
|
|
`BotScheduler.cpp:412-417` uses mutex-protected priority queue, creating global bottleneck
|
|||
|
|
|
|||
|
|
**Current Code:**
|
|||
|
|
```cpp
|
|||
|
|
void BotScheduler::ProcessSchedule()
|
|||
|
|
{
|
|||
|
|
while (actionsProcessed < MAX_ACTIONS_PER_UPDATE)
|
|||
|
|
{
|
|||
|
|
ScheduleEntry entry;
|
|||
|
|
{
|
|||
|
|
std::lock_guard<std::mutex> lock(_scheduleQueueMutex); // ⚠️ GLOBAL LOCK
|
|||
|
|
if (_scheduleQueue.empty())
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
entry = _scheduleQueue.top();
|
|||
|
|
_scheduleQueue.pop();
|
|||
|
|
}
|
|||
|
|
// ... process entry ...
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Optimized Code (Lock-Free Priority Queue):**
|
|||
|
|
```cpp
|
|||
|
|
#include <boost/lockfree/queue.hpp>
|
|||
|
|
|
|||
|
|
class LockFreeScheduler
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
void ScheduleAction(ScheduleEntry const& entry)
|
|||
|
|
{
|
|||
|
|
// Lock-free push
|
|||
|
|
_queue.push(entry);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void ProcessSchedule()
|
|||
|
|
{
|
|||
|
|
ScheduleEntry entry;
|
|||
|
|
uint32 processed = 0;
|
|||
|
|
|
|||
|
|
// Lock-free pop
|
|||
|
|
while (processed < MAX_ACTIONS_PER_UPDATE && _queue.pop(entry))
|
|||
|
|
{
|
|||
|
|
if (entry.executeTime <= std::chrono::system_clock::now())
|
|||
|
|
{
|
|||
|
|
ExecuteScheduledAction(entry);
|
|||
|
|
++processed;
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// Re-queue if not time yet
|
|||
|
|
_queue.push(entry);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
// Lock-free queue (Boost implementation)
|
|||
|
|
boost::lockfree::queue<ScheduleEntry> _queue{1000};
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Expected Impact:**
|
|||
|
|
- **Contention:** 95%+ reduction (lock-free vs mutex)
|
|||
|
|
- **Throughput:** 2-3x improvement in schedule processing
|
|||
|
|
- **5000 bots:** Eliminates global bottleneck, saves ~1-2% CPU
|
|||
|
|
|
|||
|
|
**Alternative (Windows-Specific - Interlocked Slist):**
|
|||
|
|
```cpp
|
|||
|
|
#include <Windows.h>
|
|||
|
|
|
|||
|
|
class WindowsLockFreeQueue
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
WindowsLockFreeQueue()
|
|||
|
|
{
|
|||
|
|
InitializeSListHead(&_head);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Push(ScheduleEntry const& entry)
|
|||
|
|
{
|
|||
|
|
auto* node = (SLIST_ENTRY*)_aligned_malloc(sizeof(Node), MEMORY_ALLOCATION_ALIGNMENT);
|
|||
|
|
new (&((Node*)node)->entry) ScheduleEntry(entry);
|
|||
|
|
|
|||
|
|
InterlockedPushEntrySList(&_head, node);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool Pop(ScheduleEntry& entry)
|
|||
|
|
{
|
|||
|
|
SLIST_ENTRY* node = InterlockedPopEntrySList(&_head);
|
|||
|
|
if (!node) return false;
|
|||
|
|
|
|||
|
|
entry = ((Node*)node)->entry;
|
|||
|
|
_aligned_free(node);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
struct Node
|
|||
|
|
{
|
|||
|
|
SLIST_ENTRY slistEntry;
|
|||
|
|
ScheduleEntry entry;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
SLIST_HEADER _head;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Windows Interlocked Slist Benefits:**
|
|||
|
|
- **Lowest overhead:** Kernel-optimized lock-free implementation
|
|||
|
|
- **Cache-friendly:** Designed for x86/x64 cache coherency
|
|||
|
|
- **No external dependencies:** Built into Windows SDK
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 5: Testing Strategy
|
|||
|
|
|
|||
|
|
### 5.1 Stress Testing Plan
|
|||
|
|
|
|||
|
|
**Test Environment:**
|
|||
|
|
- **Hardware:** Intel Core i9-12900K (16 cores, 24 threads), 64GB RAM, NVMe SSD
|
|||
|
|
- **OS:** Windows 11 Pro (latest)
|
|||
|
|
- **Database:** MySQL 9.4 on same machine (production would be separate)
|
|||
|
|
- **Build:** Release build with optimizations enabled
|
|||
|
|
|
|||
|
|
**Test Scenarios:**
|
|||
|
|
|
|||
|
|
#### **Scenario 1: Gradual Scaling Test**
|
|||
|
|
```
|
|||
|
|
Goal: Find breaking point and validate linear scaling
|
|||
|
|
|
|||
|
|
1. Spawn 100 bots - Measure CPU, Memory, DB queries
|
|||
|
|
2. Spawn 250 bots - Measure CPU, Memory, DB queries
|
|||
|
|
3. Spawn 500 bots - Measure CPU, Memory, DB queries
|
|||
|
|
4. Spawn 1000 bots - Measure CPU, Memory, DB queries
|
|||
|
|
5. Spawn 2000 bots - Measure CPU, Memory, DB queries
|
|||
|
|
6. Spawn 5000 bots - Measure CPU, Memory, DB queries
|
|||
|
|
|
|||
|
|
Metrics to track:
|
|||
|
|
- Per-bot update time (average, P95, P99)
|
|||
|
|
- Memory usage (private bytes, working set)
|
|||
|
|
- Database connection count
|
|||
|
|
- Mutex contention (lock wait time)
|
|||
|
|
- Thread pool utilization
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Expected Results (Without Optimizations):**
|
|||
|
|
| Bot Count | CPU Usage | Memory | DB Connections | Status |
|
|||
|
|
|-----------|-----------|--------|----------------|--------|
|
|||
|
|
| 100 | 0.6-1.5% | 2 GB | 10-20 | ✓ STABLE |
|
|||
|
|
| 250 | 1.5-3.8% | 5 GB | 20-40 | ✓ STABLE |
|
|||
|
|
| 500 | 3-7.6% | 10 GB | 40-80 | ✓ STABLE |
|
|||
|
|
| 1000 | 6-15.2% | 20 GB | 80-150 | ⚠️ DEGRADED |
|
|||
|
|
| 2000 | 12-30% | 40 GB | 150-300 | ⚠️ DEGRADED |
|
|||
|
|
| 5000 | 30-75% | 101 GB | 300-500 | 🔴 **FAILURE** |
|
|||
|
|
|
|||
|
|
**Expected Results (With All Optimizations):**
|
|||
|
|
| Bot Count | CPU Usage | Memory | DB Connections | Status |
|
|||
|
|
|-----------|-----------|--------|----------------|--------|
|
|||
|
|
| 100 | 0.4-1% | 1.5 GB | 5-10 | ✓ STABLE |
|
|||
|
|
| 250 | 1-2.5% | 3.5 GB | 10-20 | ✓ STABLE |
|
|||
|
|
| 500 | 2-5% | 7 GB | 15-30 | ✓ STABLE |
|
|||
|
|
| 1000 | 4-10% | 14 GB | 25-50 | ✓ STABLE |
|
|||
|
|
| 2000 | 8-20% | 28 GB | 40-80 | ✓ STABLE |
|
|||
|
|
| 5000 | 20-50% | 70 GB | 80-150 | ⚠️ **ACCEPTABLE** |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### **Scenario 2: Combat Stress Test**
|
|||
|
|
```
|
|||
|
|
Goal: Measure performance under maximum AI load (all bots in combat)
|
|||
|
|
|
|||
|
|
1. Spawn 1000 bots
|
|||
|
|
2. Engage all bots in combat simultaneously (raid boss simulation)
|
|||
|
|
3. Measure combat-specific metrics:
|
|||
|
|
- Combat rotation execution time
|
|||
|
|
- Spell cast frequency
|
|||
|
|
- Target selection overhead
|
|||
|
|
- Group coordination overhead
|
|||
|
|
|
|||
|
|
Expected bottlenecks:
|
|||
|
|
- ClassAI::OnCombatUpdate() CPU usage
|
|||
|
|
- Spell availability checks
|
|||
|
|
- Target validation
|
|||
|
|
- Threat calculation
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### **Scenario 3: Login Stress Test**
|
|||
|
|
```
|
|||
|
|
Goal: Measure database performance during mass login
|
|||
|
|
|
|||
|
|
1. Prepare 5000 bot accounts with characters
|
|||
|
|
2. Trigger simultaneous login (worst case)
|
|||
|
|
3. Measure:
|
|||
|
|
- Time to login all 5000 bots
|
|||
|
|
- Database connection pool saturation
|
|||
|
|
- Query execution time (P95, P99)
|
|||
|
|
- Memory allocation rate during login
|
|||
|
|
|
|||
|
|
Expected issues:
|
|||
|
|
- Connection pool exhaustion
|
|||
|
|
- Query holder blocking
|
|||
|
|
- Heap fragmentation from Player object creation
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.2 Performance Monitoring
|
|||
|
|
|
|||
|
|
**Windows-Specific Tools:**
|
|||
|
|
|
|||
|
|
#### **1. Visual Studio Diagnostic Tools (CPU Profiling)**
|
|||
|
|
```
|
|||
|
|
Steps:
|
|||
|
|
1. Open worldserver.exe in Visual Studio 2022
|
|||
|
|
2. Debug → Performance Profiler
|
|||
|
|
3. Select "CPU Usage" and "Memory Usage"
|
|||
|
|
4. Start profiling session
|
|||
|
|
5. Spawn 1000 bots
|
|||
|
|
6. Stop profiling after 60 seconds
|
|||
|
|
7. Analyze hot paths in flamegraph
|
|||
|
|
|
|||
|
|
Expected hotspots:
|
|||
|
|
- BotSession::Update()
|
|||
|
|
- BotAI::UpdateAI()
|
|||
|
|
- ProcessBotPackets()
|
|||
|
|
- Database query callbacks
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### **2. Windows Performance Toolkit (ETL Traces)**
|
|||
|
|
```powershell
|
|||
|
|
# Start ETL trace with CPU and memory providers
|
|||
|
|
wpr.exe -start CPU -start ReferenceSet
|
|||
|
|
|
|||
|
|
# Run test scenario (spawn 5000 bots)
|
|||
|
|
# ...
|
|||
|
|
|
|||
|
|
# Stop trace and save
|
|||
|
|
wpr.exe -stop worldserver_5000bots.etl
|
|||
|
|
|
|||
|
|
# Analyze in Windows Performance Analyzer (WPA)
|
|||
|
|
wpa.exe worldserver_5000bots.etl
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**WPA Analysis Focus:**
|
|||
|
|
- CPU usage per thread
|
|||
|
|
- Context switches (indicates contention)
|
|||
|
|
- Memory allocation patterns
|
|||
|
|
- Heap fragmentation visualization
|
|||
|
|
|
|||
|
|
#### **3. PerfView (Heap Analysis)**
|
|||
|
|
```powershell
|
|||
|
|
# Collect GC and heap events
|
|||
|
|
PerfView.exe /GCOnly /AcceptEULA run worldserver.exe
|
|||
|
|
|
|||
|
|
# After test, analyze heap allocations
|
|||
|
|
PerfView.exe worldserver.exe.gcdump
|
|||
|
|
|
|||
|
|
# Look for:
|
|||
|
|
# - Large object allocations
|
|||
|
|
# - Allocation hotspots (call stacks)
|
|||
|
|
# - Heap fragmentation metrics
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.3 Metrics to Track
|
|||
|
|
|
|||
|
|
**System Metrics (Windows Performance Counters):**
|
|||
|
|
```cpp
|
|||
|
|
// Add to BotPerformanceMonitor.cpp
|
|||
|
|
|
|||
|
|
class WindowsPerformanceCounters
|
|||
|
|
{
|
|||
|
|
public:
|
|||
|
|
void Initialize()
|
|||
|
|
{
|
|||
|
|
// Open process handle
|
|||
|
|
PDH_STATUS status;
|
|||
|
|
status = PdhOpenQuery(nullptr, 0, &_query);
|
|||
|
|
|
|||
|
|
// Add CPU counter
|
|||
|
|
status = PdhAddCounter(_query, L"\\Process(worldserver)\\% Processor Time", 0, &_cpuCounter);
|
|||
|
|
|
|||
|
|
// Add memory counters
|
|||
|
|
status = PdhAddCounter(_query, L"\\Process(worldserver)\\Private Bytes", 0, &_memoryCounter);
|
|||
|
|
status = PdhAddCounter(_query, L"\\Process(worldserver)\\Working Set", 0, &_workingSetCounter);
|
|||
|
|
|
|||
|
|
// Add thread count
|
|||
|
|
status = PdhAddCounter(_query, L"\\Process(worldserver)\\Thread Count", 0, &_threadCounter);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Update()
|
|||
|
|
{
|
|||
|
|
PdhCollectQueryData(_query);
|
|||
|
|
|
|||
|
|
PDH_FMT_COUNTERVALUE value;
|
|||
|
|
|
|||
|
|
PdhGetFormattedCounterValue(_cpuCounter, PDH_FMT_DOUBLE, nullptr, &value);
|
|||
|
|
_currentCpuUsage = value.doubleValue;
|
|||
|
|
|
|||
|
|
PdhGetFormattedCounterValue(_memoryCounter, PDH_FMT_LARGE, nullptr, &value);
|
|||
|
|
_currentMemoryUsage = value.largeValue;
|
|||
|
|
|
|||
|
|
PdhGetFormattedCounterValue(_workingSetCounter, PDH_FMT_LARGE, nullptr, &value);
|
|||
|
|
_currentWorkingSet = value.largeValue;
|
|||
|
|
|
|||
|
|
PdhGetFormattedCounterValue(_threadCounter, PDH_FMT_LONG, nullptr, &value);
|
|||
|
|
_currentThreadCount = value.longValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
PDH_HQUERY _query;
|
|||
|
|
PDH_HCOUNTER _cpuCounter, _memoryCounter, _workingSetCounter, _threadCounter;
|
|||
|
|
double _currentCpuUsage;
|
|||
|
|
LONGLONG _currentMemoryUsage, _currentWorkingSet;
|
|||
|
|
LONG _currentThreadCount;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Per-Bot Metrics:**
|
|||
|
|
- Update time (microseconds)
|
|||
|
|
- Memory usage (bytes)
|
|||
|
|
- AI decision time (microseconds)
|
|||
|
|
- Combat rotation time (microseconds)
|
|||
|
|
- Movement calculation time (microseconds)
|
|||
|
|
- Database query time (microseconds)
|
|||
|
|
|
|||
|
|
**Aggregate Metrics:**
|
|||
|
|
- Total bots active
|
|||
|
|
- Bots in combat
|
|||
|
|
- Bots following
|
|||
|
|
- Bots idle
|
|||
|
|
- Average FPS (server tick rate)
|
|||
|
|
- Database connection count
|
|||
|
|
- Thread pool utilization
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 6: Implementation Roadmap
|
|||
|
|
|
|||
|
|
### Priority 1 - CRITICAL (Implement First)
|
|||
|
|
|
|||
|
|
**Week 1-2: Memory Optimizations**
|
|||
|
|
- [ ] **Task 1.1:** Implement WorldPacket object pool (Optimization #1)
|
|||
|
|
- File: `C:\TrinityBots\TrinityCore\src\modules\Playerbot\Session\BotSession.cpp`
|
|||
|
|
- Estimated time: 2 hours
|
|||
|
|
- Expected impact: 50% reduction in packet allocations
|
|||
|
|
|
|||
|
|
- [ ] **Task 1.2:** Implement ring buffer for packet queues (Optimization #2.C)
|
|||
|
|
- File: `C:\TrinityBots\TrinityCore\src\modules\Playerbot\Session\BotSession.h`
|
|||
|
|
- Estimated time: 1 hour
|
|||
|
|
- Expected impact: 7.5 MB memory savings
|
|||
|
|
|
|||
|
|
- [ ] **Task 1.3:** Implement lazy inventory loading (Optimization #2.B)
|
|||
|
|
- File: `C:\TrinityBots\TrinityCore\src\modules\Playerbot\Session\BotSession.cpp`
|
|||
|
|
- Estimated time: 3 hours
|
|||
|
|
- Expected impact: 7.8 MB memory savings
|
|||
|
|
|
|||
|
|
**Week 3-4: CPU Optimizations**
|
|||
|
|
- [ ] **Task 2.1:** Implement batch bot update system (Optimization #3)
|
|||
|
|
- File: New file `C:\TrinityBots\TrinityCore\src\modules\Playerbot\Session\BotUpdateBatcher.cpp`
|
|||
|
|
- Estimated time: 4 hours
|
|||
|
|
- Expected impact: 20-40% CPU reduction
|
|||
|
|
|
|||
|
|
- [ ] **Task 2.2:** Reduce BotSession::Update validation overhead (Optimization #4)
|
|||
|
|
- File: `C:\TrinityBots\TrinityCore\src\modules\Playerbot\Session\BotSession.cpp`
|
|||
|
|
- Estimated time: 30 minutes
|
|||
|
|
- Expected impact: 10-15% CPU reduction
|
|||
|
|
|
|||
|
|
- [ ] **Task 2.3:** Implement cached spell availability (Optimization #5)
|
|||
|
|
- Files: All ClassAI implementation files
|
|||
|
|
- Estimated time: 8 hours (1-2 hours per class × 12 classes)
|
|||
|
|
- Expected impact: 30-50% combat rotation CPU reduction
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Priority 2 - HIGH (Implement Second)
|
|||
|
|
|
|||
|
|
**Week 5-6: Database Optimizations**
|
|||
|
|
- [ ] **Task 3.1:** Implement batch character loader (Optimization #6)
|
|||
|
|
- File: New file `C:\TrinityBots\TrinityCore\src\modules\Playerbot\Session\BatchCharacterLoader.cpp`
|
|||
|
|
- Estimated time: 6 hours
|
|||
|
|
- Expected impact: 96% query reduction, 95% faster login
|
|||
|
|
|
|||
|
|
- [ ] **Task 3.2:** Implement spell data cache (Optimization #7.A)
|
|||
|
|
- File: New file `C:\TrinityBots\TrinityCore\src\modules\Playerbot\Database\SpellDataCache.cpp`
|
|||
|
|
- Estimated time: 3 hours
|
|||
|
|
- Expected impact: 90% spell query reduction
|
|||
|
|
|
|||
|
|
- [ ] **Task 3.3:** Implement quest data cache (Optimization #7.B)
|
|||
|
|
- File: New file `C:\TrinityBots\TrinityCore\src\modules\Playerbot\Database\QuestDataCache.cpp`
|
|||
|
|
- Estimated time: 3 hours
|
|||
|
|
- Expected impact: 90% quest query reduction
|
|||
|
|
|
|||
|
|
**Week 7-8: Lock-Free Optimizations**
|
|||
|
|
- [ ] **Task 4.1:** Replace scheduler mutex with Windows Interlocked Slist (Optimization #8)
|
|||
|
|
- File: `C:\TrinityBots\TrinityCore\src\modules\Playerbot\Lifecycle\BotScheduler.cpp`
|
|||
|
|
- Estimated time: 4 hours
|
|||
|
|
- Expected impact: 95% contention reduction
|
|||
|
|
|
|||
|
|
- [ ] **Task 4.2:** Audit and replace other mutex hotspots with lock-free alternatives
|
|||
|
|
- Files: Various
|
|||
|
|
- Estimated time: 6 hours
|
|||
|
|
- Expected impact: 10-20% overall CPU reduction
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Priority 3 - MEDIUM (Implement Third)
|
|||
|
|
|
|||
|
|
**Week 9-10: Additional Optimizations**
|
|||
|
|
- [ ] **Task 5.1:** Implement shared spell book (Optimization #2.A)
|
|||
|
|
- File: New file `C:\TrinityBots\TrinityCore\src\modules\Playerbot\AI\SharedSpellBook.cpp`
|
|||
|
|
- Estimated time: 3 hours
|
|||
|
|
- Expected impact: 4.5 MB memory savings
|
|||
|
|
|
|||
|
|
- [ ] **Task 5.2:** Profile and optimize movement calculations
|
|||
|
|
- File: `C:\TrinityBots\TrinityCore\src\modules\Playerbot\Movement\LeaderFollowBehavior.cpp`
|
|||
|
|
- Estimated time: 4 hours
|
|||
|
|
- Expected impact: 20-30% movement CPU reduction
|
|||
|
|
|
|||
|
|
- [ ] **Task 5.3:** Reduce logging overhead in hot paths
|
|||
|
|
- Files: Various
|
|||
|
|
- Estimated time: 2 hours
|
|||
|
|
- Expected impact: 5-15% CPU reduction (logging disabled)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Testing Milestones
|
|||
|
|
|
|||
|
|
**After Priority 1 (Week 4):**
|
|||
|
|
- [ ] Run gradual scaling test (100, 250, 500, 1000 bots)
|
|||
|
|
- [ ] Validate memory usage < 15 GB for 1000 bots
|
|||
|
|
- [ ] Validate CPU usage < 10% for 1000 bots
|
|||
|
|
|
|||
|
|
**After Priority 2 (Week 8):**
|
|||
|
|
- [ ] Run login stress test (5000 bots)
|
|||
|
|
- [ ] Validate login time < 60 seconds for 5000 bots
|
|||
|
|
- [ ] Run gradual scaling test up to 2000 bots
|
|||
|
|
|
|||
|
|
**After Priority 3 (Week 10):**
|
|||
|
|
- [ ] Run full 5000 bot stress test
|
|||
|
|
- [ ] Target metrics:
|
|||
|
|
- CPU: < 50% total (across all cores)
|
|||
|
|
- Memory: < 80 GB
|
|||
|
|
- Login time: < 60 seconds
|
|||
|
|
- Per-bot update time: < 0.1ms average
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Conclusion
|
|||
|
|
|
|||
|
|
### Current State Assessment
|
|||
|
|
The TrinityCore Playerbot module demonstrates **good architectural design** with proper use of modern C++ patterns (smart pointers, RAII, move semantics). However, the current implementation is **optimized for correctness and safety** rather than extreme scalability.
|
|||
|
|
|
|||
|
|
**Estimated Current Capacity:** 500-800 concurrent bots
|
|||
|
|
|
|||
|
|
### Path to 5000 Bots
|
|||
|
|
|
|||
|
|
To achieve the 5000 concurrent bot target, the following changes are **mandatory**:
|
|||
|
|
|
|||
|
|
**CRITICAL (Must Have):**
|
|||
|
|
1. Object pooling for packets (Optimization #1)
|
|||
|
|
2. Batch bot updates with thread pool (Optimization #3)
|
|||
|
|
3. Batch character loading (Optimization #6)
|
|||
|
|
4. Cached spell availability (Optimization #5)
|
|||
|
|
|
|||
|
|
**HIGH (Should Have):**
|
|||
|
|
5. Lock-free scheduler (Optimization #8)
|
|||
|
|
6. Validation overhead reduction (Optimization #4)
|
|||
|
|
7. Spell/quest data caching (Optimization #7)
|
|||
|
|
|
|||
|
|
**MEDIUM (Nice to Have):**
|
|||
|
|
8. Ring buffer packet queues (Optimization #2.C)
|
|||
|
|
9. Shared spell book (Optimization #2.A)
|
|||
|
|
10. Lazy inventory loading (Optimization #2.B)
|
|||
|
|
|
|||
|
|
### Projected Results (With All Optimizations)
|
|||
|
|
|
|||
|
|
| Metric | Current (5000 bots) | Optimized (5000 bots) | Target | Status |
|
|||
|
|
|--------|---------------------|----------------------|--------|--------|
|
|||
|
|
| **Memory** | 101-116 GB | 60-75 GB | < 50 GB | ⚠️ **CLOSE** |
|
|||
|
|
| **CPU** | 30-75% | 20-40% | < 50% | ✓ **ACHIEVABLE** |
|
|||
|
|
| **Login Time** | 250-1000s | 30-60s | < 60s | ✓ **ACHIEVABLE** |
|
|||
|
|
| **Per-Bot Update** | 2.1-5.2 µs | 0.8-2.0 µs | < 0.1ms | ✓ **EXCEEDS** |
|
|||
|
|
|
|||
|
|
### Risk Assessment
|
|||
|
|
|
|||
|
|
**Memory Target (50GB):** ⚠️ **CHALLENGING**
|
|||
|
|
- Optimizations reduce memory to 60-75 GB
|
|||
|
|
- To reach 50 GB target, consider:
|
|||
|
|
- Reducing Player object size (requires TrinityCore core modifications)
|
|||
|
|
- More aggressive lazy loading
|
|||
|
|
- Unloading inactive bot data to database
|
|||
|
|
|
|||
|
|
**CPU Target (500%):** ✓ **ACHIEVABLE**
|
|||
|
|
- Optimizations should bring CPU to 20-40% (well under 50% target)
|
|||
|
|
- Thread pool ensures good core utilization
|
|||
|
|
- Lock-free structures minimize contention
|
|||
|
|
|
|||
|
|
**Overall Feasibility:** ✓ **ACHIEVABLE WITH EFFORT**
|
|||
|
|
- 8-10 weeks of implementation
|
|||
|
|
- Requires Windows-specific optimizations
|
|||
|
|
- May need to compromise on memory target (60-75 GB instead of 50 GB)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Appendix A: Windows Profiling Commands
|
|||
|
|
|
|||
|
|
### Visual Studio 2022 Performance Profiler
|
|||
|
|
```cmd
|
|||
|
|
REM Command-line profiling
|
|||
|
|
vsperf.exe /launch:worldserver.exe /args:"--config playerbot_test.conf"
|
|||
|
|
vsperf.exe /shutdown
|
|||
|
|
|
|||
|
|
REM Analyze results
|
|||
|
|
vsperfcmd.exe /report worldserver.exe.vsp
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Windows Performance Toolkit (WPR/WPA)
|
|||
|
|
```cmd
|
|||
|
|
REM Start CPU profiling
|
|||
|
|
wpr.exe -start CPU -start ReferenceSet
|
|||
|
|
|
|||
|
|
REM Run worldserver with 5000 bots...
|
|||
|
|
REM (wait for test to complete)
|
|||
|
|
|
|||
|
|
REM Stop profiling
|
|||
|
|
wpr.exe -stop worldserver_5000bots.etl
|
|||
|
|
|
|||
|
|
REM Open in Windows Performance Analyzer
|
|||
|
|
wpa.exe worldserver_5000bots.etl
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Dr. Memory (Memory Leak Detection)
|
|||
|
|
```cmd
|
|||
|
|
REM Download Dr. Memory for Windows
|
|||
|
|
drmemory.exe -logdir . -- worldserver.exe --config playerbot_test.conf
|
|||
|
|
|
|||
|
|
REM Run test scenario, then analyze results
|
|||
|
|
drmemory.exe -results worldserver.exe.*.txt
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### PerfView (Heap Analysis)
|
|||
|
|
```cmd
|
|||
|
|
REM Collect heap allocations
|
|||
|
|
PerfView.exe /GCOnly /AcceptEULA run worldserver.exe --config playerbot_test.conf
|
|||
|
|
|
|||
|
|
REM Analyze allocations
|
|||
|
|
PerfView.exe worldserver.exe.gcdump
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Appendix B: Performance Benchmark Template
|
|||
|
|
|
|||
|
|
**File:** `C:\TrinityBots\TrinityCore\src\modules\Playerbot\Tests\PerformanceBenchmark.cpp`
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
#include "PerformanceBenchmark.h"
|
|||
|
|
#include "BotPerformanceMonitor.h"
|
|||
|
|
#include "BotSpawner.h"
|
|||
|
|
#include "Log.h"
|
|||
|
|
#include <chrono>
|
|||
|
|
#include <fstream>
|
|||
|
|
|
|||
|
|
namespace Playerbot
|
|||
|
|
{
|
|||
|
|
|
|||
|
|
void PerformanceBenchmark::RunScalingTest()
|
|||
|
|
{
|
|||
|
|
TC_LOG_INFO("playerbot.test", "Starting scaling test...");
|
|||
|
|
|
|||
|
|
std::ofstream csv("scaling_test_results.csv");
|
|||
|
|
csv << "BotCount,CpuPercent,MemoryMB,AvgUpdateTimeUs,P95UpdateTimeUs,P99UpdateTimeUs\\n";
|
|||
|
|
|
|||
|
|
std::vector<uint32> botCounts = {100, 250, 500, 1000, 2000, 5000};
|
|||
|
|
|
|||
|
|
for (uint32 count : botCounts)
|
|||
|
|
{
|
|||
|
|
TC_LOG_INFO("playerbot.test", "Testing with {} bots...", count);
|
|||
|
|
|
|||
|
|
// Spawn bots
|
|||
|
|
SpawnBots(count);
|
|||
|
|
|
|||
|
|
// Run for 60 seconds
|
|||
|
|
auto startTime = std::chrono::steady_clock::now();
|
|||
|
|
while (std::chrono::duration_cast<std::chrono::seconds>(
|
|||
|
|
std::chrono::steady_clock::now() - startTime).count() < 60)
|
|||
|
|
{
|
|||
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Collect metrics
|
|||
|
|
auto stats = sPerfMonitor.GetStatistics(MetricType::AI_DECISION_TIME);
|
|||
|
|
double cpuPercent = GetCpuUsage();
|
|||
|
|
double memoryMB = GetMemoryUsageMB();
|
|||
|
|
|
|||
|
|
// Write to CSV
|
|||
|
|
csv << count << ","
|
|||
|
|
<< cpuPercent << ","
|
|||
|
|
<< memoryMB << ","
|
|||
|
|
<< stats.GetAverage() << ","
|
|||
|
|
<< stats.p95.load() << ","
|
|||
|
|
<< stats.p99.load() << "\\n";
|
|||
|
|
|
|||
|
|
// Cleanup
|
|||
|
|
DespawnAllBots();
|
|||
|
|
std::this_thread::sleep_for(std::chrono::seconds(5)); // Recovery time
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
csv.close();
|
|||
|
|
TC_LOG_INFO("playerbot.test", "Scaling test complete. Results in scaling_test_results.csv");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
double PerformanceBenchmark::GetCpuUsage()
|
|||
|
|
{
|
|||
|
|
// Windows-specific CPU usage measurement
|
|||
|
|
static ULARGE_INTEGER lastCPU, lastSysCPU, lastUserCPU;
|
|||
|
|
static int numProcessors = -1;
|
|||
|
|
static HANDLE self = GetCurrentProcess();
|
|||
|
|
|
|||
|
|
if (numProcessors == -1)
|
|||
|
|
{
|
|||
|
|
SYSTEM_INFO sysInfo;
|
|||
|
|
GetSystemInfo(&sysInfo);
|
|||
|
|
numProcessors = sysInfo.dwNumberOfProcessors;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ULARGE_INTEGER now, sys, user;
|
|||
|
|
FILETIME ftime, fsys, fuser;
|
|||
|
|
|
|||
|
|
GetSystemTimeAsFileTime(&ftime);
|
|||
|
|
memcpy(&now, &ftime, sizeof(FILETIME));
|
|||
|
|
|
|||
|
|
GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser);
|
|||
|
|
memcpy(&sys, &fsys, sizeof(FILETIME));
|
|||
|
|
memcpy(&user, &fuser, sizeof(FILETIME));
|
|||
|
|
|
|||
|
|
double percent = 0.0;
|
|||
|
|
if (lastCPU.QuadPart != 0)
|
|||
|
|
{
|
|||
|
|
percent = (sys.QuadPart - lastSysCPU.QuadPart) + (user.QuadPart - lastUserCPU.QuadPart);
|
|||
|
|
percent /= (now.QuadPart - lastCPU.QuadPart);
|
|||
|
|
percent /= numProcessors;
|
|||
|
|
percent *= 100;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
lastCPU = now;
|
|||
|
|
lastUserCPU = user;
|
|||
|
|
lastSysCPU = sys;
|
|||
|
|
|
|||
|
|
return percent;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
double PerformanceBenchmark::GetMemoryUsageMB()
|
|||
|
|
{
|
|||
|
|
PROCESS_MEMORY_COUNTERS_EX pmc;
|
|||
|
|
GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc));
|
|||
|
|
return pmc.PrivateUsage / (1024.0 * 1024.0); // Convert to MB
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} // namespace Playerbot
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**End of Report**
|