| # Performance Regression Tests | |
| **Phase:** 243 - Memory & Performance Bug Discovery | |
| **Location:** `backend/tests/performance_regression/` | |
| **Last Updated:** March 25, 2026 | |
| ## Purpose | |
| Performance regression tests use **pytest-benchmark** to detect performance degradations in critical paths: API latency, database queries, and governance cache operations. | |
| **20% Regression Threshold:** Detects significant performance degradations while allowing for minor measurement noise (±5-10%) | |
| **Graceful Degradation:** Tests skip with pytest.skip if pytest-benchmark unavailable | |
| ## Key Features | |
| - **Performance Regression Detection:** Detects >20% degradation vs baseline | |
| - **Baseline Comparison:** Compare current performance against established baseline | |
| - **Benchmark Statistics:** Mean, min, max, stddev for rigorous analysis | |
| - **TestClient Pattern:** Use TestClient instead of httpx for faster benchmarks | |
| - **Fixture Reuse:** Import db_session, authenticated_user from e2e_ui/fixtures | |
| ## Test Coverage | |
| ### API Latency Regression (`test_api_latency_regression.py`) | |
| **Tests:** | |
| - `test_api_get_agent_latency()` - GET /api/v1/agents/{id} latency | |
| - `test_api_list_agents_latency()` - GET /api/v1/agents latency | |
| - `test_api_create_agent_latency()` - POST /api/v1/agents latency | |
| - `test_api_delete_agent_latency()` - DELETE /api/v1/agents/{id} latency | |
| **Fixtures:** | |
| - `check_regression` - Regression checker with 20% threshold | |
| - `performance_baseline` - Baseline loader from JSON | |
| **Focus Areas:** | |
| - API endpoint response time | |
| - Request validation overhead | |
| - Database query performance | |
| - JSON serialization overhead | |
| ### Database Query Regression (`test_database_query_regression.py`) | |
| **Tests:** | |
| - `test_query_agent_by_id_latency()` - Agent by ID query | |
| - `test_query_agents_list_latency()` - Agent list query | |
| - `test_query_executions_by_agent_latency()` - Executions by agent query | |
| - `test_query_bulk_operations_latency()` - Bulk insert/update operations | |
| **Fixtures:** | |
| - `check_regression` - Regression checker | |
| - `db_session` - Database session (imported from e2e_ui/fixtures) | |
| **Focus Areas:** | |
| - Database query execution time | |
| - Index effectiveness | |
| - N+1 query problems | |
| - Bulk operation efficiency | |
| ### Governance Cache Regression (`test_governance_cache_regression.py`) | |
| **Tests:** | |
| - `test_cache_get_latency()` - Cache get latency | |
| - `test_cache_set_latency()` - Cache set latency | |
| - `test_cache_hit_rate()` - Cache hit rate regression | |
| - `test_cache_bulk_operations_latency()` - Bulk cache operations | |
| **Fixtures:** | |
| - `check_regression` - Regression checker | |
| - `governance_cache` - GovernanceCache instance | |
| **Focus Areas:** | |
| - Cache lookup performance | |
| - Cache miss latency | |
| - Cache hit rate (inverted logic: lower hit rate = regression) | |
| - Bulk cache operation efficiency | |
| ## Fixtures | |
| ### check_regression | |
| **Purpose:** Regression checker with 20% threshold | |
| **Usage:** | |
| ```python | |
| def test_api_latency(benchmark, check_regression): | |
| result = benchmark(api_call) | |
| check_regression(result, threshold=0.2) # 20% threshold | |
| ``` | |
| **Parameters:** | |
| - `result` - Benchmark result dict (mean, min, max, stddev) | |
| - `threshold` - Regression threshold (default: 0.2 = 20%) | |
| **Assertions:** | |
| - Fails if `mean` increased >20% vs baseline | |
| - Fails if `hit_rate` decreased >20% vs baseline (inverted logic) | |
| ### performance_baseline | |
| **Purpose:** Load baseline from JSON file | |
| **Usage:** | |
| ```python | |
| def test_with_baseline(performance_baseline): | |
| baseline = performance_baseline["test_api_get_agent_latency"] | |
| print(f"Baseline mean: {baseline['mean']}") | |
| ``` | |
| **Baseline Location:** `backend/tests/performance_regression/.benchmarks/` | |
| ## Test Patterns | |
| ### API Latency Pattern | |
| ```python | |
| @pytest.mark.benchmark | |
| def test_api_get_agent_latency(benchmark, check_regression): | |
| """ | |
| PROPERTY: GET /api/v1/agents/{id} should complete in <100ms (P50) | |
| STRATEGY: Use pytest-benchmark to measure API latency. Compare against | |
| baseline using 20% regression threshold. | |
| INVARIANT: mean_latency < 100ms AND regression < 20% | |
| RADII: 1000 benchmark iterations provides 99% confidence with 5ms | |
| measurement precision. | |
| BASELINE: Initial baseline established 2026-03-24 | |
| """ | |
| from fastapi.testclient import TestClient | |
| from main import app | |
| # Setup | |
| client = TestClient(app) | |
| # Benchmark function | |
| def get_agent(): | |
| response = client.get("/api/v1/agents/test-agent-1") | |
| assert response.status_code == 200 | |
| return response.json() | |
| # Run benchmark | |
| result = benchmark.pedantic(get_agent, iterations=1000, rounds=10) | |
| # Check regression (20% threshold) | |
| check_regression(result, threshold=0.2) | |
| # Assert baseline performance | |
| assert result["mean"] < 0.1 # 100ms | |
| ``` | |
| ### Database Query Pattern | |
| ```python | |
| @pytest.mark.benchmark | |
| def test_query_agent_by_id_latency(benchmark, check_regression, db_session): | |
| """ | |
| PROPERTY: Agent by ID query should complete in <50ms | |
| STRATEGY: Benchmark database query with 1000 iterations | |
| """ | |
| from core.models import AgentRegistry | |
| # Setup | |
| agent_id = "test-agent-1" | |
| # Benchmark function | |
| def query_agent(): | |
| agent = db_session.query(AgentRegistry).filter(AgentRegistry.id == agent_id).first() | |
| assert agent is not None | |
| return agent | |
| # Run benchmark | |
| result = benchmark(query_agent) | |
| # Check regression | |
| check_regression(result, threshold=0.2) | |
| # Assert baseline performance | |
| assert result["mean"] < 0.05 # 50ms | |
| ``` | |
| ### Cache Hit Rate Pattern (Inverted Logic) | |
| ```python | |
| @pytest.mark.benchmark | |
| def test_cache_hit_rate(benchmark, check_regression): | |
| """ | |
| PROPERTY: Cache hit rate should remain >90% | |
| STRATEGY: Measure cache hit rate over 1000 operations. Use inverted | |
| logic for regression: hit_rate DECREASE is regression. | |
| INVARIANT: hit_rate > 0.9 AND hit_rate decrease < 20% | |
| RADII: 1000 operations provides 99% confidence for hit rate estimation | |
| """ | |
| from core.governance_cache import GovernanceCache | |
| # Setup | |
| cache = GovernanceCache() | |
| cache.set("agent:1", {"data": "value1"}) | |
| # Benchmark function | |
| def cache_hit_rate(): | |
| hits = 0 | |
| total = 100 | |
| for i in range(total): | |
| if cache.get("agent:1"): | |
| hits += 1 | |
| return hits / total | |
| # Run benchmark | |
| result = benchmark(cache_hit_rate) | |
| # Check regression (inverted logic: hit_rate decrease is regression) | |
| check_regression(result, threshold=0.2, metric="hit_rate") | |
| # Assert baseline performance | |
| assert result["mean"] > 0.9 # 90% hit rate | |
| ``` | |
| ## Running Tests | |
| ### Run All Performance Regression Tests | |
| ```bash | |
| cd backend | |
| pytest tests/performance_regression/ --benchmark-only | |
| # Compare against baseline | |
| pytest tests/performance_regression/ --benchmark-only --benchmark-compare=baseline | |
| # Fail on regression (>20% degradation) | |
| pytest tests/performance_regression/ --benchmark-only --benchmark-compare=baseline --benchmark-compare-fail=mean:20% | |
| # Generate comparison table | |
| pytest tests/performance_regression/ --benchmark-only --benchmark-compare=baseline --benchmark-compare-fail=mean:20% --benchmark-sort=name | |
| ``` | |
| ### Run Specific Test File | |
| ```bash | |
| # API latency regression | |
| pytest tests/performance_regression/test_api_latency_regression.py --benchmark-only | |
| # Database query regression | |
| pytest tests/performance_regression/test_database_query_regression.py --benchmark-only | |
| # Governance cache regression | |
| pytest tests/performance_regression/test_governance_cache_regression.py --benchmark-only | |
| ``` | |
| ### Run Single Test | |
| ```bash | |
| pytest tests/performance_regression/test_api_latency_regression.py::test_api_get_agent_latency --benchmark-only -v | |
| ``` | |
| ### Initialize Baseline (First Time) | |
| ```bash | |
| # Generate initial baseline | |
| pytest tests/performance_regression/ --benchmark-only --benchmark-autosave | |
| # Commit baseline | |
| git add backend/tests/performance_regression/.benchmarks/ | |
| git commit -m "perf: initialize performance regression baseline" | |
| ``` | |
| ### Update Baseline (After Valid Improvements) | |
| ```bash | |
| # Run tests and autosave new baseline | |
| pytest tests/performance_regression/ --benchmark-only --benchmark-autosave | |
| # Commit updated baseline | |
| git add backend/tests/performance_regression/.benchmarks/ | |
| git commit -m "perf: update performance baseline (valid improvement)" | |
| ``` | |
| ## Troubleshooting | |
| ### Common Issues | |
| **1. pytest-benchmark not installed** | |
| ```bash | |
| # Symptom: Tests fail with "pytest-benchmark not installed" | |
| # Solution: Install pytest-benchmark | |
| pip install pytest-benchmark | |
| ``` | |
| **2. Baseline missing** | |
| ```bash | |
| # Symptom: Tests fail with "baseline not found" | |
| # Solution: Generate initial baseline | |
| pytest tests/performance_regression/ --benchmark-only --benchmark-autosave | |
| ``` | |
| **3. Performance regression false positives (<10% regression)** | |
| ```bash | |
| # Symptom: Test fails with <10% regression | |
| # Solution: Re-run test, adjust threshold, or mark as flaky | |
| pytest tests/performance_regression/test_api_latency_regression.py::test_api_get_agent_latency --benchmark-only -v | |
| ``` | |
| **4. TestClient not available** | |
| ```bash | |
| # Symptom: ImportError: TestClient not available | |
| # Solution: Install FastAPI test dependencies | |
| pip install fastapi[all] | |
| ``` | |
| **5. Database session fixture not found** | |
| ```bash | |
| # Symptom: Fixture 'db_session' not found | |
| # Solution: Import from e2e_ui/fixtures | |
| # Add to conftest.py: | |
| # from tests.e2e_ui.fixtures.database_fixtures import db_session | |
| ``` | |
| ### Debugging Performance Regressions | |
| **View Benchmark Comparison Output:** | |
| ```bash | |
| # View benchmark comparison table | |
| pytest tests/performance_regression/ --benchmark-only --benchmark-compare=baseline --benchmark-compare-fail=mean:20% --benchmark-sort=name | |
| # Output columns: | |
| # - name (benchmark name) | |
| # - mean (current execution time) | |
| # - min/max/stddev (execution time statistics) | |
| # - rounds (number of iterations) | |
| # - baseline (baseline execution time) | |
| # - change (percentage change vs baseline) | |
| # Regression example: | |
| # name mean baseline change | |
| # test_api_get_agent_latency 150ms 100ms +50% # REGRESSION | |
| ``` | |
| **Performance Regression Categories:** | |
| 1. **API Latency:** Increased response time (e.g., database query N+1 problem) | |
| 2. **Database Queries:** Slower queries (missing index, inefficient join) | |
| 3. **Cache Hit Rate:** Reduced cache effectiveness (cache invalidation issue) | |
| **Common Performance Regression Patterns:** | |
| ```python | |
| # Pattern 1: N+1 query problem | |
| agents = db.query(Agent).all() | |
| for agent in agents: # N+1: N additional queries | |
| executions = db.query(Execution).filter_by(agent_id=agent.id).all() | |
| # Solution: Eager loading | |
| from sqlalchemy.orm import joinedload | |
| agents = db.query(Agent).options(joinedload(Agent.executions)).all() | |
| # Pattern 2: Inefficient database query | |
| results = db.query(Agent).filter(Agent.status == "active").all() # Full table scan | |
| # Solution: Add index | |
| CREATE INDEX idx_agent_status ON agents(status); | |
| # Pattern 3: Cache miss storm | |
| for agent_id in agent_ids: # N cache misses | |
| agent = cache.get(f"agent:{agent_id}") | |
| # Solution: Bulk cache get | |
| agents = cache.get_many([f"agent:{id}" for id in agent_ids]) | |
| ``` | |
| ## Examples | |
| ### Writing Performance Regression Tests | |
| **Example 1: API Latency Regression** | |
| ```python | |
| import pytest | |
| from tests.performance_regression.conftest import check_regression | |
| @pytest.mark.benchmark | |
| def test_api_get_agent_latency(benchmark, check_regression): | |
| """ | |
| PROPERTY: GET /api/v1/agents/{id} should complete in <100ms (P50) | |
| STRATEGY: Use pytest-benchmark to measure API latency. Compare against | |
| baseline using 20% regression threshold. | |
| INVARIANT: mean_latency < 100ms AND regression < 20% | |
| RADII: 1000 benchmark iterations provides 99% confidence with 5ms | |
| measurement precision. | |
| BASELINE: Initial baseline established 2026-03-24 | |
| """ | |
| from fastapi.testclient import TestClient | |
| from main import app | |
| from core.models import AgentRegistry | |
| from sqlalchemy.orm import Session | |
| # Setup | |
| client = TestClient(app) | |
| with Session() as db: | |
| agent = db.query(AgentRegistry).first() | |
| # Benchmark function | |
| def get_agent(): | |
| response = client.get(f"/api/v1/agents/{agent.id}") | |
| assert response.status_code == 200 | |
| return response.json() | |
| # Run benchmark | |
| result = benchmark.pedantic(get_agent, iterations=1000, rounds=10) | |
| # Check regression (20% threshold) | |
| check_regression(result, threshold=0.2) | |
| # Assert baseline performance | |
| assert result["mean"] < 0.1 # 100ms | |
| ``` | |
| **Example 2: Database Query Regression** | |
| ```python | |
| @pytest.mark.benchmark | |
| def test_query_agent_by_id_latency(benchmark, check_regression, db_session): | |
| """ | |
| PROPERTY: Agent by ID query should complete in <50ms | |
| STRATEGY: Benchmark database query with 1000 iterations | |
| """ | |
| from core.models import AgentRegistry | |
| # Setup | |
| agent_id = "test-agent-1" | |
| # Benchmark function | |
| def query_agent(): | |
| agent = db_session.query(AgentRegistry).filter(AgentRegistry.id == agent_id).first() | |
| assert agent is not None | |
| return agent | |
| # Run benchmark | |
| result = benchmark(query_agent) | |
| # Check regression | |
| check_regression(result, threshold=0.2) | |
| # Assert baseline performance | |
| assert result["mean"] < 0.05 # 50ms | |
| ``` | |
| **Example 3: Cache Hit Rate Regression (Inverted Logic)** | |
| ```python | |
| @pytest.mark.benchmark | |
| def test_cache_hit_rate(benchmark, check_regression): | |
| """ | |
| PROPERTY: Cache hit rate should remain >90% | |
| STRATEGY: Measure cache hit rate over 1000 operations. Use inverted | |
| logic for regression: hit_rate DECREASE is regression. | |
| INVARIANT: hit_rate > 0.9 AND hit_rate decrease < 20% | |
| RADII: 1000 operations provides 99% confidence for hit rate estimation | |
| """ | |
| from core.governance_cache import GovernanceCache | |
| # Setup | |
| cache = GovernanceCache() | |
| cache.set("agent:1", {"data": "value1"}) | |
| # Benchmark function | |
| def cache_hit_rate(): | |
| hits = 0 | |
| total = 100 | |
| for i in range(total): | |
| if cache.get("agent:1"): | |
| hits += 1 | |
| return hits / total | |
| # Run benchmark | |
| result = benchmark(cache_hit_rate) | |
| # Check regression (inverted logic: hit_rate decrease is regression) | |
| check_regression(result, threshold=0.2, metric="hit_rate") | |
| # Assert baseline performance | |
| assert result["mean"] > 0.9 # 90% hit rate | |
| ``` | |
| ## Best Practices | |
| 1. **Establish Baselines:** Generate baselines after valid performance improvements | |
| 2. **Use Realistic Thresholds:** 20% regression threshold balances noise sensitivity | |
| 3. **Benchmark Critical Paths:** Focus on user-facing operations (API latency, database queries) | |
| 4. **TestClient Pattern:** Use TestClient instead of httpx for faster benchmarks | |
| 5. **Fixture Reuse:** Import db_session, authenticated_user from e2e_ui/fixtures | |
| 6. **Document Invariants:** Use PROPERTY/STRATEGY/INVARIANT/RADII format | |
| 7. **Baseline Management:** Commit baselines to git for reproducible regression detection | |
| ## References | |
| - **Phase 243 Documentation:** `docs/archive/implementation/MEMORY_PERFORMANCE_BUG_DISCOVERY.md` | |
| - **pytest-benchmark Documentation:** https://pytest-benchmark.readthedocs.io/ | |
| - **Conftest:** `backend/tests/performance_regression/conftest.py` | |
| - **Weekly CI:** `.github/workflows/memory-performance-weekly.yml` | |
| - **Baseline Management:** `backend/tests/performance_regression/.benchmarks/` | |
| --- | |
| *Last Updated: March 25, 2026* | |
| *Phase 243 - Memory & Performance Bug Discovery* | |