camel_cli/commands/
bench_instrument.rs1use 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
28const BENCH_START: &str = "BenchStart";
30
31pub 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 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
59fn 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)); 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
75fn 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
87fn 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 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 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}