Bump to v3.0.2: fix slow native-memory leak on the receive side

Bug: Andre reported audio latency feeling laggier after long sessions
on his Win10 desktop receiving Opus from his laptop. His 23-hour log
showed working set climbing from 83 MB at startup to 3.5 GB at the
end, with the managed heap staying tiny (~5-7 MB) the whole time.
CPU climbed alongside (from steady-state ~7% mid-session to peaks of
~60% by the end) and audio threads ended up doing ~4x the work they
did at the start. Andre's perception of latency drift was the CPU
pressure showing up in audio scheduling, not the buffer itself
growing (bufAvg stayed roughly stable at 25-28 ms).

Root cause: Concentus.Native (introduced in v2.2 / shipped in v3.0)
returns concrete NativeOpusDecoder / NativeOpusEncoder objects that
implement IDisposable and own native libopus state. Three call sites
were taking the IOpusDecoder / IOpusEncoder interface reference and
never calling Dispose:

  * StreamSession.Dispose — comment literally said "IOpusDecoder has
    no Dispose; nothing else to free", which was correct for the
    pure-managed Concentus.OpusDecoder pre-v2.2 but stopped being
    correct the moment we added the native binding
  * OpusEncoderState.Dispose — same misleading comment, same bug
  * SenderLane.OnCodecChanged — overwrote the existing encoder field
    without disposing the old instance on codec change

Compounding factor: Program.Main sets GCSettings.LatencyMode =
GCLatencyMode.SustainedLowLatency to keep audio scheduling smooth
(it suppresses gen2 collections). That's correct for the hot path
but it ALSO suppresses the finalizer pass that would have released
the leaked native handles as a backstop. Because the managed heap
stayed tiny, the GC never saw enough pressure to force a gen2 pass
on its own, and the native state piled up indefinitely. Multi-output
receive multiplied the per-output growth.

Fix is in two parts:

1. Call (... as IDisposable)?.Dispose() at every release point —
   StreamSession.Dispose, OpusEncoderState.Dispose,
   SenderLane.OnCodecChanged before overwrite, AudioRecorder's
   Concentus.Oggfile-backed OpusOggFileWriter.Dispose. The
   as-IDisposable cast handles both the native and the pure-managed
   path transparently (managed-only IOpusDecoder isn't IDisposable;
   the as-cast yields null and the null-conditional is a no-op).

2. Periodic native-memory reaper in MainForm.SnapshotLogIfDue — once
   every 300 snapshot ticks (~5 min), run
   GC.Collect(2, Optimized, blocking, !compacting) +
   WaitForPendingFinalizers on a background Task.Run so the gen2
   work doesn't hitch the UI thread. Audio threads are separate and
   unaffected. Serves as belt-and-braces for any future code path we
   forget to wire and for cleaning up any per-call native scratch
   the underlying library might accumulate that isn't owned by a
   single .NET wrapper.

Expected behaviour after fix: working set settles around 100-200 MB
on a typical receive session and holds roughly flat for as long as
the app stays running. CPU stays at its early-session baseline
across multi-hour sessions. Andre's "latency drift" symptom should
disappear.

Wire format unchanged; same codec list, same UI, same defaults.
v3.0.2 talks to other v3.0.x peers exactly as v3.0 / v3.0.1 do.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-05-27 23:16:51 +01:00
co-authored by Claude Opus 4.7
parent d904bafe73
commit 8aa8d0c3bd
8 changed files with 135 additions and 16 deletions
+43
View File
@@ -20,6 +20,49 @@ internal sealed class AboutDialog : Form
/// updates" path.</summary>
private const string ReleaseNotes =
"""
RemSound v3.0.2
Hot-fix for a slow memory leak in the receive side. If
you leave RemSound running for many hours receiving
audio, its memory use was creeping up steadily small
at first, but big enough after a day to slow the
computer down and make audio feel slightly laggy.
What was happening: RemSound uses a small library
called Concentus to decode incoming Opus audio. In
version 2.2 we switched it from a pure software
version to a faster native version. The native version
keeps some memory in Windows itself rather than in
RemSound, and you're supposed to tell it explicitly
when you're done with it. Our code wasn't doing that
it was relying on the system to notice and tidy up
eventually, but a setting we use to keep audio smooth
also stops Windows from doing that tidy-up. Result:
memory built up over hours and never came back.
The fix is twofold. First, RemSound now does that
tidy-up properly every time it finishes with a piece
of the audio pipeline at the end of a session, when
you change codec, and when a recording stops. Second,
as a backstop in case any other piece of the puzzle
ever has the same shape of bug, RemSound now does a
quick "release any leftover memory" pass once every
five minutes in the background. The pass doesn't
affect audio it runs on its own and is over before
the next audio packet arrives.
Reported by a user whose desktop was using 3.5 GB of
memory after running RemSound continuously for nearly
a full day. After the fix, expect memory to settle at
around 100-200 megabytes and stay there for as long as
you leave RemSound running.
Nothing else has changed from v3.0.1 same wire
format, same codec list, same everything. If you've
not noticed any slowdown after long sessions, the fix
is still worth having because the leak was happening
under the surface even if you didn't see it.
RemSound v3.0.1
Hot-fix for a bug in the "Automatically open my router