//! 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(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::>(), 56); assert_eq!(std::mem::size_of::>(), 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 ); }