1use std::collections::VecDeque;
32
33const MLFQ_TREE_MAX_NODES: usize = crate::bpf_intf::mlfq_consts_MLFQ_TREE_MAX_NODES as usize;
36
37const MLFQ_TREE_MAX_DEPTH: usize = crate::bpf_intf::mlfq_consts_MLFQ_TREE_MAX_DEPTH as usize;
39
40const MLFQ_TREE_NR_FEATURES: usize = 9;
43
44pub const DEFAULT_MIN_REL_VAR_REDUCTION: f64 = 1e-3;
54
55pub struct FitScratch {
63 pub weights: Vec<f64>,
64 pub sorted: Vec<WeightedSample>,
65 pub left: Vec<WeightedSample>,
66 pub right: Vec<WeightedSample>,
67 pub nodes: Vec<TreeNode>,
68 #[allow(private_interfaces)]
69 pub queue: VecDeque<NodeSpec>,
70 pub preds: Vec<u64>,
71 pub actuals: Vec<u64>,
72 pub ema_preds: Vec<u64>,
73 pub weights_full: Vec<f64>,
74}
75
76impl FitScratch {
77 pub fn new() -> Self {
79 Self {
80 weights: Vec::with_capacity(16384),
81 sorted: Vec::with_capacity(16384),
82 left: Vec::with_capacity(16384),
83 right: Vec::with_capacity(16384),
84 nodes: Vec::with_capacity(2048),
85 queue: VecDeque::with_capacity(2048),
86 preds: Vec::with_capacity(2048),
87 actuals: Vec::with_capacity(2048),
88 ema_preds: Vec::with_capacity(2048),
89 weights_full: Vec::with_capacity(16384),
90 }
91 }
92}
93
94impl Default for FitScratch {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
108#[repr(C)]
109pub struct TreeFeats {
110 pub prev_burst_ns: u64,
112 pub sleep_ns: u64,
114 pub ema: u64,
116 pub io_wait: u32,
118 pub wake_cnt: u32,
120 pub wake_lat_us: u32,
122 pub queue_wait_us: u32,
124 pub sq_ema: u64,
126 pub sleep_var_ratio: u32,
128 pub pad: u32,
130 pub gpu_submit: u32,
132 pub pad2: u32,
134}
135
136#[derive(Clone, Copy, Debug)]
147#[repr(C, packed)]
148pub struct TreeSample {
149 pub pid: u32,
151 pub queue: u32,
153 pub feats: TreeFeats,
155 pub label_ns: u64,
157 pub version: u32,
159}
160
161#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
169#[repr(C)]
170pub struct TreeNode {
171 pub threshold: u64,
173 pub left: u32,
175 pub right: u32,
177 pub feature: u8,
179 pub pad: [u8; 7],
181}
182
183#[derive(Clone, Debug, Default)]
189pub struct SerializedTree {
190 pub nodes: Vec<TreeNode>,
192}
193
194fn feat_value(f: TreeFeats, id: u8) -> u64 {
200 match id {
201 0 => f.prev_burst_ns,
202 1 => f.sleep_ns,
203 2 => f.ema,
204 3 => f.io_wait as u64,
205 4 => f.wake_cnt as u64,
206 5 => f.wake_lat_us as u64,
207 6 => f.queue_wait_us as u64,
208 7 => f.sq_ema,
209 8 => f.gpu_submit as u64,
210 9 => f.sleep_var_ratio as u64,
211 _ => 0,
212 }
213}
214
215fn midpoint(v: u64, w: u64) -> u64 {
222 debug_assert!(w > v);
223 v + (w - v) / 2
224}
225
226type WeightedSample = (TreeSample, f64);
229
230pub(crate) struct NodeSpec {
234 idx: usize,
235 samples: Vec<WeightedSample>,
236 depth: usize,
237}
238
239#[allow(dead_code)]
248pub fn sample_weights(n: usize) -> Vec<f64> {
249 let half_life = n as f64 / 2.0;
250 (0..n)
251 .map(|i| 2.0f64.powf(-((n - i) as f64) / half_life))
252 .collect()
253}
254
255pub fn sample_weights_into(n: usize, out: &mut Vec<f64>) {
259 out.clear();
260 if out.capacity() < n {
261 out.reserve(n - out.len());
262 }
263 let half_life = n as f64 / 2.0;
264 for i in 0..n {
265 out.push(2.0f64.powf(-((n - i) as f64) / half_life));
266 }
267}
268
269fn label_totals(samples: &[WeightedSample]) -> (f64, f64, f64) {
273 samples
274 .iter()
275 .fold((0.0, 0.0, 0.0), |(sw, swy, swy2), (s, w)| {
276 let y = s.label_ns as f64;
277 (sw + w, swy + w * y, swy2 + w * y * y)
278 })
279}
280
281#[allow(dead_code)]
285fn partition(
286 samples: &[WeightedSample],
287 feature: u8,
288 threshold: u64,
289) -> (Vec<WeightedSample>, Vec<WeightedSample>) {
290 let mut left = Vec::with_capacity(samples.len());
291 let mut right = Vec::with_capacity(samples.len());
292 for s in samples {
293 if feat_value(s.0.feats, feature) <= threshold {
294 left.push(*s);
295 } else {
296 right.push(*s);
297 }
298 }
299 (left, right)
300}
301
302fn leaf_prediction(mean_ns: u64) -> u32 {
308 mean_ns.min(u32::MAX as u64) as u32
309}
310
311#[allow(dead_code)]
325fn best_split(
326 samples: &[WeightedSample],
327 min_samples_leaf: usize,
328 sse: f64,
329 min_rel_var_reduction: f64,
330) -> Option<(u8, u64)> {
331 let n = samples.len();
332 let (total_w, total_wy, total_wy2) = label_totals(samples);
333 let min_reduction = sse * min_rel_var_reduction;
334 let mut best: Option<(u8, u64, f64)> = None;
335
336 for feature in 0..MLFQ_TREE_NR_FEATURES {
337 let mut sorted: Vec<WeightedSample> = samples.to_vec();
338 sorted.sort_by_key(|(s, _)| feat_value(s.feats, feature as u8));
339
340 let mut sw_l = 0.0f64;
341 let mut swy_l = 0.0f64;
342 let mut swy2_l = 0.0f64;
343 let mut i = 0usize;
344 while i < n {
345 let v = feat_value(sorted[i].0.feats, feature as u8);
346 let mut j = i;
347 while j < n && feat_value(sorted[j].0.feats, feature as u8) == v {
348 let (s, w) = sorted[j];
349 let y = s.label_ns as f64;
350 sw_l += w;
351 swy_l += w * y;
352 swy2_l += w * y * y;
353 j += 1;
354 }
355
356 let n_l = j;
358 let n_r = n - j;
359 if n_l >= min_samples_leaf && n_r >= min_samples_leaf && j < n {
360 let v_next = feat_value(sorted[j].0.feats, feature as u8);
361 let threshold = midpoint(v, v_next);
362 let sse_l = swy2_l - swy_l * swy_l / sw_l;
363 let sw_r = total_w - sw_l;
364 let swy_r = total_wy - swy_l;
365 let swy2_r = total_wy2 - swy2_l;
366 let sse_r = swy2_r - swy_r * swy_r / sw_r;
367 let reduction = sse - sse_l - sse_r;
368
369 if reduction > min_reduction {
370 let replace = match best {
371 Some((_, _, r)) => reduction > r,
372 None => true,
373 };
374 if replace {
375 best = Some((feature as u8, threshold, reduction));
376 }
377 }
378 }
379 i = j;
380 }
381 }
382
383 best.map(|(f, t, _)| (f, t))
384}
385
386fn best_split_with_scratch(
390 samples: &[WeightedSample],
391 min_samples_leaf: usize,
392 sse: f64,
393 min_rel_var_reduction: f64,
394 scratch: &mut Vec<WeightedSample>,
395) -> Option<(u8, u64)> {
396 let n = samples.len();
397 let (total_w, total_wy, total_wy2) = label_totals(samples);
398 let min_reduction = sse * min_rel_var_reduction;
399 let mut best: Option<(u8, u64, f64)> = None;
400
401 for feature in 0..MLFQ_TREE_NR_FEATURES {
402 scratch.clear();
403 scratch.extend_from_slice(samples);
404 scratch.sort_by_key(|(s, _)| feat_value(s.feats, feature as u8));
405 let sorted = &*scratch;
406
407 let mut sw_l = 0.0f64;
408 let mut swy_l = 0.0f64;
409 let mut swy2_l = 0.0f64;
410 let mut i = 0usize;
411 while i < n {
412 let v = feat_value(sorted[i].0.feats, feature as u8);
413 let mut j = i;
414 while j < n && feat_value(sorted[j].0.feats, feature as u8) == v {
415 let (s, w) = sorted[j];
416 let y = s.label_ns as f64;
417 sw_l += w;
418 swy_l += w * y;
419 swy2_l += w * y * y;
420 j += 1;
421 }
422
423 let n_l = j;
425 let n_r = n - j;
426 if n_l >= min_samples_leaf && n_r >= min_samples_leaf && j < n {
427 let v_next = feat_value(sorted[j].0.feats, feature as u8);
428 let threshold = midpoint(v, v_next);
429 let sse_l = swy2_l - swy_l * swy_l / sw_l;
430 let sw_r = total_w - sw_l;
431 let swy_r = total_wy - swy_l;
432 let swy2_r = total_wy2 - swy2_l;
433 let sse_r = swy2_r - swy_r * swy_r / sw_r;
434 let reduction = sse - sse_l - sse_r;
435
436 if reduction > min_reduction {
437 let replace = match best {
438 Some((_, _, r)) => reduction > r,
439 None => true,
440 };
441 if replace {
442 best = Some((feature as u8, threshold, reduction));
443 }
444 }
445 }
446 i = j;
447 }
448 }
449
450 best.map(|(f, t, _)| (f, t))
451}
452
453#[allow(dead_code)]
490pub fn fit(
491 samples: &[TreeSample],
492 max_depth: usize,
493 min_samples_leaf: usize,
494 max_nodes: usize,
495 min_rel_var_reduction: f64,
496) -> SerializedTree {
497 let mut scratch = FitScratch::new();
498 fit_with_scratch(
499 samples,
500 max_depth,
501 min_samples_leaf,
502 max_nodes,
503 min_rel_var_reduction,
504 &mut scratch,
505 )
506}
507
508pub fn fit_with_scratch(
512 samples: &[TreeSample],
513 max_depth: usize,
514 min_samples_leaf: usize,
515 max_nodes: usize,
516 min_rel_var_reduction: f64,
517 scratch: &mut FitScratch,
518) -> SerializedTree {
519 if samples.is_empty() || max_nodes == 0 {
520 return SerializedTree::default();
521 }
522
523 sample_weights_into(samples.len(), &mut scratch.weights);
524 let weights = &scratch.weights;
525 scratch.nodes.clear();
526 scratch.nodes.reserve(max_nodes);
527 scratch.queue.clear();
528 let nodes = &mut scratch.nodes;
530 let queue = &mut scratch.queue;
531 nodes.push(TreeNode::default()); scratch.left.clear();
535 for (s, w) in samples.iter().copied().zip(weights.iter().copied()) {
536 scratch.left.push((s, w));
537 }
538 let mut root_samples = Vec::new();
539 std::mem::swap(&mut root_samples, &mut scratch.left);
540 queue.push_back(NodeSpec {
541 idx: 0,
542 samples: root_samples,
543 depth: 0,
544 });
545
546 while let Some(spec) = queue.pop_front() {
547 let n = spec.samples.len();
548 let (sw, swy, swy2) = label_totals(&spec.samples);
549 let sse = swy2 - swy * swy / sw;
550
551 let splittable = spec.depth < max_depth
552 && n >= 2 * min_samples_leaf
553 && nodes.len() + 2 <= max_nodes
554 && sse > 1e-12 * swy2;
564 let best = if splittable {
565 best_split_with_scratch(
567 &spec.samples,
568 min_samples_leaf,
569 sse,
570 min_rel_var_reduction,
571 &mut scratch.sorted,
572 )
573 } else {
574 None
575 };
576
577 match best {
578 Some((feature, threshold)) => {
579 let left_idx = nodes.len() as u32;
580 let right_idx = left_idx + 1;
581 nodes[spec.idx] = TreeNode {
582 threshold,
583 left: left_idx,
584 right: right_idx,
585 feature,
586 pad: [0; 7],
587 };
588 nodes.push(TreeNode::default());
590 nodes.push(TreeNode::default());
591 scratch.left.clear();
596 scratch.right.clear();
597 for s in &spec.samples {
598 if feat_value(s.0.feats, feature) <= threshold {
599 scratch.left.push(*s);
600 } else {
601 scratch.right.push(*s);
602 }
603 }
604 let mut left = Vec::new();
605 let mut right = Vec::new();
606 std::mem::swap(&mut left, &mut scratch.left);
607 std::mem::swap(&mut right, &mut scratch.right);
608 queue.push_back(NodeSpec {
609 idx: left_idx as usize,
610 samples: left,
611 depth: spec.depth + 1,
612 });
613 queue.push_back(NodeSpec {
614 idx: right_idx as usize,
615 samples: right,
616 depth: spec.depth + 1,
617 });
618 }
619 None => {
620 nodes[spec.idx] = TreeNode {
621 threshold: 0,
622 left: leaf_prediction((swy / sw) as u64),
623 right: 0,
624 feature: 0,
625 pad: [0; 7],
626 };
627 }
628 }
629 }
630
631 let mut out_nodes = Vec::new();
632 std::mem::swap(&mut out_nodes, nodes);
633 SerializedTree { nodes: out_nodes }
634}
635
636pub fn serialize_validate(tree: &SerializedTree) -> Result<(), String> {
653 let nr = tree.nodes.len();
654 if nr < 1 {
655 return Err("empty tree: the root node is required".into());
656 }
657 if nr > MLFQ_TREE_MAX_NODES {
658 return Err(format!(
659 "{nr} nodes exceed the store bound {MLFQ_TREE_MAX_NODES}"
660 ));
661 }
662
663 let mut depth = vec![usize::MAX; nr];
665 depth[0] = 0;
666 let mut queue = VecDeque::from([0usize]);
667 while let Some(i) = queue.pop_front() {
668 let node = &tree.nodes[i];
669 if node.right == 0 {
670 continue; }
672 if node.feature >= MLFQ_TREE_NR_FEATURES as u8 {
673 return Err(format!(
674 "node {i} splits on feature {} beyond the {} populated slots",
675 node.feature, MLFQ_TREE_NR_FEATURES
676 ));
677 }
678 if depth[i] >= MLFQ_TREE_MAX_DEPTH {
679 return Err(format!(
680 "node {i} at depth {} is an internal node at or beyond the walk bound {MLFQ_TREE_MAX_DEPTH}",
681 depth[i]
682 ));
683 }
684 for child in [node.left, node.right] {
685 let c = child as usize;
686 if c >= nr {
687 return Err(format!(
688 "node {i} child index {c} is out of range (nr_nodes {nr})"
689 ));
690 }
691 if c <= i {
692 return Err(format!(
693 "node {i} child index {c} precedes its parent, the BFS order is violated"
694 ));
695 }
696 if depth[c] == usize::MAX {
703 depth[c] = depth[i] + 1;
704 queue.push_back(c);
705 }
706 }
707 }
708 for (i, d) in depth.iter().enumerate() {
709 if *d > MLFQ_TREE_MAX_DEPTH {
710 return Err(format!(
711 "node {i} sits at depth {d}, beyond the walk bound {MLFQ_TREE_MAX_DEPTH}"
712 ));
713 }
714 if *d == usize::MAX {
715 return Err(format!(
716 "node {i} is not reachable from the root, the layout is not a tree"
717 ));
718 }
719 }
720 Ok(())
721}
722
723pub fn predict(tree: &SerializedTree, feats: &TreeFeats) -> u64 {
738 if tree.nodes.is_empty() {
739 return 0; }
741
742 let feat: [u64; 16] = [
743 feats.prev_burst_ns,
744 feats.sleep_ns,
745 feats.ema,
746 feats.io_wait as u64,
747 feats.wake_cnt as u64,
748 feats.wake_lat_us as u64,
749 feats.queue_wait_us as u64,
750 feats.sq_ema,
751 feats.gpu_submit as u64,
752 feats.sleep_var_ratio as u64,
753 0,
754 0,
755 0,
756 0,
757 0,
758 0,
759 ];
760 let mask = MLFQ_TREE_MAX_NODES - 1;
761 let mut idx = 0usize;
762
763 for _ in 0..MLFQ_TREE_MAX_DEPTH {
764 if idx >= tree.nodes.len() {
765 return 0; }
767 let node = &tree.nodes[idx];
768 if node.right == 0 {
769 return node.left as u64;
770 }
771 let feature = (node.feature & 0xF) as usize;
772 let next = if feat[feature] <= node.threshold {
773 node.left as usize
774 } else {
775 node.right as usize
776 };
777 idx = next & mask;
778 }
779
780 if idx >= tree.nodes.len() {
784 return 0;
785 }
786 let node = &tree.nodes[idx];
787 if node.right == 0 {
788 node.left as u64
789 } else {
790 0
791 }
792}
793
794pub fn should_publish(mae_tree: f64, mae_ema: f64, corr: f64, published_corr: Option<f64>) -> bool {
809 if mae_tree > mae_ema {
810 return false;
811 }
812 if corr < 0.30 {
813 return false;
814 }
815 if let Some(pc) = published_corr {
816 if corr <= pc + 1e-9 {
817 return false;
818 }
819 }
820 true
821}
822
823pub fn weighted_holdout_mae(preds: &[u64], actuals: &[u64], weights: &[f64]) -> f64 {
828 assert_eq!(preds.len(), actuals.len());
829 assert_eq!(preds.len(), weights.len());
830 if preds.is_empty() {
831 return 0.0;
832 }
833 let mut sw = 0.0;
834 let mut sw_err = 0.0;
835 for ((p, a), w) in preds.iter().zip(actuals).zip(weights.iter()) {
836 let err = if p >= a { *p - *a } else { *a - *p } as f64;
837 sw += *w;
838 sw_err += *w * err;
839 }
840 if sw == 0.0 {
841 0.0
842 } else {
843 sw_err / sw
844 }
845}
846
847pub const MLFQ_TREE_SAMPLE_VERSION: u32 = crate::bpf_intf::mlfq_consts_MLFQ_TREE_SAMPLE_VERSION;
850
851pub fn sample_version_matches(s: &TreeSample) -> bool {
858 s.version == MLFQ_TREE_SAMPLE_VERSION
859}
860
861const MLFQ_TREE_META_ACTIVE_SHIFT: u32 = 1;
864
865pub fn tree_meta(generation: u64, nr_nodes: usize, active: u64) -> u64 {
874 ((generation & 0xFFFF_FFFF) << crate::bpf_intf::MLFQ_TREE_META_GENERATION_SHIFT)
883 | ((nr_nodes as u64) << crate::bpf_intf::MLFQ_TREE_META_NR_NODES_SHIFT)
884 | ((active & 1) << MLFQ_TREE_META_ACTIVE_SHIFT)
885 | crate::bpf_intf::MLFQ_TREE_META_TRAINED as u64
886}
887
888#[allow(dead_code)]
893pub fn mae(preds: &[u64], actuals: &[u64]) -> f64 {
894 assert_eq!(
895 preds.len(),
896 actuals.len(),
897 "prediction and actual vectors differ in length"
898 );
899 if preds.is_empty() {
900 return 0.0;
901 }
902 let total: u128 = preds
903 .iter()
904 .zip(actuals)
905 .map(|(p, a)| (if p >= a { *p - *a } else { *a - *p }) as u128)
906 .sum();
907 total as f64 / preds.len() as f64
908}
909
910pub fn pearson(x: &[u64], y: &[u64]) -> f64 {
917 assert_eq!(x.len(), y.len(), "correlation vectors differ in length");
918 let n = x.len();
919 if n == 0 {
920 return 0.0;
921 }
922
923 let mean_x = x.iter().map(|v| *v as f64).sum::<f64>() / n as f64;
924 let mean_y = y.iter().map(|v| *v as f64).sum::<f64>() / n as f64;
925
926 let (mut cov, mut var_x, mut var_y) = (0.0, 0.0, 0.0);
927 for (xi, yi) in x.iter().zip(y) {
928 let dx = *xi as f64 - mean_x;
929 let dy = *yi as f64 - mean_y;
930 cov += dx * dy;
931 var_x += dx * dx;
932 var_y += dy * dy;
933 }
934 if var_x == 0.0 || var_y == 0.0 {
935 return 0.0;
936 }
937 cov / (var_x * var_y).sqrt()
938}
939
940#[cfg(test)]
941mod tests {
942 use super::*;
943
944 struct Rng(u64);
946
947 impl Rng {
948 fn next_u64(&mut self) -> u64 {
949 let mut x = self.0;
950 x ^= x << 13;
951 x ^= x >> 7;
952 x ^= x << 17;
953 self.0 = x;
954 x.wrapping_mul(0x2545_F491_4F6C_DD1D)
955 }
956
957 fn next_in(&mut self, lo: u64, hi: u64) -> u64 {
958 lo + self.next_u64() % (hi - lo)
959 }
960 }
961
962 fn sample(pid: u32, sleep_ns: u64, label_ns: u64) -> TreeSample {
963 TreeSample {
964 pid,
965 version: MLFQ_TREE_SAMPLE_VERSION,
966 queue: 1,
967 feats: TreeFeats {
968 sleep_ns,
969 ..TreeFeats::default()
970 },
971 label_ns,
972 }
973 }
974
975 fn leaf_index(tree: &SerializedTree, feats: &TreeFeats) -> usize {
978 let feat: [u64; 16] = [
979 feats.prev_burst_ns,
980 feats.sleep_ns,
981 feats.ema,
982 feats.io_wait as u64,
983 feats.wake_cnt as u64,
984 feats.wake_lat_us as u64,
985 feats.queue_wait_us as u64,
986 feats.sq_ema,
987 feats.gpu_submit as u64,
988 feats.sleep_var_ratio as u64,
989 0,
990 0,
991 0,
992 0,
993 0,
994 0,
995 ];
996 let mask = MLFQ_TREE_MAX_NODES - 1;
997 let mut idx = 0usize;
998 for _ in 0..MLFQ_TREE_MAX_DEPTH {
999 if idx >= tree.nodes.len() {
1000 return idx;
1001 }
1002 let node = &tree.nodes[idx];
1003 if node.right == 0 {
1004 return idx;
1005 }
1006 let feature = (node.feature & 0xF) as usize;
1007 let next = if feat[feature] <= node.threshold {
1008 node.left as usize
1009 } else {
1010 node.right as usize
1011 };
1012 idx = next & mask;
1013 }
1014 idx
1015 }
1016
1017 #[test]
1018 fn separable_dataset_recovers_split() {
1019 let mut rng = Rng(0xC0FFEE);
1024 let mut samples = Vec::new();
1025 for pid in 0..512u32 {
1026 let interactive = pid % 2 == 0;
1027 let sleep = if interactive { 500_000 } else { 2_000_000 };
1028 let sleep = sleep + rng.next_in(0, 100_000);
1029 let label = if interactive { 100_000 } else { 10_000_000 };
1030 let label = label + rng.next_in(0, 10_000);
1031 samples.push(sample(pid, sleep, label));
1032 }
1033
1034 let tree = fit(&samples, 12, 2, 63, DEFAULT_MIN_REL_VAR_REDUCTION);
1035 serialize_validate(&tree).unwrap();
1036
1037 let root = &tree.nodes[0];
1038 assert!(root.right != 0, "the root must be an internal node");
1039 assert_eq!(root.feature, 1, "the root must split on sleep_ns");
1040 assert!(
1041 root.threshold > 1_000_000 && root.threshold < 1_500_000,
1042 "root threshold {} must sit between the class regions",
1043 root.threshold
1044 );
1045
1046 for s in &samples {
1048 let feats = s.feats;
1049 let pred = predict(&tree, &feats);
1050 if s.feats.sleep_ns < 1_000_000 {
1051 assert!(
1052 (90_000..=110_000).contains(&pred),
1053 "interactive sample predicted {pred}"
1054 );
1055 } else {
1056 assert!(
1057 (9_900_000..=10_100_000).contains(&pred),
1058 "CPU-bound sample predicted {pred}"
1059 );
1060 }
1061 }
1062 }
1063
1064 #[test]
1065 fn irrelevant_feature_never_split_on() {
1066 let mut rng = Rng(0xBEEF);
1071 let mut samples = Vec::new();
1072 for pid in 0..1024u32 {
1073 let interactive = pid % 2 == 0;
1074 let sleep = if interactive { 500_000 } else { 2_000_000 };
1075 let label = if interactive { 100_000 } else { 10_000_000 };
1076 let wake_cnt = rng.next_in(0, 1_000_000);
1077 let mut s = sample(pid, sleep, label);
1078 s.feats.wake_cnt = wake_cnt as u32;
1079 samples.push(s);
1080 }
1081
1082 let tree = fit(&samples, 4, 2, 127, DEFAULT_MIN_REL_VAR_REDUCTION);
1083 serialize_validate(&tree).unwrap();
1084
1085 for (i, node) in tree.nodes.iter().enumerate() {
1086 if node.right != 0 {
1087 assert_ne!(
1088 node.feature, 4,
1089 "node {i} must not split on the irrelevant wake_cnt"
1090 );
1091 }
1092 }
1093 }
1094
1095 #[test]
1096 fn depth_and_min_leaf_caps_respected() {
1097 let mut samples = Vec::new();
1103 for (i, sleep) in (100_000u64..1_700_000).step_by(100_000).enumerate() {
1104 for k in 0..4 {
1105 let mut s = sample(i as u32 + k, sleep, sleep);
1106 s.feats.prev_burst_ns = 50_000 + 10_000 * (i % 5) as u64;
1107 samples.push(s);
1108 }
1109 }
1110
1111 let max_depth = 3;
1112 let min_samples_leaf = 2;
1113 let tree = fit(&samples, max_depth, min_samples_leaf, 1000, 0.0);
1114 serialize_validate(&tree).unwrap();
1115
1116 let mut depth = vec![0usize; tree.nodes.len()];
1118 for (i, node) in tree.nodes.iter().enumerate() {
1119 if node.right != 0 {
1120 assert!(
1121 depth[i] < max_depth,
1122 "node {i} at depth {} exceeds the cap {max_depth}",
1123 depth[i]
1124 );
1125 depth[node.left as usize] = depth[i] + 1;
1126 depth[node.right as usize] = depth[i] + 1;
1127 }
1128 }
1129
1130 let mut leaf_counts = std::collections::HashMap::new();
1132 for s in &samples {
1133 let feats = s.feats;
1134 *leaf_counts
1135 .entry(leaf_index(&tree, &feats))
1136 .or_insert(0usize) += 1;
1137 }
1138 assert!(!leaf_counts.is_empty(), "the tree must have leaves");
1139 for (leaf, count) in &leaf_counts {
1140 assert!(
1141 *count >= min_samples_leaf,
1142 "leaf {leaf} holds {count} samples, below the cap {min_samples_leaf}"
1143 );
1144 }
1145 }
1146
1147 #[test]
1148 fn serialize_walk_roundtrip() {
1149 let mut rng = Rng(0xABCDEF);
1154 let mut samples = Vec::new();
1155 for pid in 0..1024u32 {
1156 let interactive = pid % 3 == 0;
1157 let sleep = if interactive { 400_000 } else { 2_500_000 };
1158 let sleep = sleep + rng.next_in(0, 50_000);
1159 let label = if interactive { 80_000 } else { 12_000_000 };
1160 let label = label + rng.next_in(0, 200_000);
1161 samples.push(sample(pid, sleep, label));
1162 }
1163
1164 let tree = fit(&samples, 8, 4, 511, DEFAULT_MIN_REL_VAR_REDUCTION);
1165 serialize_validate(&tree).unwrap();
1166
1167 let n = samples.len();
1168 let half_life = n as f64 / 2.0;
1169 let weight_of = |i: usize| 2.0f64.powf(-((n - i) as f64) / half_life);
1170
1171 for s in &samples {
1172 let feats = s.feats; let leaf = leaf_index(&tree, &feats);
1174 let mut wsum = 0.0f64;
1175 let mut wsum_y = 0.0f64;
1176 for (j, t) in samples.iter().enumerate() {
1177 let t_feats = t.feats;
1178 if leaf_index(&tree, &t_feats) == leaf {
1179 wsum += weight_of(j);
1180 wsum_y += weight_of(j) * t.label_ns as f64;
1181 }
1182 }
1183 let expected = leaf_prediction((wsum_y / wsum) as u64) as u64;
1184 let pid = s.pid;
1185 assert_eq!(
1186 predict(&tree, &feats),
1187 expected,
1188 "round-trip prediction for pid {} via leaf {leaf}",
1189 pid
1190 );
1191 }
1192 }
1193
1194 #[test]
1195 fn holdout_mae_beats_constant_mean() {
1196 let mut rng = Rng(0x1234_5678);
1199 let mut samples = Vec::new();
1200 for pid in 0..1200u32 {
1201 let interactive = pid % 2 == 0;
1202 let sleep = if interactive { 300_000 } else { 3_000_000 };
1203 let sleep = sleep + rng.next_in(0, 100_000);
1204 let label = if interactive { 120_000 } else { 15_000_000 };
1205 let label = label + rng.next_in(0, 500_000);
1206 samples.push(sample(pid, sleep, label));
1207 }
1208
1209 let split = 2 * samples.len() / 3;
1210 let (train, test) = samples.split_at(split);
1211 let tree = fit(train, 8, 4, 511, DEFAULT_MIN_REL_VAR_REDUCTION);
1212 serialize_validate(&tree).unwrap();
1213
1214 let actuals: Vec<u64> = test.iter().map(|s| s.label_ns).collect();
1215 let preds: Vec<u64> = test
1216 .iter()
1217 .map(|s| {
1218 let feats = s.feats;
1219 predict(&tree, &feats)
1220 })
1221 .collect();
1222 let mae_tree = mae(&preds, &actuals);
1223
1224 let mean_label = train.iter().map(|s| s.label_ns as f64).sum::<f64>() / train.len() as f64;
1225 let const_preds = vec![mean_label as u64; test.len()];
1226 let mae_const = mae(&const_preds, &actuals);
1227
1228 assert!(
1229 mae_tree < mae_const,
1230 "tree MAE {mae_tree} must beat the constant-mean MAE {mae_const}"
1231 );
1232 }
1233
1234 #[test]
1235 fn empty_input_errors() {
1236 let tree = fit(&[], 4, 2, 63, DEFAULT_MIN_REL_VAR_REDUCTION);
1237 assert!(tree.nodes.is_empty(), "an empty fit yields no nodes");
1238 assert!(
1239 serialize_validate(&tree).is_err(),
1240 "an empty tree must not validate"
1241 );
1242 assert_eq!(predict(&tree, &TreeFeats::default()), 0);
1243
1244 let s = sample(0, 1_000, 1_000);
1246 let tree = fit(&[s], 4, 2, 0, DEFAULT_MIN_REL_VAR_REDUCTION);
1247 assert!(tree.nodes.is_empty());
1248 assert!(serialize_validate(&tree).is_err());
1249
1250 assert_eq!(mae(&[], &[]), 0.0);
1252 assert_eq!(pearson(&[], &[]), 0.0);
1253 }
1254
1255 #[test]
1256 fn threshold_roundtrip_u64_precision() {
1257 let lo = u64::MAX - 1_000_000;
1261 let hi = u64::MAX - 100_000;
1262 let mut samples = Vec::new();
1263 for k in 0..4 {
1264 let mut a = sample(k, lo, 100_000);
1265 a.feats.prev_burst_ns = 1_000; let mut b = sample(k + 4, hi, 10_000_000);
1267 b.feats.prev_burst_ns = 1_000;
1268 samples.push(a);
1269 samples.push(b);
1270 }
1271
1272 let tree = fit(&samples, 4, 2, 31, DEFAULT_MIN_REL_VAR_REDUCTION);
1273 serialize_validate(&tree).unwrap();
1274
1275 let root = &tree.nodes[0];
1276 assert_eq!(root.feature, 1, "the root must split on sleep_ns");
1277 assert_eq!(root.threshold, lo + (hi - lo) / 2);
1278
1279 for s in &samples {
1280 let feats = s.feats;
1281 let pid = s.pid;
1282 let pred = predict(&tree, &feats);
1283 let expected = if s.feats.sleep_ns == lo {
1284 100_000
1285 } else {
1286 10_000_000
1287 };
1288 assert_eq!(pred, expected, "pid {}", pid);
1289 }
1290
1291 let samples = [sample(0, 100, 1_000), sample(1, 101, 9_000)];
1294 let tree = fit(&samples, 4, 1, 31, DEFAULT_MIN_REL_VAR_REDUCTION);
1295 serialize_validate(&tree).unwrap();
1296 assert_eq!(tree.nodes[0].threshold, 100);
1297 let f0 = samples[0].feats;
1298 let f1 = samples[1].feats;
1299 assert_eq!(predict(&tree, &f0), 1_000);
1300 assert_eq!(predict(&tree, &f1), 9_000);
1301 }
1302
1303 #[test]
1304 fn validate_rejects_broken_trees() {
1305 let samples = [sample(0, 100, 1_000), sample(1, 101, 9_000)];
1307 let fit_ok = |min_rel: f64| fit(&samples, 4, 1, 31, min_rel);
1308 assert!(serialize_validate(&fit_ok(0.0)).is_ok());
1309
1310 let mut bad = fit_ok(0.0);
1312 bad.nodes[0].feature = 9;
1313 assert!(serialize_validate(&bad).is_err());
1314
1315 let mut bad = fit_ok(0.0);
1317 bad.nodes[0].right = MLFQ_TREE_MAX_NODES as u32;
1318 assert!(serialize_validate(&bad).is_err());
1319
1320 let mut bad = fit_ok(0.0);
1322 bad.nodes[0].left = 0;
1323 assert!(serialize_validate(&bad).is_err());
1324
1325 let mut samples = Vec::new();
1327 for pid in 0..64u32 {
1328 let sleep = if pid % 2 == 0 { 400_000 } else { 2_500_000 };
1329 let label = if pid % 2 == 0 { 80_000 } else { 12_000_000 };
1330 samples.push(sample(pid, sleep, label));
1331 }
1332 let tree = fit(&samples, 4, 2, 63, DEFAULT_MIN_REL_VAR_REDUCTION);
1333 assert!(serialize_validate(&tree).is_ok());
1334 }
1335
1336 #[test]
1337 fn metrics_on_known_vectors() {
1338 assert_eq!(mae(&[10, 20], &[12, 18]), 2.0);
1339 assert_eq!(mae(&[1_000], &[2_000]), 1_000.0);
1340
1341 let x = [1, 2, 3, 4];
1344 let y = [10, 20, 30, 40];
1345 assert!((pearson(&x, &y) - 1.0).abs() < 1e-12);
1346 assert!((pearson(&x, &[40, 30, 20, 10]) + 1.0).abs() < 1e-12);
1347 assert!(pearson(&x, &[1, 4, 4, 1]).abs() < 1e-12);
1348
1349 assert_eq!(pearson(&x, &[5, 5, 5, 5]), 0.0);
1351 }
1352
1353 #[test]
1354 fn publish_gate_and_tree_meta() {
1355 assert!(should_publish(100.0, 100.0, 0.5, None));
1360 assert!(should_publish(99.0, 100.0, 0.5, None));
1361 assert!(!should_publish(101.0, 100.0, 0.5, None));
1362 assert!(should_publish(0.0, 0.0, 0.5, None));
1363 assert!(!should_publish(90.0, 100.0, 0.29, None));
1364 assert!(should_publish(90.0, 100.0, 0.30, None));
1365 assert!(!should_publish(90.0, 100.0, 0.0, None));
1366 assert!(!should_publish(90.0, 100.0, 0.30, Some(0.30)));
1367 assert!(should_publish(90.0, 100.0, 0.31, Some(0.30)));
1368 assert!(!should_publish(90.0, 100.0, 0.50, Some(0.50)));
1369 assert!(should_publish(90.0, 100.0, 0.52, Some(0.50)));
1370 assert!(should_publish(90.0, 100.0, 0.72, Some(0.52)));
1371 let preds = [100, 200];
1373 let actuals = [110, 190];
1374 let w = [0.25, 1.0];
1375 let wm = weighted_holdout_mae(&preds, &actuals, &w);
1376 assert!((wm - 10.0).abs() < 1e-9);
1377
1378 assert_eq!(tree_meta(0, 1, 0), (1 << 8) | 1);
1382 assert_eq!(tree_meta(0, 2048, 1), (2048 << 8) | (1 << 1) | 1);
1383 let m = tree_meta(3, 7, 0);
1384 assert_eq!(m, (3u64 << 32) | (7 << 8) | 1);
1385 assert_eq!(
1386 m & crate::bpf_intf::MLFQ_TREE_META_TRAINED as u64,
1387 crate::bpf_intf::MLFQ_TREE_META_TRAINED as u64
1388 );
1389 assert_eq!((m >> 1) & 1, 0);
1390 assert_eq!((m >> 8) & 0xFFFFFF, 7);
1391 assert_eq!(m >> 32, 3);
1392 assert_eq!((tree_meta(0, 1, 1) >> 1) & 1, 1);
1394
1395 assert_eq!(tree_meta(0x1_0000_0000, 1, 0), (1 << 8) | 1);
1399 assert_eq!(
1400 tree_meta(0xFFFF_FFFF_FFFF_FFFF, 0, 0),
1401 0xFFFF_FFFF_0000_0000 | 1
1402 );
1403 assert_eq!(tree_meta(0x1_FFFF_FFFF, 1, 0), 0xFFFF_FFFF_0000_0101);
1404 }
1405
1406 #[test]
1407 fn predict_depth_exhaustion_yields_zero_on_internal() {
1408 let mut tree = SerializedTree::default();
1413 for i in 0..=MLFQ_TREE_MAX_DEPTH {
1414 tree.nodes.push(TreeNode {
1415 threshold: 0,
1416 left: (i + 1) as u32,
1417 right: (i + 1) as u32,
1418 feature: 0,
1419 pad: [0; 7],
1420 });
1421 }
1422 assert_eq!(predict(&tree, &TreeFeats::default()), 0);
1423 assert!(serialize_validate(&tree).is_err());
1426
1427 let mut tree = SerializedTree::default();
1430 for i in 0..MLFQ_TREE_MAX_DEPTH {
1431 tree.nodes.push(TreeNode {
1432 threshold: 0,
1433 left: (i + 1) as u32,
1434 right: (i + 1) as u32,
1435 feature: 0,
1436 pad: [0; 7],
1437 });
1438 }
1439 tree.nodes.push(TreeNode {
1440 threshold: 0,
1441 left: 777_777,
1442 right: 0,
1443 feature: 0,
1444 pad: [0; 7],
1445 });
1446 assert!(serialize_validate(&tree).is_ok());
1447 assert_eq!(predict(&tree, &TreeFeats::default()), 777_777);
1448 }
1449
1450 #[test]
1451 fn serialize_validate_rejects_deep_trees() {
1452 let mut tree = SerializedTree::default();
1456 for i in 0..=MLFQ_TREE_MAX_DEPTH {
1457 tree.nodes.push(TreeNode {
1458 threshold: 0,
1459 left: (i + 1) as u32,
1460 right: (i + 1) as u32,
1461 feature: 0,
1462 pad: [0; 7],
1463 });
1464 }
1465 tree.nodes.push(TreeNode {
1466 threshold: 0,
1467 left: 42,
1468 right: 0,
1469 feature: 0,
1470 pad: [0; 7],
1471 });
1472 assert!(serialize_validate(&tree).is_err());
1473 }
1474
1475 #[test]
1476 fn adversarial_constant_labels_never_split() {
1477 let samples: Vec<TreeSample> = (0..64u32)
1481 .map(|pid| sample(pid, 1_000_000, 123_456))
1482 .collect();
1483 let tree = fit(&samples, 12, 2, 2048, 0.0);
1484 assert_eq!(tree.nodes.len(), 1, "a constant label set yields one leaf");
1485 assert_eq!(tree.nodes[0].right, 0);
1486 let feats = samples[0].feats;
1487 assert_eq!(predict(&tree, &feats), 123_456);
1488 serialize_validate(&tree).unwrap();
1489 }
1490
1491 #[test]
1492 fn adversarial_max_nodes_exhaustion_mid_growth() {
1493 let mut samples = Vec::new();
1497 for (i, sleep) in (100_000u64..700_000).step_by(100_000).enumerate() {
1498 for k in 0..4 {
1499 let mut s = sample(i as u32 + k, sleep, sleep);
1500 s.feats.prev_burst_ns = 1_000;
1501 samples.push(s);
1502 }
1503 }
1504 let tree = fit(&samples, 12, 1, 3, 0.0);
1505 assert_eq!(
1506 tree.nodes.len(),
1507 3,
1508 "max_nodes 3 caps the growth at root + two leaves"
1509 );
1510 assert!(tree.nodes[0].right != 0);
1511 assert_eq!(tree.nodes[1].right, 0);
1512 assert_eq!(tree.nodes[2].right, 0);
1513 serialize_validate(&tree).unwrap();
1514 }
1515
1516 #[test]
1517 fn adversarial_extreme_labels_clamp() {
1518 let mut samples = Vec::new();
1522 for k in 0..4 {
1523 let mut a = sample(k, 100_000, u64::MAX);
1524 a.feats.prev_burst_ns = 1_000;
1525 let mut b = sample(k + 4, 200_000, 0);
1526 b.feats.prev_burst_ns = 1_000;
1527 samples.push(a);
1528 samples.push(b);
1529 }
1530 let tree = fit(&samples, 4, 2, 31, DEFAULT_MIN_REL_VAR_REDUCTION);
1531 serialize_validate(&tree).unwrap();
1532 assert_eq!(tree.nodes[0].feature, 1, "the root must split on sleep_ns");
1533 assert_eq!(
1534 predict(
1535 &tree,
1536 &TreeFeats {
1537 prev_burst_ns: 1_000,
1538 sleep_ns: 100_000,
1539 ..TreeFeats::default()
1540 }
1541 ),
1542 u32::MAX as u64,
1543 "the extreme class clamps to the u32 leaf field"
1544 );
1545 assert_eq!(
1546 predict(
1547 &tree,
1548 &TreeFeats {
1549 prev_burst_ns: 1_000,
1550 sleep_ns: 200_000,
1551 ..TreeFeats::default()
1552 }
1553 ),
1554 0
1555 );
1556 }
1557
1558 #[test]
1559 fn adversarial_min_leaf_boundary() {
1560 let class_a: Vec<TreeSample> = (0..3u32).map(|pid| sample(pid, 100_000, 100_000)).collect();
1564 let class_b: Vec<TreeSample> = (3..6u32).map(|pid| sample(pid, 200_000, 900_000)).collect();
1565 let mut samples = class_a.clone();
1566 samples.extend(class_b.iter().copied());
1567 let tree = fit(&samples, 4, 3, 31, 0.0);
1568 assert!(tree.nodes[0].right != 0, "n == 2*min_samples_leaf splits");
1569 serialize_validate(&tree).unwrap();
1570
1571 let samples = [class_a[0], class_a[1], class_b[0], class_b[1], class_b[2]];
1572 let tree = fit(&samples, 4, 3, 31, 0.0);
1573 assert_eq!(
1574 tree.nodes.len(),
1575 1,
1576 "n == 2*min_samples_leaf - 1 leaves one child below the cap: no split"
1577 );
1578 }
1579
1580 #[test]
1581 fn golden_tree_predict_matches_shared_spec() {
1582 let nodes = vec![
1591 TreeNode {
1592 threshold: 1_000_000,
1593 left: 1,
1594 right: 8,
1595 feature: 0,
1596 pad: [0; 7],
1597 },
1598 TreeNode {
1599 threshold: 1_000_000,
1600 left: 2,
1601 right: 8,
1602 feature: 0,
1603 pad: [0; 7],
1604 },
1605 TreeNode {
1606 threshold: 1_000_000,
1607 left: 3,
1608 right: 8,
1609 feature: 0,
1610 pad: [0; 7],
1611 },
1612 TreeNode {
1613 threshold: 500_000,
1614 left: 4,
1615 right: 5,
1616 feature: 1,
1617 pad: [0; 7],
1618 },
1619 TreeNode {
1620 threshold: 0,
1621 left: 1_111_111,
1622 right: 0,
1623 feature: 0,
1624 pad: [0; 7],
1625 },
1626 TreeNode {
1627 threshold: 1,
1628 left: 6,
1629 right: 7,
1630 feature: 0x84,
1631 pad: [0; 7],
1632 },
1633 TreeNode {
1634 threshold: 0,
1635 left: 2_222_222,
1636 right: 0,
1637 feature: 0,
1638 pad: [0; 7],
1639 },
1640 TreeNode {
1641 threshold: 0,
1642 left: 3_333_333,
1643 right: 0,
1644 feature: 0,
1645 pad: [0; 7],
1646 },
1647 TreeNode {
1648 threshold: 0,
1649 left: 8_888_888,
1650 right: 0,
1651 feature: 0,
1652 pad: [0; 7],
1653 },
1654 ];
1655 let tree = SerializedTree { nodes };
1656
1657 let f = |prev: u64, sleep: u64, wake: u32| TreeFeats {
1658 prev_burst_ns: prev,
1659 sleep_ns: sleep,
1660 wake_cnt: wake,
1661 ..TreeFeats::default()
1662 };
1663 assert_eq!(predict(&tree, &f(0, 400_000, 0)), 1_111_111);
1664 assert_eq!(predict(&tree, &f(0, 600_000, 0)), 2_222_222);
1665 assert_eq!(predict(&tree, &f(0, 600_000, 5)), 3_333_333);
1666 assert_eq!(predict(&tree, &f(2_000_000, 0, 0)), 8_888_888);
1667 assert_eq!(predict(&tree, &f(0, 400_000, 9)), 1_111_111);
1669 }
1670
1671 #[test]
1672 fn abi_layout_matches_bpf_structs() {
1673 use crate::bpf_intf::{mlfq_tree_feats, mlfq_tree_node, mlfq_tree_sample};
1679 use std::mem::{offset_of, size_of};
1680
1681 assert_eq!(size_of::<TreeFeats>(), size_of::<mlfq_tree_feats>());
1682 assert_eq!(size_of::<TreeNode>(), size_of::<mlfq_tree_node>());
1683 assert_eq!(size_of::<TreeSample>(), size_of::<mlfq_tree_sample>());
1684 assert_eq!(size_of::<TreeFeats>(), 64);
1685 assert_eq!(size_of::<TreeSample>(), 84);
1686 assert_eq!(size_of::<TreeNode>(), 24);
1687
1688 assert_eq!(
1689 offset_of!(TreeFeats, prev_burst_ns),
1690 offset_of!(mlfq_tree_feats, prev_burst_ns)
1691 );
1692 assert_eq!(
1693 offset_of!(TreeFeats, sleep_ns),
1694 offset_of!(mlfq_tree_feats, sleep_ns)
1695 );
1696 assert_eq!(offset_of!(TreeFeats, ema), offset_of!(mlfq_tree_feats, ema));
1697 assert_eq!(
1698 offset_of!(TreeFeats, io_wait),
1699 offset_of!(mlfq_tree_feats, io_wait)
1700 );
1701 assert_eq!(
1702 offset_of!(TreeFeats, wake_cnt),
1703 offset_of!(mlfq_tree_feats, wake_cnt)
1704 );
1705 assert_eq!(
1706 offset_of!(TreeFeats, wake_lat_us),
1707 offset_of!(mlfq_tree_feats, wake_lat_us)
1708 );
1709 assert_eq!(
1710 offset_of!(TreeFeats, queue_wait_us),
1711 offset_of!(mlfq_tree_feats, queue_wait_us)
1712 );
1713 assert_eq!(
1714 offset_of!(TreeFeats, sq_ema),
1715 offset_of!(mlfq_tree_feats, sq_ema)
1716 );
1717 assert_eq!(
1718 offset_of!(TreeFeats, sleep_var_ratio),
1719 offset_of!(mlfq_tree_feats, sleep_var_ratio)
1720 );
1721 assert_eq!(
1722 offset_of!(TreeFeats, gpu_submit),
1723 offset_of!(mlfq_tree_feats, gpu_submit)
1724 );
1725 assert_eq!(offset_of!(TreeFeats, wake_lat_us), 32);
1726 assert_eq!(offset_of!(TreeFeats, queue_wait_us), 36);
1727 assert_eq!(offset_of!(TreeFeats, sq_ema), 40);
1728 assert_eq!(offset_of!(TreeFeats, sleep_var_ratio), 48);
1729 assert_eq!(offset_of!(TreeFeats, pad), 52);
1730 assert_eq!(offset_of!(TreeFeats, gpu_submit), 56);
1731 assert_eq!(offset_of!(TreeFeats, pad2), 60);
1732
1733 assert_eq!(
1734 offset_of!(TreeNode, threshold),
1735 offset_of!(mlfq_tree_node, threshold)
1736 );
1737 assert_eq!(offset_of!(TreeNode, left), offset_of!(mlfq_tree_node, left));
1738 assert_eq!(
1739 offset_of!(TreeNode, right),
1740 offset_of!(mlfq_tree_node, right)
1741 );
1742 assert_eq!(
1743 offset_of!(TreeNode, feature),
1744 offset_of!(mlfq_tree_node, feature)
1745 );
1746 assert_eq!(offset_of!(TreeNode, feature), 16);
1747 assert_eq!(offset_of!(TreeNode, pad), offset_of!(mlfq_tree_node, pad));
1748 assert_eq!(offset_of!(TreeNode, pad), 17);
1749
1750 assert_eq!(
1751 offset_of!(TreeSample, pid),
1752 offset_of!(mlfq_tree_sample, pid)
1753 );
1754 assert_eq!(
1755 offset_of!(TreeSample, queue),
1756 offset_of!(mlfq_tree_sample, queue)
1757 );
1758 assert_eq!(
1759 offset_of!(TreeSample, feats),
1760 offset_of!(mlfq_tree_sample, feats)
1761 );
1762 assert_eq!(
1763 offset_of!(TreeSample, label_ns),
1764 offset_of!(mlfq_tree_sample, label_ns)
1765 );
1766 assert_eq!(
1767 offset_of!(TreeSample, version),
1768 offset_of!(mlfq_tree_sample, version)
1769 );
1770 assert_eq!(offset_of!(TreeSample, queue), 4);
1771 assert_eq!(offset_of!(TreeSample, feats), 8);
1772 assert_eq!(offset_of!(TreeSample, label_ns), 72);
1773 assert_eq!(offset_of!(TreeSample, version), 80);
1774 }
1775
1776 #[test]
1777 fn sample_version_tag_is_checked() {
1778 let cur = TreeSample {
1782 pid: 1,
1783 version: MLFQ_TREE_SAMPLE_VERSION,
1784 queue: 2,
1785 feats: TreeFeats::default(),
1786 label_ns: 0,
1787 };
1788 assert!(sample_version_matches(&cur));
1789
1790 let stale = TreeSample { version: 0, ..cur };
1791 assert!(!sample_version_matches(&stale));
1792 let future = TreeSample {
1793 version: MLFQ_TREE_SAMPLE_VERSION + 1,
1794 ..cur
1795 };
1796 assert!(!sample_version_matches(&future));
1797 }
1798
1799 #[test]
1800 fn recency_weights_pin_the_ends() {
1801 let n = 8;
1805 let half_life = n as f64 / 2.0;
1806 let w_newest = 2.0f64.powf(-1.0 / half_life);
1807 let w_oldest = 2.0f64.powf(-(n as f64) / half_life);
1808 assert!((w_oldest - 0.25).abs() < 1e-15);
1809 assert!((w_newest - 1.0).abs() < 0.2);
1810
1811 let ws = sample_weights(n);
1812 assert_eq!(ws.len(), n);
1813 assert!((ws[0] - w_oldest).abs() < 1e-15);
1814 assert!((ws[n - 1] - w_newest).abs() < 1e-15);
1815 for i in 0..n - 1 {
1817 assert!(ws[i] < ws[i + 1]);
1818 }
1819 }
1820}