From 9d38418817d1894b31c93ece0113e503d82167cd Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 11 Oct 2026 15:42:52 +0200 Subject: feat(node): log what a transfer moved when it ends One INFO line per lease: bytes, duration, rate, and the SCTP association's window, round trip, losses and T3 expirations. Co-Authored-By: Claude Opus 5.5 --- .../meshbay_node/transport/webrtc/sctp_stats.py | 43 ++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/meshbay-node/src/meshbay_node/transport/webrtc/sctp_stats.py (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc/sctp_stats.py') diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/sctp_stats.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/sctp_stats.py new file mode 100644 index 0000000..338ded9 --- /dev/null +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/sctp_stats.py @@ -0,0 +1,43 @@ +"""Counts what aiortc's SCTP sender does not, applied on import (aiortc 1.15). + +aiortc halves its congestion window on every loss and drops it to one packet +when the T3 timer expires, and keeps no record of either. Those two counts are +what say whether a slow transfer was held back by the path: a window that never +grows past a few hundred KB is losses, and the node could not tell. + +A loss is counted when the sender enters fast recovery, so a burst of packets +dropped together is one loss, which is also one halving of the window. +""" + +from aiortc.rtcsctptransport import RTCSctpTransport + +_receive_sack_chunk = RTCSctpTransport._receive_sack_chunk +_t3_expired = RTCSctpTransport._t3_expired + + +async def _counting_receive_sack_chunk(self, chunk) -> None: + recovering = self._fast_recovery_exit is not None + await _receive_sack_chunk(self, chunk) + if not recovering and self._fast_recovery_exit is not None: + self._mb_losses = getattr(self, "_mb_losses", 0) + 1 + + +def _counting_t3_expired(self) -> None: + self._mb_t3 = getattr(self, "_mb_t3", 0) + 1 + _t3_expired(self) + + +RTCSctpTransport._receive_sack_chunk = _counting_receive_sack_chunk +RTCSctpTransport._t3_expired = _counting_t3_expired + + +def describe(pc) -> str: + """The association of `pc` as a log suffix, or nothing when it has none.""" + sctp = getattr(pc, "sctp", None) + if sctp is None: + return "" + srtt = getattr(sctp, "_srtt", None) + rtt = f"{srtt * 1000:.0f} ms" if srtt else "?" + return (f" sctp cwnd={sctp._cwnd // 1024} KB rtt={rtt}" + f" loss={getattr(sctp, '_mb_losses', 0)}" + f" t3={getattr(sctp, '_mb_t3', 0)}") -- cgit v1.2.3