Repository navigation
fix: Read and write regular files natively so streams release the GIL - #349
christopher5106 wants to merge 1 commit into
Conversation
A Stream serves every read, seek and write of the native library through a Python callback, tens of thousands per asset (one or more per PNG chunk or BMFF box), and each callback retakes the GIL. Signing or reading files from several threads is therefore slower than from one, and any busy Python thread stalls a signature for tens of seconds. When the wrapped object is a regular file on a 64-bit POSIX platform, Stream now hands the native library libc read/lseek/write with a private duplicate of the file descriptor as context: the native call never re-enters Python. The duplicate starts at the Python object's position and gives the final position back on close(), and closing the Python file meanwhile cannot hand its number to another file. In-memory streams, pipes, append-mode files and other platforms keep the Python callbacks, as does Stream.native_file_io = False. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| if os.name != "posix" or sys.maxsize <= 2**32: | ||
| return None | ||
| try: | ||
| libc = ctypes.CDLL(None) |
There was a problem hiding this comment.
Is there another solution for this without using libc?
There was a problem hiding this comment.
Good point. Doing it without libc means the native side has to read the file itself, so I opened contentauth/c2pa-rs#2916: it adds c2pa_create_stream_from_fd(fd) to the C API, a stream backed by a native File on a duplicate of the descriptor, on Windows too. Measured from this binding through that constructor, it gives the same results as this PR (8 threads: 2.2 → 16.3 signatures/s on a 4K PNG, 5.8 → 34.4 on a 73 MB MP4; 0.14–0.21 s instead of 36–95 s next to a busy thread). Once it's released, I'll rework this PR to call it instead of libc: no libc, and no POSIX-only restriction.
There was a problem hiding this comment.
I updated contentauth/c2pa-rs#2916 (contentauth/c2pa-rs#2916) so it covers files held in memory too. It now adds native file and memory streams to the C API.
tmathern
left a comment
There was a problem hiding this comment.
Is there a solution without adding a direct libc dependency?
Changes in this pull request
Closes #348.
Streamserves every read, seek and write of the native library through a Pythonctypescallback. That is tens of thousands of callbacks per asset, each retaking the GIL, so signing from several threads is slower than from one, and a busy Python thread stalls a signature for tens of seconds. Measurements are in #348.When the wrapped object is a regular file on a 64-bit POSIX platform,
Streamnow passesc2pa_create_streamlibcread/lseek/writeinstead, with a privateos.dup()of the file descriptor as the stream context. The native call never re-enters Python, so the GIL stays released for the whole call.close()moves the Python object to where the native library stopped, so callers see the same positions as before. In the tests, source and destination end at the same place in both modes.Stream. If the Python file is closed while theStreamis still alive, its descriptor number can be reused by another file, but the native side keeps reading the right one.fstatdoesn't report as a regular file), append-mode files, Windows and 32-bit builds keep the Python callbacks unchanged.Stream.native_file_io = False.Stream.uses_native_file_iotells which path a stream took.Same benchmark as #348, macOS arm64, 14 cores, in signatures per second:
New tests in
TestNativeFileStreams:BytesIOand append-mode files don't.io.FileIOsubclass records zero calls.tests/test_unit_tests.py(469 tests) andtests/test_unit_tests_threaded.py(88) pass, andmake check-formatis clean.Checklist
docs/usage.mdabout multithreaded use andStream.native_file_ioif you'd like one.TO DOitems (or similar) have been entered as GitHub issues and the link to that issue has been included in a comment.