Python: tprof 1.3.0: now with less overhead

Back in January, I introduced tprof, a targeting profiler for Python 3.12+ that measures the time spent in specific functions, rather than your whole program. As a reminder, here’s the basic usage, specifying a target function with -t and a script to run:
$ tprof -t lib:maths ./example.py
...
🎯 tprof results:
function calls total median ± σ min … max
lib:maths() 2 610ms 305ms ± 2ms 304ms … 307ms
Today, it’s my pleasure to announce tprof 1.3.0, a big release with the following changes.
Way less overhead
tprof previously stored each recorded call as a Python int inside a dict of lists, then used the statistics module to crunch the numbers at report time. These data structures were easy to write in an initial version and get the project going, but it added meaningful overhead to every monitored call.
The new release replaces all of that with per-thread C structures: a stack of entry times and a flat array of durations per target, storing raw nanosecond int64_t values instead of boxed Python ints. On Python 3.13+, it also reads timestamps with the new PyTime_PerfCounterRaw() instead of calling time.perf_counter_ns().
Most importantly, sys.monitoring callbacks are now disabled entirely for code that isn’t a target, so non-target functions run at full speed.
The results, benchmarking a trivial function on Python 3.13 (overhead versus an unprofiled call in parentheses):
| Before | After | |
|---|---|---|
| Target call | 469ns (+439) | 170ns (+141) |
| Non-target call | 440ns (+410) | 28ns (+0) |
| Memory per 1M calls | 34.8 MiB | 8.0 MiB |
| Report per 1M calls | ~430ms | ~3ms |
In short:
- Target functions have about a 3× reduction in overhead.
- Non-target functions have none at all.
- Memory use is 4× lower.
- Generating the final report is roughly 100× faster.
Comparing against a saved baseline
tprof’s existing comparison mode (-x/--compare) is great for comparing two functions side-by-side in one run, such as “before” and “after” versions while you’re mid-refactor. But sometimes the “before” version only exists on another Git branch. For that, this release adds --json and --baseline.
Run with --json <path> to write the statistics to a file (or - for stdout) as well as printing the usual report:
$ git checkout main
$ tprof -t lib:maths --json before.json ./example.py
Then, after checking out your change, pass that file to --baseline to get a “delta” column comparing each function’s median against the saved run:
$ git checkout my-optimization
$ tprof -t lib:maths --baseline before.json ./example.py
...
🎯 tprof results:
function calls total median ± σ min … max delta
lib:maths() 2 592ms 296ms ± 2ms 294ms … 297ms -3.11%
Median, not mean
The headline statistic in tprof’s report is now the median rather than the mean, since it’s more robust to the outliers that profiling data tends to contain, like garbage collection pauses and cold caches. Comparison mode deltas, and the new baseline deltas above, are computed from medians too. The median is computed exactly, in C, via quickselect over a scratch buffer at report time, so this change doesn’t add any recording overhead—only a small amount of extra work when the report is generated.
Programmatic access to results
The tprof() context manager / decorator now yields a list of FunctionStats objects, populated once the profiled block ends, so you can use the results in code rather than only reading the printed report:
from lib import maths
from tprof import tprof
with tprof(maths) as results:
maths()
(function_stats,) = results # unpack the single result for maths()
print(f"{function_stats.name} took {function_stats.median_ns}ns")
Each FunctionStats has name, calls, total_ns, min_ns, max_ns, median_ns, and stdev_ns, matching the fields in the --json output.
Fin
If you’re already using tprof, this release should make it easier to get correct results. And if you haven’t tried it yet, install it from PyPI and give it a go.
More tea, prof?
—Adam
Check out my new book Boost Your GitHub DX.
One summary email a week, no spam, I pinky promise.
Related posts:
Tags: python