code wiki / (root) / nx_bandwidth_meter.nx

nx_bandwidth_meter.nx source

↩ module page · 205 lines · 7607 B

1// nx_bandwidth_meter.nx -- per-session bandwidth + RTT observation. 2// 3// Tracks the realized over-the-wire bytes/sec and RTT of a multiplayer 4// session so upstream code (nx_adaptive_bitrate) can pick a quality 5// tier matched to the player's actual link. 6// 7// Bytes accumulator: EWMA (exponentially-weighted moving average) over 8// recent send + receive events. EWMA alpha = 1/8 in Q14 (~0.125) by 9// default which gives ~1-second smoothing at 60 Hz updates. 10// 11// RTT accumulator: same EWMA pattern over round-trip-time samples. 12// Caller provides RTT samples in milliseconds via observe_rtt_ms(). 13// 14// Loss accumulator: caller increments observe_loss() each time a 15// packet/sample is detected dropped; the meter tracks loss-rate. 16// 17// All math in Q14 fixed-point per substrate convention. 18// 19// Source references (open): 20// - RFC 6298 (TCP RTT calculation, srtt/rttvar) 21// - RFC 8312 (CUBIC congestion control RTT/loss observations) 22// - BBR paper (Cardwell et al 2016) -- bottleneck bandwidth estimation 23// 24// Composes with nx_adaptive_bitrate for the verdict. 25// 26// genealogy_id: rfc6298_tcp_rtt + bbr_bottleneck_bw + ewma_smoothing 27// lineage_id: per_session_link_observation 28 29// nx_safety_envelope: 30// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 31// sil_target: SIL1 32// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 33// verdict: NOT_YET_EVALUATED 34 35import "nx_syscalls.nx" 36import "nx_tier.nx" 37const NX_MAGIC_9500: i64 = 9500 38const NX_MAGIC_10500: i64 = 10500 39const NX_MAGIC_100000: i64 = 100000 40 41const NX_BM_Q: i64 = 16384 42 43// EWMA alpha = 2048 / 16384 = 0.125. Time constant ~ 8 samples. 44const NX_BM_EWMA_ALPHA_Q14: i64 = 2048 45 46const NX_BM_HDR_BYTES_PER_SEC_Q14: nx_int = 0 47const NX_BM_HDR_RTT_MS_Q14: nx_int = 1 48const NX_BM_HDR_LOSS_RATE_Q14: nx_int = 2 49const NX_BM_HDR_TOTAL_BYTES: nx_int = 3 50const NX_BM_HDR_TOTAL_SAMPLES: nx_int = 4 51const NX_BM_HDR_TOTAL_LOSSES: nx_int = 5 52const NX_BM_HDR_LAST_OBS_TICK: nx_int = 6 53const NX_BM_HDR_SIZE: nx_int = 7 54 55// ===== Allocation ==================================================== 56 57func nx_bandwidth_meter_new() -> *i64 { 58 let m: *i64 = (sys_mmap(NX_BM_HDR_SIZE * 8)) as *i64 59 var i: nx_int = 0 60 while i < NX_BM_HDR_SIZE { 61 m[i] = 0 62 i = i + 1 63 } 64 return m 65} 66 67// ===== Observations ================================================= 68 69// EWMA update: new = (1 - alpha) * old + alpha * sample, all Q14. 70func _bm_ewma(old_q14: i64, sample_q14: i64) -> i64 { 71 let one_minus_alpha: i64 = NX_BM_Q - NX_BM_EWMA_ALPHA_Q14 72 let kept: i64 = (old_q14 * one_minus_alpha) / NX_BM_Q 73 let added: i64 = (sample_q14 * NX_BM_EWMA_ALPHA_Q14) / NX_BM_Q 74 return kept + added 75} 76 77// Observe `bytes` sent/received in the most recent `interval_ms`. The 78// meter folds (bytes / interval_ms * 1000) bytes/sec into the EWMA. 79func nx_bandwidth_meter_observe_bytes( 80 m: *i64, bytes: nx_int, interval_ms: nx_int 81) -> nx_int { 82 if (m as i64) == 0 { return -1 } 83 if interval_ms <= 0 { return -1 } 84 // bytes_per_sec = bytes * 1000 / interval_ms; encode as Q14. 85 let bps_int: i64 = (bytes as i64) * 1000 / (interval_ms as i64) 86 let bps_q14: i64 = bps_int * NX_BM_Q 87 let cur: i64 = m[NX_BM_HDR_BYTES_PER_SEC_Q14] 88 m[NX_BM_HDR_BYTES_PER_SEC_Q14] = _bm_ewma(cur, bps_q14) 89 m[NX_BM_HDR_TOTAL_BYTES] = m[NX_BM_HDR_TOTAL_BYTES] + (bytes as i64) 90 m[NX_BM_HDR_TOTAL_SAMPLES] = m[NX_BM_HDR_TOTAL_SAMPLES] + 1 91 return 0 92} 93 94// Observe an RTT sample in milliseconds. 95func nx_bandwidth_meter_observe_rtt_ms(m: *i64, rtt_ms: nx_int) -> nx_int { 96 if (m as i64) == 0 { return -1 } 97 if rtt_ms < 0 { return -1 } 98 let sample_q14: i64 = (rtt_ms as i64) * NX_BM_Q 99 let cur: i64 = m[NX_BM_HDR_RTT_MS_Q14] 100 if cur == 0 { 101 // First sample seeds the EWMA directly to avoid the slow ramp-up. 102 m[NX_BM_HDR_RTT_MS_Q14] = sample_q14 103 } else { 104 m[NX_BM_HDR_RTT_MS_Q14] = _bm_ewma(cur, sample_q14) 105 } 106 return 0 107} 108 109// Observe loss: caller passes (losses, total) for the most recent 110// window. Meter folds loss_rate = losses/total into the EWMA. 111func nx_bandwidth_meter_observe_loss( 112 m: *i64, losses: nx_int, total: nx_int 113) -> nx_int { 114 if (m as i64) == 0 { return -1 } 115 if total <= 0 { return -1 } 116 let sample_q14: i64 = ((losses as i64) * NX_BM_Q) / (total as i64) 117 let cur: i64 = m[NX_BM_HDR_LOSS_RATE_Q14] 118 m[NX_BM_HDR_LOSS_RATE_Q14] = _bm_ewma(cur, sample_q14) 119 m[NX_BM_HDR_TOTAL_LOSSES] = m[NX_BM_HDR_TOTAL_LOSSES] + (losses as i64) 120 return 0 121} 122 123// ===== Read accessors =============================================== 124 125func nx_bandwidth_meter_bytes_per_sec(m: *i64) -> i64 { 126 if (m as i64) == 0 { return 0 } 127 return m[NX_BM_HDR_BYTES_PER_SEC_Q14] / NX_BM_Q 128} 129 130func nx_bandwidth_meter_rtt_ms(m: *i64) -> i64 { 131 if (m as i64) == 0 { return 0 } 132 return m[NX_BM_HDR_RTT_MS_Q14] / NX_BM_Q 133} 134 135func nx_bandwidth_meter_loss_rate_q14(m: *i64) -> i64 { 136 if (m as i64) == 0 { return 0 } 137 return m[NX_BM_HDR_LOSS_RATE_Q14] 138} 139 140func nx_bandwidth_meter_total_bytes(m: *i64) -> i64 { 141 if (m as i64) == 0 { return 0 } 142 return m[NX_BM_HDR_TOTAL_BYTES] 143} 144 145// ===== Self-test ==================================================== 146 147func main() -> i64 { 148 let m: *i64 = nx_bandwidth_meter_new() 149 if (m as i64) == 0 { return __syscall(93, 1, 0, 0, 0, 0, 0) } 150 151 // T1: empty meter reads 0. 152 if nx_bandwidth_meter_bytes_per_sec(m) != 0 { return __syscall(93, 2, 0, 0, 0, 0, 0) } 153 if nx_bandwidth_meter_rtt_ms(m) != 0 { return __syscall(93, 3, 0, 0, 0, 0, 0) } 154 155 // T2: observe 1000 bytes over 100ms = 10000 B/s. After many 156 // samples the EWMA should converge. 157 var k: nx_int = 0 158 while k < 100 { 159 nx_bandwidth_meter_observe_bytes(m, 1000, 100) 160 k = k + 1 161 } 162 let bps: i64 = nx_bandwidth_meter_bytes_per_sec(m) 163 // Allow a small EWMA convergence window: target 10000, accept 9500-10500. 164 if bps < NX_MAGIC_9500 { return __syscall(93, 10, 0, 0, 0, 0, 0) } 165 if bps > NX_MAGIC_10500 { return __syscall(93, 11, 0, 0, 0, 0, 0) } 166 167 // T3: observe RTT samples around 50ms. 168 var r: nx_int = 0 169 while r < 50 { 170 nx_bandwidth_meter_observe_rtt_ms(m, 50) 171 r = r + 1 172 } 173 let rtt: i64 = nx_bandwidth_meter_rtt_ms(m) 174 if rtt < 48 { return __syscall(93, 20, 0, 0, 0, 0, 0) } 175 if rtt > 52 { return __syscall(93, 21, 0, 0, 0, 0, 0) } 176 177 // T4: loss-rate observation. 5/100 sampled, repeated -> EWMA 178 // converges to 5/100 = 0.05 = ~819 in Q14. 179 var l: nx_int = 0 180 while l < 100 { 181 nx_bandwidth_meter_observe_loss(m, 5, 100) 182 l = l + 1 183 } 184 let loss_q14: i64 = nx_bandwidth_meter_loss_rate_q14(m) 185 // 0.05 * 16384 = 819.2; accept 750-900. 186 if loss_q14 < 750 { return __syscall(93, 30, 0, 0, 0, 0, 0) } 187 if loss_q14 > 900 { return __syscall(93, 31, 0, 0, 0, 0, 0) } 188 189 // T5: total-bytes counter is exact (not EWMA'd). 190 if nx_bandwidth_meter_total_bytes(m) != NX_MAGIC_100000 { return __syscall(93, 40, 0, 0, 0, 0, 0) } 191 192 // T6: bandwidth step -- start observing 100 B/s, EWMA should track 193 // the change. Use a fresh meter. 194 let m2: *i64 = nx_bandwidth_meter_new() 195 var s: nx_int = 0 196 while s < 200 { 197 nx_bandwidth_meter_observe_bytes(m2, 100, 1000) 198 s = s + 1 199 } 200 let bps2: i64 = nx_bandwidth_meter_bytes_per_sec(m2) 201 if bps2 < 90 { return __syscall(93, 50, 0, 0, 0, 0, 0) } 202 if bps2 > 110 { return __syscall(93, 51, 0, 0, 0, 0, 0) } 203 204 return 0 205}