Skip to content

Commit f7dd066

Browse files
committed
Support zstd compressed data
1 parent 9729e91 commit f7dd066

14 files changed

Lines changed: 628 additions & 6 deletions

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.14", default-features = false, features = ["std"] }
2327
prost-derive = "0.14"
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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +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_COMPRESSED2: u32 = 83;
2122

2223
// pub const SIMPLE_PERF_RECORD_TYPE_START: u32 = 32768;
2324

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: 92 additions & 4 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;
@@ -204,6 +207,8 @@ impl<C: Read + Seek> PerfFileReader<C> {
204207
buffers_for_recycling: VecDeque::new(),
205208
current_event_body: Vec::new(),
206209
pending_first_record: None,
210+
#[cfg(feature = "zstd")]
211+
zstd_decompressor: ZstdDecompressor::new(),
207212
};
208213

209214
Ok(Self {
@@ -374,6 +379,8 @@ impl<R: Read> PerfFileReader<R> {
374379
buffers_for_recycling: VecDeque::new(),
375380
current_event_body: Vec::new(),
376381
pending_first_record,
382+
#[cfg(feature = "zstd")]
383+
zstd_decompressor: ZstdDecompressor::new(),
377384
};
378385

379386
Ok(Self {
@@ -399,6 +406,9 @@ pub struct PerfRecordIter<R: Read> {
399406
buffers_for_recycling: VecDeque<Vec<u8>>,
400407
/// For pipe mode: the first non-metadata record that was read during initialization
401408
pending_first_record: Option<(PerfEventHeader, Vec<u8>)>,
409+
/// Zstd decompressor for handling COMPRESSED records
410+
#[cfg(feature = "zstd")]
411+
zstd_decompressor: ZstdDecompressor,
402412
}
403413

404414
impl<R: Read> PerfRecordIter<R> {
@@ -467,9 +477,9 @@ impl<R: Read> PerfRecordIter<R> {
467477
}
468478
self.read_offset += u64::from(header.size);
469479

470-
if UserRecordType::try_from(RecordType(header.type_))
471-
== Some(UserRecordType::PERF_FINISHED_ROUND)
472-
{
480+
let user_record_type = UserRecordType::try_from(RecordType(header.type_));
481+
482+
if user_record_type == Some(UserRecordType::PERF_FINISHED_ROUND) {
473483
self.sorter.finish_round();
474484
if self.sorter.has_more() {
475485
// The sorter is non-empty. We're done.
@@ -484,7 +494,6 @@ impl<R: Read> PerfRecordIter<R> {
484494
let event_body_len = size - PerfEventHeader::STRUCT_SIZE;
485495
let mut buffer = self.buffers_for_recycling.pop_front().unwrap_or_default();
486496
buffer.resize(event_body_len, 0);
487-
488497
// Try to read the event body. For pipe mode, EOF here also means end of stream.
489498
match self.reader.read_exact(&mut buffer) {
490499
Ok(()) => {}
@@ -499,6 +508,28 @@ impl<R: Read> PerfRecordIter<R> {
499508
}
500509
}
501510

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

@@ -550,7 +581,64 @@ impl<R: Read> PerfRecordIter<R> {
550581
attr_index,
551582
};
552583
self.sorter.insert_unordered(sort_key, pending_record);
584+
Ok(())
585+
}
553586

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

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) {

src/record.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ impl UserRecordType {
5555
pub const PERF_TIME_CONV: Self = Self(RecordType(PERF_RECORD_TIME_CONV));
5656
pub const PERF_HEADER_FEATURE: Self = Self(RecordType(PERF_RECORD_HEADER_FEATURE));
5757
pub const PERF_COMPRESSED: Self = Self(RecordType(PERF_RECORD_COMPRESSED));
58+
pub const PERF_COMPRESSED2: Self = Self(RecordType(PERF_RECORD_COMPRESSED2));
5859

5960
pub const SIMPLEPERF_KERNEL_SYMBOL: Self = Self(RecordType(SIMPLE_PERF_RECORD_KERNEL_SYMBOL));
6061
pub const SIMPLEPERF_DSO: Self = Self(RecordType(SIMPLE_PERF_RECORD_DSO));
@@ -107,6 +108,7 @@ impl std::fmt::Debug for UserRecordType {
107108
Self::PERF_TIME_CONV => "PERF_TIME_CONV".fmt(f),
108109
Self::PERF_HEADER_FEATURE => "PERF_HEADER_FEATURE".fmt(f),
109110
Self::PERF_COMPRESSED => "PERF_COMPRESSED".fmt(f),
111+
Self::PERF_COMPRESSED2 => "PERF_COMPRESSED2".fmt(f),
110112
Self::SIMPLEPERF_KERNEL_SYMBOL => "SIMPLEPERF_KERNEL_SYMBOL".fmt(f),
111113
Self::SIMPLEPERF_DSO => "SIMPLEPERF_DSO".fmt(f),
112114
Self::SIMPLEPERF_SYMBOL => "SIMPLEPERF_SYMBOL".fmt(f),

0 commit comments

Comments
 (0)