Skip to main content

camel_cli/commands/
bench_instrument.rs

1//! Benchmark timing injection for YAML-loaded routes.
2//!
3//! When `BENCH_LATENCY_FILE` env var is set, every top-level `BuilderStep::To(_)`
4//! in each route definition is wrapped with two processors:
5//!
6//! 1. **Pre**: stores `Arc<Instant::now()>` in `exchange.extensions["BenchStart"]`
7//! 2. **Post**: reads it back, computes `elapsed().as_nanos()`, writes
8//!    `BENCH_LATENCY <id> <duration_ns>` to the shared file.
9//!
10//! Per-pair `Arc<AtomicU64>` counter guarantees coherent ids. The latency
11//! file is opened once at injection time and shared via `Arc<Mutex<File>>`
12//! (no per-tick reopen). When the env var is unset, this module is a no-op.
13//!
14//! Only top-level `To` steps are wrapped — nested steps inside `Choice`,
15//! `Split`, `Filter` etc. are left untouched (wrapping those would measure
16//! per-sub-message latency, not per-tick bridge tax).
17
18use std::fs::{File, OpenOptions};
19use std::io::Write;
20use std::sync::Arc;
21use std::sync::Mutex;
22use std::sync::atomic::{AtomicU64, Ordering};
23use std::time::Instant;
24
25use camel_api::{BoxProcessor, BoxProcessorExt, Exchange, OpaqueProcessor};
26use camel_core::{BuilderStep, RouteDefinition};
27
28/// Extension key under which the pre-`.to()` `Instant` is stored.
29const BENCH_START: &str = "BenchStart";
30
31/// If `BENCH_LATENCY_FILE` is set, instrument each route by wrapping
32/// top-level `To` steps with timing processors. Returns defs unchanged
33/// when the env var is absent (zero-cost no-op).
34pub fn maybe_instrument_routes(defs: Vec<RouteDefinition>) -> Vec<RouteDefinition> {
35    let Ok(path) = std::env::var("BENCH_LATENCY_FILE") else {
36        return defs;
37    };
38
39    let file = match OpenOptions::new().create(true).append(true).open(&path) {
40        Ok(f) => f,
41        Err(e) => {
42            // log-policy: system-broken
43            tracing::error!("bench_instrument: cannot open BENCH_LATENCY_FILE '{path}': {e}");
44            return defs;
45        }
46    };
47    let shared_file = Arc::new(Mutex::new(file));
48
49    tracing::info!("bench_instrument: wrapping top-level To steps (file={path})");
50
51    defs.into_iter()
52        .map(|def| {
53            let sf = Arc::clone(&shared_file);
54            def.map_steps(|steps| inject_timing(steps, sf))
55        })
56        .collect()
57}
58
59/// Walk the flat step list and insert pre/post processors around each `To`.
60fn inject_timing(steps: Vec<BuilderStep>, file: Arc<Mutex<File>>) -> Vec<BuilderStep> {
61    let mut result = Vec::with_capacity(steps.len() * 3);
62    for step in steps {
63        if matches!(step, BuilderStep::To(_)) {
64            let counter = Arc::new(AtomicU64::new(0)); // per-pair: ids unique within one To step
65            result.push(make_start_processor());
66            result.push(step);
67            result.push(make_end_processor(counter, Arc::clone(&file)));
68        } else {
69            result.push(step);
70        }
71    }
72    result
73}
74
75/// Create a processor that stamps `Instant::now()` into the exchange extension.
76fn make_start_processor() -> BuilderStep {
77    BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(
78        |mut exchange: Exchange| {
79            Box::pin(async move {
80                exchange.set_extension(BENCH_START, Arc::new(Instant::now()));
81                Ok(exchange)
82            })
83        },
84    )))
85}
86
87/// Create a processor that reads the stored `Instant` (via `Arc<dyn Any>`
88/// downcast to `Instant`), computes the delta, and appends
89/// `BENCH_LATENCY <id> <ns>` to the shared file.
90fn make_end_processor(counter: Arc<AtomicU64>, file: Arc<Mutex<File>>) -> BuilderStep {
91    BuilderStep::Processor(OpaqueProcessor(BoxProcessor::from_fn(
92        move |exchange: Exchange| {
93            let counter = Arc::clone(&counter);
94            let file = Arc::clone(&file);
95            Box::pin(async move {
96                let id = counter.fetch_add(1, Ordering::Relaxed) + 1;
97                let duration_ns = exchange
98                    .get_extension::<Instant>(BENCH_START)
99                    .map(|t| t.elapsed().as_nanos() as u64)
100                    .unwrap_or(0);
101                let line = format!("BENCH_LATENCY {id} {duration_ns}\n");
102                if let Ok(mut f) = file.lock() {
103                    let _ = f.write_all(line.as_bytes());
104                }
105                Ok(exchange)
106            })
107        },
108    )))
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use std::fs::File;
115
116    #[test]
117    fn inject_timing_wraps_each_top_level_to() {
118        let tmp = tempfile::NamedTempFile::new().unwrap();
119        let file = Arc::new(Mutex::new(File::create(tmp.path()).unwrap()));
120
121        let steps = vec![
122            BuilderStep::To("xslt:a".into()),
123            BuilderStep::Stop,
124            BuilderStep::To("xslt:b".into()),
125        ];
126        let out = inject_timing(steps, file);
127
128        // 2 To steps × 3 (pre + To + post) + 1 Stop = 7
129        assert_eq!(out.len(), 7);
130        assert!(matches!(out[0], BuilderStep::Processor(_)));
131        assert!(matches!(out[1], BuilderStep::To(_)));
132        assert!(matches!(out[2], BuilderStep::Processor(_)));
133        assert!(matches!(out[3], BuilderStep::Stop));
134        assert!(matches!(out[4], BuilderStep::Processor(_)));
135        assert!(matches!(out[5], BuilderStep::To(_)));
136        assert!(matches!(out[6], BuilderStep::Processor(_)));
137    }
138
139    #[test]
140    fn inject_timing_skips_non_to_steps() {
141        let tmp = tempfile::NamedTempFile::new().unwrap();
142        let file = Arc::new(Mutex::new(File::create(tmp.path()).unwrap()));
143
144        let steps = vec![BuilderStep::Stop, BuilderStep::Stop];
145        let out = inject_timing(steps, file);
146        assert_eq!(out.len(), 2);
147    }
148
149    #[test]
150    fn maybe_instrument_routes_noop_when_env_unset() {
151        // SAFETY: test is single-threaded, no other code reads this env var
152        // during the test.
153        unsafe {
154            std::env::remove_var("BENCH_LATENCY_FILE");
155        }
156        let def = camel_core::RouteDefinition::new(
157            "direct:test".to_string(),
158            vec![BuilderStep::To("mock:a".into())],
159        );
160        let defs = vec![def];
161        let out = maybe_instrument_routes(defs);
162        assert_eq!(out[0].steps().len(), 1);
163    }
164}