# Running upstream pyperformance against RustPython
Two different interpreters are involved whenever
[pyperformance](https://github.com/python/pyperformance) is pointed at
RustPython, and only one of them is RustPython itself:
- **The `pyperformance` CLI** (`pyperformance run --python `) is the
*host* process -- it parses arguments, decides which benchmarks to run, and
collects results. It always runs under a real CPython, regardless of what
`--python` points at; RustPython never has to run this command.
- **The benchmark being measured** runs as the `--python` target instead.
`pyperf`'s `Runner` re-execs that target as a subprocess to actually run the
timing loop, and the benchmark script that subprocess runs does
`import pyperf` itself to call the measurement API. So it's RustPython, as
that subprocess, that needs to import `pyperf` -- not run `pyperformance`.
`pyperf` hard-depends on `psutil`, a C-extension package, and RustPython has
no CPython C-API / extension-module loading support (`_imp.create_dynamic` /
`_imp.exec_dynamic` are not implemented, and `_sysconfigdata` doesn't provide
a full compiler config, so even building a C extension from source fails).
That's what breaks: not "installing the pyperformance CLI on RustPython" --
RustPython importing `pyperf` as a benchmark subprocess.
Workaround: `pyperf` already disables psutil usage on interpreters that
report `Py_GIL_DISABLED=1` in `sysconfig` (see `pyperf._utils.USE_PSUTIL`) --
this is exactly what RustPython reports, since it has no GIL. So `pyperf`
never actually calls into psutil at runtime on RustPython; a
functionality-free pure-Python `psutil` stub (see `stub_psutil/`) is enough
to satisfy pip's dependency resolution and let installation proceed.
This only unblocks *pure-Python* benchmarks. Any benchmark whose own
workload needs a real C extension (numpy, lxml, greenlet, PyYAML's C
accelerator, ...) will still fail to install -- and that failure is a real,
separate finding about RustPython's lack of C-extension support, not an
artifact of this workaround.
## Usage
```shell
# Build RustPython first (SSL feature needed for --install-pip, not required here)
cargo build --release
# Run every benchmark pyperformance knows about against it, and write a catalog
python3 scripts/pyperformance/run_all.py --rustpython target/release/rustpython
# Or just a subset
python3 scripts/pyperformance/run_all.py --benchmarks nqueens,richards,pyflate
```
Output goes to `scripts/pyperformance/results/`:
- `CATALOG.md` -- human-readable pass/fail table with the mean time and a
failure reason for each benchmark.
- `catalog.json` -- the same data, machine-readable.
- `raw/.json` -- pyperf's own result file for benchmarks that
passed.
The run resumes automatically: re-running the script skips any benchmark
already present in `catalog.json`. Pass `--force` to redo everything.
A host CPython venv (pyperformance itself only runs under a real CPython;
RustPython is just the `--python` target it benchmarks) and the stub wheel
are cached under `target/pyperformance/` (gitignored) and reused across
runs.
See `python3 scripts/pyperformance/run_all.py --help` for all options
(`--timeout`, `--rigorous` instead of `--fast`, `--cache-dir`, ...).
## Comparing against real CPython
`run_all.py` can target any Python executable, not just RustPython, via
`--python` + `--label`. For a real CPython you don't need (and don't want)
the psutil stub -- pass `--no-psutil-stub` so it installs and uses the
genuine `psutil`:
```shell
python3 scripts/pyperformance/run_all.py \
--python "$(command -v python3.13)" --label cpython3.13 --no-psutil-stub
python3 scripts/pyperformance/run_all.py \
--python target/release/rustpython --label rustpython
```
Each `--label` gets its own subdirectory under `results/` (its own
`catalog.json`, `CATALOG.md`, `raw/`), so multiple targets' catalogs coexist.
Then compare two catalogs:
```shell
python3 scripts/pyperformance/compare.py --baseline cpython3.13 --candidate rustpython
```
This writes `results/COMPARE-rustpython-vs-cpython3.13.md`: a per-benchmark
table with both targets' status/mean and the candidate/baseline time ratio,
plus the median slowdown across benchmarks both targets passed.
Note: pass an actual interpreter binary to `--python`, not a version-manager
shim (e.g. an `asdf`/`mise`/`pyenv` shim) -- pyperformance runs a helper
script through it directly, which some shims don't support. If unsure what a
`python3` on your PATH really resolves to, use
`python3 -c "import sys; print(sys.executable)"` and pass that path.