1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//! Runtime diagnostics for production debugging
//!
//! Provides a SIGQUIT (kill -3) handler that dumps runtime statistics to stderr,
//! similar to JVM thread dumps. This is useful for debugging production issues
//! without stopping the process.
//!
//! ## Usage
//!
//! Send SIGQUIT to a running Seq process:
//! ```bash
//! kill -3 <pid>
//! ```
//!
//! The process will dump diagnostics to stderr and continue running.
//!
//! ## Signal Safety
//!
//! Signal handlers can only safely call async-signal-safe functions. Our
//! dump_diagnostics() does I/O and acquires locks, which is NOT safe to call
//! directly from a signal handler. Instead, we spawn a dedicated thread that
//! waits for signals using signal-hook's iterator API, making all the I/O
//! operations safe.
//!
//! ## Feature Flag
//!
//! This module is only compiled when the `diagnostics` feature is enabled (default).
//! Disable it for benchmarks to eliminate SystemTime::now() syscalls and strand
//! registry overhead on every spawn.
#![cfg(feature = "diagnostics")]
use crate::memory_stats::memory_registry;
use crate::scheduler::{
ACTIVE_STRANDS, PEAK_STRANDS, TOTAL_COMPLETED, TOTAL_SPAWNED, strand_registry,
};
use std::sync::Once;
use std::sync::atomic::Ordering;
static SIGNAL_HANDLER_INIT: Once = Once::new();
/// Maximum number of individual strands to display in diagnostics output
/// to avoid overwhelming the output for programs with many strands
const STRAND_DISPLAY_LIMIT: usize = 20;
/// Install the SIGQUIT signal handler for diagnostics
///
/// This is called automatically by scheduler_init, but can be called
/// explicitly if needed. Safe to call multiple times (idempotent).
///
/// # Implementation
///
/// Uses a dedicated thread to handle signals safely. The signal-hook iterator
/// API ensures we're not calling non-async-signal-safe functions from within
/// a signal handler context.
pub fn install_signal_handler() {
SIGNAL_HANDLER_INIT.call_once(|| {
#[cfg(unix)]
{
use signal_hook::consts::SIGQUIT;
use signal_hook::iterator::Signals;
// Create signal iterator - this is safe and doesn't block
let mut signals = match Signals::new([SIGQUIT]) {
Ok(s) => s,
Err(_) => return, // Silently fail if we can't register
};
// Spawn a dedicated thread to handle signals
// This thread blocks waiting for signals, then safely calls dump_diagnostics()
std::thread::Builder::new()
.name("seq-diagnostics".to_string())
.spawn(move || {
for sig in signals.forever() {
if sig == SIGQUIT {
dump_diagnostics();
}
}
})
.ok(); // Silently fail if thread spawn fails
}
#[cfg(not(unix))]
{
// Signal handling not supported on non-Unix platforms
// Diagnostics can still be called directly via dump_diagnostics()
}
});
}
/// Dump runtime diagnostics to stderr
///
/// This can be called directly from code or triggered via SIGQUIT.
/// Output goes to stderr to avoid mixing with program output.
pub fn dump_diagnostics() {
use std::io::Write;
let mut out = std::io::stderr().lock();
let _ = writeln!(out, "\n=== Seq Runtime Diagnostics ===");
let _ = writeln!(out, "Timestamp: {:?}", std::time::SystemTime::now());
// Strand statistics (global atomics - accurate)
let active = ACTIVE_STRANDS.load(Ordering::Relaxed);
let total_spawned = TOTAL_SPAWNED.load(Ordering::Relaxed);
let total_completed = TOTAL_COMPLETED.load(Ordering::Relaxed);
let peak = PEAK_STRANDS.load(Ordering::Relaxed);
let _ = writeln!(out, "\n[Strands]");
let _ = writeln!(out, " Active: {}", active);
let _ = writeln!(out, " Spawned: {} (total)", total_spawned);
let _ = writeln!(out, " Completed: {} (total)", total_completed);
let _ = writeln!(out, " Peak: {} (high-water mark)", peak);
// Calculate potential leak indicator
// If spawned > completed + active, some strands were lost (panic, etc.)
let expected_completed = total_spawned.saturating_sub(active as u64);
if total_completed < expected_completed {
let lost = expected_completed - total_completed;
let _ = writeln!(
out,
" WARNING: {} strands may have been lost (panic/abort)",
lost
);
}
// Active strand details from registry
let registry = strand_registry();
let overflow = registry.overflow_count.load(Ordering::Relaxed);
let _ = writeln!(out, "\n[Active Strand Details]");
let _ = writeln!(out, " Registry capacity: {} slots", registry.capacity());
if overflow > 0 {
let _ = writeln!(
out,
" WARNING: {} strands exceeded registry capacity (not tracked)",
overflow
);
}
// Get current time for duration calculation
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
// Collect and sort active strands by spawn time (oldest first)
let mut strands: Vec<_> = registry.active_strands().collect();
strands.sort_by_key(|(_, spawn_time)| *spawn_time);
if strands.is_empty() {
let _ = writeln!(out, " (no active strands in registry)");
} else {
let _ = writeln!(out, " {} strand(s) tracked:", strands.len());
for (idx, (strand_id, spawn_time)) in strands.iter().take(STRAND_DISPLAY_LIMIT).enumerate()
{
let duration = now.saturating_sub(*spawn_time);
let _ = writeln!(
out,
" [{:2}] Strand #{:<8} running for {}s",
idx + 1,
strand_id,
duration
);
}
if strands.len() > STRAND_DISPLAY_LIMIT {
let _ = writeln!(
out,
" ... and {} more strands",
strands.len() - STRAND_DISPLAY_LIMIT
);
}
}
// Memory statistics (cross-thread aggregation)
let _ = writeln!(out, "\n[Memory]");
let mem_stats = memory_registry().aggregate_stats();
let _ = writeln!(out, " Tracked threads: {}", mem_stats.active_threads);
let _ = writeln!(
out,
" Arena bytes: {} (across all threads)",
format_bytes(mem_stats.total_arena_bytes)
);
if mem_stats.overflow_count > 0 {
let _ = writeln!(
out,
" WARNING: {} threads exceeded registry capacity (memory not tracked)",
mem_stats.overflow_count
);
let _ = writeln!(
out,
" Consider increasing MAX_THREADS in memory_stats.rs (currently 64)"
);
}
// Note: Channel stats are not available with the zero-mutex design.
// Channels are passed directly as Value::Channel on the stack with no global registry.
let _ = writeln!(out, "\n=== End Diagnostics ===\n");
}
/// Format bytes as human-readable string
fn format_bytes(bytes: u64) -> String {
if bytes >= 1024 * 1024 * 1024 {
format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
} else if bytes >= 1024 * 1024 {
format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
} else if bytes >= 1024 {
format!("{:.2} KB", bytes as f64 / 1024.0)
} else {
format!("{} B", bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dump_diagnostics_runs() {
// Just verify it doesn't panic
dump_diagnostics();
}
#[test]
fn test_install_signal_handler_idempotent() {
// Should be safe to call multiple times
install_signal_handler();
install_signal_handler();
install_signal_handler();
}
}