#![cfg(feature = "diagnostics")]
use crate::channel::{TOTAL_MESSAGES_RECEIVED, TOTAL_MESSAGES_SENT};
use crate::memory_stats::memory_registry;
use crate::scheduler::{PEAK_STRANDS, TOTAL_COMPLETED, TOTAL_SPAWNED, scheduler_elapsed};
use std::io::Write;
use std::sync::OnceLock;
use std::sync::atomic::Ordering;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReportFormat {
Human,
Json,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReportDestination {
Stderr,
File(String),
}
#[derive(Debug, Clone)]
pub struct ReportConfig {
pub format: ReportFormat,
pub destination: ReportDestination,
pub include_words: bool,
}
impl ReportConfig {
pub fn from_env() -> Option<Self> {
let val = std::env::var("SEQ_REPORT").ok()?;
if val.is_empty() {
return None;
}
match val.as_str() {
"0" => None,
"1" => Some(ReportConfig {
format: ReportFormat::Human,
destination: ReportDestination::Stderr,
include_words: false,
}),
"words" => Some(ReportConfig {
format: ReportFormat::Human,
destination: ReportDestination::Stderr,
include_words: true,
}),
"json" => Some(ReportConfig {
format: ReportFormat::Json,
destination: ReportDestination::Stderr,
include_words: false,
}),
s if s.starts_with("json:") => {
let path = s[5..].to_string();
Some(ReportConfig {
format: ReportFormat::Json,
destination: ReportDestination::File(path),
include_words: false,
})
}
_ => {
eprintln!("Warning: SEQ_REPORT='{}' not recognized, ignoring", val);
None
}
}
}
}
static REPORT_CONFIG: OnceLock<Option<ReportConfig>> = OnceLock::new();
fn get_report_config() -> &'static Option<ReportConfig> {
REPORT_CONFIG.get_or_init(ReportConfig::from_env)
}
#[derive(Debug)]
pub struct ReportData {
pub wall_clock_ms: u64,
pub total_spawned: u64,
pub total_completed: u64,
pub peak_strands: usize,
pub active_threads: usize,
pub total_arena_bytes: u64,
pub total_peak_arena_bytes: u64,
pub messages_sent: u64,
pub messages_received: u64,
pub word_counts: Option<Vec<(String, u64)>>,
}
fn collect_report_data(include_words: bool) -> ReportData {
let wall_clock_ms = scheduler_elapsed()
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let mem_stats = memory_registry().aggregate_stats();
let word_counts = if include_words {
read_word_counts()
} else {
None
};
ReportData {
wall_clock_ms,
total_spawned: TOTAL_SPAWNED.load(Ordering::Relaxed),
total_completed: TOTAL_COMPLETED.load(Ordering::Relaxed),
peak_strands: PEAK_STRANDS.load(Ordering::Relaxed),
active_threads: mem_stats.active_threads,
total_arena_bytes: mem_stats.total_arena_bytes,
total_peak_arena_bytes: mem_stats.total_peak_arena_bytes,
messages_sent: TOTAL_MESSAGES_SENT.load(Ordering::Relaxed),
messages_received: TOTAL_MESSAGES_RECEIVED.load(Ordering::Relaxed),
word_counts,
}
}
fn format_human(data: &ReportData) -> String {
let mut out = String::new();
out.push_str("=== SEQ REPORT ===\n");
out.push_str(&format!("Wall clock: {} ms\n", data.wall_clock_ms));
out.push_str(&format!("Strands spawned: {}\n", data.total_spawned));
out.push_str(&format!("Strands done: {}\n", data.total_completed));
out.push_str(&format!("Peak strands: {}\n", data.peak_strands));
out.push_str(&format!("Worker threads: {}\n", data.active_threads));
out.push_str(&format!(
"Arena current: {} bytes\n",
data.total_arena_bytes
));
out.push_str(&format!(
"Arena peak: {} bytes\n",
data.total_peak_arena_bytes
));
out.push_str(&format!("Messages sent: {}\n", data.messages_sent));
out.push_str(&format!("Messages recv: {}\n", data.messages_received));
if let Some(ref counts) = data.word_counts {
out.push_str("\n--- Word Call Counts ---\n");
for (name, count) in counts {
out.push_str(&format!(" {:30} {}\n", name, count));
}
}
out.push_str("==================\n");
out
}
#[cfg(feature = "report-json")]
fn format_json(data: &ReportData) -> String {
let mut map = serde_json::Map::new();
map.insert(
"wall_clock_ms".into(),
serde_json::Value::Number(data.wall_clock_ms.into()),
);
map.insert(
"strands_spawned".into(),
serde_json::Value::Number(data.total_spawned.into()),
);
map.insert(
"strands_completed".into(),
serde_json::Value::Number(data.total_completed.into()),
);
map.insert(
"peak_strands".into(),
serde_json::Value::Number((data.peak_strands as u64).into()),
);
map.insert(
"worker_threads".into(),
serde_json::Value::Number((data.active_threads as u64).into()),
);
map.insert(
"arena_bytes".into(),
serde_json::Value::Number(data.total_arena_bytes.into()),
);
map.insert(
"arena_peak_bytes".into(),
serde_json::Value::Number(data.total_peak_arena_bytes.into()),
);
map.insert(
"messages_sent".into(),
serde_json::Value::Number(data.messages_sent.into()),
);
map.insert(
"messages_received".into(),
serde_json::Value::Number(data.messages_received.into()),
);
if let Some(ref counts) = data.word_counts {
let word_map: serde_json::Map<String, serde_json::Value> = counts
.iter()
.map(|(name, count)| (name.clone(), serde_json::Value::Number((*count).into())))
.collect();
map.insert("word_counts".into(), serde_json::Value::Object(word_map));
}
let obj = serde_json::Value::Object(map);
serde_json::to_string(&obj).unwrap_or_else(|_| "{}".to_string())
}
#[cfg(not(feature = "report-json"))]
fn format_json(_data: &ReportData) -> String {
eprintln!(
"Warning: SEQ_REPORT=json requires the 'report-json' feature. Falling back to human format."
);
format_human(_data)
}
struct WordCountData {
counters: *const u64,
names: *const *const u8,
count: usize,
}
unsafe impl Send for WordCountData {}
unsafe impl Sync for WordCountData {}
static WORD_COUNT_DATA: OnceLock<WordCountData> = OnceLock::new();
fn read_word_counts() -> Option<Vec<(String, u64)>> {
let data = WORD_COUNT_DATA.get()?;
let mut counts = Vec::with_capacity(data.count);
unsafe {
for i in 0..data.count {
let counter_val = std::ptr::read_volatile(data.counters.add(i));
let name_ptr = *data.names.add(i);
let name = std::ffi::CStr::from_ptr(name_ptr as *const i8)
.to_string_lossy()
.into_owned();
counts.push((name, counter_val));
}
}
counts.sort_by(|a, b| b.1.cmp(&a.1));
Some(counts)
}
fn emit_report() {
let config = match get_report_config() {
Some(c) => c,
None => return,
};
let data = collect_report_data(config.include_words);
let output = match config.format {
ReportFormat::Human => format_human(&data),
ReportFormat::Json => format_json(&data),
};
match &config.destination {
ReportDestination::Stderr => {
let _ = std::io::stderr().write_all(output.as_bytes());
}
ReportDestination::File(path) => {
if let Ok(mut f) = std::fs::File::create(path) {
let _ = f.write_all(output.as_bytes());
} else {
eprintln!("Warning: could not write report to {}", path);
let _ = std::io::stderr().write_all(output.as_bytes());
}
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_report() {
emit_report();
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_report_init(
counters: *const u64,
names: *const *const u8,
count: i64,
) {
if counters.is_null() || names.is_null() || count <= 0 {
return;
}
let _ = WORD_COUNT_DATA.set(WordCountData {
counters,
names,
count: count as usize,
});
}
#[cfg(test)]
mod tests;