pub trait Entry {
type Rank: Ord;
fn rank(&self) -> Self::Rank;
fn accessed(&self) -> bool;
}
pub struct Update<T: Entry> {
pub to_evict: Vec<T>,
pub to_move_back: Vec<T>,
}
impl<T: Entry> Update<T> {
pub fn new(entries: impl IntoIterator<Item = T>, capacity: usize) -> Self {
let mut sorted_entries: Vec<T> = entries.into_iter().collect();
if sorted_entries.len() <= capacity {
return Self {
to_evict: Vec::new(),
to_move_back: Vec::new(),
};
}
sorted_entries.sort_by_cached_key(|e| e.rank());
let must_remove = sorted_entries.len() - capacity;
let mut to_evict = Vec::new();
let mut to_move_back = Vec::new();
for entry in sorted_entries {
if to_evict.len() == must_remove {
break;
}
if entry.accessed() {
to_move_back.push(entry);
} else {
to_evict.push(entry);
}
}
if to_evict.len() < must_remove {
let num_left_to_remove = must_remove - to_evict.len();
assert!(num_left_to_remove <= to_move_back.len());
to_evict.extend(to_move_back.drain(0..num_left_to_remove));
}
Self {
to_evict,
to_move_back,
}
}
}
#[cfg(test)]
mod test {
use crate::second_chance::*;
use proptest::collection::vec;
use proptest::prelude::*;
use proptest_derive::Arbitrary;
use std::collections::HashSet;
#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct TestEntry(u64, bool);
impl Entry for TestEntry {
type Rank = u64;
fn rank(&self) -> u64 {
self.0
}
fn accessed(&self) -> bool {
self.1
}
}
#[test]
fn smoke_test_empty() {
let result = Update::<TestEntry>::new(vec![], 4);
assert_eq!(result.to_evict, Vec::new());
assert_eq!(result.to_move_back, Vec::new());
}
#[test]
fn smoke_test_at_capacity() {
let result = Update::new(vec![TestEntry(0, true), TestEntry(1, false)], 2);
assert_eq!(result.to_evict, Vec::new());
assert_eq!(result.to_move_back, Vec::new());
}
#[test]
fn evict_first_entry() {
let result = Update::new(
vec![TestEntry(0, false), TestEntry(1, true), TestEntry(2, false)],
2,
);
assert_eq!(result.to_evict, vec![TestEntry(0, false)]);
assert_eq!(result.to_move_back, Vec::new());
}
#[test]
fn evict_first_entry_unsorted() {
let result = Update::new(
vec![TestEntry(2, false), TestEntry(1, true), TestEntry(0, false)],
2,
);
assert_eq!(result.to_evict, vec![TestEntry(0, false)]);
assert_eq!(result.to_move_back, Vec::new());
}
#[test]
fn evict_first_entry_all_touched() {
let result = Update::new(
vec![TestEntry(0, true), TestEntry(1, true), TestEntry(2, true)],
2,
);
assert_eq!(result.to_evict, vec![TestEntry(0, true)]);
assert_eq!(
result.to_move_back,
vec![TestEntry(1, true), TestEntry(2, true)]
);
}
#[test]
fn evict_second_entry() {
let result = Update::new(
vec![TestEntry(0, true), TestEntry(1, false), TestEntry(2, false)],
2,
);
assert_eq!(result.to_evict, vec![TestEntry(1, false)]);
assert_eq!(result.to_move_back, vec![TestEntry(0, true)]);
}
#[test]
fn evict_second_pass() {
let result = Update::new(
vec![TestEntry(0, true), TestEntry(1, false), TestEntry(2, true)],
1,
);
assert_eq!(
result.to_evict,
vec![TestEntry(1, false), TestEntry(0, true)]
);
assert_eq!(result.to_move_back, vec![TestEntry(2, true)]);
}
#[test]
fn evict_all_touched() {
let result = Update::new(
vec![TestEntry(1, true), TestEntry(2, true), TestEntry(0, true)],
2,
);
assert_eq!(result.to_evict, vec![TestEntry(0, true)]);
assert_eq!(
result.to_move_back,
vec![TestEntry(1, true), TestEntry(2, true)]
);
}
proptest! {
#[test]
fn compare_eviction_oracle(mut inputs in vec(any::<TestEntry>(), 0..20usize),
capacity in 1..10usize) {
let result = Update::new(inputs.clone(), capacity);
inputs.sort_by(|x, y| (x.1, x.0).cmp(&(y.1, y.0)));
let num_evictions = inputs.len().saturating_sub(capacity);
assert_eq!(&result.to_evict, &inputs[0..num_evictions]);
}
#[test]
fn compare_move_back_oracle(mut inputs in vec(any::<TestEntry>(), 0..20usize),
capacity in 1..10usize) {
let result = Update::new(inputs.clone(), capacity);
inputs.sort();
if result.to_evict.is_empty() {
assert_eq!(result.to_move_back, vec![]);
} else if result.to_evict.iter().any(|e| e.accessed()) {
let evicted: HashSet<_> = result.to_evict.iter().cloned().collect();
let expected: Vec<_> = inputs
.iter()
.filter(|e| !evicted.contains(e))
.cloned()
.collect();
assert_eq!(result.to_move_back, expected);
} else {
let max_ts = result
.to_evict
.iter()
.map(|e| e.rank())
.max()
.expect("to_evict isn't empty");
let must_move: Vec<_> = inputs
.iter()
.filter(|e| e.accessed() && e.rank() < max_ts)
.cloned()
.collect();
assert_eq!(&result.to_move_back[0..must_move.len()], &must_move);
let may_move: Vec<_> = inputs
.iter()
.filter(|e| e.accessed() && e.rank() <= max_ts)
.cloned()
.collect();
assert_eq!(&result.to_move_back, &may_move[0..result.to_move_back.len()]);
}
}
}
}