Skip to content

fix(server): measure bandwidth across a single large graphics write - #2021

Open
Greg Lamberson (glamberson) wants to merge 2 commits into
Devolutions:masterfrom
lamco-admin:fix/bandwidth-measure-large-write
Open

Greg Lamberson (glamberson) wants to merge 2 commits into
Devolutions:masterfrom
lamco-admin:fix/bandwidth-measure-large-write

Conversation

@glamberson

@glamberson Greg Lamberson (glamberson) commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Continuous bandwidth measurement opened a fixed window of RTT ticks (Start, four ticks, Stop). The client counts the ordinary traffic between Start and Stop (MS-RDPBCGR 3.2.5.14), so a window over a quiet desktop timed idle time: on a 2 ms LAN the server reported a few hundred kbps, and mstsc told the user the network was slow.
  • When an EGFX write of at least 10 KiB goes out, the server now sends Bandwidth Measure Start immediately before it and Stop immediately after, on the same stream, so the client times a burst the link actually carried. At most one such measurement per second, and never while one is outstanding. gnome-remote-desktop measures the same way.
  • The tick window takes over whenever no bracketed measurement has started for eight RTT ticks, so a session without EGFX, or one whose only large frame was the initial render, keeps measuring.
  • A bandwidth measurement the client never answers, or a bracket whose Stop is never sent, is abandoned after the same 30 s as an unanswered RTT probe (expire_stale_probes), instead of stopping bandwidth measurement for the rest of the session. This also covers the tick window, which could already get stuck that way.
  • A Bandwidth Measure Results with a zero time delta now counts as one millisecond instead of discarding the figure: The client's timer has millisecond resolution, and a burst on a fast link routinely completes within one. A result with no bytes counted still clears the stored figure.
  • New public API: AutoDetectManager::begin_bandwidth_measure and AutoDetectManager::end_bandwidth_measure. Additive; build_bandwidth_measure is unchanged.

Validation

cargo xtask check fmt/lints/tests/typos/locks all pass. Nine new tests in ironrdp-testsuite-core cover the bracket, the size threshold, the pacing, the tick window staying closed while brackets recur and resuming when they stop, the expiry of an unended bracket and of an unanswered measurement, a slow tick window still reaching its Stop, and the zero-delta rule. The existing test that treated a zero time delta as unusable now checks a zero byte count instead.

@github-actions github-actions Bot added kind/protocol Affects RDP or related protocol behavior risk/medium Behavioral change that does not substantially alter a core public API size/M Size: up to 449 counted lines and 10 files; exceeds S in either measure triage/overlap Possible overlap with another pull request; advisory only labels Sep 27, 2026
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

This pull request may overlap with #2031.

Both bracket large EGFX server writes with Bandwidth Measure Start/Stop for auto-detect per MS-RDPBCGR: this PR brackets message-channel writes, while #2031 brackets EGFX batches carried on the UDP tunnel with tunnel sub-headers. The shared scope is the server's bandwidth measurement around EGFX traffic, which a human should assess for interaction.

This notice is advisory only. Automated review continues as usual, and how these pull requests relate is for maintainers and authors to decide.

Note

LLM-assisted content (no human feedback).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR brackets large EGFX writes with Bandwidth Measure Start/Stop so the client times a real burst, retires the tick-window measurement after the first bracket, floors zero time deltas at one millisecond, and rejects zero byte counts. Independent inspection confirms the bracketing is protocol-conforming, the zero-delta floor only under-reports, and the in-tree Egfx caller pairs begin/end and aborts on write errors. Published: a one-way latch retires the tick window after a single large write, leaving one-large-write-then-quiet sessions without any bandwidth re-measurement; the new public bracket API can wedge the state machine with no timeout or cancel; and three no-behavior-change compression nits in the new code.

Comment thread crates/ironrdp-server/src/autodetect.rs Outdated
Comment thread crates/ironrdp-server/src/autodetect.rs
Comment thread crates/ironrdp-server/src/server.rs Outdated
Comment thread crates/ironrdp-server/src/autodetect.rs Outdated
Comment thread crates/ironrdp-server/src/autodetect.rs Outdated
@github-actions github-actions Bot added ai-reviewed/1 One automated review completed and removed needs-review A human reviewer is the current next actor labels Sep 28, 2026
@github-actions github-actions Bot added needs-review A human reviewer is the current next actor and removed needs-review A human reviewer is the current next actor labels Sep 28, 2026
@CBenoit Benoît Cortier (CBenoit) added automation-failed Exact-head automated classification or review failed or was unavailable and removed needs-review A human reviewer is the current next actor labels Sep 30, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR 2021 replaces the fixed tick-window bandwidth measurement (which times idle desktop stretches and reports implausibly low figures) with a Start/data/Stop bracket around EGFX writes of at least 10 KiB, paced to one per second, with the tick window kept as a fallback after eight bracket-free ticks, and adds 30 s expiry for abandoned bandwidth transactions. Independent inspection confirms the mechanism is correct: the bracketed continuous Start/Stop go out back-to-back with matching sequence numbers on the message channel, brackets never overlap and hold off the tick window, the tick counter reset keeps the fallback closed only while brackets recur, expiry covers both a Stop that is never sent and an unanswered measurement without cutting short an open window, and sequence allocation is centralized. The wire-level integration in server.rs matches the existing auto-detect path. Remaining concerns are the unverified zero-timeDelta semantic inversion (which also deviates from the spec-ma…

  1. [protocol + skeptical] Zero timeDelta clamped to 1 ms without client-behavior evidence, deviating from the spec-mandated calculation — medium 🟠 ❓ — crates/ironrdp-server/src/autodetect.rs
    measured_bandwidth_kbps inverts the previously tested semantic (zero timeDelta made the result unusable and cleared the stored figure) into clamp-to-1-ms-and-report. The timer-resolution argument is physically plausible and the old rule would discard most bracketed measurements on fast links, but nothing in the repository shows clients never use timeDelta 0 as a no-measurement sentinel, and the PR cites no evidence that gnome-remote-desktop clamps zero deltas. A client that does mean 'invalid' receives a fabricated byte_count*8 kbps figure (capped at u32::MAX, ~4.3 Tbps) advertised in RDP_NETCHAR_RESULTS. The handling also intentionally deviates from the MS-RDPBCGR-mandated (byteCount*8)/timeDelta calculation at both degenerate inputs (zero bytes yields None rather than 0 kbps; zero delta floors to 1 ms where the formula is undefined); the deviation is defensible and partly documented, but the divergence from the MUST clause should be documented against the spec. Missing client-behavior context prevents a firm conclusion.
  2. [skeptical] measured_bandwidth_kbps duplicates the PDU crate's computed_bandwidth_kbps, which now has no production callers — low 🟡 — crates/ironrdp-server/src/autodetect.rs
    After this change the server computes bandwidth from the raw Bandwidth Measure Results fields with a private helper, and a search confirms AutoDetectResponse::computed_bandwidth_kbps (ironrdp-pdu/src/rdp/autodetect.rs:613) is referenced only by its own unit tests; the server was its sole production caller. The same wire formula now lives in two crates with deliberately divergent zero handling, so any future correction (rounding, overflow, units) must be found and mirrored by hand, and the two implementations will silently disagree on what a client's result means. The stated goal of avoiding the impossible branch did not require duplication: the special case could wrap a call to the PDU helper or the helper could take the clamped delta.

Comment thread crates/ironrdp-server/src/autodetect.rs Outdated
Comment thread crates/ironrdp-server/src/autodetect.rs Outdated
Comment thread crates/ironrdp-server/src/server.rs Outdated
@github-actions github-actions Bot added ai-reviewed/2 Final automated review completed needs-author-action The pull request author is the current next actor and removed ai-reviewed/1 One automated review completed automation-failed Exact-head automated classification or review failed or was unavailable labels Sep 30, 2026
The continuous measurement opened a fixed window of RTT ticks, and the
client counts only the traffic that happens to pass between Start and
Stop, so a window over a quiet desktop timed idle time and reported a
fast link as a few hundred kbps. Bracket one EGFX write of at least
10 KiB with Start and Stop instead, at most once a second, so the
client times a burst the link carried. Sessions without EGFX keep the
tick window until the first bracketed measurement.

A zero time delta now counts as one millisecond: the client's timer has
millisecond resolution and a burst on a fast link completes within one.
A result with no bytes counted still clears the stored figure.
The first bracketed measurement no longer turns the tick window off
for good: each bracketed Start resets the tick count, so the window
takes over again once eight ticks pass without one.

A bracketed measurement, or one waiting for the client's results, is
now dropped by expire_stale_probes once it is older than the RTT probe
age, so a Stop that is never sent or a client that never answers no
longer stops measurement for the rest of the session. A tick window
that is still open is not timed, since it sends its own Stop.

Also folds the Start and Stop writes into let chains, passes the
measured fields to measured_bandwidth_kbps, and allocates sequence
numbers through one helper.
@glamberson
Greg Lamberson (glamberson) force-pushed the fix/bandwidth-measure-large-write branch from 95b0633 to 5cdf387 Compare October 1, 2026 14:16
@github-actions github-actions Bot added automation-failed Exact-head automated classification or review failed or was unavailable risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny and removed needs-author-action The pull request author is the current next actor labels Oct 1, 2026
@github-actions github-actions Bot removed the risk/medium Behavioral change that does not substantially alter a core public API label Oct 1, 2026

This branch was successfully deployed

1 active deployment
llm-providers — 5cdf387f Deployed Oct 1, 2026 by glamberson via Classify pull request #1543
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed/2 Final automated review completed automation-failed Exact-head automated classification or review failed or was unavailable kind/protocol Affects RDP or related protocol behavior risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny size/M Size: up to 449 counted lines and 10 files; exceeds S in either measure triage/overlap Possible overlap with another pull request; advisory only

Development

Successfully merging this pull request may close these issues.

2 participants