lean_ctx/core/
index_progress.rs1use std::collections::HashMap;
9use std::sync::{Mutex, OnceLock};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum IndexComponent {
13 Graph,
14 Bm25,
15 Semantic,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub struct ProgressSnapshot {
20 pub done: u64,
21 pub total: u64,
22}
23
24impl ProgressSnapshot {
25 pub fn is_determinate(&self) -> bool {
27 self.total > 0
28 }
29
30 pub fn percent(&self) -> Option<u8> {
31 if self.total == 0 {
32 return None;
33 }
34 let pct = (self.done.saturating_mul(100)) / self.total;
35 Some(pct.min(100) as u8)
36 }
37}
38
39pub struct ProgressGuard {
41 root: String,
42 component: IndexComponent,
43 cleared: bool,
44}
45
46impl ProgressGuard {
47 pub fn new(root: impl Into<String>, component: IndexComponent) -> Self {
48 Self {
49 root: root.into(),
50 component,
51 cleared: false,
52 }
53 }
54
55 pub fn report(&self, done: u64, total: u64) {
56 report(&self.root, self.component, done, total);
57 }
58
59 pub fn disarm(mut self) {
61 self.cleared = true;
62 }
63}
64
65impl Drop for ProgressGuard {
66 fn drop(&mut self) {
67 if !self.cleared {
68 clear(&self.root, self.component);
69 }
70 }
71}
72
73type ProgressMap = HashMap<(String, IndexComponent), ProgressSnapshot>;
74
75fn map() -> &'static Mutex<ProgressMap> {
76 static MAP: OnceLock<Mutex<ProgressMap>> = OnceLock::new();
77 MAP.get_or_init(|| Mutex::new(HashMap::new()))
78}
79
80pub fn report(root: &str, component: IndexComponent, done: u64, total: u64) {
83 let mut g = map()
84 .lock()
85 .unwrap_or_else(std::sync::PoisonError::into_inner);
86 g.insert(
87 (root.to_string(), component),
88 ProgressSnapshot { done, total },
89 );
90}
91
92pub fn get(root: &str, component: IndexComponent) -> ProgressSnapshot {
93 let g = map()
94 .lock()
95 .unwrap_or_else(std::sync::PoisonError::into_inner);
96 g.get(&(root.to_string(), component))
97 .copied()
98 .unwrap_or_default()
99}
100
101pub fn clear(root: &str, component: IndexComponent) {
102 let mut g = map()
103 .lock()
104 .unwrap_or_else(std::sync::PoisonError::into_inner);
105 g.remove(&(root.to_string(), component));
106}
107
108pub fn clear_root(root: &str) {
109 let mut g = map()
110 .lock()
111 .unwrap_or_else(std::sync::PoisonError::into_inner);
112 g.retain(|(r, _), _| r != root);
113}
114
115pub fn report_bm25(root: &str, done: u64, total: u64) {
117 report(root, IndexComponent::Bm25, done, total);
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn report_get_clear() {
126 let root = "__test_index_progress_report__";
127 clear_root(root);
128 report(root, IndexComponent::Bm25, 3, 10);
129 let s = get(root, IndexComponent::Bm25);
130 assert_eq!(s.done, 3);
131 assert_eq!(s.total, 10);
132 assert_eq!(s.percent(), Some(30));
133 assert!(s.is_determinate());
134 clear(root, IndexComponent::Bm25);
135 assert_eq!(get(root, IndexComponent::Bm25), ProgressSnapshot::default());
136 }
137
138 #[test]
139 fn indeterminate_when_total_zero() {
140 let root = "__test_index_progress_indet__";
141 clear_root(root);
142 report(root, IndexComponent::Graph, 0, 0);
143 let s = get(root, IndexComponent::Graph);
144 assert!(!s.is_determinate());
145 assert_eq!(s.percent(), None);
146 clear_root(root);
147 }
148
149 #[test]
150 fn percent_caps_at_100() {
151 let root = "__test_index_progress_pct__";
152 clear_root(root);
153 report(root, IndexComponent::Semantic, 15, 10);
154 assert_eq!(get(root, IndexComponent::Semantic).percent(), Some(100));
155 clear_root(root);
156 }
157
158 #[test]
159 fn guard_clears_on_drop() {
160 let root = "__test_index_progress_guard__";
161 clear_root(root);
162 {
163 let g = ProgressGuard::new(root, IndexComponent::Semantic);
164 g.report(1, 4);
165 assert_eq!(get(root, IndexComponent::Semantic).done, 1);
166 }
167 assert_eq!(
168 get(root, IndexComponent::Semantic),
169 ProgressSnapshot::default()
170 );
171 clear_root(root);
172 }
173
174 #[test]
175 fn guard_clears_after_panic() {
176 let root = "__test_index_progress_guard_panic__";
177 clear_root(root);
178 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
179 let g = ProgressGuard::new(root, IndexComponent::Bm25);
180 g.report(2, 5);
181 panic!("boom");
182 }));
183 assert_eq!(get(root, IndexComponent::Bm25), ProgressSnapshot::default());
184 clear_root(root);
185 }
186}