Performance Optimization Workflow
1. Capture and Analyze
Make a request and get structured JSON — response metadata plus a full trace analysis:
uv run plain request /path --json
uv run plain request /path --json --user 1
uv run plain request /path --json --method POST --data '{"key": "value"}'
The --user flag accepts a user ID or email.
traces is normally a list — one entry per request, so a followed redirect chain returns one per hop in hop order. Analyze each on its own; the last entry is the page that actually rendered. Never sum counts across hops: a query the framework runs once per request would look like a repeat that no change can fix.
Check trace_note before indexing. traces is null when the OpenTelemetry SDK isn't installed and [] when nothing was captured; in both cases a trace_note sibling key says why. Report that instead of indexing into nothing.
A failed request returns a different shape. If the view raised, the command exits 1 and emits {"error": "...", "traces": [...]} — no response key. The trace is still there, and analysis.exceptions carries the stacktrace, so this is the payload to read when debugging a 500. Branch on "response" in payload before reaching for it.
Each entry has name (e.g. GET /admin/p/user, including any query string) and request_id — which matches response.request_id for the hop that rendered — plus analysis (derived) and spans (raw):
analysis.query_count— statements executed in this requestanalysis.transaction_count— transaction-control statements, counted apart from queries. Withplain.postgresthis is savepoint bookkeeping;BEGIN/COMMITare issued outside the instrumented cursor and never appearanalysis.duration_ms— that request's durationanalysis.exceptions— exceptions recorded on any span, withspan,error_type,message,stacktraceanalysis.queries— each distinct statement withcount,total_duration_ms, andsources, slowest firstsources— the call sites that issued the statement, aspath:line in functionstrings. The path says whose code it is: project paths vs installed-package (site-packages) pathsspans— raw OpenTelemetry spans, a flat list (parent_span_idgives the structure)
count means one thing: how many times that statement ran in that request. Nothing is pre-diagnosed.
2. Identify Bottlenecks
You do the diagnosing. The trace reports what ran; deciding what is wrong with it is the job. Read analysis.queries and look for:
- Repeats —
count > 1is the N+1 shape. Go read the code at the recorded call sites before concluding: three executions from three branches is not the same as three from a loop, and only the source tells you which. A source is where the query executed, which for lazy querysets can be framework code (a paginator, a template render) even though project code built the queryset — a repeat with onlysite-packagessources still usually traces back to a queryset the project constructed. - Slow statements — high
total_duration_ms. A single slow query outranks a dozen fast repeats. - Cost the counts don't show —
duration_msfar exceeding the sum of query time means the time went somewhere else; readspansfor it. - Missing indexes, unnecessary work in hot paths.
Never sum counts across hops — a query the framework runs once per request would look like a repeat that no change can fix.
3. Apply Fixes
- Add
select_related()/prefetch_related()for N+1 - Add database indexes for slow queries
- Cache expensive computations
4. Verify Improvement
Re-run uv run plain request /path --json and compare analysis.query_count, the count on the statement you changed, and analysis.duration_ms for the same trace — match traces by name, not position, since a fix can change the redirect chain.