linux_perf_data/jitdump/
jitdump_reader.rs

1use linux_perf_event_reader::{Endianness, RawData};
2use std::io::{Read, Seek};
3
4use super::buffered_reader::BufferedReader;
5use super::error::JitDumpError;
6use super::header::JitDumpHeader;
7use super::read_exact::ReadExactOrUntilEof;
8use super::record::{JitDumpRawRecord, JitDumpRecordHeader, JitDumpRecordType};
9
10/// Parses a jitdump file and allows iterating over records.
11///
12/// This reader works with complete jitdump files as well as with partial files
13/// which are still being written to. This makes it useful in live-profiling
14/// settings.
15///
16/// The records refer to memory owned by the reader, to minimize copies.
17#[derive(Debug, Clone)]
18pub struct JitDumpReader<R: Read> {
19    reader: BufferedReader<R>,
20    header: JitDumpHeader,
21    endian: Endianness,
22    /// None if the reader is currently pointing to the start of the header of the next entry.
23    /// Some() if the reader is currently pointing after the header but before the body of an entry.
24    pending_record_header: Option<JitDumpRecordHeader>,
25    current_record_start_offset: u64,
26}
27
28impl<R: Read> JitDumpReader<R> {
29    /// Create a new `JitDumpReader`. `JitDumpReader` does its own buffering so
30    /// there is no need to wrap a [`File`](std::fs::File) into a `BufReader`.
31    pub fn new(reader: R) -> Result<Self, JitDumpError> {
32        Self::new_with_buffer_size(reader, 4 * 1024)
33    }
34
35    /// Create a new `JitDumpReader`, with a manually-specified buffer chunk size.
36    pub fn new_with_buffer_size(mut reader: R, buffer_size: usize) -> Result<Self, JitDumpError> {
37        let mut buf = vec![0; buffer_size];
38        let first_data_len = reader
39            .read_exact_or_until_eof(&mut buf)
40            .map_err(JitDumpError::Io)?;
41
42        let first_data = &buf[..first_data_len];
43        let header = JitDumpHeader::parse(RawData::Single(first_data))?;
44        let total_header_size = header.total_size;
45        let endian = match &header.magic {
46            b"DTiJ" => Endianness::LittleEndian,
47            b"JiTD" => Endianness::BigEndian,
48            _ => panic!(),
49        };
50
51        Ok(Self {
52            reader: BufferedReader::new_with_partially_read_buffer(
53                reader,
54                buf,
55                total_header_size as usize,
56                first_data_len,
57            ),
58            header,
59            endian,
60            pending_record_header: None,
61            current_record_start_offset: total_header_size as u64,
62        })
63    }
64
65    /// The file header.
66    pub fn header(&self) -> &JitDumpHeader {
67        &self.header
68    }
69
70    /// The file endian.
71    pub fn endian(&self) -> Endianness {
72        self.endian
73    }
74
75    /// Returns the header of the next record.
76    pub fn next_record_header(&mut self) -> Result<Option<JitDumpRecordHeader>, std::io::Error> {
77        if self.pending_record_header.is_none() {
78            if let Some(record_header_bytes) =
79                self.reader.consume_data(JitDumpRecordHeader::SIZE)?
80            {
81                self.pending_record_header =
82                    Some(JitDumpRecordHeader::parse(self.endian, record_header_bytes).unwrap());
83            }
84        };
85        Ok(self.pending_record_header.clone())
86    }
87
88    /// Returns the timestamp of the next record.
89    ///
90    /// When operating on partial files, `None` means that not enough bytes for the header
91    /// of the next record are available. `Some` means that we have enough bytes for the
92    /// header but we may not have enough bytes to get the entire record.
93    ///
94    /// If `next_record_timestamp` returns `Ok(Some(...))`, the next call to `next_record()`
95    /// can still return `None`!
96    pub fn next_record_timestamp(&mut self) -> Result<Option<u64>, std::io::Error> {
97        Ok(self.next_record_header()?.map(|r| r.timestamp))
98    }
99
100    /// Returns the record type of the next record.
101    pub fn next_record_type(&mut self) -> Result<Option<JitDumpRecordType>, std::io::Error> {
102        Ok(self.next_record_header()?.map(|r| r.record_type))
103    }
104
105    /// Returns the file offset at which the next record (specifically its record header) starts.
106    pub fn next_record_offset(&self) -> u64 {
107        self.current_record_start_offset
108    }
109
110    /// Returns the next record.
111    ///
112    /// When operating on partial files, this will return `Ok(None)` if the entire record is
113    /// not available yet. Future calls to `next_record` may return `Ok(Some)` if the
114    /// data has become available in the meantime, because they will call `read` on `R` again.
115    pub fn next_record(&mut self) -> Result<Option<JitDumpRawRecord<'_>>, std::io::Error> {
116        let record_size = match self.next_record_header()? {
117            Some(header) => header.total_size,
118            None => return Ok(None),
119        };
120        let body_size = record_size as usize - JitDumpRecordHeader::SIZE;
121
122        match self.reader.consume_data(body_size)? {
123            Some(record_body_data) => {
124                let record_header = self.pending_record_header.take().unwrap();
125                let start_offset = self.current_record_start_offset;
126                self.current_record_start_offset += record_size as u64;
127                Ok(Some(JitDumpRawRecord {
128                    endian: self.endian,
129                    start_offset,
130                    record_size,
131                    record_type: record_header.record_type,
132                    timestamp: record_header.timestamp,
133                    body: record_body_data,
134                }))
135            }
136            None => Ok(None),
137        }
138    }
139}
140
141impl<R: Read + Seek> JitDumpReader<R> {
142    /// Skip the upcoming record. If this returns true, the record has been skipped.
143    /// If `false` is returned, it means the file could not be seeked far enough to
144    /// skip the entire record (for example because this is a partial file which has
145    /// not been fully written), and the next record remains unchanged from before the
146    /// call to `skip_next_record`.
147    ///
148    /// You may want to call this if you've called `next_record_type` and have
149    /// determined that you're not interested in the upcoming record. It saves having
150    /// to read the full record into a contiguous slice of memory.
151    pub fn skip_next_record(&mut self) -> Result<bool, std::io::Error> {
152        let record_size = match self.next_record_header()? {
153            Some(record_header) => record_header.total_size,
154            None => return Ok(false),
155        };
156        let body_size = record_size as usize - JitDumpRecordHeader::SIZE; // TODO: Handle underflow
157
158        self.reader.skip_bytes(body_size)?;
159        self.pending_record_header.take();
160        self.current_record_start_offset += record_size as u64;
161        Ok(true)
162    }
163}