bijux_cli/interface/repl/
diagnostics.rs1use std::time::{Duration, Instant};
2
3use serde_json::json;
4
5use super::session::startup_repl;
6use super::types::{
7 ReplSession, REPL_MEMORY_BUDGET_BYTES, REPL_STARTUP_DIAGNOSTIC_MAX_ENTRIES,
8 REPL_STARTUP_LATENCY_BUDGET_MS,
9};
10use crate::shared::telemetry::truncate_chars;
11
12const MAX_DIAGNOSTIC_SESSION_ID_CHARS: usize = 128;
13const MAX_DIAGNOSTIC_ERROR_CHARS: usize = 1024;
14
15#[must_use]
17pub fn inspect_last_error(session: &ReplSession) -> Option<String> {
18 session.last_error.clone()
19}
20
21#[must_use]
23pub fn session_diagnostics_dump(session: &ReplSession) -> String {
24 let (session_id, session_id_truncated) =
25 truncate_chars(&session.session_id, MAX_DIAGNOSTIC_SESSION_ID_CHARS);
26 let (last_error, last_error_truncated) = match session.last_error.as_deref() {
27 Some(value) => {
28 let (bounded, truncated) = truncate_chars(value, MAX_DIAGNOSTIC_ERROR_CHARS);
29 (Some(bounded), truncated)
30 }
31 None => (None, false),
32 };
33 let plugin_completion_hooks = session
34 .plugin_completion_hooks
35 .keys()
36 .take(REPL_STARTUP_DIAGNOSTIC_MAX_ENTRIES)
37 .collect::<Vec<_>>();
38 let completion_registries = session
39 .completion_registries
40 .keys()
41 .take(REPL_STARTUP_DIAGNOSTIC_MAX_ENTRIES)
42 .collect::<Vec<_>>();
43 let payload = json!({
44 "session_id": session_id,
45 "session_id_truncated": session_id_truncated,
46 "commands_executed": session.commands_executed,
47 "last_exit_code": session.last_exit_code,
48 "trace_mode": session.trace_mode,
49 "history_size": session.history.len(),
50 "history_limit": session.history_limit,
51 "plugin_completion_hooks": plugin_completion_hooks,
52 "plugin_completion_hooks_omitted":
53 session.plugin_completion_hooks.len().saturating_sub(REPL_STARTUP_DIAGNOSTIC_MAX_ENTRIES),
54 "completion_registries": completion_registries,
55 "completion_registries_omitted":
56 session.completion_registries.len().saturating_sub(REPL_STARTUP_DIAGNOSTIC_MAX_ENTRIES),
57 "last_error": last_error,
58 "last_error_truncated": last_error_truncated,
59 });
60 format!("{}\n", serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string()))
61}
62
63#[must_use]
65pub fn estimated_session_memory_bytes(session: &ReplSession) -> usize {
66 session.prompt.len()
67 + session.profile.len()
68 + session.history.iter().map(String::len).sum::<usize>()
69 + session
70 .plugin_completion_hooks
71 .iter()
72 .map(|(k, v)| k.len() + v.iter().map(String::len).sum::<usize>())
73 .sum::<usize>()
74 + session
75 .completion_registries
76 .iter()
77 .map(|(k, v)| k.len() + v.iter().map(String::len).sum::<usize>())
78 .sum::<usize>()
79 + 1024
80}
81
82#[must_use]
84pub fn benchmark_startup_latency(iterations: usize) -> Duration {
85 let runs = iterations.max(1);
86 let started = Instant::now();
87 for _ in 0..runs {
88 let _ = startup_repl("benchmark", None);
89 }
90 let total = started.elapsed();
91 let avg_nanos = total.as_nanos() / runs as u128;
92 let nanos = u64::try_from(avg_nanos).unwrap_or(u64::MAX);
93 Duration::from_nanos(nanos)
94}
95
96#[must_use]
98pub fn check_repl_budgets(session: &ReplSession, startup_avg: Duration) -> Vec<String> {
99 let mut warnings = Vec::new();
100 if startup_avg.as_millis() > REPL_STARTUP_LATENCY_BUDGET_MS {
101 warnings.push(format!(
102 "startup latency {}ms exceeded {}ms budget",
103 startup_avg.as_millis(),
104 REPL_STARTUP_LATENCY_BUDGET_MS
105 ));
106 }
107
108 let estimated = estimated_session_memory_bytes(session);
109 if estimated > REPL_MEMORY_BUDGET_BYTES {
110 warnings.push(format!(
111 "estimated memory {} bytes exceeded {} bytes budget",
112 estimated, REPL_MEMORY_BUDGET_BYTES
113 ));
114 }
115 warnings
116}
117
118#[cfg(test)]
119mod tests {
120 use super::session_diagnostics_dump;
121 use crate::interface::repl::session::startup_repl;
122 use crate::interface::repl::types::REPL_STARTUP_DIAGNOSTIC_MAX_ENTRIES;
123
124 #[test]
125 fn diagnostics_dump_bounds_large_fields() {
126 let (mut session, _) = startup_repl("", None);
127 session.session_id = "x".repeat(512);
128 session.last_error = Some("e".repeat(4096));
129 for idx in 0..(REPL_STARTUP_DIAGNOSTIC_MAX_ENTRIES + 5) {
130 session
131 .plugin_completion_hooks
132 .insert(format!("plugin-{idx}"), vec!["status".to_string()]);
133 session
134 .completion_registries
135 .insert(format!("owner-{idx}"), vec!["status".to_string()]);
136 }
137
138 let dump = session_diagnostics_dump(&session);
139 let payload: serde_json::Value =
140 serde_json::from_str(&dump).expect("diagnostics should be valid json");
141 assert_eq!(payload["session_id"].as_str().unwrap_or_default().len(), 128);
142 assert_eq!(payload["last_error"].as_str().unwrap_or_default().len(), 1024);
143 assert_eq!(payload["plugin_completion_hooks_omitted"], 5);
144 assert_eq!(payload["completion_registries_omitted"], 5);
145 }
146}