Luigit
repositories / smith

smith

There are many coding harnesses - but this one is fast

owned by admin

smith-core/tests/alloc_bounds.rs

Raw
//! Allocation-count bounds on session hot paths (`SMH-SPEC-SPEC0001`,
//! memory accounting). Bounds only tighten.

use smith::config::Config;
use smith::message::{Message, Role};
use smith::structured::StructuredData;
use smith_alloc::{Counting, Scope, measure};
use smith_core::content_codec::{Blocks, EntryContentView};
use smith_core::frame::Frame;
use smith_core::session::{EntryContent, EntryFrame, Session};
use smith_core::store::{SessionReader, SessionWriter, open_session};
use std::sync::Mutex;

#[global_allocator]
static ALLOC: Counting = Counting;

/// Scope counters are process-wide; measurements must not overlap.
static MEASURING: Mutex<()> = Mutex::new(());

/// `fork_at` over 1000 entries: the branch list, the record `Vec`, and one
/// arena slice. Content bytes are copied, never decoded.
const FORK_AT_1000_ALLOCATIONS: u64 = 3;

/// Encoding a header frame: one buffer sized by the payload hint holds the
/// prefix and the whole payload.
const HEADER_ENCODE_ALLOCATIONS: u64 = 1;

/// `from_frames` over 1000 decoded frames: arena, stored records, the
/// parent-link table, the branch list, and one branch's records, each sized
/// up front. Independent of entry count.
const FROM_FRAMES_1000_ALLOCATIONS: u64 = 5;

fn linear_session(len: usize) -> Session {
    let mut session = Session::new();
    for turn in 0..len {
        let content = EntryContent::Message(Message::with_text(Role::User, format!("turn {turn}")));
        session.append(&content).unwrap();
    }
    session
}

/// Allocations and bytes charged to [`Scope::Session`] while `run` executes.
fn session_cost<T>(run: impl FnOnce() -> T) -> ((u64, u64), T) {
    let _serial = MEASURING
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    let (out, cost) = measure(Scope::Session, run);
    ((cost.allocations, cost.bytes), out)
}

/// Cost of one `fork_at` of the leaf.
fn fork_cost(session: &Session) -> (u64, u64) {
    let leaf = session.selected_id().expect("non-empty session");
    let (cost, fork) = session_cost(|| session.fork_at(leaf).expect("leaf is known"));
    assert_eq!(
        fork.active_branch().entries().len(),
        session.active_branch().entries().len()
    );
    let last = fork.active_branch().entries()[fork.active_branch().entries().len() - 1];
    assert_eq!(fork.bytes(&last), session.bytes(&last));
    cost
}

/// Cost of restoring `session` from its decoded frames; frame decoding
/// itself happens before the measured scope.
fn from_frames_cost(session: &Session) -> (u64, u64) {
    let mut frames = vec![Frame::Header {
        header: session.header(),
    }];
    frames.extend(
        session
            .active_branch()
            .entries()
            .iter()
            .map(|entry| Frame::Known {
                entry: EntryFrame {
                    id: entry.id,
                    parent: entry.parent,
                    timestamp_ms: entry.timestamp_ms,
                    kind: entry.kind,
                    content: session.bytes(entry).to_vec(),
                },
            }),
    );
    let (cost, restored) = session_cost(|| Session::from_frames(&frames).expect("frames restore"));
    assert_eq!(&restored, session);
    cost
}

#[test]
fn selected_context_allocation_is_independent_of_old_and_unrelated_payloads() {
    fn cost(old_bytes: usize, tag: &str) -> (u64, u64) {
        let path =
            std::env::temp_dir().join(format!("smith-effective-{tag}-{}.smh", std::process::id()));
        let _ = std::fs::remove_file(&path);
        let cfg = Config::default_valid();
        let mut opened = open_session(&path, cfg.clone()).unwrap();
        let old = EntryFrame::new(
            None,
            &EntryContent::Message(Message::with_text(Role::User, "x".repeat(old_bytes))),
        )
        .unwrap();
        let compact = EntryFrame::new(
            Some(old.id),
            &EntryContent::Meta {
                kind: smith_core::session::COMPACTION_KIND.into(),
                detail: StructuredData::from_serializable(&serde_json::json!({"summary": ""}))
                    .unwrap(),
            },
        )
        .unwrap();
        let after = EntryFrame::new(
            Some(compact.id),
            &EntryContent::Message(Message::with_text(Role::User, "after")),
        )
        .unwrap();
        let unrelated = EntryFrame::new(
            Some(old.id),
            &EntryContent::Message(Message::with_text(Role::User, "y".repeat(old_bytes))),
        )
        .unwrap();
        for entry in [&old, &compact, &after, &unrelated] {
            opened
                .writer
                .append_frame(&Frame::Known {
                    entry: entry.clone(),
                })
                .unwrap();
        }
        opened.writer.select_entry(after.id).unwrap();
        drop(opened);
        let reader = SessionReader::open(&path).unwrap();
        let (selected, cost) = measure(Scope::Session, || {
            reader.selected_session(cfg.frame_bytes_max).unwrap()
        });
        assert_eq!(selected.active_branch().entries().len(), 2);
        drop(reader);
        let _ = std::fs::remove_file(path);
        (cost.allocations, cost.bytes)
    }
    let _lock = MEASURING.lock().unwrap();
    let small = cost(100, "small");
    let large = cost(400_000, "large");
    assert_eq!(
        small, large,
        "pre-compaction and sibling payloads must not be retained or decoded"
    );
}

#[test]
fn fork_at_over_1000_entries_is_bounded_and_reproducible() {
    let session = linear_session(1000);
    let first = fork_cost(&session);
    let second = fork_cost(&session);
    assert_eq!(
        first.0, FORK_AT_1000_ALLOCATIONS,
        "fork_at allocated {} times, bound is {FORK_AT_1000_ALLOCATIONS}",
        first.0
    );
    assert_eq!(first, second);
}

#[test]
fn from_frames_allocates_independently_of_entry_count() {
    let small = from_frames_cost(&linear_session(100));
    let large = from_frames_cost(&linear_session(1000));
    assert_eq!(
        large.0, FROM_FRAMES_1000_ALLOCATIONS,
        "from_frames allocated {} times, bound is {FROM_FRAMES_1000_ALLOCATIONS}",
        large.0
    );
    assert_eq!(small.0, large.0);
    assert_eq!(large, from_frames_cost(&linear_session(1000)));
}

#[cfg(target_arch = "x86_64")]
#[test]
fn decoded_content_view_fits_one_cache_line_and_blocks_three_words() {
    assert_eq!(std::mem::size_of::<EntryContentView<'_>>(), 56);
    assert_eq!(std::mem::size_of::<Blocks<'_>>(), 24);
}

#[test]
fn text_only_content_decode_and_scan_allocate_nothing() {
    let content = EntryFrame::new(
        None,
        &EntryContent::Message(Message::with_text(Role::User, "needle")),
    )
    .unwrap()
    .content;
    let (cost, found) = session_cost(|| {
        let view = EntryContentView::decode(&content).unwrap();
        view.contains("needle")
    });
    assert!(found);
    assert_eq!(cost.0, 0, "text inspection allocated {} times", cost.0);
}

#[test]
fn nested_json_inspection_borrows_without_allocating() {
    let content = EntryFrame::new(
        None,
        &EntryContent::Meta {
            kind: "event".into(),
            detail: StructuredData::from_serializable(
                &serde_json::json!({"outer": [{"value": "needle"}, 0, false, null]}),
            )
            .unwrap(),
        },
    )
    .unwrap()
    .content;
    let (cost, found) = session_cost(|| {
        let view = EntryContentView::decode(&content).unwrap();
        view.contains("needle")
    });
    assert!(found);
    assert_eq!(cost.0, 0, "JSON inspection allocated {} times", cost.0);
}

#[test]
fn unrelated_history_does_not_become_resident_selected_context() {
    // SMH-SPEC-SPEC0001: Sessions, context memory ≠ complete file size.
    let path = std::env::temp_dir().join(format!("smith-paged-memory-{}.smh", std::process::id()));
    let _ = std::fs::remove_file(&path);
    let cfg = Config::default_valid();
    let mut writer = SessionWriter::open(&path, cfg.clone()).unwrap();
    writer
        .append_frame(&Frame::Header {
            header: Session::new().header(),
        })
        .unwrap();
    let root = EntryFrame::new(
        None,
        &EntryContent::Message(Message::with_text(Role::User, "root")),
    )
    .unwrap();
    writer
        .append_frame(&Frame::Known {
            entry: root.clone(),
        })
        .unwrap();
    for _ in 0..128 {
        let sibling = EntryFrame::new(
            Some(root.id),
            &EntryContent::Message(Message::with_text(Role::User, "x".repeat(32 * 1024))),
        )
        .unwrap();
        writer
            .append_frame(&Frame::Known { entry: sibling })
            .unwrap();
    }
    writer.select_entry(root.id).unwrap();
    let reader = SessionReader::open(&path).unwrap();
    let _serial = MEASURING
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    let (selected, cost) = measure(Scope::IoRequest, || {
        reader.selected_session(cfg.frame_bytes_max).unwrap()
    });
    assert_eq!(selected.selected_id(), Some(root.id));
    assert_eq!(selected.active_branch().entries().len(), 1);
    assert!(
        cost.peak < 256 * 1024,
        "selected context peak {} bytes for {}-byte file",
        cost.peak,
        reader.file_size()
    );
    assert!(reader.file_size() > 4 * 1024 * 1024);
    drop(reader);
    drop(writer);
    let _ = std::fs::remove_file(path);
}

#[test]
fn header_frame_encodes_into_one_allocation() {
    let header = Frame::Header {
        header: Session::new().header(),
    };
    let (cost, encoded) =
        session_cost(|| header.encode(smith_core::frame::DEFAULT_MAX_FRAME_BYTES));
    assert!(encoded.is_ok());
    assert_eq!(
        cost.0, HEADER_ENCODE_ALLOCATIONS,
        "header encode allocated {} times, bound is {HEADER_ENCODE_ALLOCATIONS}",
        cost.0
    );
}