use std::collections::HashMap;
use std::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpanId(u64);
impl SpanId {
fn new(id: u64) -> Self {
Self(id)
}
pub fn raw(&self) -> u64 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpanKind {
System,
Turn,
Document,
Tool,
Other,
}
impl SpanKind {
fn default_priority(&self) -> u32 {
match self {
SpanKind::System => u32::MAX, SpanKind::Turn => 100,
SpanKind::Tool => 80,
SpanKind::Document => 50,
SpanKind::Other => 30,
}
}
}
#[derive(Debug, Clone)]
pub struct SpanInfo {
pub id: SpanId,
pub label: String,
pub kind: SpanKind,
pub range: Range<usize>,
pub priority: u32,
pub pinned: bool,
pub insertion_order: u64,
}
impl SpanInfo {
pub fn len(&self) -> usize {
self.range.len()
}
pub fn is_empty(&self) -> bool {
self.range.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct EvictionPlan {
span_ids: Vec<SpanId>,
ranges: Vec<Range<usize>>,
total_freed: usize,
}
impl EvictionPlan {
pub fn evicted_span_ids(&self) -> &[SpanId] {
&self.span_ids
}
pub fn evicted_ranges(&self) -> &[Range<usize>] {
&self.ranges
}
pub fn total_freed(&self) -> usize {
self.total_freed
}
pub fn is_empty(&self) -> bool {
self.span_ids.is_empty()
}
pub fn num_spans(&self) -> usize {
self.span_ids.len()
}
}
#[derive(Debug)]
pub struct SpanRegistry {
capacity: usize,
spans: HashMap<SpanId, SpanInfo>,
next_id: u64,
insertion_counter: u64,
}
impl SpanRegistry {
pub fn new(capacity: usize) -> Self {
Self {
capacity,
spans: HashMap::new(),
next_id: 0,
insertion_counter: 0,
}
}
pub fn register(&mut self, label: &str, kind: SpanKind, range: Range<usize>) -> SpanId {
let id = SpanId::new(self.next_id);
self.next_id += 1;
let pinned = kind == SpanKind::System;
let priority = kind.default_priority();
let insertion_order = self.insertion_counter;
self.insertion_counter += 1;
self.spans.insert(
id,
SpanInfo {
id,
label: label.to_string(),
kind,
range,
priority,
pinned,
insertion_order,
},
);
id
}
pub fn remove(&mut self, id: SpanId) -> Option<SpanInfo> {
self.spans.remove(&id)
}
pub fn get(&self, id: SpanId) -> Option<&SpanInfo> {
self.spans.get(&id)
}
pub fn num_spans(&self) -> usize {
self.spans.len()
}
pub fn total_positions(&self) -> usize {
self.spans.values().map(|s| s.len()).sum()
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn free_positions(&self) -> usize {
self.capacity.saturating_sub(self.total_positions())
}
pub fn pin(&mut self, id: SpanId) -> bool {
if let Some(span) = self.spans.get_mut(&id) {
span.pinned = true;
true
} else {
false
}
}
pub fn unpin(&mut self, id: SpanId) -> bool {
if let Some(span) = self.spans.get_mut(&id) {
span.pinned = false;
true
} else {
false
}
}
pub fn set_priority(&mut self, id: SpanId, priority: u32) -> bool {
if let Some(span) = self.spans.get_mut(&id) {
span.priority = priority;
true
} else {
false
}
}
pub fn plan_eviction(&self, needed: usize) -> EvictionPlan {
if needed == 0 {
return EvictionPlan {
span_ids: Vec::new(),
ranges: Vec::new(),
total_freed: 0,
};
}
let mut candidates: Vec<&SpanInfo> = self.spans.values().filter(|s| !s.pinned).collect();
candidates.sort_by(|a, b| {
a.priority
.cmp(&b.priority)
.then(a.insertion_order.cmp(&b.insertion_order))
});
let mut plan_ids = Vec::new();
let mut plan_ranges = Vec::new();
let mut freed = 0usize;
for span in candidates {
if freed >= needed {
break;
}
plan_ids.push(span.id);
plan_ranges.push(span.range.clone());
freed += span.len();
}
EvictionPlan {
span_ids: plan_ids,
ranges: plan_ranges,
total_freed: freed,
}
}
pub fn iter(&self) -> impl Iterator<Item = &SpanInfo> {
self.spans.values()
}
pub fn span_ids_by_position(&self) -> Vec<SpanId> {
let mut entries: Vec<_> = self.spans.values().collect();
entries.sort_by_key(|s| s.range.start);
entries.iter().map(|s| s.id).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_and_lookup() {
let mut reg = SpanRegistry::new(1024);
let id = reg.register("sys", SpanKind::System, 0..50);
let info = reg.get(id).unwrap();
assert_eq!(info.label, "sys");
assert_eq!(info.kind, SpanKind::System);
assert_eq!(info.range, 0..50);
assert!(info.pinned);
assert_eq!(info.len(), 50);
}
#[test]
fn basic_eviction_plan() {
let mut reg = SpanRegistry::new(2048);
let _sys = reg.register("system", SpanKind::System, 0..50);
let _t1 = reg.register("turn-1", SpanKind::Turn, 50..200);
let _d1 = reg.register("doc-a", SpanKind::Document, 200..600);
let _t2 = reg.register("turn-2", SpanKind::Turn, 600..900);
let plan = reg.plan_eviction(500);
assert!(plan.total_freed() >= 500);
assert_eq!(plan.evicted_span_ids()[0], _d1);
assert!(plan.num_spans() >= 2);
}
#[test]
fn system_spans_are_pinned() {
let mut reg = SpanRegistry::new(1024);
let sys = reg.register("system", SpanKind::System, 0..100);
let _t1 = reg.register("turn-1", SpanKind::Turn, 100..200);
let plan = reg.plan_eviction(200);
assert!(!plan.evicted_span_ids().contains(&sys));
assert_eq!(plan.total_freed(), 100);
}
#[test]
fn eviction_respects_priority_order() {
let mut reg = SpanRegistry::new(2048);
let _sys = reg.register("system", SpanKind::System, 0..50);
let other = reg.register("misc", SpanKind::Other, 50..200); let doc = reg.register("doc", SpanKind::Document, 200..500); let tool = reg.register("tool", SpanKind::Tool, 500..700); let turn = reg.register("turn", SpanKind::Turn, 700..1000);
let plan = reg.plan_eviction(2000);
let ids = plan.evicted_span_ids();
assert_eq!(ids[0], other);
assert_eq!(ids[1], doc);
assert_eq!(ids[2], tool);
assert_eq!(ids[3], turn);
}
#[test]
fn fifo_tiebreaking_within_same_priority() {
let mut reg = SpanRegistry::new(2048);
let t1 = reg.register("turn-1", SpanKind::Turn, 100..300);
let t2 = reg.register("turn-2", SpanKind::Turn, 300..500);
let _t3 = reg.register("turn-3", SpanKind::Turn, 500..700);
let plan = reg.plan_eviction(250);
assert_eq!(plan.evicted_span_ids()[0], t1);
let plan = reg.plan_eviction(500);
assert_eq!(plan.evicted_span_ids()[0], t1);
assert_eq!(plan.evicted_span_ids()[1], t2);
}
#[test]
fn zero_needed_returns_empty() {
let mut reg = SpanRegistry::new(1024);
let _ = reg.register("turn-1", SpanKind::Turn, 0..100);
let plan = reg.plan_eviction(0);
assert!(plan.is_empty());
assert_eq!(plan.total_freed(), 0);
}
#[test]
fn remove_span() {
let mut reg = SpanRegistry::new(1024);
let id = reg.register("turn-1", SpanKind::Turn, 0..100);
assert_eq!(reg.num_spans(), 1);
assert_eq!(reg.total_positions(), 100);
let removed = reg.remove(id).unwrap();
assert_eq!(removed.label, "turn-1");
assert_eq!(reg.num_spans(), 0);
assert_eq!(reg.total_positions(), 0);
assert!(reg.get(id).is_none());
}
#[test]
fn pin_and_unpin() {
let mut reg = SpanRegistry::new(1024);
let id = reg.register("turn-1", SpanKind::Turn, 0..100);
assert!(!reg.get(id).unwrap().pinned);
reg.pin(id);
assert!(reg.get(id).unwrap().pinned);
let plan = reg.plan_eviction(100);
assert!(plan.is_empty());
reg.unpin(id);
let plan = reg.plan_eviction(100);
assert_eq!(plan.num_spans(), 1);
}
#[test]
fn custom_priority() {
let mut reg = SpanRegistry::new(2048);
let t1 = reg.register("turn-1", SpanKind::Turn, 0..200); let t2 = reg.register("turn-2", SpanKind::Turn, 200..400);
reg.set_priority(t1, 200);
let plan = reg.plan_eviction(250);
assert_eq!(plan.evicted_span_ids()[0], t2);
}
#[test]
fn free_positions_tracking() {
let mut reg = SpanRegistry::new(1024);
assert_eq!(reg.free_positions(), 1024);
let id = reg.register("turn-1", SpanKind::Turn, 0..300);
assert_eq!(reg.free_positions(), 724);
reg.remove(id);
assert_eq!(reg.free_positions(), 1024);
}
#[test]
fn span_ids_by_position() {
let mut reg = SpanRegistry::new(2048);
let b = reg.register("b", SpanKind::Turn, 200..400);
let a = reg.register("a", SpanKind::Turn, 0..200);
let c = reg.register("c", SpanKind::Turn, 400..600);
let sorted = reg.span_ids_by_position();
assert_eq!(sorted, vec![a, b, c]);
}
#[test]
fn eviction_plan_covers_ranges() {
let mut reg = SpanRegistry::new(2048);
let _ = reg.register("sys", SpanKind::System, 0..50);
let _ = reg.register("doc", SpanKind::Document, 50..300);
let _ = reg.register("turn", SpanKind::Turn, 300..600);
let plan = reg.plan_eviction(400);
assert_eq!(plan.evicted_ranges()[0], 50..300);
}
#[test]
fn insufficient_evictable_space() {
let mut reg = SpanRegistry::new(1024);
let _ = reg.register("sys", SpanKind::System, 0..800); let _ = reg.register("turn", SpanKind::Turn, 800..900);
let plan = reg.plan_eviction(500);
assert_eq!(plan.total_freed(), 100);
assert_eq!(plan.num_spans(), 1);
}
}