Share a pyinstrument report
pyinstrument's terminal output is excellent for the person who ran it and useless for everybody else. It collapses to your terminal width, hides frames under its own significance threshold, and turns into an unreadable block of box-drawing characters the moment it is pasted into Slack.
The HTML renderer is the shareable one:
pyinstrument --html -o profile.html manage.py some_slow_command
or, in code, around the part you actually care about:
from pyinstrument import Profiler
profiler = Profiler(async_mode="enabled")
profiler.start()
run_the_slow_thing()
profiler.stop()
with open("profile.html", "w") as f:
f.write(profiler.output_html())
That file is self-contained — the viewer and the profile data are both inlined — which makes it easy to produce and awkward to deliver. It is too big to paste, it downloads instead of opening when attached to a ticket, and Slack will not preview it.
Publish it
Drag profile.html into the app, or send it to the
API:
curl -fsS -X POST "https://commareports.com/api/v1/reports" \
-H "Authorization: Bearer $COMMA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile html profile.html \
--arg t "pyinstrument — nightly export command" '{title: $t, html: $html}')"
Report HTML renders with scripts enabled inside a sandboxed iframe
(allow-scripts, no allow-same-origin), so the collapsible tree, the
timeline/call-stack toggle, and the "hide irrelevant frames" control all work
for the reader — including the branches you did not expand before sending it.
Profiling a Django or FastAPI request
The common case is not a script, it is one slow endpoint. pyinstrument ships middleware for exactly this:
# settings.py
MIDDLEWARE = ["pyinstrument.middleware.ProfilerMiddleware", *MIDDLEWARE]
PYINSTRUMENT_PROFILE_DIR = "profiles"
Every request with ?profile writes an HTML profile into profiles/. Publish
the one that matters rather than trying to describe it:
curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
-H "Authorization: Bearer $COMMA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile html "$(ls -t profiles/*.html | head -1)" \
--arg t "GET /api/orders — 4.2s p99" '{title: $t, html: $html}')"
PATCHing one report id keeps a single URL per endpoint and appends a revision per capture, so "did the index actually help?" is a diff rather than a memory.
What review adds
- Anchored threads on the expensive frame — see commenting on HTML.
- Revisions, so before-and-after is one artifact with one link.
- Access per report — a profile exposes your internal module layout, so private, team or named-reviewer access is the right default. See the sharing model.
Limits
- Entry HTML: 5 MB. pyinstrument bundles its viewer into every file, so a short profile is already a few hundred KB; long captures of async code with many distinct stacks are the ones to watch.
- 60 requests/minute per token.
Try it
Comma is free — unlimited reports, unlimited commenters, unlimited revision history.