From pytest-profiling
Diagnose slow Python test suites, identify bottlenecks, apply fixes, and produce a written report. Use when the user asks to "speed up tests", "profile tests", "diagnose slow tests", "why are tests slow", "optimize test suite", "test performance", or mentions slow pytest runs.
npx claudepluginhub clemux/claude-code-plugins --plugin pytest-profilingThis skill uses the workspace's default tool permissions.
Systematic workflow for diagnosing, fixing, and reporting on slow Python/pytest test suites. Measure first, optimize based on data, never guess.
Guides browser automation using Playwright and Puppeteer for web testing, scraping, and AI agents. Covers selectors, auto-waits, test isolation, and anti-detection patterns.
Provides checklists for code reviews covering functionality, code quality, security, performance, tests, and maintainability. Use for PRs, audits, team standards, or training.
Guides A/B test setup with mandatory gates for hypothesis validation, metrics definition, sample size calculation, and execution readiness checks.
Systematic workflow for diagnosing, fixing, and reporting on slow Python/pytest test suites. Measure first, optimize based on data, never guess.
Read project conventions — Check AGENTS.md, CLAUDE.md, or pyproject.toml to understand:
uv run pytest, pytest, make test, etc.)Ask the user:
Check for pyinstrument — If not installed, ask the user before adding it:
uv pip list | grep pyinstrument
# If missing, ask before running: uv add --dev pyinstrument
All steps are read-only. No code changes.
uv run pytest <test_dir> --durations=20 -q
Record:
Note whether the slowest items are setup, call, or teardown — this determines where to dig.
Pick 2-3 of the slowest tests and run:
uv run pytest <test_dir> -k "name_of_slow_test" -q --setup-show
This shows the full fixture dependency chain. Look for:
uv run pyinstrument -r text -m pytest <test_dir> -k "name_of_slow_test" -q
The call tree shows exactly where CPU time is spent. Repeat for 2-3 of the slowest tests to find common patterns.
Common hotspots to look for:
bcrypt.hashpw / bcrypt.gensalt — password hashing (~200ms per call at default 12 rounds)MetaData.create_all — SQLAlchemy table creationTRUNCATE ... CASCADE — PostgreSQL lock acquisitionTestClient(app) — ASGI lifespan enter/exitrsa.generate_private_key()If a specific operation is suspect, isolate and measure it:
uv run python -c "
import time
# ... setup code ...
times = []
for i in range(10):
start = time.time()
# ... the suspect operation ...
times.append(time.time() - start)
print(f'avg: {sum(times)/len(times)*1000:.1f}ms')
"
This is useful for comparing alternatives (e.g., TRUNCATE vs DELETE, bcrypt rounds 12 vs 4).
Summarize findings before touching any code:
(per-call cost) × (number of calls) = total savingsDesign targeted fixes based on the profiling data. Apply one fix at a time and measure after each.
Patch bcrypt.gensalt to use minimum rounds (4) in tests via a session-scoped autouse fixture in the root tests/conftest.py:
@pytest.fixture(autouse=True, scope="session")
def _fast_bcrypt():
original_gensalt = bcrypt.gensalt
def fast_gensalt(rounds=4, prefix=b"2b"):
return original_gensalt(rounds=4, prefix=prefix)
with patch("bcrypt.gensalt", fast_gensalt):
yield
Replace TRUNCATE ... CASCADE with DELETE FROM in reverse FK order. TRUNCATE acquires ACCESS EXCLUSIVE locks (~300ms even on empty tables). DELETE with row-level locks takes ~2ms for small row counts.
If a fixture is pure setup with no per-test state, consider widening its scope:
scope="module" — shared within one test filescope="session" — shared across the entire runOnly do this if tests don't mutate the fixture's state.
Cache a test keypair as a session-scoped fixture instead of generating per-test.
These are one-time costs and generally not worth optimizing. Note them in the report but don't act on them unless they dominate.
After each optimization:
uv run pytest <test_dir> -q --tb=noCommit each optimization as a separate commit with measured timings in the message:
perf(tests): <what changed>
<Why it helps.>
<previous>s → <new>s (saved ~<delta>s)
Use conventional commits (perf: prefix for performance improvements).
Write a report to the location chosen by the user. The report must include all of the following sections:
# Test Suite Performance: Analysis & Fixes
**Date:** YYYY-MM-DD
**Result:** <before>s → <after>s (<speedup>x faster)
## Baseline
- Test command: `<exact command>`
- Total tests: X passed, Y failed (pre-existing), Z skipped
- Wall time: Xs
## Methodology
1. `pytest --durations=20` — identify slowest test phases
2. `pytest --setup-show` — trace fixture setup chains
3. `pyinstrument` — CPU call trees on slowest tests
## Profiling Findings
| Component | Time | Scope | Notes |
|---|---|---|---|
| ... | ... | ... | ... |
## Bottleneck Breakdown (estimated)
- **<bottleneck 1>:** N calls × Xms = ~Ys (Z%)
- **<bottleneck 2>:** N calls × Xms = ~Ys (Z%)
- **Actual test work:** ~Ys (Z%)
## Fixes
### Fix N: <title>
**Commit:** `<commit message>`
**File:** `<path>`
**Time saved:** <before>s → <after>s (**-Xs**)
<Description of what changed and why.>
### Combined Result
| State | Wall time | Delta |
|---|---|---|
| Baseline | Xs | — |
| + fix 1 | Xs | -Xs |
| + fix 2 | Xs | -Xs |
| **Total saved** | | **-Xs (Nx)** |
## Remaining Slow Tests
| Test | Time | Reason |
|---|---|---|
| ... | ... | ... |
Commit the report as a separate docs: commit after all optimization commits.