Skip to content

pypy support: fix a cpyext incompatibility in chunkers/reader, adjust test expectations, see #1755 - #9976

Draft
ThomasWaldmann wants to merge 3 commits into
borgbackup:masterfrom
ThomasWaldmann:try-pypy
Draft

pypy support: fix a cpyext incompatibility in chunkers/reader, adjust test expectations, see #1755#9976
ThomasWaldmann wants to merge 3 commits into
borgbackup:masterfrom
ThomasWaldmann:try-pypy

Conversation

@ThomasWaldmann

@ThomasWaldmann ThomasWaldmann commented Jul 30, 2026

Copy link
Copy Markdown
Member

Summary

With this PR, borg works on pypy3.11, tested with a py3.11 nightly build
(pypy-c-jit-187982-8ed7cc691813, PyPy 8.0.0-alpha0 / Python 3.11.15) on macOS/arm64:
the full test suite passes there (2534 passed, 384 skipped, 1 xfailed), and CPython is
unaffected (full suite verified, no regressions). A nightly is required - the pypy fixes borg
needs are not in a release yet.

Changes

chunkers/reader: memcpy instead of memoryview slice assignment. This is what currently
keeps borg from running on pypy at all: mv[a:b] = src raises TypeError when the statement
is compiled into a C extension - it works in interpreted code, and item assignment works -
because pypy's cpyext memoryview lacks the assignment slot (pypy/pypy#5564). Without this,
506 of ~2900 tests fail, i.e. everything that writes an archive. Copying via memcpy()
through Cython typed memoryviews avoids creating the slice objects and is a wash on CPython:
1.689 vs 1.680 GB/s (best of 6, chunking a 2 GiB file through the block reader path).

chunkers/reader: limit the read size in the no-readv fallback. Not pypy-specific, but
that is where it hurts most: without os.readv (win32, pypy - pypy/pypy#5565) we read with
os.read, which allocates a buffer of the requested size no matter how few bytes it then
returns (pypy also zeroes it, pypy/pypy#5566). Asking for the whole free scan buffer (up to
8 MiB) therefore costs MBs of allocation per small file. Backing up 20000 small files
(82.6 MiB of data) requested 468 GiB in 60002 read calls; capping the request at 256 KiB -
same call count, same data - cuts the time in os.read from 15.1 s to 1.3 s:

uncapped capped at 256 KiB
pypy, 20000 small files 27.6 s 12.3 s
CPython without readv (= win32), same 4.04 s 2.42 s (readv path: 2.36 s)
chunking a 2 GiB file unaffected unaffected

pypy support proper, all of it about behaviour that is not a pypy bug:

  • platformflags: add is_pypy.
  • item_test: xfail test_unknown_property - setting undeclared attributes on cdef class
    instances is not blocked under cpyext.
  • msgpack_test: pypy only has the pure-python msgpack, expect it to be slow there.
  • lock_cmds_test: tolerate the pure-python msgpack warning on stderr, and use
    sys.executable instead of "python3" (robustness, not pypy-specific).

Performance

pypy remains slower than CPython for borg's workload and this PR does not change that:
borg create of a 2 GiB file is 4.8 s vs 10.8 s, 20000 small files 2.4 s vs 12.3 s (it was
27 s before the read-size cap above). Profiling says the rest is the cpyext tax, spread over
all of borg's C extensions rather than one hot spot - a trivial C ext call is 304 ns on pypy
vs 45 ns on CPython (a pure python call is 1 ns vs 50 ns), a 4 KiB bytes object created in
C 3.0 us vs 0.6 us, AES-OCB on a 2 KiB chunk 20.8 us vs 2.0 us, lz4 12.2 us vs 1.0 us,
ChunkIndex set+get 20.2 us vs 1.8 us. Where borg is plain Python, pypy is faster
(borg create -n, walk and stat only: 0.71 s vs 0.43 s including interpreter startup).
So: compatibility, not speed.

See #1755. The CI job running the suite on a pypy nightly came from #10142 and #10143.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.88%. Comparing base (7d81c68) to head (4be2170).
⚠️ Report is 1 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9976      +/-   ##
==========================================
- Coverage   86.90%   86.88%   -0.02%     
==========================================
  Files         101      101              
  Lines       17863    17864       +1     
  Branches     2708     2708              
==========================================
- Hits        15523    15521       -2     
- Misses       1636     1637       +1     
- Partials      704      706       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

ThomasWaldmann and others added 2 commits August 18, 2026 04:53
pypy's cpyext memoryview does not support slice assignment (mv[a:b] = src):
the statement works in interpreted code, but raises TypeError when it is
compiled into a C extension, like in this .pyx module. That broke about 500
tests on pypy - basically every operation that writes an archive.

Copying via memcpy through Cython typed memoryviews avoids creating the
slice objects and is a wash on CPython (1.689 vs 1.680 GB/s, best of 6,
chunking a 2 GiB file through the block reader path).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>


borg works on pypy3.11: tested with a py3.11 nightly build (8ed7cc691813,
PyPy 8.0.0-alpha0) on macOS/arm64, where the full test suite passes
(2534 passed, 1 xfailed) and CPython is unaffected.

The pypy incompatibilities found in the first round (see borgbackup#1755 and the
previous version of this branch) are all fixed in pypy now, so their
workarounds are gone again: fcntl.F_FULLFSYNC (pypy/pypy#5543), hmac.digest
with memoryview/bytearray (pypy/pypy#5544), os.link(follow_symlinks=False)
(pypy/pypy#5545). The chunk-data memoryview leak (pypy/pypy#5546) is fixed,
too: chunking 21.5 GB without releasing the memoryviews now peaks at 252 MB
instead of growing linearly with the data volume.

What remains is not pypy bugs:
- platformflags: add is_pypy.
- item_test: xfail test_unknown_property - setting undeclared attributes on
  cdef class instances is not blocked under cpyext.
- msgpack_test: pypy only has the pure-python msgpack, so expect it to be
  slow there.
- lock_cmds_test: tolerate the pure-python msgpack warning on stderr, and
  use sys.executable instead of "python3" (robustness, not pypy-specific).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rgbackup#1755

Without os.readv (win32, pypy) we read via os.read and copy into the scan
buffer. os.read allocates a buffer of the requested size, no matter how few
bytes it then returns, so requesting the whole free scan buffer (up to 8MiB
for the default max chunk size) is expensive for small files: one allocation
of that size per read call, several calls per file.

Backing up 20000 small files (82.6MiB of data) requested 468GiB in 60002
os.read calls. Capping the request at 256KiB - same call count, same data -
cuts the time spent in os.read from 15.1s to 1.3s and the total runtime from
27.6s to 12.3s on pypy (which also zeroes the allocation). On CPython without
readv it is 4.04s -> 2.42s, close to the 2.36s of the readv path. Chunking a
2GiB file is unaffected (the big reads there return what they ask for).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant