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}