1use std::collections::{HashMap, VecDeque};
9use std::time::{Duration, Instant};
10
11use crate::snapshot::FilesystemUsage;
12
13const SAMPLE_EVERY: Duration = Duration::from_secs(30);
15
16const WINDOW: Duration = Duration::from_secs(30 * 60);
18
19const MIN_SPAN: Duration = Duration::from_secs(10 * 60);
21
22#[derive(Debug, Default)]
24pub struct FillTracker {
25 samples: HashMap<String, VecDeque<(Instant, u64)>>,
27 last_sample: Option<Instant>,
28}
29
30impl FillTracker {
31 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn annotate(&mut self, filesystems: &mut [FilesystemUsage], now: Instant) {
39 let due = self
40 .last_sample
41 .is_none_or(|at| now.saturating_duration_since(at) >= SAMPLE_EVERY);
42 if due {
43 self.last_sample = Some(now);
44 self.samples
46 .retain(|key, _| filesystems.iter().any(|fs| fs_key(fs) == *key));
47 for fs in filesystems.iter() {
48 let samples = self.samples.entry(fs_key(fs)).or_default();
49 samples.push_back((now, used_bytes(fs)));
50 while samples
51 .front()
52 .is_some_and(|(at, _)| now.saturating_duration_since(*at) > WINDOW)
53 {
54 samples.pop_front();
55 }
56 }
57 }
58 for fs in filesystems.iter_mut() {
59 fs.full_in_secs = self
60 .samples
61 .get(&fs_key(fs))
62 .and_then(|samples| full_in_secs(samples, fs.available_bytes));
63 }
64 }
65}
66
67fn fs_key(fs: &FilesystemUsage) -> String {
69 fs.mount_points
70 .first()
71 .cloned()
72 .unwrap_or_else(|| fs.device.clone())
73}
74
75fn used_bytes(fs: &FilesystemUsage) -> u64 {
76 fs.total_bytes.saturating_sub(fs.available_bytes)
77}
78
79fn full_in_secs(samples: &VecDeque<(Instant, u64)>, available: u64) -> Option<u64> {
83 let (first, _) = *samples.front()?;
84 let (last, _) = *samples.back()?;
85 if last.saturating_duration_since(first) < MIN_SPAN || available == 0 {
86 return None;
87 }
88 let n = samples.len() as f64;
90 let points = samples.iter().map(|(at, used)| {
91 (
92 at.saturating_duration_since(first).as_secs_f64(),
93 *used as f64,
94 )
95 });
96 let (sum_x, sum_y) = points
97 .clone()
98 .fold((0.0, 0.0), |(sx, sy), (x, y)| (sx + x, sy + y));
99 let (mean_x, mean_y) = (sum_x / n, sum_y / n);
100 let (cov, var) = points.fold((0.0, 0.0), |(c, v), (x, y)| {
101 (
102 c + (x - mean_x) * (y - mean_y),
103 v + (x - mean_x) * (x - mean_x),
104 )
105 });
106 let bytes_per_sec = cov / var;
107 (bytes_per_sec > 0.0).then(|| (available as f64 / bytes_per_sec).round() as u64)
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 const GIB: u64 = 1 << 30;
115
116 fn fs(available: u64) -> FilesystemUsage {
117 FilesystemUsage {
118 device: "/dev/md0".to_owned(),
119 mount_points: vec!["/srv".to_owned()],
120 total_bytes: 100 * GIB,
121 available_bytes: available,
122 ..Default::default()
123 }
124 }
125
126 #[test]
127 fn predicts_from_steady_growth_only() {
128 let start = Instant::now();
129 let mut tracker = FillTracker::new();
130 let mut list = Vec::new();
132 for step in 0..=20u64 {
133 list = vec![fs(60 * GIB - step * GIB / 2)];
134 tracker.annotate(&mut list, start + SAMPLE_EVERY * step as u32);
135 if step < 20 {
136 assert_eq!(list[0].full_in_secs, None, "too short at step {step}");
137 }
138 }
139 assert_eq!(list[0].full_in_secs, Some(50 * 60));
141
142 let mut later = vec![fs(50 * GIB)];
144 tracker.annotate(
145 &mut later,
146 start + SAMPLE_EVERY * 20 + Duration::from_secs(1),
147 );
148 assert_eq!(later[0].full_in_secs, Some(50 * 60));
149
150 let mut tracker = FillTracker::new();
152 for step in 0..=40u64 {
153 list = vec![fs(10 * GIB + step * GIB / 4)];
154 tracker.annotate(&mut list, start + SAMPLE_EVERY * step as u32);
155 }
156 assert_eq!(list[0].full_in_secs, None);
157 }
158
159 #[test]
160 fn forgets_old_samples_and_unmounted_filesystems() {
161 let start = Instant::now();
162 let mut tracker = FillTracker::new();
163 for step in 0..=120u64 {
165 let used = if step < 20 { step } else { 20 };
166 let mut list = vec![fs(80 * GIB - used * GIB)];
167 tracker.annotate(&mut list, start + SAMPLE_EVERY * step as u32);
168 if step == 120 {
169 assert_eq!(list[0].full_in_secs, None);
170 }
171 }
172 assert!(tracker.samples["/srv"].len() <= 61);
173 tracker.annotate(&mut [], start + SAMPLE_EVERY * 121);
174 assert!(tracker.samples.is_empty());
175 }
176}