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