1use std::collections::{BTreeMap, HashSet, VecDeque};
2use std::sync::{Arc, LazyLock};
3use std::time::Instant;
4
5use parking_lot::Mutex;
6use serde::Serialize;
7
8use super::InspectCategory;
9use crate::lsp::roots::ServerKey;
10
11const RETAINED_CALLS: usize = 64;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
16#[serde(rename_all = "snake_case")]
17pub enum InspectPhaseId {
18 LspStart,
19 LspQuiescence,
20 Tier2Rescan,
21 CallgraphReady,
22 StatVerification,
23}
24
25impl InspectPhaseId {
26 pub const fn as_str(self) -> &'static str {
27 match self {
28 Self::LspStart => "lsp_start",
29 Self::LspQuiescence => "lsp_quiescence",
30 Self::Tier2Rescan => "tier2_rescan",
31 Self::CallgraphReady => "callgraph_ready",
32 Self::StatVerification => "stat_verification",
33 }
34 }
35
36 const fn takes_producer(self) -> bool {
37 matches!(self, Self::LspStart | Self::LspQuiescence)
38 }
39
40 const fn takes_category(self) -> bool {
41 matches!(
42 self,
43 Self::Tier2Rescan | Self::CallgraphReady | Self::StatVerification
44 )
45 }
46}
47
48#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
51pub struct InspectPhaseEntry {
52 pub id: InspectPhaseId,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub producer: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub category: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub also_satisfied: Option<Vec<String>>,
59}
60
61impl InspectPhaseEntry {
62 pub fn lsp(id: InspectPhaseId, server: &ServerKey) -> Self {
63 assert!(id.takes_producer(), "producer is only valid for LSP phases");
64 Self {
65 id,
66 producer: Some(server.kind.id_str().to_string()),
67 category: None,
68 also_satisfied: None,
69 }
70 }
71
72 pub fn category(id: InspectPhaseId, category: InspectCategory) -> Self {
73 assert!(
74 id.takes_category(),
75 "category is only valid for category-attributed phases"
76 );
77 Self {
78 id,
79 producer: None,
80 category: Some(category.as_str().to_string()),
81 also_satisfied: None,
82 }
83 }
84
85 pub fn with_also_satisfied(
86 mut self,
87 categories: impl IntoIterator<Item = InspectCategory>,
88 ) -> Self {
89 let mut seen = HashSet::new();
90 let categories = categories
91 .into_iter()
92 .map(|category| category.as_str().to_string())
93 .filter(|category| seen.insert(category.clone()))
94 .collect::<Vec<_>>();
95 if !categories.is_empty() {
96 self.also_satisfied = Some(categories);
97 }
98 self
99 }
100}
101
102#[derive(Clone, Debug)]
103pub struct InspectPhaseRecord {
104 pub entry: InspectPhaseEntry,
105 pub started: Instant,
106 pub completed: Option<Instant>,
107 pub terminal_error: Option<String>,
108}
109
110impl InspectPhaseRecord {
111 pub fn is_completed(&self) -> bool {
112 self.completed.is_some()
113 }
114
115 pub fn terminal_error(&self) -> Option<&str> {
116 self.terminal_error.as_deref()
117 }
118
119 pub fn duration_ms(&self) -> Option<u128> {
120 self.completed
121 .map(|completed| completed.duration_since(self.started).as_millis())
122 }
123}
124
125#[derive(Clone, Debug)]
126pub struct InspectPhaseLogSnapshot {
127 pub request_id: String,
128 pub records: Vec<InspectPhaseRecord>,
129 pub blocking_waited: bool,
130}
131
132#[derive(Default)]
133struct InspectPhaseLogState {
134 request_id: String,
135 records: Vec<InspectPhaseRecord>,
136 blocking_waited: bool,
137}
138
139#[derive(Clone, Default)]
143pub struct InspectPhaseLog {
144 state: Arc<Mutex<InspectPhaseLogState>>,
145}
146
147impl InspectPhaseLog {
148 pub fn for_request(request_id: impl Into<String>) -> Self {
149 let log = Self {
150 state: Arc::new(Mutex::new(InspectPhaseLogState {
151 request_id: request_id.into(),
152 ..InspectPhaseLogState::default()
153 })),
154 };
155 retain_log(log.clone());
156 log
157 }
158
159 pub fn start(&self, entry: InspectPhaseEntry) -> InspectPhaseHandle {
160 let mut state = self.state.lock();
161 let index = state.records.len();
162 state.records.push(InspectPhaseRecord {
163 entry,
164 started: Instant::now(),
165 completed: None,
166 terminal_error: None,
167 });
168 InspectPhaseHandle {
169 log: self.clone(),
170 index,
171 completed: false,
172 }
173 }
174
175 pub fn note_blocking_wait(&self) {
176 self.state.lock().blocking_waited = true;
177 }
178
179 pub fn snapshot(&self) -> InspectPhaseLogSnapshot {
180 let state = self.state.lock();
181 InspectPhaseLogSnapshot {
182 request_id: state.request_id.clone(),
183 records: state.records.clone(),
184 blocking_waited: state.blocking_waited,
185 }
186 }
187
188 pub fn terminal_inputs(&self) -> (Vec<InspectPhaseEntry>, bool) {
190 let state = self.state.lock();
191 let entries = state
192 .records
193 .iter()
194 .filter(|record| record.is_completed() && record.terminal_error.is_none())
195 .map(|record| record.entry.clone())
196 .collect();
197 (entries, state.blocking_waited)
198 }
199
200 pub fn in_flight_entry(&self) -> Option<InspectPhaseEntry> {
203 self.state
204 .lock()
205 .records
206 .iter()
207 .rev()
208 .find(|record| !record.is_completed())
209 .map(|record| record.entry.clone())
210 }
211}
212
213pub struct InspectPhaseHandle {
214 log: InspectPhaseLog,
215 index: usize,
216 completed: bool,
217}
218
219impl InspectPhaseHandle {
220 pub fn complete(mut self) {
221 let mut state = self.log.state.lock();
222 if let Some(record) = state.records.get_mut(self.index) {
223 record.completed = Some(Instant::now());
224 }
225 self.completed = true;
226 }
227
228 pub fn fail(mut self, error: impl Into<String>) {
229 let mut state = self.log.state.lock();
230 if let Some(record) = state.records.get_mut(self.index) {
231 record.completed = Some(Instant::now());
232 record.terminal_error = Some(error.into());
233 }
234 self.completed = true;
235 }
236}
237
238impl Drop for InspectPhaseHandle {
239 fn drop(&mut self) {
240 if !self.completed {
241 let mut state = self.log.state.lock();
242 if let Some(record) = state.records.get_mut(self.index) {
243 record.completed = Some(Instant::now());
244 record.terminal_error = Some("phase handle dropped before completion".to_string());
245 }
246 }
247 }
248}
249
250struct RetainedLogs {
251 logs: BTreeMap<String, InspectPhaseLog>,
252 order: VecDeque<String>,
253}
254
255static RETAINED_LOGS: LazyLock<Mutex<RetainedLogs>> = LazyLock::new(|| {
256 Mutex::new(RetainedLogs {
257 logs: BTreeMap::new(),
258 order: VecDeque::new(),
259 })
260});
261
262fn retain_log(log: InspectPhaseLog) {
263 let request_id = log.snapshot().request_id;
264 let mut retained = RETAINED_LOGS.lock();
265 if !retained.logs.contains_key(&request_id) {
266 retained.order.push_back(request_id.clone());
267 }
268 retained.logs.insert(request_id, log);
269 while retained.order.len() > RETAINED_CALLS {
270 if let Some(expired) = retained.order.pop_front() {
271 retained.logs.remove(&expired);
272 }
273 }
274}
275
276pub fn inspect_phase_log_for_request(request_id: &str) -> Option<InspectPhaseLogSnapshot> {
279 RETAINED_LOGS
280 .lock()
281 .logs
282 .get(request_id)
283 .map(InspectPhaseLog::snapshot)
284}
285
286pub fn format_wait_text(entries: &[InspectPhaseEntry], blocking_waited: bool) -> String {
289 let completed = if entries.is_empty() {
290 "none".to_string()
291 } else {
292 entries
293 .iter()
294 .map(|entry| entry.id.as_str())
295 .collect::<Vec<_>>()
296 .join(",")
297 };
298 format!(
299 "waited: {}; completed: {completed}",
300 if blocking_waited { "yes" } else { "no" }
301 )
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn wait_text_reads_completed_phase_order_and_recorded_wait_state() {
310 let log = InspectPhaseLog::for_request("inspect-log-order");
311 log.start(InspectPhaseEntry::category(
312 InspectPhaseId::StatVerification,
313 InspectCategory::DeadCode,
314 ))
315 .complete();
316 let (entries, waited) = log.terminal_inputs();
317 assert_eq!(
318 format_wait_text(&entries, waited),
319 "waited: no; completed: stat_verification"
320 );
321
322 log.note_blocking_wait();
323 let (entries, waited) = log.terminal_inputs();
324 assert_eq!(
325 format_wait_text(&entries, waited),
326 "waited: yes; completed: stat_verification"
327 );
328 }
329
330 #[test]
331 fn failed_records_do_not_become_completed_phase_entries() {
332 let log = InspectPhaseLog::for_request("inspect-log-incomplete");
333 let phase = log.start(InspectPhaseEntry::category(
334 InspectPhaseId::Tier2Rescan,
335 InspectCategory::Duplicates,
336 ));
337 phase.fail("scan failed");
338
339 let (entries, waited) = log.terminal_inputs();
340 assert!(entries.is_empty());
341 assert!(!waited);
342 let snapshot = inspect_phase_log_for_request("inspect-log-incomplete").unwrap();
343 assert_eq!(snapshot.records[0].terminal_error(), Some("scan failed"));
344 assert!(snapshot.records[0].completed.is_some());
345 assert!(snapshot.records[0].duration_ms().is_some());
346 }
347}