diff --git a/Cargo.toml b/Cargo.toml index 3712e95..883ce40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,8 @@ readme = "README.md" repository = "https://github.com/ovnanova/atomalloc" [dependencies] -tokio = { version = "1.41.0", features = ["full"] } crossbeam = "0.8.4" +smol = "2.0.2" [profile.release] lto = true @@ -17,3 +17,7 @@ codegen-units = 1 panic = "abort" opt-level = 3 strip = true + +[dev-dependencies] +macro_rules_attribute = "0.2.0" +smol-macros = "0.1.1" diff --git a/src/block.rs b/src/block.rs index bf5ee26..b560b45 100644 --- a/src/block.rs +++ b/src/block.rs @@ -59,7 +59,7 @@ impl Block { for (i, &byte) in data[chunk_start..chunk_end].iter().enumerate() { self.data[offset + chunk_start + i].store(byte, Ordering::Release); } - tokio::task::yield_now().await; + smol::future::yield_now().await; } self.state.fetch_and(!ZEROED_FLAG, Ordering::Release); @@ -81,7 +81,7 @@ impl Block { for i in chunk_start..chunk_end { result.push(self.data[offset + i].load(Ordering::Acquire)); } - tokio::task::yield_now().await; + smol::future::yield_now().await; } Ok(result) @@ -104,7 +104,7 @@ impl Block { for i in offset..end { self.data[i].store(0, Ordering::Release); } - tokio::task::yield_now().await; + smol::future::yield_now().await; } self.state.fetch_or(ZEROED_FLAG, Ordering::Release); @@ -142,7 +142,7 @@ impl BlockOps for Block { for i in offset..end { block.data[i].store(0, Ordering::Release); } - tokio::task::yield_now().await; + smol::future::yield_now().await; } block.state.fetch_or(ZEROED_FLAG, Ordering::Release); diff --git a/src/lib.rs b/src/lib.rs index 573daa4..9a76f7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ impl AtomAlloc { stats.clone(), )); - tokio::task::yield_now().await; + smol::future::yield_now().await; Self { pool, diff --git a/src/stats.rs b/src/stats.rs index 11e56d3..5d7d1da 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -36,42 +36,42 @@ impl AtomAllocStats { pub async fn record_cache_hit(&self) { self.cache_hits.fetch_add(1, Ordering::Release); - tokio::task::yield_now().await; + smol::future::yield_now().await; } pub async fn record_cache_miss(&self) { self.cache_misses.fetch_add(1, Ordering::Release); - tokio::task::yield_now().await; + smol::future::yield_now().await; } // Stats retrieval pub async fn allocated_bytes(&self) -> usize { let result = self.total_allocated.load(Ordering::Acquire); - tokio::task::yield_now().await; + smol::future::yield_now().await; result } pub async fn freed_bytes(&self) -> usize { let result = self.total_freed.load(Ordering::Acquire); - tokio::task::yield_now().await; + smol::future::yield_now().await; result } pub async fn current_bytes(&self) -> usize { let result = self.current_allocated.load(Ordering::Acquire); - tokio::task::yield_now().await; + smol::future::yield_now().await; result } pub async fn cache_hits(&self) -> usize { let result = self.cache_hits.load(Ordering::Acquire); - tokio::task::yield_now().await; + smol::future::yield_now().await; result } pub async fn cache_misses(&self) -> usize { let result = self.cache_misses.load(Ordering::Acquire); - tokio::task::yield_now().await; + smol::future::yield_now().await; result } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 191c5c1..fb0a7cc 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,154 +1,166 @@ use atomalloc::{block::BlockOps, config::AtomAllocConfig, error::AtomAllocError, AtomAlloc}; +use macro_rules_attribute::apply; +use smol_macros::{test, Executor}; use std::{alloc::Layout, sync::Arc, time::Duration}; -#[tokio::test] -async fn test_basic_alloc_dealloc() { - let config = AtomAllocConfig::get_default_for_tests(); - let allocator = AtomAlloc::with_config(config).await; - let layout = Layout::from_size_align(64, 8).unwrap(); - - println!("\n=== Starting basic allocation test ==="); - - // Get initial stats - let stats_before = allocator.stats().await; - println!("Initial stats: {:?}", stats_before); - - // Single allocation - let block = allocator.allocate(layout).await.unwrap(); - let stats_after_alloc = allocator.stats().await; - println!("Stats after allocation: {:?}", stats_after_alloc); - assert_eq!(block.size(), 64); - - // Deallocate - allocator.deallocate(block).await; - let stats_final = allocator.stats().await; - println!("Final stats: {:?}", stats_final); - - assert_eq!(stats_final.allocated - stats_before.allocated, 64); - assert_eq!(stats_final.freed - stats_before.freed, 64); +#[apply(test!)] +async fn test_basic_alloc_dealloc(ex: &Executor<'_>) { + ex.spawn(async { + let config = AtomAllocConfig::get_default_for_tests(); + let allocator = AtomAlloc::with_config(config).await; + let layout = Layout::from_size_align(64, 8).unwrap(); + + println!("\n=== Starting basic allocation test ==="); + + // Get initial stats + let stats_before = allocator.stats().await; + println!("Initial stats: {:?}", stats_before); + + // Single allocation + let block = allocator.allocate(layout).await.unwrap(); + let stats_after_alloc = allocator.stats().await; + println!("Stats after allocation: {:?}", stats_after_alloc); + assert_eq!(block.size(), 64); + + // Deallocate + allocator.deallocate(block).await; + let stats_final = allocator.stats().await; + println!("Final stats: {:?}", stats_final); + + assert_eq!(stats_final.allocated - stats_before.allocated, 64); + assert_eq!(stats_final.freed - stats_before.freed, 64); + }).await; } -#[tokio::test] -async fn test_concurrent_allocations() { - let allocator = Arc::new(AtomAlloc::new().await); - let mut handles = vec![]; - - for _ in 0..100 { - let allocator = Arc::clone(&allocator); - handles.push(tokio::spawn(async move { - let layout = Layout::from_size_align(64, 8).unwrap(); - let block = allocator.allocate(layout).await.unwrap(); - tokio::task::yield_now().await; - allocator.deallocate(block).await; - })); - } - - for handle in handles { - handle.await.unwrap(); - } +#[apply(test!)] +async fn test_concurrent_allocations(ex: &Executor<'_>) { + ex.spawn(async { + let allocator = Arc::new(AtomAlloc::new().await); + let mut handles = vec![]; + + for _ in 0..100 { + let allocator = Arc::clone(&allocator); + handles.push(smol::spawn(async move { + let layout = Layout::from_size_align(64, 8).unwrap(); + let block = allocator.allocate(layout).await.unwrap(); + smol::future::yield_now().await; + allocator.deallocate(block).await; + })); + } + + for handle in handles { + handle.await; + } + }).await; } -#[tokio::test] -async fn test_pool_reuse() { - let allocator = AtomAlloc::new().await; - let layout = Layout::from_size_align(64, 8).unwrap(); - - // First allocation - let block1 = allocator.allocate(layout).await.unwrap(); - let gen1 = block1.generation(); - allocator.deallocate(block1).await; - - // Second allocation should reuse block - let block2 = allocator.allocate(layout).await.unwrap(); - let gen2 = block2.generation(); - assert_eq!(gen1, gen2, "Block should be reused"); - allocator.deallocate(block2).await; +#[apply(test!)] +async fn test_pool_reuse(ex: &Executor<'_>) { + ex.spawn(async { + let allocator = AtomAlloc::new().await; + let layout = Layout::from_size_align(64, 8).unwrap(); + + // First allocation + let block1 = allocator.allocate(layout).await.unwrap(); + let gen1 = block1.generation(); + allocator.deallocate(block1).await; + + // Second allocation should reuse block + let block2 = allocator.allocate(layout).await.unwrap(); + let gen2 = block2.generation(); + assert_eq!(gen1, gen2, "Block should be reused"); + allocator.deallocate(block2).await; + }).await; } -#[tokio::test] -async fn test_memory_limits() { - // Configure with 2KB total but only 512B max block size - let config = AtomAllocConfig { - max_memory: 2048, // 2KB total - max_block_size: 512, // But max block still 512B - min_block_size: 64, // Min block 64B - alignment: 8, - cache_ttl: Duration::from_secs(60), - max_caches: 100, - initial_pool_size: 512, - zero_on_dealloc: true, - }; - - println!("\n=== Starting memory limits test with size classes ==="); - println!("Config: {:?}", config); - let allocator = AtomAlloc::with_config(config).await; - - // First 512B allocation - let layout1 = Layout::from_size_align(400, 8).unwrap(); - let block1 = allocator.allocate(layout1).await.unwrap(); - let stats1 = allocator.stats().await; - println!("Stats after first allocation (400B->512B): {:?}", stats1); - assert_eq!(stats1.current, 512); - - // Try to allocate more than max block size - let layout2 = Layout::from_size_align(600, 8).unwrap(); - let result = allocator.allocate(layout2).await; - println!( - "Second allocation result (600B): {:?}", - result.as_ref().err() - ); - assert!( - matches!(result.as_ref(), Err(AtomAllocError::OutOfMemory)), - "Expected OutOfMemory for too-large block" - ); - - allocator.deallocate(block1).await; +#[apply(test!)] +async fn test_memory_limits(ex: &Executor<'_>) { + ex.spawn(async { + // Configure with 2KB total but only 512B max block size + let config = AtomAllocConfig { + max_memory: 2048, // 2KB total + max_block_size: 512, // But max block still 512B + min_block_size: 64, // Min block 64B + alignment: 8, + cache_ttl: Duration::from_secs(60), + max_caches: 100, + initial_pool_size: 512, + zero_on_dealloc: true, + }; + + println!("\n=== Starting memory limits test with size classes ==="); + println!("Config: {:?}", config); + let allocator = AtomAlloc::with_config(config).await; + + // First 512B allocation + let layout1 = Layout::from_size_align(400, 8).unwrap(); + let block1 = allocator.allocate(layout1).await.unwrap(); + let stats1 = allocator.stats().await; + println!("Stats after first allocation (400B->512B): {:?}", stats1); + assert_eq!(stats1.current, 512); + + // Try to allocate more than max block size + let layout2 = Layout::from_size_align(600, 8).unwrap(); + let result = allocator.allocate(layout2).await; + println!( + "Second allocation result (600B): {:?}", + result.as_ref().err() + ); + assert!( + matches!(result.as_ref(), Err(AtomAllocError::OutOfMemory)), + "Expected OutOfMemory for too-large block" + ); + + allocator.deallocate(block1).await; + }).await; } -#[tokio::test] -async fn test_strict_memory_limits() { - // Configure for 1KB total, but effectively ~768B usable - let config = AtomAllocConfig { - max_memory: 1024, // 1KB total limit - max_block_size: 512, // Largest block is 512B - min_block_size: 64, // Smallest block is 64B - alignment: 8, - cache_ttl: Duration::from_secs(60), - max_caches: 100, - initial_pool_size: 512, - zero_on_dealloc: true, - }; - - println!("\n=== Starting strict memory limit test ==="); - let allocator = AtomAlloc::with_config(config).await; - - // First 512B allocation should work - let layout1 = Layout::from_size_align(300, 8).unwrap(); - let block1 = allocator.allocate(layout1).await.unwrap(); - let stats1 = allocator.stats().await; - println!("After first allocation (300B->512B): {:?}", stats1); - assert_eq!(stats1.current, 512); - - // Second 512B should fail due to effective memory limit - let layout2 = Layout::from_size_align(400, 8).unwrap(); - let result = allocator.allocate(layout2).await; - println!( - "Second allocation result (400B->512B): {:?}", - result.as_ref().err() - ); - assert!(matches!(result.as_ref(), Err(AtomAllocError::OutOfMemory))); - - // Free first block - allocator.deallocate(block1).await; - let stats2 = allocator.stats().await; - println!("After deallocation: {:?}", stats2); - assert_eq!(stats2.current, 0); - - // Now we can allocate again - let block2 = allocator.allocate(layout2).await.unwrap(); - let stats3 = allocator.stats().await; - println!("After final allocation: {:?}", stats3); - assert_eq!(stats3.current, 512); - - allocator.deallocate(block2).await; +#[apply(test!)] +async fn test_strict_memory_limits(ex: &Executor<'_>) { + ex.spawn(async { + // Configure for 1KB total, but effectively ~768B usable + let config = AtomAllocConfig { + max_memory: 1024, // 1KB total limit + max_block_size: 512, // Largest block is 512B + min_block_size: 64, // Smallest block is 64B + alignment: 8, + cache_ttl: Duration::from_secs(60), + max_caches: 100, + initial_pool_size: 512, + zero_on_dealloc: true, + }; + + println!("\n=== Starting strict memory limit test ==="); + let allocator = AtomAlloc::with_config(config).await; + + // First 512B allocation should work + let layout1 = Layout::from_size_align(300, 8).unwrap(); + let block1 = allocator.allocate(layout1).await.unwrap(); + let stats1 = allocator.stats().await; + println!("After first allocation (300B->512B): {:?}", stats1); + assert_eq!(stats1.current, 512); + + // Second 512B should fail due to effective memory limit + let layout2 = Layout::from_size_align(400, 8).unwrap(); + let result = allocator.allocate(layout2).await; + println!( + "Second allocation result (400B->512B): {:?}", + result.as_ref().err() + ); + assert!(matches!(result.as_ref(), Err(AtomAllocError::OutOfMemory))); + + // Free first block + allocator.deallocate(block1).await; + let stats2 = allocator.stats().await; + println!("After deallocation: {:?}", stats2); + assert_eq!(stats2.current, 0); + + // Now we can allocate again + let block2 = allocator.allocate(layout2).await.unwrap(); + let stats3 = allocator.stats().await; + println!("After final allocation: {:?}", stats3); + assert_eq!(stats3.current, 512); + + allocator.deallocate(block2).await; + }).await; }