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 { node.left as u64 } else { 0 }
788}
789
790pub fn should_publish(mae_tree: f64, mae_ema: f64, corr: f64, published_corr: Option<f64>) -> bool {
805 if mae_tree > mae_ema {
806 return false;
807 }
808 if corr < 0.30 {
809 return false;
810 }
811 if let Some(pc) = published_corr
812 && corr <= pc + 1e-9
813 {
814 return false;
815 }
816 true
817}
818
819pub fn weighted_holdout_mae(preds: &[u64], actuals: &[u64], weights: &[f64]) -> f64 {
824 assert_eq!(preds.len(), actuals.len());
825 assert_eq!(preds.len(), weights.len());
826 if preds.is_empty() {
827 return 0.0;
828 }
829 let mut sw = 0.0;
830 let mut sw_err = 0.0;
831 for ((p, a), w) in preds.iter().zip(actuals).zip(weights.iter()) {
832 let err = if p >= a { *p - *a } else { *a - *p } as f64;
833 sw += *w;
834 sw_err += *w * err;
835 }
836 if sw == 0.0 { 0.0 } else { sw_err / sw }
837}
838
839pub const MLFQ_TREE_SAMPLE_VERSION: u32 = crate::bpf_intf::mlfq_consts_MLFQ_TREE_SAMPLE_VERSION;
842
843pub fn sample_version_matches(s: &TreeSample) -> bool {
850 s.version == MLFQ_TREE_SAMPLE_VERSION
851}
852
853const MLFQ_TREE_META_ACTIVE_SHIFT: u32 = 1;
856
857pub fn tree_meta(generation: u64, nr_nodes: usize, active: u64) -> u64 {
866 ((generation & 0xFFFF_FFFF) << crate::bpf_intf::MLFQ_TREE_META_GENERATION_SHIFT)
875 | ((nr_nodes as u64) << crate::bpf_intf::MLFQ_TREE_META_NR_NODES_SHIFT)
876 | ((active & 1) << MLFQ_TREE_META_ACTIVE_SHIFT)
877 | crate::bpf_intf::MLFQ_TREE_META_TRAINED as u64
878}
879
880#[allow(dead_code)]
885pub fn mae(preds: &[u64], actuals: &[u64]) -> f64 {
886 assert_eq!(
887 preds.len(),
888 actuals.len(),
889 "prediction and actual vectors differ in length"
890 );
891 if preds.is_empty() {
892 return 0.0;
893 }
894 let total: u128 = preds
895 .iter()
896 .zip(actuals)
897 .map(|(p, a)| (if p >= a { *p - *a } else { *a - *p }) as u128)
898 .sum();
899 total as f64 / preds.len() as f64
900}
901
902pub fn pearson(x: &[u64], y: &[u64]) -> f64 {
909 assert_eq!(x.len(), y.len(), "correlation vectors differ in length");
910 let n = x.len();
911 if n == 0 {
912 return 0.0;
913 }
914
915 let mean_x = x.iter().map(|v| *v as f64).sum::<f64>() / n as f64;
916 let mean_y = y.iter().map(|v| *v as f64).sum::<f64>() / n as f64;
917
918 let (mut cov, mut var_x, mut var_y) = (0.0, 0.0, 0.0);
919 for (xi, yi) in x.iter().zip(y) {
920 let dx = *xi as f64 - mean_x;
921 let dy = *yi as f64 - mean_y;
922 cov += dx * dy;
923 var_x += dx * dx;
924 var_y += dy * dy;
925 }
926 if var_x == 0.0 || var_y == 0.0 {
927 return 0.0;
928 }
929 cov / (var_x * var_y).sqrt()
930}
931
932#[cfg(test)]
933mod tests {
934 use super::*;
935
936 struct Rng(u64);
938
939 impl Rng {
940 fn next_u64(&mut self) -> u64 {
941 let mut x = self.0;
942 x ^= x << 13;
943 x ^= x >> 7;
944 x ^= x << 17;
945 self.0 = x;
946 x.wrapping_mul(0x2545_F491_4F6C_DD1D)
947 }
948
949 fn next_in(&mut self, lo: u64, hi: u64) -> u64 {
950 lo + self.next_u64() % (hi - lo)
951 }
952 }
953
954 fn sample(pid: u32, sleep_ns: u64, label_ns: u64) -> TreeSample {
955 TreeSample {
956 pid,
957 version: MLFQ_TREE_SAMPLE_VERSION,
958 queue: 1,
959 feats: TreeFeats {
960 sleep_ns,
961 ..TreeFeats::default()
962 },
963 label_ns,
964 }
965 }
966
967 fn leaf_index(tree: &SerializedTree, feats: &TreeFeats) -> usize {
970 let feat: [u64; 16] = [
971 feats.prev_burst_ns,
972 feats.sleep_ns,
973 feats.ema,
974 feats.io_wait as u64,
975 feats.wake_cnt as u64,
976 feats.wake_lat_us as u64,
977 feats.queue_wait_us as u64,
978 feats.sq_ema,
979 feats.gpu_submit as u64,
980 feats.sleep_var_ratio as u64,
981 0,
982 0,
983 0,
984 0,
985 0,
986 0,
987 ];
988 let mask = MLFQ_TREE_MAX_NODES - 1;
989 let mut idx = 0usize;
990 for _ in 0..MLFQ_TREE_MAX_DEPTH {
991 if idx >= tree.nodes.len() {
992 return idx;
993 }
994 let node = &tree.nodes[idx];
995 if node.right == 0 {
996 return idx;
997 }
998 let feature = (node.feature & 0xF) as usize;
999 let next = if feat[feature] <= node.threshold {
1000 node.left as usize
1001 } else {
1002 node.right as usize
1003 };
1004 idx = next & mask;
1005 }
1006 idx
1007 }
1008
1009 #[test]
1010 fn separable_dataset_recovers_split() {
1011 let mut rng = Rng(0xC0FFEE);
1016 let mut samples = Vec::new();
1017 for pid in 0..512u32 {
1018 let interactive = pid % 2 == 0;
1019 let sleep = if interactive { 500_000 } else { 2_000_000 };
1020 let sleep = sleep + rng.next_in(0, 100_000);
1021 let label = if interactive { 100_000 } else { 10_000_000 };
1022 let label = label + rng.next_in(0, 10_000);
1023 samples.push(sample(pid, sleep, label));
1024 }
1025
1026 let tree = fit(&samples, 12, 2, 63, DEFAULT_MIN_REL_VAR_REDUCTION);
1027 serialize_validate(&tree).unwrap();
1028
1029 let root = &tree.nodes[0];
1030 assert!(root.right != 0, "the root must be an internal node");
1031 assert_eq!(root.feature, 1, "the root must split on sleep_ns");
1032 assert!(
1033 root.threshold > 1_000_000 && root.threshold < 1_500_000,
1034 "root threshold {} must sit between the class regions",
1035 root.threshold
1036 );
1037
1038 for s in &samples {
1040 let feats = s.feats;
1041 let pred = predict(&tree, &feats);
1042 if s.feats.sleep_ns < 1_000_000 {
1043 assert!(
1044 (90_000..=110_000).contains(&pred),
1045 "interactive sample predicted {pred}"
1046 );
1047 } else {
1048 assert!(
1049 (9_900_000..=10_100_000).contains(&pred),
1050 "CPU-bound sample predicted {pred}"
1051 );
1052 }
1053 }
1054 }
1055
1056 #[test]
1057 fn irrelevant_feature_never_split_on() {
1058 let mut rng = Rng(0xBEEF);
1063 let mut samples = Vec::new();
1064 for pid in 0..1024u32 {
1065 let interactive = pid % 2 == 0;
1066 let sleep = if interactive { 500_000 } else { 2_000_000 };
1067 let label = if interactive { 100_000 } else { 10_000_000 };
1068 let wake_cnt = rng.next_in(0, 1_000_000);
1069 let mut s = sample(pid, sleep, label);
1070 s.feats.wake_cnt = wake_cnt as u32;
1071 samples.push(s);
1072 }
1073
1074 let tree = fit(&samples, 4, 2, 127, DEFAULT_MIN_REL_VAR_REDUCTION);
1075 serialize_validate(&tree).unwrap();
1076
1077 for (i, node) in tree.nodes.iter().enumerate() {
1078 if node.right != 0 {
1079 assert_ne!(
1080 node.feature, 4,
1081 "node {i} must not split on the irrelevant wake_cnt"
1082 );
1083 }
1084 }
1085 }
1086
1087 #[test]
1088 fn depth_and_min_leaf_caps_respected() {
1089 let mut samples = Vec::new();
1095 for (i, sleep) in (100_000u64..1_700_000).step_by(100_000).enumerate() {
1096 for k in 0..4 {
1097 let mut s = sample(i as u32 + k, sleep, sleep);
1098 s.feats.prev_burst_ns = 50_000 + 10_000 * (i % 5) as u64;
1099 samples.push(s);
1100 }
1101 }
1102
1103 let max_depth = 3;
1104 let min_samples_leaf = 2;
1105 let tree = fit(&samples, max_depth, min_samples_leaf, 1000, 0.0);
1106 serialize_validate(&tree).unwrap();
1107
1108 let mut depth = vec![0usize; tree.nodes.len()];
1110 for (i, node) in tree.nodes.iter().enumerate() {
1111 if node.right != 0 {
1112 assert!(
1113 depth[i] < max_depth,
1114 "node {i} at depth {} exceeds the cap {max_depth}",
1115 depth[i]
1116 );
1117 depth[node.left as usize] = depth[i] + 1;
1118 depth[node.right as usize] = depth[i] + 1;
1119 }
1120 }
1121
1122 let mut leaf_counts = std::collections::HashMap::new();
1124 for s in &samples {
1125 let feats = s.feats;
1126 *leaf_counts
1127 .entry(leaf_index(&tree, &feats))
1128 .or_insert(0usize) += 1;
1129 }
1130 assert!(!leaf_counts.is_empty(), "the tree must have leaves");
1131 for (leaf, count) in &leaf_counts {
1132 assert!(
1133 *count >= min_samples_leaf,
1134 "leaf {leaf} holds {count} samples, below the cap {min_samples_leaf}"
1135 );
1136 }
1137 }
1138
1139 #[test]
1140 fn serialize_walk_roundtrip() {
1141 let mut rng = Rng(0xABCDEF);
1146 let mut samples = Vec::new();
1147 for pid in 0..1024u32 {
1148 let interactive = pid % 3 == 0;
1149 let sleep = if interactive { 400_000 } else { 2_500_000 };
1150 let sleep = sleep + rng.next_in(0, 50_000);
1151 let label = if interactive { 80_000 } else { 12_000_000 };
1152 let label = label + rng.next_in(0, 200_000);
1153 samples.push(sample(pid, sleep, label));
1154 }
1155
1156 let tree = fit(&samples, 8, 4, 511, DEFAULT_MIN_REL_VAR_REDUCTION);
1157 serialize_validate(&tree).unwrap();
1158
1159 let n = samples.len();
1160 let half_life = n as f64 / 2.0;
1161 let weight_of = |i: usize| 2.0f64.powf(-((n - i) as f64) / half_life);
1162
1163 for s in &samples {
1164 let feats = s.feats; let leaf = leaf_index(&tree, &feats);
1166 let mut wsum = 0.0f64;
1167 let mut wsum_y = 0.0f64;
1168 for (j, t) in samples.iter().enumerate() {
1169 let t_feats = t.feats;
1170 if leaf_index(&tree, &t_feats) == leaf {
1171 wsum += weight_of(j);
1172 wsum_y += weight_of(j) * t.label_ns as f64;
1173 }
1174 }
1175 let expected = leaf_prediction((wsum_y / wsum) as u64) as u64;
1176 let pid = s.pid;
1177 assert_eq!(
1178 predict(&tree, &feats),
1179 expected,
1180 "round-trip prediction for pid {} via leaf {leaf}",
1181 pid
1182 );
1183 }
1184 }
1185
1186 #[test]
1187 fn holdout_mae_beats_constant_mean() {
1188 let mut rng = Rng(0x1234_5678);
1191 let mut samples = Vec::new();
1192 for pid in 0..1200u32 {
1193 let interactive = pid % 2 == 0;
1194 let sleep = if interactive { 300_000 } else { 3_000_000 };
1195 let sleep = sleep + rng.next_in(0, 100_000);
1196 let label = if interactive { 120_000 } else { 15_000_000 };
1197 let label = label + rng.next_in(0, 500_000);
1198 samples.push(sample(pid, sleep, label));
1199 }
1200
1201 let split = 2 * samples.len() / 3;
1202 let (train, test) = samples.split_at(split);
1203 let tree = fit(train, 8, 4, 511, DEFAULT_MIN_REL_VAR_REDUCTION);
1204 serialize_validate(&tree).unwrap();
1205
1206 let actuals: Vec<u64> = test.iter().map(|s| s.label_ns).collect();
1207 let preds: Vec<u64> = test
1208 .iter()
1209 .map(|s| {
1210 let feats = s.feats;
1211 predict(&tree, &feats)
1212 })
1213 .collect();
1214 let mae_tree = mae(&preds, &actuals);
1215
1216 let mean_label = train.iter().map(|s| s.label_ns as f64).sum::<f64>() / train.len() as f64;
1217 let const_preds = vec![mean_label as u64; test.len()];
1218 let mae_const = mae(&const_preds, &actuals);
1219
1220 assert!(
1221 mae_tree < mae_const,
1222 "tree MAE {mae_tree} must beat the constant-mean MAE {mae_const}"
1223 );
1224 }
1225
1226 #[test]
1227 fn empty_input_errors() {
1228 let tree = fit(&[], 4, 2, 63, DEFAULT_MIN_REL_VAR_REDUCTION);
1229 assert!(tree.nodes.is_empty(), "an empty fit yields no nodes");
1230 assert!(
1231 serialize_validate(&tree).is_err(),
1232 "an empty tree must not validate"
1233 );
1234 assert_eq!(predict(&tree, &TreeFeats::default()), 0);
1235
1236 let s = sample(0, 1_000, 1_000);
1238 let tree = fit(&[s], 4, 2, 0, DEFAULT_MIN_REL_VAR_REDUCTION);
1239 assert!(tree.nodes.is_empty());
1240 assert!(serialize_validate(&tree).is_err());
1241
1242 assert_eq!(mae(&[], &[]), 0.0);
1244 assert_eq!(pearson(&[], &[]), 0.0);
1245 }
1246
1247 #[test]
1248 fn threshold_roundtrip_u64_precision() {
1249 let lo = u64::MAX - 1_000_000;
1253 let hi = u64::MAX - 100_000;
1254 let mut samples = Vec::new();
1255 for k in 0..4 {
1256 let mut a = sample(k, lo, 100_000);
1257 a.feats.prev_burst_ns = 1_000; let mut b = sample(k + 4, hi, 10_000_000);
1259 b.feats.prev_burst_ns = 1_000;
1260 samples.push(a);
1261 samples.push(b);
1262 }
1263
1264 let tree = fit(&samples, 4, 2, 31, DEFAULT_MIN_REL_VAR_REDUCTION);
1265 serialize_validate(&tree).unwrap();
1266
1267 let root = &tree.nodes[0];
1268 assert_eq!(root.feature, 1, "the root must split on sleep_ns");
1269 assert_eq!(root.threshold, lo + (hi - lo) / 2);
1270
1271 for s in &samples {
1272 let feats = s.feats;
1273 let pid = s.pid;
1274 let pred = predict(&tree, &feats);
1275 let expected = if s.feats.sleep_ns == lo {
1276 100_000
1277 } else {
1278 10_000_000
1279 };
1280 assert_eq!(pred, expected, "pid {}", pid);
1281 }
1282
1283 let samples = [sample(0, 100, 1_000), sample(1, 101, 9_000)];
1286 let tree = fit(&samples, 4, 1, 31, DEFAULT_MIN_REL_VAR_REDUCTION);
1287 serialize_validate(&tree).unwrap();
1288 assert_eq!(tree.nodes[0].threshold, 100);
1289 let f0 = samples[0].feats;
1290 let f1 = samples[1].feats;
1291 assert_eq!(predict(&tree, &f0), 1_000);
1292 assert_eq!(predict(&tree, &f1), 9_000);
1293 }
1294
1295 #[test]
1296 fn validate_rejects_broken_trees() {
1297 let samples = [sample(0, 100, 1_000), sample(1, 101, 9_000)];
1299 let fit_ok = |min_rel: f64| fit(&samples, 4, 1, 31, min_rel);
1300 assert!(serialize_validate(&fit_ok(0.0)).is_ok());
1301
1302 let mut bad = fit_ok(0.0);
1304 bad.nodes[0].feature = 9;
1305 assert!(serialize_validate(&bad).is_err());
1306
1307 let mut bad = fit_ok(0.0);
1309 bad.nodes[0].right = MLFQ_TREE_MAX_NODES as u32;
1310 assert!(serialize_validate(&bad).is_err());
1311
1312 let mut bad = fit_ok(0.0);
1314 bad.nodes[0].left = 0;
1315 assert!(serialize_validate(&bad).is_err());
1316
1317 let mut samples = Vec::new();
1319 for pid in 0..64u32 {
1320 let sleep = if pid % 2 == 0 { 400_000 } else { 2_500_000 };
1321 let label = if pid % 2 == 0 { 80_000 } else { 12_000_000 };
1322 samples.push(sample(pid, sleep, label));
1323 }
1324 let tree = fit(&samples, 4, 2, 63, DEFAULT_MIN_REL_VAR_REDUCTION);
1325 assert!(serialize_validate(&tree).is_ok());
1326 }
1327
1328 #[test]
1329 fn metrics_on_known_vectors() {
1330 assert_eq!(mae(&[10, 20], &[12, 18]), 2.0);
1331 assert_eq!(mae(&[1_000], &[2_000]), 1_000.0);
1332
1333 let x = [1, 2, 3, 4];
1336 let y = [10, 20, 30, 40];
1337 assert!((pearson(&x, &y) - 1.0).abs() < 1e-12);
1338 assert!((pearson(&x, &[40, 30, 20, 10]) + 1.0).abs() < 1e-12);
1339 assert!(pearson(&x, &[1, 4, 4, 1]).abs() < 1e-12);
1340
1341 assert_eq!(pearson(&x, &[5, 5, 5, 5]), 0.0);
1343 }
1344
1345 #[test]
1346 fn publish_gate_and_tree_meta() {
1347 assert!(should_publish(100.0, 100.0, 0.5, None));
1352 assert!(should_publish(99.0, 100.0, 0.5, None));
1353 assert!(!should_publish(101.0, 100.0, 0.5, None));
1354 assert!(should_publish(0.0, 0.0, 0.5, None));
1355 assert!(!should_publish(90.0, 100.0, 0.29, None));
1356 assert!(should_publish(90.0, 100.0, 0.30, None));
1357 assert!(!should_publish(90.0, 100.0, 0.0, None));
1358 assert!(!should_publish(90.0, 100.0, 0.30, Some(0.30)));
1359 assert!(should_publish(90.0, 100.0, 0.31, Some(0.30)));
1360 assert!(!should_publish(90.0, 100.0, 0.50, Some(0.50)));
1361 assert!(should_publish(90.0, 100.0, 0.52, Some(0.50)));
1362 assert!(should_publish(90.0, 100.0, 0.72, Some(0.52)));
1363 let preds = [100, 200];
1365 let actuals = [110, 190];
1366 let w = [0.25, 1.0];
1367 let wm = weighted_holdout_mae(&preds, &actuals, &w);
1368 assert!((wm - 10.0).abs() < 1e-9);
1369
1370 assert_eq!(tree_meta(0, 1, 0), (1 << 8) | 1);
1374 assert_eq!(tree_meta(0, 2048, 1), (2048 << 8) | (1 << 1) | 1);
1375 let m = tree_meta(3, 7, 0);
1376 assert_eq!(m, (3u64 << 32) | (7 << 8) | 1);
1377 assert_eq!(
1378 m & crate::bpf_intf::MLFQ_TREE_META_TRAINED as u64,
1379 crate::bpf_intf::MLFQ_TREE_META_TRAINED as u64
1380 );
1381 assert_eq!((m >> 1) & 1, 0);
1382 assert_eq!((m >> 8) & 0xFFFFFF, 7);
1383 assert_eq!(m >> 32, 3);
1384 assert_eq!((tree_meta(0, 1, 1) >> 1) & 1, 1);
1386
1387 assert_eq!(tree_meta(0x1_0000_0000, 1, 0), (1 << 8) | 1);
1391 assert_eq!(
1392 tree_meta(0xFFFF_FFFF_FFFF_FFFF, 0, 0),
1393 0xFFFF_FFFF_0000_0000 | 1
1394 );
1395 assert_eq!(tree_meta(0x1_FFFF_FFFF, 1, 0), 0xFFFF_FFFF_0000_0101);
1396 }
1397
1398 #[test]
1399 fn predict_depth_exhaustion_yields_zero_on_internal() {
1400 let mut tree = SerializedTree::default();
1405 for i in 0..=MLFQ_TREE_MAX_DEPTH {
1406 tree.nodes.push(TreeNode {
1407 threshold: 0,
1408 left: (i + 1) as u32,
1409 right: (i + 1) as u32,
1410 feature: 0,
1411 pad: [0; 7],
1412 });
1413 }
1414 assert_eq!(predict(&tree, &TreeFeats::default()), 0);
1415 assert!(serialize_validate(&tree).is_err());
1418
1419 let mut tree = SerializedTree::default();
1422 for i in 0..MLFQ_TREE_MAX_DEPTH {
1423 tree.nodes.push(TreeNode {
1424 threshold: 0,
1425 left: (i + 1) as u32,
1426 right: (i + 1) as u32,
1427 feature: 0,
1428 pad: [0; 7],
1429 });
1430 }
1431 tree.nodes.push(TreeNode {
1432 threshold: 0,
1433 left: 777_777,
1434 right: 0,
1435 feature: 0,
1436 pad: [0; 7],
1437 });
1438 assert!(serialize_validate(&tree).is_ok());
1439 assert_eq!(predict(&tree, &TreeFeats::default()), 777_777);
1440 }
1441
1442 #[test]
1443 fn serialize_validate_rejects_deep_trees() {
1444 let mut tree = SerializedTree::default();
1448 for i in 0..=MLFQ_TREE_MAX_DEPTH {
1449 tree.nodes.push(TreeNode {
1450 threshold: 0,
1451 left: (i + 1) as u32,
1452 right: (i + 1) as u32,
1453 feature: 0,
1454 pad: [0; 7],
1455 });
1456 }
1457 tree.nodes.push(TreeNode {
1458 threshold: 0,
1459 left: 42,
1460 right: 0,
1461 feature: 0,
1462 pad: [0; 7],
1463 });
1464 assert!(serialize_validate(&tree).is_err());
1465 }
1466
1467 #[test]
1468 fn adversarial_constant_labels_never_split() {
1469 let samples: Vec<TreeSample> = (0..64u32)
1473 .map(|pid| sample(pid, 1_000_000, 123_456))
1474 .collect();
1475 let tree = fit(&samples, 12, 2, 2048, 0.0);
1476 assert_eq!(tree.nodes.len(), 1, "a constant label set yields one leaf");
1477 assert_eq!(tree.nodes[0].right, 0);
1478 let feats = samples[0].feats;
1479 assert_eq!(predict(&tree, &feats), 123_456);
1480 serialize_validate(&tree).unwrap();
1481 }
1482
1483 #[test]
1484 fn adversarial_max_nodes_exhaustion_mid_growth() {
1485 let mut samples = Vec::new();
1489 for (i, sleep) in (100_000u64..700_000).step_by(100_000).enumerate() {
1490 for k in 0..4 {
1491 let mut s = sample(i as u32 + k, sleep, sleep);
1492 s.feats.prev_burst_ns = 1_000;
1493 samples.push(s);
1494 }
1495 }
1496 let tree = fit(&samples, 12, 1, 3, 0.0);
1497 assert_eq!(
1498 tree.nodes.len(),
1499 3,
1500 "max_nodes 3 caps the growth at root + two leaves"
1501 );
1502 assert!(tree.nodes[0].right != 0);
1503 assert_eq!(tree.nodes[1].right, 0);
1504 assert_eq!(tree.nodes[2].right, 0);
1505 serialize_validate(&tree).unwrap();
1506 }
1507
1508 #[test]
1509 fn adversarial_extreme_labels_clamp() {
1510 let mut samples = Vec::new();
1514 for k in 0..4 {
1515 let mut a = sample(k, 100_000, u64::MAX);
1516 a.feats.prev_burst_ns = 1_000;
1517 let mut b = sample(k + 4, 200_000, 0);
1518 b.feats.prev_burst_ns = 1_000;
1519 samples.push(a);
1520 samples.push(b);
1521 }
1522 let tree = fit(&samples, 4, 2, 31, DEFAULT_MIN_REL_VAR_REDUCTION);
1523 serialize_validate(&tree).unwrap();
1524 assert_eq!(tree.nodes[0].feature, 1, "the root must split on sleep_ns");
1525 assert_eq!(
1526 predict(
1527 &tree,
1528 &TreeFeats {
1529 prev_burst_ns: 1_000,
1530 sleep_ns: 100_000,
1531 ..TreeFeats::default()
1532 }
1533 ),
1534 u32::MAX as u64,
1535 "the extreme class clamps to the u32 leaf field"
1536 );
1537 assert_eq!(
1538 predict(
1539 &tree,
1540 &TreeFeats {
1541 prev_burst_ns: 1_000,
1542 sleep_ns: 200_000,
1543 ..TreeFeats::default()
1544 }
1545 ),
1546 0
1547 );
1548 }
1549
1550 #[test]
1551 fn adversarial_min_leaf_boundary() {
1552 let class_a: Vec<TreeSample> = (0..3u32).map(|pid| sample(pid, 100_000, 100_000)).collect();
1556 let class_b: Vec<TreeSample> = (3..6u32).map(|pid| sample(pid, 200_000, 900_000)).collect();
1557 let mut samples = class_a.clone();
1558 samples.extend(class_b.iter().copied());
1559 let tree = fit(&samples, 4, 3, 31, 0.0);
1560 assert!(tree.nodes[0].right != 0, "n == 2*min_samples_leaf splits");
1561 serialize_validate(&tree).unwrap();
1562
1563 let samples = [class_a[0], class_a[1], class_b[0], class_b[1], class_b[2]];
1564 let tree = fit(&samples, 4, 3, 31, 0.0);
1565 assert_eq!(
1566 tree.nodes.len(),
1567 1,
1568 "n == 2*min_samples_leaf - 1 leaves one child below the cap: no split"
1569 );
1570 }
1571
1572 #[test]
1573 fn golden_tree_predict_matches_shared_spec() {
1574 let nodes = vec![
1583 TreeNode {
1584 threshold: 1_000_000,
1585 left: 1,
1586 right: 8,
1587 feature: 0,
1588 pad: [0; 7],
1589 },
1590 TreeNode {
1591 threshold: 1_000_000,
1592 left: 2,
1593 right: 8,
1594 feature: 0,
1595 pad: [0; 7],
1596 },
1597 TreeNode {
1598 threshold: 1_000_000,
1599 left: 3,
1600 right: 8,
1601 feature: 0,
1602 pad: [0; 7],
1603 },
1604 TreeNode {
1605 threshold: 500_000,
1606 left: 4,
1607 right: 5,
1608 feature: 1,
1609 pad: [0; 7],
1610 },
1611 TreeNode {
1612 threshold: 0,
1613 left: 1_111_111,
1614 right: 0,
1615 feature: 0,
1616 pad: [0; 7],
1617 },
1618 TreeNode {
1619 threshold: 1,
1620 left: 6,
1621 right: 7,
1622 feature: 0x84,
1623 pad: [0; 7],
1624 },
1625 TreeNode {
1626 threshold: 0,
1627 left: 2_222_222,
1628 right: 0,
1629 feature: 0,
1630 pad: [0; 7],
1631 },
1632 TreeNode {
1633 threshold: 0,
1634 left: 3_333_333,
1635 right: 0,
1636 feature: 0,
1637 pad: [0; 7],
1638 },
1639 TreeNode {
1640 threshold: 0,
1641 left: 8_888_888,
1642 right: 0,
1643 feature: 0,
1644 pad: [0; 7],
1645 },
1646 ];
1647 let tree = SerializedTree { nodes };
1648
1649 let f = |prev: u64, sleep: u64, wake: u32| TreeFeats {
1650 prev_burst_ns: prev,
1651 sleep_ns: sleep,
1652 wake_cnt: wake,
1653 ..TreeFeats::default()
1654 };
1655 assert_eq!(predict(&tree, &f(0, 400_000, 0)), 1_111_111);
1656 assert_eq!(predict(&tree, &f(0, 600_000, 0)), 2_222_222);
1657 assert_eq!(predict(&tree, &f(0, 600_000, 5)), 3_333_333);
1658 assert_eq!(predict(&tree, &f(2_000_000, 0, 0)), 8_888_888);
1659 assert_eq!(predict(&tree, &f(0, 400_000, 9)), 1_111_111);
1661 }
1662
1663 #[test]
1664 fn abi_layout_matches_bpf_structs() {
1665 use crate::bpf_intf::{mlfq_tree_feats, mlfq_tree_node, mlfq_tree_sample};
1671 use std::mem::{offset_of, size_of};
1672
1673 assert_eq!(size_of::<TreeFeats>(), size_of::<mlfq_tree_feats>());
1674 assert_eq!(size_of::<TreeNode>(), size_of::<mlfq_tree_node>());
1675 assert_eq!(size_of::<TreeSample>(), size_of::<mlfq_tree_sample>());
1676 assert_eq!(size_of::<TreeFeats>(), 64);
1677 assert_eq!(size_of::<TreeSample>(), 84);
1678 assert_eq!(size_of::<TreeNode>(), 24);
1679
1680 assert_eq!(
1681 offset_of!(TreeFeats, prev_burst_ns),
1682 offset_of!(mlfq_tree_feats, prev_burst_ns)
1683 );
1684 assert_eq!(
1685 offset_of!(TreeFeats, sleep_ns),
1686 offset_of!(mlfq_tree_feats, sleep_ns)
1687 );
1688 assert_eq!(offset_of!(TreeFeats, ema), offset_of!(mlfq_tree_feats, ema));
1689 assert_eq!(
1690 offset_of!(TreeFeats, io_wait),
1691 offset_of!(mlfq_tree_feats, io_wait)
1692 );
1693 assert_eq!(
1694 offset_of!(TreeFeats, wake_cnt),
1695 offset_of!(mlfq_tree_feats, wake_cnt)
1696 );
1697 assert_eq!(
1698 offset_of!(TreeFeats, wake_lat_us),
1699 offset_of!(mlfq_tree_feats, wake_lat_us)
1700 );
1701 assert_eq!(
1702 offset_of!(TreeFeats, queue_wait_us),
1703 offset_of!(mlfq_tree_feats, queue_wait_us)
1704 );
1705 assert_eq!(
1706 offset_of!(TreeFeats, sq_ema),
1707 offset_of!(mlfq_tree_feats, sq_ema)
1708 );
1709 assert_eq!(
1710 offset_of!(TreeFeats, sleep_var_ratio),
1711 offset_of!(mlfq_tree_feats, sleep_var_ratio)
1712 );
1713 assert_eq!(
1714 offset_of!(TreeFeats, gpu_submit),
1715 offset_of!(mlfq_tree_feats, gpu_submit)
1716 );
1717 assert_eq!(offset_of!(TreeFeats, wake_lat_us), 32);
1718 assert_eq!(offset_of!(TreeFeats, queue_wait_us), 36);
1719 assert_eq!(offset_of!(TreeFeats, sq_ema), 40);
1720 assert_eq!(offset_of!(TreeFeats, sleep_var_ratio), 48);
1721 assert_eq!(offset_of!(TreeFeats, pad), 52);
1722 assert_eq!(offset_of!(TreeFeats, gpu_submit), 56);
1723 assert_eq!(offset_of!(TreeFeats, pad2), 60);
1724
1725 assert_eq!(
1726 offset_of!(TreeNode, threshold),
1727 offset_of!(mlfq_tree_node, threshold)
1728 );
1729 assert_eq!(offset_of!(TreeNode, left), offset_of!(mlfq_tree_node, left));
1730 assert_eq!(
1731 offset_of!(TreeNode, right),
1732 offset_of!(mlfq_tree_node, right)
1733 );
1734 assert_eq!(
1735 offset_of!(TreeNode, feature),
1736 offset_of!(mlfq_tree_node, feature)
1737 );
1738 assert_eq!(offset_of!(TreeNode, feature), 16);
1739 assert_eq!(offset_of!(TreeNode, pad), offset_of!(mlfq_tree_node, pad));
1740 assert_eq!(offset_of!(TreeNode, pad), 17);
1741
1742 assert_eq!(
1743 offset_of!(TreeSample, pid),
1744 offset_of!(mlfq_tree_sample, pid)
1745 );
1746 assert_eq!(
1747 offset_of!(TreeSample, queue),
1748 offset_of!(mlfq_tree_sample, queue)
1749 );
1750 assert_eq!(
1751 offset_of!(TreeSample, feats),
1752 offset_of!(mlfq_tree_sample, feats)
1753 );
1754 assert_eq!(
1755 offset_of!(TreeSample, label_ns),
1756 offset_of!(mlfq_tree_sample, label_ns)
1757 );
1758 assert_eq!(
1759 offset_of!(TreeSample, version),
1760 offset_of!(mlfq_tree_sample, version)
1761 );
1762 assert_eq!(offset_of!(TreeSample, queue), 4);
1763 assert_eq!(offset_of!(TreeSample, feats), 8);
1764 assert_eq!(offset_of!(TreeSample, label_ns), 72);
1765 assert_eq!(offset_of!(TreeSample, version), 80);
1766 }
1767
1768 #[test]
1769 fn sample_version_tag_is_checked() {
1770 let cur = TreeSample {
1774 pid: 1,
1775 version: MLFQ_TREE_SAMPLE_VERSION,
1776 queue: 2,
1777 feats: TreeFeats::default(),
1778 label_ns: 0,
1779 };
1780 assert!(sample_version_matches(&cur));
1781
1782 let stale = TreeSample { version: 0, ..cur };
1783 assert!(!sample_version_matches(&stale));
1784 let future = TreeSample {
1785 version: MLFQ_TREE_SAMPLE_VERSION + 1,
1786 ..cur
1787 };
1788 assert!(!sample_version_matches(&future));
1789 }
1790
1791 #[test]
1792 fn recency_weights_pin_the_ends() {
1793 let n = 8;
1797 let half_life = n as f64 / 2.0;
1798 let w_newest = 2.0f64.powf(-1.0 / half_life);
1799 let w_oldest = 2.0f64.powf(-(n as f64) / half_life);
1800 assert!((w_oldest - 0.25).abs() < 1e-15);
1801 assert!((w_newest - 1.0).abs() < 0.2);
1802
1803 let ws = sample_weights(n);
1804 assert_eq!(ws.len(), n);
1805 assert!((ws[0] - w_oldest).abs() < 1e-15);
1806 assert!((ws[n - 1] - w_newest).abs() < 1e-15);
1807 for i in 0..n - 1 {
1809 assert!(ws[i] < ws[i + 1]);
1810 }
1811 }
1812}