#![cfg(target_arch = "wasm32")]
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use wasm_bindgen::prelude::*;
use web_sys::console;
#[derive(Clone)]
pub struct MemoryBudget {
limit: usize,
current: Arc<AtomicUsize>,
}
impl MemoryBudget {
pub fn new() -> Self {
Self::with_limit(1_500_000_000) }
pub fn with_limit(limit: usize) -> Self {
console::log_1(&format!("Memory budget: {} MB", limit / 1_000_000).into());
Self { limit, current: Arc::new(AtomicUsize::new(0)) }
}
pub fn try_allocate(&self, size: usize) -> Result<MemoryAllocation, JsValue> {
let old_total = self.current.fetch_add(size, Ordering::SeqCst);
let new_total = old_total + size;
if new_total > self.limit {
self.current.fetch_sub(size, Ordering::SeqCst);
return Err(JsValue::from_str(&format!(
"Memory budget exceeded: {} MB used, tried to allocate {} MB, limit {} MB",
old_total / 1_000_000,
size / 1_000_000,
self.limit / 1_000_000
)));
}
console::log_1(
&format!(
"Memory: {} MB / {} MB ({:.1}%)",
new_total / 1_000_000,
self.limit / 1_000_000,
(new_total as f64 / self.limit as f64) * 100.0
)
.into(),
);
Ok(MemoryAllocation { size, budget: self.clone() })
}
pub fn current_usage(&self) -> usize {
self.current.load(Ordering::SeqCst)
}
pub fn limit(&self) -> usize {
self.limit
}
pub fn available(&self) -> usize {
self.limit.saturating_sub(self.current_usage())
}
}
impl Default for MemoryBudget {
fn default() -> Self {
Self::new()
}
}
pub struct MemoryAllocation {
size: usize,
budget: MemoryBudget,
}
impl Drop for MemoryAllocation {
fn drop(&mut self) {
self.budget.current.fetch_sub(self.size, Ordering::SeqCst);
}
}
pub struct LateMaterializationExecutor {
budget: MemoryBudget,
}
impl LateMaterializationExecutor {
pub fn new(budget: MemoryBudget) -> Self {
Self { budget }
}
pub fn filter_indices<T, F>(&self, column: &[T], predicate: F) -> Result<Vec<usize>, JsValue>
where
F: Fn(&T) -> bool,
{
let max_indices_size = column.len() * std::mem::size_of::<usize>();
let _allocation = self.budget.try_allocate(max_indices_size)?;
let indices: Vec<usize> = column
.iter()
.enumerate()
.filter_map(|(idx, val)| if predicate(val) { Some(idx) } else { None })
.collect();
console::log_1(
&format!(
"Filter selectivity: {:.2}% ({} / {} rows)",
(indices.len() as f64 / column.len() as f64) * 100.0,
indices.len(),
column.len()
)
.into(),
);
Ok(indices)
}
pub fn memory_stats(&self) -> MemoryStats {
MemoryStats {
used_bytes: self.budget.current_usage(),
limit_bytes: self.budget.limit(),
available_bytes: self.budget.available(),
usage_percent: (self.budget.current_usage() as f64 / self.budget.limit() as f64)
* 100.0,
}
}
}
impl Default for LateMaterializationExecutor {
fn default() -> Self {
Self::new(MemoryBudget::default())
}
}
#[derive(Debug, Clone)]
pub struct MemoryStats {
pub used_bytes: usize,
pub limit_bytes: usize,
pub available_bytes: usize,
pub usage_percent: f64,
}
impl MemoryStats {
pub fn format(&self) -> String {
format!(
"{:.1} MB / {:.1} MB ({:.1}%)",
self.used_bytes as f64 / 1_000_000.0,
self.limit_bytes as f64 / 1_000_000.0,
self.usage_percent
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_budget_new() {
let budget = MemoryBudget::new();
assert_eq!(budget.limit(), 1_500_000_000);
assert_eq!(budget.current_usage(), 0);
assert_eq!(budget.available(), 1_500_000_000);
}
#[test]
fn test_memory_budget_with_limit() {
let budget = MemoryBudget::with_limit(1_000_000);
assert_eq!(budget.limit(), 1_000_000);
}
#[test]
fn test_memory_allocation_success() {
let budget = MemoryBudget::with_limit(1_000_000);
let alloc = budget.try_allocate(500_000);
assert!(alloc.is_ok());
assert_eq!(budget.current_usage(), 500_000);
}
#[test]
fn test_memory_allocation_exceeds_budget() {
let budget = MemoryBudget::with_limit(1_000_000);
let result = budget.try_allocate(1_500_000);
assert!(result.is_err());
assert_eq!(budget.current_usage(), 0); }
#[test]
fn test_memory_allocation_raii() {
let budget = MemoryBudget::with_limit(1_000_000);
{
let _alloc = budget.try_allocate(500_000).unwrap();
assert_eq!(budget.current_usage(), 500_000);
} assert_eq!(budget.current_usage(), 0); }
#[test]
fn test_filter_indices_basic() {
let executor = LateMaterializationExecutor::default();
let data = vec![10, 20, 30, 40, 50];
let indices = executor.filter_indices(&data, |&v| v > 25).unwrap();
assert_eq!(indices, vec![2, 3, 4]);
}
#[test]
fn test_filter_indices_selectivity() {
let executor = LateMaterializationExecutor::default();
let data: Vec<i32> = (0..1000).collect();
let indices = executor.filter_indices(&data, |&v| v > 900).unwrap();
assert_eq!(indices.len(), 99); }
#[test]
fn test_memory_stats() {
let budget = MemoryBudget::with_limit(2_000_000_000);
let _alloc = budget.try_allocate(500_000_000).unwrap();
let executor = LateMaterializationExecutor::new(budget);
let stats = executor.memory_stats();
assert_eq!(stats.used_bytes, 500_000_000);
assert_eq!(stats.limit_bytes, 2_000_000_000);
assert_eq!(stats.available_bytes, 1_500_000_000);
assert!((stats.usage_percent - 25.0).abs() < 0.1);
}
#[test]
fn test_memory_stats_format() {
let stats = MemoryStats {
used_bytes: 750_000_000,
limit_bytes: 1_500_000_000,
available_bytes: 750_000_000,
usage_percent: 50.0,
};
let formatted = stats.format();
assert!(formatted.contains("750"));
assert!(formatted.contains("1500"));
assert!(formatted.contains("50"));
}
}