Skip to content

Commit f9383e4

Browse files
committed
feat: support zstd compressed data
1 parent a0f9ab7 commit f9383e4

14 files changed

+640
-18
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ documentation = "https://docs.rs/linux-perf-data/"
1212
repository = "https://github.com/mstange/linux-perf-data/"
1313
exclude = ["/.github", "/.vscode", "/tests"]
1414

15+
[features]
16+
default = ["zstd"]
17+
zstd = ["zstd-safe"]
18+
1519
[dependencies]
1620
byteorder = "1.4.3"
1721
memchr = "2.4.1"
@@ -21,6 +25,7 @@ linux-perf-event-reader = "0.10.0"
2125
linear-map = "1.2.0"
2226
prost = { version = "0.13", default-features = false, features = ["std"] }
2327
prost-derive = "0.13"
28+
zstd-safe = { version = "7.2", optional = true }
2429

2530
[dev-dependencies]
2631
yaxpeax-arch = { version = "0.3", default-features = false }

src/constants.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub const PERF_RECORD_EVENT_UPDATE: u32 = 78;
1818
pub const PERF_RECORD_TIME_CONV: u32 = 79;
1919
pub const PERF_RECORD_HEADER_FEATURE: u32 = 80;
2020
pub const PERF_RECORD_COMPRESSED: u32 = 81;
21-
pub const PERF_RECORD_FINISHED_INIT: u32 = 82;
21+
pub const PERF_RECORD_COMPRESSED2: u32 = 83;
2222

2323
// pub const SIMPLE_PERF_RECORD_TYPE_START: u32 = 32768;
2424

src/decompression.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use zstd_safe::{DCtx, InBuffer, OutBuffer};
2+
3+
/// A zstd decompressor for PERF_RECORD_COMPRESSED records.
4+
pub struct ZstdDecompressor {
5+
dctx: Option<DCtx<'static>>,
6+
/// Buffer for partial perf records that span multiple compressed chunks
7+
partial_record_buffer: Vec<u8>,
8+
}
9+
10+
impl Default for ZstdDecompressor {
11+
fn default() -> Self {
12+
Self::new()
13+
}
14+
}
15+
16+
impl ZstdDecompressor {
17+
pub fn new() -> Self {
18+
Self {
19+
dctx: None,
20+
partial_record_buffer: Vec::new(),
21+
}
22+
}
23+
24+
/// Decompress a chunk of zstd data.
25+
pub fn decompress(&mut self, compressed_data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
26+
let dctx = self.dctx.get_or_insert_with(DCtx::create);
27+
28+
let mut decompressed = vec![0; compressed_data.len() * 4];
29+
let mut in_buffer = InBuffer::around(compressed_data);
30+
let mut total_out = 0;
31+
32+
while in_buffer.pos < in_buffer.src.len() {
33+
let available = decompressed.len() - total_out;
34+
let mut out_buffer = OutBuffer::around(&mut decompressed[total_out..]);
35+
36+
match dctx.decompress_stream(&mut out_buffer, &mut in_buffer) {
37+
Ok(_) => {
38+
total_out += out_buffer.pos();
39+
if out_buffer.pos() == available {
40+
decompressed.resize(decompressed.len() + compressed_data.len() * 4, 0);
41+
}
42+
}
43+
Err(code) => {
44+
let error_name = zstd_safe::get_error_name(code);
45+
return Err(std::io::Error::new(
46+
std::io::ErrorKind::InvalidData,
47+
format!("Zstd decompression failed: {}", error_name),
48+
));
49+
}
50+
}
51+
}
52+
53+
decompressed.truncate(total_out);
54+
55+
// Prepend any partial record data from the previous chunk
56+
if !self.partial_record_buffer.is_empty() {
57+
let mut combined = std::mem::take(&mut self.partial_record_buffer);
58+
combined.extend_from_slice(&decompressed);
59+
decompressed = combined;
60+
}
61+
62+
Ok(decompressed)
63+
}
64+
65+
/// Save partial record data that spans to the next compressed chunk.
66+
pub fn save_partial_record(&mut self, data: &[u8]) {
67+
self.partial_record_buffer = data.to_vec();
68+
}
69+
}

src/feature_sections.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,40 @@ impl SampleTimeRange {
4949
}
5050
}
5151

52+
/// Information about compression used in the perf.data file.
53+
#[derive(Debug, Clone, Copy)]
54+
pub struct CompressionInfo {
55+
pub version: u32,
56+
/// Compression algorithm type. 1 = Zstd
57+
pub type_: u32,
58+
/// Compression level (e.g., 1-22 for Zstd)
59+
pub level: u32,
60+
/// Compression ratio achieved
61+
pub ratio: u32,
62+
/// mmap buffer size
63+
pub mmap_len: u32,
64+
}
65+
66+
impl CompressionInfo {
67+
pub const STRUCT_SIZE: usize = 4 + 4 + 4 + 4 + 4;
68+
pub const ZSTD_TYPE: u32 = 1;
69+
70+
pub fn parse<R: Read, T: ByteOrder>(mut reader: R) -> Result<Self, std::io::Error> {
71+
let version = reader.read_u32::<T>()?;
72+
let type_ = reader.read_u32::<T>()?;
73+
let level = reader.read_u32::<T>()?;
74+
let ratio = reader.read_u32::<T>()?;
75+
let mmap_len = reader.read_u32::<T>()?;
76+
Ok(Self {
77+
version,
78+
type_,
79+
level,
80+
ratio,
81+
mmap_len,
82+
})
83+
}
84+
}
85+
5286
pub struct HeaderString;
5387

5488
impl HeaderString {

src/file_reader.rs

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use linux_perf_event_reader::{
99
use std::collections::{HashMap, VecDeque};
1010
use std::io::{Cursor, Read, Seek, SeekFrom};
1111

12+
#[cfg(feature = "zstd")]
13+
use crate::decompression::ZstdDecompressor;
14+
1215
use super::error::{Error, ReadError};
1316
use super::feature_sections::AttributeDescription;
1417
use super::features::Feature;
@@ -196,6 +199,8 @@ impl<C: Read + Seek> PerfFileReader<C> {
196199
buffers_for_recycling: VecDeque::new(),
197200
current_event_body: Vec::new(),
198201
pending_first_record: None,
202+
#[cfg(feature = "zstd")]
203+
zstd_decompressor: ZstdDecompressor::new(),
199204
};
200205

201206
Ok(Self {
@@ -230,10 +235,7 @@ impl<R: Read> PerfFileReader<R> {
230235
}
231236
}
232237

233-
fn parse_pipe_impl<T: ByteOrder>(
234-
mut reader: R,
235-
endian: Endianness,
236-
) -> Result<Self, Error> {
238+
fn parse_pipe_impl<T: ByteOrder>(mut reader: R, endian: Endianness) -> Result<Self, Error> {
237239
let mut attributes = Vec::new();
238240
let mut feature_sections = LinearMap::new();
239241
let mut pending_first_record: Option<(PerfEventHeader, Vec<u8>)> = None;
@@ -302,16 +304,16 @@ impl<R: Read> PerfFileReader<R> {
302304
// If EVENT_DESC feature is present, extract event names from it
303305
if let Some(event_desc_data) = feature_sections.get(&Feature::EVENT_DESC) {
304306
let event_desc_attrs = AttributeDescription::parse_event_desc_section::<_, T>(
305-
Cursor::new(&event_desc_data[..])
307+
Cursor::new(&event_desc_data[..]),
306308
)?;
307309

308310
// Match attributes by event IDs and update names
309311
for attr in attributes.iter_mut() {
310312
// Find matching event in EVENT_DESC by comparing event IDs
311313
if let Some(event_desc_attr) = event_desc_attrs.iter().find(|desc| {
312-
!desc.event_ids.is_empty() &&
313-
!attr.event_ids.is_empty() &&
314-
desc.event_ids[0] == attr.event_ids[0]
314+
!desc.event_ids.is_empty()
315+
&& !attr.event_ids.is_empty()
316+
&& desc.event_ids[0] == attr.event_ids[0]
315317
}) {
316318
attr.name = event_desc_attr.name.clone();
317319
}
@@ -384,6 +386,8 @@ impl<R: Read> PerfFileReader<R> {
384386
buffers_for_recycling: VecDeque::new(),
385387
current_event_body: Vec::new(),
386388
pending_first_record,
389+
#[cfg(feature = "zstd")]
390+
zstd_decompressor: ZstdDecompressor::new(),
387391
};
388392

389393
Ok(Self {
@@ -408,6 +412,9 @@ pub struct PerfRecordIter<R: Read> {
408412
buffers_for_recycling: VecDeque<Vec<u8>>,
409413
/// For pipe mode: the first non-metadata record that was read during initialization
410414
pending_first_record: Option<(PerfEventHeader, Vec<u8>)>,
415+
/// Zstd decompressor for handling COMPRESSED records
416+
#[cfg(feature = "zstd")]
417+
zstd_decompressor: ZstdDecompressor,
411418
}
412419

413420
impl<R: Read> PerfRecordIter<R> {
@@ -458,7 +465,9 @@ impl<R: Read> PerfRecordIter<R> {
458465
Ok(header) => header,
459466
Err(e) => {
460467
// For pipe mode with unbounded length, EOF just means end of stream
461-
if self.record_data_len == u64::MAX && e.kind() == std::io::ErrorKind::UnexpectedEof {
468+
if self.record_data_len == u64::MAX
469+
&& e.kind() == std::io::ErrorKind::UnexpectedEof
470+
{
462471
break;
463472
}
464473
return Err(e.into());
@@ -471,9 +480,9 @@ impl<R: Read> PerfRecordIter<R> {
471480
}
472481
self.read_offset += u64::from(header.size);
473482

474-
if UserRecordType::try_from(RecordType(header.type_))
475-
== Some(UserRecordType::PERF_FINISHED_ROUND)
476-
{
483+
let user_record_type = UserRecordType::try_from(RecordType(header.type_));
484+
485+
if user_record_type == Some(UserRecordType::PERF_FINISHED_ROUND) {
477486
self.sorter.finish_round();
478487
if self.sorter.has_more() {
479488
// The sorter is non-empty. We're done.
@@ -488,19 +497,42 @@ impl<R: Read> PerfRecordIter<R> {
488497
let event_body_len = size - PerfEventHeader::STRUCT_SIZE;
489498
let mut buffer = self.buffers_for_recycling.pop_front().unwrap_or_default();
490499
buffer.resize(event_body_len, 0);
491-
492500
// Try to read the event body. For pipe mode, EOF here also means end of stream.
493501
match self.reader.read_exact(&mut buffer) {
494-
Ok(()) => {},
502+
Ok(()) => {}
495503
Err(e) => {
496504
// For pipe mode with unbounded length, EOF just means end of stream
497-
if self.record_data_len == u64::MAX && e.kind() == std::io::ErrorKind::UnexpectedEof {
505+
if self.record_data_len == u64::MAX
506+
&& e.kind() == std::io::ErrorKind::UnexpectedEof
507+
{
498508
break;
499509
}
500510
return Err(ReadError::PerfEventData.into());
501511
}
502512
}
503513

514+
if user_record_type == Some(UserRecordType::PERF_COMPRESSED) {
515+
// PERF_COMPRESSED is the old format, not yet implemented
516+
return Err(Error::IoError(std::io::Error::new(
517+
std::io::ErrorKind::Unsupported,
518+
"PERF_COMPRESSED (type 81) is not supported yet, only PERF_COMPRESSED2 (type 83)",
519+
)));
520+
}
521+
522+
if user_record_type == Some(UserRecordType::PERF_COMPRESSED2) {
523+
#[cfg(not(feature = "zstd"))]
524+
{
525+
return Err(Error::IoError(std::io::Error::new(std::io::ErrorKind::Unsupported,
526+
"Compression support is not enabled. Please rebuild with the 'zstd' feature flag.",
527+
)));
528+
}
529+
#[cfg(feature = "zstd")]
530+
{
531+
self.decompress_and_process_compressed2::<T>(&buffer)?;
532+
continue;
533+
}
534+
}
535+
504536
self.process_record::<T>(header, buffer, offset)?;
505537
}
506538

@@ -552,7 +584,64 @@ impl<R: Read> PerfRecordIter<R> {
552584
attr_index,
553585
};
554586
self.sorter.insert_unordered(sort_key, pending_record);
587+
Ok(())
588+
}
555589

590+
/// Decompresses a PERF_RECORD_COMPRESSED2 record and processes its sub-records.
591+
#[cfg(feature = "zstd")]
592+
fn decompress_and_process_compressed2<T: ByteOrder>(
593+
&mut self,
594+
buffer: &[u8],
595+
) -> Result<(), Error> {
596+
if buffer.len() < 8 {
597+
return Err(ReadError::PerfEventData.into());
598+
}
599+
let data_size = T::read_u64(&buffer[0..8]) as usize;
600+
if data_size > buffer.len() - 8 {
601+
return Err(ReadError::PerfEventData.into());
602+
}
603+
let compressed_data = &buffer[8..8 + data_size];
604+
605+
let decompressed = self.zstd_decompressor.decompress(compressed_data)?;
606+
607+
// Parse the decompressed data as a sequence of perf records
608+
let mut cursor = Cursor::new(&decompressed[..]);
609+
let mut offset = 0u64;
610+
611+
while (cursor.position() as usize) < decompressed.len() {
612+
let header_start = cursor.position() as usize;
613+
// Check if we have enough bytes for a header
614+
let remaining = decompressed.len() - header_start;
615+
if remaining < PerfEventHeader::STRUCT_SIZE {
616+
self.zstd_decompressor
617+
.save_partial_record(&decompressed[header_start..]);
618+
break;
619+
}
620+
621+
let sub_header = PerfEventHeader::parse::<_, T>(&mut cursor)?;
622+
let sub_size = sub_header.size as usize;
623+
if sub_size < PerfEventHeader::STRUCT_SIZE {
624+
return Err(Error::InvalidPerfEventSize);
625+
}
626+
627+
let sub_event_body_len = sub_size - PerfEventHeader::STRUCT_SIZE;
628+
// Check if we have enough bytes for the sub-record body
629+
let remaining_after_header = decompressed.len() - cursor.position() as usize;
630+
if sub_event_body_len > remaining_after_header {
631+
self.zstd_decompressor
632+
.save_partial_record(&decompressed[header_start..]);
633+
break;
634+
}
635+
636+
let mut sub_buffer = self.buffers_for_recycling.pop_front().unwrap_or_default();
637+
sub_buffer.resize(sub_event_body_len, 0);
638+
cursor
639+
.read_exact(&mut sub_buffer)
640+
.map_err(|_| ReadError::PerfEventData)?;
641+
642+
self.process_record::<T>(sub_header, sub_buffer, offset)?;
643+
offset += sub_size as u64;
644+
}
556645
Ok(())
557646
}
558647

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
6565
mod build_id_event;
6666
mod constants;
67+
#[cfg(feature = "zstd")]
68+
mod decompression;
6769
mod dso_info;
6870
mod dso_key;
6971
mod error;
@@ -91,7 +93,7 @@ pub use linux_perf_event_reader::Endianness;
9193
pub use dso_info::DsoInfo;
9294
pub use dso_key::DsoKey;
9395
pub use error::{Error, ReadError};
94-
pub use feature_sections::{AttributeDescription, NrCpus, SampleTimeRange};
96+
pub use feature_sections::{AttributeDescription, CompressionInfo, NrCpus, SampleTimeRange};
9597
pub use features::{Feature, FeatureSet, FeatureSetIter};
9698
pub use file_reader::{PerfFileReader, PerfRecordIter};
9799
pub use perf_file::PerfFile;

src/perf_file.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use super::dso_info::DsoInfo;
1010
use super::dso_key::DsoKey;
1111
use super::error::Error;
1212
use super::feature_sections::{
13-
AttributeDescription, ClockData, NrCpus, PmuMappings, SampleTimeRange,
13+
AttributeDescription, ClockData, CompressionInfo, NrCpus, PmuMappings, SampleTimeRange,
1414
};
1515
use super::features::{Feature, FeatureSet};
1616
use super::simpleperf;
@@ -213,6 +213,18 @@ impl PerfFile {
213213
.transpose()
214214
}
215215

216+
/// Information about compression used in the perf.data file
217+
pub fn compression_info(&self) -> Result<Option<CompressionInfo>, Error> {
218+
self.feature_section_data(Feature::COMPRESSED)
219+
.map(|section| {
220+
Ok(match self.endian {
221+
Endianness::LittleEndian => CompressionInfo::parse::<_, LittleEndian>(section),
222+
Endianness::BigEndian => CompressionInfo::parse::<_, BigEndian>(section),
223+
}?)
224+
})
225+
.transpose()
226+
}
227+
216228
/// The meta info map, if this is a Simpleperf profile.
217229
pub fn simpleperf_meta_info(&self) -> Result<Option<HashMap<&str, &str>>, Error> {
218230
match self.feature_section_data(Feature::SIMPLEPERF_META_INFO) {

0 commit comments

Comments
 (0)