krabka_telemetry/profiling/
routes.rs1#[cfg(any(unix, test))]
9use krabka_units::{Time, secs};
10use {
11 super::config::{ProfilingConfig, ProfilingError},
12 axum::{
13 Router,
14 extract::{Query, State},
15 http::StatusCode,
16 response::{IntoResponse, Response},
17 routing::get,
18 },
19 flate2::write::GzEncoder,
20 serde::Deserialize,
21 tokio::{task, time},
22};
23#[cfg(unix)]
24use {krabka_units::convert::TimeExt as _, std::io};
25
26#[derive(Debug, Deserialize)]
27struct CpuQuery {
28 #[cfg_attr(not(unix), allow(dead_code))]
29 seconds: Option<u64>,
30}
31
32#[cfg(all(unix, feature = "heap-profiling"))]
33#[derive(Debug, Deserialize)]
34struct HeapQuery {
35 seconds: Option<u64>,
36}
37
38#[cfg(unix)]
43async fn cpu_profile(State(config): State<ProfilingConfig>, Query(q): Query<CpuQuery>) -> Response {
44 let duration = requested_duration(
45 q.seconds,
46 config.profiling_cpu_default_duration,
47 config.profiling_cpu_max_duration,
48 );
49 let guard = match profile_work(move || start_cpu_profile(&config)).await {
50 Ok(guard) => guard,
51 Err(error) => return (StatusCode::INTERNAL_SERVER_ERROR, error).into_response(),
52 };
53 time::sleep(duration.to_std()).await;
54 match profile_work(move || finish_cpu_profile(guard)).await {
55 Ok(body) => (
56 StatusCode::OK,
57 [("content-type", "application/octet-stream")],
58 body,
59 )
60 .into_response(),
61 Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error).into_response(),
62 }
63}
64
65#[cfg(unix)]
67async fn profile_work<T: Send + 'static>(
68 work: impl FnOnce() -> Result<T, String> + Send + 'static,
69) -> Result<T, String> {
70 task::spawn_blocking(work)
71 .await
72 .map_err(|error| format!("profiling worker: {error}"))?
73}
74
75#[cfg(unix)]
76fn start_cpu_profile(config: &ProfilingConfig) -> Result<pprof::ProfilerGuard<'static>, String> {
77 let blocklist = config
78 .profiling_native_frame_blocklist
79 .iter()
80 .map(String::as_str)
81 .collect::<Vec<_>>();
82 pprof::ProfilerGuardBuilder::default()
83 .frequency(config.profiling_cpu_sample_frequency.hertz())
84 .blocklist(&blocklist)
85 .build()
86 .map_err(|error| format!("profiler: {error}"))
87}
88
89#[cfg(unix)]
90fn finish_cpu_profile(guard: pprof::ProfilerGuard<'static>) -> Result<Vec<u8>, String> {
91 use pprof::protos::Message as _;
93
94 let report = guard
95 .report()
96 .build()
97 .map_err(|error| format!("report: {error}"))?;
98 drop(guard);
99 let profile = report.pprof().map_err(|error| format!("pprof: {error}"))?;
100 gzip(&profile.encode_to_vec()).map_err(|error| format!("gzip: {error}"))
101}
102
103#[cfg(unix)]
107fn gzip(raw: &[u8]) -> io::Result<Vec<u8>> {
108 use std::io::Write as _;
109
110 let mut encoder = GzEncoder::new(
111 Vec::with_capacity(raw.len() / 2),
112 flate2::Compression::fast(),
113 );
114 encoder.write_all(raw)?;
115 encoder.finish()
116}
117
118#[cfg(not(unix))]
121#[cfg_attr(test, mutants::skip)]
122async fn cpu_profile(
123 _config: State<ProfilingConfig>,
124 _q: Query<CpuQuery>,
125) -> axum::response::Response {
126 (
127 StatusCode::SERVICE_UNAVAILABLE,
128 "CPU profiling requires a Unix target",
129 )
130 .into_response()
131}
132
133#[cfg(all(unix, feature = "heap-profiling"))]
135#[cfg_attr(test, mutants::skip)]
136async fn heap_profile(
137 State(config): State<ProfilingConfig>,
138 Query(q): Query<HeapQuery>,
139) -> Response {
140 let Some(ctl) = jemalloc_pprof::PROF_CTL.as_ref() else {
141 return (
142 StatusCode::INTERNAL_SERVER_ERROR,
143 "jemalloc profiling not enabled (build with --features heap-profiling and set MALLOC_CONF)",
144 )
145 .into_response();
146 };
147 let mut ctl = ctl.lock().await;
148 let activated_here = !ctl.activated();
149 if activated_here {
150 if let Err(e) = ctl.activate() {
151 return (
152 StatusCode::INTERNAL_SERVER_ERROR,
153 format!("jemalloc prof activate: {e}"),
154 )
155 .into_response();
156 }
157 let duration = requested_duration(
158 q.seconds,
159 config.profiling_heap_default_duration,
160 config.profiling_heap_max_duration,
161 );
162 time::sleep(duration.to_std()).await;
163 }
164 let dump = ctl.dump_pprof();
165 if activated_here && let Err(e) = ctl.deactivate() {
166 tracing::warn!(error = %e, "could not deactivate jemalloc profiling after heap dump");
167 }
168 drop(ctl);
169 match dump {
170 Ok(pprof) => (
171 StatusCode::OK,
172 [("content-type", "application/octet-stream")],
173 pprof,
174 )
175 .into_response(),
176 Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("heap dump: {e}")).into_response(),
177 }
178}
179
180pub fn pprof_router_with_config(config: ProfilingConfig) -> Result<Router, ProfilingError> {
189 config.validate().map_err(ProfilingError::Config)?;
190 Ok(pprof_router_unchecked(config))
191}
192
193fn pprof_router_unchecked(config: ProfilingConfig) -> Router {
194 let router = Router::new().route("/debug/pprof/profile", get(cpu_profile));
195 #[cfg(all(unix, feature = "heap-profiling"))]
196 let router = router.route("/debug/pprof/heap", get(heap_profile));
197 router.with_state(config)
198}
199
200pub fn pprof_router() -> Router { pprof_router_unchecked(ProfilingConfig::default()) }
202
203#[cfg(any(unix, test))]
204fn requested_duration(seconds: Option<u64>, default: Time, maximum: Time) -> Time {
205 seconds
206 .map_or(default, |seconds| {
207 secs(u32::try_from(seconds.max(1)).unwrap_or(u32::MAX))
208 })
209 .min(maximum)
210}
211
212#[cfg(test)]
213mod tests {
214 use {
215 super::*,
216 assert2::assert,
217 krabka_units::{millis, minutes},
218 std::{sync::mpsc, time::Duration},
219 tokio::sync::oneshot,
220 };
221
222 #[cfg(unix)]
224 #[tokio::test]
225 async fn profiling_work_does_not_block_runtime_progress() {
226 let (started, waiting) = oneshot::channel();
227 let (release, released) = mpsc::channel();
228 let work = profile_work(move || {
229 started.send(()).unwrap();
230 released
231 .recv_timeout(Duration::from_secs(5))
232 .map_err(|error| error.to_string())?;
233 Ok(42)
234 });
235 let other_task = async move {
236 waiting.await.unwrap();
237 release.send(()).unwrap();
238 };
239 let (result, ()) = tokio::join!(work, other_task);
240 assert!(result == Ok(42));
241 assert!(
242 profile_work(|| Err::<(), _>("report failed".into())).await
243 == Err("report failed".into())
244 );
245 }
246
247 #[test]
248 fn requested_profile_duration_uses_configured_default_floor_and_cap() {
249 assert!(requested_duration(None, secs(2), secs(5)) == secs(2));
250 assert!(requested_duration(Some(0), secs(2), secs(5)) == secs(1));
251 assert!(requested_duration(Some(3), secs(2), secs(5)) == secs(3));
252 assert!(requested_duration(Some(9), secs(2), secs(5)) == secs(5));
253 assert!(
254 (ProfilingConfig {
255 profiling_cpu_max_duration: millis(500),
256 ..ProfilingConfig::default()
257 }
258 .validate()
259 .is_err())
260 );
261 assert!(
262 (ProfilingConfig {
263 profiling_heap_default_duration: minutes(1),
264 ..ProfilingConfig::default()
265 }
266 .validate()
267 .is_err())
268 );
269 }
270}