1use crate::profiling::mallctl;
21use http::{header, Method, Request, Response, StatusCode};
22use std::{collections::HashMap, env, fmt};
23
24#[inline]
25pub fn router(req: Request<Vec<u8>>) -> http::Result<Response<Vec<u8>>> {
26 match (req.method(), req.uri().path()) {
27 (&Method::GET, "/pprof/conf") => JeprofHandler(get_pprof_conf_handler).call(req),
28 (&Method::POST, "/pprof/conf") => JeprofHandler(post_pprof_conf_handler).call(req),
29 (&Method::GET, "/pprof/heap") => JeprofHandler(get_pprof_heap_handler).call(req),
30 (&Method::GET, "/pprof/cmdline") => JeprofHandler(get_pprof_cmdline_handler).call(req),
31 (&Method::GET, "/pprof/symbol") => JeprofHandler(get_pprof_symbol_handler).call(req),
32 (&Method::POST, "/pprof/symbol") => JeprofHandler(post_pprof_symbol_handler).call(req),
33 (&Method::GET, "/pprof/stats") => JeprofHandler(get_pprof_stats_handler).call(req),
34 _ => {
35 let body = b"Bad Request\r\n";
36 Response::builder()
37 .status(StatusCode::BAD_REQUEST)
38 .header(header::CONTENT_TYPE, "application/octet-stream")
39 .header(header::CONTENT_LENGTH, body.len())
40 .body(body.to_vec())
41 }
42 }
43}
44
45#[cfg(feature = "actix-handlers")]
46#[inline]
47pub fn actix_routes(cfg: &mut actix_web::web::ServiceConfig) {
48 cfg.service(
49 actix_web::web::scope("/pprof")
50 .route("/conf", actix_web::web::get().to(JeprofHandler(get_pprof_conf_handler)))
51 .route("/conf", actix_web::web::post().to(JeprofHandler(post_pprof_conf_handler)))
52 .route("/heap", actix_web::web::get().to(JeprofHandler(get_pprof_heap_handler)))
53 .route("/cmdline", actix_web::web::get().to(JeprofHandler(get_pprof_cmdline_handler)))
54 .route("/symbol", actix_web::web::get().to(JeprofHandler(get_pprof_symbol_handler)))
55 .route("/symbol", actix_web::web::post().to(JeprofHandler(post_pprof_symbol_handler)))
56 .route("/stats", actix_web::web::get().to(JeprofHandler(get_pprof_stats_handler))),
57 );
58}
59
60#[derive(Debug)]
61pub struct ErrorResponse(String);
62
63impl fmt::Display for ErrorResponse {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 write!(f, "ERROR: {}", self.0)
66 }
67}
68
69#[cfg(feature = "actix-handlers")]
70impl actix_web::ResponseError for ErrorResponse {}
71
72#[derive(Clone, Debug)]
73struct JeprofHandler<F>(F)
74where
75 F: Fn(&[u8], &HashMap<String, String>) -> Result<(Vec<u8>, Option<String>), ErrorResponse>
76 + Clone
77 + 'static;
78
79impl<F> JeprofHandler<F>
80where
81 F: Fn(&[u8], &HashMap<String, String>) -> Result<(Vec<u8>, Option<String>), ErrorResponse>
82 + Clone
83 + 'static,
84{
85 fn call(&self, req: Request<Vec<u8>>) -> http::Result<Response<Vec<u8>>> {
86 let params: HashMap<String, String> = parse_malloc_conf_query(req.uri().query())
87 .iter()
88 .map(|(k, v)| ((*k).to_string(), v.unwrap_or_default().to_string()))
89 .collect();
90 match self.0(req.body(), ¶ms) {
91 Ok((body, Some(content_disposition))) => response_ok_binary(body, &content_disposition),
92 Ok((body, None)) => response_ok(body),
93 Err(err) => response_err(&err.0),
94 }
95 }
96}
97
98#[cfg(feature = "actix-handlers")]
99impl<F>
100 actix_web::Handler<(actix_web::web::Payload, actix_web::web::Query<HashMap<String, String>>)>
101 for JeprofHandler<F>
102where
103 F: Fn(&[u8], &HashMap<String, String>) -> Result<(Vec<u8>, Option<String>), ErrorResponse>
104 + Clone
105 + 'static,
106{
107 type Output = Result<actix_web::HttpResponse, ErrorResponse>;
108 type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output>>>;
109
110 fn call(
111 &self,
112 (mut body, query): (
113 actix_web::web::Payload,
114 actix_web::web::Query<HashMap<String, String>>,
115 ),
116 ) -> Self::Future {
117 use futures_util::StreamExt as _;
118
119 let f = self.0.clone();
120 Box::pin(async move {
121 let mut data = Vec::<u8>::new();
122 while let Some(item) = body.next().await {
123 data.extend_from_slice(&item.map_err(|e| ErrorResponse(e.to_string()))?);
124 }
125 f(&data, &query.0).map(|(body, content_disposition)| {
126 let mut resp = actix_web::HttpResponse::Ok();
127 if let Some(filename) = content_disposition {
128 resp.insert_header(actix_web::http::header::ContentDisposition::attachment(
129 filename,
130 ));
131 }
132 resp.body(actix_web::web::Bytes::from(body))
133 })
134 })
135 }
136}
137
138#[inline]
139pub fn get_pprof_conf_handler(
140 _body: &[u8],
141 _params: &HashMap<String, String>,
142) -> Result<(Vec<u8>, Option<String>), ErrorResponse> {
143 match mallctl::enabled() {
144 Ok(true) => (),
145 _ => return Err(ErrorResponse("jemalloc profiling not enabled".to_owned())),
146 };
147
148 let Ok(state) = mallctl::active() else {
149 return Err(ErrorResponse("failed to read prof.active\r\n".to_owned()));
150 };
151 let Ok(sample) = mallctl::sample_interval() else {
152 return Err(ErrorResponse("failed to read prof.lg_sample\r\n".to_owned()));
153 };
154 let body = format!("prof.active:{state},prof.lg_sample:{sample}\r\n");
155 Ok((body.into_bytes(), None))
156}
157
158#[inline]
159pub fn post_pprof_conf_handler(
160 _body: &[u8],
161 params: &HashMap<String, String>,
162) -> Result<(Vec<u8>, Option<String>), ErrorResponse> {
163 match mallctl::enabled() {
164 Ok(true) => (),
165 _ => return Err(ErrorResponse("jemalloc profiling not enabled\r\n".to_owned())),
166 };
167
168 for (name, value) in params {
169 if let Err(e) = match name.as_str() {
170 "prof.reset" => {
171 let sample = value.parse().map_err(|_| {
172 ErrorResponse(format!("invalid prof.reset value: {value:?}\r\n"))
173 })?;
174 mallctl::reset(Some(sample))
175 }
176 "prof.active" => {
177 let Some(state) = value.parse().ok() else {
178 return Err(ErrorResponse(format!("invalid prof.active value: {value:?}\r\n")));
179 };
180 mallctl::set_active(state)
181 }
182 _ => {
183 return Err(ErrorResponse(format!("{name}={value:?} unknown\r\n")));
184 }
185 } {
186 return Err(ErrorResponse(format!("{name}={value:?} failed: {e}\r\n")));
187 }
188 }
189
190 Ok((b"OK\r\n".to_vec(), None))
191}
192
193#[inline]
194pub fn get_pprof_heap_handler(
195 _body: &[u8],
196 _params: &HashMap<String, String>,
197) -> Result<(Vec<u8>, Option<String>), ErrorResponse> {
198 match mallctl::enabled() {
199 Ok(true) => (),
200 _ => return Err(ErrorResponse("jemalloc profiling not enabled\r\n".to_owned())),
201 };
202
203 let Ok(f) = tempfile::Builder::new().prefix("jemalloc.").suffix(".prof").tempfile() else {
204 return Err(ErrorResponse("cannot create temporary file for profile dump\r\n".to_owned()));
205 };
206
207 let Ok(profile) = mallctl::dump(f.path().to_str()) else {
208 return Err(ErrorResponse("failed to dump profile\r\n".to_owned()));
209 };
210
211 let filename = f.path().file_name().expect("proper filename from tempfile");
212 Ok((profile.expect("profile not None"), Some(filename.to_string_lossy().to_string())))
213}
214
215#[inline]
217pub fn get_pprof_cmdline_handler(
218 _body: &[u8],
219 _params: &HashMap<String, String>,
220) -> Result<(Vec<u8>, Option<String>), ErrorResponse> {
221 let mut body = String::new();
222 for arg in env::args() {
223 body.push_str(arg.as_str());
224 body.push_str("\r\n");
225 }
226 Ok((body.into_bytes(), None))
227}
228
229#[inline]
231pub fn get_pprof_symbol_handler(
232 _body: &[u8],
233 _params: &HashMap<String, String>,
234) -> Result<(Vec<u8>, Option<String>), ErrorResponse> {
235 let body = b"num_symbols: 1\r\n";
237 Ok((body.to_vec(), None))
238}
239
240#[inline]
242pub fn post_pprof_symbol_handler(
243 body: &[u8],
244 _params: &HashMap<String, String>,
245) -> Result<(Vec<u8>, Option<String>), ErrorResponse> {
246 fn lookup_symbol(addr: u64) -> Option<String> {
247 let mut s: Option<String> = None;
248 backtrace::resolve(addr as *mut _, |symbol| {
249 s = symbol.name().map(|n| n.to_string());
250 });
251 s
252 }
253
254 let body = String::from_utf8_lossy(body);
255 let addrs = body
256 .split('+')
257 .filter_map(|addr| u64::from_str_radix(addr.trim_start_matches("0x"), 16).ok())
258 .map(|addr| (addr, lookup_symbol(addr)))
259 .filter_map(|(addr, sym)| sym.map(|sym| (addr, sym)));
260
261 let mut body = String::new();
262 for (addr, sym) in addrs {
263 body.push_str(format!("{addr:#x}\t{sym}\r\n").as_str());
264 }
265
266 Ok((body.into_bytes(), None))
267}
268
269#[inline]
271pub fn get_pprof_stats_handler(
272 _body: &[u8],
273 _params: &HashMap<String, String>,
274) -> Result<(Vec<u8>, Option<String>), ErrorResponse> {
275 let body = match mallctl::stats() {
276 Ok(body) => body,
277 Err(e) => return Err(ErrorResponse(format!("failed to print stats: {e}\r\n"))),
278 };
279 Ok((body, None))
280}
281
282fn parse_malloc_conf_query(query: Option<&str>) -> Vec<(&str, Option<&str>)> {
283 query
284 .map(|q| {
285 q.split(',')
286 .map(|kv| kv.splitn(2, ':').collect::<Vec<_>>())
287 .map(|v| match v.len() {
288 1 => (v[0], None),
289 2 => (v[0], Some(v[1])),
290 _ => unreachable!(),
291 })
292 .collect()
293 })
294 .unwrap_or_default()
295}
296
297fn response_ok(body: Vec<u8>) -> http::Result<Response<Vec<u8>>> {
298 Response::builder()
299 .status(StatusCode::OK)
300 .header(header::CONTENT_TYPE, "text/plain; charset=UTF-8")
301 .header(header::CONTENT_LENGTH, body.len())
302 .body(body)
303}
304
305fn response_ok_binary(body: Vec<u8>, filename: &str) -> http::Result<Response<Vec<u8>>> {
306 Response::builder()
307 .status(StatusCode::OK)
308 .header(header::CONTENT_TYPE, "application/octet-stream")
309 .header(header::CONTENT_DISPOSITION, format!("attachment; filename=\"{filename}\""))
310 .header(header::CONTENT_LENGTH, body.len())
311 .body(body)
312}
313
314fn response_err(msg: &str) -> http::Result<Response<Vec<u8>>> {
315 Response::builder()
316 .status(StatusCode::BAD_REQUEST)
317 .header(header::CONTENT_TYPE, "text/plain; charset=UTF-8")
318 .header(header::CONTENT_LENGTH, msg.len())
319 .body(msg.as_bytes().to_owned())
320}