Summary
The benchmark_entities fixture in tests/benchmark/conftest.py creates 100 entities on every test that uses it. Changing to scope="module" would provide 30-50% faster benchmark runs.
Problem
Current Implementation
# tests/benchmark/conftest.py:182-207
@pytest.fixture
def benchmark_entities(sync_limiter: Any) -> list[str]:
"""Pre-create entities with pre-warmed buckets for throughput tests."""
entity_ids = [f"bench-entity-{i:03d}" for i in range(100)]
limits = [Limit.per_minute("rpm", 1_000_000)]
for entity_id in entity_ids:
# Create entity
sync_limiter.create_entity(entity_id, name=f"Benchmark Entity {entity_id}")
# Pre-warm bucket by doing one acquire
with sync_limiter.acquire(...):
pass
return entity_ids
Issues
- Function-scoped (default): Creates 100 entities + 100 acquires per test
- Repeated setup overhead: If 5 tests use this fixture, setup runs 5 times
- Slow benchmarks: Each setup adds ~1-2 seconds (moto) or more (LocalStack)
Impact Analysis
Looking at benchmark tests that use benchmark_entities:
test_throughput.py - Multiple tests use pre-warmed entities
test_capacity.py - Capacity counting tests
With 5+ tests using this fixture, we're wasting 5-10 seconds of repeated setup.
Proposed Solution
Change fixture scope to module (shared across tests in same file):
@pytest.fixture(scope="module")
def benchmark_entities(sync_limiter: Any) -> list[str]:
"""Pre-create entities with pre-warmed buckets for throughput tests.
Module-scoped to avoid repeated setup across benchmark tests.
Entities are created once per test file.
"""
...
Why module and not session?
session scope would share across all benchmark files
- Different files may need different entity configurations
module is the right granularity for benchmark isolation
Dependency Chain
The fixture depends on sync_limiter which is function-scoped. We need to:
- Create a
module-scoped limiter for benchmarks
- Or use a separate setup that doesn't depend on the function-scoped limiter
Option A: Module-scoped benchmark limiter
@pytest.fixture(scope="module")
def benchmark_limiter(mock_dynamodb_module):
"""Module-scoped limiter for benchmark entity setup."""
with _patch_aiobotocore_response():
limiter = SyncRateLimiter(name="benchmark", region="us-east-1")
limiter._run(limiter._limiter._repository.create_table())
with limiter:
yield limiter
@pytest.fixture(scope="module")
def benchmark_entities(benchmark_limiter):
"""Pre-create entities once per module."""
...
Option B: Lazy initialization with caching
_cached_entities: list[str] | None = None
@pytest.fixture
def benchmark_entities(sync_limiter):
global _cached_entities
if _cached_entities is None:
_cached_entities = _create_entities(sync_limiter)
return _cached_entities
Recommendation: Option A is cleaner and more pytest-idiomatic.
Tasks
Acceptance Criteria
Documentation Updates
.claude/rules/testing.md Addition (done)
### Fixture Scope Selection
| Scope | Use When | Example |
|-------|----------|---------|
| `function` | Test mutates state, needs isolation | `limiter` (each test gets clean state) |
| `class` | Expensive setup shared by class | `e2e_limiter` (CloudFormation stack) |
| `module` | Expensive setup shared by file | `benchmark_entities` (100 pre-warmed entities) |
| `session` | Immutable configuration | `localstack_endpoint` (env var read) |
**Rule**: If fixture setup takes >100ms and is used by multiple tests, consider broader scope.
Performance Measurement
Current results (branch perf/171-benchmark-fixture-scope)
The module-scoped approach is slower than the original function-scoped approach:
| File |
Main (baseline) |
Feature branch |
Change |
test_throughput.py (--benchmark-skip) |
5.25-6.36s |
9.94-10.64s |
+60% slower |
test_latency.py (--benchmark-only) |
8.76s |
14.32s |
+63% slower |
test_operations.py (--benchmark-only) |
19.10s |
26.05s |
+36% slower |
Root cause analysis
The regression comes from the module-scoped benchmark_entities fixture pre-creating 111 entities (100 flat + 1 parent + 10 cascade children) with warmup acquires at module load time. This heavy upfront cost is paid once per test file but dominates the total time because:
test_throughput.py: 7 tests that previously used sync_limiter and created entities lazily now pay the 111-entity setup cost upfront. The old approach only created entities as needed within each test.
test_latency.py: 10 benchmark tests now depend on benchmark_entities, adding the 111-entity setup that didn't exist before (tests previously used sync_limiter with per-test entity creation).
test_operations.py: Mixed -- some tests correctly use module-scoped benchmark_entities for steady-state benchmarks, while others use function-scoped sync_limiter for optimization comparisons. But the module-scoped setup still runs once for the file.
Next steps
The optimization hypothesis was wrong for the moto backend -- moto entity creation is cheap enough (~10ms per entity) that pre-creating 111 entities is slower than creating a few entities per test. The module-scoped approach may only pay off with:
- LocalStack/real DynamoDB where entity creation is expensive
- Files with many tests sharing the same entities (>20+ tests)
Options to fix:
- Revert to function-scoped fixtures for moto-based benchmarks and only use module-scoped for LocalStack benchmarks
- Reduce entity count in
benchmark_entities (e.g., 10 flat + 1 parent + 3 children = 14 instead of 111)
- Keep
BenchmarkEntities dataclass but use it function-scoped with the regular sync_limiter
Summary
The
benchmark_entitiesfixture intests/benchmark/conftest.pycreates 100 entities on every test that uses it. Changing toscope="module"would provide 30-50% faster benchmark runs.Problem
Current Implementation
Issues
Impact Analysis
Looking at benchmark tests that use
benchmark_entities:test_throughput.py- Multiple tests use pre-warmed entitiestest_capacity.py- Capacity counting testsWith 5+ tests using this fixture, we're wasting 5-10 seconds of repeated setup.
Proposed Solution
Change fixture scope to
module(shared across tests in same file):Why
moduleand notsession?sessionscope would share across all benchmark filesmoduleis the right granularity for benchmark isolationDependency Chain
The fixture depends on
sync_limiterwhich is function-scoped. We need to:module-scoped limiter for benchmarksOption A: Module-scoped benchmark limiter
Option B: Lazy initialization with caching
Recommendation: Option A is cleaner and more pytest-idiomatic.
Tasks
mock_dynamodb_modulefixture withscope="module"benchmark_limiterfixture withscope="module"benchmark_entitiestoscope="module"depending onbenchmark_limiterBenchmarkEntitiesdataclass with flat + hierarchy entitiestest_throughput.pyto usebenchmark_entitiestest_latency.pyto usebenchmark_entitiestest_operations.pyto usebenchmark_entities(where appropriate)tests/benchmark/conftest.pydocstring to explain scoping strategy.claude/rules/testing.mdabout fixture scope selectionAcceptance Criteria
Documentation Updates
.claude/rules/testing.mdAddition (done)Performance Measurement
Current results (branch
perf/171-benchmark-fixture-scope)The module-scoped approach is slower than the original function-scoped approach:
test_throughput.py(--benchmark-skip)test_latency.py(--benchmark-only)test_operations.py(--benchmark-only)Root cause analysis
The regression comes from the module-scoped
benchmark_entitiesfixture pre-creating 111 entities (100 flat + 1 parent + 10 cascade children) with warmup acquires at module load time. This heavy upfront cost is paid once per test file but dominates the total time because:test_throughput.py: 7 tests that previously usedsync_limiterand created entities lazily now pay the 111-entity setup cost upfront. The old approach only created entities as needed within each test.test_latency.py: 10 benchmark tests now depend onbenchmark_entities, adding the 111-entity setup that didn't exist before (tests previously usedsync_limiterwith per-test entity creation).test_operations.py: Mixed -- some tests correctly use module-scopedbenchmark_entitiesfor steady-state benchmarks, while others use function-scopedsync_limiterfor optimization comparisons. But the module-scoped setup still runs once for the file.Next steps
The optimization hypothesis was wrong for the moto backend -- moto entity creation is cheap enough (~10ms per entity) that pre-creating 111 entities is slower than creating a few entities per test. The module-scoped approach may only pay off with:
Options to fix:
benchmark_entities(e.g., 10 flat + 1 parent + 3 children = 14 instead of 111)BenchmarkEntitiesdataclass but use it function-scoped with the regularsync_limiter