Skip to main content

ciri_toolkit/
runtime.rs

1//! Runtime diagnostics configuration shared by Scan1/Scan2/validator code.
2//!
3//! The hot paths still expose the historical `CIRI_TRACE_*` / `CIRI_PROFILE_*`
4//! environment-variable behavior for ad hoc debugging, but the preferred entry
5//! point is now the CLI. This module keeps that wiring in one place so the
6//! algorithm code can query "should I trace/profile?" without knowing whether
7//! the request came from an environment variable or an explicit command-line
8//! flag.
9
10use anyhow::Result;
11use std::collections::HashSet;
12use std::fs::File;
13use std::io::{BufWriter, Write};
14use std::sync::{Mutex, OnceLock};
15
16/// Process-wide runtime diagnostics options initialized from the CLI.
17struct RuntimeConfig {
18    trace_reads: HashSet<String>,
19    trace_writer: Option<Mutex<BufWriter<File>>>,
20    perf_writer: Option<Mutex<BufWriter<File>>>,
21}
22
23static RUNTIME_CONFIG: OnceLock<RuntimeConfig> = OnceLock::new();
24
25thread_local! {
26    /// Tracks whether the current thread is inside a traced `is_bsj_hg2` call.
27    ///
28    /// This keeps `--trace` focused on the requested read IDs instead of turning
29    /// on verbose validator branch logging for every candidate in the process.
30    static TRACE_HG2_ACTIVE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
31}
32
33/// Initializes the optional trace/perf outputs for the current process.
34///
35/// This should be called once near startup. Later calls are ignored so tests
36/// and library users do not need to coordinate teardown.
37pub fn init_runtime(
38    trace_reads: Option<&str>,
39    trace_log_path: Option<&str>,
40    perf_path: Option<&str>,
41) -> Result<()> {
42    let reads = trace_reads
43        .map(|raw| {
44            raw.split(',')
45                .filter_map(|token| {
46                    let t = token.trim();
47                    (!t.is_empty()).then(|| t.to_string())
48                })
49                .collect::<HashSet<_>>()
50        })
51        .unwrap_or_default();
52    let trace_writer = match (trace_log_path, reads.is_empty()) {
53        (Some(path), false) => Some(Mutex::new(BufWriter::new(File::create(path)?))),
54        _ => None,
55    };
56    let perf_writer = match perf_path {
57        Some(path) => Some(Mutex::new(BufWriter::new(File::create(path)?))),
58        None => None,
59    };
60    let _ = RUNTIME_CONFIG.set(RuntimeConfig {
61        trace_reads: reads,
62        trace_writer,
63        perf_writer,
64    });
65    Ok(())
66}
67
68/// Returns whether the given read ID is selected for targeted tracing.
69#[inline]
70pub fn should_trace_read(read_id: &str) -> bool {
71    if let Some(config) = RUNTIME_CONFIG.get() {
72        if config.trace_reads.contains(read_id) {
73            return true;
74        }
75    }
76    if let Ok(raw) = std::env::var("CIRI_TRACE_READS") {
77        for token in raw.split(',') {
78            let t = token.trim();
79            if !t.is_empty() && t == read_id {
80                return true;
81            }
82        }
83    }
84    false
85}
86
87/// Returns whether any CLI-provided trace reads are active.
88#[inline]
89pub fn cli_trace_enabled() -> bool {
90    RUNTIME_CONFIG
91        .get()
92        .is_some_and(|config| !config.trace_reads.is_empty())
93}
94
95/// Executes a closure while enabling `is_bsj_hg2` branch tracing on this thread.
96#[inline]
97pub fn with_trace_hg2_scope<T>(enabled: bool, f: impl FnOnce() -> T) -> T {
98    let prev = TRACE_HG2_ACTIVE.with(|flag| {
99        let prev = flag.get();
100        flag.set(enabled);
101        prev
102    });
103    let out = f();
104    TRACE_HG2_ACTIVE.with(|flag| flag.set(prev));
105    out
106}
107
108/// Returns whether `is_bsj_hg2` branch-level trace output is active.
109#[inline]
110pub fn trace_hg2_enabled() -> bool {
111    TRACE_HG2_ACTIVE.with(|flag| flag.get())
112        || matches!(std::env::var("CIRI_TRACE_HG2"), Ok(v) if !v.is_empty() && v != "0")
113}
114
115/// Returns whether Scan1 profiling is enabled.
116#[inline]
117pub fn scan1_profile_enabled() -> bool {
118    RUNTIME_CONFIG
119        .get()
120        .is_some_and(|config| config.perf_writer.is_some())
121        || matches!(std::env::var("CIRI_PROFILE_SCAN1"), Ok(v) if !v.is_empty() && v != "0")
122}
123
124/// Returns whether Scan2 profiling is enabled.
125#[inline]
126pub fn scan2_profile_enabled() -> bool {
127    RUNTIME_CONFIG
128        .get()
129        .is_some_and(|config| config.perf_writer.is_some())
130        || matches!(std::env::var("CIRI_PROFILE_SCAN2"), Ok(v) if !v.is_empty() && v != "0")
131}
132
133/// Emits one targeted read-trace line either to the configured file or stderr.
134pub fn emit_trace_line(line: &str) {
135    if let Some(config) = RUNTIME_CONFIG.get() {
136        if let Some(writer) = &config.trace_writer {
137            let mut writer = writer.lock().unwrap();
138            let _ = writeln!(writer, "{}", line);
139            let _ = writer.flush();
140            return;
141        }
142    }
143    eprintln!("{}", line);
144}
145
146/// Emits one profiling line either to the configured file or to stderr.
147pub fn emit_perf_line(line: &str) {
148    if let Some(config) = RUNTIME_CONFIG.get() {
149        if let Some(writer) = &config.perf_writer {
150            let mut writer = writer.lock().unwrap();
151            let _ = writeln!(writer, "{}", line);
152            let _ = writer.flush();
153            return;
154        }
155    }
156    eprintln!("{}", line);
157}