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