1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
13use std::fs::DirEntry;
14use std::os::unix::fs::MetadataExt;
15use std::os::unix::io::{AsFd, BorrowedFd};
16use std::path::{Path, PathBuf};
17use std::sync::atomic::{AtomicBool, Ordering};
18
19use anyhow::{Context, Result, bail};
20use inotify::{Inotify, WatchMask};
21use scx_utils::Cpumask;
22use tracing::{debug, info};
23
24fn cgroup_root_relative(path: &Path) -> String {
27 path.strip_prefix("/sys/fs/cgroup")
28 .map(|rel| format!("/{}", rel.to_string_lossy()))
29 .unwrap_or_else(|_| path.to_string_lossy().into_owned())
30}
31
32#[derive(Debug)]
34pub struct CellInfo {
35 pub cell_id: u32,
36 pub cgroup_path: Option<PathBuf>,
37 pub cgid: Option<u64>,
38 pub cpuset: Option<Cpumask>,
40}
41
42#[derive(Debug, Clone, Copy, Default)]
49struct CpuMinimum {
50 protected: usize,
51 requested: usize,
52}
53
54#[derive(Debug, Clone)]
61struct CpuRecipient {
62 id: u32,
63 weight: f64,
64 allowed: Cpumask,
65 claimed: Option<Cpumask>,
66 minimum: CpuMinimum,
67}
68
69#[derive(Debug)]
71pub struct CpuAssignment {
72 pub id: u32,
73 pub primary: Cpumask,
74 pub borrowable: Option<Cpumask>,
75}
76
77struct CpuManager<'a> {
79 domain: &'a Cpumask,
80 cpu_to_partition: Option<&'a HashMap<usize, usize>>,
83}
84
85impl<'a> CpuManager<'a> {
86 #[cfg(test)]
87 fn new(domain: &'a Cpumask) -> Self {
88 Self {
89 domain,
90 cpu_to_partition: None,
91 }
92 }
93
94 fn with_partitions(domain: &'a Cpumask, cpu_to_partition: &'a HashMap<usize, usize>) -> Self {
95 Self {
96 domain,
97 cpu_to_partition: Some(cpu_to_partition),
98 }
99 }
100
101 fn compute_targets(
106 total_cpus: usize,
107 recipients: &[(u32, f64)],
108 ) -> Result<HashMap<u32, usize>> {
109 if recipients.is_empty() {
110 bail!("compute_targets called with no cells");
111 }
112 if total_cpus < recipients.len() {
113 bail!(
114 "Not enough CPUs ({}) for {} cells (need at least 1 each)",
115 total_cpus,
116 recipients.len()
117 );
118 }
119
120 let total_weight: f64 = recipients.iter().map(|(_, w)| w).sum();
121 let num_recipients = recipients.len();
122
123 if total_weight <= 0.0 {
124 let per = total_cpus / num_recipients;
126 let remainder = total_cpus % num_recipients;
127 return Ok(recipients
128 .iter()
129 .enumerate()
130 .map(|(i, (id, _))| {
131 let extra = if i < remainder { 1 } else { 0 };
132 (*id, per + extra)
133 })
134 .collect());
135 }
136
137 let distributable = total_cpus - num_recipients;
138 let mut assigned = num_recipients;
139 let mut raw: Vec<(u32, f64, usize)> = recipients
140 .iter()
141 .map(|(id, weight)| {
142 let frac = weight / total_weight * distributable as f64;
143 let floored = frac.floor() as usize;
144 assigned += floored;
145 (*id, frac - frac.floor(), 1 + floored)
146 })
147 .collect();
148
149 let mut remainder = total_cpus - assigned;
150 raw.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
151 for entry in raw.iter_mut() {
152 if remainder == 0 {
153 break;
154 }
155 entry.2 += 1;
156 remainder -= 1;
157 }
158
159 Ok(raw.iter().map(|(id, _, count)| (*id, *count)).collect())
160 }
161
162 fn distribute_cpus_proportional(
169 cpus: &[usize],
170 recipients: &[(u32, f64)],
171 ) -> Result<HashMap<u32, Vec<usize>>> {
172 if cpus.is_empty() {
173 bail!("distribute_cpus_proportional called with no CPUs");
174 }
175 if recipients.is_empty() {
176 bail!("distribute_cpus_proportional called with no recipients");
177 }
178
179 let total_weight: f64 = recipients.iter().map(|(_, w)| w).sum();
180 let n = cpus.len();
181
182 let mut allocs: Vec<(u32, usize)> = if total_weight <= 0.0 {
183 let per = n / recipients.len();
185 let remainder = n % recipients.len();
186 recipients
187 .iter()
188 .enumerate()
189 .map(|(i, (id, _))| {
190 let extra = if i < remainder { 1 } else { 0 };
191 (*id, per + extra)
192 })
193 .collect()
194 } else {
195 let mut assigned = 0usize;
197 let mut raw: Vec<(u32, f64, usize, bool)> = recipients
198 .iter()
199 .map(|(id, weight)| {
200 let frac = weight / total_weight * n as f64;
201 let floored = frac.floor() as usize;
202 assigned += floored;
203 (*id, frac - frac.floor(), floored, *weight > 0.0)
204 })
205 .collect();
206
207 let mut remainder = n - assigned;
208 raw.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
209 for entry in raw.iter_mut() {
210 if remainder == 0 {
211 break;
212 }
213 entry.2 += 1;
214 remainder -= 1;
215 }
216
217 loop {
222 let Some(starved_idx) = raw
223 .iter()
224 .position(|(_, _, count, pos_weight)| *pos_weight && *count == 0)
225 else {
226 break;
227 };
228 let Some(donor_idx) = raw
229 .iter()
230 .enumerate()
231 .filter(|(_, (_, _, count, _))| *count > 1)
232 .max_by_key(|(_, (_, _, count, _))| *count)
233 .map(|(i, _)| i)
234 else {
235 break; };
237 raw[donor_idx].2 -= 1;
238 raw[starved_idx].2 += 1;
239 }
240
241 raw.iter().map(|(id, _, count, _)| (*id, *count)).collect()
242 };
243
244 let mut cpu_iter = cpus.iter().copied();
246 allocs.sort_by_key(|(id, _)| *id);
247 let mut result: HashMap<u32, Vec<usize>> = HashMap::new();
248 for (id, count) in allocs {
249 let cpus_for_recipient: Vec<usize> = cpu_iter.by_ref().take(count).collect();
250 if cpus_for_recipient.len() != count {
251 bail!(
252 "BUG: distribute_cpus_proportional: cell {} expected {} CPUs but got {}",
253 id,
254 count,
255 cpus_for_recipient.len()
256 );
257 }
258 if !cpus_for_recipient.is_empty() {
259 result.insert(id, cpus_for_recipient);
260 }
261 }
262
263 Ok(result)
264 }
265
266 fn reserve_requested_minimums(
273 &self,
274 recipients: &[CpuRecipient],
275 contention: &HashMap<usize, Vec<u32>>,
276 ) -> Result<HashMap<u32, Cpumask>> {
277 let by_id: HashMap<u32, &CpuRecipient> = recipients
278 .iter()
279 .map(|recipient| (recipient.id, recipient))
280 .collect();
281 let mut assignments = HashMap::new();
282 let mut reserved = Cpumask::new();
283
284 let mut requesters: Vec<&CpuRecipient> = recipients
285 .iter()
286 .filter(|recipient| recipient.minimum.requested > 0)
287 .collect();
288 requesters.sort_by_key(|recipient| recipient.id);
289
290 let mut partition_claimants: HashMap<usize, HashSet<u32>> = HashMap::new();
291 if let Some(cpu_to_partition) = self.cpu_to_partition {
292 for (cpu, claimants) in contention {
293 if let Some(&partition) = cpu_to_partition.get(cpu) {
294 partition_claimants
295 .entry(partition)
296 .or_default()
297 .extend(claimants);
298 }
299 }
300 }
301 let partition_pressure = |cpu: usize| {
302 self.cpu_to_partition
303 .and_then(|cpu_to_partition| cpu_to_partition.get(&cpu))
304 .and_then(|partition| partition_claimants.get(partition))
305 .map_or(0, HashSet::len)
306 };
307
308 let mut claimed_cpus: HashMap<u32, Vec<usize>> = HashMap::new();
309 for (&cpu, claimants) in contention {
310 for &id in claimants {
311 claimed_cpus.entry(id).or_default().push(cpu);
312 }
313 }
314
315 for recipient in requesters {
316 let mut recipient_reserved = Cpumask::new();
317 let mut unclaimed: Vec<usize> = self
318 .domain
319 .iter()
320 .filter(|&cpu| {
321 recipient.allowed.test_cpu(cpu)
322 && !reserved.test_cpu(cpu)
323 && !contention.contains_key(&cpu)
324 })
325 .collect();
326 unclaimed.sort_unstable();
327
328 let mut taken = 0usize;
329 for cpu in unclaimed.into_iter().take(recipient.minimum.requested) {
330 recipient_reserved.set_cpu(cpu).ok();
331 reserved.set_cpu(cpu).ok();
332 taken += 1;
333 }
334
335 let mut taken_from: HashMap<u32, usize> =
336 claimed_cpus.keys().map(|&id| (id, 0)).collect();
337 while taken < recipient.minimum.requested {
338 let remaining: HashMap<u32, usize> = claimed_cpus
342 .iter()
343 .map(|(&id, cpus)| {
344 (
345 id,
346 cpus.iter()
347 .filter(|&&cpu| {
348 !reserved.test_cpu(cpu) && contention[&cpu].len() == 1
349 })
350 .count(),
351 )
352 })
353 .collect();
354
355 let reservable = |cpu: usize| {
356 recipient.allowed.test_cpu(cpu)
357 && !reserved.test_cpu(cpu)
358 && contention.get(&cpu).is_some_and(|claimants| {
359 claimants.iter().all(|id| {
360 let protected = by_id
361 .get(id)
362 .map_or(0, |recipient| recipient.minimum.protected);
363 remaining.get(id).copied().unwrap_or(0) > protected
364 })
365 })
366 };
367
368 let donor = claimed_cpus
369 .iter()
370 .filter(|(_, cpus)| cpus.iter().any(|&cpu| reservable(cpu)))
371 .map(|(&id, _)| id)
372 .min_by(|&a, &b| {
373 taken_from[&a]
374 .cmp(&taken_from[&b])
375 .then(remaining[&b].cmp(&remaining[&a]))
376 .then(a.cmp(&b))
377 });
378 let Some(donor) = donor else {
379 break;
380 };
381
382 let cpu = claimed_cpus[&donor]
383 .iter()
384 .copied()
385 .filter(|&cpu| reservable(cpu))
386 .min_by(|&a, &b| {
387 let contested = |cpu: usize| contention.get(&cpu).map_or(0, Vec::len) > 1;
388 contested(a)
389 .cmp(&contested(b))
390 .then_with(|| partition_pressure(b).cmp(&partition_pressure(a)))
391 .then(a.cmp(&b))
392 })
393 .expect("donor has a reservable CPU");
394 recipient_reserved.set_cpu(cpu).ok();
395 reserved.set_cpu(cpu).ok();
396 *taken_from.entry(donor).or_insert(0) += 1;
397 taken += 1;
398 }
399
400 if recipient_reserved.weight() > 0 {
401 assignments.insert(recipient.id, recipient_reserved);
402 }
403 }
404
405 Ok(assignments)
406 }
407
408 fn compute_assignments(
422 &self,
423 recipients: &[CpuRecipient],
424 compute_borrowable: bool,
425 ) -> Result<Vec<CpuAssignment>> {
426 let domain = self.domain;
427
428 if recipients.is_empty() {
429 bail!("compute_cpu_assignments called with no recipients");
430 }
431
432 let mut seen_ids = HashSet::new();
433 for recipient in recipients {
434 if recipient.weight < 0.0 {
435 bail!(
436 "Recipient {} has negative weight {}",
437 recipient.id,
438 recipient.weight
439 );
440 }
441 if !seen_ids.insert(recipient.id) {
442 bail!("Duplicate recipient id {}", recipient.id);
443 }
444 if let Some(claimed) = &recipient.claimed {
445 let claimed_outside_allowed = claimed.and(&recipient.allowed.not()).and(domain);
446 if claimed_outside_allowed.weight() > 0 {
447 bail!(
448 "Recipient {} claims CPUs outside its allowed mask: {}",
449 recipient.id,
450 claimed_outside_allowed.to_cpulist()
451 );
452 }
453 }
454 }
455
456 let mut contention: HashMap<usize, Vec<u32>> = HashMap::new();
458 for recipient in recipients {
459 if let Some(ref claimed) = recipient.claimed {
460 for cpu in claimed.iter() {
461 if domain.test_cpu(cpu) {
462 contention.entry(cpu).or_default().push(recipient.id);
463 }
464 }
465 }
466 }
467
468 let mut recipient_cpus = self.reserve_requested_minimums(recipients, &contention)?;
469 let mut reserved = Cpumask::new();
470 for cpus in recipient_cpus.values() {
471 reserved = reserved.or(cpus);
472 }
473
474 let mut contested_cpus: Vec<usize> = Vec::new();
479 let mut unclaimed_cpus: Vec<usize> = Vec::new();
480
481 for cpu in domain.iter() {
482 if reserved.test_cpu(cpu) {
483 continue;
484 }
485 match contention.get(&cpu) {
486 None => unclaimed_cpus.push(cpu),
487 Some(claimants) if claimants.len() == 1 => {
488 let id = claimants[0];
489 recipient_cpus
490 .entry(id)
491 .or_insert_with(Cpumask::new)
492 .set_cpu(cpu)
493 .ok();
494 }
495 Some(_) => contested_cpus.push(cpu),
496 }
497 }
498
499 let mut assigned_count: HashMap<u32, usize> = recipient_cpus
505 .iter()
506 .map(|(&id, mask)| (id, mask.weight()))
507 .collect();
508 contested_cpus.sort_unstable();
509 let contested_choices: HashMap<u32, usize> = recipients
510 .iter()
511 .map(|recipient| {
512 let choices = contested_cpus
513 .iter()
514 .filter(|cpu| {
515 contention
516 .get(cpu)
517 .is_some_and(|claimants| claimants.contains(&recipient.id))
518 })
519 .count();
520 (recipient.id, choices)
521 })
522 .collect();
523 let mut weighted_contested_cpus = Vec::new();
524 for cpu in contested_cpus {
525 let claimants = contention
526 .get(&cpu)
527 .expect("contested CPU missing contention entry");
528 let protected_recipient = claimants
529 .iter()
530 .filter_map(|id| {
531 let recipient = recipients
532 .iter()
533 .find(|recipient| recipient.id == *id)
534 .expect("claimant missing recipient");
535 let assigned = assigned_count.get(id).copied().unwrap_or(0);
536 (assigned < recipient.minimum.protected)
537 .then(|| (*id, recipient.minimum.protected - assigned))
538 })
539 .min_by(|(a_id, a_deficit), (b_id, b_deficit)| {
542 contested_choices[a_id]
543 .cmp(&contested_choices[b_id])
544 .then(b_deficit.cmp(a_deficit))
545 .then(a_id.cmp(b_id))
546 })
547 .map(|(id, _)| id);
548
549 if let Some(id) = protected_recipient {
550 recipient_cpus
551 .entry(id)
552 .or_insert_with(Cpumask::new)
553 .set_cpu(cpu)
554 .ok();
555 *assigned_count.entry(id).or_insert(0) += 1;
556 } else {
557 weighted_contested_cpus.push(cpu);
558 }
559 }
560 let contested_cpus = weighted_contested_cpus;
561
562 let total_cpu_count = domain.weight();
564 let mut all_recipients_with_weights: Vec<(u32, f64)> =
565 recipients.iter().map(|r| (r.id, r.weight)).collect();
566 all_recipients_with_weights.sort_by_key(|(id, _)| *id);
567
568 let targets = Self::compute_targets(total_cpu_count, &all_recipients_with_weights)?;
569
570 let mut contested_groups: HashMap<Vec<u32>, Vec<usize>> = HashMap::new();
572 for cpu in contested_cpus {
573 if let Some(claimants) = contention.get(&cpu) {
574 let mut sorted_claimants = claimants.clone();
575 sorted_claimants.sort();
576 contested_groups
577 .entry(sorted_claimants)
578 .or_default()
579 .push(cpu);
580 }
581 }
582
583 let initial_deficit: HashMap<u32, f64> = targets
589 .iter()
590 .map(|(&id, &target)| {
591 let already = assigned_count.get(&id).copied().unwrap_or(0);
593 let deficit = if target > already {
594 (target - already) as f64
595 } else {
596 0.0
597 };
598 (id, deficit)
599 })
600 .collect();
601
602 for (claimants, cpus) in contested_groups {
603 let mut recipients_with_deficit: Vec<(u32, f64)> = Vec::new();
604 let mut all_zero = true;
605 for &id in &claimants {
606 let deficit = *initial_deficit.get(&id).ok_or_else(|| {
607 anyhow::anyhow!(
608 "BUG: recipient {} in contention map but missing from targets",
609 id
610 )
611 })?;
612 if deficit > 0.0 {
613 all_zero = false;
614 }
615 recipients_with_deficit.push((id, deficit));
616 }
617
618 if all_zero {
620 recipients_with_deficit = claimants.iter().map(|&id| (id, 1.0)).collect();
621 }
622
623 let distribution = Self::distribute_cpus_proportional(&cpus, &recipients_with_deficit)?;
624 for (id, assigned_cpus) in distribution {
625 let count = assigned_cpus.len();
626 for cpu in assigned_cpus {
627 recipient_cpus
628 .entry(id)
629 .or_insert_with(Cpumask::new)
630 .set_cpu(cpu)
631 .ok();
632 }
633 *assigned_count.entry(id).or_insert(0) += count;
634 }
635 }
636
637 if !unclaimed_cpus.is_empty() {
639 let mut unpinned_recipients: Vec<(u32, f64)> = Vec::new();
640 let mut all_zero = true;
641
642 for recipient in recipients {
643 if recipient.claimed.is_some() {
644 continue; }
646 let target = *targets.get(&recipient.id).ok_or_else(|| {
647 anyhow::anyhow!(
648 "BUG: recipient {} is unpinned but missing from targets",
649 recipient.id
650 )
651 })?;
652 let already = assigned_count.get(&recipient.id).copied().unwrap_or(0);
654 let deficit = if target > already {
655 (target - already) as f64
656 } else {
657 0.0
658 };
659 if deficit > 0.0 {
660 all_zero = false;
661 }
662 unpinned_recipients.push((recipient.id, deficit));
663 }
664 unpinned_recipients.sort_by_key(|(id, _)| *id);
665
666 if all_zero {
668 unpinned_recipients = unpinned_recipients
669 .iter()
670 .map(|(id, _)| (*id, 1.0))
671 .collect();
672 }
673
674 let distribution =
675 Self::distribute_cpus_proportional(&unclaimed_cpus, &unpinned_recipients)?;
676 for (id, assigned_cpus) in distribution {
677 let count = assigned_cpus.len();
678 for cpu in assigned_cpus {
679 recipient_cpus
680 .entry(id)
681 .or_insert_with(Cpumask::new)
682 .set_cpu(cpu)
683 .ok();
684 }
685 *assigned_count.entry(id).or_insert(0) += count;
686 }
687 }
688
689 for recipient in recipients {
692 let assigned = recipient_cpus.get(&recipient.id).map_or(0, Cpumask::weight);
693 let required = recipient.minimum.protected.max(1);
694 if assigned < required {
695 bail!(
696 "Recipient {} has {} CPUs assigned, below required minimum {} \
697 (nr_cpus={}, num_recipients={})",
698 recipient.id,
699 assigned,
700 required,
701 domain.weight(),
702 recipients.len()
703 );
704 }
705 }
706
707 let allowed_by_id: HashMap<u32, Cpumask> = recipients
708 .iter()
709 .map(|recipient| (recipient.id, recipient.allowed.clone()))
710 .collect();
711
712 Ok(recipient_cpus
714 .into_iter()
715 .map(|(id, primary)| {
716 let borrowable = if compute_borrowable {
717 let allowed = allowed_by_id
718 .get(&id)
719 .expect("recipient assignment missing allowed mask");
720 Some(domain.and(&primary.not()).and(allowed))
721 } else {
722 None
723 };
724 CpuAssignment {
725 id,
726 primary,
727 borrowable,
728 }
729 })
730 .collect())
731 }
732}
733
734fn read_dir_sorted(path: &Path) -> Result<Vec<DirEntry>> {
735 let readdir = std::fs::read_dir(path)
736 .with_context(|| format!("Failed to read directory: {}", path.display()))?;
737
738 let mut entries: Vec<_> = readdir
739 .map(|entry| {
740 entry.with_context(|| format!("Failed to read directory entry in: {}", path.display()))
741 })
742 .collect::<Result<Vec<_>>>()?;
743 entries.sort_by_key(|entry| entry.path());
744
745 Ok(entries)
746}
747
748pub struct CellManager {
750 cell_parent_path: PathBuf,
751 inotify: Inotify,
752 cells: HashMap<u64, CellInfo>,
754 cell_id_to_cgid: HashMap<u32, u64>,
756 free_cell_ids: BTreeSet<u32>,
758 detaching_cells: BTreeSet<u32>,
763 next_cell_id: u32,
764 max_cells: u32,
765 all_cpus: Cpumask,
767 exclude_names: HashSet<String>,
769 cell0_min_cpus: usize,
772 cpu_to_llc: HashMap<usize, usize>,
776 enforced_holdout: AtomicBool,
781}
782
783impl CellManager {
784 pub fn new(
785 cell_parent_path: &str,
786 max_cells: u32,
787 all_cpus: Cpumask,
788 exclude: HashSet<String>,
789 cell0_min_cpus: usize,
790 cpu_to_llc: HashMap<usize, usize>,
791 ) -> Result<Self> {
792 let path = PathBuf::from(format!("/sys/fs/cgroup{}", cell_parent_path));
793 if !path.exists() {
794 bail!("Cell parent cgroup path does not exist: {}", path.display());
795 }
796 Self::new_with_path_opts(
797 path,
798 max_cells,
799 all_cpus,
800 exclude,
801 cell0_min_cpus,
802 cpu_to_llc,
803 )
804 }
805
806 #[cfg(test)]
810 fn new_with_path(
811 path: PathBuf,
812 max_cells: u32,
813 all_cpus: Cpumask,
814 exclude: HashSet<String>,
815 ) -> Result<Self> {
816 Self::new_with_path_opts(path, max_cells, all_cpus, exclude, 0, HashMap::new())
817 }
818
819 fn new_with_path_opts(
820 path: PathBuf,
821 max_cells: u32,
822 all_cpus: Cpumask,
823 exclude: HashSet<String>,
824 cell0_min_cpus: usize,
825 cpu_to_llc: HashMap<usize, usize>,
826 ) -> Result<Self> {
827 let inotify = Inotify::init().context("Failed to initialize inotify")?;
828 inotify
829 .watches()
830 .add(&path, WatchMask::CREATE | WatchMask::DELETE)
831 .context("Failed to add inotify watch")?;
832
833 let mut mgr = Self {
834 cell_parent_path: path.clone(),
835 inotify,
836 cells: HashMap::new(),
837 cell_id_to_cgid: HashMap::new(),
838 free_cell_ids: BTreeSet::new(),
839 detaching_cells: BTreeSet::new(),
840 next_cell_id: 1, max_cells,
842 all_cpus,
843 exclude_names: exclude,
844 cell0_min_cpus,
845 cpu_to_llc,
846 enforced_holdout: AtomicBool::new(false),
847 };
848
849 mgr.cells.insert(
852 0,
853 CellInfo {
854 cell_id: 0,
855 cgroup_path: None,
856 cgid: None,
857 cpuset: None,
858 },
859 );
860 mgr.cell_id_to_cgid.insert(0, 0);
861
862 mgr.scan_existing_children()
864 .context("Failed to scan existing child cgroups at startup")?;
865 Ok(mgr)
866 }
867
868 pub fn enforced_holdout(&self) -> bool {
871 self.enforced_holdout.load(Ordering::Relaxed)
872 }
873
874 fn should_exclude(&self, path: &Path) -> bool {
875 path.file_name()
876 .and_then(|n| n.to_str())
877 .map(|name| self.exclude_names.contains(name))
878 .unwrap_or(false)
879 }
880
881 fn scan_existing_children(&mut self) -> Result<Vec<(u64, u32)>> {
882 let mut assignments = Vec::new();
883
884 let entries = read_dir_sorted(&self.cell_parent_path)
885 .context("Failed to read cell parent directory")?;
886
887 for entry in entries {
888 let file_type = entry.file_type().with_context(|| {
889 format!("Failed to get file type for: {}", entry.path().display())
890 })?;
891 if file_type.is_dir() {
892 let path = entry.path();
893 if self.should_exclude(&path) {
894 continue;
895 }
896 let cgid = path
897 .metadata()
898 .with_context(|| {
899 format!("reading inode of cgroup directory {}", path.display())
900 })?
901 .ino();
902 let (cgid, cell_id) =
903 self.create_cell_for_cgroup(&path, cgid).with_context(|| {
904 format!("Failed to create cell for cgroup: {}", path.display())
905 })?;
906 assignments.push((cgid, cell_id));
907 }
908 }
909 Ok(assignments)
910 }
911
912 #[allow(clippy::type_complexity)]
924 pub fn process_events(
925 &mut self,
926 read_records: impl FnOnce() -> Result<Vec<u64>>,
927 ) -> Result<(Vec<(u64, u32)>, Vec<u32>)> {
928 let mut buffer = [0; 1024];
929 let mut has_events = false;
930
931 loop {
933 match self.inotify.read_events(&mut buffer) {
934 Ok(events) => {
935 if events.into_iter().next().is_some() {
936 has_events = true;
937 } else {
938 break;
939 }
940 }
941 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
942 Err(e) => {
943 return Err(e).context("Failed to read inotify events");
944 }
945 }
946 }
947
948 if !has_events {
949 return Ok((Vec::new(), Vec::new()));
950 }
951
952 let current_entries = self.scan_cells()?;
954 let sub_cgids = read_records()?;
955 self.reconcile_scanned(current_entries, &sub_cgids)
956 }
957
958 #[cfg(test)]
961 fn reconcile_cells(&mut self, sub_cgids: &[u64]) -> Result<(Vec<(u64, u32)>, Vec<u32>)> {
962 let current_entries = self.scan_cells()?;
963 self.reconcile_scanned(current_entries, sub_cgids)
964 }
965
966 fn scan_cells(&self) -> Result<BTreeMap<PathBuf, u64>> {
968 let mut current_entries: BTreeMap<PathBuf, u64> = BTreeMap::new();
971 let entries = read_dir_sorted(&self.cell_parent_path)?;
972 for entry in entries {
973 let file_type = entry.file_type().with_context(|| {
974 format!("Failed to get file type for: {}", entry.path().display())
975 })?;
976 if file_type.is_dir() {
977 let path = entry.path();
978 if self.should_exclude(&path) {
979 continue;
980 }
981
982 let metadata = match entry.metadata() {
983 Ok(metadata) => metadata,
984 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
985 continue;
988 }
989 Err(e) => {
990 return Err(e).with_context(|| {
991 format!("reading inode of cgroup directory {}", path.display())
992 });
993 }
994 };
995
996 current_entries.insert(path, metadata.ino());
997 }
998 }
999 Ok(current_entries)
1000 }
1001
1002 #[allow(clippy::type_complexity)]
1006 fn reconcile_scanned(
1007 &mut self,
1008 current_entries: BTreeMap<PathBuf, u64>,
1009 sub_cgids: &[u64],
1010 ) -> Result<(Vec<(u64, u32)>, Vec<u32>)> {
1011 let mut new_cells = Vec::new();
1012
1013 let mut destroyed_cells: BTreeSet<u32> = BTreeSet::new();
1015 self.cells.retain(|&cgid, info| {
1016 if info.cell_id == 0 {
1017 return true; }
1019 let cgroup_path = info
1021 .cgroup_path
1022 .as_ref()
1023 .expect("BUG: non-zero cell missing cgroup_path");
1024 if current_entries.get(cgroup_path) == Some(&cgid) {
1027 true
1028 } else {
1029 info!(
1030 "Destroyed cell {} for cgroup {} (cgid={})",
1031 info.cell_id,
1032 cgroup_path.display(),
1033 cgid
1034 );
1035 destroyed_cells.insert(info.cell_id);
1036 false
1037 }
1038 });
1039
1040 self.cell_id_to_cgid
1043 .retain(|cell_id, _| !destroyed_cells.contains(cell_id));
1044 for &cell_id in &destroyed_cells {
1045 if sub_cgids[cell_id as usize] == 0 {
1046 self.free_cell_ids.insert(cell_id);
1047 } else {
1048 self.detaching_cells.insert(cell_id);
1049 }
1050 }
1051
1052 for (path, cgid) in current_entries {
1054 if self.cells.contains_key(&cgid) {
1055 continue; }
1057 let (cgid, cell_id) = self
1058 .create_cell_for_cgroup(&path, cgid)
1059 .with_context(|| format!("Failed to create cell for cgroup: {}", path.display()))?;
1060 new_cells.push((cgid, cell_id));
1061 }
1062
1063 Ok((new_cells, destroyed_cells.into_iter().collect()))
1064 }
1065
1066 fn create_cell_for_cgroup(&mut self, path: &Path, cgid: u64) -> Result<(u64, u32)> {
1067 let cell_id = self.allocate_cell_id().context("allocating cell ID")?;
1068
1069 let cpuset = Self::read_cpuset(path)
1070 .with_context(|| format!("reading cpuset for cgroup {}", path.display()))?;
1071 if let Some(ref mask) = cpuset {
1072 let cpuset_path = path.join("cpuset.cpus");
1073 debug!(
1074 "Cell {} has cpuset: {} (from {})",
1075 cell_id,
1076 mask.to_cpulist(),
1077 cpuset_path.display()
1078 );
1079 }
1080
1081 self.cells.insert(
1082 cgid,
1083 CellInfo {
1084 cell_id,
1085 cgroup_path: Some(path.to_path_buf()),
1086 cgid: Some(cgid),
1087 cpuset,
1088 },
1089 );
1090 self.cell_id_to_cgid.insert(cell_id, cgid);
1091
1092 info!(
1093 "Created cell {} for cgroup {} (cgid={})",
1094 cell_id,
1095 path.display(),
1096 cgid
1097 );
1098
1099 Ok((cgid, cell_id))
1100 }
1101
1102 fn read_cpuset(cgroup_path: &Path) -> Result<Option<Cpumask>> {
1104 let cpuset_path = cgroup_path.join("cpuset.cpus");
1105 match std::fs::read_to_string(&cpuset_path) {
1106 Ok(content) => {
1107 let content = content.trim();
1108 if content.is_empty() {
1109 Ok(None)
1110 } else {
1111 let mask = Cpumask::from_cpulist(content).with_context(|| {
1112 format!(
1113 "Failed to parse cpuset '{}' from {}",
1114 content,
1115 cpuset_path.display()
1116 )
1117 })?;
1118 Ok(Some(mask))
1119 }
1120 }
1121 Err(_) => Ok(None),
1123 }
1124 }
1125
1126 pub fn release_detached_cells(&mut self, sub_cgids: &[u64]) -> usize {
1130 let released: Vec<u32> = self
1131 .detaching_cells
1132 .iter()
1133 .copied()
1134 .filter(|&id| sub_cgids[id as usize] == 0)
1135 .collect();
1136 for &id in &released {
1137 self.detaching_cells.remove(&id);
1138 self.free_cell_ids.insert(id);
1139 }
1140 released.len()
1141 }
1142
1143 fn allocate_cell_id(&mut self) -> Result<u32> {
1144 if let Some(id) = self.free_cell_ids.pop_first() {
1146 return Ok(id);
1147 }
1148
1149 if self.next_cell_id >= self.max_cells {
1150 bail!("Cell ID space exhausted (max_cells={})", self.max_cells);
1151 }
1152
1153 let id = self.next_cell_id;
1154 self.next_cell_id += 1;
1155 Ok(id)
1156 }
1157
1158 pub fn compute_cpu_assignments(&self, compute_borrowable: bool) -> Result<Vec<CpuAssignment>> {
1170 self.compute_cpu_assignments_inner(None, compute_borrowable)
1172 }
1173
1174 pub fn compute_demand_cpu_assignments(
1182 &self,
1183 cell_demands: &HashMap<u32, f64>,
1184 compute_borrowable: bool,
1185 ) -> Result<Vec<CpuAssignment>> {
1186 self.compute_cpu_assignments_inner(Some(cell_demands), compute_borrowable)
1187 }
1188
1189 fn compute_cpu_assignments_inner(
1191 &self,
1192 cell_demands: Option<&HashMap<u32, f64>>,
1193 compute_borrowable: bool,
1194 ) -> Result<Vec<CpuAssignment>> {
1195 let recipients: Vec<CpuRecipient> = self
1196 .cells
1197 .values()
1198 .map(|info| {
1199 let weight = match cell_demands {
1200 Some(demands) => {
1201 let weight = *demands
1202 .get(&info.cell_id)
1203 .ok_or_else(|| {
1204 anyhow::anyhow!("Cell {} is missing from demands map", info.cell_id)
1205 })
1206 .context("building cell demand weights map")?;
1207 if weight < 0.0 {
1208 bail!(
1209 "Cell {} has negative demand weight {}",
1210 info.cell_id,
1211 weight
1212 );
1213 }
1214 weight
1215 }
1216 None => 1.0,
1217 };
1218 Ok(CpuRecipient {
1219 id: info.cell_id,
1220 weight,
1221 allowed: info.cpuset.clone().unwrap_or_else(|| self.all_cpus.clone()),
1222 claimed: info.cpuset.clone(),
1223 minimum: if info.cell_id == 0 {
1224 CpuMinimum {
1225 protected: 0,
1226 requested: self.cell0_min_cpus,
1227 }
1228 } else {
1229 CpuMinimum {
1230 protected: 1,
1231 requested: 0,
1232 }
1233 },
1234 })
1235 })
1236 .collect::<Result<Vec<_>>>()?;
1237
1238 let assignments = CpuManager::with_partitions(&self.all_cpus, &self.cpu_to_llc)
1239 .compute_assignments(&recipients, compute_borrowable)?;
1240
1241 if self.cell0_min_cpus > 0 {
1244 let mut workload_claims = Cpumask::new();
1245 for recipient in recipients.iter().filter(|recipient| recipient.id != 0) {
1246 if let Some(claimed) = &recipient.claimed {
1247 workload_claims = workload_claims.or(claimed);
1248 }
1249 }
1250 if assignments
1251 .iter()
1252 .find(|assignment| assignment.id == 0)
1253 .is_some_and(|assignment| assignment.primary.and(&workload_claims).weight() > 0)
1254 {
1255 self.enforced_holdout.store(true, Ordering::Relaxed);
1256 }
1257 }
1258
1259 Ok(assignments)
1260 }
1261
1262 pub fn get_cell_assignments(&self) -> Vec<(u64, u32)> {
1265 let mut assignments: Vec<_> = self
1266 .cells
1267 .values()
1268 .filter(|info| info.cell_id != 0)
1269 .map(|info| {
1270 (
1271 info.cgid.expect("BUG: non-zero cell missing cgid"),
1272 info.cell_id,
1273 )
1274 })
1275 .collect();
1276 assignments.sort_by_key(|(_cgid, cell_id)| *cell_id);
1277 assignments
1278 }
1279
1280 pub fn format_cell_config(&self, cpu_assignments: &[CpuAssignment]) -> String {
1283 let mut sorted: Vec<_> = cpu_assignments.iter().collect();
1284 sorted.sort_by_key(|a| a.id);
1285
1286 let mut parts = Vec::new();
1287 for assignment in sorted {
1288 let cpulist = assignment.primary.to_cpulist();
1289 if assignment.id == 0 {
1290 parts.push(format!("[0: {}]", cpulist));
1291 } else {
1292 let name = self
1294 .cells
1295 .values()
1296 .find(|info| info.cell_id == assignment.id)
1297 .and_then(|info| {
1298 info.cgroup_path
1299 .as_ref()
1300 .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
1301 })
1302 .unwrap_or_else(|| "?".to_string());
1303 parts.push(format!("[{}({}): {}]", assignment.id, name, cpulist));
1304 }
1305 }
1306 parts.join(" ")
1307 }
1308
1309 pub fn cgroup_path_for_cell(&self, cell_id: u32) -> String {
1313 self.cell_id_to_cgid
1314 .get(&cell_id)
1315 .and_then(|cgid| self.cells.get(cgid))
1316 .and_then(|info| info.cgroup_path.as_deref())
1317 .map(cgroup_root_relative)
1318 .unwrap_or_else(|| "/".to_string())
1319 }
1320
1321 pub fn refresh_cpusets(&mut self) -> Result<bool> {
1324 let mut changed = false;
1325 for info in self.cells.values_mut() {
1326 let Some(ref cgroup_path) = info.cgroup_path else {
1327 continue; };
1329 let new_cpuset = Self::read_cpuset(cgroup_path)
1330 .with_context(|| format!("reading cpuset for cgroup {}", cgroup_path.display()))?;
1331 if new_cpuset != info.cpuset {
1332 info!(
1333 "Cell {} cpuset changed: {:?} -> {:?} ({})",
1334 info.cell_id,
1335 info.cpuset.as_ref().map(|m| m.to_cpulist()),
1336 new_cpuset.as_ref().map(|m| m.to_cpulist()),
1337 cgroup_path.display(),
1338 );
1339 info.cpuset = new_cpuset;
1340 changed = true;
1341 }
1342 }
1343 Ok(changed)
1344 }
1345}
1346
1347impl AsFd for CellManager {
1348 fn as_fd(&self) -> BorrowedFd<'_> {
1349 self.inotify.as_fd()
1350 }
1351}
1352
1353#[cfg(test)]
1354impl CellManager {
1355 fn cell_count(&self) -> usize {
1358 self.cells.values().filter(|c| c.cell_id != 0).count()
1359 }
1360
1361 fn get_cell_ids(&self) -> Vec<u32> {
1364 self.cells
1365 .values()
1366 .filter(|c| c.cell_id != 0)
1367 .map(|c| c.cell_id)
1368 .collect()
1369 }
1370
1371 fn find_cell_by_name(&self, name: &str) -> Option<&CellInfo> {
1374 self.cells.values().filter(|c| c.cell_id != 0).find(|c| {
1375 c.cgroup_path
1376 .as_ref()
1377 .and_then(|p| p.file_name())
1378 .map(|n| n.to_str() == Some(name))
1379 .unwrap_or(false)
1380 })
1381 }
1382}
1383
1384#[cfg(test)]
1385#[allow(clippy::unwrap_used)]
1386mod tests {
1387 use super::*;
1388 use tempfile::TempDir;
1389
1390 const NO_SUBS: [u64; 256] = [0; 256];
1393
1394 fn cpumask_for_range(nr_cpus: usize) -> Cpumask {
1395 scx_utils::set_cpumask_test_width(nr_cpus);
1396 let mut mask = Cpumask::new();
1397 for cpu in 0..nr_cpus {
1398 mask.set_cpu(cpu).unwrap();
1399 }
1400 mask
1401 }
1402
1403 #[test]
1406 fn test_cgroup_path_for_cell() {
1407 let tmp = TempDir::new().unwrap();
1408 let child = tmp.path().join("container-a");
1409 std::fs::create_dir(&child).unwrap();
1410 let mgr = CellManager::new_with_path(
1411 tmp.path().to_path_buf(),
1412 256,
1413 cpumask_for_range(16),
1414 HashSet::new(),
1415 )
1416 .unwrap();
1417
1418 assert_eq!(mgr.cgroup_path_for_cell(0), "/");
1420
1421 let cell_id = mgr.find_cell_by_name("container-a").unwrap().cell_id;
1425 assert_eq!(
1426 mgr.cgroup_path_for_cell(cell_id),
1427 child.to_string_lossy().into_owned()
1428 );
1429
1430 assert_eq!(
1432 cgroup_root_relative(Path::new("/sys/fs/cgroup/test.slice/foobar")),
1433 "/test.slice/foobar"
1434 );
1435 }
1436
1437 fn cpumask_from_cpulist(nr_cpus: usize, cpulist: &str) -> Cpumask {
1438 scx_utils::set_cpumask_test_width(nr_cpus);
1439 Cpumask::from_cpulist(cpulist).unwrap()
1440 }
1441
1442 fn find_assignment(assignments: &[CpuAssignment], id: u32) -> &CpuAssignment {
1443 assignments.iter().find(|a| a.id == id).unwrap()
1444 }
1445
1446 #[test]
1449 fn test_scan_empty_directory() {
1450 let tmp = TempDir::new().unwrap();
1451 let mgr = CellManager::new_with_path(
1452 tmp.path().to_path_buf(),
1453 256,
1454 cpumask_for_range(16),
1455 HashSet::new(),
1456 )
1457 .unwrap();
1458
1459 assert_eq!(mgr.cell_count(), 0);
1460 }
1461
1462 #[test]
1463 fn test_scan_existing_subdirectories() {
1464 let tmp = TempDir::new().unwrap();
1465
1466 std::fs::create_dir(tmp.path().join("container-a")).unwrap();
1468 std::fs::create_dir(tmp.path().join("container-b")).unwrap();
1469
1470 let mgr = CellManager::new_with_path(
1471 tmp.path().to_path_buf(),
1472 256,
1473 cpumask_for_range(16),
1474 HashSet::new(),
1475 )
1476 .unwrap();
1477
1478 assert_eq!(mgr.cell_count(), 2);
1479
1480 let cell_ids = mgr.get_cell_ids();
1482 assert!(cell_ids.contains(&1));
1483 assert!(cell_ids.contains(&2));
1484 }
1485
1486 #[test]
1487 fn test_reconcile_detects_new_directories() {
1488 let tmp = TempDir::new().unwrap();
1489
1490 std::fs::create_dir(tmp.path().join("container-a")).unwrap();
1492 let mut mgr = CellManager::new_with_path(
1493 tmp.path().to_path_buf(),
1494 256,
1495 cpumask_for_range(16),
1496 HashSet::new(),
1497 )
1498 .unwrap();
1499 assert_eq!(mgr.cell_count(), 1);
1500
1501 std::fs::create_dir(tmp.path().join("container-b")).unwrap();
1503
1504 let (new_cells, destroyed_cells) = mgr.reconcile_cells(&NO_SUBS).unwrap();
1506 assert_eq!(new_cells.len(), 1);
1507 assert_eq!(destroyed_cells.len(), 0);
1508 assert_eq!(mgr.cell_count(), 2);
1509 }
1510
1511 #[test]
1512 fn test_reconcile_detects_removed_directories() {
1513 let tmp = TempDir::new().unwrap();
1514
1515 std::fs::create_dir(tmp.path().join("container-a")).unwrap();
1517 std::fs::create_dir(tmp.path().join("container-b")).unwrap();
1518 let mut mgr = CellManager::new_with_path(
1519 tmp.path().to_path_buf(),
1520 256,
1521 cpumask_for_range(16),
1522 HashSet::new(),
1523 )
1524 .unwrap();
1525 assert_eq!(mgr.cell_count(), 2);
1526
1527 std::fs::remove_dir(tmp.path().join("container-b")).unwrap();
1529
1530 let (new_cells, destroyed_cells) = mgr.reconcile_cells(&NO_SUBS).unwrap();
1532 assert_eq!(new_cells.len(), 0);
1533 assert_eq!(destroyed_cells.len(), 1);
1534 assert_eq!(mgr.cell_count(), 1);
1535 }
1536
1537 #[test]
1538 fn test_reconcile_replaces_reused_path_with_new_inode() {
1539 let tmp = TempDir::new().unwrap();
1540 let parked = TempDir::new().unwrap();
1541
1542 let original_path = tmp.path().join("container-a");
1543 std::fs::create_dir(&original_path).unwrap();
1544
1545 let mut mgr = CellManager::new_with_path(
1546 tmp.path().to_path_buf(),
1547 256,
1548 cpumask_for_range(16),
1549 HashSet::new(),
1550 )
1551 .unwrap();
1552 assert_eq!(mgr.cell_count(), 1);
1553
1554 let old_info = mgr.find_cell_by_name("container-a").unwrap();
1555 let old_cell_id = old_info.cell_id;
1556 let old_cgid = old_info.cgid.unwrap();
1557
1558 std::fs::rename(&original_path, parked.path().join("container-a-old")).unwrap();
1561 std::fs::create_dir(&original_path).unwrap();
1562
1563 let (new_cells, destroyed_cells) = mgr.reconcile_cells(&NO_SUBS).unwrap();
1564
1565 assert_eq!(new_cells.len(), 1);
1566 assert_eq!(destroyed_cells, vec![old_cell_id]);
1567 assert_eq!(mgr.cell_count(), 1);
1568
1569 let new_info = mgr.find_cell_by_name("container-a").unwrap();
1571 assert_eq!(new_info.cell_id, old_cell_id);
1572 assert_ne!(new_info.cgid.unwrap(), old_cgid);
1573 }
1574
1575 #[test]
1576 fn test_cell_id_reuse_after_destruction() {
1577 let tmp = TempDir::new().unwrap();
1578
1579 std::fs::create_dir(tmp.path().join("cell1")).unwrap();
1581 std::fs::create_dir(tmp.path().join("cell2")).unwrap();
1582 std::fs::create_dir(tmp.path().join("cell3")).unwrap();
1583
1584 let mut mgr = CellManager::new_with_path(
1585 tmp.path().to_path_buf(),
1586 256,
1587 cpumask_for_range(16),
1588 HashSet::new(),
1589 )
1590 .unwrap();
1591
1592 let cell2_info = mgr.find_cell_by_name("cell2").unwrap();
1594 let cell2_id = cell2_info.cell_id;
1595
1596 std::fs::remove_dir(tmp.path().join("cell2")).unwrap();
1598 mgr.reconcile_cells(&NO_SUBS).unwrap();
1599
1600 std::fs::create_dir(tmp.path().join("cell4")).unwrap();
1602 mgr.reconcile_cells(&NO_SUBS).unwrap();
1603
1604 let cell4_info = mgr.find_cell_by_name("cell4").unwrap();
1605 assert_eq!(cell4_info.cell_id, cell2_id);
1606 }
1607
1608 #[test]
1609 fn test_held_cell_id_waits_for_detach() {
1610 let tmp = TempDir::new().unwrap();
1611
1612 std::fs::create_dir(tmp.path().join("cell1")).unwrap();
1613 std::fs::create_dir(tmp.path().join("cell2")).unwrap();
1614
1615 let mut mgr = CellManager::new_with_path(
1616 tmp.path().to_path_buf(),
1617 256,
1618 cpumask_for_range(16),
1619 HashSet::new(),
1620 )
1621 .unwrap();
1622 let cell2_id = mgr.find_cell_by_name("cell2").unwrap().cell_id;
1623
1624 let mut held = NO_SUBS;
1626 held[cell2_id as usize] = 42;
1627 std::fs::remove_dir(tmp.path().join("cell2")).unwrap();
1628 mgr.reconcile_cells(&held).unwrap();
1629
1630 std::fs::create_dir(tmp.path().join("cell3")).unwrap();
1632 mgr.reconcile_cells(&held).unwrap();
1633 assert_ne!(mgr.find_cell_by_name("cell3").unwrap().cell_id, cell2_id);
1634
1635 assert_eq!(mgr.release_detached_cells(&held), 0);
1637 assert_eq!(mgr.release_detached_cells(&NO_SUBS), 1);
1638 std::fs::create_dir(tmp.path().join("cell4")).unwrap();
1639 mgr.reconcile_cells(&NO_SUBS).unwrap();
1640 assert_eq!(mgr.find_cell_by_name("cell4").unwrap().cell_id, cell2_id);
1641 }
1642
1643 #[test]
1646 fn test_cpu_assignments_no_cells() {
1647 let tmp = TempDir::new().unwrap();
1648 let mgr = CellManager::new_with_path(
1649 tmp.path().to_path_buf(),
1650 256,
1651 cpumask_for_range(16),
1652 HashSet::new(),
1653 )
1654 .unwrap();
1655
1656 let assignments = mgr.compute_cpu_assignments(false).unwrap();
1657
1658 assert_eq!(assignments.len(), 1);
1660 assert_eq!(assignments[0].id, 0);
1661 assert_eq!(assignments[0].primary.weight(), 16);
1662 }
1663
1664 #[test]
1665 fn test_cpu_assignments_proportional() {
1666 let tmp = TempDir::new().unwrap();
1667 std::fs::create_dir(tmp.path().join("container")).unwrap();
1668
1669 let mgr = CellManager::new_with_path(
1670 tmp.path().to_path_buf(),
1671 256,
1672 cpumask_for_range(16),
1673 HashSet::new(),
1674 )
1675 .unwrap();
1676 let assignments = mgr.compute_cpu_assignments(false).unwrap();
1677
1678 assert_eq!(assignments.len(), 2);
1680
1681 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
1682 let cell1 = assignments.iter().find(|a| a.id == 1).unwrap();
1683
1684 assert_eq!(cell0.primary.weight(), 8);
1685 assert_eq!(cell1.primary.weight(), 8);
1686 }
1687
1688 #[test]
1689 fn test_cpu_assignments_remainder_to_cell0() {
1690 let tmp = TempDir::new().unwrap();
1691 std::fs::create_dir(tmp.path().join("cell1")).unwrap();
1692 std::fs::create_dir(tmp.path().join("cell2")).unwrap();
1693
1694 let mgr = CellManager::new_with_path(
1695 tmp.path().to_path_buf(),
1696 256,
1697 cpumask_for_range(10),
1698 HashSet::new(),
1699 )
1700 .unwrap();
1701 let assignments = mgr.compute_cpu_assignments(false).unwrap();
1702
1703 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
1705 assert_eq!(cell0.primary.weight(), 4); }
1707
1708 #[test]
1709 fn test_cpu_assignments_too_many_cells() {
1710 let tmp = TempDir::new().unwrap();
1711
1712 for i in 1..=5 {
1714 std::fs::create_dir(tmp.path().join(format!("cell{}", i))).unwrap();
1715 }
1716
1717 let mgr = CellManager::new_with_path(
1719 tmp.path().to_path_buf(),
1720 256,
1721 cpumask_for_range(4),
1722 HashSet::new(),
1723 )
1724 .unwrap();
1725 let result = mgr.compute_cpu_assignments(false);
1726
1727 assert!(result.is_err());
1728 let err_msg = format!("{:#}", result.unwrap_err());
1729 assert!(
1730 err_msg.contains("Not enough CPUs"),
1731 "Expected 'Not enough CPUs' error, got: {}",
1732 err_msg
1733 );
1734 }
1735
1736 #[test]
1737 fn test_cpu_assignments_with_cpusets() {
1738 let tmp = TempDir::new().unwrap();
1739
1740 let cell1_path = tmp.path().join("cell1");
1742 std::fs::create_dir(&cell1_path).unwrap();
1743 std::fs::write(cell1_path.join("cpuset.cpus"), "0-3\n").unwrap();
1744
1745 let cell2_path = tmp.path().join("cell2");
1746 std::fs::create_dir(&cell2_path).unwrap();
1747 std::fs::write(cell2_path.join("cpuset.cpus"), "8-11\n").unwrap();
1748
1749 let mgr = CellManager::new_with_path(
1750 tmp.path().to_path_buf(),
1751 256,
1752 cpumask_for_range(16),
1753 HashSet::new(),
1754 )
1755 .unwrap();
1756 let assignments = mgr.compute_cpu_assignments(false).unwrap();
1757
1758 assert_eq!(assignments.len(), 3);
1760
1761 let cell1_info = mgr.find_cell_by_name("cell1").unwrap();
1763 let cell2_info = mgr.find_cell_by_name("cell2").unwrap();
1764
1765 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
1766 let cell1 = assignments
1767 .iter()
1768 .find(|a| a.id == cell1_info.cell_id)
1769 .unwrap();
1770 let cell2 = assignments
1771 .iter()
1772 .find(|a| a.id == cell2_info.cell_id)
1773 .unwrap();
1774
1775 assert_eq!(cell1.primary.weight(), 4);
1777 for cpu in 0..4 {
1778 assert!(cell1.primary.test_cpu(cpu));
1779 }
1780
1781 assert_eq!(cell2.primary.weight(), 4);
1783 for cpu in 8..12 {
1784 assert!(cell2.primary.test_cpu(cpu));
1785 }
1786
1787 assert_eq!(cell0.primary.weight(), 8);
1789 for cpu in 4..8 {
1790 assert!(cell0.primary.test_cpu(cpu));
1791 }
1792 for cpu in 12..16 {
1793 assert!(cell0.primary.test_cpu(cpu));
1794 }
1795 }
1796
1797 #[test]
1798 fn test_cpu_assignments_cpusets_cover_all_cpus() {
1799 let tmp = TempDir::new().unwrap();
1800
1801 let cell1_path = tmp.path().join("cell1");
1805 std::fs::create_dir(&cell1_path).unwrap();
1806 std::fs::write(cell1_path.join("cpuset.cpus"), "0-7\n").unwrap();
1807
1808 let cell2_path = tmp.path().join("cell2");
1809 std::fs::create_dir(&cell2_path).unwrap();
1810 std::fs::write(cell2_path.join("cpuset.cpus"), "8-15\n").unwrap();
1811
1812 let mgr = CellManager::new_with_path(
1813 tmp.path().to_path_buf(),
1814 256,
1815 cpumask_for_range(16),
1816 HashSet::new(),
1817 )
1818 .unwrap();
1819 let result = mgr.compute_cpu_assignments(false);
1820
1821 assert!(result.is_err());
1823 let err = result.unwrap_err();
1824 let err_msg = format!("{:#}", err);
1825 assert!(
1826 err_msg.contains("Recipient 0 has 0 CPUs assigned, below required minimum 1"),
1827 "Expected recipient minimum error, got: {}",
1828 err_msg
1829 );
1830 }
1831
1832 #[test]
1833 fn test_cpu_assignments_cpusets_cover_all_cpus_with_holdout() {
1834 let tmp = TempDir::new().unwrap();
1835
1836 let cell1_path = tmp.path().join("cell1");
1841 std::fs::create_dir(&cell1_path).unwrap();
1842 std::fs::write(cell1_path.join("cpuset.cpus"), "0-7\n").unwrap();
1843
1844 let cell2_path = tmp.path().join("cell2");
1845 std::fs::create_dir(&cell2_path).unwrap();
1846 std::fs::write(cell2_path.join("cpuset.cpus"), "8-15\n").unwrap();
1847
1848 let mgr = CellManager::new_with_path_opts(
1849 tmp.path().to_path_buf(),
1850 256,
1851 cpumask_for_range(16),
1852 HashSet::new(),
1853 1,
1854 HashMap::new(),
1855 )
1856 .unwrap();
1857 let assignments = mgr
1858 .compute_cpu_assignments(false)
1859 .expect("holdout should populate cell 0");
1860
1861 let cell0 = assignments
1862 .iter()
1863 .find(|a| a.id == 0)
1864 .expect("cell 0 present");
1865 assert_eq!(cell0.primary.weight(), 1);
1866
1867 let total: usize = assignments.iter().map(|a| a.primary.weight()).sum();
1868 assert_eq!(total, 16);
1869
1870 for assignment in &assignments {
1873 assert!(
1874 assignment.primary.weight() >= 1,
1875 "cell {} starved after holdout: {:?}",
1876 assignment.id,
1877 assignment.primary
1878 );
1879 }
1880 let donor_weights: Vec<usize> = assignments
1881 .iter()
1882 .filter(|a| a.id != 0)
1883 .map(|a| a.primary.weight())
1884 .collect();
1885 assert!(donor_weights.contains(&7));
1886 assert!(donor_weights.contains(&8));
1887 }
1888
1889 #[test]
1890 fn test_cpu_assignments_holdout_takes_from_largest_cell() {
1891 let tmp = TempDir::new().unwrap();
1892
1893 let cell1_path = tmp.path().join("cell1");
1899 std::fs::create_dir(&cell1_path).unwrap();
1900 std::fs::write(cell1_path.join("cpuset.cpus"), "0-7\n").unwrap();
1901
1902 let cell2_path = tmp.path().join("cell2");
1903 std::fs::create_dir(&cell2_path).unwrap();
1904 std::fs::write(cell2_path.join("cpuset.cpus"), "8-11\n").unwrap();
1905
1906 let cell3_path = tmp.path().join("cell3");
1907 std::fs::create_dir(&cell3_path).unwrap();
1908 std::fs::write(cell3_path.join("cpuset.cpus"), "12-15\n").unwrap();
1909
1910 let cpu_to_llc: HashMap<usize, usize> = (0..16usize).map(|cpu| (cpu, cpu / 8)).collect();
1911
1912 let mgr = CellManager::new_with_path_opts(
1913 tmp.path().to_path_buf(),
1914 256,
1915 cpumask_for_range(16),
1916 HashSet::new(),
1917 1,
1918 cpu_to_llc,
1919 )
1920 .unwrap();
1921 let assignments = mgr
1922 .compute_cpu_assignments(false)
1923 .expect("holdout should populate cell 0");
1924
1925 let cell0 = assignments
1926 .iter()
1927 .find(|a| a.id == 0)
1928 .expect("cell 0 present");
1929 assert_eq!(cell0.primary.weight(), 1);
1930 assert!(
1931 cell0.primary.test_cpu(0),
1932 "holdout should take CPU 0 (lowest CPU of the largest cell), got {:?}",
1933 cell0.primary
1934 );
1935 assert!(
1936 mgr.enforced_holdout(),
1937 "stealing a claimed CPU must set enforced_holdout"
1938 );
1939
1940 let total: usize = assignments.iter().map(|a| a.primary.weight()).sum();
1941 assert_eq!(total, 16);
1942
1943 let mut child_weights: Vec<usize> = assignments
1946 .iter()
1947 .filter(|a| a.id != 0)
1948 .map(|a| a.primary.weight())
1949 .collect();
1950 child_weights.sort_unstable();
1951 assert_eq!(child_weights, vec![4, 4, 7]);
1952
1953 for a in assignments.iter().filter(|a| a.id != 0) {
1955 assert!(
1956 !a.primary.test_cpu(0),
1957 "cell {} still holds held-out CPU 0",
1958 a.id
1959 );
1960 }
1961 }
1962
1963 #[test]
1964 fn test_cpu_assignments_holdout_steals_evenly_across_cells() {
1965 let tmp = TempDir::new().unwrap();
1966
1967 for (name, range) in [("cell1", "0-7"), ("cell2", "8-15"), ("cell3", "16-23")] {
1973 let p = tmp.path().join(name);
1974 std::fs::create_dir(&p).unwrap();
1975 std::fs::write(p.join("cpuset.cpus"), format!("{range}\n")).unwrap();
1976 }
1977
1978 let mgr = CellManager::new_with_path_opts(
1979 tmp.path().to_path_buf(),
1980 256,
1981 cpumask_for_range(24),
1982 HashSet::new(),
1983 6,
1984 HashMap::new(),
1985 )
1986 .unwrap();
1987 let assignments = mgr
1988 .compute_cpu_assignments(false)
1989 .expect("holdout should populate cell 0");
1990
1991 let cell0 = assignments
1992 .iter()
1993 .find(|a| a.id == 0)
1994 .expect("cell 0 present");
1995 assert_eq!(
1996 cell0.primary.weight(),
1997 6,
1998 "cell 0 holds the six reserved CPUs"
1999 );
2000
2001 let mut donor_weights: Vec<usize> = assignments
2004 .iter()
2005 .filter(|a| a.id != 0)
2006 .map(|a| a.primary.weight())
2007 .collect();
2008 donor_weights.sort_unstable();
2009 assert_eq!(
2010 donor_weights,
2011 vec![6, 6, 6],
2012 "holdout should steal evenly (two from each cell), got {donor_weights:?}"
2013 );
2014
2015 let count_in =
2017 |lo: usize, hi: usize| (lo..hi).filter(|&c| cell0.primary.test_cpu(c)).count();
2018 assert_eq!(count_in(0, 8), 2, "two CPUs taken from cell1");
2019 assert_eq!(count_in(8, 16), 2, "two CPUs taken from cell2");
2020 assert_eq!(count_in(16, 24), 2, "two CPUs taken from cell3");
2021 }
2022
2023 #[test]
2024 fn test_cpu_assignments_holdout_never_starves_a_child() {
2025 let tmp = TempDir::new().unwrap();
2026
2027 let cell1_path = tmp.path().join("cell1");
2034 std::fs::create_dir(&cell1_path).unwrap();
2035 std::fs::write(cell1_path.join("cpuset.cpus"), "0-1\n").unwrap();
2036
2037 let cell2_path = tmp.path().join("cell2");
2038 std::fs::create_dir(&cell2_path).unwrap();
2039 std::fs::write(cell2_path.join("cpuset.cpus"), "2-3\n").unwrap();
2040
2041 let mgr = CellManager::new_with_path_opts(
2042 tmp.path().to_path_buf(),
2043 256,
2044 cpumask_for_range(4),
2045 HashSet::new(),
2046 3,
2047 HashMap::new(),
2048 )
2049 .unwrap();
2050 let assignments = mgr
2051 .compute_cpu_assignments(false)
2052 .expect("holdout must not starve a child cell");
2053
2054 let total: usize = assignments.iter().map(|a| a.primary.weight()).sum();
2055 assert_eq!(total, 4);
2056
2057 for a in assignments.iter().filter(|a| a.id != 0) {
2059 assert!(
2060 a.primary.weight() >= 1,
2061 "cell {} starved by the holdout: {:?}",
2062 a.id,
2063 a.primary
2064 );
2065 }
2066
2067 let cell0 = assignments
2069 .iter()
2070 .find(|a| a.id == 0)
2071 .expect("cell 0 present");
2072 assert_eq!(
2073 cell0.primary.weight(),
2074 2,
2075 "holdout should cap at total - num_children, got {:?}",
2076 cell0.primary
2077 );
2078 }
2079
2080 #[test]
2081 fn test_cpu_assignments_holdout_overlapping_cpusets_no_starvation() {
2082 let tmp = TempDir::new().unwrap();
2083
2084 let cell1_path = tmp.path().join("cell1");
2093 std::fs::create_dir(&cell1_path).unwrap();
2094 std::fs::write(cell1_path.join("cpuset.cpus"), "0\n").unwrap();
2095
2096 let cell2_path = tmp.path().join("cell2");
2097 std::fs::create_dir(&cell2_path).unwrap();
2098 std::fs::write(cell2_path.join("cpuset.cpus"), "0-1\n").unwrap();
2099
2100 let mgr = CellManager::new_with_path_opts(
2101 tmp.path().to_path_buf(),
2102 256,
2103 cpumask_for_range(5),
2104 HashSet::new(),
2105 4,
2106 HashMap::new(),
2107 )
2108 .unwrap();
2109 let assignments = mgr
2110 .compute_cpu_assignments(false)
2111 .expect("holdout must not starve a cell that shares a contested CPU");
2112 assert!(
2116 !mgr.enforced_holdout(),
2117 "entering the steal loop without taking a claimed CPU must not set \
2118 enforced_holdout"
2119 );
2120
2121 let total: usize = assignments.iter().map(|a| a.primary.weight()).sum();
2122 assert_eq!(total, 5);
2123
2124 for a in assignments.iter().filter(|a| a.id != 0) {
2126 assert!(
2127 a.primary.weight() >= 1,
2128 "cell {} starved by the holdout: {:?}",
2129 a.id,
2130 a.primary
2131 );
2132 }
2133
2134 let cell0 = assignments
2137 .iter()
2138 .find(|a| a.id == 0)
2139 .expect("cell 0 present");
2140 assert_eq!(
2141 cell0.primary.weight(),
2142 3,
2143 "holdout should cap at the 3 unclaimed CPUs, got {:?}",
2144 cell0.primary
2145 );
2146 }
2147
2148 #[test]
2149 fn test_cpu_assignments_single_cpuset() {
2150 let tmp = TempDir::new().unwrap();
2151
2152 let cell1_path = tmp.path().join("cell1");
2154 std::fs::create_dir(&cell1_path).unwrap();
2155 std::fs::write(cell1_path.join("cpuset.cpus"), "0,2,4,6\n").unwrap();
2156
2157 let mgr = CellManager::new_with_path(
2158 tmp.path().to_path_buf(),
2159 256,
2160 cpumask_for_range(8),
2161 HashSet::new(),
2162 )
2163 .unwrap();
2164 let assignments = mgr.compute_cpu_assignments(false).unwrap();
2165
2166 assert_eq!(assignments.len(), 2);
2167
2168 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
2169 let cell1 = assignments.iter().find(|a| a.id != 0).unwrap();
2170
2171 assert_eq!(cell1.primary.weight(), 4);
2173 for cpu in [0, 2, 4, 6] {
2174 assert!(cell1.primary.test_cpu(cpu));
2175 }
2176
2177 assert_eq!(cell0.primary.weight(), 4);
2179 for cpu in [1, 3, 5, 7] {
2180 assert!(cell0.primary.test_cpu(cpu));
2181 }
2182 }
2183
2184 #[test]
2185 fn test_cpuset_parsing_from_file() {
2186 let tmp = TempDir::new().unwrap();
2187
2188 let cell_path = tmp.path().join("cell1");
2190 std::fs::create_dir(&cell_path).unwrap();
2191 std::fs::write(cell_path.join("cpuset.cpus"), "0-3,8-11,16\n").unwrap();
2192
2193 let mgr = CellManager::new_with_path(
2194 tmp.path().to_path_buf(),
2195 256,
2196 cpumask_for_range(32),
2197 HashSet::new(),
2198 )
2199 .unwrap();
2200
2201 let cell_info = mgr.find_cell_by_name("cell1").unwrap();
2203 let cpuset = cell_info.cpuset.as_ref().unwrap();
2204
2205 assert_eq!(cpuset.weight(), 9); for cpu in 0..4 {
2207 assert!(cpuset.test_cpu(cpu));
2208 }
2209 for cpu in 8..12 {
2210 assert!(cpuset.test_cpu(cpu));
2211 }
2212 assert!(cpuset.test_cpu(16));
2213 }
2214
2215 #[test]
2216 fn test_cpu_assignments_mixed_cpuset_and_no_cpuset() {
2217 let tmp = TempDir::new().unwrap();
2218
2219 let cell1_path = tmp.path().join("cell1");
2221 std::fs::create_dir(&cell1_path).unwrap();
2222 std::fs::write(cell1_path.join("cpuset.cpus"), "0-3\n").unwrap();
2223
2224 let cell2_path = tmp.path().join("cell2");
2226 std::fs::create_dir(&cell2_path).unwrap();
2227
2228 let mgr = CellManager::new_with_path(
2229 tmp.path().to_path_buf(),
2230 256,
2231 cpumask_for_range(16),
2232 HashSet::new(),
2233 )
2234 .unwrap();
2235
2236 let cell1_info = mgr.find_cell_by_name("cell1").unwrap();
2238 let cell2_info = mgr.find_cell_by_name("cell2").unwrap();
2239 assert!(cell1_info.cpuset.is_some());
2240 assert!(cell2_info.cpuset.is_none());
2241
2242 let assignments = mgr.compute_cpu_assignments(false).unwrap();
2243
2244 assert_eq!(assignments.len(), 3);
2250
2251 let cell1_assignment = assignments
2253 .iter()
2254 .find(|a| a.id == cell1_info.cell_id)
2255 .unwrap();
2256 assert_eq!(cell1_assignment.primary.weight(), 4);
2257
2258 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
2260 assert_eq!(cell0.primary.weight(), 7);
2261
2262 let cell2_assignment = assignments
2264 .iter()
2265 .find(|a| a.id == cell2_info.cell_id)
2266 .unwrap();
2267 assert_eq!(cell2_assignment.primary.weight(), 5);
2268 }
2269
2270 #[test]
2273 fn test_cpu_assignments_partial_overlap() {
2274 let tmp = TempDir::new().unwrap();
2275
2276 let cell_a_path = tmp.path().join("cell_a");
2278 std::fs::create_dir(&cell_a_path).unwrap();
2279 std::fs::write(cell_a_path.join("cpuset.cpus"), "0-7\n").unwrap();
2280
2281 let cell_b_path = tmp.path().join("cell_b");
2282 std::fs::create_dir(&cell_b_path).unwrap();
2283 std::fs::write(cell_b_path.join("cpuset.cpus"), "4-11\n").unwrap();
2284
2285 let mgr = CellManager::new_with_path(
2286 tmp.path().to_path_buf(),
2287 256,
2288 cpumask_for_range(16),
2289 HashSet::new(),
2290 )
2291 .unwrap();
2292 let assignments = mgr.compute_cpu_assignments(false).unwrap();
2293
2294 let cell_a_info = mgr.find_cell_by_name("cell_a").unwrap();
2295 let cell_b_info = mgr.find_cell_by_name("cell_b").unwrap();
2296
2297 let cell_a = assignments
2298 .iter()
2299 .find(|a| a.id == cell_a_info.cell_id)
2300 .unwrap();
2301 let cell_b = assignments
2302 .iter()
2303 .find(|a| a.id == cell_b_info.cell_id)
2304 .unwrap();
2305 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
2306
2307 assert_eq!(cell_a.primary.weight(), 6);
2311 assert_eq!(cell_b.primary.weight(), 6);
2312 assert_eq!(cell0.primary.weight(), 4);
2313
2314 for cpu in 0..4 {
2316 assert!(
2317 cell_a.primary.test_cpu(cpu),
2318 "CPU {} should be in cell_a",
2319 cpu
2320 );
2321 }
2322 for cpu in 8..12 {
2323 assert!(
2324 cell_b.primary.test_cpu(cpu),
2325 "CPU {} should be in cell_b",
2326 cpu
2327 );
2328 }
2329 for cpu in 12..16 {
2330 assert!(
2331 cell0.primary.test_cpu(cpu),
2332 "CPU {} should be in cell0",
2333 cpu
2334 );
2335 }
2336
2337 let cell_a_contested: Vec<_> = (4..8).filter(|&cpu| cell_a.primary.test_cpu(cpu)).collect();
2339 let cell_b_contested: Vec<_> = (4..8).filter(|&cpu| cell_b.primary.test_cpu(cpu)).collect();
2340 assert_eq!(cell_a_contested.len(), 2);
2341 assert_eq!(cell_b_contested.len(), 2);
2342
2343 for cpu in 0..16 {
2345 let mut count = 0;
2346 if cell_a.primary.test_cpu(cpu) {
2347 count += 1;
2348 }
2349 if cell_b.primary.test_cpu(cpu) {
2350 count += 1;
2351 }
2352 if cell0.primary.test_cpu(cpu) {
2353 count += 1;
2354 }
2355 assert!(count <= 1, "CPU {} is assigned to {} cells", cpu, count);
2356 }
2357 }
2358
2359 #[test]
2360 fn test_cpu_assignments_three_way_overlap() {
2361 let tmp = TempDir::new().unwrap();
2362
2363 let cell_a_path = tmp.path().join("cell_a");
2365 std::fs::create_dir(&cell_a_path).unwrap();
2366 std::fs::write(cell_a_path.join("cpuset.cpus"), "0-5\n").unwrap();
2367
2368 let cell_b_path = tmp.path().join("cell_b");
2369 std::fs::create_dir(&cell_b_path).unwrap();
2370 std::fs::write(cell_b_path.join("cpuset.cpus"), "0-5\n").unwrap();
2371
2372 let cell_c_path = tmp.path().join("cell_c");
2373 std::fs::create_dir(&cell_c_path).unwrap();
2374 std::fs::write(cell_c_path.join("cpuset.cpus"), "0-5\n").unwrap();
2375
2376 let mgr = CellManager::new_with_path(
2377 tmp.path().to_path_buf(),
2378 256,
2379 cpumask_for_range(12),
2380 HashSet::new(),
2381 )
2382 .unwrap();
2383 let assignments = mgr.compute_cpu_assignments(false).unwrap();
2384
2385 let cell_a_info = mgr.find_cell_by_name("cell_a").unwrap();
2386 let cell_b_info = mgr.find_cell_by_name("cell_b").unwrap();
2387 let cell_c_info = mgr.find_cell_by_name("cell_c").unwrap();
2388
2389 let cell_a = assignments
2390 .iter()
2391 .find(|a| a.id == cell_a_info.cell_id)
2392 .unwrap();
2393 let cell_b = assignments
2394 .iter()
2395 .find(|a| a.id == cell_b_info.cell_id)
2396 .unwrap();
2397 let cell_c = assignments
2398 .iter()
2399 .find(|a| a.id == cell_c_info.cell_id)
2400 .unwrap();
2401 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
2402
2403 assert_eq!(cell_a.primary.weight(), 2);
2405 assert_eq!(cell_b.primary.weight(), 2);
2406 assert_eq!(cell_c.primary.weight(), 2);
2407
2408 assert_eq!(cell0.primary.weight(), 6);
2410 for cpu in 6..12 {
2411 assert!(cell0.primary.test_cpu(cpu));
2412 }
2413
2414 let total_contested: usize = (0..6)
2416 .filter(|&cpu| {
2417 cell_a.primary.test_cpu(cpu)
2418 || cell_b.primary.test_cpu(cpu)
2419 || cell_c.primary.test_cpu(cpu)
2420 })
2421 .count();
2422 assert_eq!(total_contested, 6);
2423 }
2424
2425 #[test]
2426 fn test_cpu_assignments_odd_contested_count() {
2427 let tmp = TempDir::new().unwrap();
2428
2429 let cell_a_path = tmp.path().join("cell_a");
2431 std::fs::create_dir(&cell_a_path).unwrap();
2432 std::fs::write(cell_a_path.join("cpuset.cpus"), "0-2\n").unwrap();
2433
2434 let cell_b_path = tmp.path().join("cell_b");
2435 std::fs::create_dir(&cell_b_path).unwrap();
2436 std::fs::write(cell_b_path.join("cpuset.cpus"), "0-2\n").unwrap();
2437
2438 let mgr = CellManager::new_with_path(
2439 tmp.path().to_path_buf(),
2440 256,
2441 cpumask_for_range(8),
2442 HashSet::new(),
2443 )
2444 .unwrap();
2445 let assignments = mgr.compute_cpu_assignments(false).unwrap();
2446
2447 let cell_a_info = mgr.find_cell_by_name("cell_a").unwrap();
2448 let cell_b_info = mgr.find_cell_by_name("cell_b").unwrap();
2449
2450 let cell_a = assignments
2451 .iter()
2452 .find(|a| a.id == cell_a_info.cell_id)
2453 .unwrap();
2454 let cell_b = assignments
2455 .iter()
2456 .find(|a| a.id == cell_b_info.cell_id)
2457 .unwrap();
2458
2459 let total = cell_a.primary.weight() + cell_b.primary.weight();
2462 assert_eq!(total, 3);
2463 assert!(cell_a.primary.weight() >= 1 && cell_a.primary.weight() <= 2);
2464 assert!(cell_b.primary.weight() >= 1 && cell_b.primary.weight() <= 2);
2465
2466 for cpu in 0..3 {
2468 let a_has = cell_a.primary.test_cpu(cpu);
2469 let b_has = cell_b.primary.test_cpu(cpu);
2470 assert!(!(a_has && b_has), "CPU {} assigned to both cells", cpu);
2471 }
2472 }
2473
2474 #[test]
2475 fn test_cpu_assignments_complete_overlap() {
2476 let tmp = TempDir::new().unwrap();
2477
2478 let cell_a_path = tmp.path().join("cell_a");
2480 std::fs::create_dir(&cell_a_path).unwrap();
2481 std::fs::write(cell_a_path.join("cpuset.cpus"), "0-7\n").unwrap();
2482
2483 let cell_b_path = tmp.path().join("cell_b");
2484 std::fs::create_dir(&cell_b_path).unwrap();
2485 std::fs::write(cell_b_path.join("cpuset.cpus"), "0-7\n").unwrap();
2486
2487 let mgr = CellManager::new_with_path(
2488 tmp.path().to_path_buf(),
2489 256,
2490 cpumask_for_range(16),
2491 HashSet::new(),
2492 )
2493 .unwrap();
2494 let assignments = mgr.compute_cpu_assignments(false).unwrap();
2495
2496 let cell_a_info = mgr.find_cell_by_name("cell_a").unwrap();
2497 let cell_b_info = mgr.find_cell_by_name("cell_b").unwrap();
2498
2499 let cell_a = assignments
2500 .iter()
2501 .find(|a| a.id == cell_a_info.cell_id)
2502 .unwrap();
2503 let cell_b = assignments
2504 .iter()
2505 .find(|a| a.id == cell_b_info.cell_id)
2506 .unwrap();
2507 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
2508
2509 assert_eq!(cell_a.primary.weight(), 4);
2511 assert_eq!(cell_b.primary.weight(), 4);
2512
2513 assert_eq!(cell0.primary.weight(), 8);
2515 for cpu in 8..16 {
2516 assert!(cell0.primary.test_cpu(cpu));
2517 }
2518
2519 for cpu in 0..8 {
2521 let a_has = cell_a.primary.test_cpu(cpu);
2522 let b_has = cell_b.primary.test_cpu(cpu);
2523 assert!(!(a_has && b_has), "CPU {} assigned to both cells", cpu);
2524 }
2525 }
2526
2527 #[test]
2528 fn test_cpu_assignments_no_overlap() {
2529 let tmp = TempDir::new().unwrap();
2531
2532 let cell_a_path = tmp.path().join("cell_a");
2533 std::fs::create_dir(&cell_a_path).unwrap();
2534 std::fs::write(cell_a_path.join("cpuset.cpus"), "0-3\n").unwrap();
2535
2536 let cell_b_path = tmp.path().join("cell_b");
2537 std::fs::create_dir(&cell_b_path).unwrap();
2538 std::fs::write(cell_b_path.join("cpuset.cpus"), "4-7\n").unwrap();
2539
2540 let mgr = CellManager::new_with_path(
2541 tmp.path().to_path_buf(),
2542 256,
2543 cpumask_for_range(16),
2544 HashSet::new(),
2545 )
2546 .unwrap();
2547 let assignments = mgr.compute_cpu_assignments(false).unwrap();
2548
2549 let cell_a_info = mgr.find_cell_by_name("cell_a").unwrap();
2550 let cell_b_info = mgr.find_cell_by_name("cell_b").unwrap();
2551
2552 let cell_a = assignments
2553 .iter()
2554 .find(|a| a.id == cell_a_info.cell_id)
2555 .unwrap();
2556 let cell_b = assignments
2557 .iter()
2558 .find(|a| a.id == cell_b_info.cell_id)
2559 .unwrap();
2560 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
2561
2562 assert_eq!(cell_a.primary.weight(), 4);
2564 for cpu in 0..4 {
2565 assert!(cell_a.primary.test_cpu(cpu));
2566 }
2567
2568 assert_eq!(cell_b.primary.weight(), 4);
2569 for cpu in 4..8 {
2570 assert!(cell_b.primary.test_cpu(cpu));
2571 }
2572
2573 assert_eq!(cell0.primary.weight(), 8);
2575 for cpu in 8..16 {
2576 assert!(cell0.primary.test_cpu(cpu));
2577 }
2578 }
2579
2580 #[test]
2583 fn test_format_cell_config_only_cell0() {
2584 let tmp = TempDir::new().unwrap();
2585 let mgr = CellManager::new_with_path(
2586 tmp.path().to_path_buf(),
2587 256,
2588 cpumask_for_range(8),
2589 HashSet::new(),
2590 )
2591 .unwrap();
2592
2593 let mut mask = Cpumask::new();
2594 for cpu in 0..8 {
2595 mask.set_cpu(cpu).unwrap();
2596 }
2597
2598 let assignments = vec![CpuAssignment {
2599 id: 0,
2600 primary: mask,
2601 borrowable: None,
2602 }];
2603 let result = mgr.format_cell_config(&assignments);
2604
2605 assert_eq!(result, "[0: 0-7]");
2606 }
2607
2608 #[test]
2609 fn test_format_cell_config_with_cells() {
2610 let tmp = TempDir::new().unwrap();
2611 std::fs::create_dir(tmp.path().join("container-a")).unwrap();
2612
2613 let mgr = CellManager::new_with_path(
2614 tmp.path().to_path_buf(),
2615 256,
2616 cpumask_for_range(16),
2617 HashSet::new(),
2618 )
2619 .unwrap();
2620
2621 let mut mask0 = Cpumask::new();
2622 for cpu in 0..8 {
2623 mask0.set_cpu(cpu).unwrap();
2624 }
2625
2626 let mut mask1 = Cpumask::new();
2627 for cpu in 8..16 {
2628 mask1.set_cpu(cpu).unwrap();
2629 }
2630
2631 let assignments = vec![
2632 CpuAssignment {
2633 id: 0,
2634 primary: mask0,
2635 borrowable: None,
2636 },
2637 CpuAssignment {
2638 id: 1,
2639 primary: mask1,
2640 borrowable: None,
2641 },
2642 ];
2643 let result = mgr.format_cell_config(&assignments);
2644
2645 assert_eq!(result, "[0: 0-7] [1(container-a): 8-15]");
2646 }
2647
2648 #[test]
2651 fn test_cell_id_exhaustion() {
2652 let tmp = TempDir::new().unwrap();
2653
2654 std::fs::create_dir(tmp.path().join("cell1")).unwrap();
2657 std::fs::create_dir(tmp.path().join("cell2")).unwrap();
2658
2659 let mut mgr = CellManager::new_with_path(
2660 tmp.path().to_path_buf(),
2661 3,
2662 cpumask_for_range(16),
2663 HashSet::new(),
2664 )
2665 .unwrap();
2666 assert_eq!(mgr.cell_count(), 2); std::fs::create_dir(tmp.path().join("cell3")).unwrap();
2670 let result = mgr.reconcile_cells(&NO_SUBS);
2671
2672 assert!(result.is_err());
2673 let err = result.unwrap_err();
2674 let err_chain = format!("{:#}", err);
2675 assert!(
2676 err_chain.contains("Cell ID space exhausted"),
2677 "Expected exhaustion error, got: {}",
2678 err_chain
2679 );
2680 }
2681
2682 #[test]
2683 fn test_cell_id_reuse_prevents_exhaustion() {
2684 let tmp = TempDir::new().unwrap();
2685
2686 std::fs::create_dir(tmp.path().join("cell1")).unwrap();
2688 std::fs::create_dir(tmp.path().join("cell2")).unwrap();
2689
2690 let mut mgr = CellManager::new_with_path(
2691 tmp.path().to_path_buf(),
2692 3,
2693 cpumask_for_range(16),
2694 HashSet::new(),
2695 )
2696 .unwrap();
2697 assert_eq!(mgr.cell_count(), 2);
2698
2699 std::fs::remove_dir(tmp.path().join("cell1")).unwrap();
2701 mgr.reconcile_cells(&NO_SUBS).unwrap();
2702 assert_eq!(mgr.cell_count(), 1);
2703
2704 std::fs::create_dir(tmp.path().join("cell3")).unwrap();
2706 let result = mgr.reconcile_cells(&NO_SUBS);
2707 assert!(result.is_ok());
2708 assert_eq!(mgr.cell_count(), 2);
2709 }
2710
2711 #[test]
2714 fn test_scan_excludes_named_cgroups() {
2715 let tmp = TempDir::new().unwrap();
2716
2717 std::fs::create_dir(tmp.path().join("container-a")).unwrap();
2718 std::fs::create_dir(tmp.path().join("systemd-workaround.service")).unwrap();
2719 std::fs::create_dir(tmp.path().join("container-b")).unwrap();
2720
2721 let exclude = HashSet::from(["systemd-workaround.service".to_string()]);
2722 let mgr = CellManager::new_with_path(
2723 tmp.path().to_path_buf(),
2724 256,
2725 cpumask_for_range(16),
2726 exclude,
2727 )
2728 .unwrap();
2729
2730 assert_eq!(mgr.cell_count(), 2);
2732 assert!(mgr.find_cell_by_name("container-a").is_some());
2733 assert!(mgr.find_cell_by_name("container-b").is_some());
2734 assert!(
2735 mgr.find_cell_by_name("systemd-workaround.service")
2736 .is_none()
2737 );
2738 }
2739
2740 #[test]
2741 fn test_reconcile_excludes_named_cgroups() {
2742 let tmp = TempDir::new().unwrap();
2743
2744 std::fs::create_dir(tmp.path().join("container-a")).unwrap();
2745
2746 let exclude = HashSet::from(["ignored-service".to_string()]);
2747 let mut mgr = CellManager::new_with_path(
2748 tmp.path().to_path_buf(),
2749 256,
2750 cpumask_for_range(16),
2751 exclude,
2752 )
2753 .unwrap();
2754 assert_eq!(mgr.cell_count(), 1);
2755
2756 std::fs::create_dir(tmp.path().join("ignored-service")).unwrap();
2758 let (new_cells, destroyed_cells) = mgr.reconcile_cells(&NO_SUBS).unwrap();
2759 assert_eq!(new_cells.len(), 0);
2760 assert_eq!(destroyed_cells.len(), 0);
2761 assert_eq!(mgr.cell_count(), 1);
2762
2763 std::fs::create_dir(tmp.path().join("container-b")).unwrap();
2765 let (new_cells, destroyed_cells) = mgr.reconcile_cells(&NO_SUBS).unwrap();
2766 assert_eq!(new_cells.len(), 1);
2767 assert_eq!(destroyed_cells.len(), 0);
2768 assert_eq!(mgr.cell_count(), 2);
2769 }
2770
2771 #[test]
2774 fn test_borrowable_cpumasks_basic() {
2775 let tmp = TempDir::new().unwrap();
2776
2777 std::fs::create_dir(tmp.path().join("cell1")).unwrap();
2779 std::fs::create_dir(tmp.path().join("cell2")).unwrap();
2780
2781 let mgr = CellManager::new_with_path(
2782 tmp.path().to_path_buf(),
2783 256,
2784 cpumask_for_range(16),
2785 HashSet::new(),
2786 )
2787 .unwrap();
2788 let assignments = mgr.compute_cpu_assignments(true).unwrap();
2789
2790 for assignment in &assignments {
2792 let borrow_mask = assignment.borrowable.as_ref().unwrap();
2793 let overlap = borrow_mask.and(&assignment.primary);
2795 assert_eq!(
2796 overlap.weight(),
2797 0,
2798 "Cell {} borrowable overlaps with primary",
2799 assignment.id
2800 );
2801 let union = borrow_mask.or(&assignment.primary);
2803 assert_eq!(
2804 union.weight(),
2805 16,
2806 "Cell {} union doesn't cover all CPUs",
2807 assignment.id
2808 );
2809 }
2810 }
2811
2812 #[test]
2813 fn test_borrowable_cpumasks_no_overlap() {
2814 let tmp = TempDir::new().unwrap();
2815
2816 let cell1_path = tmp.path().join("cell1");
2817 std::fs::create_dir(&cell1_path).unwrap();
2818 std::fs::write(cell1_path.join("cpuset.cpus"), "0-3\n").unwrap();
2819
2820 let cell2_path = tmp.path().join("cell2");
2821 std::fs::create_dir(&cell2_path).unwrap();
2822 std::fs::write(cell2_path.join("cpuset.cpus"), "8-11\n").unwrap();
2823
2824 let mgr = CellManager::new_with_path(
2825 tmp.path().to_path_buf(),
2826 256,
2827 cpumask_for_range(16),
2828 HashSet::new(),
2829 )
2830 .unwrap();
2831 let assignments = mgr.compute_cpu_assignments(true).unwrap();
2832
2833 for assignment in &assignments {
2835 let borrow_mask = assignment.borrowable.as_ref().unwrap();
2836 let overlap = borrow_mask.and(&assignment.primary);
2837 assert_eq!(
2838 overlap.weight(),
2839 0,
2840 "Cell {} borrowable overlaps with primary",
2841 assignment.id
2842 );
2843 }
2844 }
2845
2846 #[test]
2847 fn test_borrowable_cpumasks_respects_cpuset() {
2848 let tmp = TempDir::new().unwrap();
2849
2850 let cell1_path = tmp.path().join("cell1");
2852 std::fs::create_dir(&cell1_path).unwrap();
2853 std::fs::write(cell1_path.join("cpuset.cpus"), "0-7\n").unwrap();
2854
2855 let cell2_path = tmp.path().join("cell2");
2856 std::fs::create_dir(&cell2_path).unwrap();
2857 std::fs::write(cell2_path.join("cpuset.cpus"), "8-15\n").unwrap();
2858
2859 let mgr = CellManager::new_with_path(
2860 tmp.path().to_path_buf(),
2861 256,
2862 cpumask_for_range(32),
2863 HashSet::new(),
2864 )
2865 .unwrap();
2866 let assignments = mgr.compute_cpu_assignments(true).unwrap();
2867
2868 let cell1_info = mgr.find_cell_by_name("cell1").unwrap();
2869 let cell2_info = mgr.find_cell_by_name("cell2").unwrap();
2870
2871 let cell1_assignment = assignments
2875 .iter()
2876 .find(|a| a.id == cell1_info.cell_id)
2877 .unwrap();
2878 let cell1_borrow = cell1_assignment.borrowable.as_ref().unwrap();
2879 for cpu in 8..32 {
2881 assert!(
2882 !cell1_borrow.test_cpu(cpu),
2883 "Cell 1 borrowable should not include CPU {} (outside cpuset)",
2884 cpu
2885 );
2886 }
2887
2888 let cell2_assignment = assignments
2890 .iter()
2891 .find(|a| a.id == cell2_info.cell_id)
2892 .unwrap();
2893 let cell2_borrow = cell2_assignment.borrowable.as_ref().unwrap();
2894 for cpu in 0..8 {
2895 assert!(
2896 !cell2_borrow.test_cpu(cpu),
2897 "Cell 2 borrowable should not include CPU {} (outside cpuset)",
2898 cpu
2899 );
2900 }
2901 for cpu in 16..32 {
2902 assert!(
2903 !cell2_borrow.test_cpu(cpu),
2904 "Cell 2 borrowable should not include CPU {} (outside cpuset)",
2905 cpu
2906 );
2907 }
2908 }
2909
2910 #[test]
2913 fn test_demand_cpu_assignments_all_idle() {
2914 let tmp = TempDir::new().unwrap();
2915 std::fs::create_dir(tmp.path().join("cell1")).unwrap();
2916 std::fs::create_dir(tmp.path().join("cell2")).unwrap();
2917
2918 let mgr = CellManager::new_with_path(
2919 tmp.path().to_path_buf(),
2920 256,
2921 cpumask_for_range(12),
2922 HashSet::new(),
2923 )
2924 .unwrap();
2925
2926 let demands: HashMap<u32, f64> = [(0, 0.0), (1, 0.0), (2, 0.0)].into();
2928 let assignments = mgr.compute_demand_cpu_assignments(&demands, false).unwrap();
2929
2930 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
2932 let cell1_info = mgr.find_cell_by_name("cell1").unwrap();
2933 let cell2_info = mgr.find_cell_by_name("cell2").unwrap();
2934 let c1 = assignments
2935 .iter()
2936 .find(|a| a.id == cell1_info.cell_id)
2937 .unwrap();
2938 let c2 = assignments
2939 .iter()
2940 .find(|a| a.id == cell2_info.cell_id)
2941 .unwrap();
2942
2943 assert_eq!(cell0.primary.weight(), 4);
2944 assert_eq!(c1.primary.weight(), 4);
2945 assert_eq!(c2.primary.weight(), 4);
2946 }
2947
2948 #[test]
2949 fn test_demand_cpu_assignments_uneven_demand() {
2950 let tmp = TempDir::new().unwrap();
2951 std::fs::create_dir(tmp.path().join("cell1")).unwrap();
2952 std::fs::create_dir(tmp.path().join("cell2")).unwrap();
2953
2954 let mgr = CellManager::new_with_path(
2955 tmp.path().to_path_buf(),
2956 256,
2957 cpumask_for_range(12),
2958 HashSet::new(),
2959 )
2960 .unwrap();
2961
2962 let cell1_info = mgr.find_cell_by_name("cell1").unwrap();
2963 let cell2_info = mgr.find_cell_by_name("cell2").unwrap();
2964
2965 let demands: HashMap<u32, f64> = [
2967 (0, 50.0),
2968 (cell1_info.cell_id, 100.0),
2969 (cell2_info.cell_id, 1.0),
2970 ]
2971 .into();
2972 let assignments = mgr.compute_demand_cpu_assignments(&demands, false).unwrap();
2973
2974 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
2975 let c1 = assignments
2976 .iter()
2977 .find(|a| a.id == cell1_info.cell_id)
2978 .unwrap();
2979 let c2 = assignments
2980 .iter()
2981 .find(|a| a.id == cell2_info.cell_id)
2982 .unwrap();
2983
2984 assert!(
2986 c1.primary.weight() > c2.primary.weight(),
2987 "Busy cell ({}) should have more CPUs than idle cell ({})",
2988 c1.primary.weight(),
2989 c2.primary.weight()
2990 );
2991 assert!(c2.primary.weight() >= 1);
2993 assert!(cell0.primary.weight() >= 1);
2994 assert_eq!(
2996 cell0.primary.weight() + c1.primary.weight() + c2.primary.weight(),
2997 12
2998 );
2999 }
3000
3001 #[test]
3002 fn test_demand_cpu_assignments_with_cpusets() {
3003 let tmp = TempDir::new().unwrap();
3004
3005 let cell_a_path = tmp.path().join("cell_a");
3007 std::fs::create_dir(&cell_a_path).unwrap();
3008 std::fs::write(cell_a_path.join("cpuset.cpus"), "0-7\n").unwrap();
3009
3010 let cell_b_path = tmp.path().join("cell_b");
3011 std::fs::create_dir(&cell_b_path).unwrap();
3012 std::fs::write(cell_b_path.join("cpuset.cpus"), "4-11\n").unwrap();
3013
3014 let mgr = CellManager::new_with_path(
3015 tmp.path().to_path_buf(),
3016 256,
3017 cpumask_for_range(16),
3018 HashSet::new(),
3019 )
3020 .unwrap();
3021
3022 let cell_a_info = mgr.find_cell_by_name("cell_a").unwrap();
3023 let cell_b_info = mgr.find_cell_by_name("cell_b").unwrap();
3024
3025 let demands: HashMap<u32, f64> = [
3027 (0, 10.0),
3028 (cell_a_info.cell_id, 90.0),
3029 (cell_b_info.cell_id, 10.0),
3030 ]
3031 .into();
3032 let assignments = mgr.compute_demand_cpu_assignments(&demands, false).unwrap();
3033
3034 let cell_a = assignments
3035 .iter()
3036 .find(|a| a.id == cell_a_info.cell_id)
3037 .unwrap();
3038 let cell_b = assignments
3039 .iter()
3040 .find(|a| a.id == cell_b_info.cell_id)
3041 .unwrap();
3042
3043 assert!(
3047 cell_a.primary.weight() > cell_b.primary.weight(),
3048 "Cell A ({}) should have more CPUs than Cell B ({})",
3049 cell_a.primary.weight(),
3050 cell_b.primary.weight()
3051 );
3052
3053 assert!(cell_a.primary.weight() >= 4);
3055 assert!(cell_b.primary.weight() >= 4);
3056 }
3057
3058 #[test]
3059 fn test_demand_cpu_assignments_idle_cell_gets_floor() {
3060 let tmp = TempDir::new().unwrap();
3061 std::fs::create_dir(tmp.path().join("cell1")).unwrap();
3062 std::fs::create_dir(tmp.path().join("cell2")).unwrap();
3063
3064 let mgr = CellManager::new_with_path(
3065 tmp.path().to_path_buf(),
3066 256,
3067 cpumask_for_range(12),
3068 HashSet::new(),
3069 )
3070 .unwrap();
3071
3072 let cell1_info = mgr.find_cell_by_name("cell1").unwrap();
3073 let cell2_info = mgr.find_cell_by_name("cell2").unwrap();
3074
3075 let demands: HashMap<u32, f64> = [
3077 (0, 50.0),
3078 (cell1_info.cell_id, 100.0),
3079 (cell2_info.cell_id, 0.0),
3080 ]
3081 .into();
3082 let assignments = mgr.compute_demand_cpu_assignments(&demands, false).unwrap();
3083
3084 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
3085 let c1 = assignments
3086 .iter()
3087 .find(|a| a.id == cell1_info.cell_id)
3088 .unwrap();
3089 let c2 = assignments
3090 .iter()
3091 .find(|a| a.id == cell2_info.cell_id)
3092 .unwrap();
3093
3094 assert_eq!(
3097 c2.primary.weight(),
3098 1,
3099 "Idle cell should get minimum target of 1 CPU"
3100 );
3101 assert!(c1.primary.weight() > cell0.primary.weight());
3103 assert!(c1.primary.weight() > c2.primary.weight());
3104 assert_eq!(
3106 cell0.primary.weight() + c1.primary.weight() + c2.primary.weight(),
3107 12
3108 );
3109 }
3110
3111 #[test]
3112 fn test_demand_cpu_assignments_negative_weight_errors() {
3113 let tmp = TempDir::new().unwrap();
3114 std::fs::create_dir(tmp.path().join("cell1")).unwrap();
3115
3116 let mgr = CellManager::new_with_path(
3117 tmp.path().to_path_buf(),
3118 256,
3119 cpumask_for_range(8),
3120 HashSet::new(),
3121 )
3122 .unwrap();
3123
3124 let cell1_info = mgr.find_cell_by_name("cell1").unwrap();
3125
3126 let demands: HashMap<u32, f64> = [(0, 50.0), (cell1_info.cell_id, -10.0)].into();
3127 let result = mgr.compute_demand_cpu_assignments(&demands, false);
3128 assert!(result.is_err(), "Negative weight should be rejected");
3129 assert!(
3130 result
3131 .unwrap_err()
3132 .to_string()
3133 .contains("negative demand weight"),
3134 "Error message should mention negative weight"
3135 );
3136 }
3137
3138 #[test]
3141 fn test_deficit_distribution_with_cpusets() {
3142 let tmp = TempDir::new().unwrap();
3146
3147 let cell_a_path = tmp.path().join("cell_a");
3149 std::fs::create_dir(&cell_a_path).unwrap();
3150 std::fs::write(cell_a_path.join("cpuset.cpus"), "0-9\n").unwrap();
3151
3152 let cell_b_path = tmp.path().join("cell_b");
3154 std::fs::create_dir(&cell_b_path).unwrap();
3155 std::fs::write(cell_b_path.join("cpuset.cpus"), "6-11\n").unwrap();
3156
3157 let mgr = CellManager::new_with_path(
3159 tmp.path().to_path_buf(),
3160 256,
3161 cpumask_for_range(16),
3162 HashSet::new(),
3163 )
3164 .unwrap();
3165
3166 let cell_a_info = mgr.find_cell_by_name("cell_a").unwrap();
3167 let cell_b_info = mgr.find_cell_by_name("cell_b").unwrap();
3168
3169 let demands: HashMap<u32, f64> = [
3171 (0, 10.0),
3172 (cell_a_info.cell_id, 90.0),
3173 (cell_b_info.cell_id, 10.0),
3174 ]
3175 .into();
3176 let assignments = mgr.compute_demand_cpu_assignments(&demands, false).unwrap();
3177
3178 let cell_a = assignments
3179 .iter()
3180 .find(|a| a.id == cell_a_info.cell_id)
3181 .unwrap();
3182 let cell_b = assignments
3183 .iter()
3184 .find(|a| a.id == cell_b_info.cell_id)
3185 .unwrap();
3186 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
3187
3188 assert_eq!(
3190 cell_a.primary.weight() + cell_b.primary.weight() + cell0.primary.weight(),
3191 16,
3192 );
3193
3194 assert!(
3196 cell_a.primary.weight() > cell_b.primary.weight(),
3197 "cell_a ({}) should have more CPUs than cell_b ({})",
3198 cell_a.primary.weight(),
3199 cell_b.primary.weight()
3200 );
3201
3202 assert!(cell_a.primary.weight() >= 1);
3204 assert!(cell_b.primary.weight() >= 1);
3205 assert!(cell0.primary.weight() >= 1);
3206 }
3207
3208 #[test]
3209 fn test_deficit_distribution_equal_weight_with_exclusive() {
3210 let tmp = TempDir::new().unwrap();
3213
3214 let cell1_path = tmp.path().join("cell1");
3216 std::fs::create_dir(&cell1_path).unwrap();
3217 std::fs::write(cell1_path.join("cpuset.cpus"), "0-7\n").unwrap();
3218
3219 std::fs::create_dir(tmp.path().join("cell2")).unwrap();
3221
3222 let mgr = CellManager::new_with_path(
3224 tmp.path().to_path_buf(),
3225 256,
3226 cpumask_for_range(16),
3227 HashSet::new(),
3228 )
3229 .unwrap();
3230
3231 let cell1_info = mgr.find_cell_by_name("cell1").unwrap();
3232 let cell2_info = mgr.find_cell_by_name("cell2").unwrap();
3233
3234 let assignments = mgr.compute_cpu_assignments(false).unwrap();
3235
3236 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
3237 let cell1 = assignments
3238 .iter()
3239 .find(|a| a.id == cell1_info.cell_id)
3240 .unwrap();
3241 let cell2 = assignments
3242 .iter()
3243 .find(|a| a.id == cell2_info.cell_id)
3244 .unwrap();
3245
3246 assert_eq!(cell1.primary.weight(), 8); assert_eq!(
3256 cell0.primary.weight() + cell1.primary.weight() + cell2.primary.weight(),
3257 16,
3258 );
3259
3260 assert!(
3262 cell0.primary.weight() >= cell2.primary.weight(),
3263 "cell0 ({}) should have >= CPUs than cell2 ({})",
3264 cell0.primary.weight(),
3265 cell2.primary.weight()
3266 );
3267 }
3268
3269 #[test]
3270 fn test_deficit_all_cells_exceed_target() {
3271 let tmp = TempDir::new().unwrap();
3274
3275 let cell_a_path = tmp.path().join("cell_a");
3277 std::fs::create_dir(&cell_a_path).unwrap();
3278 std::fs::write(cell_a_path.join("cpuset.cpus"), "0-9\n").unwrap();
3279
3280 let cell_b_path = tmp.path().join("cell_b");
3282 std::fs::create_dir(&cell_b_path).unwrap();
3283 std::fs::write(cell_b_path.join("cpuset.cpus"), "8-13\n").unwrap();
3284
3285 let mgr = CellManager::new_with_path(
3292 tmp.path().to_path_buf(),
3293 256,
3294 cpumask_for_range(20),
3295 HashSet::new(),
3296 )
3297 .unwrap();
3298
3299 let cell_a_info = mgr.find_cell_by_name("cell_a").unwrap();
3300 let cell_b_info = mgr.find_cell_by_name("cell_b").unwrap();
3301
3302 let assignments = mgr.compute_cpu_assignments(false).unwrap();
3303
3304 let cell_a = assignments
3305 .iter()
3306 .find(|a| a.id == cell_a_info.cell_id)
3307 .unwrap();
3308 let cell_b = assignments
3309 .iter()
3310 .find(|a| a.id == cell_b_info.cell_id)
3311 .unwrap();
3312 let cell0 = assignments.iter().find(|a| a.id == 0).unwrap();
3313
3314 let cell_a_contested: Vec<_> = (8..10)
3317 .filter(|&cpu| cell_a.primary.test_cpu(cpu))
3318 .collect();
3319 let cell_b_contested: Vec<_> = (8..10)
3320 .filter(|&cpu| cell_b.primary.test_cpu(cpu))
3321 .collect();
3322 assert_eq!(
3323 cell_a_contested.len(),
3324 0,
3325 "cell_a should get 0 contested CPUs (exceeded target)"
3326 );
3327 assert_eq!(
3328 cell_b_contested.len(),
3329 2,
3330 "cell_b should get all 2 contested CPUs (has deficit)"
3331 );
3332
3333 assert_eq!(
3335 cell_a.primary.weight() + cell_b.primary.weight() + cell0.primary.weight(),
3336 20,
3337 );
3338
3339 assert!(cell0.primary.weight() >= 1);
3341 assert!(cell_a.primary.weight() >= 1);
3342 assert!(cell_b.primary.weight() >= 1);
3343 }
3344
3345 #[test]
3348 fn test_distribute_proportional_basic() {
3349 let cpus: Vec<usize> = (0..8).collect();
3350 let recipients = vec![(0, 3.0), (1, 1.0)];
3351 let result = CpuManager::distribute_cpus_proportional(&cpus, &recipients).unwrap();
3352
3353 assert_eq!(result.get(&0).unwrap().len(), 6);
3355 assert_eq!(result.get(&1).unwrap().len(), 2);
3356 }
3357
3358 #[test]
3359 fn test_distribute_proportional_zero_weight_gets_nothing() {
3360 let cpus: Vec<usize> = (0..6).collect();
3361 let recipients = vec![(0, 1.0), (1, 0.0)];
3362 let result = CpuManager::distribute_cpus_proportional(&cpus, &recipients).unwrap();
3363
3364 assert_eq!(result.get(&0).unwrap().len(), 6);
3366 assert!(result.get(&1).is_none() || result.get(&1).unwrap().is_empty());
3367 }
3368
3369 #[test]
3370 fn test_distribute_proportional_all_zero_fallback() {
3371 let cpus: Vec<usize> = (0..6).collect();
3372 let recipients = vec![(0, 0.0), (1, 0.0)];
3373 let result = CpuManager::distribute_cpus_proportional(&cpus, &recipients).unwrap();
3374
3375 assert_eq!(result.get(&0).unwrap().len(), 3);
3377 assert_eq!(result.get(&1).unwrap().len(), 3);
3378 }
3379
3380 #[test]
3381 fn test_distribute_proportional_skewed_weights_floor_guarantee() {
3382 let cpus: Vec<usize> = (0..38).collect();
3385 let recipients = vec![(2, 1.0), (3, 100.0)];
3386 let result = CpuManager::distribute_cpus_proportional(&cpus, &recipients).unwrap();
3387
3388 let cell2_count = result.get(&2).map_or(0, |v| v.len());
3390 let cell3_count = result.get(&3).map_or(0, |v| v.len());
3391 assert!(
3392 cell2_count >= 1,
3393 "cell 2 must get at least 1 CPU, got {}",
3394 cell2_count,
3395 );
3396 assert!(
3397 cell3_count >= 1,
3398 "cell 3 must get at least 1 CPU, got {}",
3399 cell3_count,
3400 );
3401 assert_eq!(cell2_count + cell3_count, 38, "all CPUs must be assigned");
3402 }
3403
3404 #[test]
3405 fn test_distribute_proportional_overlapping_cpusets_no_starvation() {
3406 let cpus: Vec<usize> = (24..62).collect(); let recipients = vec![(2, 1.0), (3, 87.0)];
3411 let result = CpuManager::distribute_cpus_proportional(&cpus, &recipients).unwrap();
3412
3413 let cell2_count = result.get(&2).map_or(0, |v| v.len());
3414 let cell3_count = result.get(&3).map_or(0, |v| v.len());
3415 assert!(
3416 cell2_count >= 1,
3417 "cell 2 must not be starved, got {} CPUs",
3418 cell2_count,
3419 );
3420 assert!(
3421 cell3_count >= 1,
3422 "cell 3 must not be starved, got {} CPUs",
3423 cell3_count,
3424 );
3425 assert_eq!(cell2_count + cell3_count, 38);
3426 }
3427
3428 #[test]
3431 fn test_compute_targets_equal_weight() {
3432 let targets = CpuManager::compute_targets(12, &[(0, 1.0), (1, 1.0), (2, 1.0)]).unwrap();
3433 assert_eq!(*targets.get(&0).unwrap(), 4);
3434 assert_eq!(*targets.get(&1).unwrap(), 4);
3435 assert_eq!(*targets.get(&2).unwrap(), 4);
3436 }
3437
3438 #[test]
3439 fn test_compute_targets_with_remainder() {
3440 let targets = CpuManager::compute_targets(10, &[(0, 1.0), (1, 1.0), (2, 1.0)]).unwrap();
3441 let total: usize = targets.values().sum();
3443 assert_eq!(total, 10);
3444 for &count in targets.values() {
3445 assert!((3..=4).contains(&count));
3446 }
3447 }
3448
3449 #[test]
3452 fn test_cpu_manager_non_root_domain_unpinned_invariants() {
3453 let domain = cpumask_from_cpulist(32, "8-15");
3454 let recipients = vec![
3455 CpuRecipient {
3456 id: 10,
3457 weight: 1.0,
3458 allowed: domain.clone(),
3459 claimed: None,
3460 minimum: CpuMinimum::default(),
3461 },
3462 CpuRecipient {
3463 id: 11,
3464 weight: 1.0,
3465 allowed: domain.clone(),
3466 claimed: None,
3467 minimum: CpuMinimum::default(),
3468 },
3469 CpuRecipient {
3470 id: 12,
3471 weight: 2.0,
3472 allowed: domain.clone(),
3473 claimed: None,
3474 minimum: CpuMinimum::default(),
3475 },
3476 ];
3477
3478 let assignments = CpuManager::new(&domain)
3479 .compute_assignments(&recipients, false)
3480 .unwrap();
3481
3482 assert_eq!(assignments.len(), 3);
3483 assert_eq!(find_assignment(&assignments, 10).primary.weight(), 2);
3484 assert_eq!(find_assignment(&assignments, 11).primary.weight(), 2);
3485 assert_eq!(find_assignment(&assignments, 12).primary.weight(), 4);
3486
3487 let total: usize = assignments.iter().map(|a| a.primary.weight()).sum();
3488 assert_eq!(total, domain.weight());
3489
3490 for assignment in &assignments {
3491 assert!(
3492 assignment.borrowable.is_none(),
3493 "borrowable should stay disabled in this case"
3494 );
3495 for cpu in assignment.primary.iter() {
3496 assert!(domain.test_cpu(cpu), "CPU {} escaped the domain", cpu);
3497 }
3498 }
3499
3500 for i in 0..assignments.len() {
3501 for j in (i + 1)..assignments.len() {
3502 let overlap = assignments[i].primary.and(&assignments[j].primary);
3503 assert_eq!(
3504 overlap.weight(),
3505 0,
3506 "Recipients {} and {} overlap",
3507 assignments[i].id,
3508 assignments[j].id
3509 );
3510 }
3511 }
3512
3513 let mut union = Cpumask::new();
3514 for assignment in &assignments {
3515 union = union.or(&assignment.primary);
3516 }
3517 assert_eq!(union, domain, "Primary masks should cover the whole domain");
3518 }
3519
3520 #[test]
3521 fn test_cpu_manager_non_contiguous_domain_borrowable_invariants() {
3522 let domain = cpumask_from_cpulist(32, "8-9,12,14-15,20-21,26");
3523 let allowed_1 = cpumask_from_cpulist(32, "0-1,8-9,12");
3524 let allowed_2 = cpumask_from_cpulist(32, "9,14-15,20-22");
3525 let recipients = vec![
3526 CpuRecipient {
3527 id: 1,
3528 weight: 1.0,
3529 allowed: allowed_1.clone(),
3530 claimed: Some(allowed_1.clone()),
3531 minimum: CpuMinimum::default(),
3532 },
3533 CpuRecipient {
3534 id: 2,
3535 weight: 2.0,
3536 allowed: allowed_2.clone(),
3537 claimed: Some(allowed_2.clone()),
3538 minimum: CpuMinimum::default(),
3539 },
3540 CpuRecipient {
3541 id: 3,
3542 weight: 1.0,
3543 allowed: domain.clone(),
3544 claimed: None,
3545 minimum: CpuMinimum::default(),
3546 },
3547 ];
3548
3549 let assignments = CpuManager::new(&domain)
3550 .compute_assignments(&recipients, true)
3551 .unwrap();
3552
3553 assert_eq!(assignments.len(), 3);
3554
3555 for assignment in &assignments {
3556 for cpu in assignment.primary.iter() {
3557 assert!(
3558 domain.test_cpu(cpu),
3559 "Primary CPU {} escaped the domain",
3560 cpu
3561 );
3562 }
3563 let borrowable = assignment.borrowable.as_ref().unwrap();
3564 for cpu in borrowable.iter() {
3565 assert!(
3566 domain.test_cpu(cpu),
3567 "Borrowable CPU {} escaped the domain",
3568 cpu
3569 );
3570 }
3571 assert_eq!(
3572 assignment.primary.and(borrowable).weight(),
3573 0,
3574 "Primary and borrowable overlap for recipient {}",
3575 assignment.id
3576 );
3577 }
3578
3579 let recipient_1 = find_assignment(&assignments, 1);
3580 let recipient_2 = find_assignment(&assignments, 2);
3581 let recipient_3 = find_assignment(&assignments, 3);
3582
3583 for cpu in recipient_1.primary.iter() {
3584 assert!(
3585 allowed_1.test_cpu(cpu),
3586 "Recipient 1 got CPU {} outside allowed",
3587 cpu
3588 );
3589 }
3590 for cpu in recipient_2.primary.iter() {
3591 assert!(
3592 allowed_2.test_cpu(cpu),
3593 "Recipient 2 got CPU {} outside allowed",
3594 cpu
3595 );
3596 }
3597
3598 let allowed_1_in_domain = domain.and(&allowed_1);
3599 let allowed_2_in_domain = domain.and(&allowed_2);
3600 let recipient_1_union = recipient_1
3601 .primary
3602 .or(recipient_1.borrowable.as_ref().unwrap());
3603 let recipient_2_union = recipient_2
3604 .primary
3605 .or(recipient_2.borrowable.as_ref().unwrap());
3606 assert_eq!(recipient_1_union, allowed_1_in_domain);
3607 assert_eq!(recipient_2_union, allowed_2_in_domain);
3608
3609 let recipient_3_union = recipient_3
3610 .primary
3611 .or(recipient_3.borrowable.as_ref().unwrap());
3612 assert_eq!(recipient_3_union, domain);
3613 assert_eq!(recipient_3.primary.weight(), 1);
3614 assert!(!recipient_3.primary.test_cpu(8));
3615 assert!(!recipient_3.primary.test_cpu(9));
3616 assert!(recipient_3.primary.test_cpu(26));
3617
3618 let mut union = Cpumask::new();
3619 for assignment in &assignments {
3620 union = union.or(&assignment.primary);
3621 }
3622 assert_eq!(union, domain, "Primary masks should cover the whole domain");
3623 }
3624
3625 #[test]
3628 fn test_symmetric_pairwise_overlap_produces_equal_cells() {
3629 let tmp = TempDir::new().unwrap();
3630
3631 let cpusets = [
3641 ("cell_a", "0-3,20-27"),
3642 ("cell_b", "4-7,20-21,28-33"),
3643 ("cell_c", "8-11,22-23,28-29,34-37"),
3644 ("cell_d", "12-15,24-25,30-31,34-35,38-39"),
3645 ("cell_e", "16-19,26-27,32-33,36-39"),
3646 ];
3647 for (name, cpus) in &cpusets {
3648 let p = tmp.path().join(name);
3649 std::fs::create_dir(&p).unwrap();
3650 std::fs::write(p.join("cpuset.cpus"), format!("{cpus}\n")).unwrap();
3651 }
3652
3653 let mgr = CellManager::new_with_path(
3654 tmp.path().to_path_buf(),
3655 256,
3656 cpumask_for_range(56),
3657 HashSet::new(),
3658 )
3659 .unwrap();
3660
3661 let assignments = mgr.compute_cpu_assignments(false).unwrap();
3662
3663 let workload: Vec<_> = assignments.iter().filter(|a| a.id != 0).collect();
3664 assert_eq!(workload.len(), 5, "Expected 5 workload cells");
3665
3666 let counts: Vec<(u32, usize)> = workload
3668 .iter()
3669 .map(|a| (a.id, a.primary.weight()))
3670 .collect();
3671 for &(cell_id, count) in &counts {
3672 assert_eq!(
3673 count, counts[0].1,
3674 "Cell {} has {} CPUs, expected {} — symmetric inputs \
3675 should produce equal cell sizes. All counts: {:?}",
3676 cell_id, count, counts[0].1, counts,
3677 );
3678 }
3679
3680 for i in 0..assignments.len() {
3682 for j in (i + 1)..assignments.len() {
3683 for cpu in 0..56 {
3684 assert!(
3685 !(assignments[i].primary.test_cpu(cpu)
3686 && assignments[j].primary.test_cpu(cpu)),
3687 "CPU {} assigned to both cell {} and cell {}",
3688 cpu,
3689 assignments[i].id,
3690 assignments[j].id,
3691 );
3692 }
3693 }
3694 }
3695
3696 let total: usize = assignments.iter().map(|a| a.primary.weight()).sum();
3698 assert_eq!(total, 56, "All CPUs must be assigned");
3699 }
3700}