Skip to main content

lightswitch_proto/
profile.rs

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    /// (address, mapping_id) => location_id
25    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    /// Value and unit.
37    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    /// Run some validations to ensure that the profile is semantically correct.
91    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    /// Returns the id for a string in the string table or None if it's not
160    /// present.
161    pub fn string_id(&self, string: &str) -> Option<i64> {
162        self.known_strings.get(string).copied()
163    }
164
165    /// Inserts a string in the string table and returns its id.
166    pub fn get_or_insert_string(&mut self, string: &str) -> i64 {
167        // The first element in the string table must be the empty string.
168        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,      // only used for local symbolisation.
232            is_folded: false, // only used for local symbolisation.
233        };
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    /// Adds a memory mapping. The id of the mapping is derived from the hash of
249    /// the code region and should be unique.
250    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, // from the source code: `The leaf is at location_id\[0\].`
285            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        // Used to identify profiles generated by lightswitch.
323        // This is useful because the mapping ID is used in a non-standard way
324        // which should not be interpreted like this by other pprof sources.
325        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    // Cheat sheet:
349    // - decode protobuf: `protoc --decode perftools.profiles.Profile
350    //   src/proto/profile.proto < profile.pb`
351    // - validate it: (in pprof's codebase) `go tool pprof profile.pb`
352    // - print it: `go tool pprof -raw profile.pb`
353    // - http server: `go tool pprof -http=:8080 profile.pb`
354    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, // The IDs are incremental and start with 1.
403                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 }, // id 0 is reserved and can't be used.
466                    (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}