use serde::{Deserialize, Serialize};
pub mod tl_evt {
pub const SWITCH: u32 = 1;
pub const MIGRATE: u32 = 2;
pub const WAKEUP: u32 = 3;
pub const PI_BOOST: u32 = 4;
pub const LOCK_CONTEND: u32 = 5;
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimelineEventRaw {
pub type_: u32,
pub cpu: u32,
pub ts: u64,
pub prev_pid: u32,
pub next_pid: u32,
pub a: u64,
pub b: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(tag = "kind")]
#[allow(dead_code)] pub enum TimelineEvent {
Switch {
ts: u64,
cpu: u32,
prev_pid: u32,
next_pid: u32,
prev_state: u64,
preempt: bool,
},
Migrate {
ts: u64,
cpu: u32,
pid: u32,
orig_cpu: u32,
dest_cpu: u32,
},
Wakeup {
ts: u64,
cpu: u32,
pid: u32,
target_cpu: u32,
},
PiBoost {
ts: u64,
cpu: u32,
prober_tid: u32,
pid: u32,
old_prio: u32,
old_class_id: u32,
new_prio: u32,
new_class_id: u32,
},
LockContend {
ts: u64,
cpu: u32,
tid: u32,
lock_kva: u64,
flags: u32,
},
Unknown {
ts: u64,
cpu: u32,
type_: u32,
prev_pid: u32,
next_pid: u32,
a: u64,
b: u64,
},
}
#[allow(dead_code)]
pub fn parse_timeline_record(bytes: &[u8]) -> Option<TimelineEvent> {
if bytes.len() < std::mem::size_of::<TimelineEventRaw>() {
return None;
}
let raw = unsafe { std::ptr::read_unaligned(bytes.as_ptr() as *const TimelineEventRaw) };
Some(decode_raw(&raw))
}
#[allow(dead_code)]
pub fn parse_timeline_buf(bytes: &[u8]) -> Vec<TimelineEvent> {
let stride = std::mem::size_of::<TimelineEventRaw>();
let mut out = Vec::with_capacity(bytes.len() / stride);
let mut off = 0;
while off + stride <= bytes.len() {
if let Some(ev) = parse_timeline_record(&bytes[off..off + stride]) {
out.push(ev);
}
off += stride;
}
out
}
fn decode_raw(raw: &TimelineEventRaw) -> TimelineEvent {
match raw.type_ {
tl_evt::SWITCH => TimelineEvent::Switch {
ts: raw.ts,
cpu: raw.cpu,
prev_pid: raw.prev_pid,
next_pid: raw.next_pid,
prev_state: raw.a,
preempt: raw.b != 0,
},
tl_evt::MIGRATE => TimelineEvent::Migrate {
ts: raw.ts,
cpu: raw.cpu,
pid: raw.prev_pid,
orig_cpu: raw.b as u32,
dest_cpu: raw.a as u32,
},
tl_evt::WAKEUP => TimelineEvent::Wakeup {
ts: raw.ts,
cpu: raw.cpu,
pid: raw.prev_pid,
target_cpu: raw.a as u32,
},
tl_evt::PI_BOOST => {
let old_prio = (raw.a & 0xffff_ffff) as u32;
let old_class_id = (raw.a >> 32) as u32;
let new_prio = (raw.b & 0xffff_ffff) as u32;
let new_class_id = (raw.b >> 32) as u32;
TimelineEvent::PiBoost {
ts: raw.ts,
cpu: raw.cpu,
prober_tid: raw.prev_pid,
pid: raw.next_pid,
old_prio,
old_class_id,
new_prio,
new_class_id,
}
}
tl_evt::LOCK_CONTEND => TimelineEvent::LockContend {
ts: raw.ts,
cpu: raw.cpu,
tid: raw.prev_pid,
lock_kva: raw.a,
flags: raw.b as u32,
},
_ => TimelineEvent::Unknown {
ts: raw.ts,
cpu: raw.cpu,
type_: raw.type_,
prev_pid: raw.prev_pid,
next_pid: raw.next_pid,
a: raw.a,
b: raw.b,
},
}
}
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct TimelineCapture<'a> {
pub records: &'a [u8],
pub drops: u64,
}
pub const DEFAULT_SNAPSHOT_RING_DEPTH: usize = 60;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[allow(dead_code)]
pub struct IncrementalSnapshot {
pub captured_ns: u64,
pub monotonic_ns: u64,
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct SnapshotRing {
capacity: usize,
snapshots: std::collections::VecDeque<IncrementalSnapshot>,
}
impl SnapshotRing {
#[allow(dead_code)]
pub fn new(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
snapshots: std::collections::VecDeque::with_capacity(capacity.max(1)),
}
}
#[allow(dead_code)]
pub fn push(&mut self, snap: IncrementalSnapshot) {
if self.snapshots.len() == self.capacity {
self.snapshots.pop_front();
}
self.snapshots.push_back(snap);
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.snapshots.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.snapshots.is_empty()
}
#[allow(dead_code)]
pub fn capacity(&self) -> usize {
self.capacity
}
#[allow(dead_code)]
pub fn drain(&mut self) -> Vec<IncrementalSnapshot> {
self.snapshots.drain(..).collect()
}
#[allow(dead_code)]
pub fn snapshots(&self) -> impl Iterator<Item = &IncrementalSnapshot> {
self.snapshots.iter()
}
}
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct IncrementalCapture {
pub snapshots: Vec<IncrementalSnapshot>,
pub steady_hz: f64,
pub trigger_hz: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timeline_event_layout_pinned() {
use crate::assert::Verdict;
let total_size = std::mem::size_of::<TimelineEventRaw>();
let off_type = std::mem::offset_of!(TimelineEventRaw, type_);
let off_cpu = std::mem::offset_of!(TimelineEventRaw, cpu);
let off_ts = std::mem::offset_of!(TimelineEventRaw, ts);
let off_prev_pid = std::mem::offset_of!(TimelineEventRaw, prev_pid);
let off_next_pid = std::mem::offset_of!(TimelineEventRaw, next_pid);
let off_a = std::mem::offset_of!(TimelineEventRaw, a);
let off_b = std::mem::offset_of!(TimelineEventRaw, b);
let mut v = Verdict::new();
crate::claim!(v, total_size).eq(40usize);
crate::claim!(v, off_type).eq(0usize);
crate::claim!(v, off_cpu).eq(4usize);
crate::claim!(v, off_ts).eq(8usize);
crate::claim!(v, off_prev_pid).eq(16usize);
crate::claim!(v, off_next_pid).eq(20usize);
crate::claim!(v, off_a).eq(24usize);
crate::claim!(v, off_b).eq(32usize);
let r = v.into_result();
assert!(
r.passed,
"timeline_event layout drift detected: {:?}",
r.details,
);
}
fn raw(type_: u32, cpu: u32, ts: u64, p: u32, n: u32, a: u64, b: u64) -> Vec<u8> {
let r = TimelineEventRaw {
type_,
cpu,
ts,
prev_pid: p,
next_pid: n,
a,
b,
};
let bytes = unsafe {
std::slice::from_raw_parts(
&r as *const TimelineEventRaw as *const u8,
std::mem::size_of::<TimelineEventRaw>(),
)
};
bytes.to_vec()
}
#[test]
fn parse_switch_record() {
use crate::assert::Verdict;
let bytes = raw(tl_evt::SWITCH, 3, 1_000_000, 100, 200, 0x402, 1);
let ev = parse_timeline_record(&bytes).unwrap();
match ev {
TimelineEvent::Switch {
ts,
cpu,
prev_pid,
next_pid,
prev_state,
preempt,
} => {
let mut v = Verdict::new();
crate::claim!(v, ts).eq(1_000_000u64);
crate::claim!(v, cpu).eq(3u32);
crate::claim!(v, prev_pid).eq(100u32);
crate::claim!(v, next_pid).eq(200u32);
crate::claim!(v, prev_state).eq(0x402u64);
crate::claim!(v, preempt).eq(true);
let r = v.into_result();
assert!(r.passed, "Switch record decode drift: {:?}", r.details,);
}
other => panic!("expected Switch, got {other:?}"),
}
}
#[test]
fn parse_migrate_record() {
let bytes = raw(tl_evt::MIGRATE, 1, 2_000_000, 555, 0, 7, 2);
let ev = parse_timeline_record(&bytes).unwrap();
match ev {
TimelineEvent::Migrate {
pid,
orig_cpu,
dest_cpu,
..
} => {
assert_eq!(pid, 555);
assert_eq!(dest_cpu, 7);
assert_eq!(orig_cpu, 2);
}
other => panic!("expected Migrate, got {other:?}"),
}
}
#[test]
fn parse_wakeup_record() {
let bytes = raw(tl_evt::WAKEUP, 0, 3_000_000, 777, 0, 4, 0);
let ev = parse_timeline_record(&bytes).unwrap();
match ev {
TimelineEvent::Wakeup {
pid, target_cpu, ..
} => {
assert_eq!(pid, 777);
assert_eq!(target_cpu, 4);
}
other => panic!("expected Wakeup, got {other:?}"),
}
}
#[test]
fn parse_pi_boost_record() {
let old_a = 120u64 | (3u64 << 32); let new_b = 100u64 | (1u64 << 32); let bytes = raw(tl_evt::PI_BOOST, 2, 4_000_000, 10, 11, old_a, new_b);
let ev = parse_timeline_record(&bytes).unwrap();
match ev {
TimelineEvent::PiBoost {
prober_tid,
pid,
old_prio,
old_class_id,
new_prio,
new_class_id,
..
} => {
assert_eq!(prober_tid, 10);
assert_eq!(pid, 11);
assert_eq!(old_prio, 120);
assert_eq!(old_class_id, 3);
assert_eq!(new_prio, 100);
assert_eq!(new_class_id, 1);
}
other => panic!("expected PiBoost, got {other:?}"),
}
}
#[test]
fn parse_lock_contend_record() {
let lock_kva = 0xffff_ffff_8000_1000u64;
let flags = 0x4u64;
let bytes = raw(tl_evt::LOCK_CONTEND, 5, 5_000_000, 99, 0, lock_kva, flags);
let ev = parse_timeline_record(&bytes).unwrap();
match ev {
TimelineEvent::LockContend {
tid,
lock_kva: kva,
flags: f,
..
} => {
assert_eq!(tid, 99);
assert_eq!(kva, lock_kva);
assert_eq!(f, 0x4);
}
other => panic!("expected LockContend, got {other:?}"),
}
}
#[test]
fn parse_unknown_type_preserves_fields() {
let bytes = raw(99, 7, 6_000_000, 1, 2, 3, 4);
let ev = parse_timeline_record(&bytes).unwrap();
match ev {
TimelineEvent::Unknown {
type_,
prev_pid,
a,
b,
..
} => {
assert_eq!(type_, 99);
assert_eq!(prev_pid, 1);
assert_eq!(a, 3);
assert_eq!(b, 4);
}
other => panic!("expected Unknown, got {other:?}"),
}
}
#[test]
fn parse_truncated_record_returns_none() {
let bytes = vec![0u8; 39]; assert!(parse_timeline_record(&bytes).is_none());
}
#[test]
fn parse_timeline_buf_multi_record_with_partial_tail() {
let mut buf: Vec<u8> = Vec::new();
buf.extend(raw(tl_evt::SWITCH, 0, 1, 1, 2, 0, 0));
buf.extend(raw(tl_evt::WAKEUP, 1, 2, 3, 0, 4, 0));
buf.extend(vec![0u8; 20]);
let evs = parse_timeline_buf(&buf);
assert_eq!(evs.len(), 2);
assert!(matches!(evs[0], TimelineEvent::Switch { .. }));
assert!(matches!(evs[1], TimelineEvent::Wakeup { .. }));
}
#[test]
fn snapshot_ring_evicts_oldest() {
let mut ring = SnapshotRing::new(3);
for i in 0..5 {
ring.push(IncrementalSnapshot {
captured_ns: i,
monotonic_ns: i,
bytes: vec![i as u8],
});
}
assert_eq!(ring.len(), 3);
assert_eq!(ring.capacity(), 3);
let drained = ring.drain();
assert_eq!(drained.len(), 3);
assert_eq!(drained[0].captured_ns, 2);
assert_eq!(drained[2].captured_ns, 4);
}
#[test]
fn default_ring_depth_pinned() {
assert_eq!(DEFAULT_SNAPSHOT_RING_DEPTH, 60);
}
#[test]
fn snapshot_ring_starts_empty() {
let ring = SnapshotRing::new(8);
assert!(ring.is_empty());
assert_eq!(ring.len(), 0);
assert_eq!(ring.capacity(), 8);
}
#[test]
fn incremental_snapshot_serde_roundtrip() {
let snap = IncrementalSnapshot {
captured_ns: 1234567890,
monotonic_ns: 9876543210,
bytes: vec![1, 2, 3, 4],
};
let json = serde_json::to_string(&snap).unwrap();
let parsed: IncrementalSnapshot = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.captured_ns, 1234567890);
assert_eq!(parsed.bytes, vec![1, 2, 3, 4]);
}
#[test]
fn timeline_event_serde_roundtrip_all_variants() {
let cases = vec![
TimelineEvent::Switch {
ts: 1,
cpu: 0,
prev_pid: 10,
next_pid: 20,
prev_state: 1,
preempt: false,
},
TimelineEvent::Migrate {
ts: 2,
cpu: 1,
pid: 30,
orig_cpu: 1,
dest_cpu: 2,
},
TimelineEvent::Wakeup {
ts: 3,
cpu: 2,
pid: 40,
target_cpu: 5,
},
TimelineEvent::PiBoost {
ts: 4,
cpu: 3,
prober_tid: 1,
pid: 2,
old_prio: 120,
old_class_id: 3,
new_prio: 100,
new_class_id: 1,
},
TimelineEvent::LockContend {
ts: 5,
cpu: 4,
tid: 99,
lock_kva: 0xffff_ffff,
flags: 0x4,
},
];
for ev in cases {
let json = serde_json::to_string(&ev).expect("serialize");
let _: TimelineEvent = serde_json::from_str(&json).expect("deserialize");
}
}
}