1use std::cell::RefCell;
50use std::rc::Rc;
51
52use libfw_core::{Capabilities, IntRange};
53use serde::{Deserialize, Serialize};
54use wasm_bindgen::JsValue;
55
56pub const TUNE_WINDOW_MS: f64 = 1_000.0;
58pub const GAIN_THRESHOLD: f64 = 0.05;
60pub const RTT_INFLATE_THRESHOLD: f64 = 0.30;
62pub const DEGRADE_FACTOR: f64 = 0.5;
64pub const RTT_EWMA_ALPHA: f64 = 0.25;
66
67pub const HARD_MAX_CONCURRENCY: i64 = 64;
77pub const HARD_MAX_WINDOW: i64 = 64;
79pub const HARD_MAX_CHUNK_SIZE: i64 = 16 * 1024 * 1024;
81pub const HARD_MAX_IN_FLIGHT_BYTES: u64 = 256 * 1024 * 1024;
87pub const HARD_MAX_IN_FLIGHT_REQUESTS: usize = 64;
95pub const LEVEL_SAMPLE_SIZE: usize = 64 * 1024;
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(rename_all = "lowercase")]
112pub enum TunePhase {
113 Uninitialized,
115 Ramping,
117 Settled,
119 Degraded,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct TuneStats {
127 pub rtt_ms: f64,
129 pub mbps: f64,
131}
132
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct TuneParams {
137 pub concurrency: usize,
139 pub upload_window: usize,
141 pub download_window: usize,
143 pub chunk_size: u64,
145 pub compress_level: i32,
147}
148
149pub fn request_budget(concurrency: usize, per_file_window: usize) -> usize {
159 concurrency
160 .max(1)
161 .saturating_mul(per_file_window.max(1))
162 .clamp(1, HARD_MAX_IN_FLIGHT_REQUESTS)
163}
164
165pub const DEFAULT_TUNE_TTL_MS: u64 = 60 * 60 * 1_000;
174
175pub const TUNE_CACHE_VERSION: u32 = 1;
178
179pub trait TuneStore {
186 fn get(&self, key: &str) -> Option<String>;
188 fn set(&self, key: &str, value: &str);
190 fn remove(&self, key: &str);
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
196pub struct TuneCache {
197 pub v: u32,
199 pub caps_hash: String,
201 pub saved_at_ms: u64,
203 pub params: TuneParamsCache,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
213pub struct TuneParamsCache {
214 pub concurrency: usize,
215 pub upload_window: usize,
216 pub download_window: usize,
217 pub chunk_size: u64,
218}
219
220impl TuneParamsCache {
221 fn of(params: &TuneParams) -> Self {
222 TuneParamsCache {
223 concurrency: params.concurrency,
224 upload_window: params.upload_window,
225 download_window: params.download_window,
226 chunk_size: params.chunk_size,
227 }
228 }
229
230 fn into_params(self, compress_level: i32) -> TuneParams {
231 TuneParams {
232 concurrency: self.concurrency,
233 upload_window: self.upload_window,
234 download_window: self.download_window,
235 chunk_size: self.chunk_size,
236 compress_level,
237 }
238 }
239}
240
241pub fn tune_key(origin: &str, direction: TransferKind) -> String {
248 let dir = match direction {
249 TransferKind::Upload => "upload",
250 TransferKind::Download => "download",
251 };
252 format!("libfw.tune.v{TUNE_CACHE_VERSION}.{dir}.{origin}")
253}
254
255pub fn origin_of(url: &str) -> String {
260 let (scheme, rest) = match url.split_once("://") {
261 Some((s, r)) => (s, r),
262 None => return String::new(),
263 };
264 if !scheme.eq_ignore_ascii_case("http") && !scheme.eq_ignore_ascii_case("https") {
265 return String::new();
266 }
267 let authority = rest
269 .split(['/', '?', '#'])
270 .next()
271 .unwrap_or("")
272 .trim();
273 if authority.is_empty() {
274 return String::new();
275 }
276 format!("{}://{}", scheme.to_ascii_lowercase(), authority.to_ascii_lowercase())
277}
278
279impl TuneParams {
280 pub fn request_budget(&self) -> usize {
282 request_budget(self.concurrency, self.upload_window.max(self.download_window))
283 }
284
285 pub fn from_caps_mins(caps: &Capabilities) -> TuneParams {
287 TuneParams {
288 concurrency: floor_of(&caps.limits.concurrency, HARD_MAX_CONCURRENCY) as usize,
289 upload_window: floor_of(&caps.limits.upload_window, HARD_MAX_WINDOW) as usize,
290 download_window: floor_of(&caps.limits.download_window, HARD_MAX_WINDOW) as usize,
291 chunk_size: floor_of(&caps.limits.chunk_size, HARD_MAX_CHUNK_SIZE) as u64,
292 compress_level: caps.compression.zrip_levels.min,
293 }
294 }
295
296 pub fn from_config(
301 concurrency: usize,
302 upload_window: usize,
303 download_window: usize,
304 chunk_size: u64,
305 compress_level: i32,
306 caps: &Capabilities,
307 ) -> TuneParams {
308 TuneParams {
309 concurrency: bounded(
310 concurrency.max(1) as i64,
311 &caps.limits.concurrency,
312 HARD_MAX_CONCURRENCY,
313 ) as usize,
314 upload_window: bounded(
315 upload_window.max(1) as i64,
316 &caps.limits.upload_window,
317 HARD_MAX_WINDOW,
318 ) as usize,
319 download_window: bounded(
320 download_window.max(1) as i64,
321 &caps.limits.download_window,
322 HARD_MAX_WINDOW,
323 ) as usize,
324 chunk_size: bounded(
325 chunk_size.max(1) as i64,
326 &caps.limits.chunk_size,
327 HARD_MAX_CHUNK_SIZE,
328 ) as u64,
329 compress_level: caps.clamp_level(compress_level),
330 }
331 }
332
333 pub fn clamped_into(&self, caps: &Capabilities) -> TuneParams {
336 TuneParams {
337 concurrency: bounded(
338 self.concurrency as i64,
339 &caps.limits.concurrency,
340 HARD_MAX_CONCURRENCY,
341 ) as usize,
342 upload_window: bounded(
343 self.upload_window as i64,
344 &caps.limits.upload_window,
345 HARD_MAX_WINDOW,
346 ) as usize,
347 download_window: bounded(
348 self.download_window as i64,
349 &caps.limits.download_window,
350 HARD_MAX_WINDOW,
351 ) as usize,
352 chunk_size: bounded(
353 self.chunk_size as i64,
354 &caps.limits.chunk_size,
355 HARD_MAX_CHUNK_SIZE,
356 ) as u64,
357 compress_level: caps.clamp_level(self.compress_level),
358 }
359 }
360
361 pub fn upload_in_flight(&self) -> u64 {
363 self.concurrency as u64 * self.upload_window as u64 * self.chunk_size
364 }
365
366 pub fn download_in_flight(&self) -> u64 {
368 self.concurrency as u64 * self.download_window as u64 * self.chunk_size
369 }
370}
371
372fn floor_of(range: &IntRange, hard_max: i64) -> i64 {
378 range.min.clamp(1, hard_max.max(1))
379}
380
381fn bounded(v: i64, range: &IntRange, hard_max: i64) -> i64 {
387 let hard_max = hard_max.max(1);
388 let v = v.max(1);
389 if range.min > range.max {
390 return v.min(hard_max).max(1);
393 }
394 let lo = range.min.clamp(1, hard_max);
395 let hi = range.max.min(hard_max).max(lo);
396 v.clamp(lo, hi)
397}
398
399pub fn ewma(prev: Option<f64>, sample: f64, alpha: f64) -> f64 {
405 match prev {
406 None => sample,
407 Some(p) => alpha * sample + (1.0 - alpha) * p,
408 }
409}
410
411pub fn bdp_bytes(mbps: f64, rtt_ms: f64) -> u64 {
413 (mbps * 1e6 / 8.0 * rtt_ms / 1_000.0).max(0.0) as u64
414}
415
416pub const TARGET_CHUNK_SPAN_MS: f64 = 100.0;
428
429const CHUNK_ALIGN: u64 = 64 * 1024;
436
437pub fn chunk_for_link(mbps: f64, range: &IntRange, budget_max: u64) -> u64 {
443 let floor = floor_of(range, HARD_MAX_CHUNK_SIZE).max(1) as u64;
444 if !mbps.is_finite() || mbps <= 0.0 {
445 return floor.min(budget_max.max(1)).max(1);
446 }
447 let bytes = (mbps * 1e6 / 8.0 * TARGET_CHUNK_SPAN_MS / 1_000.0).clamp(0.0, i64::MAX as f64) as u64;
449 let aligned = (bytes / CHUNK_ALIGN).saturating_mul(CHUNK_ALIGN);
450 let clamped = bounded(aligned.max(1) as i64, range, HARD_MAX_CHUNK_SIZE) as u64;
451 clamped.clamp(1, budget_max.max(1))
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq)]
456pub enum RampDim {
457 Window,
459 Concurrency,
461}
462
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
465pub enum RampAction {
466 Raise,
468 AdvanceDim,
470 Settle,
472 Degrade,
474 Hold,
476}
477
478#[derive(Debug, Clone, Copy)]
480pub struct RampInput {
481 pub mbps: f64,
483 pub prev_mbps: f64,
485 pub rtt_inflation: f64,
487 pub errors: u32,
489 pub degraded: bool,
491 pub ramping: bool,
496 pub low_gain_windows: u32,
498 pub in_flight: u64,
500 pub bdp: u64,
508 pub at_cap: bool,
510 pub last_dim: bool,
512}
513
514pub fn ramp_action(input: &RampInput) -> (RampAction, u32) {
533 if input.errors > 0 {
534 return (RampAction::Degrade, 0);
535 }
536 if input.at_cap {
537 return if input.last_dim {
538 (RampAction::Settle, 0)
539 } else {
540 (RampAction::AdvanceDim, 0)
541 };
542 }
543 if input.bdp > 0 && input.in_flight >= input.bdp {
544 return (RampAction::Settle, 0);
545 }
546 if input.rtt_inflation > RTT_INFLATE_THRESHOLD {
547 return (RampAction::Settle, 0);
548 }
549 let gain = if input.degraded {
550 0.0
554 } else if input.prev_mbps > 0.0 {
555 input.mbps / input.prev_mbps - 1.0
556 } else if input.ramping {
557 1.0
560 } else {
561 0.0
566 };
567 if input.ramping && gain >= GAIN_THRESHOLD {
572 return (RampAction::Raise, 0);
573 }
574 let low = input.low_gain_windows + 1;
575 if low >= 2 {
576 if input.degraded || input.last_dim {
583 (RampAction::Settle, low)
584 } else {
585 (RampAction::AdvanceDim, 0)
586 }
587 } else {
588 (RampAction::Hold, low)
589 }
590}
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq)]
594pub enum CompressLevel {
595 Auto,
597 Fast,
599 Balanced,
601 Max,
603 Fixed(i32),
605}
606
607pub fn resolve_level(level: CompressLevel, caps: &Capabilities) -> i32 {
611 let z = caps.compression.zrip_levels;
612 match level {
613 CompressLevel::Fast => z.min,
614 CompressLevel::Balanced | CompressLevel::Auto => z.default,
615 CompressLevel::Max => z.max,
616 CompressLevel::Fixed(l) => z.clamp_level(l),
617 }
618}
619
620pub fn choose_auto_level(
629 candidates: &[(i32, usize, f64)],
630 uncompressed: usize,
631 mbps: f64,
632) -> i32 {
633 if candidates.is_empty() {
634 return 0;
635 }
636 let bytes_per_ms = if mbps > 0.0 {
638 mbps * 1e6 / 8.0 / 1_000.0
639 } else {
640 10e6 / 8.0 / 1_000.0
642 };
643 let max_saved = candidates
644 .iter()
645 .map(|&(_, clen, _)| uncompressed.saturating_sub(clen))
646 .max()
647 .unwrap_or(0);
648 if max_saved == 0 {
649 return candidates.iter().map(|&(l, _, _)| l).min().unwrap_or(0);
651 }
652 let mut best = (candidates[0].0, f64::NEG_INFINITY);
653 for &(lvl, clen, cms) in candidates {
654 let saved = uncompressed.saturating_sub(clen) as f64;
655 let score = saved / bytes_per_ms - cms;
656 if score > best.1 {
658 best = (lvl, score);
659 }
660 }
661 best.0
662}
663
664#[derive(Debug, Clone)]
670pub struct TuneEvent {
671 pub phase: TunePhase,
672 pub params: TuneParams,
673 pub stats: TuneStats,
674}
675
676pub type TuneHandle = Rc<RefCell<TuningEngine>>;
678
679pub struct TuningEngine {
681 enabled: bool,
682 level_cfg: CompressLevel,
683 policy_level: i32,
687 phase: TunePhase,
688 caps: Option<Capabilities>,
689 caps_hash: String,
690 params: TuneParams,
691 stats: TuneStats,
692 auto_level: Option<i32>,
694 auto_level_benchmarked: bool,
696 window_start_ms: f64,
698 window_done_base: u64,
699 window_errors: u32,
700 windows_closed: u32,
701 rtt_ewma: Option<f64>,
702 rtt_baseline: Option<f64>,
708 prev_mbps: f64,
709 low_gain_windows: u32,
710 dim: RampDim,
711 degraded_windows: u32,
712 settled: Option<TuneParams>,
716 settled_caps_hash: String,
718 settled_direction: Option<TransferKind>,
721 post_degrade_settle: bool,
724 direction: TransferKind,
726 store: Option<Rc<dyn TuneStore>>,
729 cache_ttl_ms: u64,
731 origin: String,
734 on_tuning: Option<js_sys::Function>,
736}
737
738impl TuningEngine {
739 pub fn new(enabled: bool, level_cfg: CompressLevel) -> TuningEngine {
741 TuningEngine {
742 enabled,
743 level_cfg,
744 policy_level: libfw_core::ZRIP_DEFAULT_LEVEL,
745 phase: TunePhase::Uninitialized,
746 caps: None,
747 caps_hash: String::new(),
748 params: TuneParams {
750 concurrency: 1,
751 upload_window: 1,
752 download_window: 1,
753 chunk_size: 1,
754 compress_level: 0,
755 },
756 stats: TuneStats::default(),
757 auto_level: None,
758 auto_level_benchmarked: false,
759 window_start_ms: 0.0,
760 window_done_base: 0,
761 window_errors: 0,
762 windows_closed: 0,
763 rtt_ewma: None,
764 rtt_baseline: None,
765 prev_mbps: 0.0,
766 low_gain_windows: 0,
767 dim: RampDim::Window,
768 degraded_windows: 0,
769 settled: None,
770 settled_caps_hash: String::new(),
771 settled_direction: None,
772 post_degrade_settle: false,
773 direction: TransferKind::Download,
774 store: None,
775 cache_ttl_ms: DEFAULT_TUNE_TTL_MS,
776 origin: String::new(),
777 on_tuning: None,
778 }
779 }
780
781 pub fn set_cache(&mut self, store: Rc<dyn TuneStore>, ttl_ms: u64) {
787 self.store = Some(store);
788 self.cache_ttl_ms = ttl_ms;
789 }
790
791 pub fn cache_ttl_ms(&self) -> u64 {
793 self.cache_ttl_ms
794 }
795
796 pub fn set_origin(&mut self, url: &str) {
798 self.origin = origin_of(url);
799 }
800
801 fn cache_key(&self) -> Option<String> {
804 if self.store.is_none() || self.cache_ttl_ms == 0 || self.origin.is_empty() {
805 return None;
806 }
807 Some(tune_key(&self.origin, self.direction))
808 }
809
810 fn remove_cache_entry(&self) {
812 if let (Some(store), Some(key)) = (&self.store, self.cache_key()) {
813 store.remove(&key);
814 }
815 }
816
817 fn load_cached_settle(&mut self, now_ms: f64, caps: &Capabilities) -> Option<TuneParams> {
825 let key = self.cache_key()?;
826 let store = self.store.clone()?;
827 let raw = store.get(&key)?;
828 let entry: TuneCache = match serde_json::from_str(&raw) {
829 Ok(entry) => entry,
830 Err(_) => {
831 store.remove(&key);
833 return None;
834 }
835 };
836 let aged = now_ms - entry.saved_at_ms as f64;
837 let fresh = aged >= 0.0 && aged <= self.cache_ttl_ms as f64;
838 if entry.v != TUNE_CACHE_VERSION || entry.caps_hash != caps.caps_hash() || !fresh {
839 store.remove(&key);
840 return None;
841 }
842 Some(entry.params.into_params(self.policy_level))
843 }
844
845 fn save_cached_settle(&self, now_ms: f64) {
847 let Some(key) = self.cache_key() else { return };
848 let Some(store) = self.store.as_ref() else { return };
849 let entry = TuneCache {
850 v: TUNE_CACHE_VERSION,
851 caps_hash: self.caps_hash.clone(),
852 saved_at_ms: if now_ms > 0.0 { now_ms as u64 } else { 0 },
853 params: TuneParamsCache::of(&self.params),
854 };
855 if let Ok(json) = serde_json::to_string(&entry) {
856 store.set(&key, &json);
857 }
858 }
859
860 pub fn set_direction(&mut self, kind: TransferKind) {
863 self.direction = kind;
864 }
865
866 pub fn direction(&self) -> TransferKind {
868 self.direction
869 }
870
871 pub fn set_on_tuning(&mut self, cb: Option<js_sys::Function>) {
873 self.on_tuning = cb;
874 }
875
876 pub fn phase(&self) -> TunePhase {
878 self.phase
879 }
880
881 pub fn params(&self) -> TuneParams {
883 self.params.clone()
884 }
885
886 pub fn stats(&self) -> TuneStats {
888 self.stats
889 }
890
891 pub fn enabled(&self) -> bool {
893 self.enabled
894 }
895
896 pub fn set_enabled(&mut self, enabled: bool) {
901 self.enabled = enabled;
902 }
903
904 pub fn caps(&self) -> Option<Capabilities> {
906 self.caps.clone()
907 }
908
909 pub fn state_js(&self) -> JsValue {
912 let o = js_sys::Object::new();
913 let phase = match self.phase {
914 TunePhase::Uninitialized => "uninitialized",
915 TunePhase::Ramping => "ramping",
916 TunePhase::Settled => "settled",
917 TunePhase::Degraded => "degraded",
918 };
919 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("phase"), &JsValue::from_str(phase));
920 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("params"), ¶ms_to_js(&self.params));
921 let stats = js_sys::Object::new();
922 let _ = js_sys::Reflect::set(
923 &stats,
924 &JsValue::from_str("rttMs"),
925 &JsValue::from_f64(self.stats.rtt_ms),
926 );
927 let _ = js_sys::Reflect::set(
928 &stats,
929 &JsValue::from_str("mbps"),
930 &JsValue::from_f64(self.stats.mbps),
931 );
932 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("stats"), &stats.into());
933 let _ = js_sys::Reflect::set(
934 &o,
935 &JsValue::from_str("capsHash"),
936 &JsValue::from_str(&self.caps_hash),
937 );
938 o.into()
939 }
940
941 pub fn caps_hash(&self) -> &str {
943 &self.caps_hash
944 }
945
946 pub fn begin_transfer(
960 &mut self,
961 caps: &Capabilities,
962 now_ms: f64,
963 static_params: &TuneParams,
964 ) -> TuneParams {
965 self.caps = Some(caps.clone());
966 self.caps_hash = caps.caps_hash();
967 self.windows_closed = 0;
968 self.window_errors = 0;
969 self.window_done_base = 0;
970 self.window_start_ms = now_ms;
971 self.auto_level_benchmarked = false;
972 self.auto_level = None;
973 self.post_degrade_settle = false;
974 self.prev_mbps = 0.0;
979 self.low_gain_windows = 0;
980 self.rtt_ewma = None;
981 self.rtt_baseline = None;
982 self.degraded_windows = 0;
983
984 let policy_level = static_params.compress_level;
988 self.policy_level = policy_level;
989
990 if !self.enabled {
991 self.phase = TunePhase::Uninitialized;
992 self.params = static_params.clamped_into(caps);
993 self.enforce_in_flight_budget();
994 return self.params.clone();
995 }
996
997 let in_memory = self.settled.clone().filter(|_| {
1001 self.settled_caps_hash == self.caps_hash
1002 && self.settled_direction == Some(self.direction)
1003 });
1004 let reusable = match in_memory {
1005 Some(settled) => Some(settled),
1006 None => self.load_cached_settle(now_ms, caps),
1007 };
1008 match reusable {
1009 Some(settled) => {
1010 self.params = settled.clamped_into(caps);
1011 self.params.compress_level = policy_level;
1012 self.phase = TunePhase::Settled;
1013 }
1014 None => {
1015 self.params = TuneParams::from_caps_mins(caps);
1016 self.params.compress_level = policy_level;
1017 self.stats = TuneStats::default();
1018 self.phase = TunePhase::Ramping;
1019 }
1020 }
1021 self.dim = RampDim::Window;
1022 self.enforce_in_flight_budget();
1025 self.params.clone()
1026 }
1027
1028 pub fn tick(
1035 &mut self,
1036 now_ms: f64,
1037 done_bytes: u64,
1038 rtt_ms: Option<f64>,
1039 error: bool,
1040 ) -> Option<TuneEvent> {
1041 if error {
1042 self.window_errors += 1;
1043 }
1044 if let Some(r) = rtt_ms.filter(|&r| r > 0.0) {
1045 let new_ewma = ewma(self.rtt_ewma, r, RTT_EWMA_ALPHA);
1046 if self.enabled && self.rtt_baseline.is_none() {
1047 self.rtt_baseline = Some(new_ewma);
1051 }
1052 self.rtt_ewma = Some(new_ewma);
1053 }
1054 let elapsed = now_ms - self.window_start_ms;
1055 if elapsed < TUNE_WINDOW_MS {
1056 return None;
1057 }
1058 if !self.enabled {
1059 return None;
1060 }
1061 let caps = self.caps.clone()?;
1062
1063 let bytes = done_bytes.saturating_sub(self.window_done_base);
1064 let mbps = if elapsed > 0.0 {
1065 bytes as f64 / elapsed * 1_000.0 * 8.0 / 1e6
1066 } else {
1067 0.0
1068 };
1069 self.stats.mbps = mbps;
1070 if let Some(rtt) = self.rtt_ewma {
1071 self.stats.rtt_ms = rtt;
1072 }
1073
1074 self.retune_chunk_size(mbps, &caps);
1081
1082 let ramping = self.phase == TunePhase::Ramping;
1086 let input = RampInput {
1087 mbps,
1088 prev_mbps: self.prev_mbps,
1089 rtt_inflation: if ramping { self.rtt_inflation() } else { 0.0 },
1090 errors: self.window_errors,
1091 degraded: self.phase == TunePhase::Degraded,
1092 ramping,
1093 low_gain_windows: self.low_gain_windows,
1094 in_flight: self.in_flight(),
1095 bdp: if ramping && self.dim == RampDim::Concurrency {
1098 bdp_bytes(mbps, self.rtt_ewma.unwrap_or(self.stats.rtt_ms))
1099 } else {
1100 0
1101 },
1102 at_cap: self.dim_at_cap(&caps),
1103 last_dim: self.dim == RampDim::Concurrency,
1104 };
1105 let (action, low) = ramp_action(&input);
1106 self.low_gain_windows = low;
1107 self.windows_closed += 1;
1108 self.prev_mbps = mbps;
1109
1110 match action {
1111 RampAction::Raise => {
1112 self.raise_dim(&caps);
1113 self.enforce_in_flight_budget();
1114 if self.phase == TunePhase::Degraded {
1115 self.phase = TunePhase::Ramping;
1116 }
1117 }
1118 RampAction::AdvanceDim => {
1119 self.dim = next_dim(self.dim);
1120 }
1121 RampAction::Settle => {
1122 self.phase = TunePhase::Settled;
1123 self.low_gain_windows = 0;
1124 if !self.post_degrade_settle {
1129 self.settled = Some(self.params.clone());
1130 self.settled_caps_hash = self.caps_hash.clone();
1131 self.settled_direction = Some(self.direction);
1132 self.save_cached_settle(now_ms);
1133 }
1134 }
1135 RampAction::Degrade => {
1136 self.halve_dim();
1137 self.phase = TunePhase::Degraded;
1138 self.degraded_windows = 0;
1139 self.low_gain_windows = 0;
1140 }
1141 RampAction::Hold => {
1142 if self.phase == TunePhase::Degraded {
1143 self.degraded_windows += 1;
1145 if self.degraded_windows >= 2 {
1146 self.phase = TunePhase::Settled;
1147 self.post_degrade_settle = true;
1153 }
1154 }
1155 }
1156 }
1157
1158 self.window_start_ms = now_ms;
1160 self.window_done_base = done_bytes;
1161 self.window_errors = 0;
1162
1163 let event = TuneEvent {
1164 phase: self.phase,
1165 params: self.params.clone(),
1166 stats: self.stats,
1167 };
1168 self.emit(&event);
1169 Some(event)
1170 }
1171
1172 pub fn transfer_end(&mut self, ok: bool) {
1178 if !self.enabled {
1179 return;
1180 }
1181 if !ok {
1182 self.settled = None;
1183 self.settled_caps_hash.clear();
1184 self.settled_direction = None;
1185 self.remove_cache_entry();
1186 self.phase = TunePhase::Ramping;
1187 }
1188 }
1189
1190 pub fn upload_compress_level(
1196 &mut self,
1197 caps: &Capabilities,
1198 sample: Option<&[u8]>,
1199 mbps: f64,
1200 ) -> i32 {
1201 match self.level_cfg {
1202 CompressLevel::Auto => {
1203 if !self.auto_level_benchmarked {
1204 self.auto_level_benchmarked = true;
1205 self.auto_level = Some(match sample {
1206 Some(s) if !s.is_empty() => {
1207 let s = &s[..LEVEL_SAMPLE_SIZE.min(s.len())];
1211 let candidates = auto_candidates(caps);
1212 let results = benchmark_levels(s, &candidates);
1213 choose_auto_level(&results, s.len(), mbps.max(1.0))
1214 }
1215 _ => caps.compression.zrip_levels.default,
1216 });
1217 }
1218 self.auto_level.unwrap_or(caps.compression.zrip_levels.default)
1219 }
1220 other => resolve_level(other, caps),
1221 }
1222 }
1223
1224 pub fn auto_level_ready(&self) -> bool {
1226 self.auto_level_benchmarked
1227 }
1228
1229 fn rtt_inflation(&self) -> f64 {
1230 match (self.rtt_ewma, self.rtt_baseline) {
1234 (Some(cur), Some(base)) if base > 0.0 => (cur / base - 1.0).max(0.0),
1235 _ => 0.0,
1236 }
1237 }
1238
1239 fn in_flight(&self) -> u64 {
1240 match self.last_transfer_kind() {
1241 TransferKind::Upload => self.params.upload_in_flight(),
1242 TransferKind::Download => self.params.download_in_flight(),
1243 }
1244 }
1245
1246 fn dim_value(&self) -> i64 {
1248 match self.dim {
1249 RampDim::Window => match self.last_transfer_kind() {
1250 TransferKind::Upload => self.params.upload_window as i64,
1251 TransferKind::Download => self.params.download_window as i64,
1252 },
1253 RampDim::Concurrency => self.params.concurrency as i64,
1254 }
1255 }
1256
1257 fn dim_max(&self, caps: &Capabilities) -> i64 {
1260 let advertised = match self.dim {
1261 RampDim::Window => match self.last_transfer_kind() {
1262 TransferKind::Upload => caps.limits.upload_window.max,
1263 TransferKind::Download => caps.limits.download_window.max,
1264 },
1265 RampDim::Concurrency => caps.limits.concurrency.max,
1266 };
1267 let hard = match self.dim {
1268 RampDim::Window => HARD_MAX_WINDOW,
1269 RampDim::Concurrency => HARD_MAX_CONCURRENCY,
1270 };
1271 advertised.min(hard).max(1)
1272 }
1273
1274 fn max_chunk_for_budget(&self) -> u64 {
1277 let widest = self.params.upload_window.max(self.params.download_window).max(1) as u64;
1278 let divisor = (self.params.concurrency.max(1) as u64) * widest;
1279 (HARD_MAX_IN_FLIGHT_BYTES / divisor).max(1)
1280 }
1281
1282 fn retune_chunk_size(&mut self, mbps: f64, caps: &Capabilities) {
1289 let target = chunk_for_link(mbps, &caps.limits.chunk_size, self.max_chunk_for_budget());
1290 let current = self.params.chunk_size.max(1);
1291 let grow = current.saturating_mul(3) / 2;
1292 let shrink = (current / 2).max(1);
1293 if target > grow || target < shrink {
1294 self.params.chunk_size = target;
1295 self.enforce_in_flight_budget();
1299 }
1300 }
1301
1302 fn enforce_in_flight_budget(&mut self) {
1308 let max_chunk = self.max_chunk_for_budget();
1309 self.params.chunk_size = self.params.chunk_size.clamp(1, max_chunk);
1310 }
1311
1312 fn dim_at_cap(&self, caps: &Capabilities) -> bool {
1313 self.dim_value() >= self.dim_max(caps)
1314 }
1315
1316 fn raise_dim(&mut self, caps: &Capabilities) {
1317 let dim_max: i64 = self.dim_max(caps);
1318 let step = |v: i64| -> i64 {
1321 if v <= 0 {
1322 1
1323 } else if v * 2 <= dim_max {
1324 v * 2
1325 } else {
1326 (v + 1).min(dim_max)
1327 }
1328 };
1329 match self.dim {
1330 RampDim::Window => match self.last_transfer_kind() {
1331 TransferKind::Upload => {
1332 self.params.upload_window = step(self.params.upload_window as i64).max(1) as usize
1333 }
1334 TransferKind::Download => {
1335 self.params.download_window =
1336 step(self.params.download_window as i64).max(1) as usize
1337 }
1338 },
1339 RampDim::Concurrency => {
1340 self.params.concurrency = step(self.params.concurrency as i64).max(1) as usize
1341 }
1342 }
1343 }
1344
1345 fn halve_dim(&mut self) {
1346 let step = |v: i64| -> i64 { ((v as f64) * DEGRADE_FACTOR).floor().max(1.0) as i64 };
1347 match self.dim {
1348 RampDim::Window => match self.last_transfer_kind() {
1349 TransferKind::Upload => {
1350 self.params.upload_window = step(self.params.upload_window as i64) as usize
1351 }
1352 TransferKind::Download => {
1353 self.params.download_window = step(self.params.download_window as i64) as usize
1354 }
1355 },
1356 RampDim::Concurrency => {
1357 self.params.concurrency = step(self.params.concurrency as i64) as usize
1358 }
1359 }
1360 }
1361}
1362
1363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1365pub enum TransferKind {
1366 Upload,
1367 Download,
1368}
1369
1370impl TuningEngine {
1371 fn last_transfer_kind(&self) -> TransferKind {
1372 self.direction
1375 }
1376}
1377
1378fn next_dim(dim: RampDim) -> RampDim {
1383 match dim {
1384 RampDim::Window => RampDim::Concurrency,
1385 RampDim::Concurrency => RampDim::Concurrency,
1390 }
1391}
1392
1393fn auto_candidates(caps: &Capabilities) -> Vec<i32> {
1395 let z = caps.compression.zrip_levels;
1396 vec![z.min, z.default, z.max]
1397}
1398
1399fn benchmark_levels(sample: &[u8], levels: &[i32]) -> Vec<(i32, usize, f64)> {
1403 levels
1404 .iter()
1405 .map(|&level| {
1406 let t0 = now_ms();
1407 let out = zrip::compress(sample, level).unwrap_or_else(|_| sample.to_vec());
1408 (level, out.len(), now_ms() - t0)
1409 })
1410 .collect()
1411}
1412
1413pub fn now_ms() -> f64 {
1415 #[cfg(target_arch = "wasm32")]
1416 {
1417 js_sys::Date::now()
1418 }
1419 #[cfg(not(target_arch = "wasm32"))]
1420 {
1421 std::time::SystemTime::now()
1422 .duration_since(std::time::UNIX_EPOCH)
1423 .map(|d| d.as_secs_f64() * 1_000.0)
1424 .unwrap_or(0.0)
1425 }
1426}
1427
1428impl TuningEngine {
1429 fn emit(&self, event: &TuneEvent) {
1430 if let Some(cb) = &self.on_tuning {
1431 let params = params_to_js(&event.params);
1432 let stats = js_sys::Object::new();
1433 let _ = js_sys::Reflect::set(
1434 &stats,
1435 &JsValue::from_str("rttMs"),
1436 &JsValue::from_f64(event.stats.rtt_ms),
1437 );
1438 let _ = js_sys::Reflect::set(
1439 &stats,
1440 &JsValue::from_str("mbps"),
1441 &JsValue::from_f64(event.stats.mbps),
1442 );
1443 let phase = match event.phase {
1444 TunePhase::Uninitialized => "uninitialized",
1445 TunePhase::Ramping => "ramping",
1446 TunePhase::Settled => "settled",
1447 TunePhase::Degraded => "degraded",
1448 };
1449 let _ = cb.call3(
1450 &JsValue::NULL,
1451 &JsValue::from_str(phase),
1452 ¶ms,
1453 &stats.into(),
1454 );
1455 }
1456 }
1457}
1458
1459fn params_to_js(params: &TuneParams) -> JsValue {
1460 let o = js_sys::Object::new();
1461 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("concurrency"), &JsValue::from_f64(params.concurrency as f64));
1462 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("uploadWindow"), &JsValue::from_f64(params.upload_window as f64));
1463 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("downloadWindow"), &JsValue::from_f64(params.download_window as f64));
1464 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("chunkSize"), &JsValue::from_f64(params.chunk_size as f64));
1465 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("compressLevel"), &JsValue::from_f64(params.compress_level as f64));
1466 o.into()
1467}
1468
1469#[cfg(test)]
1470mod tests {
1471 use super::*;
1472
1473 fn caps() -> Capabilities {
1474 Capabilities::default()
1475 }
1476
1477 fn static_params() -> TuneParams {
1478 TuneParams {
1479 concurrency: 4,
1480 upload_window: 8,
1481 download_window: 4,
1482 chunk_size: 2 * 1024 * 1024,
1483 compress_level: 1,
1484 }
1485 }
1486
1487 fn engine() -> TuningEngine {
1489 let mut e = TuningEngine::new(true, CompressLevel::Balanced);
1490 e.set_direction(TransferKind::Upload);
1491 e.begin_transfer(&caps(), 0.0, &static_params());
1492 e
1493 }
1494
1495 fn ramp_to_settle(e: &mut TuningEngine) {
1499 let step = 12_500_000u64;
1500 let mut done = step;
1501 for i in 0..16 {
1502 let t = 1_000.0 * (i as f64 + 1.0);
1503 if let Some(TuneEvent {
1504 phase: TunePhase::Settled,
1505 ..
1506 }) = e.tick(t, done, Some(400.0), false)
1507 {
1508 return;
1509 }
1510 done += step;
1511 }
1512 panic!("engine did not settle");
1513 }
1514
1515 #[test]
1518 fn ramp_action_error_degrades() {
1519 let input = RampInput {
1520 mbps: 10.0,
1521 prev_mbps: 5.0,
1522 rtt_inflation: 0.0,
1523 errors: 1,
1524 degraded: false,
1525 ramping: true,
1526 low_gain_windows: 0,
1527 in_flight: 100,
1528 bdp: 1_000,
1529 at_cap: false,
1530 last_dim: false,
1531 };
1532 assert_eq!(ramp_action(&input).0, RampAction::Degrade);
1533 }
1534
1535 #[test]
1536 fn ramp_action_cap_advances_or_settles() {
1537 let base = RampInput {
1538 mbps: 10.0,
1539 prev_mbps: 5.0,
1540 rtt_inflation: 0.0,
1541 errors: 0,
1542 degraded: false,
1543 ramping: true,
1544 low_gain_windows: 0,
1545 in_flight: 100,
1546 bdp: 1_000,
1547 at_cap: true,
1548 last_dim: false,
1549 };
1550 assert_eq!(ramp_action(&base).0, RampAction::AdvanceDim);
1551 let last = RampInput { last_dim: true, ..base };
1552 assert_eq!(ramp_action(&last).0, RampAction::Settle);
1553 }
1554
1555 #[test]
1556 fn ramp_action_bdp_and_rtt_saturate() {
1557 let base = RampInput {
1558 mbps: 10.0,
1559 prev_mbps: 5.0,
1560 rtt_inflation: 0.0,
1561 errors: 0,
1562 degraded: false,
1563 ramping: true,
1564 low_gain_windows: 0,
1565 in_flight: 2_000,
1566 bdp: 1_000,
1567 at_cap: false,
1568 last_dim: false,
1569 };
1570 assert_eq!(ramp_action(&base).0, RampAction::Settle, "in-flight ≥ BDP settles");
1571
1572 let rtt = RampInput {
1573 rtt_inflation: 0.5,
1574 in_flight: 10,
1575 bdp: 1_000,
1576 ..base
1577 };
1578 assert_eq!(ramp_action(&rtt).0, RampAction::Settle, "RTT inflation settles");
1579 }
1580
1581 #[test]
1582 fn ramp_action_gain_raises_and_low_gain_settles() {
1583 let growing = RampInput {
1585 mbps: 20.0,
1586 prev_mbps: 10.0,
1587 ..RampInput {
1588 mbps: 0.0,
1589 prev_mbps: 0.0,
1590 rtt_inflation: 0.0,
1591 errors: 0,
1592 degraded: false,
1593 ramping: true,
1594 low_gain_windows: 7,
1595 in_flight: 10,
1596 bdp: 1_000,
1597 at_cap: false,
1598 last_dim: false,
1599 }
1600 };
1601 let (action, low) = ramp_action(&growing);
1602 assert_eq!(action, RampAction::Raise);
1603 assert_eq!(low, 0, "raise resets the low-gain counter");
1604
1605 let flat = RampInput {
1607 mbps: 10.4,
1608 prev_mbps: 10.0,
1609 low_gain_windows: 0,
1610 ..growing
1611 };
1612 let (action, low) = ramp_action(&flat);
1613 assert_eq!(action, RampAction::Hold);
1614 assert_eq!(low, 1, "first below-threshold window holds");
1615
1616 let flat2 = RampInput { low_gain_windows: 1, ..flat };
1620 let (action, low) = ramp_action(&flat2);
1621 assert_eq!(action, RampAction::AdvanceDim);
1622 assert_eq!(low, 0, "advancing resets the low-gain counter");
1623
1624 let flat_last = RampInput {
1626 last_dim: true,
1627 low_gain_windows: 1,
1628 ..flat
1629 };
1630 assert_eq!(ramp_action(&flat_last).0, RampAction::Settle);
1631
1632 let first = RampInput {
1634 mbps: 1.0,
1635 prev_mbps: 0.0,
1636 low_gain_windows: 0,
1637 ..growing
1638 };
1639 assert_eq!(ramp_action(&first).0, RampAction::Raise);
1640 }
1641
1642 #[test]
1643 fn ramp_action_degraded_hold_never_raises() {
1644 let degraded = RampInput {
1648 mbps: 0.008,
1649 prev_mbps: 0.0,
1650 degraded: true,
1651 ..RampInput {
1652 mbps: 0.0,
1653 prev_mbps: 0.0,
1654 rtt_inflation: 0.0,
1655 errors: 0,
1656 degraded: false,
1657 ramping: true,
1658 low_gain_windows: 0,
1659 in_flight: 10,
1660 bdp: 1_000,
1661 at_cap: false,
1662 last_dim: false,
1663 }
1664 };
1665 let (action, low) = ramp_action(°raded);
1666 assert_eq!(action, RampAction::Hold);
1667 assert_eq!(low, 1, "first stability window holds");
1668 let (action, low) = ramp_action(&RampInput { low_gain_windows: 1, ..degraded });
1669 assert_eq!(action, RampAction::Settle);
1670 assert_eq!(low, 2, "second stability window settles");
1671 }
1672
1673 #[test]
1674 fn ewma_seeds_and_smooths() {
1675 assert_eq!(ewma(None, 10.0, 0.25), 10.0);
1676 let v = ewma(Some(10.0), 20.0, 0.25);
1677 assert!((v - 12.5).abs() < 1e-9);
1678 }
1679
1680 #[test]
1681 fn bdp_conversion() {
1682 assert_eq!(bdp_bytes(10.0, 100.0), 125_000);
1684 assert_eq!(bdp_bytes(0.0, 0.0), 0);
1685 }
1686
1687 #[test]
1688 fn request_budget_covers_files_and_the_per_file_window() {
1689 assert_eq!(request_budget(1, 1), 1);
1692 assert_eq!(request_budget(1, 8), 8);
1693 assert_eq!(request_budget(4, 8), 32);
1694 assert_eq!(request_budget(0, 0), 1);
1696 assert_eq!(request_budget(64, 64), HARD_MAX_IN_FLIGHT_REQUESTS);
1698
1699 let mut p = static_params();
1701 p.concurrency = 2;
1702 p.upload_window = 8;
1703 p.download_window = 4;
1704 assert_eq!(p.request_budget(), 16);
1705 p.upload_window = 4;
1706 assert_eq!(p.request_budget(), 8);
1707 }
1708
1709 #[test]
1710 fn resolve_level_mapping() {
1711 let c = caps();
1712 assert_eq!(resolve_level(CompressLevel::Fast, &c), -8);
1713 assert_eq!(resolve_level(CompressLevel::Balanced, &c), 1);
1714 assert_eq!(resolve_level(CompressLevel::Max, &c), 4);
1715 assert_eq!(resolve_level(CompressLevel::Auto, &c), 1);
1716 assert_eq!(resolve_level(CompressLevel::Fixed(2), &c), 2);
1717 assert_eq!(resolve_level(CompressLevel::Fixed(99), &c), 4);
1718 assert_eq!(resolve_level(CompressLevel::Fixed(-99), &c), -8);
1719 }
1720
1721 #[test]
1722 fn choose_auto_level_picks_by_net_gain() {
1723 assert_eq!(
1725 choose_auto_level(&[(-8, 1000, 0.1), (1, 1000, 5.0), (4, 1000, 40.0)], 1000, 10.0),
1726 -8
1727 );
1728
1729 let cpu_bound = choose_auto_level(&[(-8, 800_000, 0.1), (4, 400_000, 30.0)], 1_000_000, 1000.0);
1731 assert_eq!(cpu_bound, -8);
1732
1733 let savings = choose_auto_level(&[(-8, 800_000, 0.1), (4, 400_000, 30.0)], 1_000_000, 10.0);
1735 assert_eq!(savings, 4);
1736
1737 assert_eq!(choose_auto_level(&[(0, 500, 1.0), (4, 500, 1.0)], 1000, 100.0), 0);
1739 }
1740
1741 #[test]
1744 fn begin_transfer_miss_ramps_from_mins() {
1745 let mut e = TuningEngine::new(true, CompressLevel::Balanced);
1746 e.set_direction(TransferKind::Upload);
1747 let p = e.begin_transfer(&caps(), 0.0, &static_params());
1748 assert_eq!(e.phase(), TunePhase::Ramping);
1749 assert_eq!(p.concurrency, 1);
1750 assert_eq!(p.upload_window, 1);
1751 assert_eq!(p.chunk_size, 262_144);
1752 }
1753
1754 #[test]
1755 fn settled_reuse_first_window_does_not_raise() {
1756 let input = RampInput {
1760 mbps: 100.0,
1761 prev_mbps: 0.0,
1762 rtt_inflation: 0.0,
1763 errors: 0,
1764 degraded: false,
1765 ramping: false,
1766 low_gain_windows: 0,
1767 in_flight: 10,
1768 bdp: 0,
1769 at_cap: false,
1770 last_dim: false,
1771 };
1772 let (action, low) = ramp_action(&input);
1773 assert_eq!(action, RampAction::Hold);
1774 assert_eq!(low, 1);
1775 }
1776
1777 #[test]
1778 fn reused_params_do_not_raise_on_the_first_window() {
1779 let mut e = engine();
1780 ramp_to_settle(&mut e);
1781 let tuned = e.params().clone();
1782 e.transfer_end(true);
1783 e.begin_transfer(&caps(), 60_000.0, &static_params());
1784 let ev = e.tick(61_000.0, 100_000_000, Some(400.0), false).unwrap();
1787 assert_eq!(ev.phase, TunePhase::Settled);
1788 assert_eq!(
1789 ev.params.upload_window, tuned.upload_window,
1790 "settled reuse must not raise the window"
1791 );
1792 assert_eq!(ev.params.concurrency, tuned.concurrency);
1793 }
1794
1795 #[test]
1796 fn rtt_inflation_settles_the_ramp() {
1797 let mut e = engine();
1798 let ev = e.tick(1_000.0, 12_500_000, Some(100.0), false).unwrap();
1800 assert_eq!(ev.phase, TunePhase::Ramping);
1801 let ev = e.tick(2_000.0, 25_000_000, Some(400.0), false).unwrap();
1803 assert_eq!(ev.phase, TunePhase::Settled, "RTT inflation must settle");
1804 }
1805
1806 #[test]
1807 fn ramp_start_keeps_the_configured_compress_policy() {
1808 let mut e = TuningEngine::new(true, CompressLevel::Balanced);
1809 e.set_direction(TransferKind::Upload);
1810 let sp = static_params(); let p = e.begin_transfer(&caps(), 0.0, &sp);
1812 assert_eq!(
1813 p.compress_level, 1,
1814 "a ramp must not silently drop to the advertised minimum (-8)"
1815 );
1816 ramp_to_settle(&mut e);
1819 e.transfer_end(true);
1820 let p2 = e.begin_transfer(&caps(), 60_000.0, &sp);
1821 assert_eq!(p2.compress_level, 1, "reuse keeps the policy level");
1822 }
1823
1824 #[test]
1825 fn absurd_advertised_caps_are_clamped_to_hard_ceilings() {
1826 let mut hostile = caps();
1827 hostile.limits.concurrency = IntRange {
1828 default: 1,
1829 max: 100_000,
1830 min: 1_000,
1831 };
1832 hostile.limits.upload_window = IntRange {
1833 default: 1,
1834 max: 100_000,
1835 min: 500,
1836 };
1837 hostile.limits.download_window = IntRange {
1838 default: 1,
1839 max: 100_000,
1840 min: 500,
1841 };
1842 hostile.limits.chunk_size = IntRange {
1843 default: i64::MAX,
1844 max: i64::MAX,
1845 min: i64::MAX,
1846 };
1847 let mut e = TuningEngine::new(true, CompressLevel::Balanced);
1848 e.set_direction(TransferKind::Upload);
1849 let p = e.begin_transfer(&hostile, 0.0, &static_params());
1850 assert!(p.concurrency <= HARD_MAX_CONCURRENCY as usize);
1851 assert!(p.upload_window <= HARD_MAX_WINDOW as usize);
1852 assert!(p.download_window <= HARD_MAX_WINDOW as usize);
1853 assert!(p.chunk_size <= HARD_MAX_CHUNK_SIZE as u64);
1854 assert!(
1855 p.upload_in_flight() <= HARD_MAX_IN_FLIGHT_BYTES,
1856 "in-flight bytes {} exceed the budget",
1857 p.upload_in_flight()
1858 );
1859 }
1860
1861 #[test]
1862 fn inverted_advertised_range_does_not_panic() {
1863 let mut broken = caps();
1864 broken.limits.concurrency = IntRange {
1865 default: 4,
1866 max: 2,
1867 min: 8,
1868 };
1869 broken.limits.chunk_size = IntRange {
1870 default: 0,
1871 max: 0,
1872 min: 0,
1873 };
1874 let mut e = TuningEngine::new(false, CompressLevel::Balanced);
1876 e.set_direction(TransferKind::Upload);
1877 let p = e.begin_transfer(&broken, 0.0, &static_params());
1878 assert!(p.concurrency >= 1 && p.chunk_size >= 1);
1879 let mut e2 = TuningEngine::new(true, CompressLevel::Balanced);
1880 e2.set_direction(TransferKind::Upload);
1881 let p2 = e2.begin_transfer(&broken, 0.0, &static_params());
1882 assert!(p2.concurrency >= 1 && p2.chunk_size >= 1);
1883 }
1884
1885 #[test]
1886 fn ramp_raises_window_then_concurrency_until_cap() {
1887 let mut e = engine();
1888 let mut t = 1_000.0;
1892 let mut done = 12_500_000u64;
1893 let mut delta = 12_500_000u64;
1894 for _ in 0..3 {
1895 let ev = e.tick(t, done, Some(400.0), false).expect("window closed");
1896 assert_eq!(ev.phase, TunePhase::Ramping);
1897 t += 1_000.0;
1898 delta *= 2;
1899 done += delta; }
1901 assert_eq!(e.params().upload_window, 8);
1903 let ev = e.tick(t, done, Some(400.0), false).unwrap();
1904 assert_eq!(ev.params.upload_window, 8); t += 1_000.0;
1906 delta *= 2;
1907 done += delta;
1908 let ev = e.tick(t, done, Some(400.0), false).unwrap();
1910 assert_eq!(ev.params.concurrency, 2);
1911 let ev = e.tick(t + 1_000.0, done + delta * 2, Some(400.0), false).unwrap();
1913 assert_eq!(ev.params.concurrency, 4);
1914 assert_eq!(ev.params.upload_window, 8);
1915 }
1916
1917 #[test]
1918 fn error_window_degrades_and_halves() {
1919 let mut e = engine();
1920 let mut t = 1_000.0;
1922 let mut done = 12_500_000u64;
1923 let mut delta = 12_500_000u64;
1924 for _ in 0..2 {
1925 e.tick(t, done, Some(400.0), false);
1926 t += 1_000.0;
1927 delta *= 2;
1928 done += delta; }
1930 assert_eq!(e.params().upload_window, 4);
1931 let ev = e.tick(t, done, Some(400.0), true).unwrap();
1933 assert_eq!(ev.phase, TunePhase::Degraded);
1934 assert_eq!(ev.params.upload_window, 2);
1935 let ev = e.tick(t + 1_000.0, done + 1_000, None, false).unwrap();
1938 assert_eq!(ev.phase, TunePhase::Degraded);
1939 let ev = e.tick(t + 2_000.0, done + 2_000, None, false).unwrap();
1940 assert_eq!(ev.phase, TunePhase::Settled);
1941 assert_eq!(ev.params.upload_window, 2);
1942 }
1943
1944 #[test]
1945 fn flat_gain_explores_dimensions_then_settles() {
1946 let mut e = engine();
1947 let step = 12_500_000u64;
1952 let mut done = step;
1953 let mut t = 1_000.0;
1954
1955 let ev = e.tick(t, done, Some(400.0), false).unwrap();
1957 assert_eq!(ev.params.upload_window, 2);
1958 for _ in 0..2 {
1959 t += 1_000.0;
1960 done += step;
1961 e.tick(t, done, Some(400.0), false);
1962 }
1963 assert_eq!(
1964 e.phase(),
1965 TunePhase::Ramping,
1966 "a flat window dimension must not end the ramp"
1967 );
1968
1969 t += 1_000.0;
1971 done += step;
1972 let ev = e.tick(t, done, Some(400.0), false).unwrap();
1973 assert_eq!(ev.phase, TunePhase::Ramping);
1974 t += 1_000.0;
1975 done += step;
1976 let ev = e.tick(t, done, Some(400.0), false).unwrap();
1977 assert_eq!(ev.phase, TunePhase::Settled);
1978 assert_eq!(ev.params.upload_window, 2);
1979 e.transfer_end(true);
1980 }
1981
1982 #[test]
1983 fn chunk_size_tracks_the_link_and_ignores_jitter() {
1984 let mut e = engine();
1985 let mut done = 12_500_000u64;
1987 let ev = e.tick(1_000.0, done, Some(400.0), false).unwrap();
1988 let first = ev.params.chunk_size;
1989 assert_eq!(first, 1_245_184, "the chunk must follow the measured link");
1990
1991 let mut t = 1_000.0;
1994 for _ in 0..3 {
1995 t += 1_000.0;
1996 done += 13_500_000; let ev = e.tick(t, done, Some(400.0), false).unwrap();
1998 assert_eq!(ev.params.chunk_size, first, "jitter must not re-size the chunk");
1999 }
2000
2001 t += 1_000.0;
2003 done += 100_000_000; let ev = e.tick(t, done, Some(400.0), false).unwrap();
2005 assert!(ev.params.chunk_size > first, "the chunk must grow on a wider band");
2006 }
2007
2008 #[test]
2009 fn chunk_size_follows_the_measured_link() {
2010 let range = IntRange {
2011 min: 262_144,
2012 default: 2 * 1024 * 1024,
2013 max: 8 * 1024 * 1024,
2014 };
2015 let budget = 8 * 1024 * 1024;
2016 assert_eq!(chunk_for_link(0.0, &range, budget), 262_144);
2018 assert_eq!(chunk_for_link(10.0, &range, budget), 262_144);
2020 assert_eq!(chunk_for_link(30.0, &range, budget), 327_680);
2022 assert_eq!(chunk_for_link(100.0, &range, budget), 1_245_184);
2023 assert_eq!(chunk_for_link(1_000.0, &range, budget), 8 * 1024 * 1024);
2025 assert_eq!(chunk_for_link(1_000.0, &range, 1_500_000), 1_500_000);
2027 let mut last = 0;
2029 for mbps in [1.0, 5.0, 20.0, 80.0, 320.0] {
2030 let v = chunk_for_link(mbps, &range, budget);
2031 assert!(v >= last && v >= 262_144, "{mbps} Mb/s → {v}");
2032 last = v;
2033 }
2034 assert!(chunk_for_link(f64::NAN, &range, budget) >= 1);
2036 assert!(chunk_for_link(-5.0, &range, budget) >= 1);
2037 let inverted = IntRange {
2038 min: 9_000_000,
2039 max: 1,
2040 default: 0,
2041 };
2042 let v = chunk_for_link(50.0, &inverted, budget);
2043 assert!(v >= 1 && v <= budget, "{v}");
2044 }
2045
2046 #[test]
2047 fn settled_state_is_reused_within_the_same_client() {
2048 let mut e = engine();
2049 ramp_to_settle(&mut e);
2050 assert_eq!(e.phase(), TunePhase::Settled);
2051 let tuned = e.params().clone();
2052 e.transfer_end(true);
2053 let p = e.begin_transfer(&caps(), 60_000.0, &static_params());
2056 assert_eq!(e.phase(), TunePhase::Settled, "in-session reuse skips the ramp");
2057 assert_eq!(p.upload_window, tuned.upload_window);
2058 assert_eq!(p.concurrency, tuned.concurrency);
2059 assert_eq!(p.chunk_size, tuned.chunk_size);
2060 assert_eq!(p.compress_level, 1, "reuse keeps the configured policy level");
2061 }
2062
2063 #[test]
2064 fn a_new_engine_always_re_ramps_from_mins() {
2065 let mut e = engine();
2069 ramp_to_settle(&mut e);
2070 e.transfer_end(true);
2071 let fresh = engine();
2072 assert_eq!(fresh.phase(), TunePhase::Ramping);
2073 assert_eq!(fresh.params().upload_window, 1);
2074 assert_eq!(fresh.params().chunk_size, 262_144);
2075 }
2076
2077 #[derive(Clone, Default)]
2081 struct FakeStore(Rc<RefCell<std::collections::HashMap<String, String>>>);
2082
2083 impl FakeStore {
2084 fn len(&self) -> usize {
2085 self.0.borrow().len()
2086 }
2087 fn put_raw(&self, key: &str, value: &str) {
2088 self.0.borrow_mut().insert(key.to_string(), value.to_string());
2089 }
2090 fn keys(&self) -> Vec<String> {
2091 self.0.borrow().keys().cloned().collect()
2092 }
2093 }
2094
2095 impl TuneStore for FakeStore {
2096 fn get(&self, key: &str) -> Option<String> {
2097 self.0.borrow().get(key).cloned()
2098 }
2099 fn set(&self, key: &str, value: &str) {
2100 self.0.borrow_mut().insert(key.to_string(), value.to_string());
2101 }
2102 fn remove(&self, key: &str) {
2103 self.0.borrow_mut().remove(key);
2104 }
2105 }
2106
2107 const ORIGIN: &str = "http://localhost:8080";
2108
2109 fn wired(
2114 store: &FakeStore,
2115 ttl_ms: u64,
2116 direction: TransferKind,
2117 origin: &str,
2118 ) -> TuningEngine {
2119 let mut e = TuningEngine::new(true, CompressLevel::Balanced);
2120 e.set_cache(Rc::new(store.clone()), ttl_ms);
2121 e.set_direction(direction);
2122 e.set_origin(origin);
2123 e
2124 }
2125
2126 fn cached_engine(store: &FakeStore, ttl_ms: u64) -> TuningEngine {
2128 let mut e = wired(store, ttl_ms, TransferKind::Upload, ORIGIN);
2129 e.begin_transfer(&caps(), 0.0, &static_params());
2130 e
2131 }
2132
2133 #[test]
2134 fn a_settle_is_cached_and_reused_by_a_new_engine() {
2135 let store = FakeStore::default();
2136 let mut a = cached_engine(&store, DEFAULT_TUNE_TTL_MS);
2137 ramp_to_settle(&mut a);
2138 assert_eq!(a.phase(), TunePhase::Settled);
2139 let tuned = a.params().clone();
2140 a.transfer_end(true);
2141 assert_eq!(store.len(), 1, "the settle is written on convergence");
2142 assert_eq!(
2143 store.keys(),
2144 vec![tune_key(ORIGIN, TransferKind::Upload)],
2145 "keyed by origin + direction"
2146 );
2147
2148 let mut b = wired(&store, DEFAULT_TUNE_TTL_MS, TransferKind::Upload, ORIGIN);
2150 let p = b.begin_transfer(&caps(), 20_000.0, &static_params());
2151 assert_eq!(b.phase(), TunePhase::Settled, "a cache hit skips the ramp");
2152 assert_eq!(p.upload_window, tuned.upload_window);
2153 assert_eq!(p.concurrency, tuned.concurrency);
2154 assert_eq!(p.chunk_size, tuned.chunk_size);
2155 assert_eq!(p.compress_level, 1, "the policy level is not cached");
2156 }
2157
2158 #[test]
2159 fn a_cached_settle_expires_after_the_ttl() {
2160 let store = FakeStore::default();
2161 let ttl = 60_000;
2162 let mut a = cached_engine(&store, ttl);
2163 ramp_to_settle(&mut a);
2164 let saved = a.params().clone();
2165 assert!(saved.upload_window > 1, "the ramp must have raised something");
2166 a.transfer_end(true);
2167
2168 let row: TuneCache = serde_json::from_str(
2170 &store.get(&tune_key(ORIGIN, TransferKind::Upload)).unwrap(),
2171 )
2172 .unwrap();
2173 let saved_at = row.saved_at_ms as f64;
2174
2175 let mut b = wired(&store, ttl, TransferKind::Upload, ORIGIN);
2177 b.begin_transfer(&caps(), saved_at + ttl as f64 - 1.0, &static_params());
2178 assert_eq!(b.phase(), TunePhase::Settled);
2179
2180 let mut c = wired(&store, ttl, TransferKind::Upload, ORIGIN);
2182 let p = c.begin_transfer(&caps(), saved_at + ttl as f64 + 1.0, &static_params());
2183 assert_eq!(c.phase(), TunePhase::Ramping);
2184 assert_eq!(p.upload_window, 1, "an expired result re-ramps from the mins");
2185 assert_eq!(store.len(), 0, "an expired row is cleaned up");
2186 }
2187
2188 #[test]
2189 fn a_zero_ttl_disables_the_cache() {
2190 let store = FakeStore::default();
2191 assert_eq!(cached_engine(&store, 0).cache_ttl_ms(), 0);
2192 let mut a = cached_engine(&store, 0);
2193 ramp_to_settle(&mut a);
2194 a.transfer_end(true);
2195 assert_eq!(store.len(), 0, "ttl = 0 never writes");
2196
2197 let store2 = FakeStore::default();
2199 let mut x = cached_engine(&store2, DEFAULT_TUNE_TTL_MS);
2200 ramp_to_settle(&mut x);
2201 x.transfer_end(true);
2202 assert_eq!(store2.len(), 1);
2203 let mut y = wired(&store2, 0, TransferKind::Upload, ORIGIN);
2204 y.begin_transfer(&caps(), 20_000.0, &static_params());
2205 assert_eq!(y.phase(), TunePhase::Ramping, "ttl = 0 never reads");
2206 }
2207
2208 #[test]
2209 fn a_cached_settle_is_scoped_to_one_direction() {
2210 let store = FakeStore::default();
2211 let mut a = cached_engine(&store, DEFAULT_TUNE_TTL_MS);
2212 ramp_to_settle(&mut a);
2213 a.transfer_end(true);
2214
2215 let mut b = wired(&store, DEFAULT_TUNE_TTL_MS, TransferKind::Download, ORIGIN);
2217 let p = b.begin_transfer(&caps(), 20_000.0, &static_params());
2218 assert_eq!(b.phase(), TunePhase::Ramping);
2219 assert_eq!(p.download_window, 1);
2220
2221 let mut c = wired(&store, DEFAULT_TUNE_TTL_MS, TransferKind::Upload, ORIGIN);
2223 c.begin_transfer(&caps(), 20_000.0, &static_params());
2224 assert_eq!(c.phase(), TunePhase::Settled);
2225 }
2226
2227 #[test]
2228 fn a_cached_settle_is_scoped_to_one_origin() {
2229 let store = FakeStore::default();
2230 let mut a = cached_engine(&store, DEFAULT_TUNE_TTL_MS);
2231 ramp_to_settle(&mut a);
2232 a.transfer_end(true);
2233
2234 let mut b = wired(
2235 &store,
2236 DEFAULT_TUNE_TTL_MS,
2237 TransferKind::Upload,
2238 "https://other.example.com:8443",
2239 );
2240 b.begin_transfer(&caps(), 20_000.0, &static_params());
2241 assert_eq!(b.phase(), TunePhase::Ramping, "another origin is another link");
2242 assert_eq!(store.len(), 1, "the other origin's row is left alone");
2243 }
2244
2245 #[test]
2246 fn an_unusable_origin_disables_the_cache() {
2247 let store = FakeStore::default();
2250 for url in ["", "not a url", "ftp://host/x", "http://", "ws://h:80"] {
2251 let mut e = wired(&store, DEFAULT_TUNE_TTL_MS, TransferKind::Upload, url);
2252 let _ = e.begin_transfer(&caps(), 0.0, &static_params());
2253 ramp_to_settle(&mut e);
2254 e.transfer_end(true);
2255 assert_eq!(store.len(), 0, "{url:?} must not be cached");
2256 }
2257 }
2258
2259 #[test]
2260 fn changed_caps_invalidate_a_cached_settle() {
2261 let store = FakeStore::default();
2262 let mut a = cached_engine(&store, DEFAULT_TUNE_TTL_MS);
2263 ramp_to_settle(&mut a);
2264 a.transfer_end(true);
2265 assert_eq!(store.len(), 1);
2266
2267 let mut changed = caps();
2268 changed.limits.concurrency.default = 6;
2269 let mut b = wired(&store, DEFAULT_TUNE_TTL_MS, TransferKind::Upload, ORIGIN);
2270 let p = b.begin_transfer(&changed, 20_000.0, &static_params());
2271 assert_eq!(b.phase(), TunePhase::Ramping);
2272 assert_eq!(p.upload_window, 1);
2273 assert_eq!(store.len(), 0, "a caps mismatch drops the entry");
2274 }
2275
2276 #[test]
2277 fn a_failed_transfer_removes_the_cached_settle() {
2278 let store = FakeStore::default();
2279 let mut a = cached_engine(&store, DEFAULT_TUNE_TTL_MS);
2280 ramp_to_settle(&mut a);
2281 a.transfer_end(false);
2282 assert_eq!(store.len(), 0, "parameters that just failed are not kept");
2283
2284 let p = a.begin_transfer(&caps(), 20_000.0, &static_params());
2286 assert_eq!(a.phase(), TunePhase::Ramping);
2287 assert_eq!(p.upload_window, 1);
2288 }
2289
2290 #[test]
2291 fn a_corrupt_or_foreign_cache_entry_is_dropped() {
2292 let store = FakeStore::default();
2293 let key = tune_key(ORIGIN, TransferKind::Upload);
2294
2295 store.put_raw(&key, "{not json");
2297 let mut e = wired(&store, DEFAULT_TUNE_TTL_MS, TransferKind::Upload, ORIGIN);
2298 e.begin_transfer(&caps(), 20_000.0, &static_params());
2299 assert_eq!(e.phase(), TunePhase::Ramping);
2300 assert_eq!(store.len(), 0, "an unreadable row is removed");
2301
2302 let future = TuneCache {
2304 v: TUNE_CACHE_VERSION + 1,
2305 caps_hash: caps().caps_hash(),
2306 saved_at_ms: 1_000,
2307 params: TuneParamsCache {
2308 concurrency: 12,
2309 upload_window: 16,
2310 download_window: 16,
2311 chunk_size: 4 * 1024 * 1024,
2312 },
2313 };
2314 store.put_raw(&key, &serde_json::to_string(&future).unwrap());
2315 let mut e = wired(&store, DEFAULT_TUNE_TTL_MS, TransferKind::Upload, ORIGIN);
2316 e.begin_transfer(&caps(), 20_000.0, &static_params());
2317 assert_eq!(e.phase(), TunePhase::Ramping);
2318 assert_eq!(store.len(), 0);
2319 }
2320
2321 #[test]
2322 fn a_valid_row_written_by_another_client_is_reused() {
2323 let store = FakeStore::default();
2326 let row = TuneCache {
2327 v: TUNE_CACHE_VERSION,
2328 caps_hash: caps().caps_hash(),
2329 saved_at_ms: 5_000,
2330 params: TuneParamsCache {
2331 concurrency: 3,
2332 upload_window: 7,
2333 download_window: 3,
2334 chunk_size: 1_048_576,
2335 },
2336 };
2337 store.put_raw(
2338 &tune_key(ORIGIN, TransferKind::Upload),
2339 &serde_json::to_string(&row).unwrap(),
2340 );
2341 let mut e = wired(&store, DEFAULT_TUNE_TTL_MS, TransferKind::Upload, ORIGIN);
2342 let p = e.begin_transfer(&caps(), 10_000.0, &static_params());
2343 assert_eq!(e.phase(), TunePhase::Settled);
2344 assert_eq!(p.concurrency, 3);
2345 assert_eq!(p.upload_window, 7);
2346 assert_eq!(p.chunk_size, 1_048_576);
2347 assert_eq!(store.len(), 1, "a fresh, valid row is kept");
2348 }
2349
2350 #[test]
2351 fn tune_key_and_origin_of_normalise_the_scope() {
2352 assert_eq!(
2353 tune_key("http://localhost:8080", TransferKind::Upload),
2354 "libfw.tune.v1.upload.http://localhost:8080"
2355 );
2356 assert_eq!(
2357 tune_key("http://localhost:8080", TransferKind::Download),
2358 "libfw.tune.v1.download.http://localhost:8080"
2359 );
2360 assert_ne!(
2361 tune_key("http://a", TransferKind::Upload),
2362 tune_key("http://a", TransferKind::Download)
2363 );
2364
2365 assert_eq!(origin_of("http://Host:8080/x?y#z"), "http://host:8080");
2366 assert_eq!(origin_of("HTTPS://h/p"), "https://h");
2367 assert_eq!(origin_of("http://h"), "http://h");
2368 assert_eq!(origin_of(""), "");
2370 assert_eq!(origin_of("/relative/path"), "");
2371 assert_eq!(origin_of("ftp://h/x"), "");
2372 assert_eq!(origin_of("http://"), "");
2373 }
2374
2375 #[test]
2376 fn failed_transfer_forgets_the_settled_state() {
2377 let mut e = engine();
2378 ramp_to_settle(&mut e);
2379 e.transfer_end(false);
2380 let p = e.begin_transfer(&caps(), 60_000.0, &static_params());
2382 assert_eq!(e.phase(), TunePhase::Ramping);
2383 assert_eq!(p.upload_window, 1, "a failed transfer re-ramps from the mins");
2384 }
2385
2386 #[test]
2387 fn changed_caps_invalidate_the_settled_state() {
2388 let mut e = engine();
2389 ramp_to_settle(&mut e);
2390 e.transfer_end(true);
2391 let mut changed = caps();
2393 changed.limits.concurrency.default = 6;
2394 let p = e.begin_transfer(&changed, 60_000.0, &static_params());
2395 assert_eq!(e.phase(), TunePhase::Ramping);
2396 assert_eq!(p.upload_window, 1);
2397 }
2398
2399 #[test]
2400 fn reused_settle_does_not_re_ramp_on_later_windows() {
2401 let mut e = engine();
2402 ramp_to_settle(&mut e);
2403 let tuned = e.params().clone();
2404 e.transfer_end(true);
2405 e.begin_transfer(&caps(), 60_000.0, &static_params());
2406 let mut t = 61_000.0;
2410 for i in 1..=3u64 {
2411 let ev = e.tick(t, i * 100_000_000, Some(400.0), false).unwrap();
2412 assert_eq!(ev.phase, TunePhase::Settled);
2413 assert_eq!(ev.params.upload_window, tuned.upload_window);
2414 assert_eq!(ev.params.concurrency, tuned.concurrency);
2415 t += 1_000.0;
2416 }
2417 }
2418
2419 #[test]
2420 fn transfer_end_without_settle_keeps_ramping_next_time() {
2421 let mut e = engine();
2422 e.tick(1_000.0, 12_500_000, Some(400.0), false);
2423 e.transfer_end(true);
2424 let p = e.begin_transfer(&caps(), 60_000.0, &static_params());
2426 assert_eq!(e.phase(), TunePhase::Ramping);
2427 assert_eq!(p.upload_window, 1);
2428 }
2429
2430 #[test]
2431 fn upload_auto_level_benchmarks_once() {
2432 let mut e = TuningEngine::new(true, CompressLevel::Auto);
2433 e.begin_transfer(&caps(), 0.0, &static_params());
2434 let sample = vec![0xABu8; 256 * 1024];
2435 let lvl1 = e.upload_compress_level(&caps(), Some(&sample), 50.0);
2436 let lvl2 = e.upload_compress_level(&caps(), Some(&sample), 50.0);
2437 assert_eq!(lvl1, lvl2, "benchmark runs once, result cached");
2438 assert!(e.auto_level_ready());
2439 assert!((-8..=4).contains(&lvl1), "level inside advertised range, got {lvl1}");
2440 let mut e2 = TuningEngine::new(true, CompressLevel::Auto);
2442 e2.begin_transfer(&caps(), 0.0, &static_params());
2443 let lvl = e2.upload_compress_level(&caps(), Some(&[0u8; 0]), 50.0);
2444 assert_eq!(lvl, caps().compression.zrip_levels.default, "no sample → server default");
2445 }
2446
2447 #[test]
2448 fn disabled_engine_uses_static_params() {
2449 let mut e = TuningEngine::new(false, CompressLevel::Balanced);
2450 e.set_direction(TransferKind::Upload);
2451 let p = e.begin_transfer(&caps(), 0.0, &static_params());
2452 assert_eq!(p.concurrency, 4, "static config preserved");
2453 assert_eq!(p.upload_window, 8);
2454 assert_eq!(e.phase(), TunePhase::Uninitialized);
2455 assert!(e.tick(1_000.0, 12_500_000, Some(40.0), false).is_none());
2457 assert_eq!(p, static_params().clamped_into(&caps()));
2458 }
2459}