1#[allow(clippy::all)]
2pub mod pprof {
3 include!(concat!(env!("OUT_DIR"), "/perftools.profiles.rs"));
4}
5
6use prost::bytes;
7use std::collections::hash_map::Entry;
8use std::collections::HashMap;
9use std::time::{Duration, SystemTime};
10use thiserror;
11
12pub struct PprofBuilder {
13 time_nanos: i64,
14 duration: Duration,
15 freq_in_hz: i64,
16
17 known_mappings: HashMap<i64, u64>,
18 mappings: Vec<pprof::Mapping>,
19
20 known_strings: HashMap<String, i64>,
21 string_table: Vec<String>,
22
23 locations: Vec<pprof::Location>,
24
25 pub functions: Vec<pprof::Function>,
26
27 samples: Vec<pprof::Sample>,
28}
29
30pub enum LabelStringOrNumber {
31 String(String),
32 Number(i64, String),
34}
35
36#[derive(Debug, thiserror::Error, Eq, PartialEq)]
37pub enum PprofError {
38 #[error("null function (id=0)")]
39 NullFunction,
40 #[error("null location (id=0)")]
41 NullLocation,
42 #[error("null mapping (id=0)")]
43 NullMapping,
44
45 #[error("string not found (id={0})")]
46 StringNotFound(i64),
47 #[error("function not found (id={0})")]
48 FunctionNotFound(u64),
49 #[error("location not found (id={0})")]
50 LocationNotFound(u64),
51 #[error("mapping not found (id={0})")]
52 MappingNotFound(u64),
53
54 #[error("function id is null (id={0})")]
55 NullFunctionId(u64),
56 #[error("mapping id is null (id={0})")]
57 NullMappingId(u64),
58}
59
60impl PprofBuilder {
61 pub fn new(profile_start: SystemTime, duration: Duration, freq_in_hz: u64) -> Self {
62 Self {
63 time_nanos: profile_start
64 .duration_since(SystemTime::UNIX_EPOCH)
65 .unwrap()
66 .as_nanos() as i64,
67 duration,
68 freq_in_hz: freq_in_hz as i64,
69
70 known_mappings: HashMap::new(),
71 mappings: Vec::new(),
72 known_strings: HashMap::new(),
73 string_table: Vec::new(),
74 locations: Vec::new(),
75 functions: Vec::new(),
76 samples: Vec::new(),
77 }
78 }
79
80 pub fn validate(&self) -> Result<(), PprofError> {
82 let validate_line = |line: &pprof::Line| {
83 let function_id = line.function_id;
84 if function_id == 0 {
85 return Err(PprofError::NullFunction);
86 }
87
88 let maybe_function = self.functions.get(function_id as usize - 1);
89 match maybe_function {
90 Some(function) => {
91 if function.id == 0 {
92 return Err(PprofError::NullFunctionId(function_id));
93 }
94
95 let function_name_id = function.name;
96 self.string_table
97 .get(function_name_id as usize)
98 .ok_or(PprofError::StringNotFound(function_name_id))?;
99 }
100 None => {
101 return Err(PprofError::FunctionNotFound(function_id));
102 }
103 }
104 Ok(())
105 };
106
107 let validate_location = |location: &pprof::Location| {
108 let mapping_id = location.mapping_id;
109 if mapping_id == 0 {
110 return Err(PprofError::NullMapping);
111 }
112 let maybe_mapping = self.mappings.get(mapping_id as usize - 1);
113 match maybe_mapping {
114 Some(mapping) => {
115 if mapping.id == 0 {
116 return Err(PprofError::NullMappingId(mapping_id));
117 }
118 }
119 None => {
120 return Err(PprofError::MappingNotFound(mapping_id));
121 }
122 }
123
124 for line in &location.line {
125 validate_line(line)?;
126 }
127
128 Ok(())
129 };
130
131 for sample in &self.samples {
132 for location_id in &sample.location_id {
133 if *location_id == 0 {
134 return Err(PprofError::NullLocation);
135 }
136
137 let maybe_location = self.locations.get(*location_id as usize - 1);
138 match maybe_location {
139 Some(location) => validate_location(location)?,
140 None => {
141 return Err(PprofError::LocationNotFound(*location_id));
142 }
143 }
144 }
145 }
146 Ok(())
147 }
148
149 pub fn string_id(&self, string: &str) -> Option<i64> {
152 self.known_strings.get(string).copied()
153 }
154
155 pub fn get_or_insert_string(&mut self, string: &str) -> i64 {
157 if self.string_table.is_empty() {
159 self.known_strings.insert("".to_string(), 0);
160 self.string_table.push("".to_string());
161 }
162
163 match self.known_strings.entry(string.to_string()) {
164 Entry::Occupied(o) => *o.get(),
165 Entry::Vacant(v) => {
166 let id = self.string_table.len() as i64;
167 v.insert(id);
168 self.string_table.push(string.to_string());
169 id
170 }
171 }
172 }
173
174 pub fn add_function(&mut self, func_name: &str, filename: Option<String>) -> u64 {
175 let id = self.functions.len() as u64 + 1;
176 let name_idx = self.get_or_insert_string(func_name);
177
178 let function = pprof::Function {
179 id,
180 name: name_idx,
181 system_name: self.get_or_insert_string(""),
182 filename: self.get_or_insert_string(&filename.unwrap_or("".to_string())),
183 ..Default::default()
184 };
185
186 self.functions.push(function);
187
188 id
189 }
190
191 pub fn add_line(
192 &mut self,
193 func_name: &str,
194 file_name: Option<String>,
195 line: Option<u32>,
196 ) -> (pprof::Line, u64) {
197 let function_id = self.add_function(func_name, file_name);
198 let line = pprof::Line {
199 function_id,
200 line: line.unwrap_or(0) as i64,
201 column: 0,
202 };
203 (line, function_id)
204 }
205
206 pub fn add_location(&mut self, address: u64, mapping_id: u64, lines: Vec<pprof::Line>) -> u64 {
207 let id: u64 = self.locations.len() as u64 + 1;
208
209 let location = pprof::Location {
210 id,
211 mapping_id,
212 address,
213 line: lines, is_folded: false, };
216
217 self.locations.push(location);
218 id
219 }
220
221 pub fn add_mapping(
224 &mut self,
225 start: u64,
226 end: u64,
227 offset: u64,
228 filename: &str,
229 build_id: &str,
230 ) -> u64 {
231 let build_id = self.get_or_insert_string(build_id);
232
233 match self.known_mappings.entry(build_id) {
234 Entry::Occupied(o) => *o.get(),
235 Entry::Vacant(v) => {
236 let id = self.mappings.len() as u64 + 1;
237 v.insert(id);
238 let mapping = pprof::Mapping {
239 id,
240 memory_start: start,
241 memory_limit: end,
242 file_offset: offset,
243 filename: self.get_or_insert_string(filename),
244 build_id,
245 has_functions: false,
246 has_filenames: false,
247 has_line_numbers: false,
248 has_inline_frames: false,
249 };
250
251 self.mappings.push(mapping);
252
253 id
254 }
255 }
256 }
257 pub fn add_sample(&mut self, location_ids: Vec<u64>, count: i64, labels: &[pprof::Label]) {
258 let sample = pprof::Sample {
259 location_id: location_ids, value: vec![count, count * 1_000_000_000 / self.freq_in_hz],
261 label: labels.to_vec(),
262 };
263
264 self.samples.push(sample);
265 }
266
267 pub fn new_label(&mut self, key: &str, value: LabelStringOrNumber) -> pprof::Label {
268 let mut label = pprof::Label {
269 key: self.get_or_insert_string(key),
270 ..Default::default()
271 };
272
273 match value {
274 LabelStringOrNumber::String(string) => {
275 label.str = self.get_or_insert_string(&string);
276 }
277 LabelStringOrNumber::Number(num, unit) => {
278 label.num = num;
279 label.num_unit = self.get_or_insert_string(&unit);
280 }
281 }
282
283 label
284 }
285
286 pub fn build(mut self) -> pprof::Profile {
287 let sample_type = pprof::ValueType {
288 r#type: self.get_or_insert_string("samples"),
289 unit: self.get_or_insert_string("count"),
290 };
291
292 let period_type = pprof::ValueType {
293 r#type: self.get_or_insert_string("cpu"),
294 unit: self.get_or_insert_string("nanoseconds"),
295 };
296
297 let comments = vec![self.get_or_insert_string("lightswitch")];
301
302 pprof::Profile {
303 sample_type: vec![sample_type, period_type],
304 sample: self.samples,
305 mapping: self.mappings,
306 location: self.locations,
307 function: self.functions,
308 string_table: self.string_table,
309 drop_frames: 0,
310 keep_frames: 0,
311 time_nanos: self.time_nanos,
312 duration_nanos: self.duration.as_nanos() as i64,
313 period_type: Some(period_type),
314 period: 1_000_000_000 / self.freq_in_hz,
315 comment: comments,
316 default_sample_type: 0,
317 }
318 }
319}
320
321impl pprof::Profile {
322 pub fn decode(buf: impl bytes::Buf) -> Result<Self, prost::DecodeError> {
324 <Self as prost::Message>::decode(buf)
325 }
326
327 pub fn encode(&self, buf: &mut impl bytes::BufMut) -> Result<(), prost::EncodeError> {
329 <Self as prost::Message>::encode(self, buf)
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
342
343 #[test]
344 fn test_string_table() {
345 let mut pprof = PprofBuilder::new(SystemTime::now(), Duration::from_secs(5), 27);
346 assert_eq!(pprof.get_or_insert_string("hi"), 1);
347 assert_eq!(pprof.get_or_insert_string("salut"), 2);
348 assert_eq!(pprof.string_table, vec!["", "hi", "salut"]);
349
350 assert!(pprof.string_id("").is_some());
351 assert!(pprof.string_id("hi").is_some());
352 assert!(pprof.string_id("salut").is_some());
353 assert!(pprof.string_id("-_-").is_none());
354 }
355
356 #[test]
357 fn test_mappings() {
358 let mut pprof = PprofBuilder::new(SystemTime::now(), Duration::from_secs(5), 27);
359 assert_eq!(
360 pprof.add_mapping(0x100, 0x200, 0x0, "file.so", "sha256-abc"),
361 1
362 );
363 assert_eq!(
364 pprof.add_mapping(0x100, 0x200, 0x0, "file.so", "sha256-abc"),
365 1
366 );
367 assert_eq!(
368 pprof.add_mapping(0x200, 0x400, 0x100, "libc.so", "sha256-bad"),
369 2
370 );
371 assert_eq!(pprof.mappings[0].memory_start, 0x100);
372 assert_eq!(
373 pprof.mappings[0].filename,
374 pprof.string_id("file.so").unwrap()
375 );
376 assert_eq!(pprof.mappings.len(), 2);
377 }
378
379 #[test]
380 fn test_locations() {
381 let mut pprof = PprofBuilder::new(SystemTime::now(), Duration::from_secs(5), 27);
382 let _ = pprof.add_line("hahahaha-first-line", None, None);
383 let (line, function_id) = pprof.add_line("test-line", Some("test-file".into()), Some(42));
384
385 assert_eq!(pprof.add_location(0x123, 0x1111, vec![line]), 1);
386 assert_eq!(pprof.add_location(0x123, 0x1111, vec![line]), 2);
387 assert_eq!(pprof.add_location(0x256, 0x2222, vec![line]), 3);
388 assert_eq!(pprof.add_location(0x512, 0x3333, vec![line]), 4);
389
390 assert_eq!(pprof.locations.len(), 4);
391 assert_eq!(
392 pprof.locations[0],
393 pprof::Location {
394 id: 1, mapping_id: 0x1111,
396 address: 0x123,
397 line: vec![pprof::Line {
398 function_id,
399 line: 42,
400 column: 0,
401 }],
402 is_folded: false
403 }
404 );
405
406 assert_eq!(pprof.functions.len(), 2);
407 assert_eq!(
408 pprof.functions[1].filename,
409 pprof.string_id("test-file").unwrap()
410 );
411 }
412
413 #[test]
414 fn test_sample() {
415 let mut pprof = PprofBuilder::new(SystemTime::now(), Duration::from_secs(5), 27);
416 let labels = vec![
417 pprof.new_label("key", LabelStringOrNumber::String("value".into())),
418 pprof.new_label("key", LabelStringOrNumber::Number(123, "pid".into())),
419 ];
420 pprof.add_sample(vec![1, 2, 3], 100, &labels);
421 pprof.add_sample(vec![1, 2, 3], 100, &labels);
422
423 assert_eq!(pprof.samples.len(), 2);
424 assert_eq!(
425 pprof.samples[0].label,
426 vec![
427 pprof::Label {
428 key: pprof.string_id("key").unwrap(),
429 str: pprof.string_id("value").unwrap(),
430 ..Default::default()
431 },
432 pprof::Label {
433 key: pprof.string_id("key").unwrap(),
434 num: 123,
435 num_unit: pprof.string_id("pid").unwrap(),
436 ..Default::default()
437 }
438 ]
439 );
440 }
441
442 #[test]
443 fn test_profile() {
444 let mut pprof = PprofBuilder::new(SystemTime::now(), Duration::from_secs(5), 27);
445 let raw_samples = vec![
446 (vec![123_u64], 200),
447 (vec![0, 20, 30, 40, 50], 900),
448 (vec![1, 2, 3, 4, 5, 99999], 2000),
449 ];
450
451 for raw_sample in raw_samples {
452 let mut location_ids = Vec::new();
453 let count = raw_sample.1;
454
455 for (i, addr) in raw_sample.0.into_iter().enumerate() {
456 let mapping_id: u64 = pprof.add_mapping(
457 (i * 100) as u64,
458 (i * 100 + 100) as u64,
459 0,
460 if addr.is_multiple_of(2) {
461 "fake.so"
462 } else {
463 "test.so"
464 },
465 if addr.is_multiple_of(2) {
466 "sha256-fake"
467 } else {
468 "golang-fake"
469 },
470 );
471 location_ids.push(pprof.add_location(addr, mapping_id, vec![]));
472 }
473
474 pprof.add_sample(location_ids, count, &[]);
475 }
476
477 assert!(pprof.validate().is_ok());
478 pprof.build();
479 }
480
481 #[test]
482 fn test_encode_decode() {
483 let mut pprof = PprofBuilder::new(SystemTime::now(), Duration::from_secs(5), 27);
484 pprof.add_function("func1", None);
485 let mapping_id = pprof.add_mapping(0x33, 0x66, 0x54, "prof.so", "fake-buildid");
486 let location_ids = vec![pprof.add_location(0x33, mapping_id, vec![])];
487 pprof.add_sample(location_ids, 1, &[]);
488
489 let profile = pprof.build();
490
491 let mut buff = bytes::BytesMut::new();
492 profile.encode(&mut buff).expect("Unable to encode profile");
493
494 let decoded_profile = pprof::Profile::decode(buff.freeze()).expect("unable to decode");
495
496 assert_eq!(decoded_profile, profile);
497 }
498}