| # Memory Leak Tests | |
| **Phase:** 243 - Memory & Performance Bug Discovery | |
| **Location:** `backend/tests/memory_leaks/` | |
| **Last Updated:** March 25, 2026 | |
| ## Purpose | |
| Memory leak tests use Bloomberg's **memray** profiler to detect Python-level memory leaks in critical paths: agent execution, governance cache, LLM streaming, canvas presentation, and episodic memory operations. | |
| **Python 3.11+ Required:** memray only supports Python 3.11+ | |
| **Graceful Degradation:** Tests skip with pytest.skip if memray unavailable | |
| ## Key Features | |
| - **Python Heap Leak Detection:** Detects uncollected objects (circular references, global caches) | |
| - **Amplification Loops:** 100 iterations to amplify small leaks (1KB → 100KB) | |
| - **Flame Graph Generation:** HTML flame graphs for visualizing memory allocations | |
| - **Threshold Assertions:** 10MB leak threshold for realistic detection | |
| - **Sequential Execution:** Tests run with `-n 1` to avoid interference | |
| ## Test Coverage | |
| ### Agent Execution Memory Leaks (`test_agent_execution_leaks.py`) | |
| **Tests:** | |
| - `test_agent_execution_no_memory_leak()` - Repeated agent execution (100 iterations) | |
| - `test_concurrent_agent_execution_no_memory_leak()` - Concurrent agent execution | |
| - `test_agent_registry_no_memory_leak()` - Agent registry operations | |
| - `test_agent_governance_cache_no_memory_leak()` - Governance cache operations | |
| **Fixtures:** | |
| - `memray_session` - Memray tracker with max_memory_increase() assertion | |
| **Focus Areas:** | |
| - Agent execution lifecycle | |
| - Database connection pooling | |
| - Cache entry management | |
| - Event listener registration | |
| ### Governance Cache Memory Leaks (`test_governance_cache_leaks.py`) | |
| **Tests:** | |
| - `test_governance_cache_get_no_memory_leak()` - Cache get operations | |
| - `test_governance_cache_set_no_memory_leak()` - Cache set operations | |
| - `test_governance_cache_invalidation_no_memory_leak()` - Cache invalidation | |
| - `test_governance_cache_bulk_operations_no_memory_leak()` - Bulk operations | |
| **Fixtures:** | |
| - `memray_session` - Memray tracker | |
| - `governance_cache` - GovernanceCache instance | |
| **Focus Areas:** | |
| - Cache entry lifecycle | |
| - LRU eviction policy | |
| - Bulk operation efficiency | |
| - Cache invalidation cleanup | |
| ### LLM Streaming Memory Leaks (`test_llm_streaming_leaks.py`) | |
| **Tests:** | |
| - `test_llm_streaming_no_memory_leak()` - LLM streaming operations | |
| - `test_llm_streaming_large_response_no_memory_leak()` - Large responses (10K tokens) | |
| - `test_llm_streaming_concurrent_no_memory_leak()` - Concurrent streaming | |
| - `test_llm_streaming_error_handling_no_memory_leak()` - Error handling | |
| **Fixtures:** | |
| - `memray_session` - Memray tracker | |
| - `mock_llm_handler` - Mock BYOKHandler | |
| **Focus Areas:** | |
| - Token streaming buffers | |
| - Response accumulation | |
| - Connection pooling | |
| - Error handler cleanup | |
| ### Canvas Presentation Memory Leaks (`test_canvas_presentation_leaks.py`) | |
| **Tests:** | |
| - `test_canvas_presentation_no_memory_leak()` - Canvas presentation operations | |
| - `test_canvas_presentation_multiple_canvases_no_memory_leak()` - Multiple canvases | |
| - `test_canvas_presentation_complex_charts_no_memory_leak()` - Complex charts | |
| - `test_canvas_presentation_state_updates_no_memory_leak()` - State updates | |
| **Fixtures:** | |
| - `memray_session` - Memray tracker | |
| - `mock_canvas_tool` - Mock CanvasTool | |
| **Focus Areas:** | |
| - Canvas state management | |
| - Chart rendering buffers | |
| - WebSocket connection pooling | |
| - Event listener cleanup | |
| ### Episodic Memory Memory Leaks (`test_episodic_memory_leaks.py`) | |
| **Tests:** | |
| - `test_episode_segmentation_no_memory_leak()` - Episode segmentation | |
| - `test_episode_retrieval_no_memory_leak()` - Episode retrieval | |
| - `test_episode_lifecycle_no_memory_leak()` - Episode lifecycle | |
| - `test_episode_batch_operations_no_memory_leak()` - Batch operations | |
| **Fixtures:** | |
| - `memray_session` - Memray tracker | |
| - `episode_service` - EpisodeSegmentationService, EpisodeRetrievalService | |
| **Focus Areas:** | |
| - Episode segment storage | |
| - Vector embeddings | |
| - LanceDB connections | |
| - Batch operation cleanup | |
| ## Fixtures | |
| ### memray_session | |
| **Purpose:** Memray tracker for memory leak detection | |
| **Usage:** | |
| ```python | |
| def test_no_memory_leak(memray_session): | |
| with memray_session: | |
| # Run operations | |
| for i in range(100): | |
| execute_operation() | |
| # Assert no leak | |
| assert memray_session.get_max_memory_increase() < 10 * 1024 * 1024 # 10MB | |
| ``` | |
| **Methods:** | |
| - `get_max_memory_increase()` - Returns max memory increase in bytes | |
| - `get_flame_graph_path()` - Returns path to flame graph HTML file | |
| **Artifact Location:** `backend/tests/memory_leaks/artifacts/` | |
| ## Test Patterns | |
| ### Amplification Loop Pattern | |
| ```python | |
| @pytest.mark.memory_leak | |
| def test_operation_no_memory_leak(memray_session): | |
| """ | |
| PROPERTY: Operation should not leak memory over 100 iterations | |
| STRATEGY: Use memray to track Python heap allocations during repeated | |
| operations. Amplify potential leaks by running 100 iterations. | |
| INVARIANT: max_memory_increase < LEAK_THRESHOLD (10MB) | |
| RADII: 100 iterations provides 99% confidence for detecting leaks | |
| with 1MB amplification (100 * 10KB per operation). | |
| """ | |
| with memray_session: | |
| # Amplification loop: 100 iterations | |
| for i in range(100): | |
| execute_operation(data=f"test-{i}") | |
| # Assertion: No memory leak | |
| assert memray_session.get_max_memory_increase() < 10 * 1024 * 1024 # 10MB | |
| ``` | |
| ### Threshold Assertion Pattern | |
| ```python | |
| @pytest.mark.memory_leak | |
| def test_cache_operations_no_memory_leak(memray_session, governance_cache): | |
| """ | |
| PROPERTY: Cache operations should not leak memory | |
| THRESHOLD: 10MB leak threshold allows for GC delay and cache warming | |
| """ | |
| with memray_session: | |
| # Perform 100 cache operations | |
| for i in range(100): | |
| governance_cache.get(f"agent:{i}") | |
| governance_cache.set(f"agent:{i}", {"data": f"value-{i}"}) | |
| # Assert memory increase < 10MB | |
| max_increase = memray_session.get_max_memory_increase() | |
| assert max_increase < 10 * 1024 * 1024, f"Leaked {max_increase / 1024 / 1024:.2f}MB" | |
| ``` | |
| ### Error Handling Pattern | |
| ```python | |
| @pytest.mark.memory_leak | |
| def test_error_handling_no_memory_leak(memray_session): | |
| """ | |
| PROPERTY: Error handling should not leak resources (connections, buffers) | |
| STRATEGY: Trigger errors and verify no resource leaks | |
| """ | |
| with memray_session: | |
| for i in range(100): | |
| try: | |
| # Trigger error | |
| execute_invalid_operation() | |
| except ValueError: | |
| pass # Expected error | |
| # Assert no leak despite errors | |
| assert memray_session.get_max_memory_increase() < 10 * 1024 * 1024 | |
| ``` | |
| ## Running Tests | |
| ### Run All Memory Leak Tests | |
| ```bash | |
| cd backend | |
| pytest tests/memory_leaks/ -v -m memory_leak | |
| # Sequential execution (required for memray) | |
| pytest tests/memory_leaks/ -v -m memory_leak -n 1 | |
| # With flame graph generation | |
| pytest tests/memory_leaks/ -v -m memory_leak -n 1 --memray | |
| ``` | |
| ### Run Specific Test File | |
| ```bash | |
| # Agent execution leaks | |
| pytest tests/memory_leaks/test_agent_execution_leaks.py -v | |
| # Governance cache leaks | |
| pytest tests/memory_leaks/test_governance_cache_leaks.py -v | |
| # LLM streaming leaks | |
| pytest tests/memory_leaks/test_llm_streaming_leaks.py -v | |
| # Canvas presentation leaks | |
| pytest tests/memory_leaks/test_canvas_presentation_leaks.py -v | |
| # Episodic memory leaks | |
| pytest tests/memory_leaks/test_episodic_memory_leaks.py -v | |
| ``` | |
| ### Run Single Test | |
| ```bash | |
| pytest tests/memory_leaks/test_agent_execution_leaks.py::test_agent_execution_no_memory_leak -v | |
| ``` | |
| ### Generate Flame Graphs | |
| ```bash | |
| # Run tests with memray profiling | |
| pytest tests/memory_leaks/ -v -m memory_leak -n 1 --memray | |
| # Open flame graph in browser | |
| open backend/tests/memory_leaks/artifacts/test_agent_execution_no_memory_leak.html | |
| ``` | |
| ## Troubleshooting | |
| ### Common Issues | |
| **1. memray not installed (Python 3.11+ required)** | |
| ```bash | |
| # Symptom: Tests skip with "memray not installed" | |
| # Solution: Install memray (Python 3.11+ only) | |
| python --version # Must be 3.11+ | |
| pip install memray | |
| ``` | |
| **2. Test fails with small leak (<1MB)** | |
| ```bash | |
| # Symptom: Test fails with "Leaked 0.5MB" | |
| # Solution: This is likely GC delay, re-run test | |
| pytest tests/memory_leaks/test_agent_execution_leaks.py::test_agent_execution_no_memory_leak -v | |
| ``` | |
| **3. Flame graph not generated** | |
| ```bash | |
| # Symptom: No HTML file in artifacts/ | |
| # Solution: Run with --memray flag | |
| pytest tests/memory_leaks/ -v -m memory_leak -n 1 --memray | |
| ``` | |
| **4. Test hangs indefinitely** | |
| ```bash | |
| # Symptom: Test never completes | |
| # Solution: Test has infinite loop, check for missing break conditions | |
| pytest tests/memory_leaks/test_agent_execution_leaks.py -v -s # -s for output | |
| ``` | |
| ### Debugging Memory Leaks | |
| **View Flame Graphs:** | |
| ```bash | |
| # Open flame graph in browser | |
| open backend/tests/memory_leaks/artifacts/test_agent_execution_no_memory_leak.html | |
| # Flame graph shows: | |
| # - Function call stacks | |
| # - Memory allocation hotspots (red/yellow towers) | |
| # - Leak locations (tall towers = many allocations) | |
| ``` | |
| **Memory Leak Categories:** | |
| 1. **Python Heap Leaks:** Uncollected objects (circular references, global caches) | |
| 2. **C Extension Leaks:** Native memory leaks (e.g., database connection pools) | |
| 3. **Amplification Leaks:** Small leaks multiplied over iterations | |
| **Common Memory Leak Patterns:** | |
| ```python | |
| # Pattern 1: Global list growing unbounded | |
| GLOBAL_CACHE = [] # LEAK: Never cleared | |
| # Solution: Use LRU cache with size limit | |
| from functools import lru_cache | |
| @lru_cache(maxsize=1000) | |
| def get_agent(agent_id): | |
| return AgentRegistry.query.get(agent_id) | |
| # Pattern 2: Circular references | |
| class Agent: | |
| def __init__(self): | |
| self.parent = None | |
| self.children = [] | |
| # Circular reference: parent <-> children | |
| # Solution: Use weak references | |
| import weakref | |
| class Agent: | |
| def __init__(self): | |
| self.parent = None | |
| self.children = [] # Store weak references | |
| # Pattern 3: Unclosed database connections | |
| from sqlalchemy.orm import Session | |
| session = Session() # LEAK: Never closed | |
| # Solution: Use context manager | |
| with Session() as session: | |
| agents = session.query(Agent).all() | |
| # Connection automatically closed | |
| # Pattern 4: Event listeners not removed | |
| class EventEmitter: | |
| def __init__(self): | |
| self.listeners = [] | |
| def add_listener(self, listener): | |
| self.listeners.append(listener) # LEAK: Never removed | |
| # Solution: Auto-remove listeners or use weak references | |
| import weakref | |
| class EventEmitter: | |
| def __init__(self): | |
| self.listeners = weakref.WeakSet() | |
| def add_listener(self, listener): | |
| self.listeners.add(listener) # Auto-removed when listener GC'd | |
| ``` | |
| ## Examples | |
| ### Writing Memory Leak Tests | |
| **Example 1: Agent Execution Memory Leak** | |
| ```python | |
| import pytest | |
| from tests.memory_leaks.conftest import memray_session | |
| @pytest.mark.memory_leak | |
| def test_agent_execution_no_memory_leak(memray_session): | |
| """ | |
| PROPERTY: Agent execution should not leak memory over 100 iterations | |
| STRATEGY: Use memray to track Python heap allocations during repeated | |
| agent execution operations. Amplify potential leaks by running 100 | |
| iterations. | |
| INVARIANT: max_memory_increase < LEAK_THRESHOLD (10MB) | |
| RADII: 100 iterations provides 99% confidence for detecting leaks | |
| with 1MB amplification (100 * 10KB per execution). | |
| """ | |
| from core.agent_governance_service import AgentGovernanceService | |
| from core.models import AgentRegistry | |
| # Setup | |
| service = AgentGovernanceService() | |
| with memray_session: | |
| # Amplification loop: 100 iterations | |
| for i in range(100): | |
| agent = AgentRegistry( | |
| id=f"test-agent-{i}", | |
| name=f"Test Agent {i}", | |
| category="testing", | |
| module_path="core.agent_governance_service", | |
| class_name="AgentGovernanceService", | |
| status="AUTONOMOUS" | |
| ) | |
| # Execute agent (potential leak point) | |
| service.execute_agent(agent.id) | |
| # Assertion: No memory leak | |
| assert memray_session.get_max_memory_increase() < 10 * 1024 * 1024 # 10MB | |
| ``` | |
| **Example 2: Cache Memory Leak** | |
| ```python | |
| @pytest.mark.memory_leak | |
| def test_governance_cache_no_memory_leak(memray_session): | |
| """ | |
| PROPERTY: Governance cache should not leak memory | |
| STRATEGY: Perform 100 cache get/set operations and verify no leak | |
| """ | |
| from core.governance_cache import GovernanceCache | |
| # Setup | |
| cache = GovernanceCache() | |
| with memray_session: | |
| # Amplification loop: 100 iterations | |
| for i in range(100): | |
| cache.get(f"agent:{i}") | |
| cache.set(f"agent:{i}", {"data": f"value-{i}"}) | |
| # Assertion: No memory leak | |
| assert memray_session.get_max_memory_increase() < 10 * 1024 * 1024 # 10MB | |
| ``` | |
| **Example 3: LLM Streaming Memory Leak** | |
| ```python | |
| @pytest.mark.memory_leak | |
| def test_llm_streaming_no_memory_leak(memray_session, mock_llm_handler): | |
| """ | |
| PROPERTY: LLM streaming should not leak memory | |
| STRATEGY: Stream 100 responses and verify no leak in token buffers | |
| """ | |
| with memray_session: | |
| # Amplification loop: 100 iterations | |
| for i in range(100): | |
| response = mock_llm_handler.stream_response(f"prompt-{i}") | |
| tokens = list(response) # Consume stream | |
| # Assertion: No memory leak | |
| assert memray_session.get_max_memory_increase() < 10 * 1024 * 1024 # 10MB | |
| ``` | |
| ## Best Practices | |
| 1. **Use Amplification Loops:** 100 iterations to amplify small leaks (1KB → 100KB) | |
| 2. **Set Realistic Thresholds:** 10MB for Python heap leaks (allows for GC delay) | |
| 3. **Generate Flame Graphs:** Essential for debugging complex leaks | |
| 4. **Test Sequential Execution:** Use `-n 1` to avoid interference from parallel tests | |
| 5. **Graceful Degradation:** Skip tests if memray unavailable (Python 3.11+ required) | |
| 6. **Document Invariants:** Use PROPERTY/STRATEGY/INVARIANT/RADII format | |
| 7. **Test Critical Paths:** Focus on agent execution, cache, streaming, episodic memory | |
| ## References | |
| - **Phase 243 Documentation:** `docs/archive/implementation/MEMORY_PERFORMANCE_BUG_DISCOVERY.md` | |
| - **memray Documentation:** https://bloomberg.github.io/memray/ | |
| - **Conftest:** `backend/tests/memory_leaks/conftest.py` | |
| - **Weekly CI:** `.github/workflows/memory-performance-weekly.yml` | |
| --- | |
| *Last Updated: March 25, 2026* | |
| *Phase 243 - Memory & Performance Bug Discovery* | |