Skip to content

Commit 70c5788

Browse files
committed
Add flamegraph feature
1 parent 2973b2f commit 70c5788

File tree

7 files changed

+143
-7
lines changed

7 files changed

+143
-7
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ errno = "0.3"
3939
util = { path = "./util", version = "0.6", package = "pprof_util" }
4040
mappings = { path = "./mappings", version = "0.6" }
4141
backtrace = "0.3"
42+
inferno = "0.12"
4243

4344
[dependencies]
4445
util.workspace = true
@@ -52,6 +53,7 @@ tempfile.workspace = true
5253
tokio.workspace = true
5354

5455
[features]
56+
flamegraph = ["util/flamegraph"]
5557
symbolize = ["util/symbolize"]
5658

5759
[dev-dependencies]

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,43 @@ To generate symbolized profiles, enable the `symbolize` crate feature:
102102
jemalloc_pprof = { version = "0.6", features = ["symbolize"] }
103103
```
104104

105+
### Flamegraph SVGs
106+
107+
The `flamegraph` crate feature can also be enabled to generate interactive flamegraph SVGs directly
108+
(implies the `symbolize` feature):
109+
110+
```toml
111+
jemalloc_pprof = { version = "0.6", features = ["flamegraph"] }
112+
```
113+
114+
We can then adjust the example above to also emit a flamegraph SVG:
115+
116+
```rust,ignore
117+
#[tokio::main]
118+
async fn main() {
119+
let app = axum::Router::new()
120+
.route("/debug/pprof/heap", axum::routing::get(handle_get_heap))
121+
.route("/debug/pprof/heap/flamegraph", axum::routing::get(handle_get_heap_flamegraph));
122+
// ...
123+
}
124+
125+
pub async fn handle_get_heap_flamegraph() -> Result<impl IntoResponse, (StatusCode, String)> {
126+
use axum::body::Body;
127+
use axum::http::header::CONTENT_TYPE;
128+
use axum::response::Response;
129+
130+
let mut prof_ctl = jemalloc_pprof::PROF_CTL.as_ref().unwrap().lock().await;
131+
require_profiling_activated(&prof_ctl)?;
132+
let svg = prof_ctl
133+
.dump_flamegraph()
134+
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
135+
Response::builder()
136+
.header(CONTENT_TYPE, "image/svg+xml")
137+
.body(Body::from(svg))
138+
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
139+
}
140+
```
141+
105142
### Writeable temporary directory
106143

107144
The way this library works is that it creates a new temporary file (in the [platform-specific default temp dir](https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html)), and instructs jemalloc to dump a profile into that file. Therefore the platform respective temporary directory must be writeable by the process. After reading and converting it to pprof, the file is cleaned up via the destructor. A single profile tends to be only a few kilobytes large, so it doesn't require a significant space, but it's non-zero and needs to be writeable.

example/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ tokio = { version = "1", features = ["full"] }
1010
axum = "0.7.2"
1111
[target.'cfg(not(target_env = "msvc"))'.dependencies]
1212
tikv-jemallocator = { version = "0.6", features = ["profiling", "stats", "unprefixed_malloc_on_supported_platforms", "background_threads"] }
13+
14+
[features]
15+
flamegraph = ["jemalloc_pprof/flamegraph"]

example/src/main.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ async fn main() {
1818

1919
let app = axum::Router::new().route("/debug/pprof/heap", axum::routing::get(handle_get_heap));
2020

21+
// Add a flamegraph SVG route if enabled via `cargo run -F flamegraph`.
22+
#[cfg(feature = "flamegraph")]
23+
let app = app.route(
24+
"/debug/pprof/heap/flamegraph",
25+
axum::routing::get(handle_get_heap_flamegraph),
26+
);
27+
2128
// run our app with hyper, listening globally on port 3000
2229
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
2330
axum::serve(listener, app).await.unwrap();
@@ -32,6 +39,23 @@ pub async fn handle_get_heap() -> Result<impl IntoResponse, (StatusCode, String)
3239
Ok(pprof)
3340
}
3441

42+
#[cfg(feature = "flamegraph")]
43+
pub async fn handle_get_heap_flamegraph() -> Result<impl IntoResponse, (StatusCode, String)> {
44+
use axum::body::Body;
45+
use axum::http::header::CONTENT_TYPE;
46+
use axum::response::Response;
47+
48+
let mut prof_ctl = jemalloc_pprof::PROF_CTL.as_ref().unwrap().lock().await;
49+
require_profiling_activated(&prof_ctl)?;
50+
let svg = prof_ctl
51+
.dump_flamegraph()
52+
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
53+
Response::builder()
54+
.header(CONTENT_TYPE, "image/svg+xml")
55+
.body(Body::from(svg))
56+
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
57+
}
58+
3559
/// Checks whether jemalloc profiling is activated an returns an error response if not.
3660
fn require_profiling_activated(
3761
prof_ctl: &jemalloc_pprof::JemallocProfCtl,

src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,13 @@ impl JemallocProfCtl {
166166
let pprof = profile.to_pprof(("inuse_space", "bytes"), ("space", "bytes"), None);
167167
Ok(pprof)
168168
}
169+
170+
/// Dump a profile flamegraph in SVG format.
171+
#[cfg(feature = "flamegraph")]
172+
pub fn dump_flamegraph(&mut self) -> anyhow::Result<Vec<u8>> {
173+
let f = self.dump()?;
174+
let dump_reader = BufReader::new(f);
175+
let profile = parse_jeheap(dump_reader, MAPPINGS.as_deref())?;
176+
profile.to_flamegraph(("inuse_space", "bytes"))
177+
}
169178
}

util/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ anyhow.workspace = true
1717
num.workspace = true
1818
paste.workspace = true
1919
backtrace = { workspace = true, optional = true }
20+
inferno = { workspace = true, optional = true }
2021

2122
[features]
23+
flamegraph = ["symbolize", "dep:inferno"]
2224
symbolize = ["dep:backtrace"]

util/src/lib.rs

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ impl StringTable {
5151
}
5252

5353
#[path = "perftools.profiles.rs"]
54-
mod pprof_types;
54+
mod proto;
5555

5656
/// A single sample in the profile. The stack is a list of addresses.
5757
#[derive(Clone, Debug)]
@@ -104,8 +104,21 @@ impl StackProfile {
104104
period_type: (&str, &str),
105105
anno_key: Option<String>,
106106
) -> Vec<u8> {
107-
use crate::pprof_types as proto;
107+
let profile = self.to_pprof_proto(sample_type, period_type, anno_key);
108+
let encoded = profile.encode_to_vec();
109+
110+
let mut gz = GzEncoder::new(Vec::new(), Compression::default());
111+
gz.write_all(&encoded).unwrap();
112+
gz.finish().unwrap()
113+
}
108114

115+
/// Converts the profile into the pprof Protobuf format (see `pprof/profile.proto`).
116+
fn to_pprof_proto(
117+
&self,
118+
sample_type: (&str, &str),
119+
period_type: (&str, &str),
120+
anno_key: Option<String>,
121+
) -> proto::Profile {
109122
let mut profile = proto::Profile::default();
110123
let mut strings = StringTable::new();
111124

@@ -192,7 +205,7 @@ impl StackProfile {
192205
let addr = u64::cast_from(*addr) - 1;
193206

194207
let loc_id = *location_ids.entry(addr).or_insert_with(|| {
195-
// pprof_types.proto says the location id may be the address, but Polar Signals
208+
// profile.proto says the location id may be the address, but Polar Signals
196209
// insists that location ids are sequential, starting with 1.
197210
let id = u64::cast_from(profile.location.len()) + 1;
198211

@@ -275,11 +288,57 @@ impl StackProfile {
275288

276289
profile.string_table = strings.finish();
277290

278-
let encoded = profile.encode_to_vec();
291+
profile
292+
}
279293

280-
let mut gz = GzEncoder::new(Vec::new(), Compression::default());
281-
gz.write_all(&encoded).unwrap();
282-
gz.finish().unwrap()
294+
/// Converts the profile into a flamegraph SVG.
295+
#[cfg(feature = "flamegraph")]
296+
pub fn to_flamegraph(&self, sample_type: (&str, &str)) -> anyhow::Result<Vec<u8>> {
297+
use std::collections::HashMap;
298+
299+
// We start from a symbolized Protobuf profile.
300+
let profile = self.to_pprof_proto(sample_type, sample_type, None);
301+
302+
// Index locations, functions, and strings.
303+
let locations: HashMap<u64, proto::Location> =
304+
profile.location.into_iter().map(|l| (l.id, l)).collect();
305+
let functions: HashMap<u64, proto::Function> =
306+
profile.function.into_iter().map(|f| (f.id, f)).collect();
307+
let strings = profile.string_table;
308+
309+
// Resolve stacks as function name vectors, and sum sample values per stack. Also reverse
310+
// the stack, since inferno expects it bottom-up.
311+
let mut stacks: HashMap<Vec<&str>, i64> = HashMap::new();
312+
for sample in profile.sample {
313+
let mut stack = Vec::with_capacity(sample.location_id.len());
314+
for location in sample.location_id.into_iter().rev() {
315+
let location = locations.get(&location).expect("missing location");
316+
for line in location.line.iter().rev() {
317+
let function = functions.get(&line.function_id).expect("missing function");
318+
let name = strings.get(function.name as usize).expect("missing string");
319+
stack.push(name.as_str());
320+
}
321+
}
322+
let value = sample.value.first().expect("missing value");
323+
*stacks.entry(stack).or_default() += value;
324+
}
325+
326+
// Construct stack lines for inferno.
327+
let mut lines = stacks
328+
.into_iter()
329+
.map(|(stack, value)| format!("{} {}", stack.join(";"), value))
330+
.collect::<Vec<_>>();
331+
lines.sort();
332+
333+
// Generate the flamegraph SVG.
334+
let mut opts = inferno::flamegraph::Options::default();
335+
opts.title = sample_type.0.to_string();
336+
opts.count_name = sample_type.1.to_string();
337+
338+
let mut bytes = Vec::new();
339+
let lines = lines.iter().map(|line| line.as_str());
340+
inferno::flamegraph::from_lines(&mut opts, lines, &mut bytes)?;
341+
Ok(bytes)
283342
}
284343
}
285344

0 commit comments

Comments
 (0)