# Triage profiler output and pick the one fix worth trying first

> Paste cProfile, py-spy or Chrome profiler output and get the top hotspots by self time, an Amdahl-style ceiling, and one change to try first.

- **Author:** [Jack Thompson (@jack_thompson)](https://promptabide.com/jack_thompson)
- **Tested on:** Claude · Opus 5.5
- **You fill in:** `what`, `profile`, `target`
- **Published:** 2026-07-25
- **Updated:** 2026-09-24
- **Tags:** `coding`, `debugging`, `data-analysis`
- **Keywords:** analyze cprofile output with ai, find performance bottleneck from profiler, django n+1 query slow export, how to read python profiler output, speed up slow csv export endpoint
- **Views:** 2276
- **Likes:** 34

**Best for:** Developers with a slow endpoint or job and a profile in hand, who need to know where the next hour of optimization should go.

## Prompt

```
Here's profiler output from a slow {{what}}. Help me decide where to spend the next hour.

Profile:
{{profile}}

Fast enough means: {{target}}

1. Separate self time from total (cumulative) time, and name the top 3 hotspots by self time.
2. For each one, say whether it's (a) unnecessary work, (b) necessary work done inefficiently, or (c) waiting on I/O, locks or GC. Say what in the profile tells you: call counts, the ratio of self to total time, or the function name.
3. Estimate the ceiling: if the biggest hotspot dropped to near zero, what's the best-case total? Can the target be reached by fixing it alone? Show the arithmetic.
4. Recommend one change to try first, with code if you can, and the measurement that would prove it worked.
5. List anything else worth fixing to reach the target, in order.

Don't suggest micro-optimizations for anything under 5% of the total time.
```

## Variables

- `{{what}}` — What is slow (e.g. Django endpoint that exports 20,000 orders to CSV)
- `{{profile}}` — The profiler output, pasted (e.g. cProfile output sorted by cumulative time)
- `{{target}}` — What "fast enough" means, as a number (e.g. Under 2 seconds for 20k rows)

## Example input

````
what: Django 5 endpoint that exports 20,000 orders to CSV
target: under 2 seconds for 20k rows
profile:
```
         4180331 function calls in 9.812 seconds
   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.004    0.004    9.812    9.812 orders/export.py:12(export_orders_csv)
    20000    0.061    0.000    6.944    0.000 django/db/models/fields/related_descriptors.py:226(__get__)
    20001    0.412    0.000    7.187    0.000 django/db/backends/utils.py:98(execute)
    20001    6.338    0.000    6.338    0.000 {method 'recv_into' of '_socket.socket' objects}
    20000    0.090    0.000    2.401    0.000 orders/export.py:40(format_row)
   140000    0.198    0.000    1.902    0.000 babel/numbers.py:664(format_currency)
   140000    0.884    0.000    1.244    0.000 babel/numbers.py:1150(parse_pattern)
    20001    0.410    0.000    0.410    0.000 {method 'writerow' of '_csv.writer' objects}
```
````

## Output

*Produced by Claude · Opus 5.5.*

````
**1. Top 3 by self time (tottime)**
1. `recv_into`: **6.34 s** (65%)
2. `parse_pattern`: **0.88 s** (9%)
3. `execute`: 0.41 s, tied with `writerow` at 0.41 s (4% each)

**2. What kind of cost each one is**
- **`recv_into`: (c) waiting, caused by (a) unnecessary work.** It's socket reads from the database. The giveaway is the call count: 20,001 queries for one export. `related_descriptors.__get__` is called 20,000 times with 6.94 s cumulative. That's `order.customer` lazy-loading one customer per row, the classic N+1.
- **`parse_pattern`: (a) unnecessary work.** 140,000 calls means the currency format pattern is parsed again for every money value (7 per row). The pattern never changes.
- **`execute` / `writerow`:** necessary work at a reasonable cost. Below the 5% line, so leave them.

**3. Ceiling**
The N+1 path costs 6.94 s. Replacing it with one joined query costs about the same as the initial query, say 0.1–0.3 s.
9.81 − 6.94 + 0.3 ≈ **3.2 s best case**. That misses the 2 s target on its own. Add the formatting fix below (2.40 s down to roughly 0.5 s) and you reach about **1.3 s**, which meets it.

**4. Try first: fetch customers in the same query**
```python
orders = (
    Order.objects.filter(created_at__range=(start, end))
    .select_related("customer")          # one JOIN instead of 20,000 queries
    .only("id", "created_at", "total", "tax", "customer__email", "customer__name")
    .iterator(chunk_size=2000)
)
```
Proof it worked: rerun the profile. `execute` should drop from 20,001 calls to 1 (or a handful with chunking), and `related_descriptors.__get__` cumulative time should fall to milliseconds. In tests, wrap the export in `assertNumQueries(1)` so the N+1 can't come back.

**5. Next, in order**
1. **Stop re-parsing the currency pattern.** If the CSV is machine-read, write plain decimals (`f"{amount:.2f}"`) and let the spreadsheet format them. If it needs locale formatting, parse the pattern once outside the loop and reuse it, instead of calling `format_currency` 140,000 times.
2. Re-profile. If you're still above 2 s, look at whatever is now on top rather than guessing. The ordering usually changes once the N+1 is gone.
````

## Why it works

Splitting **self time from cumulative time** is the skill most people skip when reading profiles. It shows that `recv_into` is where time goes, but the call count shows why. The **(a)/(b)/(c) classification** forces a diagnosis (unnecessary vs inefficient vs waiting) before any fix. The **ceiling calculation with visible arithmetic** is the most useful step: it showed the obvious N+1 fix alone couldn't hit the 2 s target, so the second fix was planned up front. The **5% floor** prevents busywork on things that don't matter.

## When not to use it

A single profile of a single run misleads when the slowness is intermittent: cold caches, lock contention, noisy neighbors or GC pauses under load. Use repeated measurements or production tracing (APM, `pg_stat_statements`) for those. Sampling profilers on async code can also attribute waiting time strangely, so confirm the diagnosis with a before/after timing, not just the profile.

---

Canonical HTML: https://promptabide.com/bides/triage-profiler-output-find-bottleneck
Agent guide: https://promptabide.com/llms.txt · https://promptabide.com/agent-instructions.md
Sitemap: https://promptabide.com/sitemap.xml
