1use std::cell::RefCell;
29use std::rc::Rc;
30
31use libfw_core::Capabilities;
32use serde::{Deserialize, Serialize};
33use wasm_bindgen::JsValue;
34
35pub const TUNE_WINDOW_MS: f64 = 1_000.0;
37pub const GAIN_THRESHOLD: f64 = 0.05;
39pub const RTT_INFLATE_THRESHOLD: f64 = 0.30;
41pub const DEGRADE_FACTOR: f64 = 0.5;
43pub const RTT_EWMA_ALPHA: f64 = 0.25;
45pub const DEFAULT_TUNE_TTL_MS: u64 = 3_600_000;
47pub const CACHE_VERSION: u32 = 1;
49pub const MIN_WINDOWS_TO_PERSIST: u32 = 2;
52pub const RTT_DRIFT_RE_RAMP: f64 = 0.50;
54pub const MAX_CONSECUTIVE_FAILURES: u32 = 3;
56pub const LEVEL_SAMPLE_SIZE: usize = 64 * 1024;
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "lowercase")]
73pub enum TunePhase {
74 Uninitialized,
76 Ramping,
78 Settled,
80 Degraded,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct TuneStats {
88 pub rtt_ms: f64,
90 pub mbps: f64,
92}
93
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97pub struct TuneParams {
98 pub concurrency: usize,
100 pub upload_window: usize,
102 pub download_window: usize,
104 pub chunk_size: u64,
106 pub download_chunk_size: u64,
108 pub compress_level: i32,
110}
111
112impl TuneParams {
113 pub fn from_caps_mins(caps: &Capabilities) -> TuneParams {
115 TuneParams {
116 concurrency: caps.limits.concurrency.min.max(1) as usize,
117 upload_window: caps.limits.upload_window.min.max(1) as usize,
118 download_window: caps.limits.download_window.min.max(1) as usize,
119 chunk_size: caps.limits.chunk_size.min.max(1) as u64,
120 download_chunk_size: caps.limits.download_chunk_size.min.max(1) as u64,
121 compress_level: caps.compression.zrip_levels.min,
122 }
123 }
124
125 pub fn from_config(
128 concurrency: usize,
129 upload_window: usize,
130 download_window: usize,
131 chunk_size: u64,
132 download_chunk_size: u64,
133 compress_level: i32,
134 caps: &Capabilities,
135 ) -> TuneParams {
136 TuneParams {
137 concurrency: caps.limits.concurrency.clamp(concurrency.max(1) as i64).max(1) as usize,
138 upload_window: caps.limits.upload_window.clamp(upload_window.max(1) as i64).max(1)
139 as usize,
140 download_window: caps
141 .limits
142 .download_window
143 .clamp(download_window.max(1) as i64)
144 .max(1) as usize,
145 chunk_size: caps.limits.chunk_size.clamp(chunk_size.max(1) as i64).max(1) as u64,
146 download_chunk_size: caps
147 .limits
148 .download_chunk_size
149 .clamp(download_chunk_size.max(1) as i64)
150 .max(1) as u64,
151 compress_level: caps.clamp_level(compress_level),
152 }
153 }
154
155 pub fn clamped_into(&self, caps: &Capabilities) -> TuneParams {
158 TuneParams {
159 concurrency: caps.limits.concurrency.clamp(self.concurrency as i64).max(1) as usize,
160 upload_window: caps.limits.upload_window.clamp(self.upload_window as i64).max(1)
161 as usize,
162 download_window: caps
163 .limits
164 .download_window
165 .clamp(self.download_window as i64)
166 .max(1) as usize,
167 chunk_size: caps.limits.chunk_size.clamp(self.chunk_size as i64).max(1) as u64,
168 download_chunk_size: caps
169 .limits
170 .download_chunk_size
171 .clamp(self.download_chunk_size as i64)
172 .max(1) as u64,
173 compress_level: caps.clamp_level(self.compress_level),
174 }
175 }
176
177 pub fn upload_in_flight(&self) -> u64 {
179 self.concurrency as u64 * self.upload_window as u64 * self.chunk_size
180 }
181
182 pub fn download_in_flight(&self) -> u64 {
184 self.concurrency as u64 * self.download_window as u64 * self.download_chunk_size
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct TuneCache {
192 pub v: u32,
193 pub ts: u64,
195 pub caps_hash: String,
197 pub params: TuneParams,
198 pub stats: TuneStats,
199}
200
201pub trait TuneStore {
207 fn get(&self, key: &str) -> Option<String>;
208 fn set(&self, key: &str, value: &str);
209 fn remove(&self, key: &str);
210}
211
212pub struct LocalStore;
214
215impl TuneStore for LocalStore {
216 fn get(&self, key: &str) -> Option<String> {
217 web_sys::window()?
218 .local_storage()
219 .ok()
220 .flatten()?
221 .get_item(key)
222 .ok()
223 .flatten()
224 }
225
226 fn set(&self, key: &str, value: &str) {
227 if let Some(storage) = web_sys::window().and_then(|w| w.local_storage().ok().flatten()) {
228 let _ = storage.set_item(key, value);
229 }
230 }
231
232 fn remove(&self, key: &str) {
233 if let Some(storage) = web_sys::window().and_then(|w| w.local_storage().ok().flatten()) {
234 let _ = storage.remove_item(key);
235 }
236 }
237}
238
239pub fn tune_key(base_url: &str) -> String {
244 format!("libfw.tune.{}", origin_hash(base_url))
245}
246
247pub fn origin_hash(base_url: &str) -> String {
249 use sha2::Digest;
250 let origin = extract_origin(base_url);
251 let digest = sha2::Sha256::digest(origin.as_bytes());
252 format!("sha256-{}", hex_lower(&digest))
253}
254
255fn extract_origin(url: &str) -> String {
258 match url.find("://") {
259 Some(i) => {
260 let scheme = &url[..i];
261 let rest = &url[i + 3..];
262 let host_port = rest.split('/').next().unwrap_or("");
263 let host_port = host_port.rsplit('@').next().unwrap_or(host_port);
265 format!(
266 "{}://{}",
267 scheme.to_ascii_lowercase(),
268 host_port.to_ascii_lowercase()
269 )
270 }
271 None => url.to_ascii_lowercase(),
272 }
273}
274
275fn hex_lower(bytes: &[u8]) -> String {
276 const HEX: &[u8; 16] = b"0123456789abcdef";
277 let mut out = String::with_capacity(bytes.len() * 2);
278 for &b in bytes {
279 out.push(HEX[(b >> 4) as usize] as char);
280 out.push(HEX[(b & 0x0f) as usize] as char);
281 }
282 out
283}
284
285pub fn ewma(prev: Option<f64>, sample: f64, alpha: f64) -> f64 {
291 match prev {
292 None => sample,
293 Some(p) => alpha * sample + (1.0 - alpha) * p,
294 }
295}
296
297pub fn bdp_bytes(mbps: f64, rtt_ms: f64) -> u64 {
299 (mbps * 1e6 / 8.0 * rtt_ms / 1_000.0).max(0.0) as u64
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub enum RampDim {
305 Window,
307 Concurrency,
309 ChunkSize,
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub enum RampAction {
316 Raise,
318 AdvanceDim,
320 Settle,
322 Degrade,
324 Hold,
326}
327
328#[derive(Debug, Clone, Copy)]
330pub struct RampInput {
331 pub mbps: f64,
333 pub prev_mbps: f64,
335 pub rtt_inflation: f64,
337 pub errors: u32,
339 pub degraded: bool,
341 pub low_gain_windows: u32,
343 pub in_flight: u64,
345 pub bdp: u64,
347 pub at_cap: bool,
349 pub last_dim: bool,
351}
352
353pub fn ramp_action(input: &RampInput) -> (RampAction, u32) {
366 if input.errors > 0 {
367 return (RampAction::Degrade, 0);
368 }
369 if input.at_cap {
370 return if input.last_dim {
371 (RampAction::Settle, 0)
372 } else {
373 (RampAction::AdvanceDim, 0)
374 };
375 }
376 if input.bdp > 0 && input.in_flight >= input.bdp {
377 return (RampAction::Settle, 0);
378 }
379 if input.rtt_inflation > RTT_INFLATE_THRESHOLD {
380 return (RampAction::Settle, 0);
381 }
382 let gain = if input.degraded {
383 0.0
387 } else if input.prev_mbps > 0.0 {
388 input.mbps / input.prev_mbps - 1.0
389 } else {
390 1.0
393 };
394 if gain >= GAIN_THRESHOLD {
395 return (RampAction::Raise, 0);
396 }
397 let low = input.low_gain_windows + 1;
398 if low >= 2 {
399 (RampAction::Settle, low)
400 } else {
401 (RampAction::Hold, low)
402 }
403}
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub enum CompressLevel {
408 Auto,
410 Fast,
412 Balanced,
414 Max,
416 Fixed(i32),
418}
419
420pub fn resolve_level(level: CompressLevel, caps: &Capabilities) -> i32 {
424 let z = caps.compression.zrip_levels;
425 match level {
426 CompressLevel::Fast => z.min,
427 CompressLevel::Balanced | CompressLevel::Auto => z.default,
428 CompressLevel::Max => z.max,
429 CompressLevel::Fixed(l) => z.clamp_level(l),
430 }
431}
432
433pub fn choose_auto_level(
442 candidates: &[(i32, usize, f64)],
443 uncompressed: usize,
444 mbps: f64,
445) -> i32 {
446 if candidates.is_empty() {
447 return 0;
448 }
449 let bytes_per_ms = if mbps > 0.0 {
451 mbps * 1e6 / 8.0 / 1_000.0
452 } else {
453 10e6 / 8.0 / 1_000.0
455 };
456 let max_saved = candidates
457 .iter()
458 .map(|&(_, clen, _)| uncompressed.saturating_sub(clen))
459 .max()
460 .unwrap_or(0);
461 if max_saved == 0 {
462 return candidates.iter().map(|&(l, _, _)| l).min().unwrap_or(0);
464 }
465 let mut best = (candidates[0].0, f64::NEG_INFINITY);
466 for &(lvl, clen, cms) in candidates {
467 let saved = uncompressed.saturating_sub(clen) as f64;
468 let score = saved / bytes_per_ms - cms;
469 if score > best.1 {
471 best = (lvl, score);
472 }
473 }
474 best.0
475}
476
477#[derive(Debug, Clone)]
483pub struct TuneEvent {
484 pub phase: TunePhase,
485 pub params: TuneParams,
486 pub stats: TuneStats,
487}
488
489pub type TuneHandle = Rc<RefCell<TuningEngine>>;
491
492pub struct TuningEngine {
494 enabled: bool,
495 ttl_ms: u64,
496 level_cfg: CompressLevel,
497 phase: TunePhase,
498 caps: Option<Capabilities>,
499 caps_hash: String,
500 params: TuneParams,
501 stats: TuneStats,
502 auto_level: Option<i32>,
504 auto_level_benchmarked: bool,
506 window_start_ms: f64,
508 window_done_base: u64,
509 window_errors: u32,
510 windows_closed: u32,
511 rtt_ewma: Option<f64>,
512 prev_mbps: f64,
513 low_gain_windows: u32,
514 dim: RampDim,
515 degraded_windows: u32,
516 consecutive_failures: u32,
517 post_degrade_settle: bool,
521 direction: TransferKind,
523 on_tuning: Option<js_sys::Function>,
525}
526
527impl TuningEngine {
528 pub fn new(enabled: bool, ttl_ms: u64, level_cfg: CompressLevel) -> TuningEngine {
530 TuningEngine {
531 enabled,
532 ttl_ms,
533 level_cfg,
534 phase: TunePhase::Uninitialized,
535 caps: None,
536 caps_hash: String::new(),
537 params: TuneParams {
539 concurrency: 1,
540 upload_window: 1,
541 download_window: 1,
542 chunk_size: 1,
543 download_chunk_size: 1,
544 compress_level: 0,
545 },
546 stats: TuneStats::default(),
547 auto_level: None,
548 auto_level_benchmarked: false,
549 window_start_ms: 0.0,
550 window_done_base: 0,
551 window_errors: 0,
552 windows_closed: 0,
553 rtt_ewma: None,
554 prev_mbps: 0.0,
555 low_gain_windows: 0,
556 dim: RampDim::Window,
557 degraded_windows: 0,
558 consecutive_failures: 0,
559 post_degrade_settle: false,
560 direction: TransferKind::Download,
561 on_tuning: None,
562 }
563 }
564
565 pub fn set_direction(&mut self, kind: TransferKind) {
568 self.direction = kind;
569 }
570
571 pub fn direction(&self) -> TransferKind {
573 self.direction
574 }
575
576 pub fn set_on_tuning(&mut self, cb: Option<js_sys::Function>) {
578 self.on_tuning = cb;
579 }
580
581 pub fn phase(&self) -> TunePhase {
583 self.phase
584 }
585
586 pub fn params(&self) -> TuneParams {
588 self.params.clone()
589 }
590
591 pub fn stats(&self) -> TuneStats {
593 self.stats
594 }
595
596 pub fn enabled(&self) -> bool {
598 self.enabled
599 }
600
601 pub fn set_enabled(&mut self, enabled: bool) {
604 self.enabled = enabled;
605 }
606
607 pub fn caps(&self) -> Option<Capabilities> {
609 self.caps.clone()
610 }
611
612 pub fn state_js(&self) -> JsValue {
615 let o = js_sys::Object::new();
616 let phase = match self.phase {
617 TunePhase::Uninitialized => "uninitialized",
618 TunePhase::Ramping => "ramping",
619 TunePhase::Settled => "settled",
620 TunePhase::Degraded => "degraded",
621 };
622 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("phase"), &JsValue::from_str(phase));
623 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("params"), ¶ms_to_js(&self.params));
624 let stats = js_sys::Object::new();
625 let _ = js_sys::Reflect::set(
626 &stats,
627 &JsValue::from_str("rttMs"),
628 &JsValue::from_f64(self.stats.rtt_ms),
629 );
630 let _ = js_sys::Reflect::set(
631 &stats,
632 &JsValue::from_str("mbps"),
633 &JsValue::from_f64(self.stats.mbps),
634 );
635 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("stats"), &stats.into());
636 let _ = js_sys::Reflect::set(
637 &o,
638 &JsValue::from_str("capsHash"),
639 &JsValue::from_str(&self.caps_hash),
640 );
641 o.into()
642 }
643
644 pub fn caps_hash(&self) -> &str {
646 &self.caps_hash
647 }
648
649 pub fn begin_transfer(
656 &mut self,
657 base_url: &str,
658 caps: &Capabilities,
659 store: &dyn TuneStore,
660 now_ms: f64,
661 static_params: &TuneParams,
662 ) -> TuneParams {
663 self.caps = Some(caps.clone());
664 self.caps_hash = caps.caps_hash();
665 self.windows_closed = 0;
666 self.window_errors = 0;
667 self.window_done_base = 0;
668 self.window_start_ms = now_ms;
669 self.auto_level_benchmarked = false;
670 self.auto_level = None;
671 self.post_degrade_settle = false;
672
673 if !self.enabled {
674 self.phase = TunePhase::Uninitialized;
675 self.params = static_params.clamped_into(caps);
676 return self.params.clone();
677 }
678
679 let key = tune_key(base_url);
680 let cached: Option<TuneCache> = store
681 .get(&key)
682 .and_then(|s| serde_json::from_str(&s).ok())
683 .filter(|c: &TuneCache| c.v == CACHE_VERSION);
684
685 match cached {
686 Some(c)
687 if c.caps_hash == self.caps_hash
688 && (now_ms - c.ts as f64) < self.ttl_ms as f64 =>
689 {
690 self.params = c.params.clamped_into(caps);
692 self.stats = c.stats;
693 self.phase = TunePhase::Settled;
694 self.dim = RampDim::Window;
695 }
696 cached => {
697 if cached
699 .filter(|c| c.caps_hash != self.caps_hash)
700 .is_some()
701 {
702 store.remove(&key);
703 }
704 self.params = TuneParams::from_caps_mins(caps);
705 self.stats = TuneStats::default();
706 self.phase = TunePhase::Ramping;
707 self.dim = RampDim::Window;
708 }
709 }
710 self.params.clone()
711 }
712
713 pub fn tick(
720 &mut self,
721 now_ms: f64,
722 done_bytes: u64,
723 rtt_ms: Option<f64>,
724 error: bool,
725 ) -> Option<TuneEvent> {
726 if error {
727 self.window_errors += 1;
728 }
729 if let Some(r) = rtt_ms.filter(|&r| r > 0.0) {
730 let new_ewma = ewma(self.rtt_ewma, r, RTT_EWMA_ALPHA);
731 if self.enabled {
732 let drift_ratio = if self.stats.rtt_ms > 0.0 {
744 (new_ewma - self.stats.rtt_ms).abs() / self.stats.rtt_ms
745 } else {
746 0.0
747 };
748 let drift_visible = drift_ratio > RTT_DRIFT_RE_RAMP
749 && (self.windows_closed >= 1 || drift_ratio > 1.0);
750 if let Some(caps) = self.caps.as_ref().filter(|_| {
751 self.phase == TunePhase::Settled
752 && self.stats.rtt_ms > 0.0
753 && drift_visible
754 }) {
755 self.params = TuneParams::from_caps_mins(caps);
756 self.stats = TuneStats::default();
757 self.phase = TunePhase::Ramping;
758 self.dim = RampDim::Window;
759 self.low_gain_windows = 0;
760 }
761 }
762 self.rtt_ewma = Some(new_ewma);
763 }
764 let elapsed = now_ms - self.window_start_ms;
765 if elapsed < TUNE_WINDOW_MS {
766 return None;
767 }
768 if !self.enabled {
769 return None;
770 }
771 let caps = self.caps.clone()?;
772
773 let bytes = done_bytes.saturating_sub(self.window_done_base);
774 let mbps = if elapsed > 0.0 {
775 bytes as f64 / elapsed * 1_000.0 * 8.0 / 1e6
776 } else {
777 0.0
778 };
779 self.stats.mbps = mbps;
780 if let Some(rtt) = self.rtt_ewma {
781 self.stats.rtt_ms = rtt;
782 }
783
784 let ramping = self.phase == TunePhase::Ramping;
788 let input = RampInput {
789 mbps,
790 prev_mbps: self.prev_mbps,
791 rtt_inflation: if ramping { self.rtt_inflation() } else { 0.0 },
792 errors: self.window_errors,
793 degraded: self.phase == TunePhase::Degraded,
794 low_gain_windows: self.low_gain_windows,
795 in_flight: self.in_flight(),
796 bdp: if ramping {
797 bdp_bytes(mbps, self.rtt_ewma.unwrap_or(self.stats.rtt_ms))
798 } else {
799 0
800 },
801 at_cap: self.dim_at_cap(&caps),
802 last_dim: self.dim == RampDim::ChunkSize,
803 };
804 let (action, low) = ramp_action(&input);
805 self.low_gain_windows = low;
806 self.windows_closed += 1;
807 self.prev_mbps = mbps;
808
809 match action {
810 RampAction::Raise => {
811 self.raise_dim(&caps);
812 if self.phase == TunePhase::Degraded {
813 self.phase = TunePhase::Ramping;
814 }
815 }
816 RampAction::AdvanceDim => {
817 self.dim = next_dim(self.dim);
818 }
819 RampAction::Settle => {
820 self.phase = TunePhase::Settled;
821 self.low_gain_windows = 0;
822 }
823 RampAction::Degrade => {
824 self.halve_dim();
825 self.phase = TunePhase::Degraded;
826 self.degraded_windows = 0;
827 self.low_gain_windows = 0;
828 }
829 RampAction::Hold => {
830 if self.phase == TunePhase::Degraded {
831 self.degraded_windows += 1;
833 if self.degraded_windows >= 2 {
834 self.phase = TunePhase::Settled;
835 self.post_degrade_settle = true;
841 }
842 }
843 }
844 }
845
846 self.window_start_ms = now_ms;
848 self.window_done_base = done_bytes;
849 self.window_errors = 0;
850
851 let event = TuneEvent {
852 phase: self.phase,
853 params: self.params.clone(),
854 stats: self.stats,
855 };
856 self.emit(&event);
857 Some(event)
858 }
859
860 pub fn transfer_end(
864 &mut self,
865 base_url: &str,
866 store: &dyn TuneStore,
867 now_ms: f64,
868 ok: bool,
869 ) {
870 if !self.enabled {
871 return;
872 }
873 if ok {
874 self.consecutive_failures = 0;
875 if self.phase == TunePhase::Settled
880 && !self.post_degrade_settle
881 && self.windows_closed >= MIN_WINDOWS_TO_PERSIST
882 {
883 self.persist(base_url, store, now_ms);
884 }
885 } else {
886 self.consecutive_failures += 1;
887 if self.consecutive_failures >= MAX_CONSECUTIVE_FAILURES {
888 store.remove(&tune_key(base_url));
889 self.consecutive_failures = 0;
890 self.phase = TunePhase::Ramping;
891 }
892 }
893 }
894
895 pub fn persist(&self, base_url: &str, store: &dyn TuneStore, now_ms: f64) {
897 if !self.enabled || self.phase != TunePhase::Settled {
898 return;
899 }
900 let cache = TuneCache {
901 v: CACHE_VERSION,
902 ts: now_ms as u64,
903 caps_hash: self.caps_hash.clone(),
904 params: self.params.clone(),
905 stats: self.stats,
906 };
907 if let Ok(json) = serde_json::to_string(&cache) {
908 store.set(&tune_key(base_url), &json);
909 }
910 }
911
912 pub fn upload_compress_level(
918 &mut self,
919 caps: &Capabilities,
920 sample: Option<&[u8]>,
921 mbps: f64,
922 ) -> i32 {
923 match self.level_cfg {
924 CompressLevel::Auto => {
925 if !self.auto_level_benchmarked {
926 self.auto_level_benchmarked = true;
927 self.auto_level = Some(match sample {
928 Some(s) if !s.is_empty() => {
929 let s = &s[..LEVEL_SAMPLE_SIZE.min(s.len())];
933 let candidates = auto_candidates(caps);
934 let results = benchmark_levels(s, &candidates);
935 choose_auto_level(&results, s.len(), mbps.max(1.0))
936 }
937 _ => caps.compression.zrip_levels.default,
938 });
939 }
940 self.auto_level.unwrap_or(caps.compression.zrip_levels.default)
941 }
942 other => resolve_level(other, caps),
943 }
944 }
945
946 pub fn auto_level_ready(&self) -> bool {
948 self.auto_level_benchmarked
949 }
950
951 fn rtt_inflation(&self) -> f64 {
952 match (self.rtt_ewma, self.stats.rtt_ms) {
953 (Some(cur), base) if base > 0.0 && self.phase == TunePhase::Ramping => {
954 cur / base - 1.0
955 }
956 _ => 0.0,
957 }
958 }
959
960 fn in_flight(&self) -> u64 {
961 match self.dim {
962 RampDim::Window | RampDim::Concurrency | RampDim::ChunkSize => match self
963 .last_transfer_kind()
964 {
965 TransferKind::Upload => self.params.upload_in_flight(),
966 TransferKind::Download => self.params.download_in_flight(),
967 },
968 }
969 }
970
971 fn dim_at_cap(&self, caps: &Capabilities) -> bool {
972 match self.dim {
973 RampDim::Window => match self.last_transfer_kind() {
974 TransferKind::Upload => {
975 self.params.upload_window as i64 >= caps.limits.upload_window.max
976 }
977 TransferKind::Download => {
978 self.params.download_window as i64 >= caps.limits.download_window.max
979 }
980 },
981 RampDim::Concurrency => {
982 self.params.concurrency as i64 >= caps.limits.concurrency.max
983 }
984 RampDim::ChunkSize => match self.last_transfer_kind() {
985 TransferKind::Upload => self.params.chunk_size >= caps.limits.chunk_size.max as u64,
986 TransferKind::Download => {
987 self.params.download_chunk_size >= caps.limits.download_chunk_size.max as u64
988 }
989 },
990 }
991 }
992
993 fn raise_dim(&mut self, caps: &Capabilities) {
994 let dim_max: i64 = match self.dim {
995 RampDim::Window => match self.last_transfer_kind() {
996 TransferKind::Upload => caps.limits.upload_window.max,
997 TransferKind::Download => caps.limits.download_window.max,
998 },
999 RampDim::Concurrency => caps.limits.concurrency.max,
1000 RampDim::ChunkSize => match self.last_transfer_kind() {
1001 TransferKind::Upload => caps.limits.chunk_size.max,
1002 TransferKind::Download => caps.limits.download_chunk_size.max,
1003 },
1004 };
1005 let step = |v: i64| -> i64 {
1008 if v <= 0 {
1009 1
1010 } else if v * 2 <= dim_max {
1011 v * 2
1012 } else {
1013 (v + 1).min(dim_max)
1014 }
1015 };
1016 match self.dim {
1017 RampDim::Window => match self.last_transfer_kind() {
1018 TransferKind::Upload => {
1019 self.params.upload_window = step(self.params.upload_window as i64).max(1) as usize
1020 }
1021 TransferKind::Download => {
1022 self.params.download_window =
1023 step(self.params.download_window as i64).max(1) as usize
1024 }
1025 },
1026 RampDim::Concurrency => {
1027 self.params.concurrency = step(self.params.concurrency as i64).max(1) as usize
1028 }
1029 RampDim::ChunkSize => match self.last_transfer_kind() {
1030 TransferKind::Upload => {
1031 self.params.chunk_size = step(self.params.chunk_size as i64).max(1) as u64
1032 }
1033 TransferKind::Download => {
1034 self.params.download_chunk_size =
1035 step(self.params.download_chunk_size as i64).max(1) as u64
1036 }
1037 },
1038 }
1039 }
1040
1041 fn halve_dim(&mut self) {
1042 let step = |v: i64| -> i64 { ((v as f64) * DEGRADE_FACTOR).floor().max(1.0) as i64 };
1043 match self.dim {
1044 RampDim::Window => match self.last_transfer_kind() {
1045 TransferKind::Upload => {
1046 self.params.upload_window = step(self.params.upload_window as i64) as usize
1047 }
1048 TransferKind::Download => {
1049 self.params.download_window = step(self.params.download_window as i64) as usize
1050 }
1051 },
1052 RampDim::Concurrency => {
1053 self.params.concurrency = step(self.params.concurrency as i64) as usize
1054 }
1055 RampDim::ChunkSize => match self.last_transfer_kind() {
1056 TransferKind::Upload => {
1057 self.params.chunk_size = step(self.params.chunk_size as i64) as u64
1058 }
1059 TransferKind::Download => {
1060 self.params.download_chunk_size = step(self.params.download_chunk_size as i64)
1061 as u64
1062 }
1063 },
1064 }
1065 }
1066}
1067
1068#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1070pub enum TransferKind {
1071 Upload,
1072 Download,
1073}
1074
1075impl TuningEngine {
1076 fn last_transfer_kind(&self) -> TransferKind {
1077 self.direction
1080 }
1081}
1082
1083fn next_dim(dim: RampDim) -> RampDim {
1085 match dim {
1086 RampDim::Window => RampDim::Concurrency,
1087 RampDim::Concurrency => RampDim::ChunkSize,
1088 RampDim::ChunkSize => RampDim::ChunkSize,
1093 }
1094}
1095
1096fn auto_candidates(caps: &Capabilities) -> Vec<i32> {
1098 let z = caps.compression.zrip_levels;
1099 vec![z.min, z.default, z.max]
1100}
1101
1102fn benchmark_levels(sample: &[u8], levels: &[i32]) -> Vec<(i32, usize, f64)> {
1106 levels
1107 .iter()
1108 .map(|&level| {
1109 let t0 = now_ms();
1110 let out = zrip::compress(sample, level).unwrap_or_else(|_| sample.to_vec());
1111 (level, out.len(), now_ms() - t0)
1112 })
1113 .collect()
1114}
1115
1116pub fn now_ms() -> f64 {
1118 #[cfg(target_arch = "wasm32")]
1119 {
1120 js_sys::Date::now()
1121 }
1122 #[cfg(not(target_arch = "wasm32"))]
1123 {
1124 std::time::SystemTime::now()
1125 .duration_since(std::time::UNIX_EPOCH)
1126 .map(|d| d.as_secs_f64() * 1_000.0)
1127 .unwrap_or(0.0)
1128 }
1129}
1130
1131impl TuningEngine {
1132 fn emit(&self, event: &TuneEvent) {
1133 if let Some(cb) = &self.on_tuning {
1134 let params = params_to_js(&event.params);
1135 let stats = js_sys::Object::new();
1136 let _ = js_sys::Reflect::set(
1137 &stats,
1138 &JsValue::from_str("rttMs"),
1139 &JsValue::from_f64(event.stats.rtt_ms),
1140 );
1141 let _ = js_sys::Reflect::set(
1142 &stats,
1143 &JsValue::from_str("mbps"),
1144 &JsValue::from_f64(event.stats.mbps),
1145 );
1146 let phase = match event.phase {
1147 TunePhase::Uninitialized => "uninitialized",
1148 TunePhase::Ramping => "ramping",
1149 TunePhase::Settled => "settled",
1150 TunePhase::Degraded => "degraded",
1151 };
1152 let _ = cb.call3(
1153 &JsValue::NULL,
1154 &JsValue::from_str(phase),
1155 ¶ms,
1156 &stats.into(),
1157 );
1158 }
1159 }
1160}
1161
1162fn params_to_js(params: &TuneParams) -> JsValue {
1163 let o = js_sys::Object::new();
1164 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("concurrency"), &JsValue::from_f64(params.concurrency as f64));
1165 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("uploadWindow"), &JsValue::from_f64(params.upload_window as f64));
1166 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("downloadWindow"), &JsValue::from_f64(params.download_window as f64));
1167 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("chunkSize"), &JsValue::from_f64(params.chunk_size as f64));
1168 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("downloadChunkSize"), &JsValue::from_f64(params.download_chunk_size as f64));
1169 let _ = js_sys::Reflect::set(&o, &JsValue::from_str("compressLevel"), &JsValue::from_f64(params.compress_level as f64));
1170 o.into()
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175 use super::*;
1176 use std::cell::RefCell;
1177 use std::collections::HashMap;
1178
1179 #[derive(Default)]
1180 struct FakeStore {
1181 map: RefCell<HashMap<String, String>>,
1182 }
1183
1184 impl TuneStore for FakeStore {
1185 fn get(&self, key: &str) -> Option<String> {
1186 self.map.borrow().get(key).cloned()
1187 }
1188 fn set(&self, key: &str, value: &str) {
1189 self.map.borrow_mut().insert(key.to_string(), value.to_string());
1190 }
1191 fn remove(&self, key: &str) {
1192 self.map.borrow_mut().remove(key);
1193 }
1194 }
1195
1196 fn caps() -> Capabilities {
1197 Capabilities::default()
1198 }
1199
1200 fn static_params() -> TuneParams {
1201 TuneParams {
1202 concurrency: 4,
1203 upload_window: 8,
1204 download_window: 4,
1205 chunk_size: 2 * 1024 * 1024,
1206 download_chunk_size: 256 * 1024,
1207 compress_level: 1,
1208 }
1209 }
1210
1211 fn engine(store: &dyn TuneStore) -> TuningEngine {
1212 let mut e = TuningEngine::new(true, DEFAULT_TUNE_TTL_MS, CompressLevel::Balanced);
1213 e.set_direction(TransferKind::Upload);
1214 e.begin_transfer("https://h.example/", &caps(), store, 0.0, &static_params());
1215 e
1216 }
1217
1218 #[test]
1221 fn ramp_action_error_degrades() {
1222 let input = RampInput {
1223 mbps: 10.0,
1224 prev_mbps: 5.0,
1225 rtt_inflation: 0.0,
1226 errors: 1,
1227 degraded: false,
1228 low_gain_windows: 0,
1229 in_flight: 100,
1230 bdp: 1_000,
1231 at_cap: false,
1232 last_dim: false,
1233 };
1234 assert_eq!(ramp_action(&input).0, RampAction::Degrade);
1235 }
1236
1237 #[test]
1238 fn ramp_action_cap_advances_or_settles() {
1239 let base = RampInput {
1240 mbps: 10.0,
1241 prev_mbps: 5.0,
1242 rtt_inflation: 0.0,
1243 errors: 0,
1244 degraded: false,
1245 low_gain_windows: 0,
1246 in_flight: 100,
1247 bdp: 1_000,
1248 at_cap: true,
1249 last_dim: false,
1250 };
1251 assert_eq!(ramp_action(&base).0, RampAction::AdvanceDim);
1252 let last = RampInput { last_dim: true, ..base };
1253 assert_eq!(ramp_action(&last).0, RampAction::Settle);
1254 }
1255
1256 #[test]
1257 fn ramp_action_bdp_and_rtt_saturate() {
1258 let base = RampInput {
1259 mbps: 10.0,
1260 prev_mbps: 5.0,
1261 rtt_inflation: 0.0,
1262 errors: 0,
1263 degraded: false,
1264 low_gain_windows: 0,
1265 in_flight: 2_000,
1266 bdp: 1_000,
1267 at_cap: false,
1268 last_dim: false,
1269 };
1270 assert_eq!(ramp_action(&base).0, RampAction::Settle, "in-flight ≥ BDP settles");
1271
1272 let rtt = RampInput {
1273 rtt_inflation: 0.5,
1274 in_flight: 10,
1275 bdp: 1_000,
1276 ..base
1277 };
1278 assert_eq!(ramp_action(&rtt).0, RampAction::Settle, "RTT inflation settles");
1279 }
1280
1281 #[test]
1282 fn ramp_action_gain_raises_and_low_gain_settles() {
1283 let growing = RampInput {
1285 mbps: 20.0,
1286 prev_mbps: 10.0,
1287 ..RampInput {
1288 mbps: 0.0,
1289 prev_mbps: 0.0,
1290 rtt_inflation: 0.0,
1291 errors: 0,
1292 degraded: false,
1293 low_gain_windows: 7,
1294 in_flight: 10,
1295 bdp: 1_000,
1296 at_cap: false,
1297 last_dim: false,
1298 }
1299 };
1300 let (action, low) = ramp_action(&growing);
1301 assert_eq!(action, RampAction::Raise);
1302 assert_eq!(low, 0, "raise resets the low-gain counter");
1303
1304 let flat = RampInput {
1306 mbps: 10.4,
1307 prev_mbps: 10.0,
1308 low_gain_windows: 0,
1309 ..growing
1310 };
1311 let (action, low) = ramp_action(&flat);
1312 assert_eq!(action, RampAction::Hold);
1313 assert_eq!(low, 1, "first below-threshold window holds");
1314
1315 let flat2 = RampInput { low_gain_windows: 1, ..flat };
1317 let (action, low) = ramp_action(&flat2);
1318 assert_eq!(action, RampAction::Settle);
1319 assert_eq!(low, 2);
1320
1321 let first = RampInput {
1323 mbps: 1.0,
1324 prev_mbps: 0.0,
1325 low_gain_windows: 0,
1326 ..growing
1327 };
1328 assert_eq!(ramp_action(&first).0, RampAction::Raise);
1329 }
1330
1331 #[test]
1332 fn ramp_action_degraded_hold_never_raises() {
1333 let degraded = RampInput {
1337 mbps: 0.008,
1338 prev_mbps: 0.0,
1339 degraded: true,
1340 ..RampInput {
1341 mbps: 0.0,
1342 prev_mbps: 0.0,
1343 rtt_inflation: 0.0,
1344 errors: 0,
1345 degraded: false,
1346 low_gain_windows: 0,
1347 in_flight: 10,
1348 bdp: 1_000,
1349 at_cap: false,
1350 last_dim: false,
1351 }
1352 };
1353 let (action, low) = ramp_action(°raded);
1354 assert_eq!(action, RampAction::Hold);
1355 assert_eq!(low, 1, "first stability window holds");
1356 let (action, low) = ramp_action(&RampInput { low_gain_windows: 1, ..degraded });
1357 assert_eq!(action, RampAction::Settle);
1358 assert_eq!(low, 2, "second stability window settles");
1359 }
1360
1361 #[test]
1362 fn ewma_seeds_and_smooths() {
1363 assert_eq!(ewma(None, 10.0, 0.25), 10.0);
1364 let v = ewma(Some(10.0), 20.0, 0.25);
1365 assert!((v - 12.5).abs() < 1e-9);
1366 }
1367
1368 #[test]
1369 fn bdp_conversion() {
1370 assert_eq!(bdp_bytes(10.0, 100.0), 125_000);
1372 assert_eq!(bdp_bytes(0.0, 0.0), 0);
1373 }
1374
1375 #[test]
1376 fn origin_hash_is_stable_and_discriminates() {
1377 assert_eq!(origin_hash("https://h.example/"), origin_hash("https://h.example/a/b"));
1378 assert_eq!(origin_hash("https://h.example:8443/x"), origin_hash("https://h.example:8443/y"));
1379 assert_ne!(origin_hash("https://h.example/"), origin_hash("https://h.example:8443/"));
1380 assert_ne!(origin_hash("https://h.example/"), origin_hash("http://h.example/"));
1381 assert_ne!(origin_hash("https://h.example/"), origin_hash("https://other.example/"));
1382 assert!(tune_key("https://h.example/").starts_with("libfw.tune.sha256-"));
1383 }
1384
1385 #[test]
1386 fn resolve_level_mapping() {
1387 let c = caps();
1388 assert_eq!(resolve_level(CompressLevel::Fast, &c), -8);
1389 assert_eq!(resolve_level(CompressLevel::Balanced, &c), 1);
1390 assert_eq!(resolve_level(CompressLevel::Max, &c), 4);
1391 assert_eq!(resolve_level(CompressLevel::Auto, &c), 1);
1392 assert_eq!(resolve_level(CompressLevel::Fixed(2), &c), 2);
1393 assert_eq!(resolve_level(CompressLevel::Fixed(99), &c), 4);
1394 assert_eq!(resolve_level(CompressLevel::Fixed(-99), &c), -8);
1395 }
1396
1397 #[test]
1398 fn choose_auto_level_picks_by_net_gain() {
1399 assert_eq!(
1401 choose_auto_level(&[(-8, 1000, 0.1), (1, 1000, 5.0), (4, 1000, 40.0)], 1000, 10.0),
1402 -8
1403 );
1404
1405 let cpu_bound = choose_auto_level(&[(-8, 800_000, 0.1), (4, 400_000, 30.0)], 1_000_000, 1000.0);
1407 assert_eq!(cpu_bound, -8);
1408
1409 let savings = choose_auto_level(&[(-8, 800_000, 0.1), (4, 400_000, 30.0)], 1_000_000, 10.0);
1411 assert_eq!(savings, 4);
1412
1413 assert_eq!(choose_auto_level(&[(0, 500, 1.0), (4, 500, 1.0)], 1000, 100.0), 0);
1415 }
1416
1417 #[test]
1420 fn begin_transfer_miss_ramps_from_mins() {
1421 let store = FakeStore::default();
1422 let mut e = TuningEngine::new(true, DEFAULT_TUNE_TTL_MS, CompressLevel::Balanced);
1423 e.set_direction(TransferKind::Upload);
1424 let p = e.begin_transfer("https://h.example/", &caps(), &store, 0.0, &static_params());
1425 assert_eq!(e.phase(), TunePhase::Ramping);
1426 assert_eq!(p.concurrency, 1);
1427 assert_eq!(p.upload_window, 1);
1428 assert_eq!(p.chunk_size, 262_144);
1429 }
1430
1431 #[test]
1432 fn ramp_raises_window_then_concurrency_until_cap() {
1433 let store = FakeStore::default();
1434 let mut e = engine(&store);
1435 let mut t = 1_000.0;
1439 let mut done = 12_500_000u64;
1440 let mut delta = 12_500_000u64;
1441 for _ in 0..3 {
1442 let ev = e.tick(t, done, Some(400.0), false).expect("window closed");
1443 assert_eq!(ev.phase, TunePhase::Ramping);
1444 t += 1_000.0;
1445 delta *= 2;
1446 done += delta; }
1448 assert_eq!(e.params().upload_window, 8);
1450 let ev = e.tick(t, done, Some(400.0), false).unwrap();
1451 assert_eq!(ev.params.upload_window, 8); t += 1_000.0;
1453 delta *= 2;
1454 done += delta;
1455 let ev = e.tick(t, done, Some(400.0), false).unwrap();
1457 assert_eq!(ev.params.concurrency, 2);
1458 let ev = e.tick(t + 1_000.0, done + delta * 2, Some(400.0), false).unwrap();
1460 assert_eq!(ev.params.concurrency, 4);
1461 assert_eq!(ev.params.upload_window, 8);
1462 }
1463
1464 #[test]
1465 fn error_window_degrades_and_halves() {
1466 let store = FakeStore::default();
1467 let mut e = engine(&store);
1468 let mut t = 1_000.0;
1470 let mut done = 12_500_000u64;
1471 let mut delta = 12_500_000u64;
1472 for _ in 0..2 {
1473 e.tick(t, done, Some(400.0), false);
1474 t += 1_000.0;
1475 delta *= 2;
1476 done += delta; }
1478 assert_eq!(e.params().upload_window, 4);
1479 let ev = e.tick(t, done, Some(400.0), true).unwrap();
1481 assert_eq!(ev.phase, TunePhase::Degraded);
1482 assert_eq!(ev.params.upload_window, 2);
1483 let ev = e.tick(t + 1_000.0, done + 1_000, None, false).unwrap();
1486 assert_eq!(ev.phase, TunePhase::Degraded);
1487 let ev = e.tick(t + 2_000.0, done + 2_000, None, false).unwrap();
1488 assert_eq!(ev.phase, TunePhase::Settled);
1489 assert_eq!(ev.params.upload_window, 2);
1490 }
1491
1492 #[test]
1493 fn low_gain_twice_settles_and_persists() {
1494 let store = FakeStore::default();
1495 let mut e = engine(&store);
1496 let ev = e.tick(1_000.0, 12_500_000, Some(400.0), false).unwrap();
1499 assert_eq!(ev.phase, TunePhase::Ramping);
1500 let ev = e.tick(2_000.0, 25_000_000, Some(400.0), false).unwrap(); assert_eq!(ev.phase, TunePhase::Ramping);
1502 let ev = e.tick(3_000.0, 26_000_000, Some(400.0), false).unwrap(); assert_eq!(ev.phase, TunePhase::Settled);
1504 let ev = e.tick(4_000.0, 27_040_000, Some(400.0), false).unwrap(); assert_eq!(ev.phase, TunePhase::Settled);
1506 assert_eq!(ev.params.upload_window, 2);
1507
1508 e.transfer_end("https://h.example/", &store, 4_000.0, true);
1510 let key = tune_key("https://h.example/");
1511 let cached: TuneCache = serde_json::from_str(&store.get(&key).unwrap()).unwrap();
1512 assert_eq!(cached.params.upload_window, 2);
1513 assert_eq!(cached.caps_hash, caps().caps_hash());
1514 }
1515
1516 #[test]
1517 fn cache_reuse_settles_immediately() {
1518 let store = FakeStore::default();
1519 {
1521 let mut e = engine(&store);
1522 e.tick(1_000.0, 12_500_000, Some(400.0), false);
1523 e.tick(2_000.0, 25_000_000, Some(400.0), false);
1524 e.tick(3_000.0, 26_000_000, Some(400.0), false);
1525 e.tick(4_000.0, 27_040_000, Some(400.0), false);
1526 e.transfer_end("https://h.example/", &store, 4_000.0, true);
1527 }
1528 let mut e2 = TuningEngine::new(true, DEFAULT_TUNE_TTL_MS, CompressLevel::Balanced);
1530 e2.set_direction(TransferKind::Upload);
1531 let p = e2.begin_transfer("https://h.example/", &caps(), &store, 10_000.0, &static_params());
1532 assert_eq!(e2.phase(), TunePhase::Settled);
1533 assert_eq!(p.upload_window, 2);
1534 }
1535
1536 #[test]
1537 fn ttl_expiry_re_ramps() {
1538 let store = FakeStore::default();
1539 {
1540 let mut e = engine(&store);
1541 e.tick(1_000.0, 12_500_000, Some(400.0), false);
1542 e.tick(2_000.0, 25_000_000, Some(400.0), false);
1543 e.tick(3_000.0, 26_000_000, Some(400.0), false);
1544 e.tick(4_000.0, 27_040_000, Some(400.0), false);
1545 e.transfer_end("https://h.example/", &store, 4_000.0, true);
1546 }
1547 let mut e2 = TuningEngine::new(true, DEFAULT_TUNE_TTL_MS, CompressLevel::Balanced);
1549 e2.set_direction(TransferKind::Upload);
1550 let p = e2.begin_transfer(
1551 "https://h.example/",
1552 &caps(),
1553 &store,
1554 4_000.0 + DEFAULT_TUNE_TTL_MS as f64 + 1.0,
1555 &static_params(),
1556 );
1557 assert_eq!(e2.phase(), TunePhase::Ramping);
1558 assert_eq!(p.upload_window, 1, "expired cache → ramp from mins");
1559 }
1560
1561 #[test]
1562 fn caps_hash_change_invalidates_cache() {
1563 let store = FakeStore::default();
1564 {
1565 let mut e = engine(&store);
1566 e.tick(1_000.0, 12_500_000, Some(400.0), false);
1567 e.tick(2_000.0, 25_000_000, Some(400.0), false);
1568 e.tick(3_000.0, 26_000_000, Some(400.0), false);
1569 e.tick(4_000.0, 27_040_000, Some(400.0), false);
1570 e.transfer_end("https://h.example/", &store, 4_000.0, true);
1571 }
1572 let mut changed = caps();
1574 changed.limits.concurrency.default = 6;
1575 let mut e2 = TuningEngine::new(true, DEFAULT_TUNE_TTL_MS, CompressLevel::Balanced);
1576 e2.set_direction(TransferKind::Upload);
1577 let p = e2.begin_transfer("https://h.example/", &changed, &store, 10_000.0, &static_params());
1578 assert_eq!(e2.phase(), TunePhase::Ramping);
1579 assert_eq!(p.upload_window, 1);
1580 assert!(store.get(&tune_key("https://h.example/")).is_none(), "stale cache removed");
1581 }
1582
1583 #[test]
1584 fn short_transfer_does_not_persist() {
1585 let store = FakeStore::default();
1586 let mut e = engine(&store);
1587 e.tick(1_000.0, 12_500_000, Some(40.0), false);
1589 e.transfer_end("https://h.example/", &store, 1_500.0, true);
1590 assert!(store.get(&tune_key("https://h.example/")).is_none(), "short transfers are noise");
1591 }
1592
1593 #[test]
1594 fn repeated_failures_remove_cache() {
1595 let store = FakeStore::default();
1596 let mut e = engine(&store);
1597 e.tick(1_000.0, 12_500_000, Some(40.0), false);
1599 e.tick(2_000.0, 25_000_000, Some(40.0), false);
1600 e.tick(3_000.0, 26_000_000, Some(40.0), false);
1601 e.tick(4_000.0, 27_040_000, Some(40.0), false);
1602 e.transfer_end("https://h.example/", &store, 4_000.0, true);
1603 assert!(store.get(&tune_key("https://h.example/")).is_some());
1604
1605 for _ in 0..3 {
1607 e.transfer_end("https://h.example/", &store, 5_000.0, false);
1608 }
1609 assert!(store.get(&tune_key("https://h.example/")).is_none());
1610 }
1611
1612 #[test]
1613 fn rtt_drift_re_ramps_from_cache() {
1614 let store = FakeStore::default();
1615 {
1616 let mut e = engine(&store);
1617 e.tick(1_000.0, 12_500_000, Some(400.0), false);
1618 e.tick(2_000.0, 25_000_000, Some(400.0), false);
1619 e.tick(3_000.0, 26_000_000, Some(400.0), false);
1620 e.tick(4_000.0, 27_040_000, Some(400.0), false);
1621 e.transfer_end("https://h.example/", &store, 4_000.0, true);
1622 }
1623 let mut e2 = TuningEngine::new(true, DEFAULT_TUNE_TTL_MS, CompressLevel::Balanced);
1624 e2.set_direction(TransferKind::Upload);
1625 e2.begin_transfer("https://h.example/", &caps(), &store, 10_000.0, &static_params());
1626 assert_eq!(e2.phase(), TunePhase::Settled);
1627 let ev = e2.tick(11_000.0, 12_500_000, Some(1_200.0), false).unwrap();
1629 assert_eq!(ev.phase, TunePhase::Ramping);
1630 assert!(
1631 ev.params.upload_window <= 2,
1632 "re-ramp starts from mins (first measured window may raise once)"
1633 );
1634 }
1635
1636 #[test]
1637 fn upload_auto_level_benchmarks_once_and_caches() {
1638 let store = FakeStore::default();
1639 let mut e = TuningEngine::new(true, DEFAULT_TUNE_TTL_MS, CompressLevel::Auto);
1640 e.begin_transfer("https://h.example/", &caps(), &store, 0.0, &static_params());
1641 let sample = vec![0xABu8; 256 * 1024];
1642 let lvl1 = e.upload_compress_level(&caps(), Some(&sample), 50.0);
1643 let lvl2 = e.upload_compress_level(&caps(), Some(&sample), 50.0);
1644 assert_eq!(lvl1, lvl2, "benchmark runs once, result cached");
1645 assert!(e.auto_level_ready());
1646 assert!((-8..=4).contains(&lvl1), "level inside advertised range, got {lvl1}");
1647 let mut e2 = TuningEngine::new(true, DEFAULT_TUNE_TTL_MS, CompressLevel::Auto);
1649 e2.begin_transfer("https://h.example/", &caps(), &store, 0.0, &static_params());
1650 let lvl = e2.upload_compress_level(&caps(), Some(&[0u8; 0]), 50.0);
1651 assert_eq!(lvl, caps().compression.zrip_levels.default, "no sample → server default");
1652 }
1653
1654 #[test]
1655 fn disabled_engine_uses_static_params() {
1656 let store = FakeStore::default();
1657 let mut e = TuningEngine::new(false, DEFAULT_TUNE_TTL_MS, CompressLevel::Balanced);
1658 e.set_direction(TransferKind::Upload);
1659 let p = e.begin_transfer("https://h.example/", &caps(), &store, 0.0, &static_params());
1660 assert_eq!(p.concurrency, 4, "static config preserved");
1661 assert_eq!(p.upload_window, 8);
1662 assert_eq!(e.phase(), TunePhase::Uninitialized);
1663 assert!(e.tick(1_000.0, 12_500_000, Some(40.0), false).is_none());
1665 assert_eq!(p, static_params().clamped_into(&caps()));
1666 }
1667}