Skip to main content

lightswitch_proto/
profile.rs

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    /// Value and unit.
33    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    /// Run some validations to ensure that the profile is semantically correct.
81    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    /// Returns the id for a string in the string table or None if it's not
150    /// present.
151    pub fn string_id(&self, string: &str) -> Option<i64> {
152        self.known_strings.get(string).copied()
153    }
154
155    /// Inserts a string in the string table and returns its id.
156    pub fn get_or_insert_string(&mut self, string: &str) -> i64 {
157        // The first element in the string table must be the empty string.
158        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,      // only used for local symbolisation.
214            is_folded: false, // only used for local symbolisation.
215        };
216
217        self.locations.push(location);
218        id
219    }
220
221    /// Adds a memory mapping. The id of the mapping is derived from the hash of
222    /// the code region and should be unique.
223    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, // from the source code: `The leaf is at location_id\[0\].`
260            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        // Used to identify profiles generated by lightswitch.
298        // This is useful because the mapping ID is used in a non-standard way
299        // which should not be interpreted like this by other pprof sources.
300        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    /// deserialize a protobuf encoded message into [`Self`]
323    pub fn decode(buf: impl bytes::Buf) -> Result<Self, prost::DecodeError> {
324        <Self as prost::Message>::decode(buf)
325    }
326
327    /// serialize [`Self`] as protobuf
328    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    // Cheat sheet:
336    // - decode protobuf: `protoc --decode perftools.profiles.Profile
337    //   src/proto/profile.proto < profile.pb`
338    // - validate it: (in pprof's codebase) `go tool pprof profile.pb`
339    // - print it: `go tool pprof -raw profile.pb`
340    // - http server: `go tool pprof -http=:8080 profile.pb`
341    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, // The IDs are incremental and start with 1.
395                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}