Conversation
FfiClient.request built `(ctypes.c_ubyte * proto_len)(*proto_data)` on every call. Each `c_ubyte * n` creates a new ctypes array type, and CPython does not cache it, so every request left a type object in a reference cycle that only the cyclic garbage collector frees. The copy also unpacked the payload byte by byte. Cast a `c_char_p` over the serialized bytes instead. The native call receives the same pointer, nothing is copied, and no type is created.
zdurm
requested review from
changt,
cloudwebrtc,
lukasIO and
xianshijing-lk
as code owners
October 2, 2026 20:15
Contributor
There was a problem hiding this comment.
🔍 Devin Review: 1 flag
Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
ctypes reuses a c_ubyte * n type that another live object still holds, so in a shared test process the old per-request type could be found and the test would pass against it.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #848
FfiClient.requestbuilt(ctypes.c_ubyte * proto_len)(*proto_data)on every call. Eachc_ubyte * ncreates a new ctypes array type, and CPython does not cache array types, so every FFI request leaves a type object in a reference cycle. Only the cyclic garbage collector frees it. The old line also copied the payload one byte at a time.This PR casts a
c_char_pover the serialized bytes instead.livekit_ffi_requestreceives the same pointer, nothing is copied, and no type is created. (ctypes.castacceptsbytesat runtime, but typeshed rejects it, hence thec_char_p.)Why it matters
The FFI is called for every captured frame and every room operation, so the garbage grows with traffic. In a process that runs many agent sessions, these types survive into generation 2 and make each full collection longer. A full collection holds the GIL and pauses every event loop in the process.
Measurements
CPython 3.12.12, macOS arm64.
PyCArrayTypeobjects collected in generation 2, 18 agent sessions in one worker processThe new test
test_request_leaves_no_ctypes_array_type_in_garbagestarts a fresh interpreter, runs 50 real requests through the native library undergc.DEBUG_SAVEALL, and counts the ctypes array types in the collected garbage. A fresh interpreter is needed because ctypes reuses ac_ubyte * ntype that another live object still holds. Onmainit fails withassert '1' == '0'. With this change it passes.GIL hold on large requests
The removed line also unpacked the payload into one Python int per byte and stored them one at a time, holding the GIL for the whole copy. A large
ByteStreamWriter.writesends its payload in one request, so the copy blocked every other thread for the length of it. The cast passes the existing buffer, so the request holds the GIL only for the native call.The reproduction script in #848 sends a real request through
liblivekit_ffi(no server) while a second thread records its longest gap. Python 3.12,livekit==1.1.20, then the same install with this PR's_ffi_client.py:The machine was under other load during this run, so the before values are about twice the ones in #848. The ratio is the same.