2026-08-28 10:31:01 -07:00
|
|
|
//! M3.8 Ingest-Time Context Optimization
|
|
|
|
|
//!
|
|
|
|
|
//! Provides helpers for optimizing records at ingest time.
|
|
|
|
|
//! Designed to be called from rebuild.rs during the embed + index pipeline.
|
|
|
|
|
|
2026-08-28 10:30:02 -07:00
|
|
|
use mem_core::{Record, ContextOptimizer};
|
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
|
|
|
|
|
|
/// Tracks optimization effectiveness across records.
|
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
|
pub struct OptimizationMetrics {
|
|
|
|
|
/// Total records processed
|
|
|
|
|
pub total_records: usize,
|
|
|
|
|
/// Sum of input bytes (before optimization)
|
|
|
|
|
pub input_bytes_total: usize,
|
|
|
|
|
/// Sum of output bytes (after optimization)
|
|
|
|
|
pub output_bytes_total: usize,
|
|
|
|
|
/// Per-compressor breakdown
|
|
|
|
|
pub per_compressor: std::collections::HashMap<String, CompressorStats>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct CompressorStats {
|
|
|
|
|
pub count: usize,
|
|
|
|
|
pub input_bytes: usize,
|
|
|
|
|
pub output_bytes: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl OptimizationMetrics {
|
|
|
|
|
/// Calculate overall compression ratio as percentage
|
|
|
|
|
pub fn compression_ratio(&self) -> f32 {
|
|
|
|
|
if self.input_bytes_total == 0 {
|
|
|
|
|
0.0
|
|
|
|
|
} else {
|
|
|
|
|
(self.output_bytes_total as f32 / self.input_bytes_total as f32) * 100.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get stats for a specific compressor
|
|
|
|
|
pub fn get_compressor_stats(&self, name: &str) -> Option<&CompressorStats> {
|
|
|
|
|
self.per_compressor.get(name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Log metrics summary
|
|
|
|
|
pub fn log_summary(&self, project: &str) {
|
|
|
|
|
tracing::info!(
|
|
|
|
|
project = project,
|
|
|
|
|
total_records = self.total_records,
|
|
|
|
|
input_bytes = self.input_bytes_total,
|
|
|
|
|
output_bytes = self.output_bytes_total,
|
|
|
|
|
compression_ratio = format!("{:.1}%", self.compression_ratio()),
|
2026-08-28 10:31:01 -07:00
|
|
|
"M3.8 ingest optimization complete"
|
2026-08-28 10:30:02 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for (name, stats) in &self.per_compressor {
|
|
|
|
|
let ratio = if stats.input_bytes == 0 {
|
|
|
|
|
0.0
|
|
|
|
|
} else {
|
|
|
|
|
(stats.output_bytes as f32 / stats.input_bytes as f32) * 100.0
|
|
|
|
|
};
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
compressor = name,
|
|
|
|
|
count = stats.count,
|
|
|
|
|
input_bytes = stats.input_bytes,
|
|
|
|
|
output_bytes = stats.output_bytes,
|
|
|
|
|
ratio = format!("{:.1}%", ratio),
|
|
|
|
|
"compressor stats"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
/// Helper to optimize a single record and track metrics.
|
|
|
|
|
///
|
|
|
|
|
/// Call this in the rebuild loop when processing each record:
|
|
|
|
|
/// ```ignore
|
|
|
|
|
/// for record in source.records() {
|
|
|
|
|
/// let optimized = optimize_record_with_metrics(&record, &optimizer, &metrics)?;
|
|
|
|
|
/// embed_and_index(&optimized)?;
|
|
|
|
|
/// }
|
|
|
|
|
/// ```
|
|
|
|
|
pub fn optimize_record_with_metrics(
|
|
|
|
|
record: Record,
|
|
|
|
|
optimizer: &ContextOptimizer,
|
|
|
|
|
metrics: &Arc<Mutex<OptimizationMetrics>>,
|
|
|
|
|
) -> Result<Record, String> {
|
|
|
|
|
let input_bytes = record.text.len();
|
2026-08-28 10:30:02 -07:00
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
let optimized = match optimizer.optimize(&record.text) {
|
|
|
|
|
Ok(opt_chunk) => opt_chunk,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!(error = ?e, "optimization failed, using original");
|
|
|
|
|
return Ok(record);
|
2026-08-28 10:30:02 -07:00
|
|
|
}
|
2026-08-28 10:31:01 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let output_bytes = optimized.compressed.len();
|
|
|
|
|
let compressor_name = format!("{:?}", optimized.content_type);
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let mut m = metrics.lock().unwrap();
|
|
|
|
|
m.total_records += 1;
|
|
|
|
|
m.input_bytes_total += input_bytes;
|
|
|
|
|
m.output_bytes_total += output_bytes;
|
|
|
|
|
|
|
|
|
|
m.per_compressor
|
|
|
|
|
.entry(compressor_name.clone())
|
|
|
|
|
.or_insert_with(|| CompressorStats {
|
|
|
|
|
count: 0,
|
|
|
|
|
input_bytes: 0,
|
|
|
|
|
output_bytes: 0,
|
|
|
|
|
})
|
|
|
|
|
.count += 1;
|
|
|
|
|
|
|
|
|
|
let stats = m.per_compressor.get_mut(&compressor_name).unwrap();
|
|
|
|
|
stats.input_bytes += input_bytes;
|
|
|
|
|
stats.output_bytes += output_bytes;
|
2026-08-28 10:30:02 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
Ok(Record {
|
|
|
|
|
text: optimized.compressed,
|
|
|
|
|
..record
|
|
|
|
|
})
|
2026-08-28 10:30:02 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use mem_core::{Provenance, Role};
|
|
|
|
|
use time::macros::datetime;
|
|
|
|
|
|
|
|
|
|
fn make_test_record(text: &str) -> Record {
|
|
|
|
|
Record {
|
|
|
|
|
role: Role::User,
|
|
|
|
|
text: text.to_string(),
|
|
|
|
|
timestamp: datetime!(2024-08-20 12:00:00 UTC),
|
|
|
|
|
provenance: Provenance {
|
|
|
|
|
source_id: "test".to_string(),
|
|
|
|
|
offset: 0,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
#[test]
|
|
|
|
|
fn test_optimize_record_preserves_structure() {
|
|
|
|
|
let record = make_test_record("ERROR: simple");
|
2026-08-28 10:30:02 -07:00
|
|
|
let optimizer = ContextOptimizer::new().unwrap();
|
2026-08-28 10:31:01 -07:00
|
|
|
let metrics = Arc::new(Mutex::new(OptimizationMetrics::default()));
|
2026-08-28 10:30:02 -07:00
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
let result = optimize_record_with_metrics(record.clone(), &optimizer, &metrics);
|
|
|
|
|
assert!(result.is_ok());
|
2026-08-28 10:30:02 -07:00
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
let optimized = result.unwrap();
|
|
|
|
|
assert_eq!(optimized.role, Role::User);
|
|
|
|
|
assert_eq!(optimized.provenance.source_id, "test");
|
2026-08-28 10:30:02 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
#[test]
|
|
|
|
|
fn test_optimize_record_tracks_bytes() {
|
|
|
|
|
let record = make_test_record("ERROR: failed\nINFO: debug\nERROR: permission");
|
2026-08-28 10:30:02 -07:00
|
|
|
let optimizer = ContextOptimizer::new().unwrap();
|
2026-08-28 10:31:01 -07:00
|
|
|
let metrics = Arc::new(Mutex::new(OptimizationMetrics::default()));
|
2026-08-28 10:30:02 -07:00
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
let _ = optimize_record_with_metrics(record, &optimizer, &metrics);
|
2026-08-28 10:30:02 -07:00
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
let m = metrics.lock().unwrap();
|
|
|
|
|
assert_eq!(m.total_records, 1);
|
|
|
|
|
assert!(m.input_bytes_total > 0);
|
|
|
|
|
assert!(m.output_bytes_total > 0);
|
2026-08-28 10:30:02 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
#[test]
|
|
|
|
|
fn test_optimize_record_disabled() {
|
|
|
|
|
std::env::set_var("MEM_CONTEXT_OPTIMIZER", "off");
|
|
|
|
|
let record = make_test_record("content");
|
|
|
|
|
let optimizer = ContextOptimizer::from_env().unwrap();
|
|
|
|
|
let metrics = Arc::new(Mutex::new(OptimizationMetrics::default()));
|
2026-08-28 10:30:02 -07:00
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
let result = optimize_record_with_metrics(record, &optimizer, &metrics);
|
|
|
|
|
assert!(result.is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_compression_ratio_calculation() {
|
|
|
|
|
let metrics = OptimizationMetrics {
|
|
|
|
|
total_records: 10,
|
|
|
|
|
input_bytes_total: 1000,
|
|
|
|
|
output_bytes_total: 500,
|
|
|
|
|
per_compressor: Default::default(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let ratio = metrics.compression_ratio();
|
|
|
|
|
assert!((ratio - 50.0).abs() < 0.1, "should be 50%");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_metrics_aggregation() {
|
|
|
|
|
let metrics = Arc::new(Mutex::new(OptimizationMetrics::default()));
|
2026-08-28 10:30:02 -07:00
|
|
|
let optimizer = ContextOptimizer::new().unwrap();
|
|
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
for i in 0..3 {
|
|
|
|
|
let record = make_test_record(&format!("ERROR: {}", i));
|
|
|
|
|
let _ = optimize_record_with_metrics(record, &optimizer, &metrics);
|
2026-08-28 10:30:02 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 10:31:01 -07:00
|
|
|
let m = metrics.lock().unwrap();
|
|
|
|
|
assert_eq!(m.total_records, 3);
|
2026-08-28 10:30:02 -07:00
|
|
|
}
|
|
|
|
|
}
|