Stats and Metrics System Guide
July 14, 2026 ยท View on GitHub
Overview
The SwarmBot uses a unified statistics system to track all aspects of bot performance, resource usage, and operational metrics. Statistics are output to the console in JSON format for the graphite exporter to parse, and are also stored in Memory.stats for backward compatibility.
Architecture
The stats system is implemented in src/core/unifiedStats.ts and provides:
- Console output: Stats are output as JSON to console for real-time export to Grafana
- Zero-cost abstraction: Stats collection can be disabled without code changes
- Clean data structure: No flattening - raw object hierarchy preserved
- Automatic collection: Most stats are collected automatically during finalization
- Configurable: Smoothing factors, logging intervals, and tracking features are configurable
Stats Export Modes
The system supports two export modes:
1. Console Output (Primary Method)
Stats are output to console as a single JSON object per tick containing the entire Memory.stats structure:
{"type":"stats","data":{"tick":12345,"cpu":{"used":15.5},"empire":{"rooms":3}}}
The graphite exporter's console listener and scripts/live-cpu-profile.mjs --source console subscribe to the Screeps console stream and:
- Parse the JSON object
- Flatten or summarize the nested structure (same stats shape as memory mode)
- Send metrics to Grafana Cloud or write bounded diagnostics artifacts in real-time
This is the recommended method as it:
- Provides real-time stats updates (every tick)
- Doesn't require polling the Screeps API
- Respects API rate limits (no polling overhead)
- Has lower latency (<1 second vs polling interval)
- Uses the same flattening logic as memory mode
2. Memory Polling (Backward Compatibility)
Stats are also written to Memory.stats in a nested object structure. The graphite exporter or scripts/live-cpu-profile.mjs --source memory can poll this via the Screeps API, but this method:
- Has API rate limits (1440 requests/day for
/api/user/memory) - Has higher latency (polling interval)
- May miss rapid changes between polls
Statistics Categories
1. Empire Statistics
Empire-level metrics provide a bird's-eye view of your entire operation.
Location: Memory.stats.empire
Fields:
ownedRooms(number): Total number of owned roomstotalCreeps(number): Total number of living creepstotalStorageEnergy(number): Sum of energy in all storage structuresgclProgress(number): Current GCL progress pointsgcl(number): Current GCL levelgpl(number): Current GPL levelcpuUsed(number): CPU used this tickcpuLimit(number): CPU limit for this accountcpuBucket(number): Current CPU bucket levelheapUsed(number): Heap memory used (MB)credits(number): Total credits availablerooms(string[]): List of owned room names
Usage:
const stats = statsManager.getStats();
console.log(`Empire: ${stats.empire.ownedRooms} rooms, ${stats.empire.totalCreeps} creeps`);
console.log(`CPU: ${stats.empire.cpuUsed.toFixed(2)}/${stats.empire.cpuLimit}`);
console.log(`Bucket: ${stats.empire.cpuBucket}/10000`);
When Updated: Automatically during updateEmpireStats() call, typically once per tick.
2. Subsystem Statistics
Track CPU usage and call frequency for each major subsystem (e.g., rooms, creeps, spawns, defense, market).
Location: Memory.stats.subsystems
Structure:
{
[subsystemName: string]: {
avgCpu: number; // Exponentially smoothed average CPU
peakCpu: number; // Maximum CPU seen
calls: number; // Number of calls this tick
samples: number; // Total samples collected
}
}
Usage:
// Record subsystem execution
const startCpu = Game.cpu.getUsed();
runRoomLogic(room);
const cpuUsed = Game.cpu.getUsed() - startCpu;
statsManager.recordSubsystem("RoomLogic", cpuUsed, 1);
When Updated: Manually via recordSubsystem() calls in your code.
3. Role Statistics (Enhanced)
Aggregated statistics for each creep role, including spawning/idle/active counts and body part totals.
Location: Memory.stats.roles
Structure:
{
[roleName: string]: {
count: number; // Total creeps with this role
avgCpu: number; // Average CPU per creep
peakCpu: number; // Peak CPU for any creep
calls: number; // Total calls this tick
samples: number; // Total samples
spawningCount: number; // Number currently spawning
idleCount: number; // Number idle/not working
activeCount: number; // Number actively working
avgTicksToLive: number; // Average TTL across all creeps
totalBodyParts: number; // Total body parts across role
}
}
Usage:
// Manually record role stats with enhanced metrics
statsManager.recordRole("harvester", 5, 0.8, 5, {
spawningCount: 1,
idleCount: 1,
activeCount: 3,
avgTicksToLive: 1400,
totalBodyParts: 15
});
// Or let finalizeTick() automatically calculate from all creeps
statsManager.finalizeTick(); // Automatically aggregates role stats
When Updated:
- Manually via
recordRole()for CPU tracking - Automatically during
finalizeTick()for counts and metrics
4. Per-Creep Statistics
Individual statistics for every living creep, useful for debugging and detailed analysis.
Location: Memory.stats.creeps
Structure:
{
[creepName: string]: {
name: string; // Creep name
role: string; // Creep role
homeRoom: string; // Home room
currentRoom: string; // Current location
cpu: number; // CPU used this tick
action: string; // Current action/state
ticksToLive: number; // Remaining lifespan
hits: number; // Current hits
hitsMax: number; // Maximum hits
bodyParts: number; // Total body parts
fatigue: number; // Current fatigue
actionsThisTick: number; // Actions performed
}
}
Usage:
// Record individual creep stats
const startCpu = Game.cpu.getUsed();
runCreepLogic(creep);
const cpuUsed = Game.cpu.getUsed() - startCpu;
statsManager.recordCreep(creep, cpuUsed, "harvesting", 2);
// Access creep stats
const stats = statsManager.getStats();
const harvester1Stats = stats.creeps["harvester1"];
console.log(`${harvester1Stats.name} in ${harvester1Stats.currentRoom}: ${harvester1Stats.action}`);
When Updated:
- Manually via
recordCreep()for CPU tracking - Automatically during
finalizeTick()for basic metrics
5. Room Statistics
Per-room operational metrics including energy, creeps, hostiles, and danger levels.
Location: Memory.stats.rooms
Structure:
{
[roomName: string]: {
name: string; // Room name
rcl: number; // Controller level
energyAvailable: number; // Energy currently available
energyCapacity: number; // Max energy capacity
storageEnergy: number; // Energy in storage
creepCount: number; // Creeps in this room
hostileCount: number; // Hostile creeps detected
avgCpu: number; // Average CPU for room
peakCpu: number; // Peak CPU for room
controllerProgress: number; // Controller progress
controllerProgressTotal: number; // Total progress needed
energyHarvested: number; // Energy harvested (rolling avg)
damageReceived: number; // Damage from hostiles (rolling avg)
danger: number; // Danger level (0-3)
taskBoard?: {
tasks: number; // Total task records
open_tasks: number; // Unassigned task records
assigned_tasks: number; // Task records with active assignment
reservations: number; // Creep reservation records
stale_reservations: number; // Reservations whose creeps/tasks are stale
blocked_reservations: number; // Reservations blocked by invalid targets or state
amount: number; // Total requested task amount
reserved_amount: number; // Amount reserved by creeps
remaining_amount: number; // Unreserved amount still needing work
delivery_amount: number; // Requested amount for delivery task types
delivery_reserved_amount: number;
delivery_remaining_amount: number;
critical_delivery_remaining_amount: number; // High-priority delivery remainder
by_type: Record<string, {
tasks: number;
open_tasks: number;
assigned_tasks: number;
reservations: number;
amount: number;
reserved_amount: number;
remaining_amount: number;
}>;
};
}
}
Usage:
// Record room stats
statsManager.recordRoom(room, 0.5, 0.7, {
energyHarvested: 150,
damageReceived: 0,
danger: 0
});
When Updated: Manually via recordRoom() calls, typically in room manager. taskBoard is sampled from Memory.creepTaskBoard.rooms[roomName] when room stats are recorded, then published to Memory.stats during stats finalization.
Task-board amount telemetry
Room task-board stats expose both count backlog and amount backlog under Memory.stats.rooms[roomName].taskBoard:
| Field | Meaning |
|---|---|
tasks, open_tasks, assigned_tasks | Task counts. Use these to detect scheduler backlog by record count. |
reservations, stale_reservations, blocked_reservations | Reservation health by count. |
amount | Sum of requested task amounts. Missing, invalid, or negative amounts count as 0. |
reserved_amount | Sum reserved by creeps for those tasks. |
remaining_amount | Unreserved amount left, computed per task as max(0, amount - reserved_amount). |
delivery_amount, delivery_reserved_amount, delivery_remaining_amount | Amount totals restricted to delivery work (refillSpawn, refillExtension, refillTower, fillTerminalEnergy, storeEnergy). |
critical_delivery_remaining_amount | Remaining amount for high-priority delivery tasks only; this is the urgent refill pressure signal. |
by_type | Per task-type counts and amount totals for finding which task type dominates backlog. |
Example:
{
"taskBoard": {
"tasks": 5,
"open_tasks": 4,
"assigned_tasks": 1,
"reservations": 1,
"amount": 2300,
"reserved_amount": 100,
"remaining_amount": 2200,
"delivery_amount": 2300,
"delivery_reserved_amount": 100,
"delivery_remaining_amount": 2200,
"critical_delivery_remaining_amount": 200,
"by_type": {
"storeEnergy": {
"tasks": 2,
"open_tasks": 2,
"assigned_tasks": 0,
"reservations": 0,
"amount": 2000,
"reserved_amount": 0,
"remaining_amount": 2000
},
"refillSpawn": {
"tasks": 3,
"open_tasks": 2,
"assigned_tasks": 1,
"reservations": 1,
"amount": 300,
"reserved_amount": 100,
"remaining_amount": 200
}
}
}
}
Interpretation: storeEnergy can dominate remaining_amount because it represents bulk storage logistics, while refillSpawn/refillExtension tasks can dominate immediate survival risk. Use critical_delivery_remaining_amount and by_type.refillSpawn.remaining_amount to separate urgent refill pressure from low-urgency bulk hauling.
Defense-assist diagnostics
Helper rooms publish compact defense-assist state under:
Memory.stats.rooms[helperRoom].defense.assist[targetRoom]
Key fields:
requested,queued,spawning,staged,moving,arrived:{ guard, ranger, healer, total }body_costandaffordable: planned/queued helper-room body cost and current affordability by roleassigned_power: aggregate queued/live combat power assigned by this helper roomtarget_scoreandparity_percent: visible threat parity when target vision existsreleased.by_reason,last_reason,last_released_at: why staged creeps started travelingblock_reason:none,unaffordable,waiting-for-parity,waiting-for-quorum, orno-local-assist
Hard-threat staging pools ready, unreleased military members across all safe helper rooms assigned to the same target. Reaching target-wide parity or quorum atomically releases the ready cohort, preventing distributed waves from waiting indefinitely in any single helper room while excluding members whose home room is under attack.
Use this during post-deploy defense checks to distinguish queued, unaffordable, staged, traveling, and released defense assists without polling raw Memory.defenseRequests or creep memory.
6. Pheromone Statistics
Pheromone levels and intent for each room, showing the swarm's emergent coordination state.
Location: Memory.stats.pheromones
Structure:
{
[roomName: string]: {
room: string; // Room name
expand: number; // Expansion pheromone (0-100)
harvest: number; // Harvesting pheromone
build: number; // Building pheromone
upgrade: number; // Upgrading pheromone
defense: number; // Defense pheromone
war: number; // War pheromone
siege: number; // Siege pheromone
logistics: number; // Logistics pheromone
dominant: string | null; // Dominant pheromone type
intent: string; // Room intent/posture
}
}
Usage:
// Record pheromone stats
const swarm = memoryManager.getSwarmState(room.name);
statsManager.recordPheromones(
room.name,
swarm.pheromones,
swarm.posture,
pheromoneManager.getDominantPheromone(swarm.pheromones)
);
When Updated: Manually via recordPheromones(), typically after pheromone updates.
See Also: PHEROMONES_GUIDE.md for detailed pheromone system documentation.
7. Native Calls Statistics
Track how many times native Screeps API methods are called per tick.
Location: Memory.stats.nativeCalls
Tracked Calls:
pathfinderSearch: PathFinder.search() callsmoveTo: creep.moveTo() callsmove: creep.move() callsharvest: creep.harvest() callstransfer: creep.transfer() callswithdraw: creep.withdraw() callsbuild: creep.build() callsrepair: creep.repair() callsupgradeController: creep.upgradeController() callsattack: creep.attack() callsrangedAttack: creep.rangedAttack() callsheal: creep.heal() callsdismantle: creep.dismantle() callssay: creep.say() callstotal: Sum of all calls
Usage:
// Recording is typically done automatically via wrapper functions
// But can be done manually:
statsManager.recordNativeCall("harvest");
statsManager.recordNativeCall("transfer");
// Access stats
const stats = statsManager.getStats();
console.log(`Total native calls: ${stats.nativeCalls.total}`);
console.log(`PathFinder calls: ${stats.nativeCalls.pathfinderSearch}`);
When Updated: Manually via recordNativeCall() or via automatic tracking wrappers.
8. Kernel Process Statistics
Statistics for each registered kernel process, tracking CPU usage, execution frequency, and errors.
Location: Memory.stats.processes
Structure:
{
[processId: string]: {
id: string; // Process ID
name: string; // Display name
priority: number; // Process priority
frequency: string; // Frequency tier (high/medium/low)
state: string; // Current state
totalCpu: number; // Total CPU used
runCount: number; // Times executed
avgCpu: number; // Average CPU per run
maxCpu: number; // Peak CPU usage
lastRunTick: number; // Last execution tick
skippedCount: number; // Times skipped due to CPU
errorCount: number; // Number of errors
cpuBudget: number; // Allocated CPU budget
minBucket: number; // Minimum bucket to run
}
}
Usage:
// Collect all process stats from kernel
import { kernel } from "./core/kernel";
statsManager.collectProcessStats(kernel.getProcesses());
// Or record individual process
statsManager.recordProcess({
id: "roomRunner_W1N1",
name: "Room Runner: W1N1",
priority: 75,
frequency: "high",
state: "idle",
cpuBudget: 0.1,
minBucket: 1000,
stats: {
totalCpu: 5.5,
runCount: 100,
avgCpu: 0.055,
maxCpu: 0.15,
lastRunTick: 12345,
skippedCount: 5,
errorCount: 0
}
});
When Updated: Via collectProcessStats() or recordProcess(), typically at end of tick.
Configuration
The stats system can be configured when creating the StatsManager:
import { StatsManager } from "./core/stats";
const statsManager = new StatsManager({
enabled: true, // Enable/disable stats collection
smoothingFactor: 0.1, // EMA smoothing (0.0-1.0)
trackNativeCalls: true, // Track native API calls
logInterval: 100 // Log summary every N ticks (0 = never)
});
Key Configuration Options
- enabled: Set to
falseto completely disable stats collection (zero CPU overhead) - smoothingFactor: Controls exponential moving average smoothing (lower = slower adaptation)
- trackNativeCalls: Track native Screeps API call frequency
- logInterval: How often to log a stats summary to console
Usage Patterns
Pattern 1: Basic Subsystem Tracking
function runMySubsystem() {
const startCpu = Game.cpu.getUsed();
// Your subsystem logic here
doWork();
const cpuUsed = Game.cpu.getUsed() - startCpu;
statsManager.recordSubsystem("MySubsystem", cpuUsed, 1);
}
Pattern 2: Role-Based Tracking
function runAllCreeps() {
const roleGroups = _.groupBy(Game.creeps, c => c.memory.role);
for (const [role, creeps] of Object.entries(roleGroups)) {
const startCpu = Game.cpu.getUsed();
for (const creep of creeps) {
runCreepRole(creep);
}
const cpuUsed = Game.cpu.getUsed() - startCpu;
statsManager.recordRole(role, creeps.length, cpuUsed / creeps.length, creeps.length);
}
}
Pattern 3: Detailed Per-Creep Tracking
function runCreep(creep) {
const startCpu = Game.cpu.getUsed();
const action = runCreepLogic(creep);
const cpuUsed = Game.cpu.getUsed() - startCpu;
statsManager.recordCreep(creep, cpuUsed, action, 1);
}
Pattern 4: Automatic Collection
// In your main loop
export function loop() {
// Initialize stats for new tick
statsManager.reset(); // Optional: only if you want to clear old data
// Run your bot logic
runRooms();
runCreeps();
runSpawns();
// Update empire stats
statsManager.updateEmpireStats();
// Collect kernel process stats
statsManager.collectProcessStats(kernel.getProcesses());
// Finalize tick - automatically collects creep stats, role stats, etc.
statsManager.finalizeTick();
}
Local Stats Consumption
Stats are available in Memory.stats and as console output for local inspection and private-server validation. The repository no longer ships Grafana Graphite or Loki exporter packages.
Performance Considerations
CPU Cost
- Enabled: ~0.5-1 CPU per tick for full stats collection
- Disabled: Near-zero CPU (all methods return early)
- Selective tracking: Disable
trackNativeCallsto save ~0.1-0.2 CPU
Memory Cost
- Stats typically use 10-50 KB depending on empire size
- Per-creep stats: ~200 bytes per creep
- Per-process stats: ~150 bytes per process
- Historical data is NOT stored (only current tick)
Optimization Tips
- Disable in production: Set
enabled: falsewhen stats aren't needed - Increase log interval: Set higher values (e.g., 500) or 0 to reduce logging
- Selective tracking: Only track what you need (disable native calls if not used)
- Sample rates: Implement sampling for high-frequency operations
API Reference
StatsManager Methods
recordSubsystem(name: string, cpu: number, calls?: number): void
Record CPU usage for a subsystem.
recordRole(role: string, count: number, cpu: number, calls?: number, metrics?: {...}): void
Record statistics for a role with optional enhanced metrics.
recordRoom(room: Room, avgCpu: number, peakCpu: number, metrics?: {...}): void
Record room statistics with optional metrics like energy harvested.
recordCreep(creep: Creep, cpu: number, action: string, actionsCount?: number): void
Record individual creep statistics.
recordPheromones(roomName: string, pheromones: {...}, intent: string, dominant: string | null): void
Record pheromone statistics for a room.
recordNativeCall(type: string): void
Record a native API call.
recordProcess(process: {...}): void
Record kernel process statistics.
collectProcessStats(processes: Map<string, any>): void
Collect all kernel process statistics from a process map.
updateEmpireStats(): void
Update empire-level statistics.
finalizeTick(): void
Finalize the tick - collect automatic stats and prepare for next tick.
getStats(): StatsRoot
Get the complete stats object.
reset(): void
Reset all statistics (rarely needed).
setEnabled(enabled: boolean): void
Enable or disable stats collection.
See Also
- PHEROMONES_GUIDE.md - Detailed pheromone system documentation
- GAME_VARIABLES.md - Game constants and variables reference
- STATS_SYSTEM_OVERVIEW.md - High-level stats system overview
Troubleshooting
Stats not updating
- Ensure
statsManager.finalizeTick()is called at end of main loop - Check that
enabled: truein configuration - Verify stats collection methods are being called
High CPU usage
- Disable
trackNativeCallsif not needed - Reduce
logIntervalor set to 0 - Consider disabling stats entirely in production
Missing data
- Per-creep CPU tracking requires manual
recordCreep()calls - Process stats require
collectProcessStats()call - Empire stats require
updateEmpireStats()call
Memory overflow
- Stats use only current tick data (no history)
- If memory is critical, disable stats or reduce creep count
- Consider exporting to external storage instead