Skip to main content

actl_core/
trace.rs

1//! stderr 阶段打点(`ACTL_TRACE=1` 启用):慢环境归因仪器。
2//!
3//! stdout 契约不变(ADR-002)——trace 只进 stderr,禁用时零输出。
4//! 用途:慢日把"命令总耗时"分解到引擎阶段(窗口解析/唤醒/树遍历/L4/pattern),
5//! 与 envelope 的 duration_ms/wake_ms/waited_ms 构成完整观测面:
6//! envelope 字段 = 机器消费的常态数据;trace = 人类诊断的按需旁路。
7//! env 只在进程内读一次(OnceLock,同 timing.rs 纪律)。
8
9use std::sync::OnceLock;
10use std::time::Instant;
11
12/// 是否启用(ACTL_TRACE 存在且非 "0"/空,或设置了 ACTL_TRACE_FILE)。
13pub fn enabled() -> bool {
14    static ON: OnceLock<bool> = OnceLock::new();
15    *ON.get_or_init(|| {
16        (std::env::var("ACTL_TRACE")
17            .map(|v| !v.is_empty() && v != "0")
18            .unwrap_or(false))
19            || file_path().is_some()
20    })
21}
22
23/// 可选落盘路径(`ACTL_TRACE_FILE`):追加写,供显示端派生的执行器保留阶段证据。
24/// stderr 输出不变;文件写失败静默忽略(诊断旁路不得影响命令结果)。
25fn file_path() -> Option<&'static std::path::Path> {
26    static PATH: OnceLock<Option<std::path::PathBuf>> = OnceLock::new();
27    PATH.get_or_init(|| {
28        std::env::var_os("ACTL_TRACE_FILE")
29            .filter(|v| !v.is_empty())
30            .map(std::path::PathBuf::from)
31    })
32    .as_deref()
33}
34
35fn emit(line: &str) {
36    if enabled() {
37        eprintln!("{line}");
38    }
39    if let Some(path) = file_path() {
40        use std::io::Write;
41        if let Ok(mut file) = std::fs::OpenOptions::new()
42            .create(true)
43            .append(true)
44            .open(path)
45        {
46            let _ = writeln!(file, "{line}");
47        }
48    }
49}
50
51/// 打一个阶段耗时点:`[actl-trace] <stage> <ms>ms`(未启用时 no-op)。
52pub fn stage(stage: &str, started: Instant) {
53    if enabled() || file_path().is_some() {
54        emit(&format!(
55            "[actl-trace] {stage} {}ms",
56            started.elapsed().as_millis()
57        ));
58    }
59}
60
61/// 区间计时 guard:`let _t = trace::scope("locate.dfs");` 离开作用域自动打点。
62/// 未启用时 Drop 内不输出(构造开销一次 Instant,纳秒级)。
63pub struct TraceScope {
64    stage: &'static str,
65    started: Instant,
66}
67
68pub fn scope(stage: &'static str) -> TraceScope {
69    if enabled() || file_path().is_some() {
70        emit(&format!("[actl-trace] {stage} begin"));
71    }
72    TraceScope {
73        stage,
74        started: Instant::now(),
75    }
76}
77
78impl Drop for TraceScope {
79    fn drop(&mut self) {
80        stage(self.stage, self.started);
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn disabled_by_default_and_stage_is_silent_noop() {
90        // 不设 env(测试进程默认未设)→ enabled() 为 false;stage 不 panic 即可
91        let ok = std::panic::catch_unwind(|| {
92            stage("test.point", Instant::now());
93            let _t = scope("test.scope");
94        });
95        assert!(ok.is_ok());
96    }
97
98    #[test]
99    fn scope_drop_invokes_stage_without_panic() {
100        let _t = scope("test.lifetime");
101        drop(_t);
102    }
103}