Skip to content

fix(server): keep newest queued waves on per-batch overflow#1276

Open
clintcan wants to merge 1 commit into
Devolutions:masterfrom
clintcan:fix-server-keep-newest-waves
Open

fix(server): keep newest queued waves on per-batch overflow#1276
clintcan wants to merge 1 commit into
Devolutions:masterfrom
clintcan:fix-server-keep-newest-waves

Conversation

@clintcan
Copy link
Copy Markdown

Problem

RdpServer::dispatch_server_events in crates/ironrdp-server/src/server.rs keeps the oldest 4 queued RdpsndServerMessage::Wave events per batch and drops the rest:

  let mut wave_limit = 4;
  for event in events.drain(..) {
      // ...
      RdpsndServerMessage::Wave(data, ts) => {
          if wave_limit == 0 {
              debug!("Dropping wave");
              continue;
          }
          wave_limit -= 1;
          rdpsnd.wave(data, ts)
      }
      // ...
  }

events.drain(..) iterates oldest-first; wave_limit counts down from 4. So the four waves that survive are always the four oldest in the batch, and any newer waves are dropped.

This means a one-time dispatch stall — e.g. a video encode holding the server mutex for longer than the audio sample interval — bakes itself in as a permanent audio-latency offset: the client gets the 4 stale waves and the fresh ones it should be playing next are gone. The intent of the limit appears to be the opposite — bound latency by discarding stale audio in favor of recent audio.

I hit this building a macOS RDP server on top of ironrdp-server: after any video-encode hiccup, audio playback would lag the source by hundreds of milliseconds and never recover.

Fix

Count the wave events up front, compute how many leading ones to skip (total - WAVE_KEEP), and skip those. Same WAVE_KEEP = 4 budget — applied to the right end of the batch.

  const WAVE_KEEP: usize = 4;
  let wave_total = events
      .iter()
      .filter(|e| matches!(e, ServerEvent::Rdpsnd(RdpsndServerMessage::Wave(..))))
      .count();
  let mut wave_skip = wave_total.saturating_sub(WAVE_KEEP);
  for event in events.drain(..) {
      // ...
      RdpsndServerMessage::Wave(data, ts) => {
          if wave_skip > 0 {
              wave_skip -= 1;
              debug!("Dropping stale wave");
              continue;
          }
          rdpsnd.wave(data, ts)
      }
      // ...
  }

Safety / scope

  • No behavior change in steady state. When fewer than WAVE_KEEP waves are queued (the common case — dispatch keeps up with capture), wave_skip == 0 and every wave is sent, exactly as today.

  • Only the recovery path after a stall changes. Instead of "keep the oldest 4, lose the rest" (permanent offset), it's now "skip the oldest, keep the newest 4" (transient gap, then back in sync).

  • One extra events.iter().filter(...).count() per dispatch call. The dispatch is already proportional to batch size, so this is the same big-O.

  • All comments and the WAVE_KEEP = 4 value are preserved; just retargeted.

Verification

Built and tested in the [Devolutions/IronRDP] workspace:

  cargo build -p ironrdp-server          # ok
  cargo clippy -p ironrdp-server         # ok (only pre-existing MSRV warning)
  cargo fmt -p ironrdp-server --check    # ok

End-to-end: my downstream project ran a vendored fork with exactly this patch through hundreds of mstsc / Microsoft Remote Desktop for Mac sessions during a long debugging cycle. Audio recovers cleanly from video-encode contention now; without the patch, latency grows monotonically across the session.

`dispatch_server_events` previously kept the oldest 4 queued
`RdpsndServerMessage::Wave` events per batch and dropped any beyond
that. Since `events.drain(..)` iterates oldest-first and `wave_limit`
counts down from 4, the first 4 waves in the batch were always the
ones sent — i.e. the *oldest* are kept, the *newest* are dropped.

That means a one-time dispatch stall (e.g. a video encode holding the
server lock) bakes itself in as a permanent audio offset: the client
plays the stale 4 waves and the fresh ones are gone. The intent was
clearly the opposite — bound latency by discarding stale audio in
favor of recent audio.

Compute the wave count up front and skip the leading `total - 4` waves
instead. Same `WAVE_KEEP = 4` budget, just applied to the right end of
the batch.

No behavior change in steady state (when fewer than 4 waves are
queued, no skip happens). Only the recovery path after a stall is
fixed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant