foyer_storage/engine/block/
eviction.rs1use std::{
16 collections::{HashSet, VecDeque},
17 fmt::Debug,
18 sync::atomic::Ordering,
19};
20
21use foyer_common::strict_assert;
22use itertools::Itertools;
23
24use crate::engine::block::manager::{Block, BlockId};
25
26#[derive(Debug)]
28pub struct EvictionInfo<'a> {
29 pub blocks: &'a [Block],
31 pub evictable: &'a HashSet<BlockId>,
33 pub clean: usize,
35}
36
37pub trait EvictionPicker: Send + Sync + 'static + Debug {
39 #[expect(unused_variables)]
41 fn init(&mut self, blocks: &[BlockId], block_size: usize) {}
42
43 fn pick(&mut self, info: EvictionInfo<'_>) -> Option<BlockId>;
49
50 fn on_block_evictable(&mut self, info: EvictionInfo<'_>, block: BlockId);
52
53 fn on_block_evict(&mut self, info: EvictionInfo<'_>, block: BlockId);
55}
56
57#[derive(Debug)]
59pub struct FifoPicker {
60 queue: VecDeque<BlockId>,
61 blocks: usize,
62 probations: usize,
63 probation_ratio: f64,
64}
65
66impl Default for FifoPicker {
67 fn default() -> Self {
68 Self::new(0.1)
69 }
70}
71
72impl FifoPicker {
73 pub fn new(probation_ratio: f64) -> Self {
80 assert!(
81 (0.0..=1.0).contains(&probation_ratio),
82 "probation ratio {probation_ratio} must be in [0.0, 1.0]"
83 );
84 Self {
85 queue: VecDeque::new(),
86 blocks: 0,
87 probations: 0,
88 probation_ratio,
89 }
90 }
91}
92
93impl FifoPicker {
94 fn mark_probation(&self, info: EvictionInfo<'_>) {
95 let marks = self.probations.saturating_sub(info.clean);
96 self.queue.iter().take(marks).for_each(|rid| {
97 if info.evictable.contains(rid) {
98 info.blocks[*rid as usize]
99 .statistics()
100 .probation
101 .store(true, Ordering::Relaxed);
102 tracing::trace!(rid, "[fifo picker]: mark probation");
103 }
104 });
105 }
106}
107
108impl EvictionPicker for FifoPicker {
109 fn init(&mut self, blocks: &[BlockId], _: usize) {
110 self.blocks = blocks.len();
111 let probations = (self.blocks as f64 * self.probation_ratio).floor() as usize;
112 self.probations = probations.clamp(0, self.blocks);
113 }
114
115 fn pick(&mut self, info: EvictionInfo<'_>) -> Option<BlockId> {
116 tracing::trace!(evictable = ?info.evictable, clean = info.clean, queue = ?self.queue, "[fifo picker]: pick");
117 self.mark_probation(info);
118 let candidate = self.queue.front().copied();
119 tracing::trace!("[fifo picker]: pick {candidate:?}");
120 candidate
121 }
122
123 fn on_block_evictable(&mut self, _: EvictionInfo<'_>, block: BlockId) {
124 tracing::trace!(queue = ?self.queue, "[fifo picker]: {block} is evictable");
125 self.queue.push_back(block);
126 }
127
128 fn on_block_evict(&mut self, _: EvictionInfo<'_>, block: BlockId) {
129 tracing::trace!(queue = ?self.queue, "[fifo picker]: {block} is evicted");
130 let index = self.queue.iter().position(|r| r == &block).unwrap();
131 self.queue.remove(index);
132 }
133}
134
135#[derive(Debug)]
139pub struct InvalidRatioPicker {
140 threshold: f64,
141 block_size: usize,
142}
143
144impl InvalidRatioPicker {
145 pub fn new(threshold: f64) -> Self {
147 let ratio = threshold.clamp(0.0, 1.0);
148 Self {
149 threshold: ratio,
150 block_size: 0,
151 }
152 }
153}
154
155impl EvictionPicker for InvalidRatioPicker {
156 fn init(&mut self, _: &[BlockId], block_size: usize) {
157 self.block_size = block_size;
158 }
159
160 fn pick(&mut self, info: EvictionInfo<'_>) -> Option<BlockId> {
161 strict_assert!(self.block_size > 0);
162
163 let mut data = info
164 .evictable
165 .iter()
166 .map(|rid| {
167 (
168 *rid,
169 info.blocks[*rid as usize].statistics().invalid.load(Ordering::Relaxed),
170 )
171 })
172 .collect_vec();
173 data.sort_by_key(|(_, invalid)| *invalid);
174
175 let (rid, invalid) = data.last().copied()?;
176 if (invalid as f64 / self.block_size as f64) < self.threshold {
177 return None;
178 }
179 tracing::trace!("[invalid ratio picker]: pick {rid:?}");
180 Some(rid)
181 }
182
183 fn on_block_evictable(&mut self, _: EvictionInfo<'_>, block: BlockId) {
184 tracing::trace!("[invalid ratio picker]: {block} is evictable");
185 }
186
187 fn on_block_evict(&mut self, _: EvictionInfo<'_>, block: BlockId) {
188 tracing::trace!("[invalid ratio picker]: {block} is evicted");
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use std::{collections::HashSet, sync::Arc};
195
196 use foyer_common::spawn::Spawner;
197 use itertools::Itertools;
198
199 use super::*;
200 use crate::{
201 engine::block::manager::Block,
202 io::{device::noop::NoopPartition, engine::IoEngineBuildContext},
203 IoEngineConfig, NoopIoEngineConfig,
204 };
205
206 #[test_log::test(tokio::test)]
207 async fn test_fifo_picker() {
208 let mut picker = FifoPicker::new(0.1);
209 let mock_io_engine = NoopIoEngineConfig
210 .boxed()
211 .build(IoEngineBuildContext {
212 spawner: Spawner::current(),
213 })
214 .await
215 .unwrap();
216
217 let blocks = (0..10)
218 .map(|rid| Block::new_for_test(rid, Arc::<NoopPartition>::default(), mock_io_engine.clone()))
219 .collect_vec();
220 let mut evictable = HashSet::new();
221
222 fn info<'a>(blocks: &'a [Block], evictable: &'a HashSet<BlockId>, dirty: usize) -> EvictionInfo<'a> {
223 EvictionInfo {
224 blocks,
225 evictable,
226 clean: blocks.len() - evictable.len() - dirty,
227 }
228 }
229
230 fn check(block: &[Block], probations: impl IntoIterator<Item = BlockId>) {
231 let probations = probations.into_iter().collect::<HashSet<_>>();
232 for rid in 0..block.len() as BlockId {
233 if probations.contains(&rid) {
234 assert!(
235 block[rid as usize].statistics().probation.load(Ordering::Relaxed),
236 "probations {probations:?}, assert {rid} is probation failed"
237 );
238 } else {
239 assert!(
240 !block[rid as usize].statistics().probation.load(Ordering::Relaxed),
241 "probations {probations:?}, assert {rid} is not probation failed"
242 );
243 }
244 }
245 }
246
247 picker.init(&(0..10).collect_vec(), 0);
248
249 (0..10).for_each(|i| {
251 evictable.insert(i as _);
252 picker.on_block_evictable(info(&blocks, &evictable, 0), i);
253 });
254
255 assert_eq!(picker.pick(info(&blocks, &evictable, 0)), Some(0));
257 check(&blocks, [0]);
258 evictable.remove(&0);
259 picker.on_block_evict(info(&blocks, &evictable, 1), 0);
260
261 assert_eq!(picker.pick(info(&blocks, &evictable, 1)), Some(1));
263 check(&blocks, [0, 1]);
264 evictable.remove(&1);
265 picker.on_block_evict(info(&blocks, &evictable, 2), 1);
266
267 assert_eq!(picker.pick(info(&blocks, &evictable, 1)), Some(2));
269 check(&blocks, [0, 1]);
270 evictable.remove(&2);
271 picker.on_block_evict(info(&blocks, &evictable, 2), 2);
272
273 picker.on_block_evict(info(&blocks, &evictable, 3), 3);
274 evictable.remove(&3);
275 picker.on_block_evict(info(&blocks, &evictable, 4), 5);
276 evictable.remove(&5);
277 picker.on_block_evict(info(&blocks, &evictable, 5), 7);
278 evictable.remove(&7);
279 picker.on_block_evict(info(&blocks, &evictable, 6), 9);
280 evictable.remove(&9);
281
282 picker.on_block_evict(info(&blocks, &evictable, 7), 4);
283 evictable.remove(&4);
284 picker.on_block_evict(info(&blocks, &evictable, 8), 6);
285 evictable.remove(&6);
286 picker.on_block_evict(info(&blocks, &evictable, 9), 8);
287 evictable.remove(&8);
288 }
289
290 #[test_log::test(tokio::test)]
291 async fn test_invalid_ratio_picker() {
292 let mut picker = InvalidRatioPicker::new(0.5);
293 picker.init(&(0..10).collect_vec(), 10);
294
295 let mock_io_engine = NoopIoEngineConfig
296 .boxed()
297 .build(IoEngineBuildContext {
298 spawner: Spawner::current(),
299 })
300 .await
301 .unwrap();
302
303 let blocks = (0..10)
304 .map(|rid| Block::new_for_test(rid, Arc::<NoopPartition>::default(), mock_io_engine.clone()))
305 .collect_vec();
306 let mut evictable = HashSet::new();
307
308 (0..10).for_each(|i| {
309 blocks[i].statistics().invalid.fetch_add(i as _, Ordering::Relaxed);
310 evictable.insert(i as BlockId);
311 });
312
313 fn info<'a>(blocks: &'a [Block], evictable: &'a HashSet<BlockId>) -> EvictionInfo<'a> {
314 EvictionInfo {
315 blocks,
316 evictable,
317 clean: blocks.len() - evictable.len(),
318 }
319 }
320
321 assert_eq!(picker.pick(info(&blocks, &evictable)), Some(9));
322
323 assert_eq!(picker.pick(info(&blocks, &evictable)), Some(9));
324 evictable.remove(&9);
325 assert_eq!(picker.pick(info(&blocks, &evictable)), Some(8));
326 evictable.remove(&8);
327 assert_eq!(picker.pick(info(&blocks, &evictable)), Some(7));
328 evictable.remove(&7);
329 assert_eq!(picker.pick(info(&blocks, &evictable)), Some(6));
330 evictable.remove(&6);
331 assert_eq!(picker.pick(info(&blocks, &evictable)), Some(5));
332 evictable.remove(&5);
333
334 assert_eq!(picker.pick(info(&blocks, &evictable)), None);
335 assert_eq!(picker.pick(info(&blocks, &evictable)), None);
336 }
337}