use std::collections::{HashMap, HashSet, VecDeque};
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use super::{Remote, index};
const PREFETCHERS: usize = 4;
const FOLLOW_RUNS: usize = 3;
const BUDGET_BYTES: u64 = 1 << 30;
const BUDGET_ENTRIES: usize = 5_000;
const JUDGE_AFTER: usize = 8;
const MATCHING: f64 = 0.5;
#[derive(Default)]
pub(super) struct Prefetch {
state: Mutex<State>,
pub(super) stopped: AtomicBool,
pub(super) received: AtomicU64,
}
#[derive(Default)]
struct State {
queue: VecDeque<String>,
queued: HashSet<String>,
fetched: HashSet<String>,
claimed: usize,
resolved: usize,
matched: usize,
followed: HashSet<String>,
workers: usize,
members: Option<HashMap<String, Option<String>>>,
}
impl Prefetch {
pub(super) fn follow(&self, remote: &Arc<Remote>, root: &Path, key: &str) {
if self.stopped.load(Ordering::SeqCst) {
return;
}
let Some(known) = remote.known() else {
return;
};
let mut state = self.state.lock().unwrap();
let member = index::member(key);
let mut runs: Vec<(&String, &index::Run)> = known
.runs
.iter()
.filter(|(id, run)| {
!state.followed.contains(*id) && run.members.iter().any(|m| m == member)
})
.collect();
runs.sort_by(|(a, left), (b, right)| right.time.cmp(&left.time).then(b.cmp(a)));
let members = state.members.get_or_insert_with(|| {
let mut members: HashMap<String, Option<String>> = HashMap::new();
for key in known.keys.keys() {
members
.entry(index::member(key).to_owned())
.and_modify(|full| *full = None)
.or_insert_with(|| Some(key.clone()));
}
members
});
let mut found = Vec::new();
for (_, run) in runs.iter().take(FOLLOW_RUNS) {
let at = run.members.iter().position(|m| m == member).unwrap_or(0);
let after = run.members[at + 1..].iter();
let before = run.members[..at].iter().rev();
for name in after.chain(before) {
if let Some(Some(full)) = members.get(name) {
found.push(full.clone());
}
}
}
for (id, _) in runs.iter().take(FOLLOW_RUNS) {
state.followed.insert((*id).clone());
}
for key in found {
let local = root.join("entries").join(format!("{key}.json")).is_file();
if !local && state.queued.insert(key.clone()) {
state.queue.push_back(key);
}
}
while state.workers < PREFETCHERS && !state.queue.is_empty() {
state.workers += 1;
let remote = remote.clone();
let root = root.to_owned();
std::thread::spawn(move || work(&remote, &root));
}
}
pub(super) fn resolved(&self, key: &str) {
let mut state = self.state.lock().unwrap();
if state.queued.is_empty() {
return;
}
state.resolved += 1;
if state.queued.contains(key) {
state.matched += 1;
}
if state.resolved >= JUDGE_AFTER
&& (state.matched as f64) < state.resolved as f64 * MATCHING
{
drop(state);
self.stop();
}
}
pub(super) fn holds(&self, key: &str) -> bool {
self.state.lock().unwrap().fetched.contains(key)
}
pub(super) fn claim(&self, key: &str) -> bool {
let mut state = self.state.lock().unwrap();
let claimed = state.fetched.remove(key);
state.claimed += usize::from(claimed);
claimed
}
pub(super) fn stop(&self) {
self.stopped.store(true, Ordering::SeqCst);
self.state.lock().unwrap().queue.clear();
}
pub(super) fn summary(&self) -> (usize, usize) {
let state = self.state.lock().unwrap();
(state.fetched.len() + state.claimed, state.claimed)
}
fn next(&self) -> Option<String> {
let mut state = self.state.lock().unwrap();
let key = if self.stopped.load(Ordering::SeqCst) {
None
} else {
state.queue.pop_front()
};
if key.is_none() {
state.workers -= 1;
}
key
}
}
fn work(remote: &Arc<Remote>, root: &Path) {
let prefetch = &remote.prefetch;
while let Some(key) = prefetch.next() {
if root.join("entries").join(format!("{key}.json")).is_file() {
continue;
}
if remote.fetch_ahead(root, &key) {
let mut state = prefetch.state.lock().unwrap();
if !remote.confirmed.lock().unwrap().contains(&key) {
state.fetched.insert(key);
}
let over = prefetch.received.load(Ordering::Relaxed) > BUDGET_BYTES
|| state.fetched.len() + state.claimed >= BUDGET_ENTRIES;
drop(state);
if over {
prefetch.stop();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn queued(keys: &[&str]) -> Prefetch {
let prefetch = Prefetch::default();
prefetch
.state
.lock()
.unwrap()
.queued
.extend(keys.iter().map(|key| (*key).to_owned()));
prefetch
}
#[test]
fn prefetching_stops_once_the_run_goes_its_own_way() {
let prefetch = queued(&["a", "b"]);
for key in ["a", "x", "y", "z", "b", "v", "w"] {
prefetch.resolved(key);
}
assert!(!prefetch.stopped.load(Ordering::SeqCst));
prefetch.resolved("u");
assert!(prefetch.stopped.load(Ordering::SeqCst));
}
#[test]
fn a_run_that_follows_keeps_prefetching() {
let keys: Vec<String> = (0..10).map(|index| index.to_string()).collect();
let names: Vec<&str> = keys.iter().map(String::as_str).collect();
let prefetch = queued(&names);
for key in &names {
prefetch.resolved(key);
}
assert!(!prefetch.stopped.load(Ordering::SeqCst));
}
#[test]
fn a_prefetched_entry_is_claimed_once() {
let prefetch = Prefetch::default();
prefetch.state.lock().unwrap().fetched.insert("a".into());
assert!(prefetch.holds("a"));
assert!(prefetch.claim("a"));
assert!(!prefetch.claim("a"));
assert_eq!(prefetch.summary(), (1, 1));
}
}