psx_residency_sim/
churn.rs1use alloc::vec::Vec;
22use psx_residency::{PagePool, Placement};
23
24#[derive(Copy, Clone, Debug)]
26pub struct ChurnConfig {
27 pub pages: u32,
29 pub run_pages: (u32, u32),
31 pub operations: u32,
34 pub placement: Placement,
36 pub compact: bool,
39 pub seed: u64,
41}
42
43#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
45pub struct ChurnReport {
46 pub allocations: u32,
48 pub fragmentation_failures: u32,
51 pub forced_frees: u32,
53 pub pages_moved: u64,
55 pub mean_fragmentation_permille: u32,
57 pub max_fragmentation_permille: u32,
59 pub mean_utilization_permille: u32,
61}
62
63pub fn churn_report(config: ChurnConfig) -> ChurnReport {
65 let mut pool = PagePool::<128>::new(0, config.pages, config.placement);
66 let mut live = Vec::new();
67 let mut rng = config.seed | 1;
68 let mut next = move || {
69 rng ^= rng << 13;
70 rng ^= rng >> 7;
71 rng ^= rng << 17;
72 (rng >> 11) as u32
73 };
74 let (low, high) = config.run_pages;
75 let mut report = ChurnReport::default();
76 let mut fragmentation_sum = 0u64;
77 let mut utilization_sum = 0u64;
78 for _ in 0..config.operations {
79 let pages = low + next() % (high - low + 1);
80 report.allocations += 1;
81 let mut counted = false;
82 loop {
83 if pool.live_count() < 128 {
84 if let Some((handle, _)) = pool.allocate(pages) {
85 live.push(handle);
86 break;
87 }
88 }
89 if config.compact {
90 if let Some(moved) = pool.compact_step(|_, _| {}) {
91 report.pages_moved += moved.from.page_count as u64;
92 continue;
93 }
94 }
95 if !counted && pool.free_pages() >= pages {
96 report.fragmentation_failures += 1;
97 counted = true;
98 }
99 let victim = next() as usize % live.len();
101 let handle = live.swap_remove(victim);
102 pool.free(handle);
103 report.forced_frees += 1;
104 }
105 let shape = pool.fragmentation();
106 fragmentation_sum += shape.permille() as u64;
107 report.max_fragmentation_permille = report.max_fragmentation_permille.max(shape.permille());
108 utilization_sum += pool.used_pages() as u64 * 1000 / config.pages as u64;
109 }
110 let operations = config.operations.max(1) as u64;
111 report.mean_fragmentation_permille = (fragmentation_sum / operations) as u32;
112 report.mean_utilization_permille = (utilization_sum / operations) as u32;
113 report
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 fn run(placement: Placement, compact: bool, seed: u64) -> ChurnReport {
121 churn_report(ChurnConfig {
122 pages: 256,
123 run_pages: (2, 40),
124 operations: 4_000,
125 placement,
126 compact,
127 seed,
128 })
129 }
130
131 #[test]
132 #[cfg_attr(miri, ignore)] fn churn_is_deterministic() {
134 assert_eq!(
135 run(Placement::BestFit, false, 7),
136 run(Placement::BestFit, false, 7)
137 );
138 }
139
140 #[test]
141 #[cfg_attr(miri, ignore)] fn compaction_removes_fragmentation_failures() {
143 for seed in 1..=5 {
144 let without = run(Placement::FirstFit, false, seed);
145 let with = run(Placement::FirstFit, true, seed);
146 assert!(without.fragmentation_failures > 0, "{without:?}");
147 assert_eq!(with.fragmentation_failures, 0, "{with:?}");
150 assert!(with.mean_utilization_permille >= without.mean_utilization_permille);
151 assert!(with.pages_moved > 0);
152 }
153 }
154
155 #[test]
156 #[cfg_attr(miri, ignore)] fn best_fit_and_first_fit_both_keep_the_pool_busy() {
158 for placement in [Placement::FirstFit, Placement::BestFit] {
159 let report = run(placement, false, 3);
160 assert!(report.mean_utilization_permille > 500, "{report:?}");
161 assert!(report.max_fragmentation_permille <= 1000);
162 }
163 }
164
165 #[test]
168 #[cfg_attr(miri, ignore)] fn churn_table() {
170 for (name, placement, compact) in [
171 ("first-fit", Placement::FirstFit, false),
172 ("best-fit", Placement::BestFit, false),
173 ("first-fit + compaction", Placement::FirstFit, true),
174 ("best-fit + compaction", Placement::BestFit, true),
175 ] {
176 let mut total = ChurnReport::default();
177 let seeds = 8u32;
178 for seed in 1..=seeds as u64 {
179 let report = run(placement, compact, seed);
180 total.fragmentation_failures += report.fragmentation_failures;
181 total.forced_frees += report.forced_frees;
182 total.pages_moved += report.pages_moved;
183 total.mean_fragmentation_permille += report.mean_fragmentation_permille;
184 total.max_fragmentation_permille = total
185 .max_fragmentation_permille
186 .max(report.max_fragmentation_permille);
187 total.mean_utilization_permille += report.mean_utilization_permille;
188 }
189 std::println!(
190 "{name:24} frag-fail {:5} forced-free {:6} moved {:8} mean-frag {:4} max-frag {:4} mean-util {:4}",
191 total.fragmentation_failures / seeds,
192 total.forced_frees / seeds,
193 total.pages_moved / seeds as u64,
194 total.mean_fragmentation_permille / seeds,
195 total.max_fragmentation_permille,
196 total.mean_utilization_permille / seeds,
197 );
198 }
199 }
200}