1#![cfg(feature = "diagnostics")]
15
16use crate::channel::{TOTAL_MESSAGES_RECEIVED, TOTAL_MESSAGES_SENT};
17use crate::memory_stats::memory_registry;
18use crate::scheduler::{PEAK_STRANDS, TOTAL_COMPLETED, TOTAL_SPAWNED, scheduler_elapsed};
19use std::io::Write;
20use std::sync::OnceLock;
21use std::sync::atomic::Ordering;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ReportFormat {
30 Human,
31 Json,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum ReportDestination {
37 Stderr,
38 File(String),
39}
40
41#[derive(Debug, Clone)]
43pub struct ReportConfig {
44 pub format: ReportFormat,
45 pub destination: ReportDestination,
46 pub include_words: bool,
48}
49
50impl ReportConfig {
51 pub fn from_env() -> Option<Self> {
53 let val = std::env::var("SEQ_REPORT").ok()?;
54 if val.is_empty() {
55 return None;
56 }
57
58 match val.as_str() {
59 "0" => None,
60 "1" => Some(ReportConfig {
61 format: ReportFormat::Human,
62 destination: ReportDestination::Stderr,
63 include_words: false,
64 }),
65 "words" => Some(ReportConfig {
66 format: ReportFormat::Human,
67 destination: ReportDestination::Stderr,
68 include_words: true,
69 }),
70 "json" => Some(ReportConfig {
71 format: ReportFormat::Json,
72 destination: ReportDestination::Stderr,
73 include_words: false,
74 }),
75 s if s.starts_with("json:") => {
76 let path = s[5..].to_string();
77 Some(ReportConfig {
78 format: ReportFormat::Json,
79 destination: ReportDestination::File(path),
80 include_words: false,
81 })
82 }
83 _ => {
84 eprintln!("Warning: SEQ_REPORT='{}' not recognized, ignoring", val);
85 None
86 }
87 }
88 }
89}
90
91static REPORT_CONFIG: OnceLock<Option<ReportConfig>> = OnceLock::new();
92
93fn get_report_config() -> &'static Option<ReportConfig> {
94 REPORT_CONFIG.get_or_init(ReportConfig::from_env)
95}
96
97#[derive(Debug)]
103pub struct ReportData {
104 pub wall_clock_ms: u64,
105 pub total_spawned: u64,
106 pub total_completed: u64,
107 pub peak_strands: usize,
108 pub active_threads: usize,
109 pub total_arena_bytes: u64,
110 pub total_peak_arena_bytes: u64,
111 pub messages_sent: u64,
112 pub messages_received: u64,
113 pub word_counts: Option<Vec<(String, u64)>>,
114}
115
116fn collect_report_data(include_words: bool) -> ReportData {
118 let wall_clock_ms = scheduler_elapsed()
119 .map(|d| d.as_millis() as u64)
120 .unwrap_or(0);
121
122 let mem_stats = memory_registry().aggregate_stats();
123
124 let word_counts = if include_words {
125 read_word_counts()
126 } else {
127 None
128 };
129
130 ReportData {
131 wall_clock_ms,
132 total_spawned: TOTAL_SPAWNED.load(Ordering::Relaxed),
133 total_completed: TOTAL_COMPLETED.load(Ordering::Relaxed),
134 peak_strands: PEAK_STRANDS.load(Ordering::Relaxed),
135 active_threads: mem_stats.active_threads,
136 total_arena_bytes: mem_stats.total_arena_bytes,
137 total_peak_arena_bytes: mem_stats.total_peak_arena_bytes,
138 messages_sent: TOTAL_MESSAGES_SENT.load(Ordering::Relaxed),
139 messages_received: TOTAL_MESSAGES_RECEIVED.load(Ordering::Relaxed),
140 word_counts,
141 }
142}
143
144fn format_human(data: &ReportData) -> String {
149 let mut out = String::new();
150 out.push_str("=== SEQ REPORT ===\n");
151 out.push_str(&format!("Wall clock: {} ms\n", data.wall_clock_ms));
152 out.push_str(&format!("Strands spawned: {}\n", data.total_spawned));
153 out.push_str(&format!("Strands done: {}\n", data.total_completed));
154 out.push_str(&format!("Peak strands: {}\n", data.peak_strands));
155 out.push_str(&format!("Worker threads: {}\n", data.active_threads));
156 out.push_str(&format!(
157 "Arena current: {} bytes\n",
158 data.total_arena_bytes
159 ));
160 out.push_str(&format!(
161 "Arena peak: {} bytes\n",
162 data.total_peak_arena_bytes
163 ));
164 out.push_str(&format!("Messages sent: {}\n", data.messages_sent));
165 out.push_str(&format!("Messages recv: {}\n", data.messages_received));
166
167 if let Some(ref counts) = data.word_counts {
168 out.push_str("\n--- Word Call Counts ---\n");
169 for (name, count) in counts {
170 out.push_str(&format!(" {:30} {}\n", name, count));
171 }
172 }
173
174 out.push_str("==================\n");
175 out
176}
177
178#[cfg(feature = "report-json")]
179fn format_json(data: &ReportData) -> String {
180 let mut map = serde_json::Map::new();
181 map.insert(
182 "wall_clock_ms".into(),
183 serde_json::Value::Number(data.wall_clock_ms.into()),
184 );
185 map.insert(
186 "strands_spawned".into(),
187 serde_json::Value::Number(data.total_spawned.into()),
188 );
189 map.insert(
190 "strands_completed".into(),
191 serde_json::Value::Number(data.total_completed.into()),
192 );
193 map.insert(
194 "peak_strands".into(),
195 serde_json::Value::Number((data.peak_strands as u64).into()),
196 );
197 map.insert(
198 "worker_threads".into(),
199 serde_json::Value::Number((data.active_threads as u64).into()),
200 );
201 map.insert(
202 "arena_bytes".into(),
203 serde_json::Value::Number(data.total_arena_bytes.into()),
204 );
205 map.insert(
206 "arena_peak_bytes".into(),
207 serde_json::Value::Number(data.total_peak_arena_bytes.into()),
208 );
209 map.insert(
210 "messages_sent".into(),
211 serde_json::Value::Number(data.messages_sent.into()),
212 );
213 map.insert(
214 "messages_received".into(),
215 serde_json::Value::Number(data.messages_received.into()),
216 );
217
218 if let Some(ref counts) = data.word_counts {
219 let word_map: serde_json::Map<String, serde_json::Value> = counts
220 .iter()
221 .map(|(name, count)| (name.clone(), serde_json::Value::Number((*count).into())))
222 .collect();
223 map.insert("word_counts".into(), serde_json::Value::Object(word_map));
224 }
225
226 let obj = serde_json::Value::Object(map);
227 serde_json::to_string(&obj).unwrap_or_else(|_| "{}".to_string())
228}
229
230#[cfg(not(feature = "report-json"))]
231fn format_json(_data: &ReportData) -> String {
232 eprintln!(
233 "Warning: SEQ_REPORT=json requires the 'report-json' feature. Falling back to human format."
234 );
235 format_human(_data)
236}
237
238struct WordCountData {
244 counters: *const u64,
245 names: *const *const u8,
246 count: usize,
247}
248
249unsafe impl Send for WordCountData {}
251unsafe impl Sync for WordCountData {}
252
253static WORD_COUNT_DATA: OnceLock<WordCountData> = OnceLock::new();
254
255fn read_word_counts() -> Option<Vec<(String, u64)>> {
256 let data = WORD_COUNT_DATA.get()?;
257 let mut counts = Vec::with_capacity(data.count);
258
259 unsafe {
260 for i in 0..data.count {
261 let counter_val = std::ptr::read_volatile(data.counters.add(i));
262 let name_ptr = *data.names.add(i);
263 let name = std::ffi::CStr::from_ptr(name_ptr as *const i8)
264 .to_string_lossy()
265 .into_owned();
266 counts.push((name, counter_val));
267 }
268 }
269
270 counts.sort_by(|a, b| b.1.cmp(&a.1));
272 Some(counts)
273}
274
275fn emit_report() {
280 let config = match get_report_config() {
281 Some(c) => c,
282 None => return,
283 };
284
285 let data = collect_report_data(config.include_words);
286
287 let output = match config.format {
288 ReportFormat::Human => format_human(&data),
289 ReportFormat::Json => format_json(&data),
290 };
291
292 match &config.destination {
293 ReportDestination::Stderr => {
294 let _ = std::io::stderr().write_all(output.as_bytes());
295 }
296 ReportDestination::File(path) => {
297 if let Ok(mut f) = std::fs::File::create(path) {
298 let _ = f.write_all(output.as_bytes());
299 } else {
300 eprintln!("Warning: could not write report to {}", path);
301 let _ = std::io::stderr().write_all(output.as_bytes());
302 }
303 }
304 }
305}
306
307#[unsafe(no_mangle)]
316pub unsafe extern "C" fn patch_seq_report() {
317 emit_report();
318}
319
320#[unsafe(no_mangle)]
327pub unsafe extern "C" fn patch_seq_report_init(
328 counters: *const u64,
329 names: *const *const u8,
330 count: i64,
331) {
332 if counters.is_null() || names.is_null() || count <= 0 {
333 return;
334 }
335 let _ = WORD_COUNT_DATA.set(WordCountData {
336 counters,
337 names,
338 count: count as usize,
339 });
340}
341
342#[cfg(test)]
347mod tests;