Skip to content

Release FFI handles on Room.Disconnect#278

Open
MaxHeimbrock wants to merge 3 commits intomainfrom
max/fix-room-handle-leak-on-disconnect
Open

Release FFI handles on Room.Disconnect#278
MaxHeimbrock wants to merge 3 commits intomainfrom
max/fix-room-handle-leak-on-disconnect

Conversation

@MaxHeimbrock
Copy link
Copy Markdown
Contributor

@MaxHeimbrock MaxHeimbrock commented May 8, 2026

Summary

Room never disposed its FFI handles on disconnect — the Rust-side room (peer connection, signaling, libwebrtc state) plus every wrapped participant, publication, track, and Rtc source lingered until C# GC eventually finalized each SafeHandle. On a connect / hold / disconnect cycle the FFI handle table grew by ~10 entries that were never freed within the session.

Changes

  • Release Room FFI handles on disconnectRoom is now IDisposable. Disconnect() and the server-initiated Disconnected event both run a single idempotent Cleanup() that unsubscribes from FfiClient events and walks LocalParticipant + RemoteParticipants, disposing each participant + its publications + the publications' tracks before disposing RoomHandle itself. MeetManager.OnDestroy also disconnects so scene change / quit releases the handles.
  • Release local publication and Rtc source FFI handles on cleanupLocalTrackPublication now wraps the FFI handle returned from PublishTrackCallback and disposes it through the same cascade. RtcVideoSource and RtcAudioSource Dispose(bool) overrides now dispose their Handle.

Test plan

  • FFI handle table returns to baseline after connect / hold / disconnect with mic + camera published and a remote with mic + camera subscribed (verified via a temporary Rust diagnostic).
  • Verified each commit compiles individually (bisectable).
  • iOS build succeeds; tested on device.

Before

Before connecting:
LiveKit: [ffi-handles-diag] total=0 rooms=0 participants=0 tracks=0 publications=0 video_sources=0 audio_sources=0 video_streams=0 audio_streams=0 audio_frames=0 data_buffers=0 byte_boxes=0 other=0

During connection:
LiveKit: [ffi-handles-diag] total=10 rooms=1 participants=2 tracks=2 publications=2 video_sources=0 audio_sources=0 video_streams=1 audio_streams=1 audio_frames=0 data_buffers=0 byte_boxes=1 other=0

After disconnecting:
LiveKit: [ffi-handles-diag] total=8 rooms=1 participants=2 tracks=0 publications=2 video_sources=0 audio_sources=0 video_streams=0 audio_streams=0 audio_frames=0 data_buffers=0 byte_boxes=1 other=2

Disconnected idle at 5 minutes:
LiveKit: [ffi-handles-diag] total=1 rooms=0 participants=0 tracks=0 publications=0 video_sources=0 audio_sources=0 video_streams=0 audio_streams=0 audio_frames=0 data_buffers=0 byte_boxes=1 other=0

🤖 Generated with Claude Code

Fails without Rust fix: https://github.com/livekit/client-sdk-unity/actions/runs/25560080036?pr=278

MaxHeimbrock and others added 2 commits May 8, 2026 16:31
Room never disposed its RoomHandle on Disconnect(), and Participant /
TrackPublication / Track never disposed theirs at all. Each handle is an
independent entry in the Rust FFI handle table, so dropping one does not
cascade. The handles only got freed when GC eventually finalized each
SafeHandle, which in practice meant the entire Rust-side room (peer
connection, signaling client, libwebrtc state) plus every wrapped
participant, publication, and track lingered for an unpredictable amount
of time after the user-visible session had ended.

Make Room implement IDisposable. Disconnect() now sends the FFI request
and then runs Cleanup(), which unsubscribes from FfiClient events,
walks LocalParticipant + RemoteParticipants disposing each
participant + its publications + the publications' tracks, and finally
disposes RoomHandle itself. The same Cleanup() runs from the
Disconnected room event so server-initiated drops behave the same as
client-initiated ones. OnEventReceived guards against late events
arriving after Cleanup, so the FfiClient unsubscribe race is harmless.

DisposeHandles is added as an internal cascade: Track disposes its own
handle, TrackPublication forwards to its Track, RemoteTrackPublication
also disposes its publication handle, and Participant walks _tracks
before disposing its own handle.

The Meet sample's OnDestroy also calls Disconnect now so a scene change
or quit while still connected releases the handles instead of leaking
them until process exit.

Verified with the FFI handle table diagnostic: a connect / hold /
disconnect cycle now drops rooms, participants, tracks, and remote
publications back to zero. Local publications and audio/video source
handles still leak on this path; those are addressed separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the previous commit, Room cleanup walked participants and disposed
their handles, including the publication handles for remote tracks. Three
local-side handles still leaked on every disconnect:

- LocalTrackPublication never wrapped its FFI handle. When PublishTrack
  succeeded, OnPublish constructed the C# publication from the proto info
  alone and dropped e.Publication.Handle on the floor. The Rust side kept
  the entry alive in the FFI handle table for the rest of the process.

- RtcVideoSource and RtcAudioSource owned an FFI source handle but their
  Dispose(bool) implementations released the preview texture, capture
  buffer, and pending audio frames without ever disposing the handle. The
  source therefore stayed registered with Rust until the SafeHandle
  finalizer eventually ran.

Wrap the publication handle in the PublishTrackInstruction callback and
dispose it through the existing DisposeHandles cascade. Add Handle
disposal to the two Rtc source Dispose(bool) overrides so the Meet
sample's CleanUpAllTracks now actually frees them.

With this change, disconnecting after publishing local mic + camera
returns the FFI handle table to its pre-connect baseline on macOS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MaxHeimbrock MaxHeimbrock force-pushed the max/fix-room-handle-leak-on-disconnect branch from 097c4d7 to f234120 Compare May 8, 2026 14:31
@MaxHeimbrock MaxHeimbrock changed the base branch from max/audiostream-resample-rust to main May 8, 2026 14:31
@MaxHeimbrock MaxHeimbrock force-pushed the max/fix-room-handle-leak-on-disconnect branch from 4631a7f to 149e4f3 Compare May 8, 2026 14:46
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