Memory Profiling User Guide

Overview

Memory profiling is an optional feature in PyBulletFleet that tracks memory usage during simulation. This is particularly useful for:

  • Detecting memory leaks in long-running simulations

  • Monitoring memory consumption in multi-robot scenarios

  • Optimizing memory usage by identifying memory-intensive operations

  • Continuous Integration (CI) testing to prevent memory regressions

Memory profiling uses Python’s built-in tracemalloc module, so no external dependencies are required.


Quick Start

1. Enable Memory Profiling in Configuration

Add enable_memory_profiling: true to your YAML configuration file:

# config.yaml
enable_time_profiling: true          # Time profiling (optional, independent)
enable_memory_profiling: true   # Memory profiling
profiling_interval: 100         # Report every 100 steps

2. Run Your Simulation

from pybullet_fleet.core_simulation import MultiRobotSimulationCore

# Load configuration with memory profiling enabled
sim = MultiRobotSimulationCore.from_config("config.yaml")

# Run simulation
sim.run_simulation(duration=60.0)  # 60 seconds simulation time

3. Check Memory Usage Output

Memory statistics will be printed every profiling_interval steps:

[MEMORY] Last 100 steps: current=245.32MB (min=243.18, max=246.57), peak=248.45MB (max=250.12), growth=+3.25MB

API Reference

Enable/Disable Memory Profiling

Configuration File:

enable_memory_profiling: true   # Enable memory profiling
profiling_interval: 100         # Report interval (steps)

Python API:

sim = MultiRobotSimulationCore(
    enable_memory_profiling=True,
    profiling_interval=100
)

Get Current Memory Usage

get_memory_usage() can be called at any time while tracemalloc is active — including inside a callback during run_simulation():

# Inside a callback: monitor memory every N steps
def memory_monitor(sim_core, dt):
    mem = sim_core.get_memory_usage()
    if mem:
        print(f"Heap: {mem['current_mb']:.2f} MB, Peak: {mem['peak_mb']:.2f} MB")

sim.register_callback(memory_monitor, frequency=10)
sim.run_simulation()

You can also call it after the simulation ends to get the final snapshot:

mem_usage = sim.get_memory_usage()

if mem_usage:
    print(f"Current: {mem_usage['current_mb']:.2f} MB")
    print(f"Peak: {mem_usage['peak_mb']:.2f} MB")

Return Value:

  • None if memory profiling is not enabled

  • Dictionary with keys:

    • current_mb: Current memory usage in MB

    • peak_mb: Peak memory usage since profiling started


Understanding Memory Statistics

Memory Report Fields

[MEMORY] Last 100 steps: current=245.32MB (min=243.18, max=246.57), peak=248.45MB (max=250.12), growth=+3.25MB
  • current: Average current memory usage over last N steps

    • min: Minimum current memory in the interval

    • max: Maximum current memory in the interval

  • peak: Average peak memory (highest ever reached) over last N steps

    • max: Maximum peak memory in the interval

  • growth: Memory increase from first to last sample in interval

    • Positive value indicates memory growth (potential leak)

    • Negative value indicates memory release

    • Zero or near-zero indicates stable memory usage

Interpreting Growth Values

Growth Value

Interpretation

+0.5MB to +2MB

Normal for initialization or object spawning

+2MB to +10MB

Monitor closely, may indicate inefficiency

+10MB or more

Likely memory leak, investigate immediately

-1MB to +1MB (steady)

Healthy, stable memory usage

Consistently positive

Potential memory leak


Use Case: Detecting Memory Leaks in Long-Running Simulations

config = {
    "enable_memory_profiling": True,
    "profiling_interval": 500,  # Report every 500 steps
}

sim = MultiRobotSimulationCore.from_config(config)
# ... spawn agents ...
sim.run_simulation()  # Ctrl+C or GUI close returns normally

# Check final memory usage
final_mem = sim.get_memory_usage()
if final_mem:
    print(f"Final memory: {final_mem['current_mb']:.2f} MB")

During the run, watch the growth field in the periodic reports:

[MEMORY] Last 500 steps: current=250.45MB (...), growth=+0.12MB   # stable ✓
[MEMORY] Last 500 steps: current=250.58MB (...), growth=+0.15MB   # stable ✓
[MEMORY] Last 500 steps: current=251.02MB (...), growth=+5.45MB   # investigate!

For CI, assert a ceiling in a test:

def test_memory_usage_within_limits():
    """Ensure simulation memory stays below threshold."""
    sim = MultiRobotSimulationCore(
        enable_memory_profiling=True,
    )
    # ... spawn agents, run ...
    mem = sim.get_memory_usage()
    assert mem and mem["current_mb"] < 500.0, "Memory usage exceeds 500 MB limit"

Best Practices

✅ DO

  1. Enable memory profiling for long-running simulations (>1 minute)

  2. Monitor growth values to detect potential leaks

  3. Set profiling_interval based on simulation length:

    • Short simulations (< 1 min): profiling_interval: 50

    • Medium simulations (1-10 min): profiling_interval: 100

    • Long simulations (> 10 min): profiling_interval: 500

  4. Add memory assertions to tests:

    assert mem["current_mb"] < MAX_MEMORY_MB
    
  5. Profile before and after code changes to detect regressions

❌ DON’T

  1. Don’t enable memory profiling in production (slight performance overhead)

  2. Don’t panic over small growth values (< 2MB per 100 steps is normal)

  3. Don’t rely only on memory profiling - use time profiling together

  4. Don’t forget to close simulation after profiling


Performance Impact

Memory profiling uses Python’s tracemalloc module, which has minimal overhead:

  • CPU overhead: < 1% in most cases

  • Memory overhead: ~5-10% additional memory for tracking

  • Recommended for: Development, testing, debugging

  • Not recommended for: Production deployments requiring maximum performance


Troubleshooting

Memory profiling returns None

Problem:

mem = sim.get_memory_usage()
print(mem)  # None

Solution: Ensure enable_memory_profiling: true in configuration:

enable_memory_profiling: true

No memory statistics printed

Problem: Memory profiling enabled but no [MEMORY] logs appear.

Solution:

  • Check profiling_interval setting (default: 100 steps)

  • Run enough steps to trigger report: sim.step_once() × profiling_interval

  • Ensure logging level allows INFO messages

Memory keeps growing

Problem:

[MEMORY] Last 100 steps: growth=+5.2MB
[MEMORY] Last 100 steps: growth=+4.8MB
[MEMORY] Last 100 steps: growth=+5.1MB  # Continuously positive!

Solution:

  1. Check for memory leak candidates:

    # Bad: List comprehension creates new list every frame
    self._data = [x for x in self._data if condition(x)]
    
    # Good: In-place removal
    while self._data and not condition(self._data[0]):
        self._data.pop(0)
    
  2. Use external profilers for detailed analysis:

    pip install memory_profiler
    mprof run python examples/your_demo.py
    mprof plot
    

Example Output

Normal Memory Usage (Healthy)

[MEMORY] Last 100 steps: current=245.32MB (min=243.18, max=246.57), peak=248.45MB (max=250.12), growth=+0.15MB
[MEMORY] Last 100 steps: current=245.48MB (min=244.32, max=246.89), peak=248.45MB (max=250.12), growth=+0.12MB
[MEMORY] Last 100 steps: current=245.55MB (min=244.89, max=246.34), peak=248.45MB (max=250.12), growth=-0.05MB

Interpretation: Stable memory usage (~245MB), minimal growth, healthy simulation.

Memory Leak Detected (Unhealthy)

[MEMORY] Last 100 steps: current=245.32MB (min=243.18, max=246.57), peak=248.45MB (max=250.12), growth=+5.23MB
[MEMORY] Last 100 steps: current=250.55MB (min=248.92, max=252.14), peak=253.67MB (max=255.34), growth=+4.87MB
[MEMORY] Last 100 steps: current=255.42MB (min=253.78, max=257.09), peak=258.54MB (max=260.21), growth=+5.12MB

Interpretation: Continuous memory growth (+5MB per interval), potential memory leak, requires investigation.


References