Reading cProfile output without drowning in rows

By Priya Naidoo · Field Notes

Notebook and laptop on a desk

The first profile dump from a stressed Django view can look like a phone book. Before you chase every function above one percent, change how you sort.

Start with cumulative time on real traffic

Run python -m cProfile -o run.prof manage.py … against a fixture that mirrors production cardinality. Open with pstats and sort by cumulative, not time. Cumulative highlights frames that sit on the hot path even if their self time is tiny—think ORM hooks that trigger hundreds of queries.

Ignore transit frames until the top five stabilise

{built-in method builtins.exec} and import machinery often appear high on toy benchmarks. Re-run after warm-up requests discard cold-start noise. If the top five entries change wildly between runs, your reproduction is not steady yet—fix that before editing code.

Use callers to settle blame

When two business functions share a helper, the helper’s cumulative time is ambiguous. Drill into callers: if serialize_row is hot only under export_csv, patch there instead of micro-optimising the helper for all callers.

Pair with a wall-clock trace

cProfile measures CPU time, not waiting on Postgres. If cumulative time is low but latency is high, capture a concurrent py-spy sample during the same run. We often see gevent or asyncio code that looks idle in cProfile while blocked on I/O.

Document the snapshot

Export the top twenty lines to Markdown in your repo. Future you will forget which deploy introduced the regression; profiles age better than memory.

← Back to Field Notes