243 lines
8.5 KiB
Rust
243 lines
8.5 KiB
Rust
//! Logic related to application profiling (e.g.: CPU and heap profiling).
|
|
//!
|
|
//! Profiling functionality is gated by Cargo feature flags:
|
|
//! * `pprof_cpu_profiling` enables use of pprof to produce CPU profiles
|
|
//! * `dhat_heap_profiling` enables use of dhat to produce heap profiles
|
|
//! * `jemalloc_auto_heap_profiling` enables the jemalloc allocator and
|
|
//! automatic heap profile generation every 500MB of memory allocated.
|
|
//!
|
|
//! If run from a release bundle, profiles will be written to
|
|
//! [`warp_core::paths::state_dir()`]. Otherwise, profiles will be written
|
|
//! to the current working directory.
|
|
|
|
use cfg_if::cfg_if;
|
|
|
|
// When using jemalloc heap profiling, this static variable enables and
|
|
// configures the profiling behavior.
|
|
cfg_if! {
|
|
if #[cfg(feature = "jemalloc_auto_heap_profiling")] {
|
|
#[cfg_attr(target_vendor = "apple", export_name = "_rjem_malloc_conf")]
|
|
#[cfg_attr(not(target_vendor = "apple"), export_name = "malloc_conf")]
|
|
pub static MALLOC_CONF: &[u8] =
|
|
b"prof:true,prof_active:true,lg_prof_interval:29,lg_prof_sample:21,prof_prefix:/tmp/jeprof\0";
|
|
} else if #[cfg(feature = "jemalloc_pprof")] {
|
|
#[cfg_attr(target_vendor = "apple", export_name = "_rjem_malloc_conf")]
|
|
#[cfg_attr(not(target_vendor = "apple"), export_name = "malloc_conf")]
|
|
pub static MALLOC_CONF: &[u8] =
|
|
b"prof:true,prof_active:true,lg_prof_sample:21\0";
|
|
}
|
|
}
|
|
|
|
/// When the dhat_heap_profiling feature is enabled, a global profiler object
|
|
/// that tracks allocations until the profiler is dropped.
|
|
#[cfg(feature = "dhat_heap_profiling")]
|
|
static HEAP_PROFILER: parking_lot::Mutex<Option<dhat::Profiler>> = parking_lot::Mutex::new(None);
|
|
|
|
#[cfg(feature = "pprof_cpu_profiling")]
|
|
static CPU_PROFILER: parking_lot::Mutex<Option<pprof::ProfilerGuard>> =
|
|
parking_lot::Mutex::new(None);
|
|
|
|
/// Initializes the profiling subsystem.
|
|
pub fn init() {
|
|
#[cfg(feature = "dhat_heap_profiling")]
|
|
let _ = HEAP_PROFILER.lock().insert(
|
|
dhat::Profiler::builder()
|
|
.file_name(heap_profile_path())
|
|
.build(),
|
|
);
|
|
|
|
#[cfg(feature = "pprof_cpu_profiling")]
|
|
let _ = CPU_PROFILER
|
|
.lock()
|
|
.insert(pprof::ProfilerGuard::new(1000).unwrap());
|
|
}
|
|
|
|
/// Dumps dhat heap profiling information.
|
|
///
|
|
/// Note that this is implemented by uninitializing the profiler, and as such
|
|
/// can only be done once per run of the application.
|
|
#[cfg(feature = "dhat_heap_profiling")]
|
|
pub fn dump_dhat_heap_profile() {
|
|
let _ = HEAP_PROFILER.lock().take();
|
|
}
|
|
|
|
/// Dumps a jemalloc heap profile and sends it to Sentry.
|
|
///
|
|
/// This function spawns `go tool pprof` to fetch and symbolicate the heap
|
|
/// profile from the local HTTP server, then attaches the resulting profile
|
|
/// to a Sentry event.
|
|
#[cfg(feature = "heap_usage_tracking")]
|
|
pub async fn dump_jemalloc_heap_profile(memory_breakdown: serde_json::Value) {
|
|
use sentry::protocol::{Attachment, AttachmentType};
|
|
|
|
let result = dump_jemalloc_heap_profile_inner().await;
|
|
match result {
|
|
Ok(profile_data) => {
|
|
let attachment = Attachment {
|
|
buffer: profile_data,
|
|
filename: "heap-profile.pb".to_string(),
|
|
ty: Some(AttachmentType::Attachment),
|
|
..Default::default()
|
|
};
|
|
sentry::with_scope(
|
|
|scope| {
|
|
scope.add_attachment(attachment);
|
|
|
|
// Attach the memory breakdown as structured context so it
|
|
// is visible directly in the Sentry event.
|
|
if let serde_json::Value::Object(map) = memory_breakdown {
|
|
let context_map: std::collections::BTreeMap<
|
|
String,
|
|
sentry::protocol::Value,
|
|
> = map.into_iter().collect();
|
|
scope.set_context(
|
|
"memory_breakdown",
|
|
sentry::protocol::Context::Other(context_map),
|
|
);
|
|
}
|
|
},
|
|
|| {
|
|
sentry::capture_message(
|
|
"Excessive memory usage detected",
|
|
sentry::Level::Warning,
|
|
)
|
|
},
|
|
);
|
|
log::info!("Sent heap profile to Sentry");
|
|
}
|
|
Err(err) => {
|
|
log::warn!("Failed to dump heap profile: {err:#}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "heap_usage_tracking")]
|
|
async fn dump_jemalloc_heap_profile_inner() -> anyhow::Result<Vec<u8>> {
|
|
use anyhow::Context as _;
|
|
|
|
// Create a temporary file for the profile output.
|
|
let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
|
|
let profile_path = temp_dir.path().join("heap-profile.pb");
|
|
|
|
// Run pprof to fetch and symbolicate the heap profile.
|
|
let pprof_path = pprof_binary_path()?;
|
|
let output = command::r#async::Command::new(pprof_path)
|
|
.args(["--proto", "--symbolize=local", "-output"])
|
|
.arg(&profile_path)
|
|
.arg("http://127.0.0.1:9277/debug/pprof/heap")
|
|
.output()
|
|
.await
|
|
.context("Failed to execute pprof")?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
anyhow::bail!("pprof failed: {stderr}");
|
|
}
|
|
|
|
// Read the profile data from the temporary file.
|
|
let profile_data =
|
|
std::fs::read(&profile_path).context("Failed to read heap profile from disk")?;
|
|
|
|
Ok(profile_data)
|
|
}
|
|
|
|
#[cfg(feature = "heap_usage_tracking")]
|
|
fn pprof_binary_path() -> anyhow::Result<std::path::PathBuf> {
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(target_os = "macos")] {
|
|
use anyhow::Context as _;
|
|
|
|
let app_bundle_dir = std::path::PathBuf::from(warp_core::macos::get_bundle_path().context("Failed to get app bundle path")?);
|
|
Ok(app_bundle_dir.join("Contents/Helpers/pprof"))
|
|
}
|
|
else {
|
|
Err(anyhow::anyhow!("pprof binary path not supported on this platform"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the path at which heap profiles will be written.
|
|
#[cfg(feature = "dhat_heap_profiling")]
|
|
pub fn heap_profile_path() -> std::path::PathBuf {
|
|
profile_output_dir().join("dhat-heap.json")
|
|
}
|
|
|
|
/// Uninitializes the profiling subsystem, writing reports to disk as-needed.
|
|
pub fn teardown() {
|
|
#[cfg(feature = "dhat_heap_profiling")]
|
|
let _ = HEAP_PROFILER.lock().take();
|
|
|
|
#[cfg(feature = "pprof_cpu_profiling")]
|
|
if let Err(err) = CPU_PROFILER
|
|
.lock()
|
|
.take()
|
|
.unwrap()
|
|
.report()
|
|
.build()
|
|
.map_err(Into::into)
|
|
.and_then(write_pprof_report)
|
|
{
|
|
log::error!("Failed to write pprof data: {err:#}");
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "pprof_cpu_profiling")]
|
|
fn write_pprof_report(report: pprof::Report) -> anyhow::Result<()> {
|
|
use pprof::protos::Message as _;
|
|
|
|
let mut file = std::fs::File::create(profile_output_dir().join("profile.pb"))?;
|
|
let profile = report.pprof()?;
|
|
profile.write_to_writer(&mut file)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(any(feature = "dhat_heap_profiling", feature = "pprof_cpu_profiling"))]
|
|
fn profile_output_dir() -> std::path::PathBuf {
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(feature = "release_bundle")] {
|
|
warp_core::paths::secure_state_dir().unwrap_or(warp_core::paths::state_dir())
|
|
} else {
|
|
std::env::current_dir().ok().unwrap_or_else(|| {
|
|
dirs::home_dir().expect("Should not fail to compute both the current directory and the user's home directory")
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_family = "wasm"))]
|
|
pub fn make_router() -> axum::Router {
|
|
let router = axum::Router::new();
|
|
|
|
#[cfg(feature = "jemalloc_pprof")]
|
|
let router = router.route("/debug/pprof/heap", axum::routing::get(handle_get_heap));
|
|
|
|
router
|
|
}
|
|
|
|
#[cfg(feature = "jemalloc_pprof")]
|
|
pub async fn handle_get_heap(
|
|
) -> Result<impl axum::response::IntoResponse, (axum::http::StatusCode, String)> {
|
|
let Some(prof_ctl) = jemalloc_pprof::PROF_CTL.as_ref() else {
|
|
return Err((
|
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
"heap profiler not initialized".into(),
|
|
));
|
|
};
|
|
let mut prof_ctl = prof_ctl.lock().await;
|
|
|
|
if !prof_ctl.activated() {
|
|
return Err((
|
|
axum::http::StatusCode::FORBIDDEN,
|
|
"heap profiling not activated".into(),
|
|
));
|
|
}
|
|
|
|
let pprof = prof_ctl.dump_pprof().map_err(|err| {
|
|
(
|
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
|
err.to_string(),
|
|
)
|
|
})?;
|
|
Ok(pprof)
|
|
}
|