stackpulse 0.10.0

Linux perf_event stack sampling with native unwinding, symbolization, and compact spooling
Documentation
use std::io;

use super::sorter::EventSorter;
use super::{
    finish_prepared_event, perf_event, prepare_event, record_module, ConvertRegs,
    ConvertRegsNative, EventContext, PreparedEvent, ProcessTable, RecordingSummary,
};
use crate::spool::{ModuleOwner, ModuleRecord, ModuleTable, PerfSpoolWriter};

const LIVE_BENCH_PROCESS_ID: u32 = 42_000;
const LIVE_BENCH_USER_BASE: u64 = 0x7000_0000_0000;
const LIVE_BENCH_KERNEL_BASE: u64 = 0xffff_ffff_8100_0000;
const LIVE_BENCH_RING_COUNT: usize = 4;

pub(crate) struct LivePerfSampleBenchFixture {
    samples: perf_event::BenchSampleBatch,
    modules: Vec<ModuleRecord>,
    spool_capacity: usize,
}

impl LivePerfSampleBenchFixture {
    pub(crate) fn event_bytes(&self) -> usize {
        self.samples.event_bytes()
    }

    pub(crate) fn sample_count(&self) -> usize {
        self.samples.sample_count()
    }
}

pub(crate) fn live_perf_sample_bench_fixture() -> LivePerfSampleBenchFixture {
    let samples = perf_event::BenchSampleBatch::new(perf_event::BenchSampleBatchSpec {
        samples: 4_096,
        user_frames: 0,
        kernel_frames: 8,
        user_regs: ConvertRegsNative::regs_mask().count_ones() as usize,
        user_stack_bytes: 512,
        process_id: LIVE_BENCH_PROCESS_ID,
        thread_count: 32,
        user_base: LIVE_BENCH_USER_BASE,
        kernel_base: LIVE_BENCH_KERNEL_BASE,
    });
    let modules = live_perf_sample_bench_modules();
    let spool_capacity = 64 * 1024 + samples.frame_count() * 16 + samples.sample_count() * 16;
    LivePerfSampleBenchFixture {
        samples,
        modules,
        spool_capacity,
    }
}

pub(crate) fn bench_parse_live_perf_samples(
    fixture: &LivePerfSampleBenchFixture,
    rounds: u64,
) -> usize {
    perf_event::bench_parse_sample_records(&fixture.samples, rounds)
}

pub(crate) fn bench_perf_ring_record_lifecycle(
    fixture: &LivePerfSampleBenchFixture,
    ring_count: usize,
    rounds: u64,
) -> io::Result<usize> {
    let (batches, mut rings) = build_mock_rings(&fixture.samples, ring_count)?;

    let mut checksum = 0usize;
    let mut consumed = 0usize;
    for round in 0..rounds {
        for (ring, bytes) in rings.iter_mut().zip(&batches) {
            if round != 0 {
                super::ring_buffer::mock_publish(ring, bytes);
            }
            let head = ring.snapshot_head();
            while let Some(record) = ring.next_record_to(head)? {
                consumed += 1;
                checksum = checksum
                    .wrapping_add(record.as_bytes().len())
                    .wrapping_add(usize::from(record.as_bytes()[0]));
            }
        }
    }
    std::hint::black_box(checksum);
    Ok(consumed)
}

pub(crate) fn bench_replay_live_perf_ring_records(
    fixture: &LivePerfSampleBenchFixture,
    rounds: u64,
) -> io::Result<usize> {
    let (batches, mut rings) = build_mock_rings(&fixture.samples, LIVE_BENCH_RING_COUNT)?;

    let mut checksum = 0usize;
    for round in 0..rounds {
        let mut writer = PerfSpoolWriter::from_writer(
            Vec::with_capacity(fixture.spool_capacity),
            1_700_000_000_000_000 + round,
            1_000,
        )?;
        let mut modules = ModuleTable::default();
        let mut processes = ProcessTable::default();
        for module in &fixture.modules {
            record_module(&mut modules, &mut processes, &mut writer, module.clone())?;
        }

        let mut summary = RecordingSummary::default();
        let mut stack_scratch = Vec::with_capacity(128);
        let mut callchain_scratch = Vec::with_capacity(32);
        let mut lifecycle_actions = Vec::new();
        let mut sorter = EventSorter::<usize, u64, PreparedEvent>::new();
        let mut result: io::Result<()> = Ok(());
        {
            let mut ctx = EventContext {
                modules: &mut modules,
                processes: &mut processes,
                writer: &mut writer,
                summary: &mut summary,
                stack_scratch: &mut stack_scratch,
                callchain_scratch: &mut callchain_scratch,
                lifecycle_actions: &mut lifecycle_actions,
                inherit_child_processes: false,
            };
            for (ring_index, (ring, bytes)) in rings.iter_mut().zip(&batches).enumerate() {
                if round != 0 {
                    super::ring_buffer::mock_publish(ring, bytes);
                }
                sorter.begin_group(ring_index);
                let mut drain = fixture.samples.event_drain(ring);
                while result.is_ok() {
                    let next = drain.next_event(&mut |event| {
                        let timestamp = event.timestamp().unwrap_or(0);
                        (timestamp, prepare_event(event, ctx.summary))
                    })?;
                    let Some((timestamp, prepared)) = next else {
                        break;
                    };
                    if let Some(prepared) = prepared {
                        sorter.push_current_group(timestamp, prepared);
                    }
                }
                while let Some(prepared) = sorter.pop() {
                    if result.is_ok() {
                        result = finish_prepared_event(prepared, &mut ctx);
                    }
                }
            }
            sorter.visit_values_mut(PreparedEvent::detach_ring_storage);
            sorter.advance_round();
            while let Some(prepared) = sorter.force_pop() {
                if result.is_ok() {
                    result = finish_prepared_event(prepared, &mut ctx);
                }
            }
        }
        result?;

        let expected_samples = fixture.samples.sample_count() as u64;
        assert_eq!(
            summary.samples, expected_samples,
            "synthetic ring replay did not write every generated sample"
        );

        writer.flush()?;
        let bytes = writer.into_inner();
        checksum = checksum
            .wrapping_add(bytes.len())
            .wrapping_add(summary.samples as usize)
            .wrapping_add(summary.sample_events as usize)
            .wrapping_add(summary.ignored_user_callchain_frames as usize)
            .wrapping_add(lifecycle_actions.len());
    }
    Ok(checksum)
}

fn build_mock_rings(
    samples: &perf_event::BenchSampleBatch,
    ring_count: usize,
) -> io::Result<(Vec<Vec<u8>>, Vec<super::ring_buffer::RingBuffer>)> {
    if !(1..=samples.sample_count()).contains(&ring_count) {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "ring_count must be between one and the fixture sample count",
        ));
    }
    let mut batches = vec![Vec::new(); ring_count];
    for (index, record) in samples.records().iter().enumerate() {
        batches[index % ring_count].extend_from_slice(record.as_bytes());
    }
    let rings = batches
        .iter()
        .map(|bytes| super::ring_buffer::mock_wrapped_ring(bytes))
        .collect();
    Ok((batches, rings))
}

fn live_perf_sample_bench_modules() -> Vec<ModuleRecord> {
    let Ok(process) = crate::Pid::try_from(LIVE_BENCH_PROCESS_ID) else {
        return Vec::new();
    };
    vec![
        ModuleRecord {
            id: 0,
            owner: ModuleOwner::Process(process),
            start: LIVE_BENCH_USER_BASE,
            end: LIVE_BENCH_USER_BASE + 0x0008_0000,
            file_offset: 0,
            inode: 1_000_001,
            device_major: 0,
            device_minor: 0,
            inode_generation: 0,
            path: "/opt/stackpulse/live-bench/libworkload.so".into(),
        },
        ModuleRecord {
            id: 0,
            owner: ModuleOwner::Process(process),
            start: LIVE_BENCH_USER_BASE + 0x0010_0000,
            end: LIVE_BENCH_USER_BASE + 0x0018_0000,
            file_offset: 0,
            inode: 1_000_002,
            device_major: 0,
            device_minor: 0,
            inode_generation: 0,
            path: "/opt/stackpulse/live-bench/python3.12".into(),
        },
        ModuleRecord {
            id: 0,
            owner: ModuleOwner::Kernel,
            start: LIVE_BENCH_KERNEL_BASE,
            end: LIVE_BENCH_KERNEL_BASE + 0x0010_0000,
            file_offset: 0,
            inode: 0,
            device_major: 0,
            device_minor: 0,
            inode_generation: 0,
            path: "[kernel.kallsyms]".into(),
        },
    ]
}