mirror of
https://github.com/neondatabase/neon.git
synced 2026-01-16 18:02:56 +00:00
## Problem We don't have a convenient way to gather CPU profiles from a running binary, e.g. during production incidents or end-to-end benchmarks, nor during microbenchmarks (particularly on macOS). We would also like to have continuous profiling in production, likely using [Grafana Cloud Profiles](https://grafana.com/products/cloud/profiles-for-continuous-profiling/). We may choose to use either eBPF profiles or pprof profiles for this (pending testing and discussion with SREs), but pprof profiles appear useful regardless for the reasons listed above. See https://github.com/neondatabase/cloud/issues/14888. This PR is intended as a proof of concept, to try it out in staging and drive further discussions about profiling more broadly. Touches #9534. Touches https://github.com/neondatabase/cloud/issues/14888. ## Summary of changes Adds a HTTP route `/profile/cpu` that takes a CPU profile and returns it. Defaults to a 5-second pprof Protobuf profile for use with e.g. `pprof` or Grafana Alloy, but can also emit an SVG flamegraph. Query parameters: * `format`: output format (`pprof` or `svg`) * `frequency`: sampling frequency in microseconds (default 100) * `seconds`: number of seconds to profile (default 5) Also integrates pprof profiles into Criterion benchmarks, such that flamegraph reports can be taken with `cargo bench ... --profile-duration <seconds>`. Output under `target/criterion/*/profile/flamegraph.svg`. Example profiles: * pprof profile (use [`pprof`](https://github.com/google/pprof)): [profile.pb.gz](https://github.com/user-attachments/files/17756788/profile.pb.gz) * Web interface: `pprof -http :6060 profile.pb.gz` * Interactive flamegraph: [profile.svg.gz](https://github.com/user-attachments/files/17756782/profile.svg.gz)
92 lines
2.7 KiB
Rust
92 lines
2.7 KiB
Rust
use core::fmt;
|
|
use std::{borrow::Cow, str::FromStr};
|
|
|
|
use super::error::ApiError;
|
|
use anyhow::anyhow;
|
|
use hyper::{body::HttpBody, Body, Request};
|
|
use routerify::ext::RequestExt;
|
|
|
|
pub fn get_request_param<'a>(
|
|
request: &'a Request<Body>,
|
|
param_name: &str,
|
|
) -> Result<&'a str, ApiError> {
|
|
match request.param(param_name) {
|
|
Some(arg) => Ok(arg),
|
|
None => Err(ApiError::BadRequest(anyhow!(
|
|
"no {param_name} specified in path param",
|
|
))),
|
|
}
|
|
}
|
|
|
|
pub fn parse_request_param<T: FromStr>(
|
|
request: &Request<Body>,
|
|
param_name: &str,
|
|
) -> Result<T, ApiError> {
|
|
match get_request_param(request, param_name)?.parse() {
|
|
Ok(v) => Ok(v),
|
|
Err(_) => Err(ApiError::BadRequest(anyhow!(
|
|
"failed to parse {param_name}",
|
|
))),
|
|
}
|
|
}
|
|
|
|
pub fn get_query_param<'a>(
|
|
request: &'a Request<Body>,
|
|
param_name: &str,
|
|
) -> Result<Option<Cow<'a, str>>, ApiError> {
|
|
let query = match request.uri().query() {
|
|
Some(q) => q,
|
|
None => return Ok(None),
|
|
};
|
|
let mut values = url::form_urlencoded::parse(query.as_bytes())
|
|
.filter_map(|(k, v)| if k == param_name { Some(v) } else { None })
|
|
// we call .next() twice below. If it's None the first time, .fuse() ensures it's None afterwards
|
|
.fuse();
|
|
|
|
let value1 = values.next();
|
|
if values.next().is_some() {
|
|
return Err(ApiError::BadRequest(anyhow!(
|
|
"param {param_name} specified more than once"
|
|
)));
|
|
}
|
|
Ok(value1)
|
|
}
|
|
|
|
pub fn must_get_query_param<'a>(
|
|
request: &'a Request<Body>,
|
|
param_name: &str,
|
|
) -> Result<Cow<'a, str>, ApiError> {
|
|
get_query_param(request, param_name)?.ok_or_else(|| {
|
|
ApiError::BadRequest(anyhow!("no {param_name} specified in query parameters"))
|
|
})
|
|
}
|
|
|
|
pub fn parse_query_param<E: fmt::Display, T: FromStr<Err = E>>(
|
|
request: &Request<Body>,
|
|
param_name: &str,
|
|
) -> Result<Option<T>, ApiError> {
|
|
get_query_param(request, param_name)?
|
|
.map(|v| {
|
|
v.parse().map_err(|e| {
|
|
ApiError::BadRequest(anyhow!("cannot parse query param {param_name}: {e}"))
|
|
})
|
|
})
|
|
.transpose()
|
|
}
|
|
|
|
pub fn must_parse_query_param<E: fmt::Display, T: FromStr<Err = E>>(
|
|
request: &Request<Body>,
|
|
param_name: &str,
|
|
) -> Result<T, ApiError> {
|
|
parse_query_param(request, param_name)?.ok_or_else(|| {
|
|
ApiError::BadRequest(anyhow!("no {param_name} specified in query parameters"))
|
|
})
|
|
}
|
|
|
|
pub async fn ensure_no_body(request: &mut Request<Body>) -> Result<(), ApiError> {
|
|
match request.body_mut().data().await {
|
|
Some(_) => Err(ApiError::BadRequest(anyhow!("Unexpected request body"))),
|
|
None => Ok(()),
|
|
}
|
|
}
|