1mod cast;
2
3use std::collections::BTreeMap;
4use std::fmt;
5use std::io::BufRead;
6use std::io::Write;
7use std::path::PathBuf;
8use std::time::{Instant, SystemTime, UNIX_EPOCH};
9
10use anyhow::bail;
11use flate2::write::GzEncoder;
12use flate2::Compression;
13use prost::Message;
14
15pub use cast::CastFrom;
16pub use cast::TryCastFrom;
17
18#[cfg(feature = "flamegraph")]
19pub use inferno::flamegraph::Options as FlamegraphOptions;
20
21#[derive(Copy, Clone, Debug)]
23pub enum ProfStartTime {
24 Instant(Instant),
25 TimeImmemorial,
26}
27
28#[derive(Default)]
30struct StringTable(BTreeMap<String, i64>);
31
32impl StringTable {
33 fn new() -> Self {
34 let inner = [("".into(), 0)].into();
36 Self(inner)
37 }
38
39 fn insert(&mut self, s: &str) -> i64 {
40 if let Some(idx) = self.0.get(s) {
41 *idx
42 } else {
43 let idx = i64::try_from(self.0.len()).expect("must fit");
44 self.0.insert(s.into(), idx);
45 idx
46 }
47 }
48
49 fn finish(self) -> Vec<String> {
50 let mut vec: Vec<_> = self.0.into_iter().collect();
51 vec.sort_by_key(|(_, idx)| *idx);
52 vec.into_iter().map(|(s, _)| s).collect()
53 }
54}
55
56#[path = "perftools.profiles.rs"]
57mod proto;
58
59#[derive(Clone, Debug)]
61pub struct WeightedStack {
62 pub addrs: Vec<usize>,
63 pub weight: f64,
64}
65
66#[derive(Clone, Debug)]
68pub struct Mapping {
69 pub memory_start: usize,
70 pub memory_end: usize,
71 pub memory_offset: usize,
72 pub file_offset: u64,
73 pub pathname: PathBuf,
74 pub build_id: Option<BuildId>,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
79pub struct BuildId(pub Vec<u8>);
80
81impl fmt::Display for BuildId {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 for byte in &self.0 {
84 write!(f, "{byte:02x}")?;
85 }
86 Ok(())
87 }
88}
89
90#[derive(Default)]
92pub struct StackProfile {
93 pub annotations: Vec<String>,
94 pub stacks: Vec<(WeightedStack, Option<usize>)>,
96 pub mappings: Vec<Mapping>,
97}
98
99impl StackProfile {
100 pub fn to_pprof(
105 &self,
106 sample_type: (&str, &str),
107 period_type: (&str, &str),
108 anno_key: Option<String>,
109 ) -> Vec<u8> {
110 let profile = self.to_pprof_proto(sample_type, period_type, anno_key);
111 let encoded = profile.encode_to_vec();
112
113 let mut gz = GzEncoder::new(Vec::new(), Compression::default());
114 gz.write_all(&encoded).unwrap();
115 gz.finish().unwrap()
116 }
117
118 fn to_pprof_proto(
120 &self,
121 sample_type: (&str, &str),
122 period_type: (&str, &str),
123 anno_key: Option<String>,
124 ) -> proto::Profile {
125 let mut profile = proto::Profile::default();
126 let mut strings = StringTable::new();
127
128 let anno_key = anno_key.unwrap_or_else(|| "annotation".into());
129
130 profile.sample_type = vec![proto::ValueType {
131 r#type: strings.insert(sample_type.0),
132 unit: strings.insert(sample_type.1),
133 }];
134 profile.period_type = Some(proto::ValueType {
135 r#type: strings.insert(period_type.0),
136 unit: strings.insert(period_type.1),
137 });
138
139 profile.time_nanos = SystemTime::now()
140 .duration_since(UNIX_EPOCH)
141 .expect("now is later than UNIX epoch")
142 .as_nanos()
143 .try_into()
144 .expect("the year 2554 is far away");
145
146 for (mapping, mapping_id) in self.mappings.iter().zip(1..) {
147 let pathname = mapping.pathname.to_string_lossy();
148 let filename_idx = strings.insert(&pathname);
149
150 let build_id_idx = match &mapping.build_id {
151 Some(build_id) => strings.insert(&build_id.to_string()),
152 None => 0,
153 };
154
155 profile.mapping.push(proto::Mapping {
156 id: mapping_id,
157 memory_start: 0,
158 memory_limit: 0,
159 file_offset: 0,
160 filename: filename_idx,
161 build_id: build_id_idx,
162 ..Default::default()
163 });
164 }
165
166 let mut location_ids = BTreeMap::new();
167 #[cfg(feature = "symbolize")]
168 let mut function_ids = BTreeMap::new();
169 for (stack, anno) in self.iter() {
170 let mut sample = proto::Sample::default();
171
172 let value = stack.weight.trunc();
173 let value = i64::try_cast_from(value).expect("no exabyte heap sizes");
174 sample.value.push(value);
175
176 for addr in stack.addrs.iter().rev() {
177 let addr = u64::cast_from(*addr) - 1;
189
190 let mapping_info = self.mappings.iter().enumerate().find(|(_, mapping)| {
192 mapping.memory_start <= addr as usize && mapping.memory_end > addr as usize
193 });
194
195 let file_relative_addr = mapping_info
197 .map(|(_, mapping)| {
198 (addr as usize - mapping.memory_start + mapping.file_offset as usize) as u64
199 })
200 .unwrap_or(addr);
201
202 let loc_id = *location_ids.entry(file_relative_addr).or_insert_with(|| {
203 let id = u64::cast_from(profile.location.len()) + 1;
206
207 #[allow(unused_mut)] let mut mapping =
209 mapping_info.and_then(|(idx, _)| profile.mapping.get_mut(idx));
210
211 #[allow(unused_mut)]
213 let mut line = Vec::new();
214 #[cfg(feature = "symbolize")]
215 backtrace::resolve(addr as *mut std::ffi::c_void, |symbol| {
216 let Some(symbol_name) = symbol.name() else {
217 return;
218 };
219 let function_name = format!("{symbol_name:#}");
220 let lineno = symbol.lineno().unwrap_or(0) as i64;
221
222 let function_id = *function_ids.entry(function_name).or_insert_with_key(
223 |function_name| {
224 let function_id = profile.function.len() as u64 + 1;
225 let system_name = String::from_utf8_lossy(symbol_name.as_bytes());
226 let filename = symbol
227 .filename()
228 .map(|path| path.to_string_lossy())
229 .unwrap_or(std::borrow::Cow::Borrowed(""));
230
231 if let Some(ref mut mapping) = mapping {
232 mapping.has_functions = true;
233 mapping.has_filenames |= !filename.is_empty();
234 mapping.has_line_numbers |= lineno > 0;
235 }
236
237 profile.function.push(proto::Function {
238 id: function_id,
239 name: strings.insert(function_name),
240 system_name: strings.insert(&system_name),
241 filename: strings.insert(&filename),
242 ..Default::default()
243 });
244 function_id
245 },
246 );
247
248 line.push(proto::Line {
249 function_id,
250 line: lineno,
251 });
252
253 if let Some(ref mut mapping) = mapping {
254 mapping.has_inline_frames |= line.len() > 1;
255 }
256 });
257
258 profile.location.push(proto::Location {
259 id,
260 mapping_id: mapping.map_or(0, |m| m.id),
261 address: file_relative_addr,
262 line,
263 ..Default::default()
264 });
265 id
266 });
267
268 sample.location_id.push(loc_id);
269
270 if let Some(anno) = anno {
271 sample.label.push(proto::Label {
272 key: strings.insert(&anno_key),
273 str: strings.insert(anno),
274 ..Default::default()
275 })
276 }
277 }
278
279 profile.sample.push(sample);
280 }
281
282 profile.string_table = strings.finish();
283
284 profile
285 }
286
287 #[cfg(feature = "flamegraph")]
289 pub fn to_flamegraph(&self, opts: &mut FlamegraphOptions) -> anyhow::Result<Vec<u8>> {
290 use std::collections::HashMap;
291
292 let profile = self.to_pprof_proto(("", ""), ("", ""), None);
295
296 let locations: HashMap<u64, proto::Location> =
298 profile.location.into_iter().map(|l| (l.id, l)).collect();
299 let functions: HashMap<u64, proto::Function> =
300 profile.function.into_iter().map(|f| (f.id, f)).collect();
301 let strings = profile.string_table;
302
303 let mut stacks: HashMap<Vec<&str>, i64> = HashMap::new();
306 for sample in profile.sample {
307 let mut stack = Vec::with_capacity(sample.location_id.len());
308 for location in sample.location_id.into_iter().rev() {
309 let location = locations.get(&location).expect("missing location");
310 for line in location.line.iter().rev() {
311 let function = functions.get(&line.function_id).expect("missing function");
312 let name = strings.get(function.name as usize).expect("missing string");
313 stack.push(name.as_str());
314 }
315 }
316 let value = sample.value.first().expect("missing value");
317 *stacks.entry(stack).or_default() += value;
318 }
319
320 let mut lines = stacks
322 .into_iter()
323 .map(|(stack, value)| format!("{} {}", stack.join(";"), value))
324 .collect::<Vec<_>>();
325 lines.sort();
326
327 let mut bytes = Vec::new();
329 let lines = lines.iter().map(|line| line.as_str());
330 inferno::flamegraph::from_lines(opts, lines, &mut bytes)?;
331 Ok(bytes)
332 }
333}
334
335pub struct StackProfileIter<'a> {
336 inner: &'a StackProfile,
337 idx: usize,
338}
339
340impl<'a> Iterator for StackProfileIter<'a> {
341 type Item = (&'a WeightedStack, Option<&'a str>);
342
343 fn next(&mut self) -> Option<Self::Item> {
344 let (stack, anno) = self.inner.stacks.get(self.idx)?;
345 self.idx += 1;
346 let anno = anno.map(|idx| self.inner.annotations.get(idx).unwrap().as_str());
347 Some((stack, anno))
348 }
349}
350
351impl StackProfile {
352 pub fn push_stack(&mut self, stack: WeightedStack, annotation: Option<&str>) {
353 let anno_idx = if let Some(annotation) = annotation {
354 Some(
355 self.annotations
356 .iter()
357 .position(|anno| annotation == anno.as_str())
358 .unwrap_or_else(|| {
359 self.annotations.push(annotation.to_string());
360 self.annotations.len() - 1
361 }),
362 )
363 } else {
364 None
365 };
366 self.stacks.push((stack, anno_idx))
367 }
368
369 pub fn push_mapping(&mut self, mapping: Mapping) {
370 self.mappings.push(mapping);
371 }
372
373 pub fn iter(&self) -> StackProfileIter<'_> {
374 StackProfileIter {
375 inner: self,
376 idx: 0,
377 }
378 }
379}
380
381pub fn parse_jeheap<R: BufRead>(
383 r: R,
384 mappings: Option<&[Mapping]>,
385) -> anyhow::Result<StackProfile> {
386 let mut cur_stack = None;
387 let mut profile = StackProfile::default();
388 let mut lines = r.lines();
389
390 let first_line = match lines.next() {
391 Some(s) => s?,
392 None => bail!("Heap dump file was empty"),
393 };
394 let sampling_rate: f64 = str::parse(first_line.trim_start_matches("heap_v2/"))?;
397
398 for line in &mut lines {
399 let line = line?;
400 let line = line.trim();
401
402 let words: Vec<_> = line.split_ascii_whitespace().collect();
403 if !words.is_empty() && words[0] == "@" {
404 if cur_stack.is_some() {
405 bail!("Stack without corresponding weight!")
406 }
407 let mut addrs = words[1..]
408 .iter()
409 .map(|w| {
410 let raw = w.trim_start_matches("0x");
411 usize::from_str_radix(raw, 16)
412 })
413 .collect::<Result<Vec<_>, _>>()?;
414 addrs.reverse();
415 cur_stack = Some(addrs);
416 }
417 if words.len() > 2 && words[0] == "t*:" {
418 if let Some(addrs) = cur_stack.take() {
419 let n_objs: f64 = str::parse(words[1].trim_end_matches(':'))?;
444 let bytes_in_sampled_objs: f64 = str::parse(words[2])?;
445 let ratio = (bytes_in_sampled_objs / n_objs) / sampling_rate;
446 let scale_factor = 1.0 / (1.0 - (-ratio).exp());
447 let weight = bytes_in_sampled_objs * scale_factor;
448 profile.push_stack(WeightedStack { addrs, weight }, None);
449 }
450 }
451 }
452 if cur_stack.is_some() {
453 bail!("Stack without corresponding weight!");
454 }
455
456 if let Some(mappings) = mappings {
457 for mapping in mappings {
458 profile.push_mapping(mapping.clone());
459 }
460 }
461
462 Ok(profile)
463}