1use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering};
26use std::sync::{Arc, Mutex, OnceLock, Weak};
27use std::time::{SystemTime, UNIX_EPOCH};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[repr(u8)]
32pub enum Phase {
33 Idle = 0,
34 Walk = 1,
36 Resolve = 2,
38 Paused = 3,
40 Flush = 4,
42 Fold = 5,
44 Done = 6,
45}
46
47impl Phase {
48 pub fn as_str(self) -> &'static str {
49 match self {
50 Phase::Idle => "idle",
51 Phase::Walk => "walk",
52 Phase::Resolve => "resolve",
53 Phase::Paused => "paused",
54 Phase::Flush => "flush",
55 Phase::Fold => "fold",
56 Phase::Done => "done",
57 }
58 }
59 fn from_u8(v: u8) -> Phase {
60 match v {
61 1 => Phase::Walk,
62 2 => Phase::Resolve,
63 3 => Phase::Paused,
64 4 => Phase::Flush,
65 5 => Phase::Fold,
66 6 => Phase::Done,
67 _ => Phase::Idle,
68 }
69 }
70}
71
72pub struct IndexProgress {
74 root: String,
76 active: AtomicBool,
77 resumed: AtomicBool,
78 pack_id: AtomicU64,
79 objects_total: AtomicU64,
80 objects_done: AtomicU64,
81 bytes_done: AtomicU64,
83 bytes_total: AtomicU64,
84 phase: AtomicU8,
85 started_at_ms: AtomicU64,
86 reinflations: AtomicU64,
88 pauses: AtomicU64,
90 sheds: AtomicU64,
94 packs_done: AtomicU64,
96 peak_rss: AtomicU64,
98 mem_available: AtomicU64,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct Snapshot {
105 pub root: String,
106 pub active: bool,
107 pub resumed: bool,
108 pub pack_id: u64,
109 pub objects_total: u64,
110 pub objects_done: u64,
111 pub bytes_done: u64,
112 pub bytes_total: u64,
113 pub phase: Phase,
114 pub started_at_ms: u64,
115 pub reinflations: u64,
116 pub pauses: u64,
117 pub packs_done: u64,
118 pub peak_rss: u64,
125 pub mem_available: u64,
128}
129
130impl Snapshot {
131 pub fn percent(&self) -> u64 {
133 if self.objects_total == 0 {
134 0
135 } else {
136 (self.objects_done * 100 / self.objects_total).min(100)
137 }
138 }
139}
140
141fn now_ms() -> u64 {
142 SystemTime::now()
143 .duration_since(UNIX_EPOCH)
144 .map(|d| d.as_millis() as u64)
145 .unwrap_or(0)
146}
147
148impl IndexProgress {
149 pub fn new(root: &str) -> Arc<Self> {
151 let p = Arc::new(Self {
152 root: root.to_string(),
153 active: AtomicBool::new(false),
154 resumed: AtomicBool::new(false),
155 pack_id: AtomicU64::new(0),
156 objects_total: AtomicU64::new(0),
157 objects_done: AtomicU64::new(0),
158 bytes_done: AtomicU64::new(0),
159 bytes_total: AtomicU64::new(0),
160 phase: AtomicU8::new(Phase::Idle as u8),
161 started_at_ms: AtomicU64::new(0),
162 reinflations: AtomicU64::new(0),
163 pauses: AtomicU64::new(0),
164 sheds: AtomicU64::new(0),
165 packs_done: AtomicU64::new(0),
166 peak_rss: AtomicU64::new(0),
167 mem_available: AtomicU64::new(0),
168 });
169 registry().lock().unwrap_or_else(|e| e.into_inner()).push(Arc::downgrade(&p));
170 p
171 }
172
173 pub fn detached() -> Arc<Self> {
175 let p = Self::new("");
176 p
177 }
178
179 pub fn root(&self) -> &str {
180 &self.root
181 }
182
183 pub fn begin(&self, pack_id: u64, bytes_total: u64, resumed: bool) {
185 self.pack_id.store(pack_id, Ordering::Relaxed);
186 self.bytes_total.store(bytes_total, Ordering::Relaxed);
187 self.bytes_done.store(0, Ordering::Relaxed);
188 self.objects_total.store(0, Ordering::Relaxed);
189 self.objects_done.store(0, Ordering::Relaxed);
190 self.reinflations.store(0, Ordering::Relaxed);
191 self.pauses.store(0, Ordering::Relaxed);
192 self.resumed.store(resumed, Ordering::Relaxed);
193 self.started_at_ms.store(now_ms(), Ordering::Relaxed);
194 self.phase.store(Phase::Walk as u8, Ordering::Relaxed);
195 self.active.store(true, Ordering::Release);
196 }
197
198 pub fn set_total(&self, objects: u64) {
200 self.objects_total.store(objects, Ordering::Relaxed);
201 }
202
203 pub fn set_phase(&self, phase: Phase) {
204 self.phase.store(phase as u8, Ordering::Relaxed);
205 }
206
207 #[inline]
209 pub fn object_done(&self, bytes: u64) {
210 self.objects_done.fetch_add(1, Ordering::Relaxed);
211 self.bytes_done.fetch_add(bytes, Ordering::Relaxed);
212 }
213
214 pub fn add_reinflations(&self, n: u64) {
215 self.reinflations.fetch_add(n, Ordering::Relaxed);
216 }
217
218 pub fn add_pause(&self) {
219 self.pauses.fetch_add(1, Ordering::Relaxed);
220 self.sample_memory();
221 }
222
223 pub fn add_shed(&self) {
225 self.sheds.fetch_add(1, Ordering::Relaxed);
226 }
227
228 pub fn sheds(&self) -> u64 {
230 self.sheds.load(Ordering::Relaxed)
231 }
232
233 pub fn sample_memory(&self) {
240 self.peak_rss
241 .store(crate::memguard::peak_rss_bytes(), Ordering::Relaxed);
242 self.mem_available.store(
243 crate::memguard::global().available().unwrap_or(0),
244 Ordering::Relaxed,
245 );
246 }
247
248 pub fn rate(&self) -> u64 {
250 let s = self.snapshot();
251 let started = s.started_at_ms;
252 let now = now_ms();
253 if started == 0 || now <= started {
254 return 0;
255 }
256 s.objects_done * 1000 / (now - started)
257 }
258
259 pub fn finish(&self) {
261 self.packs_done.fetch_add(1, Ordering::Relaxed);
262 self.phase.store(Phase::Done as u8, Ordering::Relaxed);
263 self.active.store(false, Ordering::Release);
264 }
265
266 pub fn snapshot(&self) -> Snapshot {
267 Snapshot {
268 root: self.root.clone(),
269 active: self.active.load(Ordering::Acquire),
270 resumed: self.resumed.load(Ordering::Relaxed),
271 pack_id: self.pack_id.load(Ordering::Relaxed),
272 objects_total: self.objects_total.load(Ordering::Relaxed),
273 objects_done: self.objects_done.load(Ordering::Relaxed),
274 bytes_done: self.bytes_done.load(Ordering::Relaxed),
275 bytes_total: self.bytes_total.load(Ordering::Relaxed),
276 phase: Phase::from_u8(self.phase.load(Ordering::Relaxed)),
277 started_at_ms: self.started_at_ms.load(Ordering::Relaxed),
278 reinflations: self.reinflations.load(Ordering::Relaxed),
279 pauses: self.pauses.load(Ordering::Relaxed),
280 packs_done: self.packs_done.load(Ordering::Relaxed),
281 peak_rss: self.peak_rss.load(Ordering::Relaxed),
282 mem_available: self.mem_available.load(Ordering::Relaxed),
283 }
284 }
285}
286
287pub const TICK: std::time::Duration = std::time::Duration::from_secs(10);
289
290pub struct ProgressTicker {
306 stop: Arc<AtomicBool>,
307 handle: Option<std::thread::JoinHandle<()>>,
308}
309
310impl ProgressTicker {
311 pub fn start(progress: Arc<IndexProgress>) -> Self {
314 let stop = Arc::new(AtomicBool::new(false));
315 let s2 = stop.clone();
316 let handle = std::thread::Builder::new()
317 .name("znippy-index-tick".into())
318 .spawn(move || {
319 let started = std::time::Instant::now();
320 let mut next = std::time::Instant::now() + TICK;
321 while !s2.load(Ordering::Relaxed) {
322 let now = std::time::Instant::now();
333 if now < next {
334 std::thread::park_timeout(next - now);
335 continue;
336 }
337 next = now + TICK;
338 progress.sample_memory();
339 let s = progress.snapshot();
340 if !s.active {
341 continue;
342 }
343 let secs = started.elapsed().as_secs_f64().max(1e-3);
344 tracing::info!(
345 target: "gunnar::index",
346 event = "index.progress",
347 repo = %s.root,
348 pack_id = s.pack_id,
349 phase = s.phase.as_str(),
350 percent = s.percent(),
351 objects_done = s.objects_done,
352 objects_total = s.objects_total,
353 bytes_done = s.bytes_done,
354 bytes_total = s.bytes_total,
355 objects_per_s = (s.objects_done as f64 / secs) as u64,
356 reinflations = s.reinflations,
357 pauses = s.pauses,
358 peak_rss = s.peak_rss,
359 mem_available = s.mem_available,
360 elapsed_ms = started.elapsed().as_millis() as u64,
361 "index {} {}% {}/{} {} {}/s; {} re-inflations, {} pauses, peak rss {}, {} available",
362 s.root,
363 s.percent(),
364 s.objects_done,
365 s.objects_total,
366 s.phase.as_str(),
367 (s.objects_done as f64 / secs) as u64,
368 s.reinflations,
369 s.pauses,
370 crate::memguard::fmt_bytes(s.peak_rss),
371 crate::memguard::fmt_bytes(s.mem_available)
372 );
373 }
374 })
375 .ok();
376 ProgressTicker { stop, handle }
377 }
378}
379
380impl Drop for ProgressTicker {
381 fn drop(&mut self) {
382 self.stop.store(true, Ordering::Relaxed);
383 if let Some(h) = self.handle.take() {
384 h.thread().unpark();
389 let _ = h.join();
390 }
391 }
392}
393
394fn registry() -> &'static Mutex<Vec<Weak<IndexProgress>>> {
395 static R: OnceLock<Mutex<Vec<Weak<IndexProgress>>>> = OnceLock::new();
396 R.get_or_init(|| Mutex::new(Vec::new()))
397}
398
399pub fn snapshot_all(active_only: bool) -> Vec<Snapshot> {
403 let mut r = registry().lock().unwrap_or_else(|e| e.into_inner());
404 r.retain(|w| w.strong_count() > 0);
405 r.iter()
406 .filter_map(|w| w.upgrade())
407 .map(|p| p.snapshot())
408 .filter(|s| !active_only || s.active)
409 .collect()
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
426 fn dropping_the_ticker_does_not_block_the_caller() {
427 let p = IndexProgress::new("/tmp/x/alice/tick-drop.git/gunnar-git.znippy");
428 p.begin(1, 1 << 20, false);
429 let ticker = ProgressTicker::start(p.clone());
430 std::thread::sleep(std::time::Duration::from_millis(20));
432 let t = std::time::Instant::now();
433 drop(ticker);
434 let waited = t.elapsed();
435 assert!(
436 waited < std::time::Duration::from_millis(50),
437 "drop of a ticker blocked the caller for {waited:?} — a parked ticker must be \
438 unparked and joined at once, not waited out"
439 );
440 }
441
442 #[test]
443 fn a_counter_reports_what_the_worker_did_and_percent_is_applied_output() {
444 let p = IndexProgress::new("/tmp/x/alice/linux.git/gunnar-git.znippy");
445 assert!(!p.snapshot().active);
446 p.begin(7, 8 << 30, true);
447 p.set_total(13_948_244);
448 p.set_phase(Phase::Resolve);
449 for _ in 0..4_700_000 {
450 p.object_done(600);
451 }
452 let s = p.snapshot();
453 assert!(s.active && s.resumed);
454 assert_eq!(s.pack_id, 7);
455 assert_eq!(s.objects_done, 4_700_000);
456 assert_eq!(s.percent(), 33);
457 assert_eq!(s.bytes_done, 4_700_000 * 600);
458 assert_eq!(s.phase, Phase::Resolve);
459 assert!(s.started_at_ms > 0);
460 p.finish();
461 let s = p.snapshot();
462 assert!(!s.active);
463 assert_eq!(s.phase, Phase::Done);
464 assert_eq!(s.packs_done, 1);
465 }
466
467 #[test]
468 fn the_registry_lists_live_counters_and_forgets_dropped_ones() {
469 let root = format!("/reg-test-{}-a", std::process::id());
470 let a = IndexProgress::new(&root);
471 a.begin(1, 10, false);
472 let b = IndexProgress::new(&format!("{root}-b"));
473 let all = snapshot_all(false);
474 assert!(all.iter().any(|s| s.root == root));
475 assert!(all.iter().any(|s| s.root == format!("{root}-b")));
476 let active = snapshot_all(true);
477 assert!(active.iter().any(|s| s.root == root));
478 assert!(!active.iter().any(|s| s.root == format!("{root}-b")), "b never began");
479 drop(b);
480 assert!(!snapshot_all(false).iter().any(|s| s.root == format!("{root}-b")), "swept on drop");
481 assert_eq!(Phase::from_u8(2), Phase::Resolve);
482 assert_eq!(Phase::Paused.as_str(), "paused");
483 }
484}