use super::linux_mm::{MemoryTraceParser, MemoryTraceRecord, MemoryTracepoint};
use super::{O11yEvent, O11yEventKind, O11yPipeline, O11yService, O11yServiceState, O11yServiceStatus};
use std::collections::{BTreeMap, BTreeSet};
use std::fs::{self, File, OpenOptions};
use std::io::{self, Read};
use std::os::unix::fs::OpenOptionsExt;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::io::unix::AsyncFd;
use tokio::sync::mpsc::Sender;
#[derive(Clone, Debug)]
pub struct LinuxMemoryTraceOptions {
pub tracefs_root: PathBuf,
pub events: BTreeSet<MemoryTracepoint>,
pub buffer_size_kib: u32,
}
impl Default for LinuxMemoryTraceOptions {
fn default() -> Self {
Self {
tracefs_root: "/sys/kernel/tracing".into(),
events: BTreeSet::from([
MemoryTracepoint::OomVictim,
MemoryTracepoint::DirectReclaim,
MemoryTracepoint::Compaction,
MemoryTracepoint::Migration,
]),
buffer_size_kib: 64,
}
}
}
impl LinuxMemoryTraceOptions {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn validate(&self) -> io::Result<()> {
if self.events.is_empty() || !(1..=4096).contains(&self.buffer_size_kib) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"memory tracing requires events and a 1..=4096 KiB per-CPU buffer",
));
}
Ok(())
}
}
impl<T: Send + Sync + 'static> O11yPipeline<T> {
pub fn with_linux_memory_trace(mut self, options: LinuxMemoryTraceOptions) -> io::Result<Self> {
options.validate()?;
let name = "linux-memory-trace";
if self.services.iter().any(|service| service.name == name) {
return Err(io::Error::new(io::ErrorKind::AlreadyExists, "Linux memory trace service already attached"));
}
self.service_statuses.lock().map_err(|_| io::Error::other("service status poisoned"))?.insert(
name.into(),
O11yServiceStatus {
name: name.into(),
state: O11yServiceState::Pending,
},
);
let sender = self.sender();
self.services.push(O11yService {
name: name.into(),
future: Box::pin(async move { run_trace(options, sender).await.map_err(|e| e.to_string()) }),
});
Ok(self)
}
}
struct TraceInstance {
path: PathBuf,
}
impl TraceInstance {
fn create(options: &LinuxMemoryTraceOptions) -> io::Result<Self> {
static NONCE: AtomicU64 = AtomicU64::new(0);
let path = options
.tracefs_root
.join("instances")
.join(format!("wora-memory-{}-{}", std::process::id(), NONCE.fetch_add(1, Ordering::Relaxed)));
fs::create_dir(&path)?;
Ok(Self { path })
}
}
impl Drop for TraceInstance {
fn drop(&mut self) {
let _ = write_control(self.path.join("tracing_on"), "0");
let _ = write_control(self.path.join("events/enable"), "0");
if let Err(error) = fs::remove_dir(&self.path) {
tracing::warn!(path = %self.path.display(), %error, "could not remove owned memory trace instance");
}
}
}
async fn run_trace<T: Send + Sync + 'static>(options: LinuxMemoryTraceOptions, sender: Sender<O11yEvent<T>>) -> io::Result<()> {
options.validate()?;
let instance = TraceInstance::create(&options)?;
write_control(instance.path.join("tracing_on"), "0")?;
write_control(instance.path.join("events/enable"), "0")?;
write_control(instance.path.join("trace_clock"), "mono")?;
write_control(instance.path.join("buffer_size_kb"), &options.buffer_size_kib.to_string())?;
let mut enabled = Vec::new();
let mut unavailable = BTreeMap::new();
for event in options.events.iter().flat_map(|family| family.events()) {
let path = instance.path.join("events").join(event).join("enable");
match write_control(&path, "1") {
Ok(()) => enabled.push((*event).into()),
Err(error) if error.kind() == io::ErrorKind::NotFound => {
unavailable.insert((*event).into(), error.to_string());
}
Err(error) => return Err(error),
}
}
if enabled.is_empty() {
return Err(io::Error::other("no selected memory tracepoints available"));
}
let file = OpenOptions::new()
.read(true)
.custom_flags(libc::O_NONBLOCK)
.open(instance.path.join("trace_pipe"))?;
let reader = register_owned_file(file)?;
emit(
&sender,
MemoryTraceRecord::Started {
instance: instance.path.display().to_string(),
enabled,
unavailable,
},
)
.await?;
write_control(instance.path.join("tracing_on"), "1")?;
stream_trace(reader, sender).await
}
async fn emit<T>(sender: &Sender<O11yEvent<T>>, record: MemoryTraceRecord) -> io::Result<()> {
sender
.send(O11yEvent {
timestamp: chrono::Utc::now(),
kind: O11yEventKind::MemoryTrace(Box::new(record)),
})
.await
.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "memory trace observability channel closed"))
}
async fn stream_trace<T>(reader: AsyncFd<File>, sender: Sender<O11yEvent<T>>) -> io::Result<()> {
let mut parser = MemoryTraceParser::new();
let mut pending = Vec::new();
let mut bytes = [0u8; 16384];
loop {
let mut ready = reader.readable().await?;
let length = match ready.try_io(|reader| {
let mut file = reader.get_ref();
file.read(&mut bytes)
}) {
Ok(result) => result?,
Err(_) => continue,
};
if length == 0 {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "memory trace stream ended"));
}
pending.extend_from_slice(&bytes[..length]);
while let Some(end) = pending.iter().position(|b| *b == b'\n') {
let line: Vec<_> = pending.drain(..=end).collect();
let line = std::str::from_utf8(&line).map_err(io::Error::other)?;
if let Some(record) = parser.parse_line(line).map_err(|e| io::Error::other(format!("{e:?}")))? {
emit(&sender, record).await?;
}
}
if pending.len() > 65536 {
return Err(io::Error::other("memory trace line exceeds 64 KiB"));
}
}
}
fn write_control(path: impl AsRef<std::path::Path>, value: &str) -> io::Result<()> {
use std::io::Write;
let path = path.as_ref();
OpenOptions::new()
.write(true)
.open(path)
.and_then(|mut file| file.write_all(value.as_bytes()))
.map_err(|error| io::Error::new(error.kind(), format!("{}: {error}", path.display())))
}
#[allow(deprecated, reason = "Owned File guarantees a stable descriptor for the entire registration lifetime")]
fn register_owned_file(file: File) -> io::Result<AsyncFd<File>> {
AsyncFd::with_interest(file, tokio::io::Interest::READABLE)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::os::fd::OwnedFd;
use std::os::unix::net::UnixStream;
#[tokio::test]
async fn nonblocking_stream_forwards_records_and_cancels_without_events() -> Result<(), Box<dyn std::error::Error>> {
let (socket, mut writer) = UnixStream::pair()?;
socket.set_nonblocking(true)?;
let file = File::from(OwnedFd::from(socket));
let reader = register_owned_file(file)?;
let (sender, mut receiver) = tokio::sync::mpsc::channel::<O11yEvent<()>>(2);
let task = tokio::spawn(stream_trace(reader, sender));
writer.write_all(b" trigger-9 [000] .... 13.000001: mark_victim: pid=77\nCPU:0 [LOST 5 EVENTS]\n")?;
let event = tokio::time::timeout(std::time::Duration::from_secs(2), receiver.recv())
.await?
.ok_or("no event")?;
assert!(matches!(event.kind, O11yEventKind::MemoryTrace(record) if matches!(*record, MemoryTraceRecord::Event { .. })));
let event = receiver.recv().await.ok_or("no loss notification")?;
assert!(matches!(event.kind, O11yEventKind::MemoryTrace(record) if matches!(*record, MemoryTraceRecord::Lost { .. })));
task.abort();
assert!(tokio::time::timeout(std::time::Duration::from_secs(2), task).await?.is_err());
Ok(())
}
#[tokio::test]
async fn setup_failure_removes_only_owned_instance_and_is_reported() -> Result<(), Box<dyn std::error::Error>> {
let root = tempfile::tempdir()?;
fs::create_dir(root.path().join("instances"))?;
fs::write(root.path().join("tracing_on"), "unchanged")?;
let options = LinuxMemoryTraceOptions {
tracefs_root: root.path().into(),
..Default::default()
};
let (sender, _) = tokio::sync::mpsc::channel::<O11yEvent<()>>(1);
assert!(run_trace(options.clone(), sender).await.is_err());
assert_eq!(fs::read_dir(root.path().join("instances"))?.count(), 0);
assert_eq!(fs::read_to_string(root.path().join("tracing_on"))?, "unchanged");
let pipeline = O11yPipeline::<()>::builder()
.sink("capture", super::super::O11yMemorySink::new(Default::default()))
.build()?
.with_linux_memory_trace(options.clone())?;
assert!(pipeline.with_linux_memory_trace(options).is_err());
Ok(())
}
}