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 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    /// (address, mapping_id) => location_id
26    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    /// Value and unit.
38    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    /// Run some validations to ensure that the profile is semantically correct.
92    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    /// Returns the id for a string in the string table or None if it's not
161    /// present.
162    pub fn string_id(&self, string: &str) -> Option<i64> {
163        self.known_strings.get(string).copied()
164    }
165
166    /// Inserts a string in the string table and returns its id.
167    pub fn get_or_insert_string(&mut self, string: &str) -> i64 {
168        // The first element in the string table must be the empty string.
169        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,      // only used for local symbolisation.
233            is_folded: false, // only used for local symbolisation.
234        };
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    /// Adds a memory mapping. The id of the mapping is derived from the hash of
250    /// the code region and should be unique.
251    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, // from the source code: `The leaf is at location_id\[0\].`
286            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        // Used to identify profiles generated by lightswitch.
324        // This is useful because the mapping ID is used in a non-standard way
325        // which should not be interpreted like this by other pprof sources.
326        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    /// deserialize a protobuf encoded message into [`Self`]
349    pub fn decode(buf: impl bytes::Buf) -> Result<Self, prost::DecodeError> {
350        <Self as prost::Message>::decode(buf)
351    }
352
353    /// serialize [`Self`] as protobuf
354    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    // Cheat sheet:
362    // - decode protobuf: `protoc --decode perftools.profiles.Profile
363    //   src/proto/profile.proto < profile.pb`
364    // - validate it: (in pprof's codebase) `go tool pprof profile.pb`
365    // - print it: `go tool pprof -raw profile.pb`
366    // - http server: `go tool pprof -http=:8080 profile.pb`
367    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, // The IDs are incremental and start with 1.
416                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 }, // id 0 is reserved and can't be used.
479                    (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}