aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport/webrtc/sctp_stats.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-11 15:42:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-11 15:43:06 +0200
commit9d38418817d1894b31c93ece0113e503d82167cd (patch)
treeadf4c8126c90d60f6a16724d14eafb80f33d5289 /packages/meshbay-node/src/meshbay_node/transport/webrtc/sctp_stats.py
parentb39e73cbb279ab0274e8cd0b84304a602fe1d5ef (diff)
downloadmeshbay-9d38418817d1894b31c93ece0113e503d82167cd.tar.gz
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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc/sctp_stats.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/sctp_stats.py43
1 files changed, 43 insertions, 0 deletions
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)}")