katra-trace 0.1.0

Katra3D trace format: versioned, deterministic, correlated event capture.
Documentation
//! Trace writer: appends length-prefixed bincode records to a file.

use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::Path;

use katra_core::{KatraError, Result};
use serde::Serialize;

use crate::header::{FORMAT_VERSION, SCHEMA_HASH, TRACE_MAGIC, TraceHeader};
use crate::record::{TraceRecord, TraceSummary};

/// Appends records to a trace file. Not `Sync`; a profiler must serialize
/// writes (the profiler's sink mutex does this).
pub struct TraceWriter {
    inner: BufWriter<File>,
    count: u64,
}

impl TraceWriter {
    /// Create (truncate) a trace file and write the header.
    pub fn create(path: &Path, header: &TraceHeader) -> Result<Self> {
        let file = OpenOptions::new()
            .create(true)
            .truncate(true)
            .write(true)
            .open(path)
            .map_err(KatraError::from)?;
        let mut w = TraceWriter { inner: BufWriter::new(file), count: 0 };
        w.write_header(header)?;
        Ok(w)
    }

    fn write_raw<T: Serialize>(&mut self, value: &T) -> Result<()> {
        let bytes =
            bincode::serialize(value).map_err(|e| KatraError::Protocol(format!("bincode: {e}")))?;
        let len = bytes.len() as u32;
        self.inner
            .write_all(&len.to_le_bytes())
            .and_then(|_| self.inner.write_all(&bytes))
            .map_err(KatraError::from)?;
        Ok(())
    }

    fn write_header(&mut self, header: &TraceHeader) -> Result<()> {
        debug_assert_eq!(header.format_version, FORMAT_VERSION);
        debug_assert_eq!(header.schema_hash, SCHEMA_HASH);
        let bytes = bincode::serialize(header)
            .map_err(|e| KatraError::Protocol(format!("bincode header: {e}")))?;
        self.inner
            .write_all(&TRACE_MAGIC)
            .and_then(|_| self.inner.write_all(&FORMAT_VERSION.to_le_bytes()))
            .and_then(|_| self.inner.write_all(&(bytes.len() as u32).to_le_bytes()))
            .and_then(|_| self.inner.write_all(&bytes))
            .map_err(KatraError::from)?;
        Ok(())
    }

    /// Append one record.
    pub fn write(&mut self, record: &TraceRecord) -> Result<()> {
        self.write_raw(record)?;
        self.count += 1;
        Ok(())
    }

    /// Append one event.
    pub fn write_event(&mut self, event: &katra_core::TraceEvent) -> Result<()> {
        self.write(&TraceRecord::Event(event.clone()))
    }

    /// Number of records written.
    pub fn count(&self) -> u64 {
        self.count
    }

    /// Flush buffered bytes to the file without finishing the session.
    pub fn flush_buffered(&mut self) -> Result<()> {
        self.inner.flush().map_err(KatraError::from)
    }

    /// Write the session summary and flush. The file is left open for
    /// inspection; call `close` to release it.
    pub fn finish(&mut self, summary: &TraceSummary) -> Result<()> {
        self.write(&TraceRecord::SessionSummary(summary.clone()))?;
        self.inner.flush().map_err(KatraError::from)
    }

    /// Close the file (flush and drop).
    pub fn close(self) -> Result<()> {
        let mut w = self;
        w.inner.flush().map_err(KatraError::from)?;
        drop(w.inner);
        Ok(())
    }
}

/// Helper to assemble a header with sensible defaults for a given workload.
pub fn default_header(
    workload: &str,
    capture: &katra_core::CaptureOptions,
    notes: Vec<String>,
) -> TraceHeader {
    let now_wall = katra_core::wall_now_ns();
    let now_mono = katra_core::monotonic_now_ns();
    TraceHeader {
        format_version: FORMAT_VERSION,
        schema_hash: SCHEMA_HASH,
        tool_version: format!("katra3d {}", katra_core::KATRA_VERSION),
        capture_options: capture.clone(),
        start_wall_ns: now_wall,
        start_mono_ns: now_mono,
        process_id: std::process::id() as u64,
        workload: workload.to_string(),
        host: crate::reader::host_info(),
        notes,
    }
}