Skip to main content

PyxTrace

Catch Python performance regressions in CI — deterministically,
so your benchmark gate stops crying wolf.

PyPI License CI

Recording a run, committing a change, and pyxtrace diff attributing the regression to the call that caused it

Narrated version (MP4, 1 min)


Why

Wall-clock benchmarks are noisy enough to make a pull-request gate unreliable. Run the same unchanged code seven times and time it, then count the operations it performed on those same runs (python benchmarks/overhead.py):

wall-clock       : 2.06ms  2.06ms  2.20ms  2.04ms  2.06ms  2.05ms  2.17ms   → 3.1% spread
operation counts : 3196    3196    3196    3196    3196    3196    3196     → 0.0% spread

That 3.1% is on an idle local machine, which is the best case — a shared CI runner with a noisy neighbour is worse, and smaller workloads are worse still (sub-millisecond ones swing well past 15%). Meanwhile the counts are not "stable," they are identical.

So on wall-clock you either set the threshold tight and get flaky failures, or set it wide and miss real regressions. Most teams end up deleting the job.

PyxTrace counts operations — function calls and lines executed — instead of measuring time. Those counts are byte-for-byte reproducible, so a 10% threshold actually means something. Sampling profilers cannot do this: sampling is non-deterministic by construction.

Install

pip install pyxtrace

One dependency (rich). Python 3.10+.

Use

Record a run per commit, then compare commits:

pyxtrace examples/orders.py            # → .pyxtrace/<commit sha>.pyxt
# ... commit your change ...
pyxtrace examples/orders.py
pyxtrace diff HEAD~1 HEAD              # any git ref, or a .pyxt path

diff exits 1 when something regressed, so it drops straight into CI.

What a caught regression looks like

The bundled example has a batched customer lookup. Set PYXTRACE_NPLUSONE=1 and it fetches each order's customer individually instead:

⚠  orders.py::process_order  +120 calls to other functions  (same code, more work)
   process_order() runs 120x and makes 1 more call(s) to fetch_customer() each time
   Attributed to:
     orders.py::fetch_customer   0 → 120 calls   new

⚠  orders.py::execute  +5,950% operations  (6 → 363)

⚠  orders.py::fetch_customer  new operations  (0 → 240)

✗ FAIL — 3 function(s) regressed

process_order ranks first: it is where the extra work was introduced. execute and fetch_customer merely run more often as a consequence.

Every line there is arithmetic over exact counts, and there is no confidence score because there is nothing to be uncertain about. Note also what it does not say. "Runs 120x and makes 1 more call each time" is a measurement; calling it an N+1 would be a conclusion, and one input size cannot support it. The same shape is produced by a function that simply does more work per call. Reading it is your job, and you have the counts to do it with.

A single run

pyxtrace examples/orders.py
                pyxTrace – orders.py
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ function                   ┃ calls ┃ lines ┃ own ms ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ orders.py::process_order   │   120 │   360 │    0.2 │
│ orders.py::handle_request  │     1 │   245 │    0.1 │
│ orders.py::fetch_orders    │     1 │   122 │    0.0 │
└────────────────────────────┴───────┴───────┴────────┘

calls and lines are deterministic. own ms is timing and varies run to run — it is shown for orientation and is never what diff gates on.

In CI

Run files are keyed by commit sha, so a PR gate records the base commit and the head and diffs them by name:

- uses: actions/checkout@v4
  with: { fetch-depth: 0 }                  # the base commit has to be present
- run: pip install pyxtrace
- run: pyxtrace benchmarks/workload.py      # HEAD → .pyxtrace/<sha>.pyxt
- run: |
    git checkout -q ${{ github.event.pull_request.base.sha }}
    pyxtrace benchmarks/workload.py         # base → .pyxtrace/<sha>.pyxt
    git checkout -q -
- run: pyxtrace diff ${{ github.event.pull_request.base.sha }} HEAD --threshold 10

Because the counts are exact, the base run can be recorded once and cached (actions/cache on .pyxtrace/) instead of re-run per PR; the same commit always produces the same bytes.

Options

Command What it does
pyxtrace run SCRIPT Profile → .pyxtrace/<commit sha>.pyxt (run is optional; -o overrides)
pyxtrace diff BASE HEAD Compare two commits (or .pyxt paths); exit 1 on regression
--threshold N Percent growth that fails the gate (default 10)
--min-ops N Ignore growth below N operations (default 10), so a 2→3 line change is not an alarm
--root DIR Profile this directory instead of the script's own, for an installed or out-of-tree package

Overhead

Measured on fib(22), a deliberately call-heavy worst case (python benchmarks/overhead.py):

fib(22) Real code
PyxTrace (default profiling path) ~49x ~9x
cProfile (stdlib, for reference) ~8x ~3.4x

fib(22) is a deliberate worst case: it does almost nothing between calls, so the per-event cost has nothing to amortise against. The real-code column is sqlglot parsing 838 SQL statements, which is the number to plan around. Trace a representative benchmark script, not your whole test suite.

Limits

Stated plainly, because a profiler that hides these is worse than none:

  • Work inside C is invisible. PyxTrace counts Python operations. A regression that lives in a C extension, a regex, NumPy, a database driver, or any I/O wait produces no extra Python operations and will not be caught. A measured example: moving the same workload onto a backtracking regex made it 230,000x slower with byte-identical counts, and the gate passed. This is the mirror image of what sampling profilers cannot do, and it is the first thing to check before adopting PyxTrace. It is strongest on pure-Python CPU-bound code: parsers, template engines, serializers, business logic.
  • Subprocesses are not traced. Threads and asyncio are: coroutines run on the event loop's thread, and worker threads are covered via threading.settrace. A thread started before tracing begins is not.
  • Only code under one root directory is profiled — the script's own directory by default, or --root DIR. Anything outside it is skipped, and a run that captured nothing but the entry script says so.
  • Operation counts are a proxy for cost, not a measure of it. An algorithmic change can cut operations and still be slower. Gate on counts, then confirm with a real timer.
  • Not for production. This is a deterministic tracer for CI. To profile a live process, use py-spy.

When to use something else

You want Use
Profile a running production process py-spy
Find where memory is allocated memray
A readable one-off wall-clock profile pyinstrument
A timeline / flame-graph UI VizTracer
Know whether this PR made things slower PyxTrace

Contributing

git clone https://github.com/AbhineetSaha/pyxTrace.git
cd pyxTrace
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

pytest -q                        # tests
python benchmarks/overhead.py    # overhead + determinism gate

The overhead gate fails past 150x. If a change pushes it over, that is the change to reconsider — this tool is only useful if people can afford to run it.

License

MIT — see LICENSE.

Metadata

Release files for pyxtrace 2.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyxtrace 2.1.0
File Size Uploaded Provenance Actions
pyxtrace-2.1.0.tar.gz 22.5 kB With provenance Details for pyxtrace-2.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pyxtrace 2.1.0
File Interpreter ABI Platform Actions
pyxtrace-2.1.0-py3-none-any.whl Python 3 none any Details for pyxtrace-2.1.0-py3-none-any.whl

Total release size: 40.7 kB

Release files / pyxtrace-2.1.0.tar.gz

Download URL pyxtrace-2.1.0.tar.gz
Size 22.5 kB
Tags Source
SHA-256 checksum
How to use checksums
a9aa5b7c5973db508d62b62321021507c252dd1c492ffaa7a269bef6e5e90759
BLAKE2b-256 checksum
How to use checksums
eb77bfe3a78dda2440d9cdb1aee87074ac2da1d07cc009dc5539261a9b27dc3a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / pyxtrace-2.1.0-py3-none-any.whl

Download URL pyxtrace-2.1.0-py3-none-any.whl
Size 18.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7c11de6c819eed58215631a5dd3a513b923a1c1a9e253efd4cc6b695a8a15ae8
BLAKE2b-256 checksum
How to use checksums
808dd3689a2ab242ee82c5d24815f405e9fe258354d52e42b3294fe3d2f720fa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Security for pyxtrace 2.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pyxtrace-2.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page