Skip to content

Commit f825061

Browse files
committed
Add symbolize feature for online symbolization
This patch adds a `symbolize` feature which performs online symbolization of profiles using the `backtrace` crate. This is often much more convenient than having to manually obtain binaries and symbolize the profiles, and matches the behavior of e.g. the `pprof-rs` CPU profiler. It will also allow emitting e.g. flamegraph SVGs directly, which I'll submit in a follow-up PR. Online symbolization uses a fair amount of memory for symbol caches, but this is often a worthwhile tradeoff.
1 parent 00cdef9 commit f825061

File tree

5 files changed

+72
-7
lines changed

5 files changed

+72
-7
lines changed

Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ num = "0.4"
3838
errno = "0.3"
3939
util = { path = "./util", version = "0.6", package = "pprof_util" }
4040
mappings = { path = "./mappings", version = "0.6" }
41+
backtrace = "0.3"
4142

4243
[dependencies]
4344
util.workspace = true
@@ -50,6 +51,9 @@ tracing.workspace = true
5051
tempfile.workspace = true
5152
tokio.workspace = true
5253

54+
[features]
55+
symbolize = ["util/symbolize"]
56+
5357
[dev-dependencies]
5458
tikv-jemallocator = "0.6"
5559
axum = "0.7"

Makefile

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,19 @@ capi:
66
fmt:
77
cargo fmt --all -- --check
88

9+
# Run Clippy with default and all features, to ensure both variants compile.
910
.PHONY: lint
1011
lint:
1112
cargo clippy --workspace -- -D warnings
13+
cargo clippy --workspace --all-features -- -D warnings
1214

1315
.PHONY: doc
1416
doc:
1517
RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo doc --all-features --no-deps
1618

1719
.PHONY: test
1820
test: fmt lint doc
19-
cargo test --workspace
21+
cargo test --workspace --all-features
2022

2123
.PHONY: clean
2224
clean:

README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,14 @@ curl localhost:3000/debug/pprof/heap > heap.pb.gz
9393
pprof -http=:8080 heap.pb.gz
9494
```
9595

96-
> Note: The profiling data is not symbolized, so either `addr2line` or `llvm-addr2line` needs to be available in the path and pprof needs to be able to discover the respective debuginfos.
96+
> Note: if symbolization is not enabled, either `addr2line` or `llvm-addr2line` needs to be available in the path and pprof needs to be able to discover the respective debuginfos.
97+
98+
To generate symbolized profiles, enable the `symbolize` crate feature:
99+
100+
```toml
101+
[dependencies]
102+
jemalloc_pprof = { version = "0", features = ["symbolize"] }
103+
```
97104

98105
### Writeable temporary directory
99106

util/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ prost.workspace = true
1616
anyhow.workspace = true
1717
num.workspace = true
1818
paste.workspace = true
19+
backtrace = { workspace = true, optional = true }
20+
21+
[features]
22+
symbolize = ["dep:backtrace"]

util/src/lib.rs

+53-5
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ impl StackProfile {
168168
}
169169

170170
let mut location_ids = BTreeMap::new();
171+
#[cfg(feature = "symbolize")]
172+
let mut function_ids: BTreeMap<String, u64> = BTreeMap::new();
171173
for (stack, anno) in self.iter() {
172174
let mut sample = proto::Sample::default();
173175

@@ -193,15 +195,61 @@ impl StackProfile {
193195
// pprof_types.proto says the location id may be the address, but Polar Signals
194196
// insists that location ids are sequential, starting with 1.
195197
let id = u64::cast_from(profile.location.len()) + 1;
196-
let mapping_id = profile
198+
199+
#[allow(unused_mut)] // for feature = "symbolize"
200+
let mut mapping = profile
197201
.mapping
198-
.iter()
199-
.find(|m| m.memory_start <= addr && m.memory_limit > addr)
200-
.map_or(0, |m| m.id);
202+
.iter_mut()
203+
.find(|m| m.memory_start <= addr && m.memory_limit > addr);
204+
205+
// If online symbolization is enabled, resolve the function and line.
206+
#[allow(unused_mut)]
207+
let mut line = Vec::new();
208+
#[cfg(feature = "symbolize")]
209+
backtrace::resolve(addr as *mut std::ffi::c_void, |symbol| {
210+
let Some(symbol_name) = symbol.name() else {
211+
return;
212+
};
213+
let function_name = format!("{symbol_name:#}");
214+
let lineno = symbol.lineno().unwrap_or(0) as i64;
215+
216+
let function_id = *function_ids.entry(function_name).or_insert_with_key(
217+
|function_name| {
218+
let function_id = profile.function.len() as u64 + 1;
219+
let system_name = String::from_utf8_lossy(symbol_name.as_bytes());
220+
let filename = symbol
221+
.filename()
222+
.map(|path| path.to_string_lossy())
223+
.unwrap_or(std::borrow::Cow::Borrowed(""));
224+
225+
if let Some(ref mut mapping) = mapping {
226+
mapping.has_functions = true;
227+
mapping.has_filenames |= !filename.is_empty();
228+
mapping.has_line_numbers |= lineno > 0;
229+
}
230+
231+
profile.function.push(proto::Function {
232+
id: function_id,
233+
name: strings.insert(function_name),
234+
system_name: strings.insert(&system_name),
235+
filename: strings.insert(&filename),
236+
..Default::default()
237+
});
238+
function_id
239+
},
240+
);
241+
242+
line.push(proto::Line {
243+
function_id,
244+
line: lineno,
245+
});
246+
});
247+
201248
profile.location.push(proto::Location {
202249
id,
203-
mapping_id,
250+
mapping_id: mapping.map_or(0, |m| m.id),
204251
address: addr,
252+
line,
205253
..Default::default()
206254
});
207255
id

0 commit comments

Comments
 (0)